<- 接前文 ->
第三部分的目标是创建必要的技术实践和架构,从而使开发到运维的工作能够稳定地快速流动,并确保不会造成生产环境的混乱或客户服务的中断。这意味着需要降低在生产环境中部署和发布变更的风险。这一点可以通过一套被称为持续交付的技术实践来实现。
分支在版本控制系统中的主要作用是,让开发人员可以并行地工作在软件系统的各个组成部分,同时避免开发人员提交的代码对主干(trunk,有时候也被称为 master 或 mainline)的稳定性造成影响,或者引入错误。
然而,开发人员在自己的分支上独自工作的时间越长,就越难将变更并入主干。事实上,当分支个数和每个分支上的变更数同时增加时,合并难度会骤增。
集成问题会导致大量的返工,包括不得不通过手动合并解决变更冲突,以及多名开发人员共同解决导致自动化测试或手动测试失败的合并问题。因为在传统的开发模式里,代码集成工作通常发生在项目的末期,所以在集成工作消耗过多时间时,我们不得不为了按时发布而偷工减料。
这会导致另一个恶性循环:既然合并代码如此痛苦,那么大家索性就减少合并次数,而这会使未来的合并工作更加令人痛苦。持续集成旨在通过将合并融入日常工作来解决这个问题。
每当提交到版本控制系统的代码变更导致部署流水线失败时,我们就会群策群力地解决问题,力求尽快将部署流水线恢复到绿色状态。然而,如果开发人员长时间工作在自己的分支(也称为“特性分支”)上,只是偶尔将代码合并到主干,那么他们的每一次合并都会为主干引入大批量的变更,这会造成严重的问题。
尽管分支策略有很多种,但是可以分为以下两类
Ward Cunningham 开发了世界上第一个维基系统,也创造了“技术债务”一词。他在描述技术债务时说,如果不能主动地重构代码库,它就会慢慢地变得难以修改和维护,新特性的增加速度也会因此而下降。持续集成和基于主干的开发实践,其主要目的就是解决这些问题,从而在提高个人生产力的基础之上提高团队生产力
解决大批量合并问题的对策是,应用持续集成和基于主干的开发实践,让每个开发人员每天都至少向主干提交一次代码。这样做能够将代码提交量降低为开发团队每日的工作量。开发人员提交得越频繁,每次的提交量就越小,他们离理想的单件流状态也就越近。
频繁地向主干提交代码,意味着可以针对整个软件系统执行所有的自动化测试,并且在应用或接口的某个部分出现问题时,及时收到告警信息。由于合并问题能被及时发现,因此也能被及时解决。
我们甚至可以对部署流水线进行这样的配置:拒绝接受任何使系统偏离可部署状态的提交(例如代码变更或环境变更)。这种方式被称为门控提交,即部署流水线要确认所提交的变更能成功合并和正常构建,并且在合并到主干之前就已经通过了所有的自动化测试。如果测试失败,则开发人员将收到通知,这样就可以在不影响价值流中的其他人的情况下自己解决问题。
应用这些实践后,我们再来修订“完成”的定义(黑体文字为新增内容):“在每个迭代周期结束时,已经在类生产环境中集成和测试了可工作和可交付的代码;这些代码通过一键式流程在主干上创建,并已通过自动化测试。”
基于主干的开发方式可能是本书中最具争议的实践。许多工程师都认为它行不通,他们更喜欢在自己的分支上工作,不必与其他开发人员协作。然而,Puppet Labs 的《2015 年 DevOps 现状报告》表明,基于主干的开发方式能带来更高的生产力、更好的稳定性,甚至更高的工作满意度和更低的职业倦怠率。
虽然在开始时很难说服开发人员,但是一旦他们认识到显著的优势,就会彻底改变。持续集成实践为下一步实现低风险的自动化部署流程铺平了道路。
本章旨在通过减小生产环境部署的阻力,使运维团队或开发团队能频繁、轻松地进行部署。我们通过扩展部署流水线来实现这一点。
与仅将代码持续集成到类生产环境中不同,我们将能够按需地(即一键式发布)或自动化地(即在构建和测试成功以后,直接进行自动化部署)将已通过自动化测试和验证流程的任何构建版本发布到生产环境中。
当完整记录目前的部署流程以后,下一步的目标便是尽可能地简化和自动化手动步骤,举例如下:
大多数具有持续集成和测试功能的工具,也有扩展部署流水线的能力。通常在生产验收测试执行完之后,这些工具可以将验证过的构建版本发布到生产环境中(这样的工具包括 Jenkins Build Pipeline插件、ThoughtWorks的GoCD和Snap CI、Microsoft Visual Studio Team Services,以及Pivotal Concourse)
部署流水线有如下需求
为了更好地促进工作,需要一个可以由开发人员或运维人员来执行的代码发布流程,并且在理想情况下,应该不需要任何手动操作或工作交接。这个流程的步骤如下。
以上实践有助于成功地执行部署,谁来执行并不重要。
如果代码部署过程是自动化的,就能将其变成部署流水线的一部分。因此,自动化部署必须具备如下能力:
我们的目标是实现快速部署——不用等待数小时之后才知道部署是否成功,也不用在修复代码上耗费数小时。运用 Docker 等容器技术,可以在几秒钟或几分钟内完成很复杂的应用部署。
通过构建上述能力,能够实现一键式代码部署,通过部署流水线将代码和环境变更一起安全、快速地发布到生产环境中
在实践中,人们通常交替使用“部署”和“发布”这两个词。然而,它们其实是不同的动作,并且有着截然不同的目标。
换句话说,如果我们混淆了部署和发布,就很难界定到底由谁来对结果负责。解耦这两个活动,可以提升开发人员和运维人员快速且频繁部署的能力,同时使产品负责人承担成功发布的责任(即确保构建和发布特性所花的时间是有价值的)。
如果部署周期过长,就会限制向市场频繁地发布新特性的能力。然而,假如能够做到按需部署,那么何时向客户发布新特性,就成了业务和市场决策,而不再是技术决策。通常使用的发布模式有以下两种
解耦部署和发布将极大地改变我们的工作方式。我们不再需要为了降低对客户可能造成的负面影响而在三更半夜或周末做部署。相反,我们可以在正常的工作时段里进行部署。运维人员终于能像其他人一样正常下班了。
基于环境的发布模式,这种发布模式不需要更改应用的代码。我们使用多套环境来部署,但实际上只有一套环境处理客户流量。这种方式可以显著地降低生产环境发布的风险,并缩短部署时间。
蓝绿部署模式
蓝绿部署是 3 种模式中最简单的一种。在这种模式下,我们有两个生产环境:蓝环境和绿环境。在任一时刻,只有其中的一个环境处理客户流量。在图 12-5 中,处理客户流量的是绿环境。
在发布新版本的服务时,先把它部署到非在线环境,以便在不影响用户体验的情况下执行测试。在确信一切都正常以后,再把客户流量切换到蓝环境,用这种方式来交付新版本。之后,蓝环境就变成了生产环境,绿环境则变为预生产环境。通过把客户流量再重定向回绿环境,还可以实现回滚。
蓝绿部署模式比较简单,也非常易于在已有的系统中实现。它有很多好处,例如能使团队在正常的工作时段内执行部署工作,并在非高峰时段里轻松地实施版本切换(如变更路由配置或符号链接)。仅这些就能使部署团队的工作境遇得到巨大的改善。
处理数据库变更
当应用的两个版本依赖同一个数据库时,就会出现问题。如果部署操作需要更改数据库模式,或者添加、修改或删除表或列,那么数据库将无法同时支持应用的两个版本。一般通过下面两种方法来解决这个问题。
金丝雀发布模式和集群免疫系统发布模式
蓝绿部署模式实现起来比较简单,而且可以显著地提高软件发布的安全性。它有一些变体,能通过自动化进一步提高安全性和缩短部署时间,但同时可能引入复杂性。
金丝雀发布这个术语来自于煤矿工人把笼养的金丝雀带入矿井的传统。矿工通过金丝雀来了解矿井中一氧化碳的浓度。如果一氧化碳的浓度过高,金丝雀就会中毒,从而使矿工知道应该立刻撤离。
在金丝雀发布模式下,我们会监控软件在每个环境中的运行情况。一旦出现问题,就回滚;否则就在下一个环境中进行部署。
集群免疫系统发布模式扩展了金丝雀发布模式,将生产环境的监控系统和发布流程联系起来,并在面向用户的生产系统的性能超出预定范围时(如新用户的转化率低于 15%~20%),自动回滚代码。这种保护措施有两个明显优势:首先,避免了通过自动化测试难以发现的缺陷,例如使某些关键的页面元素不可见的页面变更(如 CSS 代码变更);其次,减少了排查和解决变更造成的性能下降问题所需的时间。
上一小节介绍了基于环境的发布模式。它的特点是,通过使用多个环境并在其间切换流量,实现部署与发布的解耦。这是完全能在基础设施层面上实现的
本小节将介绍基于应用的发布模式,通过代码来更灵活、更安全地向客户发布新特性(通常是逐一发布特性)。因为基于应用的发布模式是在应用的代码里实现的,所以需要开发团队的参与
实现特性开关
基于应用的发布模式主要是通过特性开关来实现的。特性开关机制使我们能在不进行生产环境代码部署的情况下,选择性地启用和禁用特性。通过特性开关,可以将应用的特性向某些特定用户(例如内部员工和某些客户群)开放。
特性开关的实现机制通常是用条件语句封装应用逻辑或用户界面元素,并根据保存在某处的配置信息启用或禁用某个特性。可以使用简单的应用配置文件(例如 JSON 或 XML 格式的配置文件)存储配置信息,也可以通过服务目录来配置,甚至可以专门设计用于管理特性开关的Web 服务
特性开关还具有如下优势
实现黑启动
特性开关实现的效果是,将特性部署到生产环境中,但暂时使其不可用。它使黑启动技术成为可能——先把所有特性都部署到生产环境中,然后对客户不可见的特性执行测试。对于大规模或高风险的变更来说,黑启动过程往往持续数周,从而保证在正式发布之前使用类生产负载安全地进行测试。
假设我们使用黑启动技术发布了一个有很大潜在风险的新特性,如新的搜索功能、账户创建流程或数据库查询。在将所有代码都部署到生产环境中之后,禁用新特性,然后通过修改用户会话代码调用新函数,不向用户显示调用结果,而仅记录或丢弃测试结果。
通过这种方式,我们再也不用等到大规模的发布以后才能验证客户对产品的满意度。相反,在宣布进行重大发布时,我们已经完成了对业务假设的验证,并且在真实的客户中进行了无数次的改良实验,这些措施有助于提高产品和客户需求的匹配度
持续交付和持续部署的新定义如下
持续交付是持续部署的前提条件,就像持续集成是持续交付的前提条件一样。持续部署更适用于交付线上的 Web 服务,而持续交付适用于几乎任何对质量、交付速度和结果的可预测性有要求的低风险部署和发布场景,包括嵌入式系统、商用现货产品和移动应用。
发布和部署不一定是高风险、状况百出的工作,也不一定需要几十个甚至几百个工程师加班加点地完成。相反,它们可以成为日常工作的一部分。将发布和部署融入日常工作,能够把部署时间从几个月缩短到几分钟,使组织能够快速地向客户交付价值,同时避免意外事故和服务中断。此外,开发人员和运维人员的紧密合作,能使运维工作变得人性化。
本章将介绍可以逆转上述恶性循环的措施,同时回顾一些主要的架构原型,探究有助于提高开发生产力、可测试性、可部署性和安全性的架构特性,以及相关的架构迁移策略,以便从任何现有架构安全地迁移至能更好地实现组织目标的架构
紧耦合架构不仅会降低生产力,还会影响安全变更的能力。接口定义清晰的松耦合架构则与之相反,它优化了模块间的依赖关系,提高了生产力和安全性,让小型且高产的“双比萨”团队可以执行小的变更,并能安全和独立地进行部署。因为每个服务都有一个定义明确的 API,所以更容易测试,团队之间的服务等级协议条款也更容易确定。
Google Cloud Datastor是世界上最大的一个 NoSQL 服务,但其支持团队只有大约 8 个人,这主要是因为它是构建在一层层可靠的基础服务之上的。面向服务的架构能让小型团队各自负责更小、更简单的开发任务,并且每个团队都可以独立、快速和安全地进行部署。
单体架构的本质并不坏。事实上,在产品生命周期的早期阶段,单体架构通常是最佳的选择。
适用于创业公司的单体架构(例如,需要为新特性快速创建原型,或者公司的战略目标可能出现重大改变)完全不同于拥有数百个开发团队的公司所采用的架构,后者的每一个团队都要能够独立地向客户交付价值。通过采用与时俱进的演进式架构,能够确保组织当下的需求得到满足。
如果确信已有的架构过于紧耦合,那么可以在其基础上安全地解耦部分功能。通过这种方式,负责这些功能的开发团队能够独立且安全地进行开发、测试和部署,同时减少了架构的熵。
如前所述,绞杀者应用模式涉及用 API 封装已有功能,并按照新架构实现新的功能,仅在必要时调用旧系统。在绞杀者应用模式下,所有服务都通过版本化 API 访问,也称为版本化服务或不可变服务
版本化 API 能够在不影响调用者的情况下变更服务,这降低了系统的耦合度。如果需要修改参数,就创建一个新的 API 版本,并将依赖该服务的团队迁移至新版本。如果允许新的应用与其他任何服务发生紧耦合(例如直接连接到另一个服务的数据库),那么我们将无法实现重构架构的目标
通过不断地从已有的紧耦合系统中解耦功能,工作被逐渐转移到一个安全且充满活力的生态系统中,这使开发人员的生产力大大提高,同时已有的应用功能逐渐萎缩。当所有业务功能都迁移至新架构之后,旧应用甚至可能完全消失。
绞杀者应用这一术语。这源于他在澳大利亚旅行时由当地藤类绞杀植物得到的启发。他写道:“它们的种子落在无花果树的顶部,然后藤蔓逐渐沿树干向下生长,最后在土壤中生根。多年以后,藤蔓形成奇妙和美丽的形状,但同时绞杀了其宿主树。”
通过创建绞杀者应用,可以避免运用新架构或新技术复制已有功能。现有系统本身的特点使得业务流程变得过于复杂,因此复制现有流程不可取(通过研究用户,往往能够重新设计业务流程,用更简单的流程来实现业务目标)。
与其他任何转型一样,我们要力求速战速决,并在迭代中持续交付价值。前期分析有助于识别出最小的突破口,让新架构有效地帮助我们实现业务目标。
在很大程度上,服务赖以生存的架构决定了代码的测试和部署方式。这一点已经在 Puppet Labs 的《2015 年 DevOps 现状报告》中得到了验证。该报告显示,架构是影响工程师生产力的首要因素,它还决定了是否能快速和安全地实施变更。
因为我们通常受制于追求不同方向的组织目标和长期存在的传统架构,所以必须安全地进行架构演进。本章介绍的案例描述了绞杀者应用模式等技术,这些技术可以帮助我们逐步地推进架构转型,从而跟上组织需求的变化