paint-brush
如何安全地将变更交付到生产中by@israel_hlc
16,459
16,459

如何安全地将变更交付到生产中

10m2023/10/24
Read on Terminal Reader

我们都知道演习,对吧?当我们完成代码更改并[希望]在本地计算机上对其进行测试后,我们会将更改推送到周期的下一个阶段。本地测试是非常有偏见的,理想情况下,我们希望在更稳定的环境中验证更改(并且不仅仅遵循实施更改的工程师的观点)。
featured image - 如何安全地将变更交付到生产中
undefined HackerNoon profile picture

我们都知道演习,对吧?当我们完成代码更改并[希望]在本地计算机上对其进行测试后,我们会将更改推送到周期的下一个阶段。本地测试是非常有偏见的,理想情况下,我们希望在更稳定的环境中验证更改(并且不仅仅遵循实施更改的工程师的观点)。


下一步似乎很自然:将更改推送到可靠的暂存环境,并让合作伙伴(QA、PM、其他工程师)在移动更改之前帮助进行验证。接下来将进行错误修复和重新验证,直到我们认为它足够好,可以投入生产。伟大的!


然而,在大多数情况下,这根本不会发生。这可能是由于各种原因造成的,但无论是什么原因,其结果是我们经常必须在生产服务器得到足够好的测试或验证之前就对它们进行升级。


问题是……如果有东西坏了怎么办?其实,如何更早发现问题呢?好消息:可以采用一些工具和实践来使生产中的测试和验证不仅对您和您的公司来说是一种安全的做法,甚至可能是一个好主意。

基线:指标

在我们开始生产测试之前,我们必须讨论指标:我们需要它们来验证我们正在交付的更改是否产生了预期的效果,不会导致不需要的副作用,产品仍然稳定等。 -既定的指标,我们在推出更改时基本上是盲目的。我们将在本文的许多主题中引用指标,因此让我们看一下我们应该注意的两种不同类型的指标。

业务指标

在实施更改以评估影响后,应监控 KPI、目标和用户行为等业务相关指标。在进行任何更改之前,确定预计会受到影响的指标。同样重要的是护栏指标,即不应改变的指标。这些护栏的意外变化可能意味着新变化存在问题,需要进行审查。

技术指标

一旦定义了业务指标,了解技术指标也很重要。这些对于确保系统随着时间的推移而发生的变化保持健康至关重要。这里我们讨论的是系统稳定性、错误率、体积、机器容量限制等。


良好的技术指标对于解释业务指标中观察到的问题也很有用,或者快速找到回归的根本原因。例如,假设我们观察到用户在上一个版本推出后对某个特定功能的参与度大大降低。请求超时或错误率的增加可以快速显示哪些服务/端点导致了问题。

监控

我们有明确定义的业务和技术指标,很好!现在,我们必须监视他们。有很多方法可以做到这一点,但常见的第一步是构建仪表板来跟踪一段时间内的指标,从而轻松发现异常的峰值。如果仪表板允许根据可能与业务特别相关的特定细分快速过滤数据,那就更好了。主动监控仪表板是快速可视化系统中引入的新更改的效果的好方法。一些公司认为主动监控非常重要,以至于他们甚至实行 24/7 监控轮班,以尽早发现和解决问题。


监控指标的另一个好方法是通过自动检测和警报。对于关键指标,警报可以在出现问题时提供实时通知。假设我们开始推出一项功能,在该过程开始几分钟后,我们收到一条警报,指出错误率正在增加到超过特定阈值。这种早期通知可以防止我们在生产中进一步传播更改,并让我们避免很多问题!


最后,重要的是要注意我们需要多少信息以及在什么情况下需要。虽然仪表板对于提供产品和系统性能的可视化概览非常有用,但添加 1,000 个不同的图表会带来更多的混乱而不是清晰度。同样,如果我们每天收到 1,000 个警报,则无法对其进行调查和采取行动,并且最终会被忽略。

更安全着陆

指标已定义,监控到位,太棒了!现在让我们看一下一些工具和策略,以帮助我们避免问题、及早发现问题并最大程度地减少对生产的影响。根据生产环境的设置方式,其中一些将比其他更难实现,甚至组合起来可能没有多大意义。然而,这里的每一项都可以帮助我们向安全稳定的生产环境靠拢。

