如何让团队接受持续部署?

如何让团队接受持续部署?

sjmyuan 75 2024-03-10

TL;DR

持续部署是成为高效能团队的目标,而不是方法。部署失败的风险是持续部署的主要障碍。我们可以用信心水平和损失来衡量风险,只有当风险处于行动区时,团队才有可能实施持续部署。我们可以通过提升信心水平和降低损失将风险控制在行动区。

为什么团队抗拒持续部署?

我们团队在持续部署方面一直进展缓慢。虽然我们有持续集成,但所有环境的部署都有相当比例的手动操作。而且在部署生产环境之前,软件必须通过测试部门的手动测试。当我们提出持续部署时,整个团队的第一反应是不安,然后是对其可行性的质疑。

当时,我对团队的反应很不解。这么好的实践,为什么大家都不积极响应呢?不做持续部署,我们怎么成为高效能团队?为什么非要几十张故事卡一起部署呢?为什么我们要忍受200天的更改前置时间呢?

后来,我发现自己搞错了目标和方法。如果团队的每次部署都存在很多问题,甚至造成经济损失,那么即便能够建立持续部署的流水线,团队也不敢让它运行,毕竟谁都承受不了每提交一次代码就损失100万的代价。所以,持续部署不应该是团队提高效能的方法,而应该是团队提高效能的目标。要实现持续部署,第一步就是要降低部署失败的风险,以消除团队的不安。

风险通常由三个元素构成

Risk = (A,C,P)
  • A 表示事件
  • C 表示事件A发生时所造成的结果
  • P 表示事件A发生并造成结果C的不确定度

在本文中,A表示部署失败,那么我们可以用PC来衡量部署失败的风险,其中P是团队对部署成功的信心水平(注意,部署成功的信心水平 = 1 - 部署失败的信心水平),C是部署失败所造成的损失。为了更好的呈现它们之间的关系,我们可以参照福格行为模型,做如下图示


从图中可以看出

  1. 只有在行动线之上时,团队才有可能采取行动。注意,这里行动是否会发生还依赖于其他客观条件,例如收益的大小,法律/公司规定等。
  2. 损失越大,团队越过行动线所需要的信心水平就越高
  3. 损失越小,团队越过行动线所需要的信心水平就越低

我们可以通过提高信心水平和降低损失来降低部署失败的风险,从而使团队进入行动区。这时,团队已经准备好实施持续部署,我们只需要作出必要的提示和指导。但是,我们无法判断团队是否已经进入了行动区,只能通过不断收集团队对持续部署的看法来猜测团队所在的位置。

如何量化信心水平?

什么是部署成功?

在量化信心水平之前,我们需要先对部署成功进行定义。持续部署是将代码变更自动发布到生产环境中。按照当前团队的开发流程,在不考虑自动化的情况下,持续部署对部署成功的定义为

  1. 测试环境按照文档能够成功部署,且没有临时修正。
  2. 测试环境没有Bug。
  3. 生产环境按照文档能够成功部署,且没有临时修正。
  4. 生产环境没有Bug。

注意,这里的Bug是指不符合客户需求,而不是指不符合软件需求,因为软件需求可能有遗漏或错误。而且我们要求各个环境的部署和测试环境的测试都没有问题。因为按照持续部署的定义,这些都是自动化的一部分,它们的失败会影响团队对自动化的信心水平。

拉普拉斯接续法则

我们可以使用拉普拉斯接续法则(Laplace’s Rule of Succession) 来量化团队对部署成功的信心水平。它曾被拉普拉斯用来预测明天太阳升起的概率

X_1, \dots, X_{n}是已知的n次部署的结果,其值为0时表示部署失败,为1时表示部署成功。当X_1,\dots,X_n中有s次部署成功时,那么第n+1次部署X_{n+1}成功的概率为:

P(X_{n+1} = 1|X_1 +\cdots+X_n=s)=\frac{s+1}{n+2}

这里的概率为主观概率,即我们所说的信心水平。

