Skip to content

读写分离架构 ​

随着业务增长,数据库的读请求往往远多于写请求——典型互联网业务的读写比在 10:1 甚至更高。流复制让备库与主库保持近乎实时的一致,备库又支持只读查询(hot standby),于是水到渠成的优化出现了:把写流量发给主库,把读流量分散给一台或多台备库。这就是读写分离(read-write splitting / read/write split)。

读写分离是 PostgreSQL 高可用体系里「性价比最高」的横向扩展手段:它不需要分库分表,不需要应用大改,备库本来就是高可用架构的既有组件,把读流量导过去就是纯增量收益。但它也有一个绕不开的代价:复制延迟导致的一致性问题——刚写完的数据,在备库上可能还读不到。这个问题的解法决定了读写分离的架构形态与适用边界。

本页讲清读写分离的动机、三种架构模式(应用层直连、代理层、驱动层)、一致性挑战与保障手段、流量分配策略、与连接池的配合、延迟监控与降级,以及常见的坑与选型建议。备库只读查询的底层机制(查询冲突、参数调优)在下一页展开,中间件的选型与配置在第三页展开。

前置知识

建议先阅读:repmgr 自动故障转移

基础概念 ​

读写分离横跨「复制」「连接」「路由」三个领域,先把这些概念对齐。

概念英文说明
读写分离read-write splitting写走主库、读走备库的流量分配方式
复制延迟replication lag备库落后主库的时间 / 字节数
read-your-writes读己之写用户应能立刻读到刚提交的自己的写入
会话粘滞session stickiness同一会话的请求固定路由到同一节点
写后读主read-after-write routing写操作后的一段时间内,该用户的读强制走主库
LSN 等待wait for LSN应用等到备库重放到指定 LSN 再读
remote_apply同步复制等级主库等备库重放完成才返回提交成功
负载均衡load balancing多备库之间的读流量分配
降级fallback / degrade备库不可用或延迟过大时,读流量回退到主库
只读端点read endpoint面向读流量的统一入口(VIP/域名/端口)

读写分离的收益边界 ​

读写分离扩展的是读吞吐,不是写吞吐。这个认知决定了它的适用边界:

  • 收益:主库 CPU / IO 负载下降(读请求被剥离);总查询吞吐随备库数量近线性增长;主库的资源可以更专注于写入与复杂事务;
  • 无收益:写入吞吐不变(写只有主库能做);单条查询的延迟不变(除非用并行查询,那是另一回事);
  • 有代价:引入复制延迟与一致性问题;备库数量越多,主库的 walsender 与网络压力越大;运维复杂度上升。

判断「值不值得做」的核心问题是:读压力是否真的挤占了写能力? 如果主库的瓶颈在写入或锁竞争,读写分离帮不上忙,要考虑的是写入优化、分区或分片。

复制延迟的三种度量 ​

一致性方案都建立在「先量化延迟」之上,三种度量口径各有用途:

口径获取方式含义
字节差主库 pg_stat_replication 的 LSN 差值备库还差多少 WAL
时间差备库 now() - pg_last_xact_replay_timestamp()最后重放的事务距今多久
段落差receive - replay 的 LSN 差已收到但未重放的量(应用堆积)

注意时间差口径在写入稀疏的时段会「虚高」(没有新事务可重放,最后事务时间停在很久以前),需要结合字节差一起解读。

hot standby:一切的前提 ​

读写分离能成立,前提是备库可查询——这就是 hot standby(热备):备库在持续重放 WAL 的同时,对外提供只读查询。PostgreSQL 10 以后 hot_standby 默认开启。但「可查询」不等于「无限可查询」:长查询会与 WAL 重放产生冲突,可能被强制取消;备库上的序列、锁、事务控制都有专门的行为。这些细节是下一页的主题。

语法与用法 ​

读写分离的「语法」主要是三类:路由判断用的 SQL、一致性等待用的 SQL、以及同步复制等级的配置。

