这是 GitLab 14.9.3 升级到 19.2.1 系列的第二篇。迁移架构和逐站节奏见第一篇;最终冻结、增量同步和回滚见第三篇。

跨版本升级最容易把人带偏的信号是“Pod 已经 Ready”。在这次迁移里,真正决定能否进入下一站的不是探针,而是普通 schema migration、批量后台迁移、数据库支持矩阵和可恢复备份是否同时满足。

required upgrade stop 不是版本清单,而是闸门

GitLab 要求经过特定的 required upgrade stops(必要升级停靠点)。每到一站必须使用该 minor 的最新 patch,完成迁移并读取该版本的升级说明;不能只把二十多个标签排成队然后逐个替换。

我们把每一站视为一个小型变更窗口:

  1. 用当前版本检查 schema migration 与后台迁移。
  2. 创建此站回滚备份。
  3. 修改镜像并显式重建 Pod。
  4. 观察 reconfigure、Puma、Sidekiq、Gitaly、数据库和事件。
  5. 执行 gitlab:check、核心对象计数及 HTTP/SSH 冒烟。
  6. 只有全部通过才记录该站完成。

这样做看似慢,却把失败范围限制在两个相邻版本之间。遇到问题时,能够回到上一站的镜像、数据库 dump 和配置,而不是从 14.x 与 19.x 的巨大差异中猜根因。

最危险的误判:把 finalized 当成未完成

迁移过程中曾有脚本用下面的条件统计“未完成”任务:

WHERE status <> 3

这在旧版本中看起来可用,但 GitLab 17 后状态 6 也表示 finalized。旧条件会把已经完成的迁移误报为失败,甚至可能让运维脚本去调用当前版本已不存在的迁移类。

更稳妥的 SQL 是:

SELECT id, job_class_name, table_name, status
FROM batched_background_migrations
WHERE status NOT IN (3, 6);

更推荐在当前 GitLab 版本的 Rails 环境中使用官方模型和 Rake Task,而不是依赖跨版本固定的状态数字:

gitlab-rails runner '
puts Gitlab::Database::BackgroundMigration::BatchedMigration.unfinished.count
'

gitlab-rake gitlab:background_migrations:list
gitlab-rake gitlab:background_migrations:status

这里的原则很简单:不要为了让计数归零而把任务手工改成 finished。先确认父迁移、子任务、源业务数据和当前版本实现。只有在完整备份存在、并确认迁移逻辑对现存数据不适用时,才考虑执行该版本官方提供的 finalize 或 execute 任务。

历史数据为什么会阻塞后台迁移

实际遇到的另一类问题是,某个任务处理历史通知数据时,发现记录时间早于现有月分区范围。此时“造一条新业务数据让它通过”或“直接跳过任务”都会污染升级后的数据。

我们的处理顺序是:

  1. 定位具体迁移、子任务和源记录。
  2. 确认这条历史记录是否仍有有效业务关联。
  3. 核对目标表分区边界与 GitLab 当前版本的迁移逻辑。
  4. 在停写、备份和可回滚条件下,为合法历史数据准备兜底分区。
  5. 使用当前版本官方任务重新执行或 finalize。
  6. 重新检查未完成迁移数和相关数据完整性。

这个案例的价值不在于“加了一个分区”,而在于先判断数据是否合法、处理是否可逆,再运行当前版本提供的工具。GitLab 的后台迁移与业务数据相连,不能当成普通队列任务随意清状态。

GitLab 19 首次 reconfigure 退出时,不要先删 Secret

从 GitLab 18 进入 19 时,首次 reconfigure 因历史组件遗留状态退出。最危险的反应是直接删除整份 Secret 或配置,试图让 GitLab “自己重新生成”。

我们先保留备份,再区分三种情况:

  • 新版本已经移除了旧组件配置,可以由新版本重新生成不再需要的配置块。
  • Secret 确实损坏或缺失,需要从受控备份恢复。
  • 数据库中加密值已经无法被当前 Secret 解密,需要先停下调查,不能覆盖证据。

确认是第一种情况后再进行下一次启动,schema migration 随后正常完成。这里的经验是:配置问题、Secret 问题和数据解密问题的外在现象可能都是启动失败,但处理方式完全不同。

PostgreSQL 必须跟着 GitLab 分阶段走

GitLab 与 PostgreSQL 不能各自独立升级。本次采取的节奏如下:

阶段GitLabPostgreSQL 策略
起点14 / 15保留 Omnibus 内置 PostgreSQL
中段16迁移至外部 PostgreSQL 14
进入 17 前16.11PostgreSQL 14 升至兼容小版本
进入 18 前17.11PostgreSQL 14 → 16
进入 19 前18.11PostgreSQL 16 → 17
终点19.2外部 PostgreSQL 17

GitLab 19 的支持要求应以执行当日的官方安装要求为准。应用配置明确关闭内置 PostgreSQL,并使用独立数据库、账号和 Secret:

postgresql['enable'] = false
gitlab_rails['db_adapter'] = 'postgresql'
gitlab_rails['db_host'] = 'postgresql.<namespace>.svc.cluster.local'
gitlab_rails['db_port'] = 5432
gitlab_rails['db_database'] = 'gitlabhq_production'
gitlab_rails['db_username'] = 'gitlab'
gitlab_rails['db_password'] = File.read('/run/secrets/gitlab-db/password').strip

大版本升级前先执行检查:

pg_upgrade \
  --old-datadir=<old-data> \
  --new-datadir=<new-data> \
  --old-bindir=<old-bin> \
  --new-bindir=<new-bin> \
  --check

本次数据卷是支持 reflink 的 XFS,因此实际升级使用 --clone,避免再复制一份完整数据库:

pg_upgrade \
  --old-datadir=<old-data> \
  --new-datadir=<new-data> \
  --old-bindir=<old-bin> \
  --new-bindir=<new-bin> \
  --clone --jobs=4

--clone 节省的是空间和时间,不是额外备份。旧数据目录、GitLab 18 的完整恢复点和 GitLab 19 的数据库保护备份,都必须保留到最终切流和观察期结束。升级后还要运行 vacuumdb --all --analyze-in-stages --jobs=4,并从 GitLab Rails 内核对实际连接目标和数据库版本。

磁盘不足时,先定义恢复边界

在最终版本阶段,磁盘剩余空间不足以再暂存一份完整仓库归档。我们没有假装“一切都有完整备份”,而是明确记录:当前只创建数据库保护备份,同时保留上一关键版本已经校验过的完整备份、仓库数据卷和配置归档。

这是一项受控取舍。关键是知道哪一个备份包含数据库,哪一个包含仓库,并在切流前验证每一个恢复点,而不是把 database-only 备份误当作完整恢复点。

后台迁移归零、数据库版本符合支持矩阵、最终版本启动稳定后,仍然不能立刻宣布成功。旧 GitLab 在升级期间继续接收写入,最终需要冻结旧实例、识别真实变化的仓库,再完成精确同步。

下一篇:GitLab 14 升级到 19 实战复盘(三):Kubernetes 增量切流、保留 IP 与回滚。