DockOne微信分享(一八一):小米弹性调度平台Ocean


小米弹性调度平台在公司内的项目名称为Ocean(以下简称Ocean)。

Ocean目前覆盖了公司各种场景的无状态服务,同时对于一些基础服务组件,比如:MySQL、Redis、Memcache、Grafana等等抽象为了PaaS平台Ocean的服务组件。

Ocean平台作为小米公司级PaaS平台,目前正在做的事情和后续的一些规划,这里简单列几个:CI/CD、故障注入、故障自愈、容量测试等等。

目前Ocean平台已支持IDC和多云环境,此次分享只介绍IDC内的实践。

Ocean平台因启动的比较早,当时Kubernetes还没有release版本,所以早起的选型是Marathon + Mesos的架构,此次的分享底层也是Marathon + Mesos架构(目前已在做Marathon + Mesos/Kubernetes双引擎支持,本次分享不涉及Kubernetes底层引擎相关内容)。

先分享一张Ocean平台的整体架构图:
1.jpg

关于容器的存储、日志收集、其他PaaS组件(RDS、Redis等等)、动态授权、服务发现等等本次分享不做介绍。

容器网络的前世今生

做容器或者说弹性调度平台,网络是一个避不开的话题,小米在做弹性调度的时候网络有以下几方面的考虑:
  1. 要有独立、真实的内网IP,便于识别和定位,无缝对接现有的基础设施;
  2. 要与现有的物理机网络打通;
  3. 要能保证最小化的网络性能损耗(这一点基本上使我们放弃了overlay的网络方式);


因小米弹性调度平台启动的很早,而早期容器网络开源方案还不是很成熟,还不具备大规模在生产环境中使用的条件。所以综合考虑,我们选择了DHCP的方案。

DHCP方案的实现:
  1. 网络组规划好网段;
  2. 划分专属Ocean的vlan,并做tag;
  3. 搭建DHCP server,配置规划好的网段;
  4. 容器内启动DHCP client,获取IP地址。
  5. 物理机上配置虚拟网卡,比如eth0.100,注:这个100就是vlan ID,和tag做关联的,用于区分网络流量。


此方案中有几个细节需要注意:
  1. DHCP server需要做高可用:我们采用了 ospf+vip的方式;
  2. 启动的容器需要给重启网卡的能力,以获取IP地址,即启动容器时需要增加NET_ADMIN能力;
  3. 需要配置arp_ignore,关闭arp响应,net.ipv4.conf.docker0.arp_ignore=8。


DHCP网络模式,在Ocean平台运行了很长一段时间。

DHCP网络从性能上、独立IP、物理网络互通等方面都已满足需求。既然DHCP已满足需求,那么我们后来为什么更换了网络模型。

因为DHCP的方式有几个问题:
  1. IP地址不好管理,我们需要再做个旁路对IP地址的使用情况做监控,这就增加了Ocean同学维护成本;
  2. 每次资源扩容需要网络组同学帮我们手动规划和划分网段,也增加了网络同学的管理成本。


针对以上2个痛点,我们重新对网络进行了选型。重新选型时社区用的比较多的是Calico和Flannel。那我们最后为什么选择了Flannel?还是基于:要有独立IP、和现有物理网络互通、最小化网络性能损耗这3点来考虑的。

Calico在这3点都能满足,但是侵入性和复杂度比较大:
  1. Calico的路由数目与容器数目相同,非常容易超过路由器、三层交换、甚至节点的处理能力,从而限制了整个网络的扩张。
  2. Calico的每个节点上会设置大量的iptables规则、路由,对于运维和排查问题难道加大。
  3. 和现有物理网络互联,每个物理机也需要安装Felix。


而Flannel + hostgw方式对于我们现有的网络结构改动最小,成本最低,也满足我们选型需求,同时也能为我们多云环境提供统一的网络方案,因此我们最终选择了Flannel+hostgw方式。