判断当前节点角色 ​

sql
-- 应用启动或重连后判断连到的是主库还是备库
SELECT pg_is_in_recovery();
text
 pg_is_in_recovery
-------------------
 f
(1 row)

f = 主库,t = 备库。这是所有「应用层直连」方案的路由依据。

量化复制延迟 ​

sql
-- 主库上:每个备库的字节级延迟
SELECT application_name,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes
FROM pg_stat_replication;
text
 application_name | replay_lag_bytes
------------------+------------------
 standby1         |                0
 standby2         |            16432
(2 rows)
sql
-- 备库上:时间级延迟
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;
text
   replay_delay
------------------
 00:00:00.318412
(1 row)

写后等待备库追上(PG17+) ​

PostgreSQL 17 起提供了在备库上等待重放到指定 LSN 的函数,这是实现「LSN 等待」一致性方案的原生工具:

sql
-- 主库:写入并记下提交时的 WAL 位置(写在事务内查询)
INSERT INTO orders(user_id, amount) VALUES (42, 199.00);
SELECT pg_current_wal_lsn() AS written_lsn;
text
 written_lsn
-------------
 0/3000C100
(1 row)
sql
-- 备库:等待重放追上该位置(可带超时),超时返回警告
SELECT pg_wal_replay_wait('0/3000C100'::pg_lsn);

更早版本没有这个函数,等价做法是轮询 pg_last_wal_replay_lsn() 直到大于目标 LSN,或短暂休眠。

同步复制等级配置 ​

conf
# postgresql.conf —— 用 remote_apply 消除「重放滞后」造成的一致性问题
synchronous_commit = remote_apply
synchronous_standby_names = 'FIRST 1 (standby1)'

remote_apply 让主库等备库重放完成才返回提交成功,代价是每次提交多一个网络往返加重放时间,且备库故障时写入阻塞。等级的完整对比见同步复制等级。

详细说明 ​

三种架构模式 ​

读写分离有三种落地形态,复杂度与侵入度递增:

text
模式一:应用层直连(应用自己路由)
┌──────────┐  写   ┌──────┐
│  应用     │─────►│ 主库  │
│ (读写路由) │  读  └──────┘
│          │─────►┌──────┐
└──────────┘      │ 备库×N │
                  └──────┘

模式二:代理层(中间件路由)
┌──────────┐      ┌────────────┐  写  ┌──────┐
│  应用     │─────►│  代理/中间件  │────►│ 主库  │
└──────────┘  统一 │ (Pgpool-II  │  读  └──────┘
                   │  /自研网关)  │────►┌──────┐
                   └────────────┘     │ 备库×N │
                                      └──────┘

模式三:驱动/框架层(ORM 或数据源路由)
应用 → 框架读写路由(Laravel read/write、Sharding-JDBC 等) → 主库/备库
模式代表实现优点缺点
应用层直连自研路由、配置中心无额外组件、逻辑可控每个应用都要实现一遍
代理层Pgpool-II、自研网关应用零改动、统一管控多一层故障域与延迟、SQL 解析限制
驱动/框架层Laravel 读写连接、ShardingSphere应用侧声明式配置绑定技术栈、跨语言无法复用

没有哪种模式绝对占优:组件数量越少越稳,但统一管控能力越弱。选型参考第三页的对比。

read-your-writes:核心一致性挑战 ​

复制延迟带来的问题有一个精确的名字:read-your-writes(读己之写)。用户提交订单后立刻查看订单列表,如果查看请求被路由到延迟 200ms 的备库,看到的是没有新订单的旧列表——用户以为提交失败,再点一次提交,重复数据就产生了。

需要注意的是,这不是「所有读都不可信」,而是「写之后紧接着的读」不可信。读流量中真正踩中这个窗口的只是少数,因此工程上追求的不是全局强一致(那是同步复制的领域),而是把踩中窗口的读识别出来,路由到安全的地方。

