PostgreSQL 18 如何不用心跳表推进 confirmed_flush_lsn?
结论
可以不使用心跳表,利用复制协议已有的 keepalive 和客户端确认反馈推进 confirmed_flush_lsn。消费端需要持续读取复制连接,并且只确认已经安全处理的位置。
无需调整 proto_version=4。PostgreSQL 18.2 的逻辑 WAL sender 会随着解码推进更新发送位置。即使相关 WAL 没有产生 publication 内的变更消息,keepalive 也可以携带这个位置。这类协议消息不会成为 Kafka 中的行变更事件。PostgreSQL 18.2 WAL sender 源码
现有信息不足以确定停滞发生在哪一环。应先检查实际加载的 pgJDBC 小版本、读取循环和最后一次持久化确认,暂不要直接增加服务端定时推进任务。
为什么设置状态间隔仍可能没有效果
位置反馈涉及三个动作:
- 服务端解码 WAL,并通过数据消息或 keepalive 告知位置。
- 客户端处理数据,更新自己愿意确认的 flushed LSN。
- 客户端发送 standby status update,将该位置反馈给服务端。
状态间隔主要控制第三步。周期性发送旧的 flushed LSN,不会产生新的消费进度。协议中的 keepalive 使用消息类型 k,standby status update 使用 r;它们与 pgoutput 的 BEGIN / COMMIT 属于不同层次。Streaming Replication Protocol
write_lsn、flush_lsn 是 pg_stat_replication 的字段,pg_replication_slots 中需要观察的是 confirmed_flush_lsn 和 restart_lsn。对于逻辑复制槽,服务端处理反馈中的 flush 位置时会调用 LogicalConfirmReceivedLocation()。因此,如果同一连接上更大的 flush_lsn 已被服务端接受,而 confirmed_flush_lsn 长期不变,应核对查询关联的槽、连接和采样时刻。PostgreSQL 18.2 反馈处理源码
先检查 pgJDBC 的 keepalive 处理
pgJDBC 在 read() 或 readPending() 内部处理 keepalive,不会把它作为普通 pgoutput payload 返回。因此,readPending() 返回 null,不代表这次调用没有处理协议消息。
在已核对的 42.7.6 实现中,收到 keepalive 后,驱动会更新接收位置。启用 automaticFlush 时,如果最后一个 XLogData 的位置已经通过反馈确认,驱动还可以继续推进 flushed LSN。forceUpdateStatus() 会立即发送当前状态,并请求服务端回复。pgJDBC 42.7.6 实现
检查以下事项:
- 即使长期没有业务事件,消费线程是否仍持续调用 read() 或 readPending()。
- Kafka 写入完成后,是否调用 setFlushedLSN() 更新位置,并实际发送反馈。
- 是否只在 payload 非 null 的分支中维护状态,导致空闲期间缺少主动反馈或诊断。
- 是否在每轮循环中把驱动已经推进的 flushed LSN 重新设置成旧业务事件的位置。
- 实际运行的 JAR 是哪个 42.7.x 小版本,是否关闭了 automaticFlush。
withAutomaticFlush(boolean) 开关是在 42.7.6 中加入的,不能假定所有 42.7.x 都提供该方法。pgJDBC 变更记录
消费循环如何调整
以下示例持续读取复制连接,按持久化进度确认,并定期主动请求反馈。示例使用支持 withAutomaticFlush() 的驱动版本;5 秒和 20 毫秒只是示例值。
PGReplicationStream stream = pgConnection.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName("my_slot")
.withSlotOption("proto_version", 4)
.withSlotOption("publication_names", "my_publication")
.withAutomaticFlush(true)
.withStatusInterval(5, TimeUnit.SECONDS)
.start();
long lastFeedback = System.nanoTime();
while (!Thread.currentThread().isInterrupted()) {
ByteBuffer payload = stream.readPending();
if (payload != null) {
// 应用自行实现:解码并处理 pgoutput。
// 只有事务所需的 Kafka 写入及恢复状态均已持久化,
// 才返回可以安全确认的事务结束 LSN;否则返回 null。
LogSequenceNumber durableCommitLsn =
processAndGetDurableCommitLSN(payload);
if (durableCommitLsn != null) {
stream.setFlushedLSN(durableCommitLsn);
stream.forceUpdateStatus();
lastFeedback = System.nanoTime();
}
}
if (System.nanoTime() - lastFeedback
>= TimeUnit.SECONDS.toNanos(5)) {
stream.forceUpdateStatus();
lastFeedback = System.nanoTime();
}
if (payload == null) {
Thread.sleep(20);
}
}
processAndGetDurableCommitLSN() 是应用层占位方法,不属于 pgJDBC API。它需要处理事务边界,并按应用的恢复语义返回安全位置。不能仅凭 Kafka send() 返回、消息进入内存队列或单条 payload 处理完成,就认定事务已经持久化。
定期调用 forceUpdateStatus() 可以请求服务端回复;随后仍要继续读取,才能处理返回的 keepalive。仅重复调用 forceUpdateStatus() 而不读取连接,无法完成这条反馈链路。
对于异步 Kafka 发送、并行处理或启用 streaming 的消费端,还要审查 automaticFlush 与应用确认策略是否一致。驱动按复制消息位置判断进度,并不了解 Kafka 中尚未完成的工作。需要完全由应用控制时,可以使用 withAutomaticFlush(false),但随后必须自行维护安全确认位置,不能无条件执行 setFlushedLSN(getLastReceiveLSN())。
如何确认问题发生在哪一环
在普通 SQL 连接中查询,将 my_slot 替换为实际槽名:
SELECT
s.slot_name,
s.active,
s.active_pid,
s.confirmed_flush_lsn,
s.restart_lsn,
r.sent_lsn,
r.write_lsn,
r.flush_lsn,
r.replay_lsn,
r.reply_time,
pg_wal_lsn_diff(
pg_current_wal_lsn(), s.restart_lsn
) AS retained_wal_bytes
FROM pg_replication_slots AS s
LEFT JOIN pg_stat_replication AS r
ON r.pid = s.active_pid
WHERE s.slot_name = 'my_slot';
同时记录客户端的 getLastReceiveLSN()、getLastFlushedLSN()、最后完成的事务位置及反馈时间。在只有 publication 外事务的测试负载下,观察服务端发送位置、客户端接收位置和确认位置是否分别发生变化。
如果 confirmed_flush_lsn 已经推进,而 WAL 仍持续积累,需要继续检查 restart_lsn。它表示复制槽仍可能需要的最早 WAL;长事务等因素可能使它落后于确认位置。上述 retained_wal_bytes 是位置差,不是磁盘目录大小,也不保证全部 WAL 都由这个槽单独保留。pg_replication_slots 字段说明
不要从另一个 SQL 连接查询 pg_current_wal_lsn() 后,直接把结果传给 setFlushedLSN()。数据库当前 WAL 位置可能超过该消费端实际读取和持久化的位置。
是否有纯服务端的定时推进机制
pgoutput 没有提供这样的选项:让服务端在不了解下游持久化状态的情况下,自动把活跃复制槽安全确认到当前 WAL 位置。proto_version 控制协议能力,也没有提供这种确认机制。pgoutput 参数说明
pg_replication_slot_advance() 能够推进复制槽,但它不能与正在占用同一槽的消费者并发使用,也不验证 Kafka 是否已经保存相关事件。将它定期推进到当前 WAL 位置,可能跳过尚未消费的数据。只有停用消费连接,并明确确认目标范围可以跳过或已被可靠处理时,才适合考虑这类维护操作。复制槽管理函数
持续运行的消费端应优先修通读取 keepalive、确认安全位置和发送反馈的流程,无需心跳表,也不会新增需要下游过滤的行变更事件。
备注:内容仅供参考。
