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 Operator | 1.29.1 | 1.30.1 |
| PostgreSQL Pod 内的 instance manager | 14 个,均为 1.29.1 | 14 个,均为 1.30.1 |
| Helm Chart | 0.28.2 | 0.29.1 |
| Helm revision | 3 | 4 |
| PostgreSQL | 17.6 | 17.6,实际镜像摘要保持 |
| 每组实例数 | 2 | 2 |
| 数据卷 | 原有 PVC/PV | 身份及绑定保持 |
| Kubernetes | 1.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 工作副本:
- 核对新旧官方 Chart 的包摘要和旧目录中的官方文件。
- 用旧 Chart 与已确认的覆盖配置重现线上 Helm manifest。
- 对目标 Chart 做 lint、模板渲染和 API server dry-run。
- 确认新渲染包含 22 个对象、11 个 CRD,新增项为
DatabaseRoleCRD。 - 在三台节点预拉取 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 单独补齐:风险接受不等于入口问题消失
确认允许继续滚动 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_bytesFROM pg_stat_replication;
SELECT count(*) AS sync_waitersFROM pg_stat_activityWHERE 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 的成功输出更能说明,这轮维护究竟完成了什么。