识别「写后读」有两种典型模式:

  1. 会话级:同一个用户会话里,写过之后的后续读都走主库,直到某个条件(超时、离开页面)解除;
  2. 事务级:事务内一旦发生写,整个事务剩余部分全部走主库——这是最基本也最重要的规则,因为事务内的读写混用跨节点在语义上几乎总是错的(见下文常见坑)。

一致性保障手段 ​

按实现成本从低到高,四种主流手段:

手段原理成本一致性强度
会话粘滞用户会话固定路由(写后会话暂走主库)低(路由层记忆)会话内一致
写后读主写操作后给该用户/连接打标记,T 秒内读走主低时间窗口内一致
remote_apply备库重放完成才算提交成功中(提交延迟+可用性代价)全局一致(读任意节点都可见)
LSN 等待写后记下 LSN,读前在备库等待重放到该 LSN中(需要应用配合或 PG17+)单次读写一致

工程上的常见组合是「默认读备库 + 关键路径写后读主 / LSN 等待」:绝大多数读(列表页、报表、缓存回源)容忍秒级延迟走备库;提交后的跳转、支付结果页等关键读走主库或等待追平。

LSN 等待方案的完整闭环:

text
1. 应用在主库执行写事务
2. 事务提交时取得 pg_current_wal_lsn()(或用 PG17+ 的事务级信息)
3. 应用带着该 LSN 发起下一次读请求
4. 备库上执行 pg_wal_replay_wait('目标LSN') 或轮询 pg_last_wal_replay_lsn()
5. 追上后执行读查询;超时则回退主库读

复制延迟的成因分解 ​

一致性方案的设计必须建立在「延迟从哪来」的分解之上。一次提交从主库到备库可读,要经过五段:

text
① 提交与发送      主库事务提交 → walsender 把 WAL 发出
② 网络传输        TCP 往返、带宽、丢包重传
③ 接收与落盘      walreceiver 写入并 fsync 本地 pg_wal
④ 重放            startup 进程按序应用 WAL 到数据文件
⑤ 可见            新事务的快照包含该变更,读请求可见
段常见瓶颈治理方向
① 发送wal_sender_timeout 误判、大事务拖延监控 write_lag
② 网络带宽不足(跨机房常见)、丢包压缩、级联、专线
③ 落盘备库 pg_wal 盘慢(synchronous_commit 远端等待在此发生)提升备库盘性能
④ 重放单进程重放、大事务、锁等待监控 replay_lag;避免备库上的长锁
⑤ 可见本页讨论的一致性窗口路由策略与 LSN 等待

读写分离视角的两个关键结论:④ 重放是延迟堆积的主段(单进程按序重放,追不上持续写入时会持续落后);② ③ 决定延迟的下限(物理规律,无法靠配置消除)。write_lag / flush_lag / replay_lag 三个监控字段正对应这几段的耗时度量,含义详见流复制监控。

典型架构图 ​

把上一节的模式组合成生产上常见的完整形态:

text
                    ┌──────────────────────────────┐
                    │      应用集群 / API 服务        │
                    │  (或框架层读写路由)             │
                    └──────┬────────────────┬──────┘
                     写连接 │                │ 读连接
                    ┌──────┴──────┐  ┌──────┴───────────┐
                    │  写端点       │  │   读端点           │
                    │ (主库VIP/域名) │  │ (读VIP/DNS轮询)    │
                    └──────┬──────┘  └──────┬───────────┘
                           │          ┌─────┼─────┐
                    ┌──────┴──────┐   │     │     │
                    │    主库      │ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐
                    │  10.0.0.10  │ │备1 │ │备2 │ │备3 │
                    └──────┬──────┘ └───┘ └───┘ └───┘
                           │  流复制 (WAL)
                           └──────────────────────►

