读写分离中间件
前两页讲的是读写分离的架构与备库查询的底层规则,本页解决「谁来路由」的落地问题。应用层直连方案(两个数据源、框架级读写路由)在小规模下够用,但当备库数量增长、故障转移需要自动感知、延迟熔断需要统一实施时,在每个应用里重复实现一遍路由逻辑的成本就过高了——此时中间件成为自然选择:应用连接一个统一的入口,路由、负载均衡、故障感知、连接池全部在中间件层完成。
中间件不是免费的胜利。它在应用与数据库之间插入了一个新的故障域、一层 SQL 处理逻辑,也带来一批独特的坑:SET 语句建立的会话状态、事务内自动粘滞、LISTEN/NOTIFY 不透传、COPY 语义差异、游标兼容性——每一个都曾是生产事故的主角。理解这些边界,与理解它的功能同样重要。
本页先给中间件方案分类(数据库代理与应用中间件),重点展开 Pgpool-II 的配置体系(后端定义、负载均衡、延迟阈值、健康检查、Watchdog、PCP 管理命令),再讲 PHP / Laravel 的应用层实现与 ShardingSphere 的读写分离要点,最后给出代理方案的通用风险清单、选型对比与最佳实践。
前置知识
建议先阅读:备库只读查询(hot_standby)
基础概念
中间件层引入的术语不多,但每个都对应一类故障模式,先把表格建立起来。
| 概念 | 英文 | 说明 |
|---|---|---|
| 数据库代理 | database proxy | 独立进程,应用连它、它连数据库(Pgpool-II 等) |
| 应用中间件 | application middleware | 以库/SDK 或独立进程形态嵌在应用侧(ShardingSphere 等) |
| 后端节点 | backend node | 代理视角下的 PostgreSQL 实例,按编号管理 |
| 负载均衡 | load balance | SELECT 分发到备库的策略 |
| 语句级负载均衡 | statement level load balance | 事务内逐语句判断去向的能力 |
| 事务粘滞 | transaction stickiness | 事务开始后固定路由到主库的行为 |
| 延迟阈值 | delay threshold | 复制延迟超过该值时读流量回主库 |
| 健康检查 | health check | 代理对后端的存活探测 |
| 故障切换命令 | failover command | 后端角色变化时代理执行的脚本 |
| Watchdog | 看门狗 | Pgpool-II 自身的高可用机制 |
| 委托 IP | delegate IP | Watchdog 管理的虚拟 IP,应用统一连接的入口 |
| PCP | Pgpool Control Protocol | Pgpool-II 的管理命令协议 |
| 连接池 | connection cache | 代理对后端连接的复用 |
两类中间件的本质区别
数据库代理(Pgpool-II)是独立部署的进程,对应用完全透明——应用以为自己连的是 PostgreSQL,实际连的是代理。它的能力上限取决于对 SQL 协议的解析深度,它做不到的事(比如透传全部会话状态)应用也无从干预。
应用中间件(ShardingSphere-JDBC / 框架内置路由)运行在应用进程内或与应用同生命周期,能拿到完整的应用上下文(事务边界、请求语义),路由决策更聪明;代价是绑定技术栈、跨语言团队无法复用同一套配置。
选型时先回答「路由逻辑放进程外还是进程内」,再谈具体产品。
代理层的连接拓扑
┌─────────────────────────┐
│ 应用集群 │
└────────────┬────────────┘
│ 一个统一入口
┌────────────┴────────────┐
│ 代理层(Pgpool-II ×N) │
│ 路由 / 连接池 / 健康检查 │
└───┬──────────┬──────────┘
写路由 │ │ 读路由(负载均衡)
┌──────┴───┐ ┌───┴────┬────────┐
│ 主库 │ │ 备库1 │ 备库2 │
└──────────┘ └────────┴────────┘应用只看到一个地址;主库故障、备库增减、延迟熔断全部发生在代理层内部。这个「统一入口」的代价是:代理层自身必须高可用(Watchdog / 多实例 + 前端负载均衡),否则单点故障直接带走全部数据库连接。
语法与用法
Pgpool-II 的配置文件结构
Pgpool-II 的配置分布在三个文件中:
| 文件 | 内容 |
|---|---|
pgpool.conf | 主配置:后端节点、负载均衡、健康检查、Watchdog、池化 |
pcp.conf | PCP 管理命令的用户与密码(MD5 摘要存储) |
pool_hba.conf | 代理层的客户端接入控制(语义类似 pg_hba.conf) |
定义后端节点
# pgpool.conf —— 后端按 0/1/2... 编号定义
backend_hostname0 = '10.0.0.10'
backend_port0 = 5432
backend_weight0 = 0 # 主库不参与读负载均衡(权重 0)
backend_hostname1 = '10.0.0.11'
backend_port1 = 5432
backend_weight1 = 1 # 备库 1 承担读流量
backend_hostname2 = '10.0.0.12'
backend_port2 = 5432
backend_weight2 = 1 # 备库 2 承担读流量backend_weight 是按权重分配读流量的核心参数:主库权重设 0(读写分离模式下的常规做法),备库按规格或意愿分配比例。
负载均衡与流复制模式
# pgpool.conf —— 读写分离核心开关
load_balance_mode = on # SELECT 分发到备库
master_slave_mode = on # 主备模式(后端之间是复制关系)
master_slave_sub_mode = 'stream' # 复制方式为流复制
statement_level_load_balance = on # 事务内逐语句判断去向statement_level_load_balance 值得单独解释:默认情况下,一个事务一旦开始就整体路由到当前节点(粘滞),即使事务里全是 SELECT 也不会分给备库。开启语句级负载均衡后,Pgpool-II 在事务内对每条语句做只读判断,纯读语句可分发到备库——读收益更高,但对会话状态的跟踪也更复杂,需结合业务测试后开启。
延迟检查与延迟阈值
# pgpool.conf —— 流复制状态检查与延迟熔断
sr_check_period = 10 # 每 10 秒检查一次复制状态
sr_check_user = 'pgpool'
sr_check_database = 'postgres'
delay_threshold = 1000000 # 备库落后主库超过约 1MB 时,读流量回主库delay_threshold 是 Pgpool-II 实现「延迟熔断」的核心参数:以字节为单位度量备库落后量,超过阈值后该备库暂时不再接收读请求,延迟回落自动恢复。
健康检查与故障切换
# pgpool.conf —— 后端健康检查与故障动作
health_check_period = 10 # 每 10 秒探测(0 为关闭)
health_check_timeout = 5
health_check_user = 'pgpool'
health_check_max_retries = 3
health_check_retry_delay = 1
failover_command = '/etc/pgpool-II/failover.sh %d %h %p %D %m %H %M %P %r %N'
auto_failback = on # Watchdog 开启时:恢复健康的节点自动重新加入failover_command 在后端角色变化时被调用,% 开头的占位符传入节点信息(失败节点号、主机、端口等),脚本内完成 VIP 切换或调用外部高可用工具。这个脚本的质量直接决定故障切换的可靠性,必须演练。auto_failback 依赖 Watchdog:修复后的旧主库经检查健康后自动重新加入负载均衡,免去手工 pcp_attach_node。
查看代理层状态
# 连接 Pgpool-II 端口(而非 PostgreSQL 端口)
psql -h 10.0.0.5 -p 9999 -U postgres -c "SHOW POOL NODES;" node_id | hostname | port | status | lb_weight | role | select_cnt | load_balance_node | replication_delay
---------+-----------+------+--------+-----------+---------+------------+-------------------+-------------------
0 | 10.0.0.10 | 5432 | up | 0.000000 | primary | 0 | false | 0
1 | 10.0.0.11 | 5432 | up | 0.500000 | standby | 8214 | true | 0
2 | 10.0.0.12 | 5432 | up | 0.500000 | standby | 8093 | false | 16432
(3 rows)SHOW POOL NODES 是代理层健康度的第一入口:role(primary/standby)、status(up/down)、select_cnt(读流量分布)、replication_delay(延迟)一屏尽览。流量分布异常(如 select_cnt 全在主库)往往比节点宕机更常见,也更容易被忽视。
PCP 管理命令
PCP 命令用于代理层的运维操作,认证信息来自 pcp.conf:
# 查看节点与池状态
pcp_pool_status -h 10.0.0.5 -p 9898 -U pcpadmin
# 查看指定节点详情
pcp_node_info -h 10.0.0.5 -p 9898 -U pcpadmin 0
# 手工摘除节点(维护时)
pcp_detach_node -h 10.0.0.5 -p 9898 -U pcpadmin 2
# 手工重新加入节点(维护完成后)
pcp_attach_node -h 10.0.0.5 -p 9898 -U pcpadmin 2
# 查看 Watchdog 集群状态
pcp_watchdog_info -h 10.0.0.5 -p 9898 -U pcpadmin手工维护的标准流程是「detach → 数据库侧维护 → attach」,与负载均衡器摘除节点的语义完全一致。
详细说明
中间件方案分类
| 类别 | 代表 | 运行形态 | 路由依据 |
|---|---|---|---|
| 数据库代理 | Pgpool-II | 独立进程,协议级转发 | SQL 解析 + 会话状态跟踪 |
| 应用框架内置 | Laravel 读写连接、Spring 多数据源 | 应用进程内 | 应用声明的读写语义 |
| 应用侧中间件(SDK) | ShardingSphere-JDBC | 应用进程内的驱动增强 | 配置规则 + SQL 解析 |
| 应用侧中间件(Proxy) | ShardingSphere-Proxy | 独立进程 | 同上,形态类似数据库代理 |
四类的取舍本质是「智能程度 vs 侵入程度」的对角线:越靠近应用(框架内置)越智能但越绑定技术栈;越靠近数据库(代理)越通用但受 SQL 解析能力限制。
Pgpool-II 的定位与能力面
Pgpool-II 是 PostgreSQL 生态里历史最久的中间件,能力面覆盖四块,注意它们是叠加而非互斥的:
- 连接池(
connection_cache = on):复用到后端的连接,降低应用频繁建连的开销; - 读写分离 / 负载均衡:
load_balance_mode+ 后端权重; - 主备模式感知(
master_slave_mode = 'stream'):理解流复制拓扑,配合健康检查做故障感知; - Watchdog 高可用:多台 Pgpool-II 互为备份,通过委托 IP 提供统一入口。
能力面广是它的优势也是它的复杂度来源:连接池行为、复制检查、健康检查、Watchdog 四套机制相互影响,排障时要分清「问题出在哪一层」。
后端定义与权重分配
后端定义(backend_hostnameN 系列)是 Pgpool-II 的拓扑事实源,几个工程要点:
- 主库权重设 0。读写分离场景下主库不参与读分发,
backend_weight0 = 0是标配——否则部分读流量会回到主库,读写分离名存实亡; - 权重按备库规格分配。异构备库按 CPU/内存比例设权重,例如 16GB 与 64GB 备库设 1:3;
- 增删后端需要重载并注意在线恢复流程。节点变更不是纯配置问题,涉及复制重建与 PCP 操作,按官方流程操作;
SHOW POOL NODES的select_cnt是权重正确性的检验面。权重设错的第一信号就是读流量分布与预期不符。
流复制检查与 delay_threshold 的语义
sr_check_period 驱动的复制检查与 delay_threshold 的熔断逻辑配合如下:
每 sr_check_period 秒:
1. Pgpool-II 查询各备库的重放位置(走流复制检查连接)
2. 计算每台备库落后主库的字节数
3. 落后量 > delay_threshold 的备库 → 暂时移出读负载均衡名单
4. 后续检查发现落后量回落 → 自动恢复读分发阈值设定的参考逻辑:delay_threshold ≈ 业务可容忍读延迟对应的 WAL 字节数。写吞吐稳定的库可以用「每秒 WAL 产生量 × 可容忍秒数」估算;写入突发大的库要放宽阈值,否则频繁熔断反而造成主库读压力振荡。配合 statement_level_load_balance 使用时,熔断行为同样作用于事务内的读语句。
健康检查与 failover_command
健康检查(health_check_period)与复制检查是两条独立的探测线:前者判断「节点活着吗」,后者判断「节点跟得上吗」。
failover_command 是故障切换的实际执行者。它收到的占位参数包含故障节点与新主库的信息,脚本典型职责:
# failover.sh 的典型职责清单(示意)
# 1. 如果是主库故障:把 VIP 漂移到新主库,或通知外部路由组件
# 2. 记录事件日志(时间、节点、动作)
# 3. 可选:调用 Patroni/repmgr 的接口(若混合部署,职责要划清,见下文)脚本要幂等且快速返回
failover_command 在故障切换的关键路径上同步执行。脚本卡住、反复重试、或做了不可重入的操作(如重复抢 VIP),都会放大故障。脚本上线前必须在演练环境完整跑过「主库故障 → 恢复」全流程。
另一个架构级提醒:如果数据库层已有 Patroni/repmgr 在做提升决策,Pgpool-II 应只做路由与健康感知,不要在 failover_command 里再发明一套提升逻辑。两套「谁来当主库」的判定并存,是读写分离架构里最经典的互踩故障。
一次故障切换在代理层的完整时间线
把健康检查、failover_command、路由恢复串起来看一次主库故障的完整时间线,有助于划分「代理层负责什么、依赖谁」:
T+0s 主库进程崩溃
T+10s health_check_period 到期,健康检查失败(第 1 次)
T+13s 重试至 health_check_max_retries(3 次)后判定节点 down
T+13s failover_command 被执行:VIP 漂移到新主库 / 通知外部组件
T+15s 副本感知主库失联,经提升流程成为新主库(数据库层职责)
T+20s 复制检查发现备库角色变化,后端视图更新 primary/standby
T+20s 读流量按新拓扑重新分发;写流量经新 VIP 到达新主库
T+40s 旧主库修复后经 auto_failback 或 pcp_attach_node 重新加入判读这份时间线需要注意两点:T+13s 到 T+20s 之间应用写请求会失败——代理层切换完成不等于数据库层提升完成,两者是接力关系,中间的失败窗口是这套架构的固有属性,客户端重连策略就是为它设计的;failover_command 与数据库层的提升动作必须协同但不重叠(再次强调职责边界),时间线上任何「两方同时抢主库角色」的迹象都要当作架构缺陷处理。
Watchdog 与 Pgpool-II 自身高可用
代理层自己不能是单点。Pgpool-II 的 Watchdog 机制把多台 Pgpool-II 组成一个互为心跳的小集群:
- 多台 Pgpool-II 通过心跳互相监控(进程与网络双重检测);
- 全体节点协商出唯一领导者,领导者持有委托 IP(
delegate_ip,即应用连接的统一 VIP); - 领导者故障 → 其余节点选举新领导者 → 委托 IP 漂移,应用几乎无感;
- 所有 Pgpool-II 节点共享同一套
pgpool.conf后端视图,无论连到哪台,路由行为一致。
Watchdog 的仲裁机制与 Patroni 的 DCS 类似:多数派原则防脑裂(避免两台 Pgpool-II 同时持有委托 IP)。部署上至少 3 台(或 2 台 + 外部仲裁),且跨故障域分布——代理层的高可用设计原则与数据库层完全同构。
pcp 管理命令
PCP 是 Pgpool-II 的管理通道,覆盖节点管理、状态查询、Watchdog 控制。日常最常用的四个:
| 命令 | 用途 |
|---|---|
pcp_pool_status | 池与配置的全量状态 |
pcp_node_info | 单节点详情(角色、状态、延迟) |
pcp_detach_node / pcp_attach_node | 节点摘除与重新加入(维护窗口的标准动作) |
pcp_watchdog_info | Watchdog 集群状态(谁是领导者、委托 IP 在哪) |
PCP 认证用户独立于数据库用户(配置在 pcp.conf),生产上应使用强密码并限制来源网段——这条管理通道能直接改变流量拓扑,等同于高可用系统的控制面。
应用层实现:PHP 与 Laravel
不引入代理时,应用层自己完成路由。PHP 场景两个典型实现:
框架级多数据源(以 Laravel 为例,配置语法见上一页实战示例):在 config/database.php 声明 read / write 两组连接,框架按「写语句走 write、读语句走 read」自动路由,sticky = true 提供请求内的写后读粘滞。要点:
- 事务自动绑定 write 连接,事务内读写混用是安全的;
read数组多个主机时框架在它们之间轮询(注意不是感知延迟的智能均衡);sticky是请求级语义,跨请求的 read-your-writes 需要业务显式处理。
PDO 双连接(无框架或需要更细控制时):
<?php
// 读写分离的最小 PDO 实现(示意)
$write = new PDO('pgsql:host=primary.app.internal;port=5432;dbname=appdb', 'app_user', $secret);
$read = new PDO('pgsql:host=replica.app.internal;port=5432;dbname=appdb', 'app_user', $secret);
function route(PDO $write, PDO $read, string $sql, array $params = []): PDOStatement
{
// 粗分类:写关键词/事务开始强制走主库,其余走备库
$isWrite = (bool) preg_match('/^\s*(insert|update|delete|create|alter|drop|truncate|begin)/i', $sql);
$conn = $isWrite ? $write : $read;
$stmt = $conn->prepare($sql);
$stmt->execute($params);
return $stmt;
}
// 写:走主库
route($write, $read, 'INSERT INTO orders(user_id, amount) VALUES (?, ?)', [42, 199]);
// 读:走备库
$stmt = route($write, $read, 'SELECT count(*) FROM orders WHERE user_id = ?', [42]);这个极简实现只是为了说明原理,生产实现必须补充:事务粘滞(BEGIN 后全部钉主库直到提交)、nextval/FOR UPDATE 语义识别、连接级会话状态隔离。规则与上一页「备库限制清单」一一对应——应用层路由做不好语义识别,正是很多团队转向代理层的直接原因。
ShardingSphere 的读写分离
ShardingSphere 提供两种形态:JDBC 驱动增强(嵌入应用)与独立 Proxy(协议级代理)。其读写分离能力按规则配置:声明写数据源与读数据源列表、选择读流量负载均衡算法(轮询、随机、权重等)、可选配置读库可用性探测,由框架自动完成路由与故障摘除。5.x 的规则配置放在 YAML 的 rules 段下(!READWRITE_SPLITTING 规则块),属性命名随版本迭代有变化,具体参数名与写法以所用版本的官方文档为准,本页只固化功能层面的要点:
- 静态与动态策略:写/读数据源可以静态声明,也可以基于数据库发现能力动态识别主从角色;
- 负载均衡算法可插拔:轮询、随机、按权重等内置算法可选,也可扩展自定义;
- 写后读语义:同一线程/请求内的写后读有粘滞保证(其内部有路由缓存与清理机制,行为细节以官方文档为准);
- 与分片规则正交:读写分离规则可与分库分表规则叠加,这是它相对纯代理方案的结构性优势。
选它的理由通常是「未来要做分片」或「Java 技术栈深绑定」;纯 PostgreSQL 读写分离场景,Pgpool-II 或框架内置方案往往更轻。
代理方案的通用风险
无论哪种代理,都工作在「假装自己是 PostgreSQL」的夹缝里,以下风险是全品类通用的:
| 风险 | 机理 | 对策 |
|---|---|---|
SET 会话状态 | SET timezone 等建立的状态可能滞留在池化的后端连接上 | 明确会话状态生命周期,避免依赖跨请求的会话变量 |
| 事务粘滞边界 | 事务开始后必须整体钉主库,代理判断失误会拆散事务 | 事务语义测试覆盖;关键事务显式走主库入口 |
| 复制延迟 | 代理默认不做 read-your-writes 保证 | delay_threshold 类熔断 + 应用层写后读策略 |
LISTEN/NOTIFY | 通知队列是实例级的,经代理连接后订阅方与通知方可能落在不同后端 | 通知类功能直连主库或重新设计 |
COPY | 大批量导入经代理的转发行为与直连有差异,部分代理对 COPY 支持有限 | ETL / 批量导入绕过代理直连 |
| 游标与长事务 | 长期持有的游标/事务占用池化连接,破坏池化收益 | 长事务走专用端口或直连 |
| SQL 兼容边界 | 极少数语法/协议特性代理不支持或不透传 | 上线前用核心业务 SQL 全量回归测试 |
其中「SET 状态」与「LISTEN/NOTIFY」是最隐蔽的两个:它们在单库直连时代从未出过问题,加代理之后才浮现,且难以复现。
延迟导致的读旧数据的处理策略
中间件层能做的与不能做的要分清。以 Pgpool-II 为例:
能做的:delay_threshold 把延迟过大的备库移出读名单(兜底,不是保证);语句级判断把明显写后的读留在主库。
不能做的:代理无法普遍知道「这条 SELECT 依赖哪次写」。写后读的一致性最终要靠应用层策略:
- 延迟阈值熔断(代理层):
delay_threshold兜底,防止「明显落后」的备库继续接读; - 强制走主库的 API(应用层):关键读写提供显式主库入口,如
$repo->fromPrimary()->find(...),把一致性决策交给业务代码; - LSN 等待(PG17+):写后读关键路径用
pg_wal_replay_wait(见架构页实战示例); - 接受最终一致(产品层):部分场景(推荐列表、统计看板)明确标注「数据可能有分钟级延迟」,比所有手段都便宜。
四层按序使用,越靠前越精确、越靠后越便宜。架构页的一致性保障手段与本页的策略是同一套方案在两个层的投影。
监控与故障处理
代理层的监控面比单库多一层,核心清单:
| 监控项 | 来源 | 告警条件 |
|---|---|---|
| 后端节点状态 | SHOW POOL NODES 的 status/role | down、角色漂移 |
| 读流量分布 | SHOW POOL NODES 的 select_cnt | 流量全在主库、分布严重偏斜 |
| 复制延迟 | replication_delay 列 / 主库 pg_stat_replication | 超 delay_threshold 比例 |
| 代理进程与 Watchdog | pcp_watchdog_info、进程监控 | 领导者切换、心跳丢失 |
| 代理自身资源 | CPU / 连接数 / 文件句柄 | 接近 max_connections、句柄耗尽 |
| 故障事件 | failover_command 日志 | 任何触发事件 |
排障时的分层心法:先 SHOW POOL NODES 判断是「后端问题」还是「代理问题」——后端 down / 延迟大 → 去数据库层查;后端正常但应用报错 → 查代理进程、连接数、Watchdog 状态。这个二分法能砍掉一半的排障时间。
中间件选型与何时不该用
| 维度 | Pgpool-II | ShardingSphere-Proxy | 框架内置路由 |
|---|---|---|---|
| 语言/栈绑定 | 无 | 生态偏 Java | 绑定框架 |
| 连接池 | 内置 | 有连接控制 | 依赖应用/外部池 |
| 故障感知 | 健康检查 + failover 命令 | 探测 + 摘除 | 依赖外部 |
| 语句级负载均衡 | 支持(需开启) | 支持 | 有限 |
| 自身高可用 | Watchdog | 多实例 + 前端 LB | 无需(随应用) |
| 适用 | 纯 PG 读写分离 + 连接池 | 分片 + 读写分离一体 | 单应用小规模 |
何时不该用中间件——这些信号说明「先别加」:
- 备库只有一台、读压力不明显:框架级路由就够,代理层是纯增负担;
- 应用重度依赖会话级特性(
LISTEN/NOTIFY、游标、会话变量、advisory lock):代理的兼容边界会先爆; - 团队没有人力维护代理层脚本与 Watchdog 集群:半吊子的代理层比没有更危险;
- 高可用已有 Patroni 全权管理:只为读写分离再引入 Pgpool-II 要划清职责,避免两套主库判定互踩。
实战示例
示例一:最小可用的 Pgpool-II 读写分离配置
# pgpool.conf 核心段(读写分离最小集)
listen_addresses = '*'
port = 9999
# 后端定义
backend_hostname0 = '10.0.0.10'
backend_port0 = 5432
backend_weight0 = 0
backend_hostname1 = '10.0.0.11'
backend_port1 = 5432
backend_weight1 = 1
# 读写分离
load_balance_mode = on
master_slave_mode = on
master_slave_sub_mode = 'stream'
# 复制检查与延迟熔断
sr_check_period = 10
sr_check_user = 'pgpool'
sr_check_database = 'postgres'
delay_threshold = 1000000
# 健康检查
health_check_period = 10
health_check_timeout = 5
health_check_user = 'pgpool'
# 连接池
connection_cache = on# 启动后立即验证流量分布
psql -h 127.0.0.1 -p 9999 -U postgres -c "SHOW POOL NODES;"示例二:验证读写真的被分开
# 经代理执行一条写
psql -h 127.0.0.1 -p 9999 -U postgres -c \
"INSERT INTO route_check(id) VALUES (1);"
# 经代理执行一条读
psql -h 127.0.0.1 -p 9999 -U postgres -c \
"SELECT count(*) FROM route_check;"
# 再看流量计数
psql -h 127.0.0.1 -p 9999 -U postgres -c "SHOW POOL NODES;" node_id | hostname | role | select_cnt | load_balance_node | replication_delay
---------+-----------+---------+------------+-------------------+-------------------
0 | 10.0.0.10 | primary | 0 | false | 0
1 | 10.0.0.11 | standby | 1 | true | 0
(2 rows)select_cnt 在主库为 0、备库增长,证明读流量确实落到了备库;写语句(INSERT)不计入 select_cnt,落在主库。
示例三:模拟备库延迟触发熔断
# 在备库上暂停重放(演练用),制造延迟
psql -h 10.0.0.11 -c "SELECT pg_wal_replay_pause();"
# 主库写入若干数据后,观察代理的延迟判断
psql -h 127.0.0.1 -p 9999 -U postgres -c "SHOW POOL NODES;" node_id | hostname | role | load_balance_node | replication_delay
---------+-----------+---------+-------------------+-------------------
0 | 10.0.0.10 | primary | false | 0
1 | 10.0.0.11 | standby | false | 2408448
(2 rows)replication_delay 超过 delay_threshold 后 load_balance_node 变为 false——读流量自动回主库。验证完成后恢复重放:
psql -h 10.0.0.11 -c "SELECT pg_wal_replay_resume();"(pg_wal_replay_pause() / pg_wal_replay_resume() 亦可用于维护场景的延迟备库控制,见流复制相关文档。)
示例四:pcp 维护窗口操作
# 1. 维护前摘除节点 2
pcp_detach_node -h 10.0.0.5 -p 9898 -U pcpadmin 2
# 2. 确认流量不再发往该节点
psql -h 127.0.0.1 -p 9999 -U postgres -c "SHOW POOL NODES;"
# 3. ……执行数据库维护……
# 4. 重新加入
pcp_attach_node -h 10.0.0.5 -p 9898 -U pcpadmin 2
# 5. 核对状态与流量恢复
pcp_node_info -h 10.0.0.5 -p 9898 -U pcpadmin 2示例五:PHP 应用接入代理与双连接对比
<?php
// 方式 A:应用只连代理(代理负责路由)
$proxy = new PDO('pgsql:host=pgpool.internal;port=9999;dbname=appdb', 'app_user', $secret);
$proxy->query('SELECT count(*) FROM orders'); // 代理自动路由到备库
// 方式 B:应用双连接,显式控制一致性
$write = new PDO('pgsql:host=primary.app.internal;dbname=appdb', 'app_user', $secret);
$read = new PDO('pgsql:host=replica.app.internal;dbname=appdb', 'app_user', $secret);
// 关键路径:写后必须立刻读到 → 显式走主库
$write->prepare('UPDATE accounts SET balance = balance - 100 WHERE id = ?')
->execute([7]);
$balance = $write->query('SELECT balance FROM accounts WHERE id = 7')->fetchColumn();
// 非关键路径:容忍延迟 → 走备库
$total = $read->query('SELECT count(*) FROM orders')->fetchColumn();方式 A 换来运维简单、应用零改造;方式 B 换来精确的一致性控制。混合使用(普通流量走 A、关键路径走 B)在工程上完全可行。
示例六:代理层巡检脚本
#!/bin/bash
# 代理层巡检:节点状态 + 流量分布 + Watchdog
echo "== POOL NODES =="
psql -h 127.0.0.1 -p 9999 -U postgres -c "SHOW POOL NODES;"
echo "== WATCHDOG =="
pcp_watchdog_info -h 127.0.0.1 -p 9898 -U pcpadmin
echo "== 异常检查 =="
psql -h 127.0.0.1 -p 9999 -U postgres -t -c "SHOW POOL NODES;" | \
grep -E "down|standby .* \| +0 " && \
echo "ALERT: 存在 down 节点或读流量全在主库"把它挂进定时巡检,「读流量没分走」与「节点 down」两类静默失效都有了自动发现能力。
注意事项
代理层是新的故障域,自身高可用不是可选项。 多实例 + Watchdog(或前端负载均衡)是生产标配;单实例代理的单点故障半径等于全部数据库连接。
主库的 backend_weight 默认设 0。 忘了设权重导致读流量回主库,是 Pgpool-II 读写分离「配了但没用」的头号原因。
不要让代理的 failover_command 与 Patroni/repmgr 的选主逻辑并存。 一套集群只能有一个「谁来当主库」的判定者;代理层只做路由感知。
statement_level_load_balance 开启前要充分测试。 它提升了读收益,也增加了会话状态跟踪的复杂度,核心业务全量回归后再上生产。
delay_threshold 是兜底不是 read-your-writes 保证。 关键路径的一致性仍要应用层显式处理(强制主库、LSN 等待)。
SET 语句建立的会话状态在池化连接上可能「串客」。 应用避免依赖跨请求的会话变量,或为这类会话绕开代理。
LISTEN/NOTIFY、COPY、游标是代理兼容的高风险区。 涉及这些特性的功能要么绕过代理直连,要么在测试环境完整验证。
PCP 通道是控制面,权限要收紧。 独立强密码、限制来源网段,控制面失守等于整个流量拓扑可被任意改写。
failover_command 脚本必须幂等、快速返回、有日志。 它在故障切换的关键路径上同步执行,脚本质量直接决定切换可靠性。
备库操作限制在代理下依然成立。 nextval()、FOR UPDATE 照样会在备库报错——代理做的是路由,不是能力转换。
监控读流量分布与节点健康同等重要。 「备库健康但没人往它发读」是读写分离最常见的静默失效。
SHOW POOL NODES 是排障第一入口。 先分清「后端问题」还是「代理问题」,再决定去哪一层查。
故障切换时间线里的失败窗口是固有的。 代理判定 + 数据库提升之间存在秒级写失败窗口,这是架构属性而非缺陷;把它对应用的影响压到最低的手段是客户端重连策略,而不是调小健康检查周期。
最佳实践
先用
SHOW POOL NODES验证读写分离生效,再宣布上线。select_cnt的分布是唯一可信的验收标准,配置文件写对了不等于流量分对了。权重按备库规格分配,主库恒为 0。 异构备库均分流量是高频过载事故;权重配置要有据可查(写进运维文档)。
delay_threshold从业务容忍度反推,而不是拍一个字节数。 用「每秒 WAL 产生量 × 可容忍延迟秒数」估算,并在演练中验证熔断行为。代理层自身至少双实例 + Watchdog,跨故障域部署。 代理层高可用的设计原则与数据库层完全同构:奇数节点、多数派、跨域。
职责矩阵写进架构文档:路由归代理、选主归高可用工具。 Pgpool-II 与 Patroni/repmgr 并存时,白纸黑字划清边界,杜绝两套 failover 逻辑互踩。
failover_command脚本进版本库、过演练、带监控。 它是故障切换的实际执行者,按生产代码的标准管理。关键路径读显式走主库,代理阈值只做兜底。 「强制主库 API + delay_threshold」双层结构,比指望代理解决一致性问题可靠得多。
上线前用核心业务 SQL 做代理兼容性回归。 重点覆盖事务、
SET、游标、COPY、锁语义——这些是代理差异的聚集地。PCP 管理通道纳入访问控制与审计。 控制面的每一次拓扑变更都应有日志可查。
维护流程标准化:detach → 维护 → attach → 验证。 四步固定顺序写进 Runbook,避免「边接流量边维护」。
代理层资源监控不亚于数据库层。 代理的连接数、句柄、CPU 耗尽同样造成全局故障,且更难被数据库监控发现。
规模不到就别上代理。 一台备库、读压力温和的场景,框架级路由的总体成本更低;中间件的引入要有明确的收益论据。
下一节
继续学习:创建角色(CREATE ROLE)
参考链接
- Pgpool-II 官方文档
- Pgpool-II 官方文档:Load Balancing 配置
- Pgpool-II 官方文档:Watchdog 配置
- Pgpool-II 官方文档:PCP 命令
- ShardingSphere 官方文档
- Laravel 官方文档:Database — Read / Write Connections
- PHP 官方文档:PDO
- PostgreSQL 官方文档:Hot Standby
- PostgreSQL 官方文档:Replication — Configuration Parameters
- PostgreSQL 官方文档:High Availability, Load Balancing, and Replication