本文来自一次真实生产迁移。IP、域名、节点名、项目名、仓库数量、用户数量、镜像仓库、备份文件名和校验值均已脱敏或泛化;Secret、密码和内部地址只使用占位符。
升级停靠点和 PostgreSQL 支持矩阵会继续变化。执行前必须重新核对 GitLab 官方升级路径 与 安装要求。本文记录的是一次可复盘的过程,不是可以永久照抄的版本清单。
这不是一篇“把 GitLab 14 直接换成 GitLab 19 镜像”的教程。真实迁移中,我们先把一台 Omnibus GitLab 14.9.3 原样恢复到 Kubernetes,再完成二十多个版本停靠点,最后冻结旧实例、同步最终仓库增量、保留原来的 HTTP/SSH 入口。
系列共三篇:
- 本篇讲迁移设计、同版本恢复和升级节奏。
- 第二篇:升级停靠点、后台迁移与 PostgreSQL 17 讲最容易卡住的版本与数据库问题。
- 第三篇:Kubernetes 增量切流、保留 IP 与回滚 讲真正决定能否上线的最后一公里。
迁移前的真实约束
起点是一台运行多年的 GitLab CE 14.9.3。Git 仓库、内置 PostgreSQL、Redis、配置和日志都在一台虚拟机中;开发机、Jenkins 和历史仓库地址已经依赖原来的域名、服务 IP 和 SSH 入口。
目标不是为了“容器化”而容器化,而是同时完成四件事:
- 升级到 GitLab CE 19.2.1。
- 改为 Kubernetes 中的单副本 StatefulSet。
- 使用集群内独立 PostgreSQL,最终到 PostgreSQL 17。
- 尽量不改开发者和 CI 的 Git 地址,并保留可执行回滚点。
这四件事相互制约。Pod Ready 只能说明进程通过探针,不能说明数据库迁移、后台迁移、仓库对象、加密数据和最终增量都安全。
flowchart LR
A["旧 Omnibus<br/>GitLab 14.9.3"] --> B["同版本恢复<br/>Kubernetes"]
B --> C["required upgrade stops<br/>逐站验证"]
C --> D["PostgreSQL 17<br/>GitLab 19.2.1"]
D --> E["冻结旧实例<br/>最终增量切流"]
R["备份与回滚点"] --> B
R --> C
R --> D
R --> E
为什么没有直接跨版本
继续运行 14.x 不只是“版本旧”。安全更新会停止,Runner、浏览器和集成组件的兼容范围持续前移,旧操作系统和数据库也会一起进入维护末期。
但 GitLab 的升级也不能只看容器标签。大版本之间有 schema 变化、加密配置变化和 Batched Background Migration(批量后台迁移)。直接把 14.9 的数据放进 19.x,常见结果是迁移类缺失、表结构不匹配、数据库版本不受支持,或历史加密数据无法解密。
我们采用的实际路径如下。每个数字都是在目标 minor 的最新 patch 上停留并验收后,才进入下一站:
14.9.3 → 14.9.5 → 14.10.5 → 15.0.5 → 15.4.6 → 15.11.13
→ 16.0.10 → 16.1.8 → 16.2.11 → 16.3.9 → 16.7.10 → 16.11.10
→ 17.3.7 → 17.5.5 → 17.8.7 → 17.11.7
→ 18.2.8 → 18.5.7 → 18.8.11 → 18.11.9 → 19.2.1
其中有些停靠点是否需要经过,取决于实例规模、Web 节点数量及特定表的数据量。本次确认相关条件不触发后跳过了不适用的站点;这不是通用结论。每次升级都应把源版本、目标版本和实例条件重新输入 Upgrade Path 工具,再逐条阅读 Upgrade Notes。
先固定基线,后面每站都拿它验收
迁移刚开始时,我们没有立刻创建 Pod,而是先把“以后如何证明数据没有丢”写成一组可重复执行的检查。至少记录:GitLab 版本、核心对象计数、关键仓库 refs、各类附件占用、PostgreSQL 版本与扩展、服务状态以及两类迁移状态。
gitlab-rake gitlab:env:info
gitlab-rake gitlab:check SANITIZE=true
gitlab-rails runner '
puts({
users: User.count,
groups: Group.count,
projects: Project.count,
merge_requests: MergeRequest.count,
builds: Ci::Build.count
}.inspect)
'
现场经验是:gitlab:check 很有价值,但它不是数据一致性证明。它发现配置、权限和服务异常;仓库是否完整仍要比 refs、做 git fsck,并在最后做真实 clone、push 和 Jenkins 拉取测试。
备份要拆开理解
第一次演练就暴露了一个常见误区:GitLab 应用备份不是全部恢复能力。gitlab-secrets.json 保存 CI/CD 变量、2FA 等加密数据的解密能力;SSH Host Key 关系到客户端是否会看到主机指纹改变。
每个关键大版本前,我们至少保留以下内容,并记录文件大小和 SHA-256:
- GitLab 应用备份。
- PostgreSQL 独立 dump。
/etc/gitlab配置归档与gitlab-secrets.json。- SSH Host Key。
- StatefulSet、Service、PV、PVC、Secret、ConfigMap 清单。
umask 077
gitlab-backup create
gitlab-ctl backup-etc
tar -C /etc -czf /backup/ssh-host-keys.tar.gz ssh
sha256sum /backup/* > /backup/SHA256SUMS
恢复演练必须使用同一个 GitLab 版本。14.x 的备份先恢复到 14.x,确认服务和数据正常后才能开始升级;不能拿旧备份直接喂给 19.x 容器。外部 PostgreSQL dump 也要注意 pg_dump 客户端主版本不能低于服务端,必要时在 PostgreSQL Pod 或匹配版本的临时工具容器中执行。
平台迁移与版本升级必须拆开
这次最重要的控制点,是先把 GitLab 14.9.3 同版本恢复进 Kubernetes。若一边换运行平台、一边跨五个大版本,任何异常都无法快速判断是 PV、权限、网络、数据库连接还是 GitLab 升级本身造成的。
我们使用单副本 StatefulSet,本地 PersistentVolume 采用 Retain:即使误删 PVC 或 StatefulSet,宿主机数据目录也不会被回收。GitLab 是有状态服务,先把数据归属和节点绑定设计清楚,再谈版本升级。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: gitlab
spec:
replicas: 1
updateStrategy:
type: OnDelete
template:
spec:
nodeSelector:
app-type: gitlab
terminationGracePeriodSeconds: 300
OnDelete 是刻意选择。修改镜像后不会自动滚动;运维人员必须在当前站点的后台迁移、备份和验证全部完成后,才显式删除 Pod 进入下一版本。
每一站都执行同一套闸门
实际执行时,我们把每个停靠点固化为同一个节奏:检查前一站迁移、创建回滚备份、同步并预拉取下一镜像、修改 StatefulSet、显式删除 Pod、观察日志、检查迁移、执行服务与仓库验证。
kubectl -n <namespace> set image statefulset/gitlab \
gitlab=<registry>/gitlab-ce:<next-version>
kubectl -n <namespace> delete pod gitlab-0
kubectl -n <namespace> wait --for=condition=Ready \
pod/gitlab-0 --timeout=30m
kubectl -n <namespace> exec gitlab-0 -- \
gitlab-rake gitlab:check SANITIZE=true
首次启动数分钟没有 Ready 并不等于卡死。我们同时看 Pod 事件、reconfigure、数据库锁、Sidekiq、Gitaly 和磁盘 IO,避免因为一次 wait 超时就并发启动 Rails Runner 或完整 checksum,反而把存储压力放大。
完成平台迁移和升级节奏设计后,真正危险的部分才开始出现:后台迁移状态在 GitLab 17 改变,历史分区数据会阻塞任务,而 PostgreSQL 必须跟着 GitLab 分阶段升级。
DISCUSSION
讨论与反馈