跳到正文
HA_

← 全部文章

· 阅读约 3 分钟

失败即关闭的数据库迁移

ClickFortify 如何将 Prisma 迁移作为 ArgoCD PreSync 钩子运行,使有问题的迁移在任何新 Pod 启动之前就能阻止发布。

devopskubernetesarchitecture

机器翻译。 阅读原文

在大多数部署中,Schema 变更是风险最高的部分。新代码可以在几秒内回滚,而一个卡在两种结构之间的数据库却无法回滚。我是 ClickFortify 的首席架构师,这里的服务运行在 Kubernetes 上,由 ArgoCD 通过 GitOps 部署。我希望迁移步骤具备一个特性:一旦失败,任何新东西都不会运行。

这就是我所说的“失败即关闭”。失败的迁移应当阻止发布,而不是让新 Pod 在一个它们并不预期的 Schema 上启动。

迁移在哪里运行

ClickFortify 的 NestJS 服务通过 Prisma 使用 PostgreSQL。迁移通过 prisma migrate deploy 应用,它会按顺序运行所有待执行的迁移,除此之外不做任何改动。

问题在于在哪里运行它。如果在应用容器启动时运行,每个副本都会争抢执行迁移,而一旦失败,表现出来的就是不断崩溃重启的 Pod。如果在笔记本电脑上手动运行,就要依赖某个人记得去做。两种方式都做不到失败即关闭。

因此,迁移被做成一个 Kubernetes Job,由 ArgoCD 作为 PreSync 钩子运行。ArgoCD 会在应用其余清单之前运行 PreSync 钩子。如果钩子 Job 失败,同步就会中止,新的 Deployment 永远不会被应用。

apiVersion: batch/v1
kind: Job
metadata:
  name: migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  backoffLimit: 0
  activeDeadlineSeconds: 600
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: registry.example.com/api:sha-abc1234
          command: ['npx', 'prisma', 'migrate', 'deploy']

这份清单仅作示意,并非从真实清单复制而来。

与发布使用同一镜像

Job 运行的是与它所守护的发布相同的镜像。部署意味着向 ArgoCD 监视的仓库提交一个新的镜像标签,这个标签会同时应用到迁移 Job 和 api Deployment 上。因此,运行的迁移恰好就是随即将上线的代码一同发布的那些迁移。不存在一个可能与代码脱节的独立迁移镜像。

部署状态存放在独立的仓库中,与应用代码分开。这样 ArgoCD 不必在每次刷新时重新克隆庞大的 monorepo,也意味着 ArgoCD 部署的内容是一份小而可审查的标签与清单历史。

不盲目重试

backoffLimit: 0 是有意为之。Kubernetes 很乐意重试失败的 Job,但一个中途失败的迁移可能已经让 Schema 处于半应用状态。不加查看就再次运行可能让情况更糟。只尝试一次,然后停下来,让人去读日志。

activeDeadlineSeconds 为 Job 可以挂起的时间设定了上限,例如卡在某个锁上时,超过这个时间就判定为失败。

保留下来的日志

钩子的删除策略是 BeforeHookCreation。ArgoCD 只有在即将创建下一个 Job 时才会删除旧的 Job。在那之前,失败的 Job 及其 Pod 日志都会保留在集群中,而这恰恰是你最需要它们的时候。如果策略是在失败时删除钩子,就会把证据一并丢掉。

在这些日志中,一次正常的运行很容易辨认:Prisma 会报告它找到的迁移,然后列出已应用的迁移或说明没有待执行的迁移,随后 Job 完成。

顺序可能很重要

在新 Pod 之前运行迁移是正确的默认做法,有时甚至是必须的。设想这样一次发布:新代码不再写入某一列,而这一列的默认值在新的情况下已经不再正确。修复现有行和默认值的迁移必须在新代码启动之前完成,否则新行会拿到过时的默认值,而且没有任何机制去纠正它。

PreSync 钩子天然就提供了这种顺序。

给准备搭建这套机制的人的建议

  1. 将迁移作为可以中止发布的部署前钩子运行,而不是在应用启动时运行。
  2. 用与发布相同的镜像标签构建迁移 Job,这样代码和 Schema 就不会脱节。
  3. 设置 backoffLimit: 0 并保留失败 Job 的日志;半应用的 Schema 需要的是人,而不是重试。
  4. 只通过同步来部署,因为重启会跳过钩子。