4176 字
21 分钟

GitLab 升到 19.4.2:安全补丁、备份取舍与验收

2026-10-14,把自建 GitLab CE 从 19.4.1 升到了 19.4.2,GitLab Operator 同时从 3.4.1 更新到 3.4.2。 距离上一轮 19.4.1 升级只有十来天,这次主要处理安全补丁,Runner 和数据库版本继续沿用现有配置。

实际切换没有花很长时间:Operator 发布约 32 秒,GitLab CR 更新后约 4 分半恢复完整就绪。准备阶段仍然需要核对镜像、重新备份、验证恢复,再排空 CI;最后还做了 33 分 55 秒、202 次成对探针采样和 Git、LFS、Registry、artifacts、附件的业务回归。

这次有两个与上一轮不同的决定:原生备份明确排除了 Registry,升级完成后又主动删除了升级专用恢复材料。 它们分别改变了备份覆盖范围和当前可恢复状态,不能被一句“备份验证通过”带过去。

本文依据当晚执行和清理记录整理,时间均为北京时间;节点标识、内网地址、私仓、真实项目及恢复路径已经省略。命令只展示这套部署的操作形态,需要重新准备实例配置、备份和维护条件。本文写作时,次日只读复核仍未执行。

安全补丁的目标组合要一起核对#

GitLab 在 2026-10-12 发布了 19.4.2 Critical Patch。公告包含 CI/CD 正则表达式编译器的整数溢出问题,某些条件下可能导致未认证用户执行任意代码;19.4.1 位于公告列出的受影响范围。因此,这轮把范围收敛到已发布的 19.4 分支补丁。GitLab 官方补丁公告

维护窗口固定的组合如下:

对象升级前本轮结果
GitLab CE19.4.119.4.2
GitLab Chart10.4.110.4.2
GitLab Operator3.4.13.4.2
Runner manager / helper19.4.1保持 19.4.1
Runner Chart0.93.0保持 0.93.0
Kubernetes1.35.9保持
PostgreSQL / Redis17.6 / 7.0.15保持

Operator 3.4.2 的固定版本清单支持 Chart 10.4.2、10.3.5、10.2.8,原 Chart 10.4.1 不在其中。先更新 Operator、再更新实例 CR 的过渡期间,可能暂时看到旧 Chart 不受支持的提示;两步需要一起准备,不能更新完 Operator 就结束维护。Operator 3.4.2 的 CHART_VERSIONS

Runner 的版本也单独检查了。维护窗口内,19.4 分支最新可确认的 Runner 是 19.4.1,并没有与服务端同号的 19.4.2 可供升级。这次保留已经显式固定的 manager/helper,最后通过真实 CI Job 验证兼容性。Runner 官方标签

这里继续沿用上轮的一个检查原则:Chart 0.93.0 的 appVersion 是 19.4.0,不能代替当前实际运行的 Runner 19.4.1。 版本结论来自配置、二进制和 Job 报告,不能只读 Helm 表格。

Registry 的 76.7 GB,先改变了备份方案#

上轮完整归档约 53.56 GB,主要体积来自 Registry。本次重新采集时,Registry 对象已经增长到约 76.7 GB。

原生备份会产生下载缓存、压缩组件和最终 tar,临时峰值高于最终归档大小。这些缓存如果直接落到 Toolbox 所在节点的根盘,会挤占同机 Ceph MON 的空间。上轮已经碰过磁盘告警,这次提前设置了根文件系统至少保留 35% 空闲的门槛。

最初为此创建了独立备份 Toolbox Pod 和临时 250Gi 缓存卷。它们只服务这次备份,不挂载生产数据卷,也没有流量入口。随后我决定这轮不备份 Registry,于是在尚未写入备份数据时清理了临时 Pod、PVC 和 PV,回到现有 Toolbox 执行:

Terminal window
kubectl exec -n gitlab deploy/gitlab-toolbox -c toolbox -- \
backup-utility --skip registry

这是原生工具提供的组件排除方式,但排除成功并不说明被排除的数据受到保护。Chart 备份工具及排除参数

