Ocean 调度框架

前言

MySQL、Redis、Memcache、Grafana等等抽象为了PAAS平台ocean的服务组件。

ocean平台作为小米公司级PAAS平台,目前正在做的事情和后续的一些规划,这里简单列几个:

CI/CD、故障注入、故障自愈、容量测试等等。

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

ocean平台因启动的比较早,当时k8s还没有release版本,所以早起的选型是marathon+mesos的架构,

此次的分享底层也是marathon+mesos架构(目前已在做marathon+mesos/k8s双引擎支持,本次分享不涉及k8s底层引擎相关内容)

下面介绍下今天分享的主题的大纲

1. 容器网络的前世今生

2. 发布流

3. 自动扩缩

4. 弹性ELB

5. 遇到的一些特例问题

先分享一张ocean平台的整体架构图:

Ocean 调度框架

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

 

 

正文

 

1. 容器网络的前世今生

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

a. 要有独立、真实的内网IP,便于识别和定位,无缝对接现有的基础设施;

b. 要与现有的物理机网络打通;

c. 要能保证最小化的网络性能损耗(这一点基本上使我们放弃了overlay的网络方式);

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

dhcp方案的实现:

a. 网络组规划好网段;

b. 划分专属ocean的vlan,并做tag;

c. 搭建dhcp server,配置规划好的网段;

d. 容器内启动dhcp client,获取IP地址。

e. 物理机上配置虚拟网卡,比如eth0.100,注:这个100就是vlan id,和tag做关联的,用于区分网络流量。

此方案中有几个细节需要注意:

a. dhcp server 需要做高可用:我们采用了 ospf+vip的方式;

b. 启动的容器需要给重启网卡的能力,以获取IP地址,即启动容器时需要增加NET_ADMIN能力;

c. 需要配置arp_ignore,关闭arp响应,net.ipv4.conf.docker0.arp_ignore=8

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

dhcp网络从性能上、独立IP、物理网络互通等方面都已满足需求。

既然dhcp已满足需求,那么我们后来为什么更换了网络模型。

因为dhcp的方式有几个问题:

a. IP地址不好管理,我们需要再做个旁路对IP地址的使用情况做监控,这就增加了ocean同学维护成本;

b. 每次资源扩容需要网络组同学帮我们手动规划和划分网段,也增加了网络同学的管理成本。

针对以上2个痛点,我们重新对网络进行了选型。重新选型时社区用的比较多的是calico和flannel。那我们最后为什么选择了flannel?

calico在这3点都能满足,但是侵入性和复杂度比较大:

1. calico的路由数目与容器数目相同,非常容易超过路由器、三层交换、甚至节点的处理能力,从而限制了整个网络的扩张。

2. calico的每个节点上会设置大量的iptables规则、路由,对于运维和排查问题难度加大

3. 和现有物理网络互联,每个物理机也需要安装felix。

3.ocean平台规范每台宿主机的网段(主要是根据宿主机配置,看一台宿主机上启动多少实例,根据这个规划子网掩码位数);

4. 每台容器宿主机上启动flanneld,flanneld从etcd拿宿主机的子网网段信息,并调用网络组提供的动态路由接口添加路由信息(下线宿主机删除路由信息);

5. Dockerd 用flanneld拿到的网段信息启动docker daemon。

6. 容器启动是根据bip自动分配IP。

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

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

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

 

2. 发布流

1. 需要创建要发布job的相关信息。

2. 基于基础镜像制作相关job的部署镜像。

3. 调用marathon做job部署。

4. job启动后对接小米运维平台体系。

5. 健康检查

我们做健康检查的时候偷了些懒,是基于超时机制做的。

job编译成功后,deploy调用marathon开始部署job,此时marathon 便开始对job做健康检查,

再设置的超时时间(这个超时时间是可配置的,在job信息内配置)内一直做健康检查,

直到健康检查成功,便认为job发布成功。

job部署成功后,就是接入流量了。

 

3.弹性ELB

在ocean平台流量入口被封装为了ELB基础服务。

在elb模块入口创建ELB:

可选、填写信息如下:

选择集群(即入口机房,需要根据job部署的机房进行选择,为了规范化禁止了elb、job之间的夸机房选择)

选择内、外网(该服务是直接对外提供服务,还是对内网提供服务)

选择调度算法(比如权重轮询、hash等)

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

 

4. 自动扩缩

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

1. 定时扩缩

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

定时扩缩的实践:

定时扩缩我们采用了chronos。

在deploy内封装了chronos任务下发的接口,实际下发的只是定时回调任务。

到任务时间点后触发任务,该任务会回调deploy 发布服务的接口,进行job的扩缩。

这里我们为什么没有直接调用marathon的接口,是因为在deploy中,我们可以自行控制启动的步长、是否添加报警等更多灵活的控制。

2. 基于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做扩缩,扩缩实例的过程和定时扩缩是一样的。

 

5. 遇到的一些特例问题

ocean项目从启动开始遇到了很多问题,比如早期的docker版本有bug会导致docker daemon hang住的问题,使用devicemapper卷空间管理的问题等等。

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

1. elb更新为什么没有采用marathon事件的机制

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

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

我们解决的方式是:采用了旁路服务轮询的方式。

2. 虽然ocean平台已经做了很多降低迁移成本的工作,但是对于一些新同学或者新业务,总还是会有job部署失败的情况

针对这种情况,我们增加了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核心数。