作者简介
李 雨 前
花名叫鹰缘
系统软件事业部调度系统。
关键词
Golang
调度系统
Sigma,阿里巴巴自有的内容
实践交流
工程
1.取材
资源调度领域Sigma
主要思路是说资源调度领域的Sigma,共性的借鉴性的东西,阿里特有的就不讲,更多在Q&A里面。因为涉及到实践会聚焦工程的问题,所以我会讲一些架构设计与语言的选择,和并发模式下面任务粒度怎么样去选择,还有一些比较大型、综合的解决方案。
2. 工程问题
============
所有的工程问题,突出背后的故事,有几个线索,第一个线索就是跟规模化相关,阿里的规模很大,背后就要支撑很大的规模。另外就是阿里还有很大一块就是上云,包括双十一,很多东西跟云相关。
另外,我们踩过的坑,针对这些坑的解决方案跟大家分享一下。
最后就是在Golang当中的bug,最怕低级错误引起的。
Sigma的业务有两块,一块是对内,一块是对外,对内是所有的bu都接入了Sigma的系统,我们的规模会有100万级别。Sigma的内部有一个logo,很容易理解,就是数学求和,Sigma是很大的生态系统,需要很多人和系统配合完成。
这个业务分了几个层次,上层业务偏运维,下层业务偏系统,从接入层,到中心的Master,到Slave,到最底会有一个Pouch。
这是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出来之前,那个时候就有自己演进的思路和方式,解决的问题都是一样的。第二个问题,我们有没有社区里面大家看到的方式方法,比如说前面讲的pouch最后一项就是标准化的兼容,这已经反映了我们跟社区一些好的东西已经要开始吸收,把社区好的理念拿到阿里的场景进行实践,实践好了就回馈给社区,从这社区里面学到的东西有很多很多,我们也努力向社区做一些反馈。
提问者3
提问:Sigma底层可以用pouch,docker这种吗?
李雨前:可以的,前面我说了现在出了一个CRI,那个已经兼容了,实现了CRI的协议,你用起来是一样的,你把Pouch起来的话,你用Remoteruntime配一下就可以了,我推荐你看一下pouch官方文档,里面有一些案例。
2018年的 Gopher Meetup 将在深圳开启巡回第一站,这一次邀请了很多新的讲师给大家一起交流分享Go的使用经验〜
点击阅读原文报名参加
本文分享自微信公众号 - GoCN(golangchina)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。