4378 字
22 分钟

GitLab 升到 19.4.1:备份、Operator 切换与配置收尾

2026-10-03,把自建 GitLab CE 从 19.3.2 升到了 19.4.1,同时更新 GitLab Operator 和 Kubernetes Runner。 这件事之前停在了 Operator 与 Kubernetes 的版本适配上。等集群完成 Kubernetes 1.35.9 升级和 kubeadm 接管,才重新核对目标组合并开始执行。

从 Runner 开始滚动到 GitLab CR 恢复 Running,约 14 分钟,准备和验收花的时间更多:完整备份有 53.56 GB,需要验证数据库恢复、仓库 bundle 和对象归档;备份缓存还触发了一次 Ceph MON 系统盘空间告警。升级期间,readiness 探针出现了约 39 秒的异常窗口,随后恢复,Git、CI、Registry、LFS 和附件检查通过。

当天下午又补了一轮配置整理:线上虽然已经运行新版本,服务器的原 values 文件却还保留旧 tag,靠临时覆盖文件才能复现现网。把这些版本合并回原文件,才算完成了部署入口的收尾。

本文依据本次执行记录整理,时间均为北京时间,状态覆盖到 2026-10-04 00:09 的备份清理后检查。实际节点、内网地址、私仓、项目标识和备份位置已省略。以下命令只展示操作形态,需要已有的完整实例配置和维护条件,不能当作通用升级脚本。文中验证过的临时备份后来已主动删除,保留状态在末尾单独说明;写作时,24 小时复核仍未完成。

先分清三个版本,再看 Kubernetes#

GitLab Operator、GitLab Chart 和 GitLab 应用版本并不共用一个数字,Runner 又有自己的 Chart 与镜像版本。这次固定的组合是:

对象升级前本轮结果
GitLab Operator3.3.23.4.1
GitLab Chart10.3.210.4.1
GitLab CE19.3.219.4.1
Runner Chart0.92.10.93.0
Runner manager / helper19.3.119.4.1
Kubernetes1.35.9保持
PostgreSQL / Redis17.6 / 7.0.15保持

前一次因为 Kubernetes 适配问题暂缓,不适合被简化成一个永远有效的最低版本数字。重新执行时,要把具体的 Operator 版本、Kubernetes 版本和目标 GitLab Chart 放在一起核对:集群支持范围看 Operator 安装文档,可用 Chart 则看对应 Operator 发布版本的 CHART_VERSIONS。文档的当前支持矩阵会继续变化,维护窗口必须重新检查。

3.4.1 的清单列出了 10.4.1、10.3.3 和 10.2.7,原实例使用的 10.3.2 不在其中。因此,Operator 更新和 GitLab CR 的目标版本需要一起准备,不能把只更新 Operator 当成这轮变更的结束。Operator 3.4.1 支持的 Chart

Runner 还有一个容易看错的地方:Chart 0.93.0 的 appVersion 是 19.4.0,这次实际运行的是 19.4.1。 manager 和 helper 都显式固定到了补丁版本,最后再用实际二进制与 CI Job 验证。helm list 中的 appVersion 不能代替运行版本;自定义 helper 镜像也要与 Runner 对齐。Runner helper 镜像说明

实例由 CR 管理,Operator 和 Runner 分别由 Helm 管理#

这套环境已经使用外部 PostgreSQL、Redis Sentinel 和 Rook RGW。Gitaly 仍是单副本,HTTP 入口继续经过原来的 Traefik。本轮保留这些拓扑,不同时迁数据库、换存储或调整入口。

管理关系如下:

gitlab-operator Helm release
└─ GitLab Operator
└─ GitLab CR 的 spec.chart.version / spec.chart.values
└─ 迁移任务与 GitLab 工作负载
gitlab-runner Helm release
└─ Runner manager
└─ CI Job Pod 与 helper
外部依赖:PostgreSQL、Redis/Sentinel、RGW、入口和证书

因此,升级 GitLab 本体的入口是修改 GitLab CR 的 Chart 版本。它不是另一个需要手工执行 helm upgrade gitlab ... 的独立发布步骤。官方的 Operator 升级流程也把 Runner、Operator 和实例更新分开处理。使用 Operator 升级 GitLab

本次接受维护期间暂时不可用,沿用 Operator 默认升级编排。这个维护约束没有被写成零中断承诺,后面的探针记录也保留了真实失败。

备份先验证,再进入切换#

07:11 开始重新采集基线:三节点 Ready,etcd 三成员健康,Ceph HEALTH_OK、18 个 OSD up/in、745 个 PG clean;GitLab 原有工作负载就绪,五个 PVC Bound。