注意,我们没有使用如下公式

P(X_{n+1} = 1|X_1 +\cdots+X_n=s)=\frac{s}{n}

是因为

  1. 上述公式无法处理没有任何部署的情况, \frac{0}{0} 没有意义。
  2. 上述公式无法处理已知部署全是成功情况,\frac{10}{10}不代表团队相信下一次部署一定会成功。
  3. 上述公式无法处理已知部署全是失败情况,\frac{0}{10}不代表团队相信下一次部署一定会失败。
  4. 在部署较多时,两个公式的结果是趋于一致的。但在部署数量较少时,拉普拉斯接续法则更加合理。

部署对信心水平的影响

第一次部署的影响最为明显

在没有进行过任何部署时,团队对第一次部署的信心水平为

P(X_1 = 1) = \frac{0+1}{0+2} = 50\%

这意味着团队对部署结果没有明显的偏好。

当第一次部署成功时,团队对第二次部署的信心水平变为

P(X_2 = 1|X_1=1) = \frac{1+1}{1+2} \approx 66.7\%

当第一次部署失败时,团队对第二次部署的信心水平变为

P(X_2 = 1|X_1=0) = \frac{0+1}{1+2} \approx 33.3\%

可以看到通过第一次部署,团队已经对后续部署的结果有了明显的倾向性。

连续的部署成功可以快速增加信心水平

如果团队能够连续部署成功3次的话,团队对第4次部署的信心水平为

P(X_4 = 1|X_1+X_2+X_3=3) = \frac{3+1}{3+2} = 80\%

而要达到90\%以上的信心水平,团队至少要连续部署成功8次

P(X_9 = 1|X_1+X_2+X_3+X_4+X_5+X_6+X_7+X_8=8) = \frac{8+1}{8+2} = 90\%

部署失败后需要更多的部署成功来挽回信心水平

假设团队在第4次部署时失败的话,需要连续部署成功4次,信心水平才能重回80\%

P(X_5 =1 |X_1+X_2+X_3+X_4=3) = \frac{3+1}{4+2} \approx 66.7\%
P(X_6=1|X_1+X_2+X_3+X_4+X_5=4) = \frac{4+1}{5+2} \approx 71.4\%
P(X_7=1|X_1+X_2+X_3+X_4+X_5+X_6=5) = \frac{5+1}{6+2} = 75\%
P(X_8=1|X_1+X_2+X_3+X_4+X_5+X_6+X_7=6) = \frac{6+1}{7+2} \approx 77.8\%
P(X_9=1|X_1+X_2+X_3+X_4+X_5+X_6+X_7+X_8=7) = \frac{7+1}{8+2} = 80\%

如何提高信心水平?

从上节的分析可以看出,我们要提高信心水平,就必须保证部署的成功,也就是部署的软件是符合客户的需求,这也是《质量免费》中对质量的定义。换句话说,提高信心水平就是提高部署的质量。

提高部署的自动化水平

相对于软件开发流程中的其他部分,部署的复杂度,工作量和发生频率都较小,很容易被团队所忽略,经常需要一些手动操作来完成。这些手动操作是最容易出错的,也是最容易被识别的,团队应该优先使用自动化脚本将其减少到最小程度,从而减少部署所引起的错误,例如我们可以做到只需一个点击就能完成各个环境的部署。

提高自动化测试质量

我们在部署成功中要求测试环境和生产环境都没有Bug,能够保证这一点的其中一环就是自动化测试。我们把自动化测试的质量定义为,它可以快速的验证软件是否能够按照软件需求运行。

按照上面的定义,自动化测试的类型和测试用例应该是从软件需求中来,而不是从软件代码中来。从这个角度看,测试覆盖率和测试数量都不应该作为自动化测试质量的衡量指标,正确的衡量指标应该是自动化测试通过后的Bug数量。 这就要求我们更加关注如何根据软件需求来设计自动化测试的类型和测试用例,也就是自动化测试策略。有了自动化测试策略之后,我们才可能对团队提出具体要求,从而提高自动化测试的质量。

