状态:草稿(2026-10-17),部分为实际测量。 基准版本:cloudnative-pg v1.30.0(commit 4b5e244a7d03)、plugin-barman-cloud v0.14.0(1e13020fe5f0)、cnpg-i v0.6.0(844d20b2b783)、cnpg-i-machinery v0.4.2(e3ead2b31db4)。由于小型集群正在运行该版本(Operator 1.30.0,插件 v0.14.0),因此实际测量于 2026-10-17 在该集群中 仅通过读取 获得(读取 CR、读取 Pod 配置、读取运行中 Pod 内的
/proc·配置文件、读取日志)。未进行写入、重启和恢复测试。 编写规则:以实测数据为基础,未测量的标记为“未实测”,未确认的标记为“未确认”。
目录
- 1. 一句话总结:PostgreSQL 旁边的“常驻管理员”与“外包快递员”
- 2. 架构图
- 3. 代码地图
- 4. 跟踪核心路径
- 5. 作者的贡献与后续延伸
- 6. 实测:从小型集群中读取的内容
- 7. 从哪里开始阅读
- 8. 适合做出第一次贡献的地方
- 9. 局限性
- 参考
1. 一句话总结:PostgreSQL 旁边的“常驻管理员”与“外包快递员”
在 Kubernetes 上运行 PostgreSQL 的难点不在于启动 Pod,而在于确定主节点(primary)、在节点故障时进行切换,以及将 WAL 毫无遗漏地运送到外部。CloudNativePG(CNPG)不使用 StatefulSet,而是直接创建 Pod 和 PVC,并在每个 Pod 内部让 常驻管理员(实例管理器)占据 PID 1 的位置,将 PostgreSQL 作为子进程运行。
在最新版本中,备份和 WAL 归档这件“搬运货物”的工作正在从 Operator 主体中剥离,转交给 外包快递员(插件)来负责。两者之间的契约是名为 CNPG-I 的 gRPC 接口,而由 CNPG 项目直接打造的快递员就是 barman-cloud 插件。这种设计的目标是让 Operator 主体无需了解 S3、GCS、Azure 的具体细节,同时允许插件独立于 Operator 的发布进行修复。
| 组件 | 职责 | 运行位置 |
|---|---|---|
| CNPG Operator | Cluster、Backup、Pooler 等的协调,创建 Pod、PVC、Service,决定故障转移(failover) | cnpg-system 的 Deployment |
实例管理器(manager instance run) |
作为子进程运行 PostgreSQL,编写配置文件,提供状态 HTTP 端点 | 实例 Pod 的 PID 1 |
manager wal-archive / wal-restore |
PostgreSQL 的 archive_command·restore_command 调用的一次性命令 |
实例 Pod,每个 WAL 一次 |
| barman-cloud 插件,operator 半边 | 观察 Cluster 准备 RBAC,生成向 Pod 注入边车的补丁 | cnpg-system 的 Deployment |
| barman-cloud 插件,instance 半边 | WAL 归档与恢复、基础备份、指标 | 实例 Pod 的边车 |
| ObjectStore CR | 存放到哪个存储桶、使用什么凭证、采用什么压缩方式 | 应用命名空间 |
2. 架构图
读图指南:
- 上方区域是
cnpg-system。Operator 与插件 Deployment 通过 TCP + mTLS gRPC 进行通信。寻找插件的方法不是通过配置文件,而是通过 Service 的标签与注释(见 4.2 节)。 - 下方区域是一个实例 Pod。PostgreSQL 容器的 PID 1 是与 Operator 相同的二进制文件(
manager),该二进制文件是由 init 容器从 Operator 镜像中提取并复制到共享卷中的。因此,PostgreSQL 镜像中无需包含 CNPG 代码。 - 插件的 Pod 内半边作为边车附加,并与 PostgreSQL 容器通过共享卷
/plugins上的 Unix 套接字使用 gRPC 进行通信。 - 一个插件之所以被拆分到两个地方,是因为它们的工作性质不同。修改 Pod 规范的工作必须在 Pod 创建 之前 在 Operator 旁边完成,而读取 WAL 文件的工作必须在能够看到 PGDATA 的 Pod 内部 完成。
3. 代码地图
同时查看四个代码仓库。仅挑选了顶层中最核心的部分。
| 路径 | 内容 | 首个阅读的文件 |
|---|---|---|
cloudnative-pg api/v1/ |
Cluster、Backup 等 CRD 的 Go 类型与辅助函数 | cluster_types.go, backup_funcs.go |
cloudnative-pg internal/controller/ |
Operator 的 reconcile 循环(Cluster、Backup、Plugin …) | cluster_controller.go, backup_controller.go |
cloudnative-pg internal/cnpi/plugin/ |
CNPG-I 客户端:插件发现、连接池、RPC 包装器 | repository/setup.go, client/wal.go |
cloudnative-pg internal/cmd/manager/ |
单个二进制文件的子命令(instance run, wal-archive, backup …) |
instance/run/cmd.go, walarchive/cmd.go |
cloudnative-pg pkg/management/postgres/ |
实例管理器主体:PostgreSQL 运行、Web 服务器、归档器 | archiver/archiver.go, webserver/local.go |
cloudnative-pg pkg/postgres/ |
PostgreSQL 配置规则(包含必填值) | configuration.go |
cnpg-i proto/ |
接口定义(9 个服务) | identity.proto, wal.proto |
cnpg-i-machinery pkg/pluginhelper/ |
插件侧 gRPC 服务骨架、对象解码器 | http/server.go |
plugin-barman-cloud internal/cnpgi/ |
插件实现: operator/(Deployment 半侧)、 instance/(Sidecar 半侧)、 common/(WAL) |
operator/lifecycle.go, common/wal.go |
plugin-barman-cloud api/v1/ |
ObjectStore CRD | objectstore_types.go |
4. 跟踪核心路径
4.1 Cluster reconcile 循环:先加载插件再开始
Reconcile
(internal/controller/cluster_controller.go:169)
在读取 Cluster 后,首先会收集 该 Cluster 使用的插件名称(这是getPluginsNeededForReconcile, :298).
.spec.plugins 与外部集群中列出的插件合并后的列表)。利用这些名称,在 5 秒内建立连接,
并将它们放入
(WithPlugins, internal/cnpi/plugin/client/client.go:79)
context 中,随后进入内部循环。如果找不到插件,则将 phase 更改为 "Unknown plugin" 并等待 10 秒后重试。这是为了防止静默忽略那些已列出但不可见的插件。
内部的 reconcile, :310 虽然很长,但顺序非常清晰。
- 填充默认值、确定镜像、更新插件状态、创建辅助对象(Secret、Service、PDB)
(
createPostgresClusterObjects,cluster_create.go:57). - 插件的 pre-reconcile 钩子
(
preReconcilePluginHooks,plugins.go:36)。 在这里,barman-cloud 会创建供边车(sidecar)读取 ObjectStore 和 Secret 的 Role 与 RoleBinding(第 4.5 节)。 - 通过 HTTPS 查询各实例的状态
(
GetStatusFromInstances,pkg/management/postgres/webserver/client/remote/instance.go:183)。 如果发现有两个 primary, 则什么都不做 ,5 秒后再观察。等待返回的旧 primary 自行降级,以防止数据分叉。 - 处理切换(switchover/failover)、匹配 Pod 与 PVC
(
reconcileResources,:825). - 最后处理插件的 post 钩子、同步状态,并写入“healthy”阶段(phase)
(
finalizeReconciliation,plugins.go:105).
当 Operator 在 Kubernetes 中创建对象时,插件也会介入。Operator 的客户端会包装 Create·Patch·Delete 并首先调用插件的 LifecycleHook (extendedClient.Create, internal/cnpi/plugin/operatorclient/client.go:71)。
用于构建实例 Pod 规格(spec)的 NewInstance, pkg/specs/pods.go:485
也会在最后以“evaluate”动作为参数调用相同的钩子。由于应用了插件返回的 JSON 补丁(patch)的 Pod 会被实际创建,因此无需 Webhook 即可注入边车。
4.2 Operator 如何寻找插件:一个 Service 和一个 Socket 文件夹
插件通过两条途径被发现。
- 远程 (TCP): Operator 有一个专门监视 Service 的控制器
(
PluginReconciler.reconcile,internal/controller/plugin_controller.go:135)。 如果 Operator 命名空间中的 Service 带有cnpg.io/pluginName标签,它就会从注释(annotation)中读取端口以及服务器和客户端证书 Secret 的名称,并使用<서비스>.<네임스페이스>.svc:<포트>创建 TLS 1.3 mTLS 连接配置并注册到存储库中 (RegisterRemotePlugin,internal/cnpi/plugin/repository/setup.go:177)。 这一设计让安装插件的工作简化为“创建一个 Deployment 和一个 Service”。 - 本地 (Unix Socket): 实例 Pod 内的管理器会扫描
/plugins文件夹,并将文件名作为插件名称 (RegisterUnixSocketPluginsInPath,setup.go:193).
无论通过哪种途径,连接后要做的事情都是一样的。
LoadPlugin, internal/cnpi/plugin/connection/connection.go:442
通过此 Identity 服务询问名称、版本以及 实现了哪些服务,并针对每个实现的服务进一步询问详细能力(capability)。之后的每一次调用都将通过此能力列表进行过滤。例如,WAL 归档只会发送给具备 ARCHIVE_WAL 能力的插件。
CNPG-I 的服务在 cnpg-i/proto/ 中有九个。
分别为 Identity(identity.proto:23), Operator, OperatorLifecycle,
ReconcilerHooks, WAL(wal.proto:23), Backup, RestoreJobHooks,
Postgres 和 Metrics。每次请求都会通过 JSON 携带整个 Cluster。这是为了让插件无需在 Kubernetes 中重新读取 Cluster,并确保根据 Operator 看到该瞬间的 Cluster 进行判断。
如果在基准版 Operator 代码中实际统计调用 RPC 的地方(通过 grep 查找*Client().<RPC> 调用位置),会发现 WAL 服务的 Status·
SetFirstRequired 和 Operator 服务的 Deregister 找不到调用地方。它们似乎是 proto 中存在但 1.30.0 Operator 尚未使用的地方(意图未确认)。
插件端的服务器骨架由 cnpg-i-machinery 提供。
Server.Start, pkg/pluginhelper/http/server.go:152
首先注册 Identity,并由 “enricher” 函数挂载其余服务。如果提供了服务器地址,则开启 TCP,否则通过插件名称开启
Unix 套接字(createListener, :304).
4.3 实例管理器:PID 1 不是 PostgreSQL
实例 Pod 的命令是 /controller/manager instance run (见第 6 节实测)。这个二进制文件是 bootstrap-controller init 容器把自己复制到共享卷中的结果(bootstrap NewCmd, internal/cmd/manager/bootstrap/cmd.go:34).
runSubCommand, internal/cmd/manager/instance/run/cmd.go:157
,就会发现这个 PID 1 实际上是一个 小型控制器管理器。它运行在 controller-runtime 之上,负责托管实例、数据库和角色的 reconciler、日志管道、PostgreSQL 生命周期管理器、远程/本地 Web 服务器以及指标服务器。
PostgreSQL 由生命周期管理器启动
(runPostgresAndWait, internal/cmd/manager/instance/run/lifecycle/run.go:40)。
在完成初始化(匹配以确定使用哪个角色启动的 signal 文件)后,它将 postmaster 作为子进程运行,而当 postmaster 结束后,管理器也会察觉这一点并决定下一步行动。这就是为什么 PID 1 是管理器而不是 PostgreSQL:必须将 Kubernetes 终止和启动容器的单元与停止和重启 PostgreSQL 的单元分离开来,这样才能在
不重启容器
的情况下执行重新读取配置或主节点降级等操作。 不重启容器
。
操作员在第 4.1 节第 3 步中查询的目标是远程 Web 服务器的 /pg/status (pgStatus, pkg/management/postgres/webserver/remote.go:320)。
相反,Pod 内部的一次性命令(wal-archive, backup)则是通过本地 Web 服务器将工作移交给 PID 1 或获取 Cluster 缓存。
4.4 WAL 片段的旅程:从 archive_command 到 S3
archive_command用户无法更改。 CNPG 将此值固定为“必填项” (pkg/postgres/configuration.go:589,CnpgConfigurationSettings.MandatorySettings)。该值为/controller/manager wal-archive --log-destination … %p。 无论使用哪种备份方式,PostgreSQL 侧的配置只有一行且完全相同,改变的只有其后的 Go 代码。wal-archive命令。NewCmd,internal/cmd/manager/walarchive/cmd.go:40从本地 Web 服务器的缓存中获取 Cluster 并调用archiver.Run,pkg/management/postgres/archiver/archiver.go:124。如果自己当前不是主节点(正在转换中),则会拒绝。这是为了防止两个节点同时上传相同的 WAL。 完成后,成功或失败将作为ContinuousArchiving条件记录在 Cluster 中。- 插件优先。
internalRun,:156的第一个动作是将任务交托给插件。
// We allow plugins to archive WALs even if there is no plugin
// directly enabled by the user, to retain compatibility with
// the old API.
if err := archiveWALViaPlugins(ctx, cluster, pgData, walName); err != nil {
return err
}
// If the used chosen a plugin to do WAL archiving, we don't
// trigger the legacy archiving process.
if cluster.GetEnabledWALArchivePluginName() != "" {
return nil
}
来源:cloudnative-pg v1.30.0 pkg/management/postgres/archiver/archiver.go,Apache-2.0
.spec.plugins[] 如果存在 isWALArchiver: true 插件,则根本不会走原有的 (in-tree) barman 路径。如果不存在,则在下方检查
.spec.backup.barmanObjectStore ,如果连这也没有,则什么也不做并返回成功。即使是没有配置备份的 Cluster,
archive_mode 也是开启的并且会运行相同的命令,但 WAL 不会被上传到任何地方并被当作成功处理(第 6 节实测)。
4. 遍历套接字并调用 gRPC。 archiveWALViaPlugins, :277
挑选出 /plugins 中启用了 Cluster 插件的套接字进行连接,并由
ArchiveWAL, internal/cnpi/plugin/client/wal.go:34
携带 Cluster JSON 和 WAL 路径来调用 WAL.Archive 。这是一个针对每个 WAL 启动新进程并重新建立连接的架构。
5. 由边车(Sidecar)接收。 插件的
WALServiceImplementation.Archive, internal/cnpgi/common/wal.go:107
从 Cluster JSON 中提取插件参数(barmanObjectName, serverName),读取 ObjectStore 将凭据转换为环境变量,如果是首次运行则检查目标路径是否为空,接着收集足够 maxParallel 的已准备好 WAL 并并行执行 barman-cloud-wal-archive 。
决定路径的参数中包含了 configuration.ServerName (:196)。这个值与第 5 节的 serverName 话题相连。
archive_command 如果失败,PostgreSQL 将重试相同的 WAL,而在此期间 pg_wal 会不断累积,因此这个链条中的任何一环断开,最终都会表现为 磁盘被塞满。barman CLI 的退出代码会被包装成 gRPC 错误字符串返回(第 6 节)。
4.5 barman-cloud 插件:一个二进制文件,两个半边
插件二进制文件的子命令有四个: instance·operator·restore·healthcheck (cmd/manager/main.go:52).
- operator 半边在 Identity 中通告了
RECONCILER_HOOKS,LIFECYCLE_SERVICE,INSTANCE_SIDECAR_INJECTION三项能力 (operator/identity.go:45;服务器配置为operator/start.go:44)。 pre 钩子(ReconcilerImplementation.Pre,operator/reconciler.go:62)读取 Cluster 所指向的 ObjectStore,若不存在则返回“请重试 (REQUEUE)”,若存在则配置用于边车的 Role 和 RoleBinding。Lifecycle 钩子(LifecycleHook,operator/lifecycle.go:88)接收 Pod 和 Job,在reconcileInstancePod,:325→reconcilePodSpec,:370中创建边车容器,并将RestartPolicy: Always挂载到该容器上 (放入:433) ) (injectPluginSidecarPodSpec,:539)。 由于它是 Kubernetes 原生边车,因此会比 PostgreSQL 更早启动并保持套接字开启,直到 PostgreSQL 结束后才关闭。这个顺序可以防止在上传最后一个 WAL 的过程中边车提前终止。由于它原样复制并挂载了 PostgreSQL 容器的卷,因此边车也能看到 PGDATA。 - instance 半边通告了 WAL、Backup 和 Metrics(
instance/identity.go:46),由于启动时没有地址,因此通过 Unix 套接字监听(instance/start.go:47).
4.6 单次 Backup:method 决定路径
BackupReconciler.Reconcile, internal/controller/backup_controller.go:127
找到 Cluster 后,首先检查形式上的先决条件
(checkPrerequisites, :390).
if backup.Spec.Method == apiv1.BackupMethodPlugin {
if len(cluster.Spec.Plugins) == 0 {
const message = "cannot proceed with the backup as the cluster has no plugin configured"
return flagMissingPrerequisite(message, "ClusterHasNoBackupExecutorPlugin")
}
return nil, nil
}
if cluster.Spec.Backup == nil {
const message = "cannot proceed with the backup as the cluster has no backup section"
return flagMissingPrerequisite(message, "ClusterHasBackupConfigured")
}
来源:cloudnative-pg v1.30.0 internal/controller/backup_controller.go,Apache-2.0
method: plugin ),如果是,则只需检查是否存在至少一个插件即可通过。其余的 method 全部要求 .spec.backup 。失败将以
TerminalError 结束并不再重试,因为无限期重试配置错误的备份毫无意义。
通过后,路径会根据 method 分流。 plugin 和 barmanObjectStore 是 由实例管理器执行的备份(IsManagedByInstance, api/v1/backup_funcs.go:319),
volumeSnapshot ,而则是操作员直接拍摄快照。实例侧备份会根据目标 Pod(根据target 设置选择主节点或最靠前的备用节点)并在该 Pod 的 PostgreSQL 容器中执行 /controller/manager backup <이름> (startInstanceManagerBackup, :968)。
该命令向本地 Web 服务器请求备份
(requestBackup, pkg/management/postgres/webserver/local.go:131),如果是 plugin,则
PluginBackupCommand.invokeStart, webserver/plugin_backup.go:72
通过套接字调用 Backup.Backup 。边车的
BackupServiceImplementation.Backup, instance/backup.go:71
执行 barman-cloud-backup (服务器名称参数同样是 configuration.ServerName),并将结果返回给 Backup 的 status。
5. 作者的贡献与后续
作者在将使用内置 (in-tree) barmanObjectStore 进行备份的 CNPG 1.30 集群迁移到插件(v0.14.0)的过程中遇到了两次坎坷,并将这段经验通过两个文档 PR 进行了补充。截至 2026-10-17,两者均在等待评审中。
① serverName 去哪了 — plugin-barman-cloud#1164(Issue #1163)。
先前的指南要求将内置的 barmanObjectStore 节直接原封不动地迁移到 ObjectStore 的 spec.configuration 中。然而,如果其中包含
serverName ,ObjectStore CRD 会予以拒绝。规则是
api/v1/objectstore_types.go:61
中的单行 CEL(!has(self.serverName)),提示信息为“请使用 Cluster 的 serverName 插件参数”。插件读取该值的位置是 NewFromCluster, internal/cnpgi/operator/config/config.go:142
。
serverName := cluster.Name
for _, plugin := range cluster.Spec.Plugins {
if plugin.IsEnabled() && plugin.Name == metadata.PluginName {
if pluginServerName, ok := plugin.Parameters["serverName"]; ok {
serverName = pluginServerName
}
}
}
来源:plugin-barman-cloud v0.14.0 internal/cnpgi/operator/config/config.go,Apache-2.0
该值会原封不动地进入第 4.4 节的 WAL 路径和第 4.6 节的备份路径。也就是说, serverName 是“存储桶中的文件夹名称”,如果在迁移时该值发生变化,插件就会在原有备份旁边 创建一个新文件夹并开始写入。对于外部集群(恢复/副本源)侧,默认值也有所不同。
内置方式会解析为外部集群的名称
(ExternalCluster.GetServerName, cloudnative-pg/api/v1/cluster_funcs.go:403),而插件会解析为 当前正在创建的 Cluster 的名称(config.go:163,
:173)。PR 在先前的指南中记录了这两者的差异。外部集群侧仅通过代码对比进行了确认,未通过恢复重新验证(未实测)。
② “no backup section” — plugin-barman-cloud#1166(Issue #1165)。
如果在迁移到插件时从 Cluster 中删除了 .spec.backup ,则未编写 method 的 Backup/ScheduledBackup 会落入上述第 4.6 节引用的第二个分支。
这是因为 Backup CRD 的 method 默认值为 barmanObjectStore (经第 6 节安装的 CRD 确认)。由于错误信息未收录在插件文档的故障排除章节中,因此我在该章节中补充了原因和修复方法(method: plugin + pluginConfiguration.name, kubectl cnpg backup -m plugin)。
③ #679 评论。 针对 SeaweedFS 报告 WAL 归档因退出代码 4 而失败的问题,我留言说明使用相同的边车镜像(v0.14.0)无法复现,并提示“退出代码 4 是 barman 的通用错误,请直接查看边车日志中紧邻前方的异常行”。第 4.4 节的第 5 步正是执行该命令的地方,退出代码会被包装为 gRPC 错误一直向上 manager wal-archive 传递。
6. 实测:从小集群中读到的内容
2026-10-17,所有命令均为读取(kubectl get·logs,在运行中的 Pod 上用 exec 查看 /proc和配置文件 cat)。应用命名空间和集群名称已被隐去,原始数据单独妥善保管。
安装版本与集群。 cnpg-system 中各有一个 operator(cloudnative-pg:1.30.0)和插件(plugin-barman-cloud:v0.14.0)Deployment。共有四个 Cluster,全部为 Cluster in healthy state、PostgreSQL 18.4 镜像。
| Cluster | 实例 | 插件 | .spec.backup |
ObjectStore |
|---|---|---|---|---|
| 应用 DB 集群 A | 2 | barman-cloud(isWALArchiver: true) |
无 | 有 |
| 认证服务 DB 集群 B | 2 | barman-cloud | 无 | 有 |
| 集群 C、D | 1, 2 | 无 | 无 | 无 |
发现插件(第 4.2 节)。 插件 Service 的标签为 cnpg.io/pluginName: barman-cloud.cloudnative-pg.io,注释为
cnpg.io/pluginPort: 9090 以及服务器/客户端证书 Secret 名称两个。Operator 日志显示在过去 24 小时内有 Registered plugin 7 次。集群 A 的 .status.pluginStatus 版本为 0.14.0,能力为 TYPE_RECONCILER_HOOKS·TYPE_LIFECYCLE_SERVICE·
TYPE_INSTANCE_SIDECAR_INJECTION ,这与第 4.5 节 operator 半边的 Identity 响应完全一致。
实例 Pod(第 4.3·4.5 节)。 集群 A 的两个 Pod 配置完全相同。
| 项目 | 值 |
|---|---|
| initContainers | bootstrap-controller(Operator 镜像 1.30.0)、 plugin-barman-cloud(边车镜像 v0.14.0、 restartPolicy: Always) |
| containers | postgres 共一个) |
| postgres 容器 PID 1 | /controller/manager instance run --status-port-tls --log-level=info |
| postmaster | PID 37,父 PID 1。其下有 checkpointer、walwriter、archiver(“last was …66”)、walsender 等 |
/plugins |
套接字文件 barman-cloud.cloudnative-pg.io 一个, /proc/net/unix 显示监听 1、连接 1 |
边车不是作为 containers ,而是作为 initContainers 连入 restartPolicy: Always 的。这与第 4.5 节中的读取结果完全一致。
插件 Deployment 的调试日志中留下了注入补丁的痕迹,该日志的 caller 是 internal/cnpgi/operator/lifecycle.go:364 。
在 v0.14.0 标签中,同一文件的第 364 行正好是 "generated patch" 日志行,从而额外确认了运行中的构建版本与所读源码版本一致。
补丁内容是向 /spec/volumes 添加 plugins emptyDir、向 /spec/initContainers/1 添加边车的 JSON 补丁。
archive_command(第 4.4 节)。 读取了 PGDATA 的配置文件。
| 文件 | 值 |
|---|---|
custom.conf |
archive_command = '/controller/manager wal-archive --log-destination /controller/log/postgres.json %p' |
custom.conf |
archive_mode = 'on' |
override.conf |
restore_command = '/controller/manager wal-restore --log-destination /controller/log/postgres.json %f %p' |
Cluster 配置的 archive_timeout 为 5 分钟。在既无备份也无插件的集群 C 的 custom.conf 中也存在相同的 archive_command 和
archive_mode = 'on' 。按照第 4.4 节第 3 步所述,这些 WAL 将不会去往任何地方并以“成功”结束。
WAL 归档日志。 在边车日志中,过去 24 小时内 Archived WAL file 这 322条。如果每5分钟强制轮转一次 WAL,一天就是 288个,剩下的看起来是通过实际写入填满的切片。最近四个切片(UTC 16:06~16:21,间隔 5分钟)的耗时(elapsedWalTime)为
0.237~0.256秒。每行前面都有 Executing barman-cloud-wal-archive 和参数列表(--gzip, --endpoint-url <가림>, s3://<버킷>/、服务器名称)。相反,在 postgres 容器日志中,同一 24小时内包含 wal-archive 的行为 0条。成功的 wal-archive 属于 info
级别,不会留下任何内容,因此要查看 WAL 归档,必须查看 Sidecar 日志。
备份(第 4.6 节)。 有两个 ScheduledBackup,两者都记录了 method: plugin 和 pluginConfiguration.name 。读取已安装的 CRD 后发现,Backup 的 spec.method 默认值确实是 barmanObjectStore ,而 ObjectStore CRD 的 spec.configuration 中
!has(self.serverName) 规则依然存在(第 5 节两个 PR 的依据)。
| Cluster | 备份次数 | 结果 | 备注 |
|---|---|---|---|
| A | 31 (09-16 ~ 10-16,每天) | 全部 completed | 最近一次:03:30:00 开始,03:33:40 结束(约 3分 40秒) |
| B | 28 (首次手动 1 + 每天 27) | completed 26, failed 2 | 10-06·10-07 失败: rpc error: code = Unknown desc = exit status 1 |
B 的失败提示与第 4.4 节末尾提到的“barman CLI 退出代码被包装在 gRPC 错误字符串中的形态”完全一致。原因本身并未在本文中追究(未确认)。
状态分布在两个地方。 集群 A 的 .status.firstRecoverabilityPoint 和 .status.lastSuccessfulBackup 停留在 2026-08-23 。这是使用 in-tree 进行备份的最后一天。由于该字段在 API 类型注解中被标注为“Deprecated: 不会在备份插件中设置”,因此在迁移到插件后便不再更新。当前的值位于 ObjectStore 侧。
| 位置 | 最早可恢复时间点 | 最后一次成功备份 |
|---|---|---|
Cluster .status(in-tree 时期的值) |
2026-08-23 07:25 | 2026-08-23 07:32 |
ObjectStore .status.serverRecoveryWindow |
2026-09-16 03:33 | 2026-10-16 03:33 |
由于保留策略为 30 天,ObjectStore 侧的最早恢复时间点会在一个月前被截断。如果告警正在观察 Cluster 侧的字段,在迁移后它会静默停止更新。
不存在连接泄漏。 鉴于第 4.4 节的 archiveWALViaPlugins 等四个地方存在创建并关闭一个未使用的插件存储库的代码(第 8 节),我们观察了实际的客户端连接池是否会堆积而不关闭。在连续 26 天每天进行备份的 primary Pod 中,
/proc/net/unix 的插件套接字仅有 1 个处于等待、1 个处于连接状态。没有发现泄漏的痕迹。
7. 从哪里开始阅读
cnpg-i/proto/identity.proto和wal.proto。先看契约,双方的代码就很容易读懂。cnpg-i-machinery pkg/pluginhelper/http/server.go:152。 查看插件开放服务器的方式(TCP 对比 Socket)。plugin-barman-cloud internal/cnpgi/operator/lifecycle.go:88与common/wal.go:107。这是两个半部分各自所做的工作。cloudnative-pg internal/controller/plugin_controller.go:135→internal/cnpi/plugin/connection/connection.go:442。 这是 Operator 查找插件并读取其能力的地方。pkg/management/postgres/archiver/archiver.go:156。 in-tree 与插件路径在一个函数中分叉。internal/controller/cluster_controller.go:310。 最后阅读大循环,就能看出前面看到的各个片段是如何嵌入其中的。
8. 适合首次贡献的地方
这是在阅读源码时实际注意到的一些地方。目前尚未提交到任何地方,提交前应先检查现有的 Issue。
- Reason 名称与含义相反。
.spec.backup由于没有 , 当失败时,事件的 Reason 为ClusterHasBackupConfigured(backup_controller.go:422)。 由于告警规则可能会检查这个字符串,与其直接修改,不如先通过 Issue 询问为佳。 - 日志字段拼写错误。
startBarmanBackup在"backupNamespace"中放入了backup.Name(pkg/management/postgres/webserver/local.go:213)。 插件侧也有类似的情况。如果读取用于备份的 ObjectStore 失败,日志中会打印 用于恢复的 键 (instance/backup.go:86,instance/metrics.go:119). - 未使用的插件存储库。
plugins := repository.New(); defer plugins.Close()之后有四个不使用plugins的函数 (archiver.go:285,walrestore/cmd.go:275,plugin_backup.go:78,restore.go:1067)。 实际连接是通过在NewClient内部创建的其他存储库建立的(create.go:40,紧下方的注释中有「make che socketDir」的拼写错误)。根据第 6 节的实际测量,并不存在泄漏,因此这是一个清理性质的 PR。 - in-tree 与插件在并行归档错误处理上的差异。 注释说明,in-tree 只会返回并行上传的多个 WAL 中 请求的第一个切片的错误 (
archiver.go:264)。 而插件只要这批打包中的 任意切片 失败就会返回错误 (common/wal.go:222)。 值得确认这是否为有意为之的差异(未确认)。 - 文档:迁移后停止更新的 Cluster 状态字段。 正如第 6 节中所看到的,
firstRecoverabilityPoint·lastSuccessfulBackup在迁移当天停止更新。 在先前的指南中多加一行“现在请查看 ObjectStore 的 status”,这属于与 #1164 相同性质的文档贡献。
两个存储库的 CONTRIBUTING 都指向 CloudNativePG 的公共贡献指南(行为准则、AI 使用、工作流程)。如果获得了 AI 的帮助,在提交中使用
Assisted-by 尾注(Trailer)进行说明是该项目的做法。截至 2026-10-17,带有 good first issue 标签开启的 Issue 在主仓库中有 2 个,
在插件仓库中有 0 个。
9. 局限性
- 恢复路径(
wal-restore、恢复 Job、RestoreJobHooks)、replica 集群、卷快照(volume snapshot)备份、切换与故障转移(failover)的详细判定以及 Pooler 均未涉及。 barman-cloud-*命令本身(Python barman)以及插件使用的 Go 库(cloudnative-pg/barman-cloud)的内部未予探究。 第 6 节的Executing barman-cloud-wal-archive日志也是该库留下的行。- 集群 B 的两次备份失败、外部集群的 serverName 默认值差异在实际恢复中会如何表现,均未进行确认(未确认·未实测)。
- 单个 WAL 产生新进程和新 gRPC 连接的成本并未单独测量。第 6 节的 0.25 秒是 Sidecar 测得的上传时间(未实测)。
小 T 寄语。 PostgreSQL 只在屋内写信,放进邮箱(Socket)后,隔壁的快递员(Sidecar)就会拿出去。信封上 写的地址就是 serverName,所以搬家时如果改写了地址,就会在旧邮箱旁多出一个新邮箱!
参考
所有资料均于 2026-10-17 查阅。
- cloudnative-pg v1.30.0 源码: https://github.com/cloudnative-pg/cloudnative-pg/tree/v1.30.0
- plugin-barman-cloud v0.14.0 源码与文档: https://github.com/cloudnative-pg/plugin-barman-cloud/tree/v0.14.0, 先前指南 https://github.com/cloudnative-pg/plugin-barman-cloud/blob/v0.14.0/web/docs/migration.md, 故障排除 https://github.com/cloudnative-pg/plugin-barman-cloud/blob/v0.14.0/web/docs/troubleshooting.md
- cnpg-i v0.6.0 协议: https://github.com/cloudnative-pg/cnpg-i/tree/v0.6.0/proto, 说明 https://github.com/cloudnative-pg/cnpg-i/blob/v0.6.0/docs/protocol.md
- cnpg-i-machinery v0.4.2: https://github.com/cloudnative-pg/cnpg-i-machinery/tree/v0.4.2
- CloudNativePG 贡献指南: https://github.com/cloudnative-pg/governance/blob/main/CONTRIBUTING.md
- 作者的 PR、Issue、评论: https://github.com/cloudnative-pg/plugin-barman-cloud/pull/1164, https://github.com/cloudnative-pg/plugin-barman-cloud/pull/1166, https://github.com/cloudnative-pg/plugin-barman-cloud/issues/679
- 同系列文章: MetalLB 阅读
引用的代码属于 cloudnative-pg 和 plugin-barman-cloud,两个存储库均采用 Apache License 2.0 许可证。