要点:

  • 写端点与读端点是两个独立的「稳定地址」,切换与扩容都发生在端点背后;
  • 主库故障时,写端点漂移到新主库(高可用层负责),读端点把故障备库摘除即可,两件事互不阻塞;
  • 备库越多,读端点的负载均衡压力越大,这也是很多团队选择代理层方案的原因。

流量分配策略 ​

决定「一条 SQL 去哪」的策略,按粒度递增:

策略说明适用
按连接连接建立时指定目标(两个数据源)最简单,写后读由应用保证
按事务事务一律走主库,自动提交的只读语句走备库通用默认策略
按语句解析 SQL:写语句/SELECT ... FOR UPDATE 走主库,纯 SELECT 走备库代理层方案的核心能力
按业务声明式指定:报表模块读备库,交易模块全走主库业务可清晰切分时
按比例读流量按比例分配到备库(考虑备库规格差异)备库规格不均时

按语句解析有明确的局限:只看 SQL 文本无法知道语义。例如 SELECT nextval(...) 是只读语句但不能在备库执行;带副作用的函数(pg_sleep 无所谓,但自定义的写表函数在 SELECT 里调用)会让纯文本路由出错。所以代理层方案必须保留「强制走主库」的逃生通道(注释 hint、白名单)。

备库数量与负载均衡 ​

备库数量取决于读吞吐需求与容错要求:

  • 一主一备:读写分离收益有限(只是分走一半读),但故障转移的最小配置;
  • 一主两备:常见起点——一台承接读流量,另一台做热备与容错,读流量与高可用互不挤占;
  • 一主 N 备:读扩展为主;要考虑主库的 walsender 上限(max_wal_senders)、级联复制(下游备库从备库拉 WAL,减轻主库压力)与归档带宽。

多备库负载均衡的注意点:

  1. 备库规格不同时要加权分配(16GB 备库和 64GB 备库均分流量是常见事故);
  2. 备库故障要自动摘除并告警,读端点绝不能把流量发给失联节点;
  3. 延迟差异大的备库做分层:低延迟备库服务实时性敏感的读,高延迟备库服务报表类读;
  4. 级联复制的下游备库延迟天然更高一档,适合放报表与分析类流量。

与连接池的配合 ​

连接池是读写分离架构里的常客,但连接池不等于读写分离——它解决连接复用,不解决路由。两者的正确组合方式:

text
应用 → 应用内连接池(写池 + 读池分开) → 主库 / 备库
应用 → PgBouncer(每池一个后端指向)   → 主库 / 备库
应用 → Pgpool-II(既是池也是路由)    → 主库 / 备库

关键规则:

  1. 写池与读池必须分开,不能共用(否则「会话粘滞」「事务一致性」全部失效);
  2. PgBouncer 是纯连接池,不做读写路由——应用连 PgBouncer 时要开两个端口/两个池分别指向主库与备库;
  3. 事务内的连接复用要以事务边界为准:事务开始到提交之间,连接不能跨节点(框架层的事务粘滞就是保证这一点);
  4. 连接池的 server_reset_query 等重置机制要在主备两端行为一致,避免会话状态(如 SET 过的参数)串库。

延迟监控与自动降级 ​

读写分离体系必须回答「备库延迟多大时不能再读它」。标准做法是给读路由加延迟熔断:

text
延迟熔断的参考逻辑:
1. 采集:每个备库的 replay_lag_bytes 与 replay_delay
2. 阈值:设定字节阈值与时间阈值(如 1MB / 5s,按业务敏感度定)
3. 动作:超阈值 → 该备库从读端点摘除 → 读流量回主库(或剩余健康备库)
4. 恢复:延迟回落后自动(或人工确认)重新加入
5. 告警:熔断动作本身必须告警——它意味着读能力降级

监控查询组合(主库视角):

sql
SELECT application_name, state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes,
       write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
text
 application_name |   state   | lag_bytes | write_lag  | flush_lag  | replay_lag
------------------+-----------+-----------+------------+------------+------------
 standby1         | streaming |         0 | 00:00:00.0004 | 00:00:00.0012 | 00:00:00.0035
 standby2         | streaming |     82432 | 00:00:00.9 | 00:00:00.9 | 00:00:01.2