下图是《Agile Testing》中的测试四象限,它列出了不同的测试类型及其适用场景。

agile-testing-quadrants

以最常见的单元测试,组件测试和功能测试为例,自动化测试策略可以要求团队成员以测试驱动开发的形式来实现功能,并结合当前架构给出必要的测试用例示例和实现步骤示例。当Bug发生后,我们可以根据测试策略进行复盘,要么要求团队遵守测试策略,要么更新测试策略,以确保自动化测试可以检测到该Bug。反之,自动化测试策略的缺失会导致团队对自动化测试没有统一的要求,自动化测试的质量也就无法提高。

自动化测试的速度要快,它既体现在运行速度上,也体现在问题定位速度上。对于支持团队开发的测试类型,其运行时间应该控制在5分钟以内,这样团队成员的心流状态才不容易被打断,运行测试的次数也会增加。另外,测试的错误信息要准确,这样可以快速定位到有问题的代码,例如我们可以要求一个测试是只有一个断言,测试名称要符合given-when-then的格式等。

提高代码质量

我们对代码质量的定义为代码能够按照软件需求运行。团队成员是软件需求和代码之间的桥梁,要提高代码质量,团队成员需要能够正确理解软件需求和熟练掌握软件开发基础(语言,框架,架构,系统等)。

正确理解软件需求

理解软件需求的过程是一个获取知道-不理解和不知道-不理解的知识的过程,它依赖于团队成员的业务知识,理解过程中的参与人员和自动化测试。团队成员的业务知识越多,参与这个理解过程的人越多,自动化测试的质量越高,越有助于团队成员正确理解软件需求。

我们可以尝试下面的实践来加深团队成员对软件需求的理解。

  1. 项目开始前的业务上下文分享。上下文应该包括业务背景,业务价值,相关干系人,沟通渠道,解决方案,优先级等。这样有助于统一团队成员对项目的认知。
  2. 有BA,QA,TL甚至PM参与的故事卡Kick Off。团队对软件需求的理解是随着时间推移而不断加深的,故事卡的内容无法及时反映这些变化。这就需要我们在做每张故事卡之前进行Kick Off,以便获取到最新的软件需求信息。在Kick Off时,BA可能会修改需求,QA可能会提供额外的测试用例,TL可能会优化技术解决方案,PM可能会补充遗漏的软件需求,总之参加的人员类型越丰富,团队成员对软件需求的理解就会越深刻。
  3. 有BA和QA参与的Desk Check。在软件需求实现之后,团队成员在代码层面对需求有了更深的理解,而BA和QA可能从业务层面对需求有了更深的理解,团队可以通过Desk Check来再次对齐需求。
  4. 自动化测试支持本地运行。自动化测试除了可以验证软件是否符合当前的软件需求外,也可以验证在代码变更后软件是否仍然符合已有的软件需求。支持本地运行的自动化测试可以帮助团队成员在开发期间发掘当前需求与已有需求之间的关系。
  5. 结对编程。结对编程可以允许两个人从不同的角度对同一个软件需求进行理解,特别是在资深-资深,资深-新人的模式下,更有利于加深对软件需求的理解。
  6. 在开发过程中及时向BA反馈问题。在开发过程中,团队成员对需求的理解是不断加深的。通过分析已有需求的代码实现,我们会发现很多原始需求无法解答的问题,这时我们需要及时向BA进行反馈和澄清,确保需求的正确性和完整性。
  7. 轻量级的RFC 。RFC是一种决策流程,它需要邀请所有相关方对方案进行审查和反馈,以确保方案的合理性。软件需求往往包含很多的方案决策,在方案涉及其他团队时使用RFC,可以确保团队成员能够深入理解软件需求的上下游关系。

熟练掌握软件开发基础

软件开发基础决定了软件需求的代码实现,它既依赖于团队成员的技术能力,也依赖于团队的知识积累。

