切换前先实现一致
我们维护着一项供其他多个团队和服务依赖的服务。多年来,它一直存在两个版本:部署在本地服务器上的 Python 2 生产版本,以及位于独立分支、已经现代化的 Python 3 版本,后者一直等待云迁移,以便最终取代原版本。
许多遗留系统升级看起来都像这样。最终状态显而易见:新的语言版本、新的基础设施、旧系统下线,因此团队会直奔目标。工作在长期存在的分支或“v2”重写版本上进行,计划是在生产环境之外完成所有艰难工作,最后一次性切换。
永远无法落地的重写
这个计划很少能兑现承诺。新版本尚未发布期间,风险并不会消失,而是在不断累积。
每个紧急修复仍然必须先合入旧版本,然后再移植到新版本,或者被遗忘。两个版本之间每一个有意或无意的行为差异,都是一个等待被发现的 bug;而且往往要等到切换之后才会被下游团队发现,那时最难追溯问题的来源。
长期存在的分支让人觉得效率很高,因为它们把所有事情捆绑成一次事件。实际上,它们做的是把风险集中到某个时间点,并延迟本可以告诉你出了问题的反馈。
先倒退一步,才能向前交付
我们着手迁移时,本能做法是推动最终状态:让现代版本在云上运行,然后开始把消费者迁移过去。我主张先倒退一步:先让现代版本在旧的基础设施上投入生产,这样在其他事情发生变化之前,新旧版本就已经使用同一套代码。
旧主机运行的操作系统已经老旧到其 Python 支持最高只能到 3.6,而现代版本的目标版本是 3.7。我们本可以绕过这个限制:从源代码构建 Python,或者在从未为容器设计的主机上叠加容器,但每一种变通方案都会扩大本已足够复杂的迁移影响面。
因此,我们将服务降级到 3.6,并恢复了本地部署配置,在运行时根据环境选择设置。一个代码库,同时可以部署到旧主机和 Kubernetes。
版本降级看起来像是在倒退。正是它让迁移的其余部分变得简单直接。
一次只改变一个变量
实现 parity(一致性)后,每次发布都会同时部署到两个环境。新旧环境之间的行为差异会立即以小步增量的方式暴露出来,而不是在切换时一次性全部出现。两个版本之间多年来积累的分歧,在旧基础设施仍可作为备用方案时就逐步得到解决。
切换到云端时,迁移已经纯粹变成了基础设施变更。我们不必同时追问“平台迁移是否成功?”和“重写是否成功?”——如果出了问题,我们知道那是平台的问题,因为代码完全相同。切换过程平淡无奇,而这正是你希望看到的结果。这也是在 micro-frontends(微前端)之间逐步升级 React所遵循的原则:安排好变量的变更顺序,让每次发布只改变一件事。
交付迁移路径,而不是最终状态
迁移完成后,那项停滞多年的 Python 升级作为一次常规发布得以完成——旧主机的版本上限已经不复存在,而且只剩一个代码库需要升级。最终状态之所以更快到来,正是因为我们不再直接瞄准它。
如果一个分支已经开放数月,或者一次重写正在等待其他团队完成迁移,这通常说明这项变更试图一次完成太多事情。寻找一个现在就可以发布的中间状态,即使它看起来像是在倒退一步。最终状态从来不是难点——难点在于路径,而最安全的路径,就是能够一步一步发布的路径。
随机一篇博客