自动化测试

当项目脱离正轨时,自动化测试通常会被搁置,但它可以加快开发速度,并使生产变更更安全、更快速。越早发现问题,就能越快解决,从而减少在此过程中花费的总时间。恢复更改、修复并再次推送更改的过程通常压力很大,并且会占用宝贵的时间。


对于大多数项目来说,以单元、集成和端到端测试实现 100% 测试覆盖率为目标可能是理想的。相反,根据努力与收益来确定测试的优先顺序。指标可以指导这一点:覆盖核心业务功能可能比影响较小的利基功能更重要,对吗?从核心功能开始,随着系统的发展而扩展。


发布到生产过程应包括在部署到生产之前运行测试套件。测试失败应该暂停发布,防止生产问题。推迟某个功能的发布比第二天发现它完全出现故障要好。

内部测试

Dogfooding 是指在功能到达最终用户之前发布功能进行内部测试的过程。在测试期间,该功能在生产中可用,但仅限内部用户(员工、团队成员等)。这样,我们可以使用真实的生产数据来测试和验证新功能是否按预期工作,而不会影响外部用户。


狗食测试有不同的策略。为了简化概述,我们可以将它们分为两个更大的桶:

  1. 完整的人工测试:这很常见,例如在 iOS/Android 应用程序中,我们有内置工具可以向特定用户发布新的应用程序版本,然后在商店中向公众提供相同的版本。
  2. 选择性测试:有时,不可能(甚至不需要)对整个工件进行测试,但我们仍然可以根据特定的用户信息允许进行测试。举例来说,我们可以通过交叉一些数据来识别员工。然后,可以将应用程序配置为通过检查该数据并将用户分支到所需的行为来启用/禁用特定功能。然后,应用程序包含这两个功能,但只有部分用户会受到新更改的影响。我们将在下一个主题中回顾其中的一些概念。

金丝雀发布

金丝雀发布是一个发布过程,其中不是将生产中的更改立即推广到所有服务器,而是将更改提供给其中的一小部分服务器并监视一段时间。只有在证明变更稳定后,才会将其推送到生产环境。


金丝雀发布


这是测试新功能和危险变更的最强大工具之一,从而减少在生产中破坏某些内容的机会。通过对一组用户测试更改,如果检测到任何问题,我们可以停止/恢复部署过程,从而避免对大多数用户造成影响。

蓝绿部署

蓝绿部署是一种 DevOps 实践,旨在通过使用两个服务器集群(蓝色和绿色)并在它们之间切换生产流量来防止停机。在功能推出期间,更改将发布到一组(绿色),同时保持另一组(蓝色)不变。如果出现问题,流量可以迅速恢复到蓝色服务器,因为它们继续运行以前的版本。


#蓝绿部署

蓝绿部署通常与我们之前讨论的金丝雀发布进行对比。我们不会深入讨论此讨论的细节,但重要的是要提及这一点,以帮助我们决定哪些工具更适合我们的工作。

终止开关和功能切换

终止开关并非起源于软件工程背景,理解其用途的最佳方法是回顾最初的意图和设计。在工业中使用的机械中,终止开关是一种安全机制,可以通过非常简单的交互(通常是简单的按钮或开/关开关)尽快将其关闭。它们是为了紧急情况而存在的,以防止一次事件(例如机器故障)导致更严重的事件(受伤或死亡)。


在软件工程中,终止开关具有类似的目的:我们接受丢失(或终止)特定功能以试图保持系统正常运行。从较高的层面来看,该实现是一种条件检查(请参阅下面的代码片段),通常添加在特定更改或功能的入口点中。


 if (feature_is_enabled('feature_x')) {xNewBehaviour();} else {xOldBehaviour();}


举例来说,我们正在迁移到新的第三方 API。测试中一切正常,金丝雀版本稳定,然后更改 100% 推广到生产环境。一段时间后,新的 API 开始与容量作斗争,请求开始失败(还记得技术指标吗?)。因为我们有一个终止开关,API 请求可以立即恢复到旧的 API,而且我们不需要恢复到以前的版本,或者快速发布修补程序。


