TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

CloudNativePG 读书笔记:PID 1 的管理器、套接字那头的插件、一条 WAL 的旅程

状态:草稿(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 旁边的“常驻管理员”与“外包快递员”

在 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 네임스페이스 CNPG 오퍼레이터 Cluster 컨트롤러 Backup 컨트롤러 Plugin 컨트롤러 플러그인 저장소 (gRPC 연결 풀) barman-cloud operator 반쪽 Identity ReconcilerHooks OperatorLifecycle Deployment 1개 gRPC mTLS 인스턴스 파드 (DB 한 대) postgres 컨테이너 PID 1: manager (instance run) └ postgres (자식) └ wal-archive (매번) plugin-barman-cloud instance 반쪽 네이티브 사이드카 WAL · Backup Metrics 소켓 init: bootstrap-controller → manager 복사 상태 조회 /pg/status 사이드카 주입 패치 barman-cloud-* S3 호환 객체 저장소 ObjectStore CR 어디에·어떤 자격으로 소켓: 두 컨테이너가 같이 붙인 /plugins 볼륨의 유닉스 소켓. 오퍼레이터 ↔ 플러그인 Deployment 는 Service 로 찾는다.
图:Operator、插件的两个半边、实例 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 虽然很长,但顺序非常清晰。

  1. 填充默认值、确定镜像、更新插件状态、创建辅助对象(Secret、Service、PDB) (createPostgresClusterObjects, cluster_create.go:57).
  2. 插件的 pre-reconcile 钩子 (preReconcilePluginHooks, plugins.go:36)。 在这里,barman-cloud 会创建供边车(sidecar)读取 ObjectStore 和 Secret 的 Role 与 RoleBinding(第 4.5 节)。
  3. 通过 HTTPS 查询各实例的状态 (GetStatusFromInstances, pkg/management/postgres/webserver/client/remote/instance.go:183)。 如果发现有两个 primary, 则什么都不做 ,5 秒后再观察。等待返回的旧 primary 自行降级,以防止数据分叉。
  4. 处理切换(switchover/failover)、匹配 Pod 与 PVC (reconcileResources, :825).
  5. 最后处理插件的 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 文件夹

插件通过两条途径被发现。

无论通过哪种途径,连接后要做的事情都是一样的。 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

WAL 한 조각과 백업 한 번이 가는 길 ① WAL 아카이브 ② 백업(Backup CR) postgres archiver archive_command 실행 manager wal-archive PID 1 과 같은 바이너리 archiveWALViaPlugins /plugins 소켓을 훑는다 WAL.Archive (gRPC) 사이드카가 받는다 barman-cloud-wal-archive --gzip, 한 조각 약 0.25초 조건 갱신 ContinuousArchiving=True ScheduledBackup 매일 한 번 Backup 을 만든다 Backup 컨트롤러 checkPrerequisites method 가 plugin 인가? 예 → 플러그인 이름만 확인 아니오 → .spec.backup 필요 파드 안에서 exec manager backup 「이름」 Backup.Backup (gRPC) 로컬 웹서버 → 사이드카 barman-cloud-backup 끝나면 phase=completed destinationPath / serverName / wals·base serverName 은 Cluster 의 플러그인 파라미터에서 #1166 이 다룬 오류 method 생략 → 기본값 → no backup section #1164 이 다룬 자리 serverName 은 ObjectStore 가 아니라 플러그인 파라미터
图:一个 WAL 片段和一次备份的传输路径
  1. archive_command 用户无法更改。 CNPG 将此值固定为“必填项” (pkg/postgres/configuration.go:589, CnpgConfigurationSettings.MandatorySettings)。该值为 /controller/manager wal-archive --log-destination … %p 。 无论使用哪种备份方式,PostgreSQL 侧的配置只有一行且完全相同,改变的只有其后的 Go 代码。
  2. wal-archive 命令。 NewCmd, internal/cmd/manager/walarchive/cmd.go:40 从本地 Web 服务器的缓存中获取 Cluster 并调用 archiver.Run, pkg/management/postgres/archiver/archiver.go:124 。如果自己当前不是主节点(正在转换中),则会拒绝。这是为了防止两个节点同时上传相同的 WAL。 完成后,成功或失败将作为 ContinuousArchiving 条件记录在 Cluster 中。
  3. 插件优先。 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).

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. 从哪里开始阅读

  1. cnpg-i/proto/identity.proto 和 wal.proto。先看契约,双方的代码就很容易读懂。
  2. cnpg-i-machinery pkg/pluginhelper/http/server.go:152。 查看插件开放服务器的方式(TCP 对比 Socket)。
  3. plugin-barman-cloud internal/cnpgi/operator/lifecycle.go:88 与 common/wal.go:107。这是两个半部分各自所做的工作。
  4. cloudnative-pg internal/controller/plugin_controller.go:135 → internal/cnpi/plugin/connection/connection.go:442。 这是 Operator 查找插件并读取其能力的地方。
  5. pkg/management/postgres/archiver/archiver.go:156。 in-tree 与插件路径在一个函数中分叉。
  6. internal/controller/cluster_controller.go:310。 最后阅读大循环,就能看出前面看到的各个片段是如何嵌入其中的。

8. 适合首次贡献的地方

这是在阅读源码时实际注意到的一些地方。目前尚未提交到任何地方,提交前应先检查现有的 Issue。

两个存储库的 CONTRIBUTING 都指向 CloudNativePG 的公共贡献指南(行为准则、AI 使用、工作流程)。如果获得了 AI 的帮助,在提交中使用 Assisted-by 尾注(Trailer)进行说明是该项目的做法。截至 2026-10-17,带有 good first issue 标签开启的 Issue 在主仓库中有 2 个, 在插件仓库中有 0 个。

9. 局限性

小 T 寄语。 PostgreSQL 只在屋内写信,放进邮箱(Socket)后,隔壁的快递员(Sidecar)就会拿出去。信封上 写的地址就是 serverName,所以搬家时如果改写了地址,就会在旧邮箱旁多出一个新邮箱!

参考

所有资料均于 2026-10-17 查阅。

引用的代码属于 cloudnative-pg 和 plugin-barman-cloud,两个存储库均采用 Apache License 2.0 许可证。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论