二十年来,数据库模式迁移回滚一直依赖于预先规划的向下迁移脚本。但这个被广泛接受的方案存在一些关键缺陷,导致团队在紧急情况下不得不采用风险较高的手动操作来回滚。
本文将探讨为什么现有工具和实践无法实现 GitOps 承诺的声明式和持续调和工作流,以及如何通过 Operator 模式构建更稳健、更安全的回滚方案。
撤销功能的重要性
在软件开发中,能够撤销变更是一项重要的能力。撤销功能可以说是现代计算机最具革命性的特性之一。
回想打字机时代,修改错误是一件非常麻烦的事。需要将纸张退回,重新打字,这会留下明显的修改痕迹。对于大段修改,整页都需要重新打印。修正液虽然提供了一种临时解决方案,但使用起来既慢又不精确。
数字工具彻底改变了这一切。撤销功能将纠错简化为一键操作,让创作者可以自由尝试而无需担心犯错。这种转变让完美主义带来的压力被尝试、失败和改进的自由所取代。
回滚在软件交付中的作用
在软件交付领域,拥有撤销能力同样重要。能够将变更回滚到之前的状态,是团队部署新功能和修复问题时的重要保障。具体来说,回滚能力会影响一个关键指标:平均恢复时间(MTTR)。
MTTR
衡量系统从故障中恢复所需的时间。当部署失败或生产环境出现问题时,团队通常有两个选择:排查并修复问题(前滚),或回滚到之前的稳定版本。
当问题的解决方案不能立即确定,或问题影响较大时,回滚通常是最快的恢复服务的方式。这就是为什么可靠的回滚机制对于降低 MTTR 和确保服务高可用性至关重要。
回滚的实现机制
在文字处理器等本地应用中实现撤销功能相对简单。虽然具体实现方式有多种,但核心原理是相同的:系统会跟踪每一个变更,并能够恢复到之前的状态。
但在现代云原生应用这样的分布式系统中,情况就复杂得多。变更会涉及多个相互依赖的组件和配置。
《Accelerate:精益软件和 DevOps 的科学》一书指出了实现回滚的关键能力。作者将"全面配置管理"列为实现高效软件交付的关键技术实践之一:
"应该能够仅通过版本控制中的信息,以完全自动化的方式配置环境并构建、测试和部署软件。"
理论上,这意味着如果我们能在版本控制中存储系统的所有信息,并有自动化方式应用这些变更,就应该能通过应用之前的提交来回滚到之前的状态。
GitOps 与回滚
全面配置管理的理念逐渐演变为基础设施即代码和GitOps等实践。这些方法提倡将所有配置和基础设施定义以声明式格式存储在版本控制中,并通过自动化工具将变更应用到系统。
ArgoCD 和 Flux 等项目使 GitOps 在 Kubernetes 集群管理中得到广泛应用。通过提供在 Git 中定义系统期望状态的结构化方式(如 Kubernetes 清单),并自动将实际状态与之同步,GitOps 工具提供了一种标准化的实现方案。
从理论上看,GitOps 为我们带来了一个可行的回滚方案 - 只要回滚有问题的提交就可以了!
然而问题解决了吗?事实并非如此简单。
现实的挑战
尝试完全遵循 GitOps 理念的团队往往会发现,声明式和持续调谐工作流并不像表面看起来那么容易实现。让我们来分析原因。
声明式资源管理对于容器等无状态资源非常有效。Kubernetes 处理部署、服务等资源的方式与 GitOps 完美契合。一个典型的部署流程是这样的:
创建新版本应用的副本集 执行健康检查确保新版本正常 逐步将流量迁移到新版本 在新版本稳定后逐步缩减并移除旧版本
但这种方式是否适用于数据库这样的有状态资源呢?假设我们要修改数据库模式,流程会是这样:
用新模式启动新数据库 检查新模式是否正常 将流量迁移到新数据库 移除旧数据库
这种方式虽然技术上可行...但显然不够实用。
无状态资源易于管理是因为我们可以随时丢弃有问题的部分重新开始。但数据库不同,它们是有状态的,不仅包含软件组件(数据库引擎)和配置(服务器参数和模式),还包含宝贵的数据。而数据本身是无法从版本控制中恢复的。
像数据库这样的有状态资源需要不同的变更管理方式。
向上和向下迁移
管理数据库模式变更的传统做法是使用向上和向下迁移脚本,配合 Flyway
或 Liquibase
等迁移工具。这个方案看似简单:当需要修改模式时,编写一个描述如何应用变更的脚本(向上迁移),同时编写一个描述如何撤销变更的脚本(向下迁移)。
例如,假设你想要在名为 users
的表中添加一个名为 short_bio
的列,你的向上迁移脚本可能看起来像这样:
ALTER TABLE users
ADD COLUMN short_bio TEXT;
你的向下迁移脚本可能看起来像这样:
ALTER TABLE users DROP COLUMN short_bio;
理论上,这个概念是合理的,并且满足了全面配置管理的要求。所有需要应用和回滚变更的信息都存储在版本控制中。
然而,理论与实践不同。
向下迁移的神话
在研究了数百名工程师对此问题的看法后,我们发现,尽管这一概念被广泛接受,但实际上很少使用,为什么?
天真的假设
当你编写一个向下迁移脚本时,你本质上是在编写一个脚本,将来用于撤销你即将进行的更改。根据定义,这个脚本是在向上更改被应用之前编写的。这意味着向下迁移脚本基于的假设是更改将被正确应用。
但万一它们没有被正确应用呢?
假设向上迁移应该添加两列,但向下迁移脚本只写入删除这两列的脚本。但万一迁移只部分应用,只添加了一列呢?运行向下迁移脚本会失败,我们就会陷入未知的未知状态。
是的,一些数据库(如 PostgreSQL)支持事务 DDL,这意味着如果迁移失败,更改将被回滚,你最终会得到一个与特定修订一致的状态。但对于 PostgreSQL,一些操作不能在事务中运行,数据库最终可能会处于不一致的状态。
对于不支持事务 DDL 的 MySQL,情况更糟。如果迁移在半途失败,你将只剩下部分应用的迁移,而没有回滚的方法。
数据丢失
当你在一个本地数据库上工作时,没有实际的流量,使用向上/向下迁移机制可能感觉像是在你最喜欢的文本编辑器中使用撤销和重做。但在一个实际的环境中,情况并非如此。
如果你成功部署了一个添加列的迁移,然后决定回滚它,它的逆操作(DROP COLUMN
)不仅会删除该列,还会删除该列中的所有数据。重新应用迁移不会恢复数据,因为数据在列被删除时已经丢失。
出于这个原因,希望临时部署一个先前版本的团队通常不会回滚数据库更改,因为这样做会导致数据丢失。相反,他们需要评估实际情况,并找出其他处理这种情况的方法。
与现代部署实践的不兼容性
许多现代部署实践(如持续交付和 GitOps)提倡自动化和可重复的软件交付流程。这意味着部署过程应该是确定性的,不应需要人工干预。一个常见的方法是有一个接收提交的管道,然后自动部署该提交的制品到目标环境。
由于几乎不可能遇到零变更失败率的部署,回滚部署是每个人都需要准备的事情。
理论上,回滚部署应该像部署先前版本的应用程序一样简单。当涉及到应用程序代码的版本时,这完全可行。我们拉取并部署对应先前版本的容器镜像。
这种方法不适用于数据库,原因有两个:
对于大多数迁移工具, down
或rollback
是单独的命令,需要专门执行。这意味着部署机制需要知道目标数据库的当前版本,以便决定是迁移还是回滚。当我们从先前版本拉取制品时,它们不包含用于将数据库更改回滚到必要模式的向下文件——它们仅在未来的提交中创建!
这些差距意味着团队有两个选择:要么他们需要手动干预来回滚数据库更改,要么他们需要开发一个可以自动处理回滚的定制解决方案。
向下迁移与 GitOps
回到我们探索数据库回滚与 GitOps 是否兼容的主要主题,让我们扩展最后一个点。
ArgoCD 文档建议使用一个 Kubernetes Job
来执行选择的迁移工具,并将 Job
注释为 PreSync
钩子。
这个镜像通常是作为 CI/CD 管道的一部分构建的,并包含相关的迁移工具和迁移脚本:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: migrate
image: your-migration-image:{{ .Values.image.tag }} # Example using Helm values
restartPolicy: Never
当 ArgoCD 检测到 Git 仓库中的新提交时,它会创建一个新的 Job
来运行迁移工具。如果迁移成功,Job
将成功完成,并部署新版本的应用程序。
这适用于向上迁移,但当你需要回滚时会发生什么呢?
团队通常会遇到我们上面提到的两个问题:
部署机制不知道目标数据库的当前版本,因此无法决定是迁移还是回滚。除非团队在镜像中仔细考虑了这一点并实现了决定做什么的机制,否则部署机制将始终迁移。 用于回滚的镜像不包含用于将数据库更改回滚到必要模式的向下文件。大多数迁移工具会默默地将数据库保持在当前状态。
数据库不再与当前 Git 提交同步,违反了所有 GitOps 原则。 需要回滚数据库更改的团队被迫使用需要人工干预和协调的手动过程。
Operators:通往 GitOps 之路
Operator 模式是 Kubernetes 原生的一种扩展 Kubernetes API 以管理额外资源的方法。Operators 通常会提供两个主要组件:一个自定义资源定义(CRD)来定义新的资源类型,和一个控制器,用于监视这些资源的变化并采取相应的行动。
Operator 模式非常适合管理像数据库这样的有状态资源。通过扩展 Kubernetes API 以表示数据库模式的新资源类型,我们可以以 GitOps 友好的方式管理模式更改。一个专门的控制器可以监视这些资源的变化,并以一种 Naive Job
无法做到的方式应用必要的更改。
Atlas Operator
Atlas Operator 是一个开源的 Kubernetes Operator,它使你能够从 Kubernetes 集群管理数据库模式。基于 Atlas - 一个数据库模式即代码工具(有时称为"像 Terraform 的数据库"),Atlas Operator 扩展了 Kubernetes API 以支持数据库模式管理。
Atlas 有两个核心功能,有助于构建 GitOps 友好的模式管理解决方案:
一个复杂的数据迁移规划器,它可以通过比较模式期望状态与当前数据库状态来生成迁移。 一个迁移分析器,它可以分析迁移并确定是否安全应用,并在迁移应用之前揭示风险。
声明式与版本化流程
Atlas 支持两种管理数据库模式更改的流程:声明式和版本化。它们反映在 Atlas Operator 管理的两种主要资源中:
声明式:AtlasSchema
第一种资源类型是 AtlasSchema
,它用于采用声明式流程。使用 AtlasSchema
,你以声明式的方式定义数据库模式期望状态,并提供目标数据库的连接字符串。
然后,Operator 负责生成必要的迁移以将数据库模式带到期望状态,并将其应用到数据库。以下是一个 AtlasSchema
资源示例:
apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasSchema
metadata:
name: myapp
spec:
url: mysql://root:pass@mysql:3306/example
schema:
sql: |
create table users (
id int not null auto_increment,
name varchar(255) not null,
email varchar(255) unique not null,
short_bio varchar(255) not null,
primary key (id)
);
当 AtlasSchema
资源被应用到集群时,Atlas Operator 将计算数据库在 url
和期望模式之间的差异,并生成必要的迁移以将数据库带到期望状态。
每当 AtlasSchema
资源被更新时,Operator 将重新计算差异并应用必要的更改。
版本化:AtlasMigration
第二种资源类型是 AtlasMigration
,它用于采用版本化流程。使用 AtlasMigration
,你定义要应用于数据库的确切迁移。然后,Operator 负责应用必要的迁移以将数据库模式带到期望状态。
以下是一个 AtlasMigration
资源示例:
apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasMigration
metadata:
name: atlasmigration-sample
spec:
url: mysql://root:pass@mysql:3306/example
dir:
configMapRef:
name: "migration-dir" # Ref to a ConfigMap containing the migration files
当 AtlasMigration
资源被部署到 Kubernetes 集群时,Atlas Operator 会自动将 dir
字段指定目录中的迁移脚本应用到 url
字段指定的目标数据库。类似于传统的数据库迁移工具,Atlas 会在目标数据库中维护一个元数据表来追踪已执行的迁移记录,确保迁移的可靠性和幂等性。
基于 Atlas Operator 的数据库回滚实践
Atlas Operator 采用了一种 GitOps 友好的方式来处理数据库回滚。这正是 Operator 模式大放异彩的地方 - 它能够基于上下文做出智能决策,优雅地处理受管资源的变更。
在 ArgoCD 管理的环境中进行数据库回滚非常简单直观 - 只需将 AtlasSchema
或 AtlasMigration
资源回退到之前的版本即可。Atlas Operator 会自动分析变更并生成必要的迁移脚本,安全地将数据库模式恢复到目标状态。
Operator 模式带来的革新性优势
在处理前文提到的各种边界场景时,我们发现它们往往需要人工判断和干预。那么,如何将这些运维经验转化为自动化的解决方案呢?
Operator 模式为我们提供了一个绝佳的框架,让我们能够将运维知识编码到软件中。让我们来看看 Operator 模式是如何优雅地解决这些挑战的:
智能意图识别: Operator 能够准确理解向上和向下迁移的场景。通过对比数据库当前状态和期望版本,Operator 可以自动决定迁移方向。
完整的状态感知: 不同于只能访问构建时镜像的普通 Job,Operator 可以通过 Kubernetes API 将执行元数据持久化到 ConfigMap 中。这使得 Operator 即使在当前镜像缺乏状态信息的情况下也能执行向下迁移。
精准的差异计算: 得益于 Atlas 强大的模式即代码引擎,Operator 可以在数据库处于任何状态时都能计算出正确的迁移路径。
内置安全保障: Operator 会对每个迁移进行安全性分析。这个关键特性可以有效防范危险操作。您甚至可以为特定类型的变更配置手动审批流程!
总结
本文深入探讨了在 GitOps 环境中进行数据库模式回滚的技术挑战。我们分析了传统迁移方案的局限性,以及如何通过 Operator 模式构建更加智能和自动化的解决方案。Atlas Operator 为我们提供了一个强大而优雅的工具,帮助我们在云原生时代更好地管理数据库模式变更。
本文使用 Cursor 转译,并做了部分修改,原文链接:https://atlasgo.io/blog/2024/11/14/the-hard-truth-about-gitops-and-db-rollbacks