下面简单介绍下Ocean在Flannel+hostgw上的实践。
  1. Ocean和网络组协商,规划了一个Ocean专用的大网段;
  2. 网络组同学为Ocean平台提供了动态路由添加、删除的接口,即提供了路由、三层交换简单OpenAPI能力;
  3. Ocean平台规范每台宿主机的网段(主要是根据宿主机配置,看一台宿主机上启动多少实例,根据这个规划子网掩码位数);
  4. 每台容器宿主机上启动Flanneld,Flanneld从etcd拿宿主机的子网网段信息,并调用网络组提供的动态路由接口添加路由信息(下线宿主机删除路由信息);
  5. Dockerd用Flanneld拿到的网段信息启动Docker daemon。
  6. 容器启动是根据bip自动分配IP。


这样容器的每个IP就分配好了。容器的入网和出网流量,都依赖于宿主机的主机路由,所以没有overlay额外封包解包的相关网络消耗,只有docker0网桥层的转发损耗,再可接受范围内。

以上为小米ocean平台改造后的网络情况。

网络相关的实践,我们简单介绍到这里,下面介绍发布流。

发布流

对于一个服务或者任务(以下统称job)的发布流程,涉及如下几个方面:
  1. 需要创建要发布job的相关信息。
  2. 基于基础镜像制作相关job的部署镜像。
  3. 调用Marathon做job部署。
  4. job启动后对接小米运维平台体系。
  5. 健康检查


发布流程的统一管理系统(以下统称deploy)做发布流整个Pipeline的管理、底层各个组件的调用、维护了各个stage的状态。

下面针对这几点展开详细介绍下:

job的相关信息:job我们可以理解为业务需要部署的项目模板,是Ocean平台发布的最小粒度单元。因其为业务项目模板,所以需要填写的信息都是和业务项目相关的内容,需要填写job名称、选择集群(在哪个机房部署)、给定产品库地址(业务代码的Git或SVN地址)、选择容器模板(启动的容器需要多大的资源,比如1CPU 2G内存 100G磁盘等)、选择基础镜像版本(比如CentOS:7.3,Ubuntu:16.04等)、选择依赖的组件(比如JDK、Resin、Nginx、Golang、PHP等等业务需要根据自己的代码语言和环境需求选择)、填写启动命令(服务如何启动)、监听端口(服务监听的端口是多少,该端口有几个作用:1. 提供服务;2. 健康检查;3. 创建ELB关联;4. 会和job名字一起上报到zk,便于一些还没有服务发现的新项目平滑使用Ocean平台提供的服务发现机制)。

以上是最基本的job信息,还有一些其他的个性化设置,比如环境变量、共享内存、是否关联数据库等等,这里不展开介绍了。

制作job镜像:上面的job信息创建好后,便可以进入真正的发布流程了。发布的时候会根据用户设置的job信息、基于Ocean提供的基础镜像来制作job镜像。这里面主要有2个流程,一个是docker build 制作镜像,一个是业务代码的编译、打包。

Docker build 基于上面填写的job信息解析成的Dockerfile进行。我们为什么不直接提供Dockerfile的支持,而做了一层页面的封装:
  • 对于开发接入成本比较高,需要单独了解Dockerfile文件格式和规范。
  • 开发人员越多,写错Dockerfile的几率越大,对于Ocean同学来说排错的成本就会越高。
  • 此封装可以规范Dockerfile,业务只需要关心和job相关的最基本信息即可,不需要了解Dockerfile具体长什么样子。


Docker build会在镜像里拿业务代码,然后进行业务代码的编译、打包;关于业务编译、打包Ocean内做了一些针对原部署系统(服务部署到物理机)的兼容处理,可以使业务直接或很少改动的进行迁移,大大降低了迁移的成本。

job镜像build成功后,会push到Ocean私有的Registry。

调用marathon做job部署:镜像build成功后,deploy会调用Marathon的接口,做job的部署动作(底层Marathon + Mesos之间调度这里也不展开讲,主要说下我们的Ocean上做的事情)。

job部署分2种情况:
  • 新job的部署:这个比较简单,deploy直接调用Marathon创建新的job即可。
  • job版本更新:更新我们需要考虑一个问题,如何使job在更新过程中暂停,即支持版本滚动更新和业务上的灰度策略。