(2 rows)

standby2 的延迟接近 1 秒、累积 80KB,如果业务阈值是 1 秒,监控应在它跨线前预警而不是等熔断发生后才看见。

常见坑 ​

读写分离的坑大多与「SQL 语义 vs 物理复制」的错配有关,逐个点名:

坑现象根源与对策
事务内读写混用事务内写了但读不到事务必须整体走主库;框架层事务粘滞兜底
临时表备库上会话相关行为与主库预期不一致临时表操作留在主库连接里
序列 nextval()备库报错无法执行序列取值是写操作,必须走主库(详见下一页)
SELECT ... FOR UPDATE备库报错或行为异常行锁是写意图,必须走主库
DDL备库报错只读DDL 是写操作,走主库
大查询拖垮复制备库重放被取消或延迟飙升长查询与重放冲突,配置 hot_standby_feedback/max_standby_streaming_delay(下一页)
连接串指错「读流量全在主库」或「写进备库报错」路由配置核对 + 启动时用 pg_is_in_recovery() 自检
读端点指向主库备库空转,读压力没分走监控各节点 QPS 分布

其中「事务内读写混用」是最高频也最隐蔽的坑:单测环境延迟为零,问题不暴露;上生产偶发「读不到刚写的数据」,排查半天发现是某个事务里的读被路由到了备库。规则要定为:只要进入写事务,连接全程钉死在主库。

什么业务适合读写分离 ​

读写分离不是万能扩展,它适合与不适合的场景界限分明:

适合不适合
读多写少(读写比 5:1 以上)写吞吐是瓶颈
读请求容忍秒级延迟(列表、搜索、报表)强一致读占比高(扣减库存、余额校验)
复杂分析查询可隔离到专用备库单条查询本身慢(需要的是索引/优化,不是备库)
已有流复制高可用架构(备库是现成资源)写入需要横向扩展(需要的是分片/分区)

一个实用的检验法:把「当前主库上最耗资源的 10 条 SQL」拉出来看——如果它们都是可容忍延迟的读,读写分离立竿见影;如果瓶颈是写、锁或单条慢查询,先做别的事。

实战示例 ​

示例一:为应用准备写端点与读端点自检 ​

应用启动时对两个端点各做一次角色自检,防止配置指错:

bash
# 写端点必须是主库
psql -h primary.app.internal -c "SELECT pg_is_in_recovery();"
text
 pg_is_in_recovery
-------------------
 f
(1 row)
bash
# 读端点必须是备库(若读端点负载均衡到多台,逐台检查)
psql -h replica.app.internal -c "SELECT pg_is_in_recovery();"
text
 pg_is_in_recovery
-------------------
 t
(1 row)

把这个检查放进部署流水线,路由配置错误在部署阶段就被拦截。

示例二:搭建延迟熔断的监控 ​

sql
-- 采集脚本:输出每个备库的延迟,超阈值输出告警标记
SELECT application_name,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes,
       CASE
         WHEN pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) > 1048576
         THEN 'ALERT'
         ELSE 'OK'
       END AS status
FROM pg_stat_replication;
text
 application_name | lag_bytes | status
------------------+-----------+--------
 standby1         |         0 | OK
 standby2         |   3145728 | ALERT
(2 rows)

示例三:LSN 等待实现写后读(PG17+) ​

sql
-- 步骤 1:主库上提交写入并记录 LSN
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 7;
COMMIT;
SELECT pg_current_wal_lsn() AS lsn_after_commit;
text
 lsn_after_commit
------------------
 0/3000D200
(1 row)
sql
-- 步骤 2:备库上等待追上后再读
SELECT pg_wal_replay_wait('0/3000D200'::pg_lsn);
SELECT balance FROM accounts WHERE id = 7;
text
 pg_wal_replay_wait
--------------------

