周光明
携程旅行网
高级技术经理
我的演讲主题是《携程持续交付与构建平台实践》。
首先看一下携程目前持续交付的简介。我们现在有 8000 多个应用,整体研发人员大概有 3000 多位,每天在各个环境上部署的次数有 6000 多次,因此持续交付对于我们来说是一个非常重要的能力。
我们先看一下持续交付对于我们的意义,首先是效率的提升,我们知道部署是一个很麻烦的事情,如果说你有多个环境需要部署,那部署的难度也会直线上升。而这个时候如果我们有一个工具去做这样的事情,研发人员就可以将更多的精力投入到研发它的功能上面,让产品的迭代更加迅速。
第二是质量保障,我们在持续交付的过程中穿插了一些代码扫描、单测或者集成测试的过程,可以让整个产品的质量在交付过程中得到很好的保障,也可以让我们在交付的时候更加有信心。
第三是安全可靠,如果没有这个机器就要人工跑上去进行部署,就会对线上系统增加很多误操作的隐患。如果大家知道的话,携程之前也是有过一段非常惨痛的经历的,某个运维人员在线上的操作导致网站大面积的瘫痪,我们对这个印象是非常深刻的。
第四是团队协作,在传统交付模型从产品讨论到上线需要经过很长时间,有可能出现一个现象,在开发阶段的时候开发人员可能在闷头写代码、测试人员没有什么事情做,到了测算阶段这个现象又会反过来。如果我们采用小步快走的方式可以让各个团队之间的协作更加紧密和紧凑。
最后是流程更加透明,因为我们使用的是统一的规范、统一的工具,在交付过程当中的每个细节都可以被暴露出来。不管谁多写一个 bug 或者少写一个单测都会被系统记录下来,作为后面的依据。
这是目前我们简单的交付流程,首先是研发人员 Push 代码,扫描单测集成测试,再将结果反馈,之后创建一个版本,版本是什么概念待会再说。
创建版本之后进行打包,再部署到测试环境,部署成功之后我们会同志周边的自动化测试平台或者性能平台,项目测试人员、QA根据测试结果进行审批工作,就可以将项目部署到下一个测试框架或者生产环境当中。
对于研发阶段来说,我们目前主要推崇的分支管理模型是 Master 分支和 Feature 分支,多个分支同时进行功能的开发,我们是将其可视化的。我们之所以这样做的原因,因为可以让代码的冲突在合并之前就暴露出来并提前解决。
如果这个时候线上有一些紧密的 bug 要修复,也可以通过 Master 提交一个代码,但是提交的代码也会被合并到之前创建的代码当中。
接下来解释一下刚才提到的版本概念,我们知道很多开源软件是使用Git Tags作为开源版本的,但是一个 Git Tags 可以快速找到准确的内容。如果一些项目比较复杂,可能会有一些其它代码的依赖。 因此我们可以将源码打包成一个版本,打包的东西就会比使用Git Tags 更加准确。
但是有些代码依赖,不是一个特定版本号,可能是一个范围,上个月打包出来的结果和今天打包出来的结果就会不一致,如果这样就会被部署增加很多不稳定的因素。
第三点是在一些比较特殊的项目里面,除了语言依赖之外,还会有环境依赖。
以前容器没有出现之前,我们将环境依赖的过程写到项目原码的脚本,通过部署运行多个程序安装那些依赖。 但是在有了容器之后,我们就可以将环境的依赖也作为版本的因素,容器就可以很好的帮我们解决这个问题。因此一个明确版本的概念,对于交付来说也是相当重要的。
下面介绍一下我们这些年的部署模型的演进。
在2015年之前我们使用虚拟机做单机多应用的部署。
对于运维来说成本相当高,在2015年的时候我们重新做了一个发布系统,也是主推单机单应用的部署模型。 到了2016年我们开始研究容器,但是将容器作为一个虚拟机的方式,我们叫它“胖容器”,以这样的方式部署应用。
我们的整个部署过程,上面写的是一个生态环境的部署过程,总体来说还是有一些复杂的。因此我将几个比较关键的概念整理出来。
首先是Group,一组暴露同一服务的集合,对于单机单应用,我们理解是从一个机器,Group也是我们部署的基本单元。
第二是拉入拉出,我们其中的某个成员是否接受流量和请求,流量可能来自SOB或者消息系统推送的消息。
第三是堡垒机,是指生产环境Group中第一台被发布验证的机器,有点像金丝雀部署模型中金丝雀的角色。
第四是点火,是指应用初始化、预热、加载数据等过程,我们认为点火成功才是应用部署成功的一个最终状态。
第五是分批,我们将同一个Group分成多个批次进行滚动部署,减少线上变更对于线上的影响。
第六是降级,刚才也提到降级的事情,我们可以有一个拉入拉出,比如我们的发布需要对应用进行拉入拉出,如果这个时候出现了故障,有应用出现线上的紧密Bug,我们可以通过降级的方式忽略拉入拉出,虽然会丢失一些线上流量,但是可以保证应用被成功的部署到生产上,因此也减少了线上的损失。
第七是刹车,刹车是如果线上的部署失败的机器大于一个比例之前,我们都会停止部署行为,人工排查到底是什么原因,是需要回滚还是修改Bug之后打另外一个版本进行修复。
第八是回滚,回滚的概念就不用多说了,我们需要稳定的符合预期的回滚逻辑。
这是部署过程,首先是拉出堡垒机,部署堡垒机成功之后需要点火,进行测试验证后拉入堡垒机,堡垒机会作为一台正常机器进行工作。
这些都没问题之后我们再将剩余批次进行滚动部署,滚动部署的时候发现部署失败的机器比较多就需要进行刹车判断。
目前我们的 PaaS 平台上支持了测试和生产多个环境的资源管理,这些资源当中既有容器,也有虚拟机,甚至还有物理机的管理,因此我们的后端需要对接 OpenStack、Mesos 等稳定管理平台。
目前我们可以将资源放在私有云的多个数据中心,也可以将资源放在像 AWS 公有云之上,因为携程目前也是希望可以走国际化的趋势。
下面说一下环境管理,对于功能测试我们有一个FAT的环境,FAT又分成多个FAT环境,可以满足用户同时进行多个功能测试的需求。在FAT之上有一个FWS环境,它是一个更加稳定的FAT环境。
对于性能测试我们也是有多套性能测试环境,有点像刚才美团点评的泳道的概念。
FAT环境部署成功之后,这个时候需要 QA 人员的测试验收,才可以将应用发布到下一个UAT环境,UAT是一个相对更加接近生产的测试环境。最后是生产环境。
交付我就简单的介绍到这里,因为我当时收到演讲的主题是跟 Jenkins 相关的,所以我把后面的一些章节都放在 Jenkins 上面。
我们可以看到刚才的流程图上很大一部分工作是通过统一构建平台实现的,接下来我们介绍一下统一构建平台。我相信在座有很多Jenkins用户或者爱好者,我先说Jenkins。
首先 Jenkins 非常的方便,一个外包就可以轻松搞定部署这件事情。Jenkins已经发展了很多年,非常的成熟稳定,插件非常丰富,基本上满足各种各样的需求。当然这些来自社区活跃人员,强大的 Pipeline 可以将配置转化成代码,也是大大增强了我们的生产力。
但是 Jenkins 也不是完美的,它也有一些问题,就是它的单点故障和单机性能的问题。刚才的主题也讲到了这一点,我看台下的很多同学也比较关心这一点,我主要介绍一下我们是怎么看待这两个问题和解决这两个问题的。
首先是单点故障,很多团队都是采用一组一备的 Jenkins 模式,如果出现故障的时候需要切换的方式将故障转移,稍微成熟一点的团队会用Keepalived+Virtual IP。还有可以将Jenkins打包放在Mesos或K8S上面,也可以购买CloudBees服务,也是比较省心一点的。
解决了单点故障的问题,Jenkins Master的上线总归是有限的,随着业务的增长每天的数量越来越成为负担。官方提供了几个维度拆分Jenkins Master的方式,分别是从环境、组织结构、产品线、插件可制定性、人员访问权限控制、出现故障时的影响等几个方面,分别分析了它的利弊。当然每个团队各自的情况不一样,需要根据我们各自的情况作出决策。
拆分了之后,我们要预估一下 Jenkins Master 的单台机器的承载能力,我这边也是提供了一个比较有意思的公式。
根据研发人员的数量预估Job、Master 和 executors 的数量,根据这个公式大概推导有多少个Job和Master,我们每天大概是12000次构建数量,我们现在管理了20000多个Jobs ,这些Jobs 跑在Jenkins Master上。
因为我们团队条件一般,所以只能选择自己做一个平台满足我们大量的运维工作,自己动手丰衣足食。
首先看一下构建系统的整体架构,也是一个比较简单、比较传统的架构模型,我们也是在上层封装了一层API层,负责各种类型的构建请求,在Worker层,将每种构建类型调度到不同的Jenkins Master上。调度到Jenkins Master之后,就是Jenkins Master发挥自己能力的时候了。
接下来我们稍微看一下Worker层处理了哪些事情,有些人可能会疑惑为什么我们有这么多Jobs,我们最早的时候不是按照这样的方式,是按照每种类型一个Job,这样的好处是我们可以维护比较少的Job的情况。但是这样也有一些缺点,比如说我要更新Job配置的时候,影响范围太大,可能几千个应用都是依赖Job,可能影响范围都非常大。
另外大家都共用一个Job,保留也会成问题,因为应用是把之后用户需要保存与环境差异,如果都用一个Job space随时都会被冲掉。其次保留了Worker space之后,我们可以减少代码下载的完成度,可以让每个进程都更快。
当一个Job创建的时候或者每次构建任务进来的时候,我们都会对比当前Job的配置是不是最新的可用的,如果是就将它更新,如果目前线上没有这个Job,我们就会根据这个模版创建一个新的Job,有了这个机制我们就可以将Jenkins Master作为一个没有状态的服务来看待。
接下来我们参考了Labeling模型,可以根据标签匹配找到满足条件的Master。我们也可以把Job与Master标签进行匹配。做了之后下面的工作就会比较容易,我们在系统中同时注册了多个Master,势必有多台Master满足构建条件。
我们在Master上配置了容量配比,比如说新建Job时按照Master容量可以承接多少数量。最后是故障转移,当构建进来的时候我们看之前用过的Master是不是健康的,如果健康我们会优先把它转移到这台上面,如果不健康我们会选择另外条件满足的Master。我们也会做Master的检测,如果某台Master不健康会拉出整个Master集群。
我们在多个维度做了监控和告警,第一个维度是操作系统层面的,也就是一些常规的指标,像CPU之类的。
第二个是应用系统层面的,包括API层的可用性、Worker可用性、Jenkins可用性等等。
第三个是业务逻辑层面的,主要检测的是比如说每一个构建队列是否堵塞,系统容量是否达到瓶颈,因为我们对每台Master都做了容量预估,我们希望当有大面积的构建请求进来的时候,我们可以提早知道进行扩容。
Pipeline关键Step是否超时,我们进行容器调度的时候,是不是Step创建时间比我们预期的要长等,接下来我会细讲如何进行容器调度。
这是构建系统的简单界面,这是首页,包含了目前各种状态的构建数量,还有一些简单的统计。这是构建系统中所有Job的列表情况,包括它之前构建了多少次、它是什么类型的。这是所有构建的一个任务情况,这是现在Jenkins Master线上集群的情况,包括支持的类型情况、监控指标等等。这是容量配比,因为在使用静态的时候,可以做一个小的配比。构建平台的介绍就简单介绍到这里。
接下来是我们如何使用K8S进行Jenkins管理。首先是Jenkins集成的演进,跟我们刚才看到的应用集成演进是类似的,但是时间上面稍微比他们快一些,因为在公司级别技术演进的时候经常会使用Jenkins作为一个试验田,因为它也比较合适。
2016年的时候我们有大量Windows虚拟机,我们就会做构建,维护这些机器的成本只有自己冷暖自知。我们开始调研了Windows 的能力,在我们这边也是有不错的工作现象,但是在业务那边不买单。因为当时比较抵触,所以没有在业务发展下来,我们也有几台机器提供服务。
我主要讲以下两个方面,第一是Slave弹性调度、第二是Workspace的问题,我们看一下为什么存在这两个问题、如何处理好。
这是单日构建数量以及容量数量趋势图。每一种类型根据调用频繁程度和特性,配置出合适的Podidle Minutes参数,控制Slave保留时间。可以看到构建数量趋势明显比容器数量趋势缓和一些,这个时候我们开始下班,构建数量比较延后和缓和一些。我们没有应用实时的创建和销毁,希望不要太过频繁的创建和消毁,既满足我们对弹性调度的要求,也不会让整个系统的性能受到太大的影响。
既然我们实现了弹性调度,对于每个Slave创建的时间我们是特别关心的,因为它不像静态Slave可以有请求进来直接拿来用,还是需要有一段时间给initialDelay Jenkins和Slave进行连接和创建的。但是我们发现采用弹性调度的方式之后,Slave的创建逻辑并不总是符合我们的预期。
举个例子,当我在空闲的时候,Jenkins可以创建Slave,但是隔了几秒钟又有一个新的任务进来了就需要等一段时间。不知道现场有多少个同学发现过这个问题?有吗?有,既然有这样的问题,我们就要去面对它。
我们首先梳理了一遍调度的逻辑,通过改变上面的几个参数,我们是可以达到目的的。接下来介绍一下几个参数的逻辑,有些人可能是对这个逻辑比较清楚的。首先是initialDelay,它是一个连接的时间参数,我们的initialDelay Jenkins Master是没有任何静态Slave的,它可以随时被销毁、随时被创建,所以我们将参数设置成0。
第二个参数是Decay,它是Jenkins负载统计公式中指数移动平均值中的平滑指数,我刚才说的现象是因为Jenkins在内部维护多个负载情况的序列,这些序列的数据有等待队列数量、各种状态的数量等等。它们的值是通过EMA公式计算出来的,比如说history0是前一时刻的值,等待队列数量就是0。
这个时候来了一个数量就是1,假如说decay是0.2,计算出来的值就是0.8。我们可以观察到decay的值越到,当前负载越接近实际值。因为我们不希望Jenkins的保守创建逻辑增加整体的构建时间,因此我们让它负载统计中的值更加接近于当前的实际情况。但是这个值也不能太小,如果太小Jenkins就过于敏感,有一个请求过来就帮你创建,还没创建好下一次还会创建一个,这样浪费很多资源。
第三个公式不Jenkins判断是不是创建Slave的不等式,不等式右边为什么是1-m呢?m是作为一个参数来用的,如果根据EMA的值计算,它是永远不会等于1的,只是会无限接近于1,因此我们需要一个偏移量控制它是不是创建Slave。
m的公式也是上面三个参数有关的,首先看一下最后的totalSnapshot,这是当前可以用的数量,这里这三个参数的来源也是在插件的文档里面有写到,我们使用这三个参数也是可以比较好的工作。
经过长时间的观察,我们发现这样一个调整对于创建参数是比较平稳的,也没有太大波动。我们创建一个Slave大概是20多秒时间,因为采用的调度方式不是立即创建和消毁,所以每天大概有几十个创建时间,相当于每天的构建数量是可以被接受的。
长时间运行之后,我们还会发现有个别的Slave的创建时间会超过5分钟,不知道那位同学有没有这个现象发生?没有是吗?这是为什么呢?一开始我也不知道,所以我们又重新梳理了一下整个创建流程,也是找到了其中的原因。Jenkins的调度逻辑是通过一个轮训逻辑做的,遍历Labels。
如果在系统当中这个Labels是没有出现过的,它要创建一个新的Labels,它是不会更新Labels集合的。但是Jenkins每隔5分钟会更新一次Labels集合,最后我们也是使用了创建中Label时主动调用reset方法。
具体的细节需要在座的再看一下,才能发现是什么原因。目前已经运行了差不多一年时间,后来刚刚看到那些问题再也没有出现过了。
接下来讲一下Workspace保留问题,什么是Workspace保留问题?刚刚已经介绍过了,Workspace对于整个系统来说是非常重要的。首先用户排障需要现场,另外我们通过复用Workspace可以减少下载源码的次数。
但是一旦Workspace消毁之后,之前没有做任何处理,在这个Slave上面所有数据就跟着一起被消毁了。我们首先想到的方式是将上面的数据挂载到Slave上面,消毁也不会有影响,下一次重新被挂载进去。
但是这样只解决了一个问题,Slave不会被删,但是通过Jenkins Master怎么看?我们目前的做法是让Slave与Master在同一个Node且共享同一Workspace,通过Master查看Workspace的能力,看看在Master上面运行的其它Workspace。
但是我们遇到一个问题,一个Job同一时间只能在一个Master上面运行,因为我不可能把一个目录同时给两个Master,这样可能会产生无法预期的结果。
我们解决这个问题的方法是,我们通过上层来做一个调度,将之前Jenkins Master控制并发的这一阶段放在系统上面。因为我们的Job已经拆分得很细了,因此对于单个Job的并发需求来说不算大。
接下来再介绍一下我们是如何通过StatefulSet管理Jenkins Master的,我们想是不是可以通过StatefulSet维护Jenkins Master集群,因为我们希望更加自动化,所以我们解决了以下两个问题。
第一是Job会对之前成功运行的Jenkins Master有亲和性,它会默认跑到之前运行过的Master,因此我们为了尽可能的使用之前的Workspace,需要将Master pod尽可能固定在Node上面。
我们做了一个调度器,我们创建出来的Master pod后面有序号,可以很好的映射到每一台机器上面。
第二是我们需要将上面的目录既挂载在Master、又挂载到Slave上面,因此创建集群的时候需要事先将挂载目录准备好,所以我们也是开发了一个插件CHostpath Volume Driver。除了这些之外,我们还将Java依赖的东西也是放在上面,每次创建Slave的时候也可以挂载到Slave上面,可以提高构建的性能。
有了这些之后,我们再看一下整个集群创建的流程,其实只要维护一个StatefulSet就可以了。首先是StatefulSet,后面创建、更新或者扩容的时候都要创建一个Pod,再创建出Volume,再创建容器,运行Entrypoint。因为刚才提到Jenkins Job的配置,将Jenkins Master的配置放在上面也是支持版本的控制。
下载下来到本地初始化目录,因为初始化之后有些配置需要根据当前机器的情况做修改,比如说IP,最后启动Jenkins,向构建系统注册实例,StatefulSet更新完之后只需要在系统里面拉入就可以对外配比服务了,中间不需要做过多手工干预。
上面大概介绍了持续交付、构建平台、Jenkins on K8S 使用实践,接下来说一下问题与改进。
第一是多环境应用镜像问题,我们现在是一个环境有一个镜像,为什么会这样?因为在早些年没有配置中心的时候,我们的配置是写在代码里的,在发布的时候会根据每个环境将配置重新做修改,再打成包,再打成镜像。
当然这样就会造成比较多的问题,比如环境之间的差异,因为一个应用的发布最好以镜像作为版本,这样就会作为测试环境与其它测试环境不一致,也会降低发布效率。
第二是非标准应用容器化,我相信在一个规模比较大的公司可能交付过程不太一样,构建过程或者打镜像过程都不太一样,我们只能尽可能满足一些大众化的需求,有一些比较特殊的我们是无法给出解决方案的,这些人基本上是手工操作申请虚拟机部署,也没有人管他们。
另外是支持用户自定义Dockerfile,运行构建与制作镜像,减少运维成本。
第三是资源混部,我刚刚说的整个构建系统的调度,Master是非常必要的,我们未来希望可以构建系统的机器在业务比较紧张的时候,可以使用其它的业务计算资源,也可以提升整个数据中心的资源利用率。
说明:本文为周光明老师在 DOIS 2018 · 深圳站分享整理而成。
更多精彩,点击 阅读原文