其他分享
首页 > 其他分享> > 小米弹性调度平台Ocean

小米弹性调度平台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平台的整体架构图:

图片


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


image.png


做容器或者说弹性调度平台,网络是一个避不开的话题,小米在做弹性调度的时候网络有以下几方面的考虑:

  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平台改造后的网络情况。
网络相关的实践,我们简单介绍到这里,下面介绍发布流。


image.png


对于一个服务或者任务(以下统称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的支持,而做了一层页面的封装:


Docker build会在镜像里拿业务代码,然后进行业务代码的编译、打包;关于业务编译、打包Ocean内做了一些针对原部署系统(服务部署到物理机)的兼容处理,可以使业务直接或很少改动的进行迁移,大大降低了迁移的成本。
job镜像build成功后,会push到Ocean私有的Registry。
调用marathon做job部署:镜像build成功后,deploy会调用Marathon的接口,做job的部署动作(底层Marathon + Mesos之间调度这里也不展开讲,主要说下我们的Ocean上做的事情)。
job部署分2种情况:
  1. 新job的部署:这个比较简单,deploy直接调用Marathon创建新的job即可。

  2. 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发布成功。发布成功后,整个发布流结束。


image.png


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的基本流程就分享完了,下面说下自动扩缩。


image.png


自动扩缩目前包括定时扩缩和基于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做扩缩,扩缩实例的过程和定时扩缩是一样的。


image.png


Ocean从启动开始遇到了很多问题,比如早起的Docker版本有bug会导致docker daemon hang住的问题,使用Device Mapper卷空间管理的问题等等。
下面针对本次的分享,简单列5个我们遇到的问题,然后是怎么解决的。
1、ELB更新为什么没有采用Marathon事件的机制,而是使用了旁路服务做轮询?

  1. 我们发现marathon的事件并不是实时上报,所以这个实时性达不到业务的要求;

  2. 在我们的环境中也碰到了事件丢失的问题。


所以我们采用了旁路服务轮询的方式。
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核心数。


标签:容器,ELB,调度,Ocean,扩缩,job,小米,Marathon
来源: https://blog.51cto.com/u_15127630/2776727