← 中文资源

简体中文版本

将 PostgreSQL 只读查询安全路由到副本

用显式一致性规则、请求级主库固定和复制延迟保护,把分析与目录读取分流到副本,避免陈旧数据进入用户可见路径。

阅读英文原文

概述

副本是可能落后几毫秒的只读拷贝,适合目录页、报表和搜索建议,不适合结算金额、授权判断或用户刚写完就要读到的数据。路由层必须把能力写清楚:readQuery 默认走副本,writeQuery 和事务始终走主库,pinToPrimary 表示当前请求已经跨过写边界。复制延迟保护每五秒采样一次主库的 pg_stat_replication,同时检查 replay_lag 和 WAL 字节差;副本断流或落后时回退主库。

步骤

先确认拓扑:

psql "$DATABASE_URL_PRIMARY" -Atc "SELECT pg_is_in_recovery();"
psql "$DATABASE_URL_REPLICA" -Atc "SELECT pg_is_in_recovery();"
psql "$DATABASE_URL_PRIMARY" -c "SELECT application_name, state, replay_lag, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind FROM pg_stat_replication;"

主库应返回 f,副本返回 t,复制行处于 streaming。创建两个连接池,副本连接设置 application_name: 'app-replica-1',并用 AsyncLocalStorage 保存请求状态。withRequestRouting(next) 在请求入口建立状态,pinToPrimary() 在写操作成功后置位。readQuery 若发现已固定或延迟保护判定副本不新鲜,就使用主库。

延迟查询使用:

SELECT EXTRACT(EPOCH FROM replay_lag) * 1000 AS lag_ms,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind
FROM pg_stat_replication
WHERE application_name = $1 AND state = 'streaming';

把结果缓存五秒;没有复制行、查询失败、延迟超过两秒或落后超过 16 MB 时都回退主库。写事务通过 primary.connect() 获取客户端,绝不复用副本连接。

验证

在 app.profiles 中插入一个测试用户,向其发送 PATCH 更新 display_name,并在同一请求内立即读回。响应必须已经是新值,因为 pinToPrimary() 强制后续读取走主库。再直接查询副本,复制追上后也应显示新值。若副本持续落后,先检查网络吞吐和长查询,不要直接调低阈值。

注意事项

迁移、序列分配和 advisory lock 必须留在主库。不要向副本发送 SET 后再假设后续查询会落到同一连接。为副本池单独设置上限,避免分析流量耗尽普通页面读取。请求级固定无法保证跨服务、跨设备的写后读一致性;需要时使用主库读取或传递版本令牌。副本不是备份,也不是自动故障转移方案。