从技术上讲,终止开关实际上是功能切换(也称为功能标志)的特定用例。当我们讨论这个主题时,值得一提的是功能切换的另一个巨大好处:支持基于主干的开发。由于功能切换,新代码可以安全地投入生产,即使它不完整或尚未测试。

保持旧行为可访问

上面示例的代码可能让我们中的一些人想知道这是否真的是一个好的模式,新旧行为同时存在于应用程序中。我同意这可能不是我们想要的代码库的最终状态,否则,每一段代码最终都会被 if/else 子句包围,使代码立即变得不可读。


然而,我们不应该总是急于删除旧的行为。是的,一旦代码停止使用并避免技术债务,就立即清理代码是非常诱人的。但也可以在功能切换下将其保留一段时间。有时,新功能可能需要一段时间才能稳定下来,而备份选项是一种安全的机制,以防我们需要恢复它,即使只是很短的时间。


每个版本的生命周期都不同,跟踪何时是摆脱旧代码的好时机是一个很好的做法。保持代码整洁并减少维护开销将避免相反的情况,即尽管我们在代码中禁用了该功能,但考虑到它被禁用以来已经过去了多长时间,它可能已被破坏。

影子测试

我最喜欢的实现更安全更改的技术之一称为影子测试或影子模式。它包括执行旧行为和新行为以比较结果,但禁用一些适用的新行为副作用。让我们看一下这个简单的例子:


 int sum(int a, int b) {int currentResult = currentMathLib.sum(a, b);int newResult = newMathLib.sum(a, b);logDivergences(a, b, currentResult, newResult);return currentResult;}void logSumDivergences(int a, int b, int currentResult, int newResult) {if (currentResult != newResult) {logger.warn(      'Divergence detected when executing {0} + {1}: {2} != {3}',a, b, currentResult, newResult);}}


尽管两个求和运算都被执行,但新的求和运算仅用于比较和记录差异。这种技术对于监控复杂的系统变化特别有用,我们期望新旧行为之间有一定的一致性。另一个很好的用例是当我们必须对我们不太熟悉的产品进行更改时,或者当我们不太了解哪些边缘情况可能会受到预期更改的影响时。


在更复杂的场景中,我们可能需要在启用影子测试之前禁用一些副作用。例如,假设我们正在实现一个新的后端 API 来注册用户并将其保存到数据库,返回用户 ID。我们甚至可以有一个影子数据库来执行完整的过程,但发送“注册成功”电子邮件两次(每个后端 API 一次)绝对不是一个好主意。同样在同一个示例中,我们需要更深入的比较逻辑,因为简单地比较返回的用户 ID 并没有多大用处。


最后,重要的是要了解需要监视和测试什么,以及如果未实现平等,将应用什么标准。在某些关键场景中,我们必须迭代影子测试,直到结果完全相同。在其他情况下,当新的实施提供的额外好处超过损失时,有一定百分比的差异可能是可以接受的。

日志

即使有强有力的保障措施,系统也可能会出现故障。当这种情况发生时,我们需要能够以适当的细节水平了解正在发生的事情,否则,可能很难找到有效的解决方案。这就是日志来拯救世界的地方。


虽然日志记录并不是一个新概念,并且存在许多易于实施的解决方案,但确保有效的日志仍然具有挑战性。通常,日志不明确、过于复杂、缺少或充斥着不相关的条目,导致故障排除变得困难。然而,日志不仅仅用于解决问题。正确的日志记录有助于验证新功能和更改的有效性。通过对日志条目进行采样,我们可以跟踪用户旅程并确认系统按预期运行。

最后的想法

将代码传送到生产环境有时很危险,但我们有许多策略可以使该过程更加安全。即使我们发现了问题,了解什么是可接受的或不可接受的也很重要。并非所有故障都必须导致回滚。如果我们试图修复严重的安全缺陷或遵守新法规怎么办?制定明确的标准并了解变更的重要性对于确定何时中止或在出现问题时继续进行非常重要。回到开头,主要指标可以帮助我们进行决策过程。


大家安全着陆!