共计 9438 个字符,预计需要花费 24 分钟才能阅读完成。
通过近 3 年的建设打磨,美团流水线引擎实现了服务端的基建对立,每日撑持近十万次的流水线执行量,零碎成功率放弃在 99.99% 以上。本文次要介绍在自研引擎建设层面遇到的挑战以及解决方案。
1. 背景
继续交付这个概念最早在 2006 年麻利大会上被提出,通过多年的倒退,目前已成为很多技术团队晋升研发效力的必经之路。通过建设部署流水线,买通从代码开发到性能交付的整个环节,以自动化的形式实现构建、测试、集成、公布等一系列行为,最终实现向用户继续高效地交付价值。
流水线引擎作为撑持部署流水线的底座,它的好坏间接影响着部署流水线建设的程度。业界通常的做法是通过 Jenkins、GitlabCI 等开源工具(或私有云产品)进行搭建,这是一条能帮忙业务疾速落地继续交付的路线,美团晚期也是采纳搭建 Jenkins 的形式来疾速撑持业务。
但随着越来越多业务开始做继续交付的建设,这种“短平快”形式的弊病逐步浮现。比方,工具建设没有对立的规范,各业务都须要去理解整个工具链的细节,建设老本高、程度参差不齐,很少有业务能搭建残缺的部署流水线。同时,业务每天的构建量都在快速增长,逐步超过 Jenkins 等开源工具所能接受的极限,在交付高峰期工作重大排队、服务不可用景象频出,重大影响着业务交付的顺畅度。
美团在流水线引擎的建设层面大略经验了几个阶段。在 2019 年以前,次要围绕 Jenkins 进行优化,2019 年开始正式立项打造自研的流水线引擎,大抵的历程如下:
- 第一阶段(2014-2015):搭建 Jenkins 对立集群,解决业务接入的通用问题(如单点登录、代码仓库集成、音讯告诉、执行机的动静扩缩等),升高业务的建设老本。
- 第二阶段(2016-2018):拆分多个 Jenkins 集群,解决业务增长导致单集群性能瓶颈。最多时有十几个集群,这些集群通常是按业务线维度划分,并由业务自行建设。但随着工夫的推移,集群的拆分治理难度越来越大,Jenkins 安全隐患频出,对平台方造成了很大的运维累赘。
- 第三阶段(2019- 至今):为了彻底解决引擎单机瓶颈和工具反复建设问题,咱们开始自研分布式流水线引擎(美团外部项目名称为 Pipeline),并逐渐收敛各业务依赖的底层基建。
通过 3 年左右的建设打磨,流水线引擎实现了服务端的基建对立,涵盖 到店、到家、公众点评、美团优选、美团平台、主动配送车、根底研发平台 等简直所有的业务,反对 Java、C++、NodeJS、Golang 等多种语言。在性能和稳定性方面,引擎每日撑持 近十万次 的流水线执行量(作业调度峰值每小时达上万次),零碎成功率放弃在 99.99% 以上(排除业务代码本身起因和第三方工具的问题)。
上面咱们次要介绍下咱们在自研引擎建设上遇到的挑战以及对应的解决方案。
2. 问题及思路
2.1 业务介绍
1)什么是流水线
咱们能够把流水线的执行看作是对代码一步步加工,最终交付到线上的过程。依据业务定义的程序关系,顺次执行相应的加工或品质校验行为(如构建、代码扫描、接口测试、部署工具等),整个执行过程相似一个有向无环图。
2)基本概念
- 组件:出于代码复用和业务共享的思考,咱们将某一工具的操作行为封装成一个组件,示意对于一项具体的加工或校验行为。通过组件形式,业务能够便捷地应用已集成的品质工具(如动态代码扫描、安全漏洞剖析等),缩小在同一工具上的反复开发成本;对于不满足需要的场景,业务能够自定义一个新的组件。
- 组件作业:示意组件的一次运行实例。
- 资源:为组件作业调配的一个可执行环境。
- 流水线编排:示意流水线中不同组件执行的先后顺序。
- 引擎:负责调度所有的组件作业,为其调配相应的执行资源,保障流水线执行按预期实现。
2.2 次要挑战
1)调度效率瓶颈
对调度工夫绝对敏感,流水线大部分是短时作业(作业继续数十秒到分钟不等),如果调度工夫过长,业务能显著感知到流水线执行变慢了。咱们须要保障作业调度工夫在一个可控的范畴内,避免出现调度瓶颈。
- 从业务场景思考,调度逻辑存在肯定的业务复杂性(如组件串并行判断、优先级抢占、降级跳过、复用上一次后果等),不仅仅是作业与资源的匹配计算,作业调度耗时存在肯定的业务开销。
- 引擎撑持公司每天近十万次的执行量,峰值量状况下,并发调度的作业量大,常见的开源工具(Jenkins/GitLab CI/Tekton 等)都是采纳单体调度模式,作业是串行调度的,容易呈现调度瓶颈。
2)资源分配问题
对于作业系统来说,作业数通常都是大于资源数的(实在部署状况,资源不是有限的),作业积压是零碎设计时必须思考的问题。如何在无限的资源下,尽可能进步作业的吞吐能力,同时升高在资源有余状况时造成对外围业务场景的影响。
- 如果只依附动静扩容,容易呈现资源有余时无奈扩容、作业排队期待的状况。特地是对于依赖流水线做研发卡控的业务,这会间接阻塞业务的上线流程。
- 出于执行耗时的思考,大部分资源采纳预部署的形式,缩短资源申请和利用启动的筹备工夫。而对于预部署的资源,如何进行无效划分,既保证每类资源都有肯定配额,同时也避免出现局部资源利用率过低,影响作业整体的吞吐能力。
- 不是所有工具的执行资源都由引擎治理(如公布零碎,部署工作的资源管理是独自的),在作业的资源分配上,还须要思考不同的资源管理形式。
3)工具差异化问题
公司内不同业务的差异化大,波及的质效类工具泛滥,如何设计一个适合的插件化架构,满足不同工具的接入需要。
- 不同工具实现模式差异化大,有些工具有独立的平台,能够通过接口方式进行集成,有些仅仅是一段代码片段,还须要提供相应的运行环境。面对不同的接入状态,引擎如何屏蔽不同工具带来的差别,使业务在编排流水线时不必关注到工具的实现细节。
- 随着业务场景的不断丰富,组件执行还会波及人工交互(审批场景)、反对重试、异步解决、故障复原等能力,这些能力的扩大如何尽可能减少对系统的冲击,升高实现的复杂度。
2.3 解决思路
1)拆分调度决策与资源分配,解决调度效率瓶颈
从上述剖析,一个作业的理论调度耗时 = 单个作业的调度耗时 * 待调度的作业数。因为单个作业的调度耗时会受具体的业务逻辑影响,不确定性大,优化空间无限。而串行调度问题绝对明确,在作业调度工夫和数量不可控的状况下,是一个适合的优化方向。
对于串行调度,业界常见的做法是依照业务线维度拆分多个集群,摊派总的调度压力。但这种形式存在的问题是资源分配不具备灵活性,很容易呈现资源的调配不均,在整体资源有余时,无奈从全局上思考高优作业的资源分配。并且,多集群治理(新增集群 / 拆分现有集群)也是不小的运维累赘。
进一步剖析,串行调度次要是为了防止资源竞争问题,取得绝对最优的资源。这对于流水线场景(作业量大于资源量且都是短时作业),资源最优解不是强诉求。并且,资源量的并发度绝对作业量更可控,依据作业执行快慢不同,咱们通过被动拉取作业的形式,管制拉取的数量和频率,从而无效升高了资源竞争的状况。
最终,咱们在设计上采取了调度决策与资源分配拆散的模式:
- 调度决策:负责计算出能够调度的作业,提交决策,期待适合的资源来执行。该模块具体程度扩大,分担调度决策的压力。
- 资源分配:负责保护作业与资源的关系,通过被动拉取作业的形式,资源能够向任意的实例拉取作业,勾销了原先串行分配资源的单点限度。
在这种模式下,作业调度、资源分配都具备程度扩大能力,领有更高的性能和零碎可用性。也利于作业调度的逻辑可能独立演进,便于开发、测试以及灰度上线。
2)引入资源池管理模式,实现资源的灵便调配
思考到不是所有资源都由引擎治理,咱们引入资源池的概念来屏蔽不同资源形式的差别,每个资源池代表一类资源的汇合,不同资源池的资源管理形式能够是多样化的。通过该形式,咱们将资源分配的问题简化为作业与资源池的匹配问题,依据作业的理论状况,正当设置不同的资源池大小,并配合监控伎俩对资源池进行动静调整。
在具体措施上,咱们抉择“标签”的形式建设作业与资源池的匹配关系,通过从作业与资源两个维度来满足上述条件。
- 在作业端,作业基于标签属性拆分到不同的作业队列,并引入优先级概念,保障每个队列中作业按优先级高下被拉取到,防止在积压时,高优作业排在前面无奈被及时处理,阻塞业务研发流程。
在资源端,联合资源的理论场景,提供三种不同的资源池治理形式,以解决不同资源类型的配额和利用率问题。
- 预置的公共资源,这部分资源会提前在资源池上扩容进去,次要应答业务高频应用的且对工夫敏感的组件作业。在资源配额和利用率上,依据资源池的历史状况和实时监控,动静调整不同资源池的大小。
- 按需应用的资源,次要针对公共资源环境不满足的状况,业务须要自定义资源环境,思考到这部分作业的体量不大,间接采纳实时扩容的形式,相比预置资源的形式,能够取得更好的资源利用率。
- 内部平台的资源,这些资源的治理平台方比咱们更有教训,平台方通过管制向引擎拉取作业的频率和数量,自行治理作业的吞吐状况。
3)引入组件的分层设计,满足工具差异化需要
为了放弃工具接入的自由度,引擎提供了作业维度最根本的操作接口(拉取作业、查问作业状态、上报作业后果),不同工具能够依据作业接口模式实现定制化的组件开发。
组件开发次要波及①实现业务逻辑和②确定交付形式两局部工作,而与引擎的零碎交互绝对是规范的。咱们依据组件执行过程进行分层设计,拆分出业务逻辑、零碎交互与执行资源三层。在向引擎屏蔽工具实现细节的同时,能够更好地满足多样化的接入场景。
- 零碎交互层,该层绝对组件开发者是通明的,依据引擎提供的接口制订对立的流程交互规范,以向引擎屏蔽不同组件的实现差别。
- 执行资源层,次要解决工具运行形式的差异化,通过反对多种组件交付模式(如镜像、插件装置、独立服务)满足工具与引擎的不同集成形式。
- 业务逻辑层,针对业务不同的开发场景,采纳多种适配器的抉择,来满足业务不同的开发诉求。
3. 整体架构
- 触发器:作为流水线的触发入口,治理多种触发源及触发规定(Pull Request、Git Push、API 触发、定时触发等)。
- 工作核心:治理流水线构建过程中的运行实例,提供流水线运行、停止、重试、组件作业后果上报等操作。
- 决策者:对所有期待调度的作业进行决策,并将决策后果同步给工作核心,由工作核心进行作业状态的变更。
- Worker:负责向工作核心拉取可执行的作业,并为作业调配具体的执行资源。
- 组件 SDK:作为执行组件业务逻辑的壳,负责真正调起组件,实现组件初始化与状态同步的零碎交互。
4. 外围设计点
4.1 作业调度设计
1)调度过程
上面,咱们以一个简略的流水线调度示例(源码检出 – [并行:代码扫描,构建] – 部署),来介绍调度设计中各模块的合作过程。
大抵逻辑如下:
- 当触发流水线构建后,零碎会在 工作核心 创立该编排所要执行的所有组件作业。并且将作业状态的变动以事件形式告诉决策者进行决策。
- 决策者 接管决策事件,依据决策算法计算出可被调度的作业,向 工作核心 提交作业的状态变更申请。
- 工作核心 接管决策申请,实现作业状态变更(作业状态变更为已决策),同时退出相应的期待队列。
- Worker 通过长轮询形式拉取到和本人匹配的期待队列的作业,开始执行作业,执行实现后将后果上报给 工作核心。
- 工作核心 依据 Worker 上报的作业执行后果变更作业状态,同时向 决策者 发动下一轮决策。
- 以此重复,直至流水线下所有作业都已执行实现或呈现作业失败的状况,对流水线进行最终决策,完结本次执行。
整个过程中,工作核心作为一个分布式存储服务,对立保护流水线和作业的状态信息,以 API 形式与其余模块进行交互。而决策者和 Worker 通过监听作业状态的变动执行相应的逻辑。
2)作业状态流转
上面是一个作业残缺的状态机,咱们通过作业决策、拉取、ACK 以及后果上报一系列事件,最终实现作业从初始状态向完结状态的流转过程。
状态机在接管某种状态转移的事件(Event)后,将以后状态转移至下一个状态(Transition),并执行相应的转移动作(Action)。
在理论场景中,因为调度过程波及链路长、各环节稳定性无奈齐全保障,容易产生因异常情况导致状态不流转的状况。为此,在设计上利用数据库保障状态变更的正确性,同时为非完结状态作业设立相应的弥补机制,确保任一环节异样后作业能够复原正确流转。
咱们重点从 作业决策 和作业拉取 这两个要害过程来看状态流转过程可能呈现的问题,以及在设计上是如何解决的。
作业决策过程:工作核心接管调度作业的决策,将可调度的作业从 unstart 变为 pending 状态,同时将作业退出期待队列,期待被拉取。
未收到决策事件:因为决策者服务本身的问题或网络起因,导致决策事件的申请失败,作业长时间处于未调度状态。
- 解决方案:引入定时监测的机制,对于无过程状态作业且处于未完结状态的流水线进行从新决策,防止决策服务短时间异样导致决策失败。
反复决策:因为网络提早、音讯重试景象可能呈现多个决策者同时决策同一个作业,产生作业转移的并发问题。
- 解决方案:减少 pending 的状态示意作业已被决策到,并通过数据库乐观锁机制进行状态变更,保障仅有一个决策会真正失效。
状态变更过程异样:因为存在异构数据库,状态变更和退出队列可能存在数据不统一,导致作业无奈被失常调度。
- 解决方案:采纳最终一致性的计划,容许调度的短暂提早。采纳先变更数据库,再退出队列的操作程序。利用弥补机制,定时监测队列队首的作业信息,若 pending 状态下的作业有早于队首作业的,进行从新入队操作。
作业拉取过程:工作核心依据 Worker 拉取作业的事件申请,从期待队列中获取待调度作业,将作业的状态从 pending 变更为 scheduled,并返回给 Worker。
作业失落问题:这里存在两种状况,①作业从队列中移除,但在状态将要变更时异样了;②作业从队列中移除,也正确变更了状态。但因为 poll 申请连贯超时,未失常返回给 Worker。
- 解决方案:前者通过作业决策环节中对 pending 状态的作业弥补机制,重新加入队列;后者对于状态已变更的状况,已调度的作业减少 ACK 机制,若超时未确认,状态会流转回 pending 状态,期待被从新拉取。
作业被多个 Worker 拉取:Worker 在接管到作业后,遇到长时间的 GC,导致状态流转回 pending 状态,在 Worker 复原后,可能呈现作业已调配到另一个 Worker 上。
- 解决方案:通过数据库乐观锁机制保障仅有一个 Worker 更新胜利,并记录作业与 Worker 的关系,便于对作业进行停止以及 Worker 故障后的复原操作。
3)决策过程
决策过程是从所有未启动的作业中筛选出能够被调度的作业,通过肯定的程序将其提交给工作核心,期待被资源拉取的过程。整个筛选过程能够分为串并行程序、条件过滤、优先级设置三局部。
- 串并行程序 :绝对于 DAG 中简单的寻路场景,流水线场景比拟明确,是将代码逐渐加工验证,通过开发、测试、集成、上线等一系列阶段的过程。阶段间是严格串行的,阶段内出于执行效率的思考,会存在串并行执行的状况。这里通过模型设计,将 DAG 的调度问题转变成作业的先后秩序问题,引入run order 概念,为每个组件作业设置具体的执行秩序,依据以后已执行作业的秩序,疾速筛选出下一批秩序仅大于以后的作业,若并行执行,仅需将作业的秩序设置成雷同即可。
- 条件过滤:随着业务场景扩大,不是所有的作业都须要调度资源,进行真正的执行。如某类耗时的组件,在代码和组件参数都不变的状况下,能够间接复用上一次的执行后果,或者在零碎层面针对某类工具异样时进行组件跳过的降级操作。针对这类状况,在作业真正提交给工作核心之前,会减少一层条件判断(条件分为全局设置的零碎条件以及用户条件),这些条件以责任链模式进行顺次匹配过滤,依据匹配到的条件独自向工作核心提交决策。
- 优先级设置 :从零碎全局思考,在作业呈现积压时,业务更关怀外围场景下整条流水线是否能尽早执行实现,而不是单个作业的排队状况。所以,在优先级设置上除了基于 工夫戳的绝对偏心策略 外,引入 流水线类型的权重值(如公布流水线 > 自测流水线;人工触发 > 定时执行),保障外围场景流水线相干作业可能尽早被调度到。
4.2 资源池划分设计
1)整体计划
咱们采纳多队列的设计,联合标签建设作业队列与资源池的匹配关系,以保障不同队列资源的无效划分,在呈现队列积压、资源池故障、无可扩资源等状况时,最大限度地升高影响范畴,防止所有作业全局排队期待的景象。
2)模型关系
作业队列与标签的关系:队列与标签采纳 1 对 1 的关系,升高业务了解和运维老本。
- 当队列积压时,能疾速定位到某个标签没资源了。
- 标签资源有余时,也能疾速判断影响的具体队列状况。
标签与资源池的关系:标签和资源池采纳多对多的关系,次要从资源整体利用率和对外围队列的资源可用性保障思考。
- 对于一些作业量较少的队列,独自调配一个资源池会造成大部分工夫资源是闲暇状态,资源利用率低。咱们通过给资源池打多标签的形式,既保证了队列有肯定的资源配额,同时也能解决其余标签的作业,进步资源的利用率。
- 对于外围场景的队列,通常标签资源会调配到多个资源池上,保障资源的肯定冗余,同时也升高单个资源池整体故障带来的影响。
3)标签设计
标签的目标是建设资源(池)与作业(队列)的匹配关系。在设计上,为便于标签治理和前期保护,咱们采纳二维标签的模式,通过组件和流水线两个维度,独特决定一个作业所属标签及对应的资源。
- 第一维度:组件维度,对资源做通用划分。联合组件的业务笼罩状况、作业执行量、对机器和环境的特殊要求(如 SSD、Dev 环境等),对须要独立资源的组件进行打标,划分出不同的公共资源池(每个公共资源池执行一类或多类组件作业),在引擎层面统一分配,保障所有作业都有可失常运行。
- 第二维度:流水线维度,依据业务场景进行划分。联合业务对资源隔离 / 作业积压敏感度的诉求,按需进行划分。有些心愿资源齐全独立的业务,会从所有的公共资源池进行切分;有些仅对局部外围场景下的资源须要保障,依据链路上波及的组件,选择性地从局部公共资源池进行划分,实现业务隔离和资源利用率的均衡。
注:每个维度都会设一个 other 的默认值用来兜底,用于解决无资源划分需要的场景。
4)队列拆分设计
依据作业所属标签不同拆分出多个队列,保障每个队列的独立性,升高作业积压的影响范畴。整个拆分过程能够分为入队和出队两局部:
- 入队过程:通过计算作业在组件和流水线两个维度的属性值,来确定作业对应的标签。联合模型关系中标签与队列(1 对 1)的关系,为每个标签按需创立一个队列,存储该标签作业,不同队列间作业做排他解决,简化出队的实现复杂度。
- 出队过程 :队列拆分后,因为标签和资源池(多对多)的关系,资源池的一次作业拉取申请往往会波及多个队列。出于拉取效率的思考,采纳轮询的形式顺次对单队列进行出队操作,直到达到该次申请的作业数下限或所有可选队列为空时返回后果。该形式能够 防止同时对多个队列加锁 ,并且在前置环节会 对多标签进行随机排序,升高多个申请同时操作一个队列的竞争概率。
4.3 组件分层设计
1)分层架构
- 业务层:引入适配层,满足组件开发中多样化的需要场景,同时防止下层差别净化到上层。
- 零碎交互层:设立对立的流程规范,保障引擎和组件交互过程的一致性,便于对立解决非功能性的系统优化。
- 执行资源层:提供多种资源策略,向下层屏蔽不同资源类型的差别。
2)规范的交互流程设计
在零碎交互层,组件与引擎交互的过程中,有两个环节是确定的,①组件作业的状态机流转,这波及到组件执行的整个生命周期治理,若容许存在不同的状态流转关系,整个治理过程会非常凌乱;②引擎对外提供的接口范畴,从服务间解耦的角度,对外提供的接口次要是组件作业维度的接口操作,不应该耦合任何组件外部的实现细节。
联合作业状态机 + 引擎提供的接口,确定了组件执行根本的零碎交互流程。利用模版模式,形象出 init()
、run()
、queryResult()
、uploadArtifacts()
等 必要办法 供业务实现,整个交互流程则由零碎对立解决,业务无需关怀。
3)扩大根底能力
组件执行除了失常的执行流程外,随着业务场景的丰盛,还会波及组件停止、回调(人工审批场景)等操作,这些操作的引入势必会扭转原先的交互流程。为了不减少额定的交互复杂度,在拉取作业环节,减少作业的事件类型(运行、停止、回调等事件),Worker 依据拉取到的不同事件,执行相应的扩大逻辑。同时,引入新的扩大也不会影响到已有的交互流程。
基于上述扩大,咱们可能更好地将一些通用能力下沉到 Daemon Thread 层。如后果查问流程,通过守护线程的形式,勾销了原先同步期待的查问限度,这对于须要异步化解决的场景(如组件作业逻辑已执行完,仅在期待内部平台接口返回后果)能够提前开释资源,进步资源执行的利用率。并且,当执行资源故障重启后,后果查问线程会主动复原待处理异步作业。这部分能力的反对在业务层是通明的,不扭转整个交互流程。
4)引入适配器
业务虽能够通过必要办法实现自定义组件,但这些办法过于根底,业务在一些特定场景下实现老本较高。如对于组件反对 Shell 的脚本化调用,业务其实仅需提供可执行的 Shell 即可,通用约定的形式,其余必要办法的实现都能够交由零碎实现。
针对业务个性化的解决,采纳适配器模式,通用引入不同 Command(ShellCommand、xxCommand)来默认实现特定场景下的必要办法,升高业务的开发成本。同时,放弃零碎侧流程的一致性,通过 动静注入 Command的形式,避免对业务个性化解决的耦合。
5)成果
目前已反对 Shell 组件、服务组件、容器组件等多种接入形式,平台上已提供 数百个组件 ,组件开发方波及 数十个业务线。组件库笼罩源码域、构建域、测试域、部署域、人工审批域等多个环节,买通了研发过程所波及的各个根底工具。
5. 后续布局
- 借助 Serverless 等云原生技术,摸索更轻量、高效的资源管理计划,提供更精细化的资源策略,从资源的弹性、启动减速、环境隔离三个方面为业务提供更优的资源托管能力。
- 面向组件开发者,提供从开发、上线到经营的一站式开发治理平台,升高组件开发、经营老本,使更多工具方、集体开发者能参加其中,独特打造丰盛多样的业务场景,造成良性的组件经营生态。
6. 本文作者
耿杰、春晖、志远等,来自研发品质与效率部研发平台团队。
招聘信息
美团研发品质及效率部,负责公司研发效力畛域平台和工具的建设(包含研发需要管理工具、CI/CD 流水线、分布式代码仓库、多语言构建工具、公布平台、测试环境治理平台、全链路压测平台等),致力于一直推动优良的研发理念和工程实际,建设一流的工程基础设施。咱们长期招聘高级、资深技术专家,Base 北京、上海。感兴趣的同学能够将简历发送至 gengjie02@meituan.com(邮件主题:美团研发品质及效率部)。
浏览美团技术团队更多技术文章合集
前端 | 算法 | 后端 | 数据 | 平安 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2021 年货】、【2020 年货】、【2019 年货】、【2018 年货】、【2017 年货】等关键词,可查看美团技术团队历年技术文章合集。
| 本文系美团技术团队出品,著作权归属美团。欢送出于分享和交换等非商业目标转载或应用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者应用。任何商用行为,请发送邮件至 tech@meituan.com 申请受权。