阿里巴巴 9 年双 11 经历下来,交易额增长了 280 倍、交易峰值增长 800 多倍、系统数呈现爆发式增长。系统在支撑双 11 过程中的复杂度和支撑难度以指数级形式上升。双 11 峰值的本质是用有限的成本最大化提升用户体验和集群吞吐能力,用合理的代价解决峰值。始于 2011 年建设的调度&集群管理系统 Sigma 就是为了支撑如此庞大的体系而建立的。
本文作者李雨前(花名鹰缘) ,阿里巴巴技术专家 2015 年开始参入调度系统的建设,参与和推动调度系统的多个版本的演进以及支持双11大促资源的分配和管理。
如果您是资深架构师、Docker/Kubernetes/CloudNative 技术专家,对阿里系统软件事业部感兴趣,欢迎投递简历到 yingyuan.lyq@alibaba-inc.com 。
Sigma 有 Alikenel、SigmaSlave、SigmaMaster 三层大脑联动协作,Alikenel 部署在每一台物理机上,对内核进行增强,在资源分配、时间片分配上进行灵活的按优先级和策略调整,对任务的时延,任务时间片的抢占、不合理抢占的驱逐都能通过上层的规则配置自行决策。SigmaSlave 可以在本机进行容器 CPU 分配、应急场景处理等。通过本机 Slave 对时延敏感任务的干扰快速做出决策和响应,避免因全局决策处理时间长带来的业务损失。SigmaMaster 是一个最强的中心大脑,可以统揽全局,为大量物理机的容器部署进行资源调度分配和算法优化决策。
整个架构是面向终态的设计理念,收到请求后把数据存储到持久化存储层,调度器识别调度需求分配资源位置,Slave识别状态变化推进本地分配部署。系统整体的协调性和最终一致性非常好。我们在 2011 年开始做调度系统,2016 年用 Go 语言重写,2017 年兼容了 kubernetes API,希望结合生态的力量,共同建设和发展。
Sigma 总览
Sigma 的业务有两块,一块是对内,一块是对外,对内是所有的 BU 都接入了Sigma 的系统,我们的规模会有百万级别。Sigma 的内部有一个 logo,很容易理解,就是数学求和,Sigma 是很大的生态系统,需要很多人和系统配合完成。
这个业务分了几个层次,上层业务偏运维,下层业务偏系统,从接入层,到中心的 Master,到Slave,到最底会有一个 PouchContainer(阿里开源的富容器引擎)。
这是
Sigma 的架构
可以看出来,颜色有三块,一块是 Sigma 的,一个是 0 层,还有一块是关于 Fuxi 的,通俗的理解就是 Sigma 管在线,然后是 Fuxi 管离线的,中间是一个协调层,这样理解起来就跟 Mesos 比较接近一点。这个架构肯定不是最优的,但是它存在有它客观的原因。
我会从 Sigma 这边抽象的四个案例。我会给大家展示是什么的同时,讲背后我们的故事。
案例1
首先看一下 APIServer
我们平常写代码经常会接触到这些事情,在业务领域、调度领域稍稍不同,背后要做发布、扩容、销毁、启停、升级,还有云化,特别是双十一到阿里云买服务器,所以会有混合云需求。我们规模很大,就会要求所有简单的事情,怎么样让它在规模化的场景下面依然能够工作。我们调度系统跟运维有关,核心的内容就是怎么样做到运维友好。还有就是我们做在线的容器服务,肯定要做到高可用,还有一致性。
解决思路
1.数据一致性
数据的一致性上面我们是用 etcd/redis,我们会用一个实时+全量的方式做到数据的一致性
2.状态的一致性
想做到很好是很难的,但是我们把状态的一致性转为存储一致性来做,就会降低处理问题的难度。
3.简单的
我们没有追求技术看起来非常完美的方案,先把业务推起来能够用就好了。
4.高可用-无状态
我们要做到前面说的高可用,有几种方案,一个是多 Master 结构,还有无状态,还有就是快速的 failover,我们希望做到无状态。
5.降级-抢占
规模大了以后就会有一个问题,很多人都要资源,这个时候肯定会有一些稀缺的资源,系统要支持降级抢占。
6.内外兼容:一个团队两块牌子
要做到上云,所有的思考都要考虑到对内对外是一班人马,一个团队会有两个牌子,这是我们整个出发点的思路。
之后我们选择由 APIServer 把前面的发布、扩容等都放在 task,丢到 Redis 里,底层的 Worker 消费一些任务,做到无状态,得出来的结论就是架构整体的设计大于语言的选择。
数据一致性里面,如果能做到了实时,数据应该就是一致性的,为什么还要加全量来弥补这个过程?背后一个重要的原因就是物理机的硬件或者是软件会经常出故障,系统多了之后,很难保证故障的事件跟整个链路 100 闭环起来,就导致总有一些地方的数据不知道为什么不一致,有很多硬件、软件的故障导致实践的层面做不到一致性,就只好加一个全量弥补。还有另外一种方案不要全量,我们定时同步,但是有一个问题,时间窗口怎么定,定几分钟。同步的一瞬间,数据会有一些比对,这个时候加锁,成本维护会多一点。
状态一致性问题转为存储一致性的问题最好的就是转为存储一致性,我们为什么说要面向 etcd,在很大的系统里面有很多的设备系统,没有办法面向事务去做,最好是分布式的存储把这些问题兜掉。
为什么说简单够用,后面会有数据给大家看一下,要承载的量很大,没有办法做到 95%以上才上线,大家等不及,可能做到 85%就要上线了。
还有就是降级,前面我提到了阿里这么多的 BU,每一个 BU 都提了很多的预算,去采购,这就浪费很多资源,这中间有一个共享的 buffer,肯定会有抢占,这个在开源里面也会有一些策略。
内外兼容,如果是创业公司会有体会,一部分东西是基于内部的服务器,还有一些是购买上云的服务,这个时候管理起来,不可能上云的是一套管理方式,自己内部又是一套方式,至少在开发理解上是一模一样的,在设计和架构的时候要屏蔽很多的差异。基于这样的思考,我们觉得这种方式相对来说会比较好一点,不然尝试换其他的方式,问题解决可能会稍微麻烦一点。
前面讲了设计的架构,接下来讲的案例是一个真实的场景。
我们在阿里要做一次发布,比如说一开始要拉镜像,就要关闭告警,可能有一些应用要下线了,还要停止应用。然后开始容器升级,还要做业务逻辑的检查,检查完之后才把告警打开。不管是上云还是内部,这套流程肯定是公共的,具体对接的时候又不一样,内部和云上不一样,云上提供一个 SDK,云上的接口的异步性的流转和内部也不一样,所以要抽出来。
内部和阿里云上面都是这样的事情,阿里云比较特别的是什么呢?在 before 会做一些处理,我们核心的架构是很固定的,在阿里主流的语言是 JAVA,我们讨论架构的时候,没有局限于上来就用 JAVA,Golang,或者别的,我们把架构想清楚,之后才选择用什么样的开发语言。
这是我们数据的表现:
这个表现是任务量,数字隐去了,发布的量非常大,平常的量比较小,可以看到它有周期性的规则,因为阿里的体量有发布窗口期,那一天整个的发布量大了,会有一个全局的管控。
案例2
调度里面核心的模块之一是 Scheduler,在很多物理机上面选最佳的物理机,把容器布上去,常规的做法就是要有过滤链,过滤完之后会有一个权重的链,最后拿到我想要的机器上去创建这个容器。因为规模大,一个请求在一个机房里面去扫,这一个机房可能就是万级别的节点,肯定要考虑到性能的优化,所以筛选的规模比较大一点。它是一个链式的,首先要选择哪些服务器作为点,在这一块,我们的想法就是要并发起来,怎么样从工程上面做一个实践。绿色的框是顺序过程,红色的框是并发的阶段,或者是关键资源的锁的过程。每一个并发的粒度看成是一个物理机,相当于每一个物理机去筛选这个是否要,这是一种模式。
另外一种模式是说在整个机器的顶层加一个 glock,是顺序还是并行都不用管,这个锁加在上面,前面的锁是加在下面,两者看起来好像没有太大的区别,测试下来发现性能不一样。有几个因素,一个是下面要做过滤链或者做 weight,看开销;还有每一次筛选的规模,比如一天下来有十万的请求,每个请求背景物理机是好几万的规模还是几百的规模,最后对整体的性能要求是不一样的。
粗粒度并发的性能比较好,我们选择了第二种场景。深入的思考的时候,我们发现第一种场景行不通,原因是有些全局的资源没有在一个协程里面更改,另外一个协程不能立即可见。
案例3
Golang 最近几年非常的火爆,但是在阿里大的氛围里面更强调的是 JAVA 语言,我们引入 Golang 不可能一上来就大规模,需要有一个成功的案例,或者是小规模实践的过程。在这种环境下面,我们想让 Golang 有一席之地,首选的方式就是如何做到快速的打磨,跑得很慢的时候,语言构架系统可能就会被淘汰掉了。这是我们上一个版本,有大概五个链路,有一个架子,每一条链路是做的过程当中,一步一步完善出来。今天我们把链路搬出来跟 K8S 做比较的时候,很多地方都是相通的,但是具体框架的编码实践上面来说是有很大的差异。我们早些时候摸索的这些东西,可能就是业务驱动或者概念驱动,没有真正做到工程或是回馈社区的驱动。未来我们换了一种方式,我们可能是以工程的方式驱动,就是回馈社区。
案例4
我讲一下怎么解读这个图。分几层,上面这一层更强调的怎么样编排任务,中间的这一个层是讲整个容器的;纵向有三个部分,最左边是讲怎么样调度的,中间是一个容器的引擎,再后面是容器的运行时。
在 PouchContainer 里面,官方写了很多的 Features,这些 Features 是源于阿里真实的实践。为什么叫富容器,容器经典的代表大家可能想到 Docker,那 Docker 之前呢?比如阿里的虚拟机比较流行,从虚拟机过渡到容器,这么大的规模,需要适应运维的习惯,要有这样的感知、理念,这个时候容器的技术肯定要做很厚。然后再编排,现在我们了解的有 K8S,大家觉得很完美,但是那在 K8S 之前是什么,阿里的体量并不是 2015 年那一刻才长大的,2015 年之前就很大了。
我们有一个强隔离,为什么说强隔离?我们平时和别人探讨问题的思路有两种思路,第一种思路是一上去就排查,把问题解决掉。还有就看自己的系统,拼命的证明不是我的问题。在这个时候你强隔离就好说了,问题排查或者是黑盒子,特别是规模很大的时候就需要隔离很强,每个人在我的领域范围内很容易定位。从去年到今年,阿里做了很多混部的宣传,混部没有强隔离也有问题的。
还有就是为什么要引入 P2P?最早 P2P 是在流媒体里面,为什么又跟容器关联起来了,就是因为互联网里面有很强的思维,如果你慢的话,别人就会忍无可忍的。量少的话比工作更快一点。但是规模大的时候没有办法快,在链路上面来,包括业界也是一样,连路瓶颈已经在拉镜像,由此阿里很自然的就推了 P2P 加速。
还有内核的兼容,外界有一个说法——CTO 说阿里的商业成功,掩盖了阿里的技术成功,这个话确实有道理。有些业务我们是在 2011 年的时候拿到 2.6 版本,现在业务最新的到了 4.10,那些老的业务每天服务的人群量很小,不能说这个业务不赚钱或者没有前景。把它下掉了也不行,要给一个缓冲期,该升级了,这个没有办法一步到位。这个时候要做规模化的升级,或者技术的换代,内核必须要做兼容。
前面的内容只是把阿里巴巴自己的问题解决了,并没有把这些赋能给社区,所以要做标准化的思考,这个是为了未来把好东西回馈社区,所以必须要做标准化的兼容,再反过头来比这张图,就知道这个架构怎么思考,为什么要兼容 CRI。
代码1
前面我说最难调的 bug 是低级错误造成的,这个低级错误什么意思呢?
Golang 一开始很多人用 map 循环的时候,就受到了指针变量的误用,典型的表现就是,一个对象循环之后就会发现中间的所有的值一模一样,大家首先想到的是业务逻辑是不是不对,根本不会想到 for 循环是不是出了问题。后来我们发现我们对语言本身的理解还不是特别的到位,犯了低级的错误,这个 PPT 下面有详细的代码案例。
代码2
还有 map 对象的异步序列化,现在看来本质上是我们用法不对,当时理解跟业务场景有关,我们每次筛选服务器有上万的规模,为什么选择这条服务器,要用日志把它记下来。后面的优化、迭代要进行消息分析。我们就想到异步做这个事情,把对象存在异步里面去刷盘。但是我们犯了致命的错误,就是 Goroute 对同一个 map 执行了读写并发,这样就出现了 map 读写的冲突。如果知道对象是共享的资源,我们就会加锁,但是这种场景下面我们没有考虑到这个资源也会导致问题。业务场景,特别是异步对象持久化的,要有这样一个意识,规模大的时候要考虑这样一个问题。
代码3
我们做的过程当中发现有一个资源泄露的问题,是场景导致的,我们起了很多 Goroute,我们有个主任务,主任务会起很多子任务,子任务做一些循环的操作。到阿里云买服务器,阿里云是异步接口,现在起很多任务去买服务器,买完之后把请求发货去,它返还我一个异步的 ID,我再请求他状态执行结果。比如说我先请求,完了之后它分配我一些资源,启动这些资源。比如发起申请,然后 start,看看这个 start 是不是完成,start 也是有一个过程。比如阿里 90 分钟再建一个淘宝,要拿到资源,肯定要很快。
我们为什么会遇到泄露呢?我们主任务要申请 200 个实例,我会发起 200 或 50 个并发的请求到阿里云,但是不可能等 200 个请求全部完成了,200 个来了 15 个就已经向业务交付,用户的体验就很好。但是这种滚动式的交付,不可能无休止的等待,你得一分钟之内完成。
除了滚动式的交付,还有总体任务时间的控制,这就涉及到两级的 Goroute,第一级 Goroute 是总任务,子任务也是 Goroute,这就会涉及到泄露的问题。假设主任务超时了,就不管子任务,子任务一直在跑就会把资源耗完。
如果大家对 K8S 里面的代码,关于定时任务的框架有所了解的话,就会有更强的体会。当时我们如果看到这个的话就会借鉴他的方式,就不会采取我们自己造轮子的方式。这个案例最重要的就是以后写 Goroute 的时候要注意是不是有泄露。
代码4
我们为什么要做 Pod 打散?Pod 概念在阿里有几级,第一级是物理层级,第二级是机框打散,往上还有核心交换机的打散。比如我有两台服务器,其中有一台服务器挂掉以后,可用性一下子降到了 50%,其实创业公司还是可以接受的。但是在阿里,如果今天挂了 1%,就有非常多人投诉,所以要求非常高,所以哪怕是 1%的流量受到损失了,客服的量就一下子多很多。硬件故障和软件故障是天然的,我们无法规避怎么办呢?所有的服务不要扎堆,物理机,核心交换机上面也不要扎堆,这就是 Pod 打散。
Pod 打散背后还有一层思考,比如说这台机器已经有了相同的 Pod,另外一台 Pod 一定不会往里面部署,有很强的隔离,要么是 0,要么是 1,这个打散是强制的。但是这里讲的 Pod 打散不是强制的,是尽力交付。默认情况下尽可能地打散,如果打散不了,只能在一起,但是一旦有打散的机会,一定给你打散,我们的打散是在得分之后再做的事情,这个跟 K8S 不一样,K8S 是强硬的,要打散就一定打散,要不然交付不成功。
阿里的服务量很大,服务器各个地方都有,其实它可以有一定的容忍度。假设有一万个实例,可以允许 1%的实例有波动偏差,这个时候 Pod 就可以满足它。之所以能够接受就是因为它的体量太大了,有扛波动的能力,这个跟我们接触到的 K8S 完全不一样,这是规模化的视角看到的东西。
3.总结
设计层面:整体架构层面优先语言选择
性能层面:任务粒度选择
数据驱动:状态一致性转移为存储一致性
语言理解:Map 异步序列化,Map 循环指针
多层并发:可控超时
调度算法:规模下 pod 打散
Pouch Container:拥抱开源,回归社区
【提问环节】
提问1:我想请教一下,你是利用 redis 实现数据一致性,数据一致性代替状态移植,这个什么意思?
李雨前:比如说在 K8S 里面是面向结果交付的,我要三个实例我把数字改了你就可以扩出来,但是在 K8S 没有出来之前,阿里已经存在了这些需求,那个时候是面向过程的,什么意思呢?我要拉镜像,我要关报警,这一系列的动作从容器交付周期来说,就是事务的一部分,这个时候是有状态的。比如说,你想一种方式是每一个状态都带一个 ID,一连串的串回去,不可能是并发的。每一个状态都是并发的往前做,到一个阶段再并发做另外一个阶段的事情,这个我们理解为是有状态的,这些状态我们做的时候,做不出来很好效果。
为什么要放在 redis,我们做之前尝试过一点一点去做,最后我们怎么做呢?每一个状态做完了就丢到 redis 里面去。下一个任务做的时候就知道上一个任务是什么,只要状态满足了就做下一个事情,最后你后面写很多的任务,做的事情就很简单,上一个状态任务是什么就完了,中间做什么事情,也不需要看到全局的交互关系,这个就放到存储,包括维护的一致性。
提问2:其实 Sigma 的思路好像跟 K8S 有点像,我不知道 Sigma 有没有像 K8S 这种封装吗?是怎么暴露服务的?
李雨前:你觉得他们两个很像,那说明你对 K8S 或者 Sigma 有不错的理解。实际上包括 Sigma,包括 mesos,包括 Omega,包括 yarn,这些东西你比较的时候,很多地方有相似之处,从整体上架构来理解,他们确实是相似的,但是我前面提到了Sigma 肯定是在 K8S 出来之前,那个时候就有自己演进的思路和方式,解决的问题都是一样的。第二个问题,我们有没有社区里面大家看到的方式方法,比如说前面讲的 PouchContainer 最后一项就是标准化的兼容,这已经反映了我们跟社区一些好的东西已经要开始吸收,把社区好的理念拿到阿里的场景进行实践,实践好了就回馈给社区,从这社区里面学到的东西有很多很多,我们也努力向社区做一些反馈。
提问3:Sigma 底层可以用 PouchContainer,Docker 这种吗?
李雨前:可以的,前面我说了现在出了一个 CRI,那个已经兼容了,实现了 CRI 的协议,你用起来是一样的,你把 PouchContainer 起来的话,你用 Remoteruntime 配一下就可以了,我推荐你看一下 PouchContainer 官方文档,里面有一些案例。
有疑问加站长微信联系(非本文作者)