提升团队成员技术能力的最好方法就是言传身教。笔者在团队内部做过各种各样的培训和工作坊,效果都不是特别理想,因为它们只能传授知识,不能有效的将知识转换为他人的技能。从个人经验来看,最好的培训方式就是结对编程,它既可以用来传授知识,也可以用来实践知识,口头的传授永远没有实际行动传递的信息量大。代码审查也是一种可以提升技术能力的实践,它可以让团队成员从更多的角度来理解代码,对技能的宽度和深度都很有帮助。

团队的知识积累主要体现在下述文档中

  1. 新人文档。这是每一个团队成员加入团队时看的第一份文档,它包含了环境配置,权限申请,业务介绍,工作规范,常用资料等内容。每个新人都应该可以根据这份文档在一周内开始工作,而且他们需要在发现文档有错误或遗漏时对文档进行更新。
  2. ADR。在软件需求的实现过程中,团队会有各种各样的讨论和决定,它们都是团队的宝贵财富,我们可以用ADR来记录这些讨论和决定。这样在有新成员加入或维护已有功能时,我们可以很容易的获取到全部的上下文。
  3. C4。C4图可以让团队成员从不同的层面了解当前系统,是他们能够开始工作的基础。

提高软件需求的质量

软件需求质量的定义为软件需求能够代表客户的需求。当软件能够按照软件需求运行,但无法满足客户需求时,就是软件需求的质量有问题。《专业主义》将质量问题的首要原因归结为与客户沟通不力,客户的需求就像科学中的真理一样无法达到,我们只能通过猜想与反驳来无限逼近。即使客户直接告诉我们需求,往往也是不可靠的,他们的需求会随着时间和环境的改变而改变。但是我们可以通过各种各样的软件需求收集方法来帮助我们和客户自己发现尽可能多的高质量的软件需求,例如调查问卷,用例分析,原型验证等。不管采用什么样的方法,对软件需求的质量影响最大的两个方面是

  1. 与客户的沟通距离。与客户的沟通频率越高,沟通程度越深,就越有可能得到高质量的软件需求
  2. 软件需求的验证成本。在软件需求收集阶段,我们要努力证明需求是错的,而不是对的。软件需求的验证成本越低,验证频率就可以越高,得到高质量需求的可能性就越大。最坏的情况是在生产环境进行验证,这种验证可以是预防式的,例如A/B测试,也可以是突发式的,那就是线上事故。

持续改进

连续的部署失败会快速降低信心水平,提升质量的重要组成部分就是解决已有问题并预防其再次发生。每个迭代结束后的复盘是团队最重要的质量改进实践,它的重心不应该是讨论如何进行团建活动,而应该是讨论我们在当前迭代有哪些问题以及该如何改进。PIR是对线上事故的复盘,涉及的往往是一些重大问题,它可以回顾事故的时间线,分析事故的根本原因以及讨论如何预防事故再次发生。

如何量化损失?

我们在这里用质量成本中的错误成本来量化损失,它是指部署失败之后处理相关问题所牵涉的费用。注意,这里之所以不考虑预防成本和鉴别成本,是因为团队往往不会把复盘,测试,重构等当做风险因素进行考虑。

损失 = 问题评估时间\times工资 + 问题修复时间\times工资 + 客服时间\times工资 + 客户补偿+隐性损失
  • 问题评估时间:发现问题以后定位问题原因以及给出解决方案所用的时间。
  • 问题修复时间:实施解决方案所用的时间。
  • 客服时间:问题出现之后客服处理客户投诉所用的时间。
  • 客户补偿:公司给予客户的金钱补偿。
  • 隐性损失:问题出现之后公司应得却没有得到的收入,其中包括由客户不满所导致的收入损失。

如何降低损失?

建立有效的告警机制

告警是指监控系统在检测到问题发生时能够执行给定的操作以便团队及时处理,例如发送邮件,拨打电话,发送Slack消息等。

告警要有策略

