PostgreSQL码农集散地

稳如老狗:PG 19 停库不再死等 WAL Sender

PG 19 预览 - WAL Sender 关闭超时:复制环境下的优雅关闭

本文档解读 Fujii Masao 提交的 wal_sender_shutdown_timeout GUC,该参数限制 walsender 在关闭期间等待复制的最长时间,解决逻辑复制场景下关闭延迟的问题。

相关 Commit

Commit
描述
a8f45dee9176
Add wal_sender_shutdown_timeout GUC to limit shutdown wait for replication
21b018e7eab5
Reduce log level of some logical decoding messages from LOG to DEBUG1
57706799186a
Remove redundant SetLatch() calls in interrupt handling functions
2497dac55648
Fix FK triggers losing DEFERRABLE/INITIALLY DEFERRED when marked ENFORCED again

核心价值

  • 可控关闭时间:设置最大等待时间,避免无限等待
  • 解决痛点:逻辑复制 apply worker 阻塞时,walsender 不再无限等待
  • 可配置:默认 -1(禁用),可以设为毫秒级超时

原理详解

1. GUC 定义

1.1 变量定义 (src/backend/replication/walsender.c line 146-148)

/* 最大等待时间(毫秒),-1 表示禁用 */
int wal_sender_shutdown_timeout = -1;

1.2 GUC 元数据 (src/backend/utils/misc/guc_parameters.dat line 3507-3515)

wal_sender_shutdown_timeout
    GUC_NAME
    wal_sender_shutdown_timeout
    GUC_CONTEXT  SUGGEST_CONTEXT
    vartype      INT
    min          -1
    max          INT_MAX / 1000
    unit         ms
    boot_val     -1
    range_val    -1, 2147483
    group        CONNECTIONS_AND_AUTH / REPLICATION
    short_desc   "Sets the maximum time to wait for replication to finish during shutdown."

1.3 外部声明 (src/include/replication/walsender.h line 36)

extern PGDLLIMPORT int wal_sender_shutdown_timeout;

2. 核心检查函数

2.1 WalSndCheckShutdownTimeout() (line 2964-2996)

staticvoid
WalSndCheckShutdownTimeout(void)
{
    TimestampTz now = GetCurrentTimestamp();

/* 如果没有请求关闭,直接返回 */
if (shutdown_request_timestamp == 0)
return;

/* 如果已超过超时时间 */
if (TimestampDifferenceExceeds(shutdown_request_timestamp,
                                  now,
                                  wal_sender_shutdown_timeout))
    {
/* 执行强制关闭 */
        WalSndDoneImmediate();
    }
}

2.2 WalSndComputeSleeptime() (line 2876-2919)

计算 sleep 时间,当有关闭请求且设置了超时时:

staticlong
WalSndComputeSleeptime(TimestampTz now)
{
    TimestampTz wakeup_time;
long sleeptime = 10000;  /* 默认 10 秒 */

if (wal_sender_timeout > 0 && last_reply_timestamp > 0)
    {
/* 基于 wal_sender_timeout 计算唤醒时间 */
        wakeup_time = TimestampTzPlusMilliseconds(last_reply_timestamp,
                                                  wal_sender_timeout);

/* 如果还没有发送 ping,在超时一半时唤醒 */
if (!waiting_for_ping_response)
            wakeup_time = TimestampTzPlusMilliseconds(last_reply_timestamp,
                                                      wal_sender_timeout / 2);

/* 计算相对唤醒时间 */
        sleeptime = TimestampDifferenceMilliseconds(now, wakeup_time);
    }

if (shutdown_request_timestamp != 0 && wal_sender_shutdown_timeout > 0)
    {
long shutdown_sleeptime;

        wakeup_time = TimestampTzPlusMilliseconds(shutdown_request_timestamp,
                                                  wal_sender_shutdown_timeout);

        shutdown_sleeptime = TimestampDifferenceMilliseconds(now, wakeup_time);

/* 选择最早的唤醒时间 */
if (shutdown_sleeptime < sleeptime)
            sleeptime = shutdown_sleeptime;
    }

return sleeptime;
}

2.3 WalSndDoneImmediate() (line 3710-3748) - 强制关闭实现

staticvoid
WalSndDoneImmediate(void)
{
    WalSndState state = WalSnd->state;

/* 只对活动状态执行强制关闭 */
if (state == WALSNDSTATE_CATCHUP ||
        state == WALSNDSTATE_STREAMING ||
        state == WALSNDSTATE_STOPPING)
    {
/* 发送 CopyDone 命令 */
        EndCommand("CopyDone", DestReceiver);

/* 尝试刷新输出,但不阻塞 */
        pq_flush_if_writable();

/* 记录警告日志 */
        ereport(WARNING,
            (errmsg("terminating walsender due to replication shutdown timeout"),
             errdetail("Walsender process might have been terminated before "
"all WAL data was replicated to the receiver.")));

/* 标记不再发送输出 */
        whereToSendOutput = DestNone;

/* 立即退出进程 */
        proc_exit(0);
    }
}

3. 调用时机

WalSndCheckShutdownTimeout() 在以下位置被调用:

位置
行号
场景
WalSndSendFunctions()
1689
在发送函数中定期检查
WalSndReceive
2011
接收 WAL 数据时检查
WalSndLastCycle()
3090
主循环每次迭代时检查

