一次性部署多个架构更改
PlanetScale 的数据库分支功能采用声明式架构的方法,但我们更进一步,将分支中的所有更改作为一个单一部署处理。PlanetScale 尽可能接近原子性地部署你整套更改,这意味着在整个部署过程中,生产数据库架构保持稳定,当所有更改准备就绪时,它几乎会一次性改变。在这篇文章中,我们将讨论原子性多更改部署的优点,并深入探讨实现它的技术挑战。
为什么我们选择“接近原子性”?
原子性确保的是更改的“全有或全无”原则。例如,在数据事务中,你可以改变两个表的数据,比如向一个表插入数据并更新另一个表,并强制执行要么两个变化都发生,要么都不会发生。你可以在事务中运行这两个操作,并通过 COMMIT
来最终定稿。数据库会使用事务计数器和更改日志。如果事务途中出现崩溃,数据库的恢复过程可以可靠地识别此事务未完成,并撤销部分更改。
然而,对数据更改来说这一切看起来很简单,但对于架构更改来说就复杂得多了。
PlanetScale 利用 Vitess 支持下的 MySQL。而 MySQL 本身不支持事务性和原子性地对多个表的架构定义进行更改。如果你想 CREATE
一个表、ALTER
另一个表并 DROP
第三个表,这些更改必须按某种顺序运行。因此,我们使用术语“接近原子性”。我们也在 PlanetScale 的 gated 部署中使用该概念,并经常用“gated(受限)”来表示所有更改一起完成。
为什么选择原子性多更改部署?
我们倾向于将架构更改视为一种部署,就像代码部署一样。然而,传统的关系型数据库系统经常让我们认为架构更改必然是危险的、破坏性的、不可逆的并且是顺序的。
假设我们的代码变更需要修改三个不同的表:
- 在一个表上添加一个列并修改现有列;
- 在另一个表上添加一个列和索引;
- 在第三个表上添加新的检查约束。
在数据库系统看来,这些是三个不相关的更改,但我们知道它们在语义上是相关的。
对大表的架构更改可能需要很长时间,有时需要几个小时甚至更久。假设每个更改都需要8小时,通过传统方法,我们需要依次运行这些 ALTER TABLE
命令。整个部署过程预计需要 24 小时。但在此期间,数据库处于语义上的不一致状态。部署部分完成,部分仍在排队等待执行。
理想情况下,我们可以等到 24 小时后完成整个部署。但实际上,我们可能发现设计存在问题,或者发生了突发事件需要取消部署。如果部署进行到第 10 小时,一个更改已经完成,其他仍在等待中。在传统数据库中,完成的架构更改无法直接取消。
此外,我们已经习惯了架构更改的危险性。错误的架构修改,比如删除错误的列、对数据类型的错误假设或者遗漏约束,常常是导致生产系统中断的根源。因此,像这样对三个表进行的部署在每个时区都充满风险。
受限部署机制(Gated Deployments)
受限部署提供了一种新方式,使所有更改都在一个阶段中被准备好,直到所有更改都能被一次性完成。在上例中,我们可以假设所有更改准备阶段约需要 24 小时。在准备结束后,部署过程将一次性在生产环境中应用所有更改。尽管无法确保完全的原子性,但这些更改在几秒内快速完成。
通过这种方法,整个部署过程只有一次“重大事件”。由于受限部署允许选择何时完成这些更改,你可以完全控制“事件”的时间窗口。如果在准备阶段发生改变或者优先权发生变化,部署也可以被安全取消,而不会对生产环境造成影响。与传统方法相比,架构部分部署的潜在摩擦点从几天或几小时缩短为几秒。
技术概述及复杂场景
一些架构变更操作是即时完成的。例如,创建新表或修改视图定义。这些操作没有直接关联的数据,执行非常快。一些 ALTER TABLE
操作也可以快速完成,但许多其他操作并不具备这种性质。如果我们希望将 CREATE TABLE
、ALTER VIEW
和 ALTER TABLE
等这些操作都作为单一分支和单一部署的一部分,我们需要确保这些操作能够同时完成。如果有多个影响大表的 ALTER TABLE
操作,还需要协同计划这些操作,以确保不会过度负载生产数据库。
此外,架构变更可能存在依赖关系。用户可以随意修改其分支,当准备将这些分支的更改部署到生产环境时,可能会发现一个更改依赖另一个已经完成的变更。
解决方案是同时运行一些变更,同时协调长时间运行的变更和立即完成的变更,并处理所有需要依赖顺序的复杂情况。
并发处理长时间运行的架构变更
正如《How Online Schema Change tools work》中所述,PlanetScale 使用一个复杂的“复制并交换”算法来模拟 ALTER TABLE
操作。我们创建一个新表使其类似于原始表,然后修改该表,并通过复制现有行和同步实时变更来使新表与原始表保持一致,最后将新表和原始表交换。
这种模拟机制让我们能够在并发执行多表操作的同时灵活控制切换时间。当我们完成了表的现有数据集复制后,可以继续观察表的实时变更,理论上可以无限期维持这种状态,直到决定进行最终切换为止。在切换时,我们会施加一个短暂锁定以完成最后几步的更改。
并发处理和依赖关系排序的挑战
假设两种更改的依赖关系如下:
- 添加一个
info
列,然后修改视图以读取此列; - 删除一个
info
列,并修改视图排除此列。
在这类场景下,我们必须确保操作顺序合理。例如,前者需要先完成列的添加,然后才能更新视图;而后者需要先准备好删除列的操作以免视图变为无效,但在删除较大表的情况下也需要进行额外协调。
使用 schemadiff
PlanetScale 利用 Vitess 的 schemadiff
库。该库在比较两个模式时,会分析各实体之间的依赖关系,并生成有效的语句顺序。它可以将变化按等价类分组,并确保每个等价类中的操作顺序合理。
部署流程的编排
PlanetScale 使用 Vitess 并发执行变更,同时将所有变更过程进行分阶段调度,直到确认所有变更都已准备好完成。完成后,这些更改将在“接近原子性”的时间窗口内全部实施。
用户还有 30 分钟的窗口期可以撤销这些更改而不丢失积累的数据。如果需要撤销部署,系统会按最初部署的反顺序进行撤销,以确保依赖关系的完整性。
局限性
资源有限,并非所有操作都可以并行处理。试图在一个部署中修改上百个表是不可行的,可能也不是最佳实践。同时,过度复杂的分支可能会导致逻辑上的部署不可行。
结论
将多个架构变更请求作为一个整体部署是一项复杂的任务,需涉及验证、调度和执行。但这种方法带来的好处显而易见:部署可在最后时刻取消,不会对生产环境产生影响;部署过程可控,且危险点减少到一个时间窗口。通过声明式方法告诉数据库“目标状态”,而非如何实现它,将使部署更加简单高效。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接
本文链接:http://choupangxia.com/2025/09/13/deploying-multiple-schema-changes-at-once/