Skip to content

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 连接池

基础概念 ​

概念英文 / 记法说明
前端frontendHAProxy 对外监听的入口(地址 + 端口)
后端backend一组真实服务器与转发策略的集合
监听段listenfrontend + backend 合二为一的简写段
健康检查health check判定后端节点是否可用的探测(TCP/HTTP/PostgreSQL)
角色感知路由role-aware routing依据节点角色(主/备)而不是单纯存活来分流
负载均衡算法balance在多个可用节点间分配连接的策略
虚拟 IPVIP对外统一服务的 IP,随主节点漂移
VRRPVirtual Router Redundancy ProtocolKeepalived 用来协商 VIP 归属的协议
脑裂split-brain两台节点都认为自己是 VIP 持有者的故障状态

HAProxy 在 PostgreSQL 高可用中的角色 ​

一套典型的高可用链路里,每个组件回答一个问题:

text
┌──────────┐      ┌──────────────────┐      ┌───────────┐      ┌─────────────┐
│  应用     │ ───→ │ 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 集群的骨架:

conf
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

语法校验与启停:

bash
# 校验配置文件语法(改配置后必做)
haproxy -c -f /etc/haproxy/haproxy.cfg

sudo systemctl enable --now haproxy
sudo systemctl reload haproxy

Keepalived 的配置文件是 /etc/keepalived/keepalived.conf,核心是 vrrp_script 与 vrrp_instance 两个段,完整示例见后文「实战示例」。

详细说明 ​

mode tcp 基础配置与超时 ​

PostgreSQL 是 TCP 协议,所以 HAProxy 一律 mode tcp。三个段的关系:frontend 定义「从哪进」,backend 定义「到哪去、怎么挑」,listen 把两者写进一段,适合简单场景:

conf
# 写法一: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 connect3s~5sHAProxy 建立到后端连接的超时,不宜过长
timeout client24h前端连接不活动的最大时长。数据库连接天然长空闲,设短了会反复掐断空闲会话
timeout server24h后端连接不活动的最大时长,一般与 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」:

conf
backend pg_native
    option pgsql-check user haproxy_check
    server db1 192.168.1.11:5432 check inter 2s

它只需要数据库里存在可登录的检查账号,不依赖任何外部组件。但它分不清主库和备库——备库也会应答成功,所以只能用于「转发到某个确定角色的库」由外部保证的场景。

其二,TCP 探测 option tcp-check。只验证端口可达,最廉价也最粗糙,一般只做兜底:

conf
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 探测它,就同时拿到了存活 + 角色两个信息:

conf
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能PatroniPatroni 集群(生产标准方案)

基于 Patroni REST API 的角色感知路由 ​

Patroni REST API 的端点语义是角色路由的全部基础(完整列表见下一页,这里只列路由相关的):

端点返回 200 的条件返回 503 的条件
/primary(旧名 /master)当前节点是主库当前节点是备库或未就绪
/replica当前节点是备库且数据足够新当前节点是主库或落后过多
/health节点正常运行(不限角色)节点异常
/read-only当前节点可提供只读服务不可提供

据此可以构建更细的读流量分层——例如把「同步备库」单独成一个 backend,供对延迟敏感的读服务使用:

conf
# 只把读流量发给同步备库
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 只指向备库。要点有三:

  1. 写 backend 天然就是「单活」的。多个 server 行不是「多台主库」,而是「任何时候只有一台检查通过」,这正是角色路由的效果,不需要额外的 active/passive 逻辑。
  2. 读流量必须容忍复制延迟。备库落后是常态,走 5001 的查询要做好「刚写的数据读不到」的准备;要求读己之写的请求必须回落到写端口。流复制原理参见流复制原理。
  3. 切换期间读写端口同时收敛。旧主库降级、新主库提升的几秒内,5000 端口可能短暂无可用节点(所有检查失败),应用的重试机制此时接管;5001 端口则始终至少有备库可用。
conf
# 写:单活,指向主库
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
interfaceVRRP 通告与 VIP 绑定的网卡
virtual_router_id虚拟路由器 ID(1~255),同网段唯一
priority优先级(1~255),数值大者当选
advert_int通告间隔(秒),默认 1
authentication通告的认证口令(防误配,非加密手段)
virtual_ipaddressVIP 列表,可带掩码与设备名
track_script关联的健康检查脚本,失败时降级优先级或移交 VIP
notify_master / notify_backup / notify_fault / notify角色变化时触发的钩子脚本
nopreempt抢占开关:开启后故障恢复的一方不夺回 VIP,避免二次切换
unicast_peer单播对端列表,用于禁用组播的环境(公有云 VPC 必备)

主备两台 HAProxy 的差异化配置只有 state/priority 两行,其余完全一致:

conf
# 主节点(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 真的在服务」,而不只是「进程存在」:

bash
#!/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 0
conf
vrrp_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:

conf
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 自带两套监控出口:

conf
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
bash
# 命令行查看后端状态(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

常见问题集中在三件事:

  1. 会话保持。TCP 模式没有 HTTP cookie 可用;PostgreSQL 场景通常也不需要粘住节点——写端口单活、读端口各库等价。确需按来源粘住时用 balance source,但要想清楚它会把某个大客户端的所有连接压在同一节点上。
  2. 真实客户端 IP。TCP 代理会让后端看到的源地址变成 HAProxy 的地址。解法是 PROXY 协议:HAProxy 侧对相应 server 写 send-proxy,后端支持 PROXY 协议时才能还原真实 IP(PgBouncer 1.15 起支持 PROXY 协议 v1/v2,PostgreSQL 本体不支持,需要 pg_hba 层面另行评估)。多数场景用日志层面记录客户端 IP 就够了。
  3. 日志。option tcplog 输出的默认格式已含客户端地址、前端/后端名、选中节点、连接时长与字节数,配合 log-format 可自定义;排查「连接被谁断开」时它是第一现场。

实战示例 ​

示例一:完整生产配置(读写分离 + 统计) ​

conf
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 兜底方案 ​

只有「一主一备、由脚本保证角色」的简单环境,可以用内置检查:

conf
backend pg_native
    option pgsql-check user haproxy_check
    server db1 192.168.1.11:5432 check inter 2s fall 3 rise 2
sql
-- 在两个节点都创建只用于健康检查的账号
CREATE ROLE haproxy_check WITH LOGIN PASSWORD 'Ch4ngeM3_Str0ng';
ini
# pg_hba.conf 放行(按需收紧网段)
host  all  haproxy_check  192.168.1.0/24  scram-sha-256

示例三:Keepalived 主备双机 ​

主节点 /etc/keepalived/keepalived.conf(备节点仅 state/priority 两行不同):

conf
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 决定初次归属。

示例四:验证健康检查与故障转移 ​

bash
# 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_
text
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——这正是角色路由的正确形态。

bash
# 3. 从写端口建立会话确认落在主库
psql "host=192.168.1.100 port=5000 dbname=mydb user=app" -c "SELECT pg_is_in_recovery();"
text
 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,也不保证事务完整性。 连接中断就是中断,正在进行的事务被回滚;长事务场景的应用要有幂等重试设计。

最佳实践 ​

  1. Patroni 集群一律用 httpchk + REST API 做健康检查。 只有它能同时回答「活着吗」和「是什么角色」两个问题,这是角色路由的基石。

  2. 读写分端口,写端口不配 balance。 5000/5001 两个端口语义清晰,应用按需选择端口,比 SQL 解析型中间件简单可靠得多。

  3. 所有 backend 统一开启 on-marked-down shutdown-sessions。 故障转移后立刻斩断指向旧角色的连接,是「切换后应用自动恢复」的必要条件。

  4. inter/fall/rise 参数化调优并演练。 inter 3s fall 3 意味着最长约 9 秒才摘除故障节点;对 RTO 敏感的系统缩短间隔,但要权衡检查风暴。

  5. HAProxy 双机 + Keepalived VIP,且全用 unicast。 云环境尤其如此;同时用防火墙明确放行 VRRP 协议(IP 协议号 112)。

  6. 免抢占配置(双 state BACKUP + nopreempt)。 避免主节点恢复瞬间 VIP 二次漂移造成抖动,让 VIP 稳定跟随当前健康节点。

  7. PgBouncer 放在 HAProxy 之后、数据库节点上。 故障转移时新连接直达新主库的池,无需清理旧池;若 PgBouncer 集中部署在前,切换脚本必须包含 RECONNECT。

  8. 用 show stat + 统计页做双通道监控。 命令行喂给 Prometheus(有现成的 haproxy exporter),统计页留给人工排障;关注每台 server 的 UP/DOWN 翻转与 session 计数。

  9. 配置变更先 haproxy -c -f 再 reload。 reload 不中断存量连接,是加节点、改检查的标准方式;重启才是兜底。

  10. 把切换演练写成脚本并定期执行。 手动 kill 主库 → 观察 HAProxy 统计翻转 → 应用重连,全链路时间记录下来,这才是真实的 RTO。演练流程参见灾难恢复演练。

  11. 每跳超时从外到内递减。 应用侧 > HAProxy > PgBouncer > PostgreSQL,保证最内层先暴露问题,外层拿到的是可解释的错误而不是悬空等待。

  12. 统计页与 stats socket 都要限制访问。 拓扑信息本身也是敏感数据;stats auth + 绑定内网地址是最低要求。

下一节

继续学习:Patroni 高可用方案

参考链接 ​