最终原生归档为 5,275,361,280 字节,约 5.28 GB,包含数据库、仓库以及实际有数据的附件、artifacts、LFS、packages 和 external diffs 等组件;已启用但为空的对象存储按空数据记录,dependency proxy 桶也核对为空。归档中没有 Registry 数据。

本轮的恢复边界因此很明确:升级时保留现有 Registry 服务和存储,并做认证读写回归;如果发生 Registry 数据丢失,这份归档不能恢复它。维护后的 Registry 验证和备份覆盖范围是两件需要分别记录的事。

真正完成的备份验证#

原生归档从 20:03:51 开始生成,20:15:00 完成,20:22 完成以下放行检查:

材料本轮验证能证明的内容
GitLab 原生归档64 个仓库 bundle 镜像克隆及 git fsck --full,6 个压缩组件完整解码这些归档内部组件通过格式与完整性检查
归档副本双节点 SHA256 一致,S3 全量读回后摘要一致已验证本次传输和存储副本一致
PostgreSQL custom dump完整解码,在独立 UTF8 实例中真实恢复这份数据库导出经过恢复验证
Redis RDB对精确副本执行原生 checker该文件通过原生格式检查
etcd 快照原生状态检查和第二份校验Kubernetes 状态快照可读取
原配置与 Rails secrets服务器受保护目录分开保存,双份摘要一致当时保留了独立的配置和密钥恢复来源

PostgreSQL 验证库显式使用 UTF8,避免重蹈上轮临时库默认 SQL_ASCII 的问题。它没有生产 PVC、没有 Service,也不持有生产 ServiceAccount token。恢复后查询到 64 个项目、10 个用户、80 个命名空间,未完成后台迁移为 0。

Rails secrets 需要单独处理,原生归档不包含这些密钥。这次将恢复副本直接保存在服务器独立目录,权限为 0600,验证双份摘要;内容没有进入对话、日志或博客仓库。Rails secrets 备份说明

这些仍是在线、多时间点备份。数据库、仓库和对象组件的采集时间不同,两台节点和 S3 仍处于同一机房故障域;本次完成的是数据库隔离恢复和归档检查,没有做完整 GitLab 整站恢复,也没有全业务停写的一致冻结点或 RPO0 承诺。

保持原拓扑,按 Operator 的管理关系切换#

实例继续使用外部 PostgreSQL、Redis Sentinel 和对象存储,GitLab 本体由 CR 管理,Operator 和 Runner 分别有自己的 Helm release。本轮版本变更落在两个入口:

Operator Helm release
└─ Operator 3.4.1 → 3.4.2
└─ GitLab CR 的 spec.chart.version
└─ Chart 10.4.1 → 10.4.2
└─ 迁移任务与应用组件滚动
Runner Helm release
└─ manager/helper 保持 19.4.1,暂停接单后恢复

GitLab 本体没有独立执行一次 helm upgrade gitlab ...。更新 CR 后,迁移与工作负载编排由 Operator 完成。官方 Operator 升级流程

制品和部署文件先过检查#

服务器的 Chart 工作目录没有 Git 元数据,文件名与版本号不足以证明它就是完整发布输入。本次重新获取官方 Chart 包、比对 SHA256,并用完整现用配置做新旧渲染和 server dry-run。

检查重点是资源集合、selector、Service spec、StatefulSet PVC 模板、RBAC 和 Secret 引用。迁移 Job、证书及测试 hook 的生成名称会随版本变化,不能直接把所有名字差异都当成资源替换风险。

Operator 3.4.1 与 3.4.2 的 CRD 文件一致,本次没有更新 CRD。13 个目标镜像经过既有镜像代理预拉到三节点,39 份 image ID 一致;没有把镜像下载失败留到切换阶段处理。

排空 CI,再更新 Operator 和 CR#

先记录 Runner 的原暂停状态,停止新接单,确认运行作业为 0,再开始切换。排空 Runner 只用于避免 CI 作业跨越维护窗口,并不代表 Git、Web、API 或对象存储已经全站停写。

Operator 使用已经校验的官方包和完整配置发布,命令形态是:

Terminal window
helm upgrade gitlab-operator ./gitlab-operator \
-n gitlab-system --reset-values -f values.yaml \
--atomic --wait --timeout 15m