4. 关闭流程

1. Postmaster 发送 SIGUSR2 信号
   └── got_SIGUSR2 = true (line 3885)

2. WalSndCheckShutdownTimeout() 首次被调用
   └── 记录 shutdown_request_timestamp

3. 后续每次调用检查是否超时
   └── TimestampDifferenceExceeds() 比较时间差

4. 超时后调用 WalSndDoneImmediate()
   └── 发送 CopyDone
   └── 记录警告日志
   └── 设置 whereToSendOutput = DestNone
   └── proc_exit(0)

5. 传统行为 vs 新行为对比

传统行为(问题场景)

时间线:
T=0:    管理员执行 pg_ctl stop
T=0:    Postmaster 向所有 walsender 发送 SIGUSR2
T=0:    walsender 开始等待复制完成
T=1:    apply worker 获取排他锁被阻塞
T=30:   管理员等待 30 秒...
T=60:   管理员等待 60 秒...
T=300:  apply worker 仍未释放锁,关闭卡住
        (如果设置了 statement_timeout,可能永远不会释放)

新行为(wal_sender_shutdown_timeout=30)

时间线:
T=0:    管理员执行 pg_ctl stop
T=0:    Postmaster 向所有 walsender 发送 SIGUSR2
T=0:    walsender 开始等待,最长 30 秒
T=1:    apply worker 获取排他锁被阻塞
T=30:   WalSndCheckShutdownTimeout() 检测到超时
T=30:   WalSndDoneImmediate() 执行强制关闭
T=30:   记录警告日志,walsender 退出
T=31:   Postmaster 继续关闭流程

使用实践

1. 配置 wal_sender_shutdown_timeout

在 postgresql.conf 中设置:

# 推荐值:30 秒(对于大多数场景足够)
wal_sender_shutdown_timeout = 30s

# 保守值:60 秒
# wal_sender_shutdown_timeout = 60s

# 禁用(默认行为,无限等待)
# wal_sender_shutdown_timeout = -1

2. 查看当前配置

-- 查看 wal_sender_timeout 和 wal_sender_shutdown_timeout
SHOW wal_sender_shutdown_timeout;
SHOW wal_sender_timeout;

-- 查看 walsender 状态
SELECT
    pid,
    state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn,
    sync_state
FROM pg_stat_replication;

3. 监控关闭过程

-- 查看正在进行的 walsender 进程
SELECT
    pid,
    usename,
    application_name,
    state,
    query_start,
query
FROM pg_stat_activity
WHERE application_name LIKE'%walsender%';

-- 查看 slot 状态
SELECT
    slot_name,
plugin,
    slot_type,
    active,
    restart_lsn
FROM pg_replication_slots;

4. 物理复制 vs 逻辑复制

物理复制(通常不需要设置)

# 物理复制通常是同步的,walsender 等待确认
# 关闭时很少出现长时间等待
wal_sender_shutdown_timeout = -1  # 默认

逻辑复制(推荐设置)

# 逻辑复制 apply worker 可能被长时间运行的语句阻塞
wal_sender_shutdown_timeout = 30s

5. 高可用场景下的设置

场景 1:主库切换

# 主库切换时,确保 walsender 不会无限等待
wal_sender_shutdown_timeout = 60s

# 同时设置合理的 wal_sender_timeout
wal_sender_timeout = 60s

场景 2:级联复制

# 级联复制中,中间节点可能需要更长时间
wal_sender_shutdown_timeout = 120s

6. 日志分析

当发生超时关闭时,日志会记录:

WARNING:  terminating walsender process due to replication shutdown timeout
DETAIL:  Walsender process might have been terminated before all WAL data was replicated to the receiver.

处理建议:

  1. 检查 apply worker 阻塞原因
  2. 考虑增加超时时间
  3. 检查复制槽状态
  4. 评估数据一致性风险

7. 与其他参数的关系

参数
用途
建议关系
wal_sender_timeout
walsender 空闲超时
应 <= wal_sender_shutdown_timeout
wal_sender_shutdown_timeout
关闭时等待超时
设置为合理的关闭时间
wal_keep_size
保留的 WAL 空间
确保有足够空间保留未复制数据
max_replication_slots
复制槽数量
确保足够支持所有订阅

8. 故障排除

问题:关闭时复制数据丢失

原因:超时设置过短,WAL 还在等待复制时被强制关闭

解决:

  1. 增加 wal_sender_shutdown_timeout
  2. 检查 apply worker 阻塞原因
  3. 确保 wal_keep_size 足够大
-- 检查复制延迟
SELECT
    slot_name,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;

问题:walsender 一直不退出

原因:wal_sender_shutdown_timeout = -1(禁用)

解决:设置合理的超时值

wal_sender_shutdown_timeout = 60s

总结

wal_sender_shutdown_timeout 是复制环境下的重要改进,解决了逻辑复制场景下关闭延迟的痛点。

关键要点:

  1. 解决问题:apply worker 阻塞导致 walsender 无限等待
  2. 推荐值:30-60 秒对大多数场景足够
  3. 物理复制:通常不需要设置,保持默认 -1
  4. 逻辑复制:建议设置,避免关闭卡住
  5. 数据安全:超时后会记录警告,可能丢失未复制的 WAL 数据