告警不能随意设置,我们需要根据软件需求来设置告警,要像自动化测试一样有告警策略。常见的告警类型有,系统告警,API告警,性能告警等。我们还需要根据问题的严重程度来设置合理的告警范围,以避免人力损失。

告警要及时

从问题发生的那一刻起,每一秒都可能对客户造成影响,而且持续时间越长,损失就越大。如果我们能在问题发生的同时收到告警,就可以在第一时间开始处理问题,减少损失。生产环境有很多成熟的告警工具可以使用,例如NewRelic,CloudWatch,AlertManager等。在设置告警时,我们需要仔细设置触发规则,以尽量缩短检测时间。

告警要准确

准确的告警可以减少问题评估时间。它包含两个方面,一是准确的告警触发规则,二是准确的告警信息。错误的触发规则可能导致应该检测到的问题没有被检测到,或者不是问题却被检测为有问题,从而导致收入损失或人力损失。错误的告警信息可能导致我们无法快速定位问题,从而增加问题评估时间,例如告警信息显示B系统出现了问题,可实际却是A系统出现了问题,我们就需要更多的评估时间来把问题定位到A系统。

建立有效的灾难恢复计划

在问题发生后,我们需要以最快的速度将软件恢复到正常状态以减少损失。对于简单问题,我们可以通过回滚版本,关闭功能开关,切换流量等来完成软件恢复。而复杂问题往往牵扯多个系统,并且伴有数据恢复或消息回放,我们无法通过简单的操作来完成软件恢复。所以,我们需要每个系统都有一个灾难恢复计划,它应该包含该系统可能遇到的各种问题及其恢复方案,并且我们还需要定期对该计划进行演练,以确保每个团队成员都了解在灾难发生时应该如应对。

当我们预估某个部署失败会造成较大损失时,可以采取预防式的部署策略来降低损失,例如金丝雀部署蓝绿部署等。

提高软件的可用性

对于不稳定的第三方服务,特别是在生产环境中出现过问题的第三方服务,我们需要降低对它的信心水平,在架构设计时预防其出现问题,从而降低其引起的损失。

首先,需要根据第三方服务的可用性来提高架构的可用性。例如,针对第三方服务可能出现的区域不可用,我们的架构需要支持多区域灾备。针对第三方服务维护窗口造成的服务不可用,我们的架构需要支持数据回放,以便在服务恢复后继续业务。针对第三方服务的请求速率限制,我们的架构需要支持消峰和缓存机制。

其次,需要根据第三方服务的可靠性来提高架构的可用性。例如,针对第三方服务的异步消息,我们的架构需要支持超时后的默认操作。针对三方服务的同步消息,我们的架构需要支持同步到异步的转换或消息缓存,以便在失败时回放消息。针对第三方服务的同步调用失败,我们的架构需要支持同步重试或异步重试。

总结

对于持续部署,团队总是会提出各种各样的担忧,如果我们的衡量指标无法反映这种担忧,就无法与团队进行合理的沟通,持续部署也就无法顺利实施。即便我们通过行政命令强行实施,一旦政策,环境或团队出现变动,持续部署很可能会被停止。

本文中的风险衡量指标属于必要性指标,如果想要实施持续部署,这两个指标必须在团队的行动区。但在行动区的团队不一定会实施持续部署,因为团队可能会有一些客观条件的限制,例如法律/公司规定团队必须在得到书面许可后才能执行生产环境的部署。

本文中的风险衡量指标和DORA指标的对应关系如下:

  1. 信心水平对应变更失败率。信心水平用部署成功来衡量,而变更失败率仅用生产环境的Bug来衡量。
  2. 损失对应平均恢复时间。损失中的问题评估时间和问题修复时间再加上问题检测时间就是平均恢复时间,但平均恢复时间没有考虑客服时间,客户补偿和隐性损失。

已经统计了DORA指标的团队也可以根据变更失败率和平均恢复时间来评估团队是否已经准备好实施持续部署。