Marathon原生是不支持滚动更新的,所以我们采用了一个折中的办法。
在做job更新的时候,不做job的更新,是创建一个新的job,除版本号外新job名字和旧job名字相同,然后做旧job缩减操作,新job扩容操作,这个流程在deploy上就比较好控制了。

更新期间第一个新job启动成功后默认暂停,便于业务做灰度和相关的回归测试等。

对接运维平台体系:基础镜像内打包了docker init,容器在启动的时候docker init作为1号进程启动,然后我们在docker init中做了和目前运维平台体系打通的事情,以及容器内一些初始化相关的事情。

包括将job关联到业务的产品线下、启动监控Agent、日志收集、对接数据流平台、注册/删除ELB、启动日志试试返回给deploy等等。

健康检查:我们做健康检查的时候偷了些懒,是基于超时机制做的。
job编译成功后,deploy调用Marathon开始部署job,此时Marathon便开始对job做健康检查,再设置的超时时间(这个超时时间是可配置的,在job信息内配置)内一直做健康检查,直到健康检查成功,便认为job发布成功。发布成功后,整个发布流结束。

弹性ELB

job部署成功后,就是接入流量了。在Ocean平台流量入口被封装为了ELB基础服务。

在ELB模块入口创建ELB:选择集群(即入口机房,需要根据job部署的机房进行选择,为了规范化禁止了elb、job之间的夸机房选择);选择内、外网(该服务是直接对外提供服务,还是对内网提供服务);填写监听端口(job对外暴露的端口);选择调度算法(比如权重轮询、hash等);选择线路(如果是对外提供服务,是选择BGP、还是单线等)。

ELB创建好后,会提供一个ELB的中间域名,然后业务域名就可以cname到这个中间域名,对外提供服务了。

大家可以看到,ELB的创建是直接和job名字关联的,那么job目前的容器实例、之后自动扩缩的容器实例都是怎么关联到ELB下的呢?

这里也分2种情况:
  1. job已经启动,然后绑定ELB:这种情况下,我们做了一个旁路服务,
    已轮询的方式从Marathon获取实例信息,和创建的ELB后端信息进行比较,并以Marathon的信息为准,更新ELB的后端。
  2. 绑定ELB后,job扩缩:上面在发布流中提到,docker init会做ELB的注册、删除动作。


job在扩容的时候会在docker init初始化中将job注册到ELB后端;job在缩容的时候会接收终止信息,在接收终止信号后,docker init做回收处理,然后job实例退出。在回收处理的过程中会操作该实例从ELB摘除。

到此ELB的基本流程就分享完了,下面说下自动扩缩。

自动扩缩

自动扩缩目前包括定时扩缩和基于Falcons的动态扩缩。

定时扩缩

比如一些服务会有明显的固定时间点的高峰和低谷,这个时候定时扩缩就很适合这个场景。

定时扩缩的实践:定时扩缩我们采用了Chronos。

在deploy内封装了Chronos任务下发的接口,实际下发的只是定时回调任务。到任务时间点后触发任务,该任务会回调deploy 发布服务的接口,进行job的扩缩。这里我们为什么没有直接调用Marathon的接口,是因为在deploy中,我们可以自行控制启动的步长、是否添加报警等更多灵活的控制。

基于Falcon动态扩缩

Falcon是小米内部的监控平台(和开源的Open-Falcon差别并不大,但是Ocean平台job内的Falcon Agent 基于容器做过了些改造)。Ocean平台是基于Falcon做的动态调度。用户自行在Ocean上配置根据什么指标进行动态调度,目前支持CPU、内存、thirft cps。这些metric通过Falcon Agent 上报到Falcon平台。用于做单容器本身的监控和集群聚合监控的基础数据。

然后我们基于聚合监控来做动态扩缩。例如,我们在Ocean平台上配置了基于CPU的扩缩,配置后,deploy会调用Falcon的接口添加集群聚合的监控和回调配置,如果实例平均CPU使用率达到阈值,Falcon会回调deploy做扩缩,扩缩实例的过程和定时扩缩是一样的。