恢复材料分层准备,因为 etcd 快照、数据库导出和 GitLab 原生备份保护的是不同内容。

材料当时完成的验证能说明的范围
新 etcd 快照etcdutl 检查、双节点 SHA256 比对Kubernetes 元数据快照可读取
PostgreSQL custom dump完整解码,在独立临时 Pod 中真实恢复并查询这份数据库导出经过恢复验证
GitLab 原生完整归档两节点摘要一致、仓库与压缩组件检查归档传输和内部数据格式通过检查
Redis RDB从副本取得、原生 checker 与摘要校验该 RDB 文件通过原生格式检查
原配置与实例 CR服务器侧受保护归档保留升级前配置来源

PostgreSQL 的第一次临时恢复没有直接通过。临时库默认编码是 SQL_ASCII,中文字段恢复时报了长度错误;确认生产库使用 UTF8 后,只重建隔离验证库并显式指定 UTF8,第二次完整恢复成功。验证 Pod 不使用生产 PVC,也没有对外 Service,生产数据库没有因此被改动。

GitLab 原生归档最终为 53,564,200,960 字节,约 53.56 GB,包含数据库、repositories、Registry、uploads、artifacts、LFS 和 external diffs。验证没有停在“tar 文件存在”:

  • 52 个仓库 bundle 逐一做镜像克隆,再运行 git fsck --full。
  • 六个 gzip 组件完整解压,检查 CRC 和文件结束位置。
  • 两台节点上的副本逐文件比对 SHA256。
  • 对未启用功能的缺桶提示和已配置空桶分别核对,避免把现用数据缺失当成可忽略提示。

备份通过 Toolbox 的原生 backup-utility 完成,最终归档也保存在对象存储中。GitLab Chart 备份与恢复

这些检查仍有边界。数据库独立恢复用的是稍早的一份 dump,完整 GitLab 归档包含后来创建的合成验证项目;各组件在线采集时间不同,没有形成全业务一致的停写冻结点。两台主机的系统盘与 Ceph 数据盘分开,但仍处于同一机房。本轮没有做完整 GitLab 整站恢复,也没有覆盖全部 PV 或虚机磁盘。

53 GB 备份带来的两个现场问题#

压缩速度和传输路径#

归档体积主要来自 Registry:它的压缩组件就有约 52.33 GB。第一次备份的串行 gzip 太慢,受控停止后保留中间材料,再用相同原生流程重试。

加速方式是下载并校验官方 pigz 2.8 包,仅在本次维护目录和 Toolbox 临时路径中使用四线程压缩 wrapper。压缩、解压往返检查通过后才继续,没有替换应用镜像或安装主机软件包。

完整归档从 Toolbox 所在节点直接经 SSH 复制到维护节点,避免让几十 GB 的数据穿过 Kubernetes API server。随后再复制到第二台节点并核对摘要。

中间还遇到过“目标文件已存在”的保护条件。确认那是已经复制完成的同一份归档后,继续验证现有文件,没有关闭保护或覆盖已完成副本。

临时缓存把 MON 所在系统盘压到了告警线#

备份流程会先在 Toolbox 所在文件系统组装数据。这里同时存在下载缓存、压缩组件和最终归档,所需临时空间会高于最终 tar 的大小。官方备份文档也说明了这个空间需求,但这次是在实际数据量下遇到了它。Toolbox 临时磁盘空间

同一台物理机还承载 Ceph MON。备份 Registry 的临时缓存让系统盘空闲降到 129 GiB,约 29%,触发 MON_DISK_LOW。

当时的处理顺序是先确认完整归档已经生成,再删除本次已归档的 Registry 下载缓存。空闲空间回到 179 GiB,Ceph 恢复 HEALTH_OK;Toolbox 随后正常更新,临时文件被回收,结束采样时空闲约 289 GiB。

本次没有降低 MON 告警阈值。这个故障说明,备份预算需要算上临时展开和压缩过程;当 Toolbox 与存储控制组件共用系统盘时,应用备份也会影响存储层的健康条件。

先核对文件与镜像,再依次升级#

服务器上的 Chart 工作目录没有 Git 元数据,其中还保留了实例配置。旧目录不能只凭名字就当成目标发布源,所以本轮使用官方 Chart 包,并核对下载摘要,再与现网配置组合验证。

发布前做了几项具体检查:

  • 实例 YAML 的 spec 与线上 CR 完全一致。
  • 新旧 GitLab Chart 渲染后的工作负载集合、selector 和 StatefulSet PVC 模板保持。
  • Operator 3.3.2 与 3.4.1 的官方 CRD 文件相同,本轮没有更新 CRD。
  • 15 个目标镜像在三台节点预拉取,45 份 image ID 核对一致。