--atomic 的边界是这个 Operator Helm release,不能替代 GitLab CR 或迁移后数据库的恢复方案。数据库迁移开始后,失败处置优先保留现场并前向修复,不自动降级应用或回灌旧数据库。

新 Operator 就绪并取得 leader Lease 后,再以 UID、resourceVersion 和原版本断言保护实例更新。JSON Patch 的核心字段如下,其中前两个值必须重新采集:

[
{"op": "test", "path": "/metadata/uid", "value": "<当前实例 UID>"},
{"op": "test", "path": "/metadata/resourceVersion", "value": "<当前 resourceVersion>"},
{"op": "test", "path": "/spec/chart/version", "value": "10.4.1"},
{"op": "replace", "path": "/spec/chart/version", "value": "10.4.2"}
]

本次只改变实例的 spec.chart.version,同步服务器实例文件,沿用 Operator 默认编排。虽然默认流程面向零停机升级,这个环境的单副本组件、存储切换和实际请求仍可能产生中断,所以维护前接受了暂时不可用,没有增加零中断承诺。

时间实际状态
20:23暂停 Runner,运行作业归零
20:24:51开始发布 Operator 3.4.2
20:25:23新 Operator 就绪并取得 leader;更新 GitLab CR,generation 11 → 12
20:29:58本次 generation 完整就绪,Running / Available=True / Upgrading=False
20:32:0014 项后台迁移全部 finished,开始连续观察
21:05:55完成 33 分 55 秒的连续探针观察

结构迁移检查的 2,572 条状态输出全部为 up,main/ci 的后台迁移检查也通过。这里以本次 generation、迁移和新版本副本作为判据,没有只看旧 Pod 仍然 Ready。

48 秒的探针异常和几个启动事件#

窗口总共记录了 270 次 health/readiness 成对采样。health 全部为 200;readiness 在切换时有 3 次 503:

采样时间
前一次成功20:25:47
首次失败20:25:58
最后一次失败20:26:22
下一次成功20:26:35

用相邻成功采样界定,影响窗口约 47.76 秒。这说明当时整体就绪检查确实失败过;/-/health 返回 200 也不能推出 Git、数据库或所有用户请求均正常,更不能把这一轮写成零停机升级。

另外保留了几项实际事件:

  • Gitaly 原卷重新挂载。 出现过 exclusive attachment/detach 等待,随后正常挂载,没有强制 detach 或修改原卷。
  • 两个 KAS 副本启动失败。 日志原因是 Redis Sentinel 的 DNS lookup timeout;自动重试后恢复,最终重启数分别为 1 和 2,后续保持 Ready。本轮没有修改 DNS/CNI,超时根因没有被宣称已解决。
  • Sidekiq HPA 新副本探针失败。 观察期间一个新增副本在启动时出现 readiness/liveness 告警,随后正常 Ready、重启数为 0,队列没有积压;结合创建、启动与当前状态解释事件,没有仅凭一条 Warning 判断为持续故障。
  • 临时数据库验证 Pod 的探针取消。 发生在清理已经完成验证的 Pod 时,没有当成生产数据库异常。

迁移完成后的 202 次成对采样全部为 200。这个 33 分 55 秒观察覆盖了本窗口的运行状态,24 小时复核另行安排,不提前算作已经通过。

功能验收覆盖已经配置的业务路径#

验证使用独立私有项目和合成数据。升级前先跑一轮基线,升级后恢复 Runner 接单,再跑同类验证。

验证项升级后结果
CI 实际版本Job 报告 GitLab 19.4.2、Runner 19.4.1
HTTPS Git分支 clone/push 路径通过
内部 SSH GitGitLab Shell Service 路径 clone/push 通过
LFS旧对象、本窗口升级前新增对象内容保持;升级后新 256 KiB 对象上传并重新克隆通过
Registry认证上传,manifest/blob 读回与 SHA256 一致
Artifacts上传下载,ZIP CRC 与 proof.json 一致
附件原附件升级后可读,新上传附件读回一致
浏览器会话原 Chrome 登录会话刷新后显示 19.4.2,可访问私有项目
数据卷原五对 PVC/PV UID 和 CSI volumeHandle 保持

内部 SSH 验证不代表外部 TCP22 入口已经验收;保留既有浏览器会话也不等于重新执行了一轮密码登录测试。

