PAYATHON 2026

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 小版本、读取循环和最后一次持久化确认,暂不要直接增加服务端定时推进任务。

为什么设置状态间隔仍可能没有效果

位置反馈涉及三个动作:

  1. 服务端解码 WAL,并通过数据消息或 keepalive 告知位置。
  2. 客户端处理数据,更新自己愿意确认的 flushed LSN。
  3. 客户端发送 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、确认安全位置和发送反馈的流程,无需心跳表,也不会新增需要下游过滤的行变更事件。

备注:内容仅供参考。