(1 row)

 balance
---------
    900
(1 row)

返回即为追上;等待超时则收到警告,此时应回退主库读。

示例四:Laravel 框架层读写分离配置 ​

php
// config/database.php —— 框架自动路由读/写连接
'pgsql' => [
    'driver'   => 'pgsql',
    'host'     => 'primary.app.internal',   // 写连接默认目标
    'database' => 'appdb',
    'username' => 'app_user',
    'password' => 'secret',
    'charset'  => 'utf8',
    'prefix'   => '',

    'read' => [                              // 读连接目标(可多个,框架做负载均衡)
        'host' => ['replica1.app.internal', 'replica2.app.internal'],
    ],
    'write' => [                             // 写连接目标
        'host' => ['primary.app.internal'],
    ],

    'sticky' => true,                        // 请求内写过之后,后续读复用写连接
],

sticky = true 是框架对 read-your-writes 的最小实现:同一个请求周期内执行过写,该请求后续的读都走写连接。请求结束即失效,跨请求仍可能读到备库旧数据——关键路径仍需显式处理。

示例五:验证读流量确实被分走 ​

sql
-- 在主库上统计活跃连接的来源分布
SELECT client_addr,
       count(*) FILTER (WHERE state = 'active') AS active,
       count(*) AS total
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY client_addr
ORDER BY total DESC;
text
 client_addr  | active | total
--------------+--------+-------
 10.1.0.20    |      3 |    60
 10.1.0.21    |      1 |     8
(2 rows)

如果原本期望「应用经读端点打到备库」,主库上却看到大量来自应用网段的连接,说明读路由没有生效(读端点配错、DNS 指错、连接池池化复用了写连接),要逐层排查。

示例六:备库不可用时的降级演练 ​

bash
# 演练:停掉一台备库,验证读端点摘除与流量回退
ssh replica2-host "sudo systemctl stop postgresql"

# 读端点(负载均衡器)健康检查摘除该节点后:
# 1. 读流量不再打向 replica2(查负载均衡器状态页)
# 2. 剩余备库与主库读流量上升,观察主库连接数是否逼近上限
psql -h primary.app.internal -c \
  "SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';"
text
 count
-------
    78
(1 row)

演练要验证的临界点:全部备库同时不可用时,主库能否独自扛住全部读流量(连接数上限、CPU、内存)。如果扛不住,说明读端点需要「全量回主」之外的降级策略(限流、只读缓存兜底)。

示例七:估算 WAL 产生速率与备库追赶时间 ​

延迟熔断阈值与容量规划都需要回答「主库每秒产生多少 WAL」。用两次采样做差:

sql
-- 第一次采样(记录时间与 LSN)
SELECT pg_current_wal_lsn() AS lsn1, clock_timestamp() AS t1;
text
   lsn1    |          t1
-----------+-------------------------------
 0/3000C100 | 2026-09-30 14:00:00.000000+08
(1 row)
sql
-- 一分钟后第二次采样并求差
SELECT clock_timestamp() AS t2,
       pg_size_pretty(
         pg_wal_lsn_diff(pg_current_wal_lsn(), '0/3000C100'::pg_lsn)
       ) AS wal_per_minute;
text
          t2              | wal_per_minute
--------------------------+----------------
 2026-09-30 14:01:00+08   | 42 MB
(1 row)

解读:每分钟约 42MB ≈ 每秒 0.7MB。假设某备库当前落后 100MB,在持续写入不衰减的前提下,追平大约需要 100 ÷ 0.7 ≈ 143 秒——这个估算直接回答了「熔断后多久能恢复读分发」。如果业务对读延迟的容忍上限是 5 秒,那么 delay_threshold 类阈值就应该设在 5 × 0.7 ≈ 3.5MB 附近,而不是拍一个 100MB。建议在业务高峰、平峰两个时段各采样一次,取较悲观值。

注意事项 ​