测试中还遇到一次权限边界:最初尝试在 CI 脚本中用 CI_JOB_TOKEN 上传新 LFS 对象,服务端返回 403。它发生在升级前,原因是该 token 不支持这种写入方式,不能算成版本回归。调整合成测试后,CI 基线通过;新 LFS 上传使用仅绑定验证项目的临时 deploy key,经原生 Git LFS 路径验证,结束后撤销权限并删除临时私钥。

Runner 没有配置缓存后端,所以本次没有把 cache 标成通过,也没有借补丁维护新增缓存能力。验证报告要区分已通过、未配置和未验证。

最终 Kubernetes、etcd、Ceph 与数据库复制正常;Sidekiq 没有入队积压或重试积压,历史 dead/failed 计数与基线保持。本窗口内核日志没有新的匹配存储错误,既有 Ceph AUTH 静默仍保留,历史存储根因没有因此归入本次修复。

线上版本对齐后,服务器文件也要能复现它#

本次把官方 Operator 3.4.2 Chart、完整原配置和实例文件同步回日常部署目录。values.yaml 固定到 3.4.2,实例文件与 live CR spec 一致,再次通过 Helm lint 和 --reset-values server dry-run。

Runner 文件和 release 保持原样。旧升级说明标为历史,当前说明指向本轮版本;凭据仍由原有受保护文件和 Secret 管理。

还有一个容易混淆的恢复材料:发布源同步前工作副本里的 CR,已经是新版本 10.4.2。 真正升级前的素材要看切换前保存的原配置归档和原 CR。目录叫作 before,并不能证明其中所有文件都来自数据库迁移之前。

结束阶段另建了双份 etcd 快照,它保护的是升级后的 Kubernetes 状态,不等于新版本业务数据的完整备份。

历史恢复材料后来清理了,服务器和本地分开核对#

当晚完成验收后,我决定清理 GitLab 版本升级专用备份。这一决定取代了原先保留到 24 小时复核后的安排,但次日只读复核继续执行。

服务器和 S3 清理前先保存文件集合、inode、大小、mtime 和现用配置/凭据元数据,再按精确路径和对象清单删除。范围包括两节点上的原生归档、PostgreSQL dump、Redis RDB、升级前后 etcd 快照、原配置与 Rails 密钥副本,以及旧工作副本和上轮遗留配置快照。

本次共删除 111 个服务器文件和 1 个 S3 对象,逻辑大小约 17.59 GB。S3 桶未启用版本控制,精确对象查询随后返回 404。这个数字包含多份副本,不代表 Ceph 当时立即回收的物理空间。

另外保留了 7 份共享基础设施维护窗口备份、独立 SMTP 配置回滚副本、执行日志和脱敏证据。它们不属于这次版本升级专用清理范围,也不能被当成本次完整恢复点。

服务器清理后又补查本地。生产恢复归档没有下载到本机,但仍有升级验证克隆、artifacts/附件临时副本和执行脚本。最终清理了 4 个本地临时目录、223 个文件,约 7.68 MB;文章、脱敏审计证据和其他维护资料保留,没有把本地数字重复计入服务器的 17.59 GB。

清理后的 GitLab 版本、generation、Helm revision、现用配置摘要、凭据文件元数据及线上 Rails Secret UID 保持;health/readiness 均为 200,基础设施正常。

因此,本文的备份验证描述的是维护时的历史事实,这批升级专用恢复材料已经不存在。 后续维护需要重新建立恢复点,不能沿用旧清单中的成功状态。按 GitLab 的恢复要求,原生归档还必须匹配精确版本和 CE/EE 类型;本轮归档来自 19.4.1 CE,也不能直接视作 19.4.2 的恢复包。恢复前置要求

写作时,升级、已配置业务路径回归和本轮连续观察已完成;2026-10-15 20:35 的单次只读复核仍待执行。复核说明已同步备份删除状态,不会把主动清理当成新增故障。

这轮保留下来的维护习惯很具体:根据实际数据量决定备份范围和临时容量,用迁移状态、目标 generation 和真实业务验证判断发布结果,再让服务器配置、审计记录和恢复材料的可用状态一起收尾。