HAProxy / Keepalived
HAProxy 是业界最成熟的高性能 TCP/HTTP 负载均衡器之一。在 PostgreSQL 高可用体系里,它的角色非常明确:TCP 层的流量分发与角色识别——应用只管连到 HAProxy 暴露的端口,HAProxy 通过健康检查判定每个后端节点当前是主库还是备库,把写流量送到主库、把读流量摊到各备库。它不解析 SQL、不参与选主,只做「把连接发给谁」这一个决定,但这个决定恰恰是故障转移后应用能否自动恢复的关键一环。
Keepalived 则解决另一个问题:HAProxy 自身的单点。它实现了 VRRP(Virtual Router Redundancy Protocol)协议,让两台(或多台)HAProxy 服务器共享一个虚拟 IP(VIP),主节点故障时 VIP 自动漂移到备节点。VIP 漂移发生在网络层,客户端无感知,这比任何应用层的重连逻辑都要快、都要可靠。
本页先讲清 HAProxy 在 PostgreSQL 高可用中的职责边界,再逐项给出 mode tcp 配置、三种健康检查方式、基于 Patroni REST API 的角色感知路由、读写分离双后端、Keepalived VIP 方案与脑裂防护,最后组合出「应用 → HAProxy → PgBouncer → PostgreSQL」的完整链路。层次关系是:Patroni(决策,见下一页)负责选出主库,HAProxy(路由)负责把流量发过去,PgBouncer(复用)负责收敛连接数。
前置知识
建议先阅读:PgBouncer 连接池
基础概念
| 概念 | 英文 / 记法 | 说明 |
|---|---|---|
| 前端 | frontend | HAProxy 对外监听的入口(地址 + 端口) |
| 后端 | backend | 一组真实服务器与转发策略的集合 |
| 监听段 | listen | frontend + backend 合二为一的简写段 |
| 健康检查 | health check | 判定后端节点是否可用的探测(TCP/HTTP/PostgreSQL) |
| 角色感知路由 | role-aware routing | 依据节点角色(主/备)而不是单纯存活来分流 |
| 负载均衡算法 | balance | 在多个可用节点间分配连接的策略 |
| 虚拟 IP | VIP | 对外统一服务的 IP,随主节点漂移 |
| VRRP | Virtual Router Redundancy Protocol | Keepalived 用来协商 VIP 归属的协议 |
| 脑裂 | split-brain | 两台节点都认为自己是 VIP 持有者的故障状态 |
HAProxy 在 PostgreSQL 高可用中的角色
一套典型的高可用链路里,每个组件回答一个问题:
┌──────────┐ ┌──────────────────┐ ┌───────────┐ ┌─────────────┐
│ 应用 │ ───→ │ HAProxy + VIP │ ───→ │ PgBouncer │ ───→ │ PostgreSQL │
│ 连到谁? │ │ 「发给谁?」 │ │ 「复用」 │ │ 「谁在决策?」│
└──────────┘ └──────────────────┘ └───────────┘ └─────────────┘
▲ ▲
│ 健康检查 │ 健康检查
▼ ▼
8008 (REST API) 5432 (数据端口)
Patroni 告知角色 真实流量- PostgreSQL 谁当主库由 Patroni/repmgr 等编排组件决定(决策层);
- HAProxy 通过健康检查知道「此刻谁是主库」,把写端口流量转过去(路由层);
- PgBouncer 在 HAProxy 之后收敛连接数(复用层,也可放在 HAProxy 之前)。
HAProxy 的价值在于故障转移后的零配置恢复:Patroni 完成提升的瞬间,原主库的健康检查开始失败、新主库开始成功,HAProxy 自动完成流量切换,应用甚至只需要断线重连,不需要任何人工干预。
Keepalived 解决 HAProxy 自身的单点
HAProxy 是一个进程、一台机器,机器宕机它也跟着没了。Keepalived 通过 VRRP 让多台 HAProxy 共享一个 VIP:主节点周期性广播 VRRP 通告,备节点监听通告;一旦通告消失,优先级最高的备节点接管 VIP。整个过程通常在秒级完成,且对客户端完全透明——应用始终连 VIP,从不关心后面是哪台机器。
语法与用法
HAProxy 的配置文件是 /etc/haproxy/haproxy.cfg,由 global、defaults、frontend、backend(或合并的 listen)段组成。一个面向 Patroni 集群的骨架:
global
log 127.0.0.1 local0
maxconn 4096
daemon
defaults
log global
mode tcp
option tcplog
timeout connect 5s
timeout client 24h
timeout server 24h
# 写端口:所有连接都发给当前主库
frontend pg_write
bind *:5000
default_backend pg_primary
backend pg_primary
option httpchk GET /primary
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node1 192.168.1.11:5432 check port 8008
server node2 192.168.1.12:5432 check port 8008
server node3 192.168.1.13:5432 check port 8008
# 读端口:连接摊到各备库
frontend pg_read
bind *:5001
default_backend pg_replicas
backend pg_replicas
balance roundrobin
option httpchk GET /replica
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node1 192.168.1.11:5432 check port 8008
server node2 192.168.1.12:5432 check port 8008
server node3 192.168.1.13:5432 check port 8008语法校验与启停:
# 校验配置文件语法(改配置后必做)
haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl enable --now haproxy
sudo systemctl reload haproxyKeepalived 的配置文件是 /etc/keepalived/keepalived.conf,核心是 vrrp_script 与 vrrp_instance 两个段,完整示例见后文「实战示例」。
详细说明
mode tcp 基础配置与超时
PostgreSQL 是 TCP 协议,所以 HAProxy 一律 mode tcp。三个段的关系:frontend 定义「从哪进」,backend 定义「到哪去、怎么挑」,listen 把两者写进一段,适合简单场景:
# 写法一:frontend + backend 分离(推荐,复杂规则更清晰)
frontend pg_write
bind *:5000
default_backend pg_primary
# 写法二:listen 合并写法
listen pg_write
bind *:5000
option httpchk GET /primary
http-check expect status 200
server node1 192.168.1.11:5432 check port 8008
server node2 192.168.1.12:5432 check port 8008超时三件套是 TCP 代理最容易配错的地方:
| 参数 | 建议值 | 说明 |
|---|---|---|
timeout connect | 3s~5s | HAProxy 建立到后端连接的超时,不宜过长 |
timeout client | 24h | 前端连接不活动的最大时长。数据库连接天然长空闲,设短了会反复掐断空闲会话 |
timeout server | 24h | 后端连接不活动的最大时长,一般与 client 对称 |
不要把超时设短来「清理连接」
数据库连接在事务之间空闲几小时是常态。把 timeout client/timeout server 设成几分钟,会把正常空闲的连接反复掐断,应用表现为「隔一阵就报 broken pipe」。清理连接是应用侧或 PgBouncer(client_idle_timeout)的职责,不是 HAProxy 的。
其他高频选项:option tcplog 启用 TCP 格式日志(记录客户端 IP、连接时长、后端节点等);default-server 把公共检查参数收敛到一行;on-marked-down shutdown-sessions 在节点被判定下线时立刻断开它上面的既有会话,强制流量重新分配——故障转移场景强烈建议开启,否则应用还挂在已经降级的旧主库上。
三种健康检查方式
HAProxy 对 PostgreSQL 有三种可用的健康检查,从简单到强大:
其一,内置 PostgreSQL 检查 option pgsql-check。HAProxy 向后端发送 PostgreSQL 启动包,根据应答判断「这是一个活的 PostgreSQL」:
backend pg_native
option pgsql-check user haproxy_check
server db1 192.168.1.11:5432 check inter 2s它只需要数据库里存在可登录的检查账号,不依赖任何外部组件。但它分不清主库和备库——备库也会应答成功,所以只能用于「转发到某个确定角色的库」由外部保证的场景。
其二,TCP 探测 option tcp-check。只验证端口可达,最廉价也最粗糙,一般只做兜底:
backend pg_tcp
option tcp-check
server db1 192.168.1.11:5432 check inter 2s fall 3 rise 2其三,HTTP 检查 Patroni REST API(生产首选)。Patroni 在每个节点的 8008 端口暴露 REST API,/primary 端点「当前节点是主库时返回 200,否则返回 503」,/replica 端点同理。HAProxy 用 option httpchk 探测它,就同时拿到了存活 + 角色两个信息:
backend pg_primary
option httpchk GET /primary
http-check expect status 200
server db1 192.168.1.11:5432 check port 8008 inter 3s注意 check port 8008:真实流量走 5432,健康检查走 8008,两个端口各司其职。三种方式的选择逻辑:
| 方式 | 能否识别角色 | 依赖 | 适用 |
|---|---|---|---|
pgsql-check | 不能 | 检查账号 | 无编排组件的简单主备 |
tcp-check | 不能 | 无 | 兜底探测 |
httpchk + Patroni | 能 | Patroni | Patroni 集群(生产标准方案) |
基于 Patroni REST API 的角色感知路由
Patroni REST API 的端点语义是角色路由的全部基础(完整列表见下一页,这里只列路由相关的):
| 端点 | 返回 200 的条件 | 返回 503 的条件 |
|---|---|---|
/primary(旧名 /master) | 当前节点是主库 | 当前节点是备库或未就绪 |
/replica | 当前节点是备库且数据足够新 | 当前节点是主库或落后过多 |
/health | 节点正常运行(不限角色) | 节点异常 |
/read-only | 当前节点可提供只读服务 | 不可提供 |
据此可以构建更细的读流量分层——例如把「同步备库」单独成一个 backend,供对延迟敏感的读服务使用:
# 只把读流量发给同步备库
backend pg_sync_replica
option httpchk GET /sync
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node2 192.168.1.12:5432 check port 8008
server node3 192.168.1.13:5432 check port 8008/replica 还支持查询参数(如 lag)让 Patroni 在检查时附带校验复制延迟,把落后过多的备库自动摘出读池,细节以 Patroni 官方 REST API 文档为准。
读写分离:primary 与 replicas 双后端
标准的双端口模型:写端口 5000 只指向主库(backend 里所有节点都参与检查,但同一时刻只有主库检查通过),读端口 5001 只指向备库。要点有三:
- 写 backend 天然就是「单活」的。多个
server行不是「多台主库」,而是「任何时候只有一台检查通过」,这正是角色路由的效果,不需要额外的 active/passive 逻辑。 - 读流量必须容忍复制延迟。备库落后是常态,走 5001 的查询要做好「刚写的数据读不到」的准备;要求读己之写的请求必须回落到写端口。流复制原理参见流复制原理。
- 切换期间读写端口同时收敛。旧主库降级、新主库提升的几秒内,5000 端口可能短暂无可用节点(所有检查失败),应用的重试机制此时接管;5001 端口则始终至少有备库可用。
# 写:单活,指向主库
backend pg_primary
option httpchk GET /primary
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node1 192.168.1.11:5432 check port 8008
server node2 192.168.1.12:5432 check port 8008
server node3 192.168.1.13:5432 check port 8008
# 读:多活,指向备库
backend pg_replicas
balance roundrobin
option httpchk GET /replica
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node1 192.168.1.11:5432 check port 8008
server node2 192.168.1.12:5432 check port 8008
server node3 192.168.1.13:5432 check port 8008负载均衡算法
balance 决定连接如何在多个可用后端间分配。对 PostgreSQL 常用的有两个:
| 算法 | 行为 | 适用 |
|---|---|---|
roundrobin | 依权重轮询分发 | 备库规格一致、查询耗时均匀,最常用 |
leastconn | 优先发给当前连接数最少的节点 | 各节点负载不均、长查询与短查询混杂 |
static-rr | 静态权重轮询,不看实时负载 | 节点规格不同(如 1 大 2 小) |
source | 按客户端 IP 哈希,同一来源固定到同一节点 | 需要按来源粘住的场景(少见) |
PostgreSQL 读池的现实是「连接长、空闲多、耗时方差大」,leastconn 通常比 roundrobin 更均匀;规格一致的集群两者差异不大。写 backend 只有一个可用节点,balance 无意义,不配置即可。
VIP + Keepalived:原理与配置
VRRP 的要点:多台节点组成一个虚拟路由器,共享一个 virtual_router_id(同一二层网络内必须唯一,取值 1~255)和一个 VIP;主节点以 advert_int 为周期(默认 1 秒)发送 VRRP 通告,备节点在多个通告周期内收不到通告时,优先级最高者接管 VIP。Keepalived 的 vrrp_instance 配置项:
| 配置项 | 说明 |
|---|---|
state MASTER / state BACKUP | 初始角色。配合 nopreempt 时两边都写 BACKUP |
interface | VRRP 通告与 VIP 绑定的网卡 |
virtual_router_id | 虚拟路由器 ID(1~255),同网段唯一 |
priority | 优先级(1~255),数值大者当选 |
advert_int | 通告间隔(秒),默认 1 |
authentication | 通告的认证口令(防误配,非加密手段) |
virtual_ipaddress | VIP 列表,可带掩码与设备名 |
track_script | 关联的健康检查脚本,失败时降级优先级或移交 VIP |
notify_master / notify_backup / notify_fault / notify | 角色变化时触发的钩子脚本 |
nopreempt | 抢占开关:开启后故障恢复的一方不夺回 VIP,避免二次切换 |
unicast_peer | 单播对端列表,用于禁用组播的环境(公有云 VPC 必备) |
主备两台 HAProxy 的差异化配置只有 state/priority 两行,其余完全一致:
# 主节点(192.168.1.21):state MASTER, priority 150
# 备节点(192.168.1.22):state BACKUP, priority 100
vrrp_script chk_haproxy {
script "/usr/local/bin/check_haproxy.sh"
interval 2
fall 2
rise 2
}
vrrp_instance VI_PG {
state MASTER # 备节点写 BACKUP
interface eth0
virtual_router_id 51
priority 150 # 备节点写 100
advert_int 1
authentication {
auth_type PASS
auth_pass Pg_VIP_Key
}
virtual_ipaddress {
192.168.1.100/24 dev eth0
}
track_script {
chk_haproxy
}
unicast_src_ip 192.168.1.21
unicast_peer {
192.168.1.22
}
}notify 系列钩子常用来联动周边:例如 notify_master "/usr/local/bin/promote_notice.sh" 在本机接管 VIP 时发告警、更新 DNS 或触发缓存清理。
Keepalived 健康检查脚本与脑裂防护
vrrp_script 是 VIP 是否漂移的裁判。它必须检查「HAProxy 真的在服务」,而不只是「进程存在」:
#!/bin/bash
# /usr/local/bin/check_haproxy.sh —— 校验进程存在且统计端口可访问
pidof haproxy >/dev/null 2>&1 || exit 1
echo "show stat" | timeout 2 socat stdio /run/haproxy/admin.sock >/dev/null 2>&1 || exit 1
exit 0vrrp_script chk_haproxy {
script "/usr/local/bin/check_haproxy.sh"
interval 2
timeout 1
fall 2
rise 2
weight 20
}fall/rise 控制状态翻转阈值:连续 2 次失败才判定故障、连续 2 次成功才恢复,避免抖动。脑裂防护的要点:
- 防误切:健康检查只看 HAProxy 自身;如果检查里包含「下游 PostgreSQL 是否可用」,PG 抖动会导致 VIP 无谓漂移。
- 防双主 VIP:脑裂的本质是两台都听不到对方又都认为对方死了。
unicast_peer单播(云环境必配)+ 同网段防火墙放行 VRRP(IP 协议号 112)+ 独立心跳链路,能把概率压到极低。 - 降低危害:VIP 双活期间客户端可能连到两台 HAProxy,但写流量最终只指向一个 PostgreSQL 主库(由 Patroni 保证),因此危害限于读流量分裂,而不是数据分叉。
nopreempt避免恢复瞬间二次抖动:使用时两边state都写BACKUP、仅用priority区分先后。
Keepalived 不校验 PostgreSQL 角色
Keepalived 只保证「VIP 落在健康的那台 HAProxy 上」,不保证「HAProxy 后面的 PostgreSQL 是主库」。角色正确性完全由 HAProxy 的健康检查(Patroni REST API)保证,两层职责不要混淆。
与 PgBouncer 的组合部署
HAProxy 与 PgBouncer 的位置有两种摆法,语义不同:
摆法一:HAProxy 在前(推荐)。 应用 → HAProxy → PgBouncer → PostgreSQL。PgBouncer 部署在数据库节点上(或独立部署),HAProxy 的 backend 直接指向各节点的 PgBouncer 端口 6432,健康检查仍走 Patroni 8008:
backend pg_write_pool
option httpchk GET /primary
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node1 192.168.1.11:6432 check port 8008
server node2 192.168.1.12:6432 check port 8008
server node3 192.168.1.13:6432 check port 8008这样故障转移时 HAProxy 直接把应用的新连接导向新主库上的 PgBouncer,链路里没有过期的连接缓存。
摆法二:PgBouncer 在前。 应用 → PgBouncer → HAProxy → PostgreSQL。PgBouncer 的 [databases] 指向 HAProxy 的写端口,由 HAProxy 决定真实落点。优点是 PgBouncer 集中部署一份;缺点是故障转移后 PgBouncer 池中残留的旧连接需要 RECONNECT 清理,多一个切换步骤。
两种摆法都成立,关键是不要在故障转移脚本里引入人工步骤。另外注意 TCP 代理链路中每跳的超时(应用 → HAProxy → PgBouncer → PG)应从外到内递减,避免内层超时先触发导致外层拿到无意义的错误。
监控统计与常见问题
HAProxy 自带两套监控出口:
global
stats socket /run/haproxy/admin.sock mode 660 level admin
listen stats
mode http
bind *:8404
stats enable
stats uri /stats
stats auth admin:Ch4ngeM3_Str0ng# 命令行查看后端状态(UP/DOWN、连接数、检查结果)
echo "show stat" | socat stdio /run/haproxy/admin.sock | head -n 5
# 或直接抓取统计页
curl -s -u admin:Ch4ngeM3_Str0ng http://127.0.0.1:8404/stats\;csv | head常见问题集中在三件事:
- 会话保持。TCP 模式没有 HTTP cookie 可用;PostgreSQL 场景通常也不需要粘住节点——写端口单活、读端口各库等价。确需按来源粘住时用
balance source,但要想清楚它会把某个大客户端的所有连接压在同一节点上。 - 真实客户端 IP。TCP 代理会让后端看到的源地址变成 HAProxy 的地址。解法是 PROXY 协议:HAProxy 侧对相应 server 写
send-proxy,后端支持 PROXY 协议时才能还原真实 IP(PgBouncer 1.15 起支持 PROXY 协议 v1/v2,PostgreSQL 本体不支持,需要 pg_hba 层面另行评估)。多数场景用日志层面记录客户端 IP 就够了。 - 日志。
option tcplog输出的默认格式已含客户端地址、前端/后端名、选中节点、连接时长与字节数,配合log-format可自定义;排查「连接被谁断开」时它是第一现场。
实战示例
示例一:完整生产配置(读写分离 + 统计)
global
log 127.0.0.1 local0
maxconn 4096
daemon
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
defaults
log global
mode tcp
option tcplog
timeout connect 5s
timeout client 24h
timeout server 24h
# ---------- 写端口:只发主库 ----------
frontend pg_write
bind *:5000
default_backend pg_primary
backend pg_primary
option httpchk GET /primary
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node1 192.168.1.11:5432 check port 8008
server node2 192.168.1.12:5432 check port 8008
server node3 192.168.1.13:5432 check port 8008
# ---------- 读端口:只发备库 ----------
frontend pg_read
bind *:5001
default_backend pg_replicas
backend pg_replicas
balance leastconn
option httpchk GET /replica
http-check expect status 200
default-server inter 3s fall 3 rise 1 on-marked-down shutdown-sessions
server node1 192.168.1.11:5432 check port 8008
server node2 192.168.1.12:5432 check port 8008
server node3 192.168.1.13:5432 check port 8008
# ---------- 统计页 ----------
listen stats
mode http
bind *:8404
stats enable
stats uri /stats
stats auth admin:Ch4ngeM3_Str0ng示例二:无 Patroni 时的 pgsql-check 兜底方案
只有「一主一备、由脚本保证角色」的简单环境,可以用内置检查:
backend pg_native
option pgsql-check user haproxy_check
server db1 192.168.1.11:5432 check inter 2s fall 3 rise 2-- 在两个节点都创建只用于健康检查的账号
CREATE ROLE haproxy_check WITH LOGIN PASSWORD 'Ch4ngeM3_Str0ng';# pg_hba.conf 放行(按需收紧网段)
host all haproxy_check 192.168.1.0/24 scram-sha-256示例三:Keepalived 主备双机
主节点 /etc/keepalived/keepalived.conf(备节点仅 state/priority 两行不同):
global_defs {
router_id haproxy_pg_1
enable_script_security
script_user root
}
vrrp_script chk_haproxy {
script "/usr/local/bin/check_haproxy.sh"
interval 2
timeout 1
fall 2
rise 2
}
vrrp_instance VI_PG {
state MASTER
interface eth0
virtual_router_id 51
priority 150
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass Pg_VIP_Key
}
virtual_ipaddress {
192.168.1.100/24 dev eth0
}
track_script {
chk_haproxy
}
unicast_src_ip 192.168.1.21
unicast_peer {
192.168.1.22
}
notify_master "/usr/local/bin/vip_notify.sh master"
notify_backup "/usr/local/bin/vip_notify.sh backup"
notify_fault "/usr/local/bin/vip_notify.sh fault"
}nopreempt 与 state 的配合
nopreempt 只有在初始 state BACKUP 的实例上才生效。若主节点写 state MASTER,故障恢复后仍会抢占 VIP,nopreempt 被忽略。正确的免抢占写法是:两边都写 state BACKUP,用 priority 决定初次归属。
示例四:验证健康检查与故障转移
# 1. 三台节点的 Patroni 端点语义确认
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.11:8008/primary # 主库应为 200
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.12:8008/primary # 备库应为 503
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.12:8008/replica # 备库应为 200
# 2. HAProxy 统计确认各 server 状态
echo "show stat" | socat stdio /run/haproxy/admin.sock | cut -d, -f1-2,18 | grep pg_pg_primary,node1,UP
pg_primary,node2,DOWN
pg_primary,node3,DOWN
pg_replicas,node1,DOWN
pg_replicas,node2,UP
pg_replicas,node3,UP只有 node1 的写后端是 UP,只有 node2/node3 的读后端是 UP——这正是角色路由的正确形态。
# 3. 从写端口建立会话确认落在主库
psql "host=192.168.1.100 port=5000 dbname=mydb user=app" -c "SELECT pg_is_in_recovery();" pg_is_in_recovery
-------------------
f
(1 row)注意事项
健康检查端口与流量端口必须分开。 流量走 5432、检查走 8008,一行 check port 8008 缺了就会把 HTTP 检查发到 PostgreSQL 协议端口,检查必然失败且日志里充满协议错误。
on-marked-down shutdown-sessions 会切断现存连接。 这是特性而非缺陷:节点角色变化后,旧连接上的事务已经没有意义。但它意味着应用必须容忍连接被断开并重连。
故障转移瞬间写端口可能短暂无可用后端。 旧主库检查失败、新主库检查通过之间存在秒级窗口,应用必须有重试;窗口长短由 inter/fall/rise 与 Patroni 的提升速度共同决定。
读端口流量要容忍复制延迟。 HAProxy 只判断「是备库」,不判断「延迟多少」;需要延迟约束时使用 Patroni /replica 端点的 lag 参数或 /sync 端点。
timeout client/server 不要设短。 数据库连接长空闲是常态,短超时会反复掐断正常会话;连接清理交给 PgBouncer 或应用。
Keepalived 的 virtual_router_id 同网段必须唯一。 两套不相干的 HAProxy 集群用了同一个 ID,VRRP 会互相干扰,表现为 VIP 莫名漂移。
云上环境必须配 unicast_peer。 多数公有云 VPC 不支持组播,VRRP 组播通告发不出去,备节点永远收不到通告——这是云上 Keepalived 脑裂的头号原因。
Keepalived 与 HAProxy 的健康检查要解耦。 vrrp_script 只看 HAProxy 自身;把 PostgreSQL 状态揉进去会让 VIP 随数据库抖动而漂移,放大故障。
统计页要加认证且不要暴露公网。 stats uri /stats 无认证等于把后端拓扑和连接明细公开,务必 stats auth 并限制来源。
HAProxy 不解析 SQL,也不保证事务完整性。 连接中断就是中断,正在进行的事务被回滚;长事务场景的应用要有幂等重试设计。
最佳实践
Patroni 集群一律用
httpchk+ REST API 做健康检查。 只有它能同时回答「活着吗」和「是什么角色」两个问题,这是角色路由的基石。读写分端口,写端口不配
balance。 5000/5001 两个端口语义清晰,应用按需选择端口,比 SQL 解析型中间件简单可靠得多。所有 backend 统一开启
on-marked-down shutdown-sessions。 故障转移后立刻斩断指向旧角色的连接,是「切换后应用自动恢复」的必要条件。inter/fall/rise参数化调优并演练。inter 3s fall 3意味着最长约 9 秒才摘除故障节点;对 RTO 敏感的系统缩短间隔,但要权衡检查风暴。HAProxy 双机 + Keepalived VIP,且全用 unicast。 云环境尤其如此;同时用防火墙明确放行 VRRP 协议(IP 协议号 112)。
免抢占配置(双
state BACKUP+nopreempt)。 避免主节点恢复瞬间 VIP 二次漂移造成抖动,让 VIP 稳定跟随当前健康节点。PgBouncer 放在 HAProxy 之后、数据库节点上。 故障转移时新连接直达新主库的池,无需清理旧池;若 PgBouncer 集中部署在前,切换脚本必须包含
RECONNECT。用
show stat+ 统计页做双通道监控。 命令行喂给 Prometheus(有现成的 haproxy exporter),统计页留给人工排障;关注每台 server 的 UP/DOWN 翻转与 session 计数。配置变更先
haproxy -c -f再 reload。 reload 不中断存量连接,是加节点、改检查的标准方式;重启才是兜底。把切换演练写成脚本并定期执行。 手动 kill 主库 → 观察 HAProxy 统计翻转 → 应用重连,全链路时间记录下来,这才是真实的 RTO。演练流程参见灾难恢复演练。
每跳超时从外到内递减。 应用侧 > HAProxy > PgBouncer > PostgreSQL,保证最内层先暴露问题,外层拿到的是可解释的错误而不是悬空等待。
统计页与 stats socket 都要限制访问。 拓扑信息本身也是敏感数据;
stats auth+ 绑定内网地址是最低要求。
下一节
继续学习:Patroni 高可用方案