这是 GitLab 14.9.3 升级到 19.2.1 系列的第二篇。迁移架构和逐站节奏见第一篇;最终冻结、增量同步和回滚见第三篇。
跨版本升级最容易把人带偏的信号是“Pod 已经 Ready”。在这次迁移里,真正决定能否进入下一站的不是探针,而是普通 schema migration、批量后台迁移、数据库支持矩阵和可恢复备份是否同时满足。
required upgrade stop 不是版本清单,而是闸门
GitLab 要求经过特定的 required upgrade stops(必要升级停靠点)。每到一站必须使用该 minor 的最新 patch,完成迁移并读取该版本的升级说明;不能只把二十多个标签排成队然后逐个替换。
我们把每一站视为一个小型变更窗口:
- 用当前版本检查 schema migration 与后台迁移。
- 创建此站回滚备份。
- 修改镜像并显式重建 Pod。
- 观察
reconfigure、Puma、Sidekiq、Gitaly、数据库和事件。 - 执行
gitlab:check、核心对象计数及 HTTP/SSH 冒烟。 - 只有全部通过才记录该站完成。
这样做看似慢,却把失败范围限制在两个相邻版本之间。遇到问题时,能够回到上一站的镜像、数据库 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 任务。
历史数据为什么会阻塞后台迁移
实际遇到的另一类问题是,某个任务处理历史通知数据时,发现记录时间早于现有月分区范围。此时“造一条新业务数据让它通过”或“直接跳过任务”都会污染升级后的数据。
我们的处理顺序是:
- 定位具体迁移、子任务和源记录。
- 确认这条历史记录是否仍有有效业务关联。
- 核对目标表分区边界与 GitLab 当前版本的迁移逻辑。
- 在停写、备份和可回滚条件下,为合法历史数据准备兜底分区。
- 使用当前版本官方任务重新执行或 finalize。
- 重新检查未完成迁移数和相关数据完整性。
这个案例的价值不在于“加了一个分区”,而在于先判断数据是否合法、处理是否可逆,再运行当前版本提供的工具。GitLab 的后台迁移与业务数据相连,不能当成普通队列任务随意清状态。
GitLab 19 首次 reconfigure 退出时,不要先删 Secret
从 GitLab 18 进入 19 时,首次 reconfigure 因历史组件遗留状态退出。最危险的反应是直接删除整份 Secret 或配置,试图让 GitLab “自己重新生成”。
我们先保留备份,再区分三种情况:
- 新版本已经移除了旧组件配置,可以由新版本重新生成不再需要的配置块。
- Secret 确实损坏或缺失,需要从受控备份恢复。
- 数据库中加密值已经无法被当前 Secret 解密,需要先停下调查,不能覆盖证据。
确认是第一种情况后再进行下一次启动,schema migration 随后正常完成。这里的经验是:配置问题、Secret 问题和数据解密问题的外在现象可能都是启动失败,但处理方式完全不同。
PostgreSQL 必须跟着 GitLab 分阶段走
GitLab 与 PostgreSQL 不能各自独立升级。本次采取的节奏如下:
| 阶段 | GitLab | PostgreSQL 策略 |
|---|---|---|
| 起点 | 14 / 15 | 保留 Omnibus 内置 PostgreSQL |
| 中段 | 16 | 迁移至外部 PostgreSQL 14 |
| 进入 17 前 | 16.11 | PostgreSQL 14 升至兼容小版本 |
| 进入 18 前 | 17.11 | PostgreSQL 14 → 16 |
| 进入 19 前 | 18.11 | PostgreSQL 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 在升级期间继续接收写入,最终需要冻结旧实例、识别真实变化的仓库,再完成精确同步。
DISCUSSION
讨论与反馈