57

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

 5 years ago
source link: http://dockone.io/article/7735?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

小米弹性调度平台在公司内的项目名称为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平台的整体架构图:

32MfQ37.png!web

关于容器的存储、日志收集、其他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,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK