4838 字
24 分钟

CloudNativePG 升到 1.30.1:七组数据库滚动与 VPN 依赖

2026-09-30 下午,把 CloudNativePG 从 1.29.1 升到了 1.30.1,七组 PostgreSQL 的 14 个实例管理器全部更新完成。 Helm Chart 从 0.28.2 升到 0.29.1,release 到 revision 4;PostgreSQL 引擎和镜像仍保持 17.6。

这轮最需要处理的,并不是 Helm 命令本身。管理集群的 SSH 连接经过 AnyLink VPN,而 AnyLink 的数据库也在这七组里。直接把所有实例一起滚动,可能连处理故障所需的管理入口也一起影响。

最终分成两段:先升级另外六组,暂缓 AnyLink;确认接受管理链路风险后,再单独补齐 AnyLink。17:55:35 完成全七组基础验收,22:05 提前结束后续观察。 升级完成,但没有把未满 24 小时的观察记成通过。

本文于 10 月 3 日整理,依据的是 9 月 30 日维护窗口的执行记录与已完成检查,时间均为北京时间。节点地址、业务实例名和维护路径已泛化;文中的版本和资源数量是当时基线,不代表写作当天的集群状态。之后的 Kubernetes 升级和 kubeadm 迁移不属于这次 CNPG 维护。

先说清楚:这次究竟升级哪一层#

在前一轮 Kubernetes 1.34.12 升级之后,我准备继续推进 1.35。存储组件先完成了一轮升级,CNPG 是接下来的独立维护项。

当时三台物理节点同时承载控制面、etcd、业务和 Ceph。七组 CNPG 都是两实例,分别服务于 VPN、代码托管、镜像仓库、堡垒机、游戏数据采集和两组自建业务。

项目升级前本次最终结果
CNPG Operator1.29.11.30.1
PostgreSQL Pod 内的 instance manager14 个,均为 1.29.114 个,均为 1.30.1
Helm Chart0.28.20.29.1
Helm revision34
PostgreSQL17.617.6,实际镜像摘要保持
每组实例数22
数据卷原有 PVC/PV身份及绑定保持
Kubernetes1.34.12本窗口没有升级

CNPG 的升级有两层:集群里的 Operator,以及每个 PostgreSQL Pod 内的 instance manager。后者由 init container 放入 Pod,负责管理 PostgreSQL 进程。只更新 Operator Deployment,并不意味着每个数据库实例已经用上新管理器。1.30.1 升级说明

因此,这次没有做 PostgreSQL 17 到 18 的大版本迁移,也没有导出再重建业务库。发生变化的是控制器和实例管理程序,数据库仍然使用原来的数据卷。

PostgreSQL 版本没变,为什么还要重启实例#

维护前我也问过这个问题:既然数据库镜像没有变化,能不能只更新 Operator?

实例重启不是 PostgreSQL 17.6 的升级要求,而是本次采用滚动更新管理器的结果。 CNPG 默认通过重建 Pod 更新 instance manager;这会涉及 PostgreSQL 进程重启及主库处理。

上游也支持 ENABLE_INSTANCE_MANAGER_INPLACE_UPDATES=true:原地替换管理器程序,由新进程接管仍在运行的 PostgreSQL。它默认关闭,而且更新后 Pod 的 init image 仍可能显示旧版本,所以验收需要读实际管理器版本。原地更新说明

这次没有启用原地更新,沿用原生滚动流程,并在维护期临时选择 switchover:先更新备库,等它健康后切为主库,再更新旧主库。所有实例完成后,恢复原来的 restart 方法。

需要分清下面两个字段:

字段本次设置作用
primaryUpdateStrategy保持 unsupervised由控制器自动完成主库更新阶段
primaryUpdateMethod维护期 switchover,结束后恢复 restart选择主库更新时的处理方式

restart 会在可行时重启当前主库,不能执行时仍可能改为切主。因此恢复这个值,不等于禁止未来的主库切换。滚动更新策略

一次主备切换也不是应用连接无感的承诺。验收必须覆盖恢复后的连接、复制和入口,而不能只看新 Pod 启动成功。

发布前先留下能比较、能恢复的材料#

准备阶段先记录七组 Cluster、当前主库、Pod UID、实例版本、PVC UID 和 PV 绑定,再核对全局状态。当天的基线包括三节点、13 台运行中的虚机、95 对 PV/PVC,以及 Ceph 的 18 个 OSD、3 个 MON 和 745 个 PG。

这些数量用来发现本窗口的意外变化,不能代替逐项身份比较;后来主动删除的业务,也不能按这份历史数量重新补建。

恢复材料分成两类:

  • etcd 快照:为 Kubernetes 对象与控制面状态保留恢复点。
  • 业务备份:为数据库和其他已纳入范围的业务保留数据材料。

当时校验了 43 份业务材料,约 20.24 GB,包含 PostgreSQL globals 和数据库归档、MariaDB、Redis、VictoriaMetrics 快照归档及 GitLab 仓库归档。两台节点上的副本逐文件比较大小和 SHA256;PostgreSQL globals 不包含角色密码,部分材料还做了格式解析或原生校验。

这没有变成一次完整恢复演练,也没有覆盖所有 PV、虚机磁盘和对象存储。两份校验和一致,证明复制出的材料相同;不等于全部业务已恢复成功。 etcd 快照同样不能用来替代 PostgreSQL 数据备份。备份副本后来迁移过位置,恢复时应查最新清单,而不是照抄这次维护时的节点分布。

发布材料也先在独立目录准备,没有直接覆盖日常 Chart 工作副本:

  1. 核对新旧官方 Chart 的包摘要和旧目录中的官方文件。
  2. 用旧 Chart 与已确认的覆盖配置重现线上 Helm manifest。
  3. 对目标 Chart 做 lint、模板渲染和 API server dry-run。
  4. 确认新渲染包含 22 个对象、11 个 CRD,新增项为 DatabaseRole CRD。
  5. 在三台节点预拉取 1.30.1 镜像,核对内容摘要。

第一轮 server-side apply dry-run 碰到了既有字段所有权冲突。这里没有直接加 --force-conflicts,而是改为按旧、新渲染差异做 merge/create dry-run。它只能验证候选变更能否通过 API,不能替代 Helm 实际发布后的检查,也不能当作完整模拟了 Helm 三方补丁。

真正的停止点:管理入口也依赖待升级数据库#

15:46–15:48,重新核对本机路由和服务器看到的 SSH 来源,确认当前访问集群依赖 AnyLink VPN。之前某次维护使用过同内网连接,并不能证明这次也有独立入口。

这改变了风险判断:即使不重启 VPN 应用 Pod,更新它依赖的数据库仍可能影响认证或重新连接。如果现有管理链路失效,备份和回滚材料在服务器上,也不代表我还能连进去操作。

因此先停在 Helm 发布之前。15:48:52 的复核仍是旧 Chart、revision 3,七组更新方法均为原来的 restart。随后把范围缩小为另外六组,AnyLink 暂时保留两个旧管理器。

暂缓一组,不是关闭整个 Operator#

这次给 VPN 数据库添加了一个临时的 Pod 模板调谐保护,片段如下:

metadata:
annotations:
cnpg.io/reconcilePodSpec: "disabled"

这个注解的边界必须说清。1.30.1 的实现仍会先检查数据卷缺失、投射卷、数据库镜像和显式重启注解等变化,之后才跳过 PodSpec 比较。因此它不能撤销已经开始的删除或切主,也不能冻结故障自愈。1.30.1 模板更新判定源码

保护是否生效,是靠升级前后的 AnyLink Pod UID、Cluster spec、主库、管理器版本和可执行文件摘要核对出来的,不能只检查注解存在。

新 Operator 管理旧实例的兼容路径,为这次短暂分阶段处理提供了条件。固定版本能力说明但这不代表旧管理器自动具备新功能:1.30 系列增加的主库选举 Lease 和状态端口敏感操作认证,需要结合实例管理器版本判断,不能只看 Operator 已更新。1.30 升级注意事项

六组先行:Helm 成功后,还要等数据库完成#

进入六组阶段前,又重新检查了数据库、etcd、Ceph 和卷身份,并补了一份新的 etcd 恢复点。实际顺序是:保护 AnyLink,核对它未变化,只把其余六组临时改为 switchover,然后发布新 Chart。

Operator 的关键覆盖配置是:

image:
repository: ghcr.io/cloudnative-pg/cloudnative-pg
tag: "1.30.1"
config:
data:
CLUSTERS_ROLLOUT_DELAY: "300"
INSTANCES_ROLLOUT_DELAY: "60"

这两个参数用于错开组与实例的更新启动时间,减少集中滚动的压力。Operator 配置说明我没有把它们当成“上一组验收通过才启动下一组”的串行锁;不同组仍可能处于重叠的维护阶段。

16:07:32,Helm 返回成功,revision 4 为 deployed,Operator 1/1 Ready。这时只是控制器发布成功,实例滚动还在继续。

堡垒机数据库是早期更新的组之一。它先更新备库、切换主库,再更新旧主库,期间出现过短暂的 Unhealthy 和 FailedAttachVolume 事件。到 16:13:17,两个 Pod 恢复 Ready,复制正常,应用健康接口也确认数据库和 Redis 均正常。

这些事件被保留在过程记录里,没有因为最后恢复了,就把整个窗口写成零中断。两实例架构在一个 Pod 更新时,也确实会经历副本冗余降低的阶段。

一次 SQL 检查失败,没有直接放行#

16:46,采样确认六组新管理器均已就位。16:47:14,恢复原更新方法前的严格 SQL 检查在一组自建业务库上未通过,因此恢复动作没有继续。

初次失败的具体 SQL 子字段没有保存完整,不能在复盘时猜成复制延迟、连接问题或者脚本误报。可以确认的是,随后三次直接检查都得到:

  • 备库为 streaming,同步状态为 quorum。
  • 采样回放差为 0,SyncRep 等待数为 0。
  • 复制槽 active,WAL 状态为 reserved。
  • synchronous_commit=on,同步备库要求为 ANY 1。

没有为了通过检查而修改同步复制配置。重复检查和完整复验通过后,16:51:05 才恢复六组原来的 restart 方法,16:56:16 完成六组阶段验收。

这次也暴露了检查脚本的不足:失败报告需要保存判定所依据的字段,否则后续只能证明“没有持续复现”,无法还原第一次为什么失败。

确认允许继续滚动 AnyLink 后,第二阶段只处理 VPN 数据库:不再次执行 Helm upgrade,不重启 VPN 应用,也不重新滚动另外六组。

此时管理访问仍经过 VPN,没有因为前六组完成就多出一条独立救援通道。这一点在第二阶段保持不变。

备份先撞上了只读根文件系统#

补齐前,为 AnyLink 单独生成新的 globals 和两个数据库归档,共三个有效文件。第一次尝试把临时归档写到容器 /tmp,因只读根文件系统而失败,当时还没有触发实例滚动。

处理方法是先确认容器已有可写挂载,再在 /controller 下使用专用临时子目录;没有关闭只读根文件系统,也没有碰 PGDATA。归档完成后做完整解析和双份 SHA256 比较,最后清理本次创建的临时文件。

这里使用 /controller 是基于当时容器的挂载和权限。它本来承载管理器相关内容,不能把整个目录当作临时目录清空,也不能假定其他镜像里的同名路径一定可写。

把执行放在服务器端,但保留明确的停止边界#

由于管理连接依赖正在维护的业务,单组流程放进了服务器上的一次性 systemd transient unit。任务内部有 20 分钟停止门槛,外层有 40 分钟上限,Restart=no,日志保存在仅管理员可读的目录。

这样可以减少 SSH 会话中断对执行进程的直接影响,但后台任务仍然不是独立救援入口。异常处理也没有承诺自动回滚数据库;对尚未开始的模板更新,可以尝试重新保护,已经发生的切主和 Pod 重建则需要按实际状态判断。

当天第二阶段的实际时间线是:

时间动作与结果
17:44:19新的发布前 etcd 快照完成双份校验
17:49:12单组有界任务开始
17:49–17:51先更新备库,再切主,随后更新旧主库
17:51:49–17:51:51功能检查通过,恢复 restart,清除保护注解,任务正常退出
17:53全七组与全局复验
17:54:31升级后的 etcd 快照完成双份校验
17:55:35全七组基础验收完成

最后 VPN 数据库两个管理器均为 1.30.1,主库从原来的 1 号实例切到了 3 号实例。没有为了回到原编号再做一次切主。

VPN 应用 Pod UID 保持,容器重启次数前后都是 1,已有 HTTPS 入口返回 200,当前管理连接保持。这个 HTTPS 探测因现场证书信任配置跳过了证书校验,只能说明入口响应成功;同时没有主动断开 VPN 再测试登录,不能把它描述为所有客户端重连都经过验收。

验收要看实际版本、角色和数据路径#

最终报告没有只取 Helm revision 或 Pod Ready,而是把几层证据放在一起比较:

验收层次本次检查内容
发布状态Operator 镜像 1.30.1、Chart 0.29.1、revision 4,11 个 CRD Established
实例管理器14 个实际管理器版本均为 1.30.1,没有待重启或仍在升级的状态
数据库角色七组均 2/2,实际主库与 Cluster 状态一致,备库 streaming/quorum
新的主库 Lease持有者对应实际主库,并持续续租
Service 路由-rw 指向当前主库,-ro 指向健康备库
SQL查询、复制状态、回放差与等待检查;临时表事务提交并自动清理
持久化数据原 Cluster/PVC/PV 身份与绑定保持,14 个 PostgreSQL 镜像摘要不变
应用与基础设施数据库依赖、公开入口、Kubernetes、etcd、Ceph 和原有虚机继续检查