读写分离只扩展读,不扩展写。 写瓶颈要靠写入优化、分区或分片解决,不要指望多加备库。

事务必须整体走主库。 事务内读写跨节点在语义上几乎总是错的,这是所有方案的第一铁律。

read-your-writes 是产品体验问题,不只是技术问题。 用户「提交后看不到」的投诉,往往比监控指标更早暴露一致性缺陷;关键路径的写后读要显式处理。

remote_apply 不是免费的。 它消除重放滞后的代价是提交延迟与备库故障时的写入阻塞;只在确有全局一致读需求时启用。

时间延迟指标在写入稀疏时会虚高。 now() - pg_last_xact_replay_timestamp() 在没有新事务时持续变大,熔断逻辑要结合字节差一起判断,否则深夜会误熔断。

备库不是「免费的读资源」。 每个备库消耗主库的 walsender、网络带宽,长查询还会与重放冲突;加备库前先评估长查询治理。

连接池与读写分离要协同设计。 读写池分开是底线;共用连接池时,「会话粘滞」与「事务边界」两个保证都会被池化破坏。

序列、锁、DDL 在备库上有专门行为。 看似只读的语句(nextval、FOR UPDATE)在备库不可用,路由层要按语义而非语法判断。

主库故障切换后读流量要自动跟随。 新主库提升后,原读端点对它的引用、复制拓扑的重建都要有预案,否则「切完主库发现读端点还指着旧节点」。

监控要覆盖「流量分布」而不只是「节点健康」。 备库健康但读流量没分过来(配置错误)是一种静默失效,各节点 QPS 分布是最直接的检测面。

阈值类参数要用数据推导而不是照抄。 延迟熔断的字节阈值依赖 WAL 产生速率,每个集群的答案都不同;抄来的阈值要么形同虚设,要么频繁误熔断。

备库数量与主库容量要联动评估。 每个备库消耗主库一份 WAL 发送带宽与 walsender 资源,扩备库前先确认主库与网络的余量,必要时用级联复制分层。

最佳实践 ​

  1. 从「事务走主库 + 自动提交读走备库」这条默认策略起步。 它覆盖 80% 的场景且语义安全;更细的语句级优化等流量数据说话。

  2. 识别应用中的关键路径写后读,逐个处理。 处理手段按优先级:读主库 > LSN 等待(PG17+)> remote_apply;不要试图用「全局同步复制」一刀切。

  3. 写池与读池物理分开,启动时用 pg_is_in_recovery() 自检。 配置错误在部署阶段拦截,比生产事故便宜一百倍。

  4. 建立延迟熔断:字节阈值 + 时间阈值双条件。 阈值从业务敏感度反推(如「读延迟容忍上限 2 秒」),而不是拍脑袋定 1MB。

  5. 熔断与恢复都要告警留痕。 读能力降级是业务可见事件,静默熔断会让「备库挂了但没人知道」。

  6. 备库规格不同必须加权分配流量。 均分是备库异构环境里最高频的过载事故来源。

  7. 报表与分析类流量放专用下游备库。 用级联复制隔离重型查询,避免它们与实时读争抢同一台备库。

  8. 给备库数量留高可用余量。 「N 台备库全用于读」意味着任何一台故障都直接影响读容量;要么多一台热备,要么接受读降级预案。

  9. 定期演练「备库全灭」与「主库切换」的组合场景。 读写分离架构的真实可用性只有组合故障演练才能验证。

  10. 监控各节点 QPS 与连接数分布。 「流量没分走」是读写分离最常见的静默失效,流量分布是最早的信号面。

  11. 框架层方案要理解其粘滞语义的边界。 例如请求级 sticky 不覆盖跨请求一致性,关键路径需要显式增强而不是默认依赖。

  12. 把「读端点是否真的在读备库」写进例行巡检。 一条 SQL 的成本,换的是对整个架构是否按预期运转的确认。

下一节

继续学习:备库只读查询(hot_standby)

参考链接 ​