预拉取 Gitaly 时,一台节点曾出现 short read / unexpected EOF,重试后成功。这个问题在切换前处理完,没有等旧 Pod 退出以后才去确认镜像能否下载。

Runner 先更新,并通过一次真实流水线#

先在 GitLab 暂停 Runner 接单,等待运行任务归零,再更新 manager 和 helper。官方 Runner Chart 文档也要求升级前暂停 Runner 并等待任务结束。Runner Helm 升级前提

09:00:34 开始更新,09:01:40 rollout 完成。实际二进制为 19.4.1。随后恢复接单,运行合成验证流水线,确认此时 GitLab 仍是 19.3.2,Runner 已是 19.4.1,Registry 与 artifacts 操作正常。

升级时的命令形态如下。MAINT_ROOT 代指已准备并审阅的维护目录;两条命令之间有单独验收,不能连续重放这段历史操作。

Terminal window
helm upgrade gitlab-runner "$MAINT_ROOT/charts/gitlab-runner" \
-n gitlab-runner --reuse-values \
-f "$MAINT_ROOT/runner-upgrade.yaml" \
--atomic --wait --timeout 15m
helm upgrade gitlab-operator "$MAINT_ROOT/charts/gitlab-operator" \
-n gitlab-system --reuse-values \
-f "$MAINT_ROOT/operator-upgrade.yaml" \
--atomic --wait --timeout 15m

这里的 --atomic 只作用于这两个 Helm release,不能把它理解成后续 GitLab 数据库迁移的自动回退保障。

Operator 就绪后,再更新 GitLab CR#

09:08:30 更新 Operator,09:08:47 新 Pod 已就绪并持有 leader Lease。进入 GitLab 切换前,再暂停 Runner 接收新任务。

09:09:25 更新 GitLab CR。使用带 UID、resourceVersion 和原版本断言的 JSON Patch,仅将 /spec/chart/version 从 10.3.2 改为 10.4.1,随后由 Operator 执行迁移和工作负载滚动;实例配置文件同步更新。

验收要求 status 的条件跟上本次 generation,避免把旧 Pod Ready 当成新版本已经部署完成。关键时间如下:

时间实际结果
09:09:25GitLab CR 更新,generation 10 → 11
09:14:45Running / 10.4.1,Available=True、Upgrading=False,条件 observedGeneration=11
09:23:06本次 14 项后台迁移全部 finished
09:59:35后台迁移完成后又观察了 36 分 28 秒

db:migrate:status 的 2,572 条状态输出全部为 up。后台迁移也单独检查,没有因为 Web 页面能打开就忽略它们;GitLab 的升级检查同样要求确认后台迁移状态。迁移检查

探针确实失败过,业务验收另外做#

从 09:09:55 到 09:10:26,/-/readiness?all=1 有五次采样返回 503,09:10:34 恢复。按相邻采样计算,异常窗口约 38.6 秒。

同一轮监测中,/-/health 的 1,507 次采样全部为 200。两个端点给出的信号不同,因此这里能报告的是 readiness 采样异常,不能据此精确推算全部用户请求的停机时间,也不能宣称用户侧零失败。

窗口内还记录了几项变化:

  • 一个新 KAS Pod 初次启动查询 Redis Sentinel 服务名超时,自动重启一次后恢复。
  • 迁移 Pod 有一次重启,前一实例日志已无法取得,原因未确认;最终退出 0,结构迁移与后台迁移检查通过。
  • Sidekiq HPA 因 CPU 负载从两个副本扩到三个,随后回到两个,属于正常伸缩。

这些现象没有通过重放数据库迁移或恢复整个 etcd 来处理。数据库发生迁移以后,优先按当前状态排查和前向修复;旧应用镜像与旧数据库快照不是可以随意拼接的回退组合。

真实功能验证使用私有的合成测试项目,覆盖升级前、Runner 更新后、GitLab 更新后三个阶段。

检查本次证据
CI 执行最终 Job 报告 GitLab / Runner 均为 19.4.1,helper 版本匹配
Registry认证上传,manifest 与 blob 读回并比对摘要
Artifacts上传、下载通过
Git over HTTPSclone、push 通过
Git over SSHGitLab Shell 内部 Service 路径 clone、push 通过
LFS旧对象与新上传对象在重新克隆后均通过摘要检查
附件与登录原附件内容保持,既有浏览器登录会话仍可访问私有项目

SSH 验证需要单独划清范围:域名的外部 TCP22 在升级前已经被网络设备关闭,本轮走的是内部服务路径。内部 SSH 读写通过,不代表外部 SSH 入口已经修好。