遇到的一些特例问题

Ocean从启动开始遇到了很多问题,比如早起的Docker版本有bug会导致docker daemon hang住的问题,使用Device Mapper卷空间管理的问题等等。

下面针对本次的分享,简单列5个我们遇到的问题,然后是怎么解决的。

1、ELB更新为什么没有采用Marathon事件的机制,而是使用了旁路服务做轮询?
  • 我们发现marathon的事件并不是实时上报,所以这个实时性达不到业务的要求;
  • 在我们的环境中也碰到了事件丢失的问题。


所以我们采用了旁路服务轮询的方式。

2、虽然Ocean平台已经做了很多降低迁移成本的工作,但是对于一些新同学或者新业务,总还是会有job部署失败的情况。针对这种情况,我们增加了job的调试模式,可以做到让开发同学在实例里手动启动服务,查看服务是否可以正常启动。

3、ELB的后端数目不符合预期。

主要是由于slave重启导致实例应该飘到其他的机器时,Marathon低版本的bug导致启动的实例数与预期不一致。解决该问题是通过登录到Marathon,通过扩缩实例然后使实例数达到预期,但是这又引进了另外一个问题,ELB的后端存在了残留的IP地址没有被清理,虽然这个因为健康检查而不影响流量,但是暂用了额外的IP资源,所以我们又做了个旁路服务,用于清理这些遗留IP。

4、容器内crontab不生效。业务在容器内使用了crontab,但是在相同的宿主机上,个别容器crontab不生效问题。

我们解决的方式是为启动的容器增加相应的能力,即启动的时候mesos executor增加 AUDIT_CONTROL 选项。

5、容器内看到的Nginx worker进程数没有隔离的问题。

我们在物理机上配置Nginx时通常会将Nginx的worker进程数配置为CPU核心数并且会将每个worker绑定到特定CPU上,这可以有效提升进程的Cache命中率,从而减少内存访问损耗。然后Nginx配置中一般指定worker_processes指令的参数为auto,来自动检测系统的CPU核心数从而启动相应个数的worker进程。在Linux系统上Nginx获取CPU核心数是通过系统调用 sysconf(_SC_NPROCESSORS_ONLN) 来获取的,对于容器来说目前还只是一个轻量级的隔离环境,它并不是一个真正的操作系统,所以容器内也是通过系统调用sysconf(_SC_NPROCESSORS_ONLN)来获取的,这就导致在容器内,使用Nginx如果worker_processes配置为auto,看到的也是宿主机的CPU核心数。

我们解决的方式是:劫持系统调用sysconf,在类Unix系统上可以通过LD_PRELOAD这种机制预先加载个人编写的的动态链接库,在动态链接库中劫持系统调用sysconf并根据cgroup信息动态计算出可用的CPU核心数。

Q&A

Q:请教下你们ELB用的什么代理软件,HAProxy、Nginx?是否遇到过缩容时出现部分请求失败的问题,有解决方案吗?

A:IDC ELB底层封装的是公司的LVS,LVS管理平台提供了完事的API支持,ELB这边调用LVS管理平台的API进行的相关操作。缩容目前没有遇到流量丢失问题,这个是在docker init内接收信号,然后做的回收处理。
Q:hostgw如何访问外网?

A:是通过路由出网的,容器的IP是路由上真实存在的IP网段,由网络组提供的API进行的动态配置。
Q:都劫持了,为啥不用 LXCFS?

A:LXCFS目前仅支持改变容器的CPU视图(/proc/cpuinfo文件内容)并且只有--cpuset-cpus参数可以生效,对于系统调用sysconf(_SC_NPROCESSORS_ONLN)返回的同样还是物理机的CPU核数。另:我们采用的劫持方案已开源,欢迎围观:https://github.com/agile6v/container_cpu_detection
以上内容根据2018年7月17日晚微信群分享内容整理。分享人赵云,小米云平台运维部SRE,负责小米有品产品线运维工作。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册