只看 Pod 里的 init image 还不够,验收脚本直接读取实例状态端点中的 instanceManagerVersion、executableHash、isPrimary 等字段。内部状态端点的证书校验在该探测中被跳过,不能用这项探测声称完成了证书信任链验收。

SQL 侧则通过原生视图检查复制。例如下面这类只读查询,可以分别确认角色、复制状态和同步等待;它们是观察项目示例,不是整套验收脚本:

SELECT pg_is_in_recovery(), current_setting('server_version');
-- 在当前主库上执行
SELECT state, sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_gap_bytes
FROM pg_stat_replication;
SELECT count(*) AS sync_waiters
FROM pg_stat_activity
WHERE wait_event = 'SyncRep';

这里的 0 回放差、0 等待都是采样结果。持续写入的数据库不能仅凭一次瞬时值证明整个窗口无延迟;出现异常时,仍要结合连续复查和业务行为判断。

升级验收时还做了临时表的事务提交测试,事务结束自动清理,没有改动业务表。这证明测试会话可以完成提交,不等于完成了所有业务操作或数据全量一致性核对。

全局检查继续覆盖了 7 组 PostgreSQL、5 组 MariaDB、9 组 Redis、35 条 Ingress、10 个原可达虚机端口和 85 个监控 target。原有应用失败、历史 Job 和磁盘/CPU 提示保留为基线例外,没有借这次升级消除所有告警,也没有因它们存在就忽略新的数据库问题。

升级完成后,把实际 Chart 放回日常目录#

这次还有一个明确的交付要求:新 Chart 必须回到原来的运维目录,不能只留在临时维护目录里。

16:53:45,在六组完成滚动并恢复策略后,已经把官方 Chart 0.29.1 和实际发布覆盖层同步回日常工作副本。AnyLink 后来的单组补齐没有再次发布 Helm,也没有改变这套 Chart。

同步时只更新核对过的官方资产,旧资产另存,保留目录里 37 项额外文件,包括业务清单和本地监控材料。没有清空整棵目录,也没有把含敏感配置的服务器工作目录整包复制到文章仓库。

交付结构可以概括为:

<原有 Chart 目录>/cloudnative-pg/
├── Chart.yaml # version 0.29.1 / appVersion 1.30.1
├── values.yaml # 官方默认值
├── values-instance.yaml # 本次实际发布使用的环境覆盖
└── templates/ # 已核对的目标官方模板

随后重新 lint,逐项比较官方文件,并将该目录渲染结果与批准候选、线上 Helm manifest 做语义比较。同步前后 release 都是 revision 4,说明这是交付源文件,没有顺手再发布一次。

这个目录仍是服务器上的工作副本,没有因为同步完成就自动变成 Git 管理的权威源。后续发布至少要确认目录、Chart 版本和环境覆盖一起使用,不能漏掉 -f values-instance.yaml,让默认值覆盖了已经确认的配置。

最终结论:升级完成,观察提前结束#

截至 17:55:35,七组数据库没有剩余暂缓项:14 个管理器全部为 1.30.1,PostgreSQL 保持 17.6,实例均为 2/2,同步复制和主备路由通过,原卷保持,七组都恢复 restart,临时保护注解全部清除。

后续已完成的只读复查没有发现新增持续故障,但证据存在明确边界。一次监控历史查询覆盖了 17:55:35–18:07:47,基础设施和 Pod 状态的 30 秒求值点正常;当时没有可用的 cnpg_* 历史时序,不能由 85 个 target 都正常,推导出七组数据库全时段同步复制正常。

另一次观察因连接和执行中断分时完成:CNPG 报告实际采集于 20:29,全局和入口检查在 21:29,源目录与身份核对在 22:02。同一个文件标签下的结果,也不能当作同一时刻的完整验收。复盘时应以证据内部的采集时间为准。

22:05,我决定不再继续观察,停止本轮采集并删除定时任务。 因此最终写下的是“升级及基础验收完成,24 小时观察提前结束”,而不是“稳定运行 24 小时”。长期备份、PITR、完整恢复演练和独立救援入口仍是后续事项,没有因为这次滚动成功而一并解决。

这次留下的几个做法值得继续保留:升级前先确认管理连接经过哪里;把 Operator、实例管理器和数据库引擎的版本分开验收;检查失败时保存原始判定字段;最后把实际发布材料放回下一次维护能找到的位置。它们比一次 helm upgrade 的成功输出更能说明,这轮维护究竟完成了什么。