说明:文章来源uber 博客,笔者日常对DevSecOps相关内容进行研究和学习,今天看到这篇文章,忍不住与各位分享Uber在CD上面的建设经验,文章内容仅供参考,本文只是译稿,如果翻译不妥或者错误,请斧正。
内容原文地址:https://www.uber.com/en-HK/blog/continuous-deployment/
译文如下:
Uber的业务依赖于众多微服务。确保对所有这些服务的更改能够安全、及时地部署至关重要。通过利用持续部署自动化这一过程,我们确保新功能、库更新和安全补丁都能及时交付至生产环境,从而提高了服务业务的整体代码质量。
在本文中,我们将分享如何重新构想Uber的微服务持续部署,以改善部署自动化和微服务管理的用户体验,同时应对处理大规模单一代码库及其不断增加的提交量所带来的一些特殊挑战。
在过去几年里,我们在成熟工具方面投入了大量资源,以适应业务的持续增长,并减少生产事件。随着代码输出的稳步增长,超过50%的生产事件直接由代码更改引起,我们能够在业务扩展过程中实现持续、安全的部署而不妨碍生产力,对于Uber的成功至关重要。
业界普遍认为[1][2],代码的持续部署(CD)本质上降低了引入错误或缺陷的风险。这不仅仅是因为CD本身确保了及时修补漏洞和缺陷,更因为在工程师获得足够信心让机器自动部署代码之前,必须建立起最佳实践、文化和纪律。在启用CD之前,工程师通常会确保采用良好的工程实践,例如:
代码审查
持续集成(单元测试、集成测试和负载测试)
监测(持续监控和警报,自动回滚机制)
什么构成良好的代码审查、足够的单元/集成测试覆盖等,是一个有争议的话题,超出了本文的范围。
Uber拥有广泛的工程和开发工具平台(如Ballast、SLATE),支持工程师采用良好的实践。然而,历史上,存在多种部署流程,公司内部标准或最佳实践有限。随着我们最近将所有微服务迁移到内部云平台Up,我们发现了改善这一状况的机会。
在2022年启动该项目时,我们大约有:
4500个微服务分布在3个单一代码库(Go、Java和Web)
每周5600次提交,许多提交影响超过一个服务
每周7000次生产部署
34%为手动触发(完全没有使用CD)
7%的服务使用CD自动部署到生产环境
CD在Uber并不是一个新学科。历史上,Uber的CD系统作为一个独立且单独的系统运行,采用了选择加入的方式,留给各个团队自行配置。
它具有高度的灵活性,并提供了在基于YAML的DSL中构建完全自定义CD管道的能力。由于这种灵活性,我们不可避免地产生了超过100种独特的管道模板用于部署微服务,除了运行一系列动作外,没有对测试、监控或其他操作进行强制执行。
图1:Uber旧版CD系统中的管道操作
因此,CD管道缺乏标准化阻碍了我们在全公司范围内提高部署安全性和可靠性的能力,这在Uber这样规模的微服务管理中风险巨大,因为每天都有大量更改投入生产。
除此之外,拥有两个独立的部署系统本身就是令人困惑和不理想的。因此,鉴于最近迁移到Up平台以及其成熟性和采用程度,我们决定逐步淘汰现有的CD系统,转而采用新的集成CD体验:Up CD。
我们从零开始构建了一个CD系统,旨在以可重复和安全的方式持续应用更改,我们希望通过自动化部署来防止人为错误,整合现有的测试工具,并确保在更改应用过程中监控回归。
为此,Up CD 提供了:
标准化和自动化的生产部署
以安全性为核心,与Uber的可观察性和测试堆栈紧密集成
与Up平台紧密集成的CD体验,并默认启用
针对Uber工程师需求量身定制的UI/UX,支持基于单一代码库的开发
通过构建具备这些功能的CD系统,我们期望能够提高自动化的采用率,使更多服务能够自动部署到生产环境。此外,至关重要的是,在实现这一目标的同时,我们需要降低(或至少不增加)生产环境中的事件发生率。
为了实现我们的愿景,我们设定了设计最简化部署体验的目标。该系统应安全地推进每个服务的生产环境,以便在Git代码库的主分支上运行包含所有相关更改的构建。
图2:新CD系统的架构
在以下部分,我们将突出我们重新设计的CD系统中的一些重要原则。
正如Uber许多博客文章中所述,我们面临的一个挑战是单一代码库的规模。例如,到2024年时,我们的Go单一代码库每天会有超过1000次提交,并且是近3000个微服务的源,这些微服务都可能受到单次提交的影响。显然,为每次提交构建和部署库中的每个服务将极其低效。更重要的是,这样做也没有太大意义,因为大多数提交只会影响一小部分服务。通过代码库的Bazel图,可以计算出哪些服务的代码二进制文件实际受到了提交的影响。
考虑到这一点,我们确定虽然我们的CD系统必须理解Git代码库的整个历史,以确保提交按正确的顺序部署,但对于单个服务,我们可以并且应该大幅度缩小范围。通过将每个服务限定在实际更改了代码二进制文件的提交子集上,服务所有者也能更轻松地识别他们的服务在每次部署中实际采纳了哪些更改,而不必深入到庞大的单一代码库的Git日志中。
这使我们能够采用相对简单的数据结构,每个服务都会与历史中所有实际与之关联的提交相链接。如下图所示:
图3:服务与提交历史的映射
为了实现这一点,我们决定利用Uber的Kafka消费者代理来消费一个Kafka主题,该主题会在提交推送到Git代码库时发出事件。每当发生这种情况时,会进行一个分析阶段,以将提交组织成适当的结构,并确定受提交影响(更改)的服务集合:
图4:统一的提交流程,从推送到服务处理
显然,对于每个受提交影响的服务,第一步是将其构建成可部署的容器镜像。随后,我们允许工程师自定义一系列与其服务相关的部署阶段。对于一个任意服务,单个提交的流程可能会如下图所示:
图5:统一的提交流程,从构建到部署
根据我们对之前高度可定制CD系统的经验,我们知道流程必须是有明确意见的。因此,我们决定各个阶段本身必须保持相对简单。配置选项主要限于门控条件,用户可以结合各种预定义的选项来表达在部署阶段开始之前必须满足的条件。这些条件可能包括:
提交是否在上一阶段中停留了所需的时间?
是否在用户定义的部署时间窗口内(例如,在团队的工作时间内)?
是否有其他操作正在为该服务运行(例如,手动触发的部署或自动水平扩展)?
是否有任何触发服务警报的情况,这会导致部署回滚?
每个阶段独立于其他阶段运行。对于每个阶段,最新的提交如果成功完成了上一阶段,并满足所有门控条件(如有),就会立即推进到下一个阶段。
为了实现这些机制,我们利用了Cadence,这是一个由Uber开发的开源工作流编排引擎。实现按需启动的构建和部署工作流非常简单。此外,我们将门控机制实现为工作流。每个部署阶段都有自己的门控工作流,该工作流定期运行以检查是否有提交通过了上一阶段。如果是这样,它会考虑门控条件,以确定是否现在应该触发部署。
为了确保我们设计的产品符合需求,我们进行了用户研究和调研,以了解工程师们在考虑现有工具和单一代码库规模时,实际需要什么样的CD系统。基于此,我们围绕服务的提交历史设计了用户体验,工程师可以轻松查看影响其服务的所有提交的完整列表,以及服务的当前状态。
鉴于单一代码库的提交量,即使是自动部署,也不可能(也不应该)将每个影响服务的提交都部署到生产阶段。为了简化对生产流量或其他被认为“有趣”的提交的理解,我们合并了中间提交,以提供更清晰的视图,解决了工程师的一个关键痛点。这意味着,如果发现给定生产部署存在问题,可以很容易地展开到之前的部署,以查看该部署对服务所做的确切更改。这个视图如何由服务级别的提交历史数据结构支撑应该是立刻显而易见的。
图6:提交历史的折叠视图,仅显示对服务当前状态相关的提交
当前世界状态通过提交历史左侧的“泳道”被简洁地呈现。每条泳道显示了在不同服务环境中部署的代码状态和历史。通过悬停在泳道上,可以获取有关该环境部署状态的详细信息。见下图:
图8:特定提交的历史记录,接着是当前状态和计划中的部署操作
为了提高对自动化的信任和增加其采纳率,我们明确意识到必须提供统一且简化的部署体验;仅仅在部署系统上叠加一个独立的CD编排层是不够的。
我们构建的新CD系统不仅要独立运行,还要紧密耦合于Up,并关注其他操作,确保其行动不会让用户感到意外。例如,这意味着如果工程师在CD管道之外手动启动了生产环境的部署,那么CD用户界面将把这个部署纳入服务的提交历史中。此外,系统的内部状态会被更新,反映出提交(及其之前的任何内容)已经部署到目标环境中。
如果工程师构建了一个与服务不完全相关的提交(例如,从主分支的HEAD构建)并将其部署到服务中,那么这个提交也会被添加到内部状态中,从而确保CD视图始终能够正确表示实际情况,而不会误导用户,使他们能够正确理解服务的状态。
这是一个重要的战略决策,因为它允许从手动部署逐步过渡到CD,而不是采取全有或全无的方法。这也意味着每当工程师采取某些手动操作,例如缓解事故时,CD系统能够自动做出正确的响应(通常是避免做任何操作或暂停),根据情况进行处理。
在这一部分,我们重点介绍了Up CD发布后的效果。
在内部发布我们的CD体验后,我们开始看到行为上的显著变化。正如我们所希望的那样,工程师们开始接受它,我们看到了立即的采纳,且这种采纳率不断上升:具体来说,我们看到自动部署的服务数量在12个月内从不到10%增加到近70%。
随着服务部署的频率增加,归因于特定提交的错误也变得更容易,因为每次部署的提交数量减少了。
尽管部署频率提高,我们高兴地发现生产事故的整体发生率并没有按比例增加。实际上,在CD采纳率上升的同一12个月期间,我们看到每1000次代码更改报告的事故数量减少了超过50%。
由于在实施过程中还有其他努力(将在另一篇博客文章中详细介绍),我们不能声称这是因果关系。然而,很明显,我们确实成功实现了让工程师自动将服务部署到生产环境中,而不会增加事故的频率或严重性。
然而,我们也开始看到新的挑战。特别是,我们发现对许多服务共享的单一代码库进行更改的风险(例如,更改一个所有服务共享的公共RPC库)突然增加,因为这些更改会更快地部署到所有受影响的服务中(而且这些部署可能会并行发生)。这意味着,如果这样的更改引入了一个显著的错误,而在CI过程中没有被捕捉到,自动化可能会同时破坏许多服务。
通常,一些服务会有机制来检测问题并自动回滚部署,但不太可能每个服务都能自动检测到问题。
因此,我们引入了跨服务利用提交信号的功能,以便如果某些服务未能成功部署,提交将被视为有问题。
为了获得尽可能明确的信号,我们根据内部服务分级对有风险的跨领域提交进行分阶段部署。最初,Up CD将其部署到我们最不重要的服务组中,当这些服务中的足够百分比成功部署后,才会推进到下一个级别。如果大量服务开始出现问题,部署会被停止,并通知提交作者可能存在的问题。
通过这种部署策略,我们将此类有风险更改对客户的影响降到可接受的水平,并且同样重要的是,通过为特别有风险的更改提供额外的保护措施,增加了对自动化生产部署的信任。
为了量化该项目的结果,我们总结了项目期间一些关键指标的变化,见下表:
Metric | Before Up CD (primo 2022) | Post Up CD (March 2024) |
---|---|---|
# services | 4,500 | 5,000 |
Monorepo commits / week | 5,600 | 11,000 |
Production deployments / week | 7,000 | 50,000 |
% of deployments CD orchestrated (partially or fully to production) | 66% | 95% |
% of services fully automated to production | 7% | 65% |
正如我们在前述部分详细说明的那样,我们重新设计的CD系统——Up CD,体现了我们在部署方法学中的战略转变,即将自动化作为核心原则。我们认为,这一转变对以更高的安全性和效率管理我们的复杂性和规模至关重要,将将代码交付到生产环境的负担从工程师转移到了自动化系统上。