小米弹性调度平台在公司内的项目名称为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等等)、动态授权、服务发现等等本次分享不做介绍。


容器网络的前世今生



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

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

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

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

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

*
网络组规划好网段;

*
划分专属Ocean的vlan,并做tag;

*
搭建DHCP server,配置规划好的网段;

*
容器内启动DHCP client,获取IP地址。

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

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

*
DHCP server需要做高可用:我们采用了 ospf+vip的方式;

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

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

DHCP网络模式,在Ocean平台运行了很长一段时间。
DHCP网络从性能上、独立IP、物理网络互通等方面都已满足需求。既然DHCP已满足需求,那么我们后来为什么更换了网络模型。
因为DHCP的方式有几个问题:

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

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


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

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

*
Calico的每个节点上会设置大量的iptables规则、路由,对于运维和排查问题难道加大。

*
和现有物理网络互联,每个物理机也需要安装Felix。

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

*
Ocean和网络组协商,规划了一个Ocean专用的大网段;

*
网络组同学为Ocean平台提供了动态路由添加、删除的接口,即提供了路由、三层交换简单OpenAPI能力;

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

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

*
Dockerd用Flanneld拿到的网段信息启动Docker daemon。

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




这样容器的每个IP就分配好了。容器的入网和出网流量,都依赖于宿主机的主机路由,所以没有overlay额外封包解包的相关网络消耗,只有docker0网桥层的转发损耗,再可接受范围内。
以上为小米ocean平台改造后的网络情况。
网络相关的实践,我们简单介绍到这里,下面介绍发布流。


发布流



对于一个服务或者任务(以下统称job)的发布流程,涉及如下几个方面:

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

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

*
调用Marathon做job部署。

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

*
健康检查。

发布流程的统一管理系统(以下统称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种情况:

*

job已经启动,然后绑定ELB:这种情况下,我们做了一个旁路服务, 已轮询的方式从Marathon获取实例信息,和创建的ELB后端信息进行比较,并以Marathon的信息为准,更新ELB的后端。

*
绑定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。


Kubernetes项目实战训练营




Kubernetes项目实战训练将于2018年8月17日在深圳开课,3天时间带你系统掌握Kubernetes。
本次培训包括:Docker介绍、Docker镜像、网络、存储、容器安全;Kubernetes架构、设计理念、常用对象、网络、存储、网络隔离、服务发现与负载均衡;Kubernetes核心组件、Pod、插件、微服务、云原生、Kubernetes
Operator、集群灾备、Helm等,点击下方图片查看详情。


<http://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA==&mid=2649698303&idx=2&sn=952eb71ad3b6c8cb6d92454c05841d70&chksm=8893109cbfe4998afd8295c37a17b2ab9c8059a7b04232bf5ab01ff80b47cbe6c496b8fce44c&scene=21#wechat_redirect>

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信