数据身份也做了前后对照:原五对 PVC/PV 的 UID 和 CSI volumeHandle 保持;PostgreSQL、Redis 和 Sentinel 共七个 Pod 的 UID、镜像与重启计数保持。PostgreSQL 一主一备 streaming/quorum,采样复制差为 0;Redis 副本链路正常,三台 Sentinel 的 CKQUORUM 检查通过。

另外,13 个 Secret 的 resourceVersion 发生过变化。检查 managedFields 后,确认本轮变更来自标签更新,仅涉及 metadata.labels,原 UID 保持;没有读取 Secret 数据值,也没有把 resourceVersion 改变直接判成密钥被轮换。

09:23:06 之后的 36 分 28 秒内,428 次 health/readiness 成对采样均为 200,没有新的 GitLab Warning 事件。此前一天的 Ceph 客户端 CRC 错误在本窗口没有新增匹配,但根因尚未定位,不能归入本次升级已解决的问题。

线上更新了,原配置也要能复现它#

早上的升级先用 --reuse-values 保留原 release 配置,再叠加临时升级文件。这让版本切换只涉及必要字段,但也留下了一个真实的收尾缺口:

文件刚升级完时的状态
Operator values.yaml镜像 tag 仍为 3.3.2
Runner values-instance.yamlmanager/helper 仍为 19.3.1
临时升级覆盖文件才包含本次目标版本
GitLab 实例 CR 文件Chart 已是 10.4.1,顶部部分注释仍旧

所以当时虽然线上正常,直接拿原文件重新部署仍会有问题。只说“Chart 已同步”并不能证明部署输入已经完整对齐。

下午把 Operator 3.4.1 合并回 values.yaml,把 Runner manager/helper 19.4.1 合并回 values-instance.yaml。Runner 基础 values.yaml 已与官方 Chart 0.93.0 文件逐字节一致,继续保留上游默认值;实例部署则明确加载实例配置和原有凭据文件。GitLab CR 中过时的版本注释也一并更正,解析后的 spec 保持不变。

整理后的验证改用 --reset-values,不再依赖现有 release 隐含保存的旧设置。下面是对应的只读预检形态,目录均为示例中的本地 Chart 工作目录:

Terminal window
helm upgrade gitlab-operator ./gitlab -n gitlab-system \
--reset-values -f ./gitlab/values.yaml \
--dry-run=server --hide-secret
helm upgrade gitlab-runner ./gitlab-runner -n gitlab-runner \
--reset-values \
-f ./gitlab-runner/values-instance.yaml \
-f ./gitlab-runner/values-instance.secret.yaml \
--dry-run=server --hide-secret

候选文件和写回后的文件都与原 release 的渲染结果比较:Operator 20 个、Runner 5 个非敏感对象完全一致,包括 Deployment 和 ConfigMap;两个 Chart 的 Helm lint 通过。凭据文件保持原样,由 Helm 在预检中使用,没有展示其内容。

这轮只整理服务器文件,没有执行真实升级。release revision 仍为 4/8,Deployment UID、generation 和 Pod 模板保持,GitLab CR 仍是 generation 11。临时覆盖文件退出日常部署入口,部署说明也跟着更新。

备份后来删了,文章里也要写清楚#

10 月 4 日凌晨,我决定删除本次升级和配置整理的临时备份。清理前逐项核对了路径、文件集合与大小,删除范围包括:

  • 两台节点上的完整原生归档、PostgreSQL dump、Redis RDB、升级前后 etcd 快照、原配置归档和原 CR。
  • 对象存储中的本次完整归档。
  • Chart 同步前的旧工作副本,以及两次配置整理留下的原文件备份。

共删除 145 个本地文件和 1 个 S3 对象,逻辑大小约 162.45 GB。对象所在桶未启用版本控制;删除后精确对象查询返回 404,本次备份前缀为空。这个数字是文件与对象的逻辑大小,不等同于 Ceph 当时立即回收的物理空间。

现用配置的摘要、现用凭据文件的元数据保持,日志与校验清单保留;文档里的恢复路径则标记为已失效。00:09 的清理后检查中,GitLab health/readiness 均为 200,三节点 Ready,etcd 三成员健康,Ceph HEALTH_OK。

因此,前文“备份验证通过”描述的是升级时的历史事实,这批备份现在已经不能用于恢复。 清理后检查也只是一轮状态采样,不能替代原定 10 月 4 日 09:24 以后的复核。本文写作时,本次升级已完成基础验收,24 小时稳定性验收仍待完成。

这次留下的两个具体改进点是:下一次备份前,把 Toolbox 的临时展开空间和所在节点的其他职责一起计算;下一次应用升级后,用原配置文件独立渲染并对照现网,避免临时覆盖层成为唯一能复现运行状态的入口。