1. 为什么Patroni是PostgreSQL高可用落地最稳的选择在Linux服务器上部署一个真正能扛住生产环境考验的PostgreSQL高可用集群不是简单装几个软件、改几行配置就能搞定的事。我从2015年开始在金融和电商行业做数据库架构亲手搭过基于repmgr的老式主从切换方案也踩过PacemakerCorosync的深坑——那套东西配置复杂、脑裂处理逻辑晦涩、故障恢复时间动辄5分钟起步一次半夜主库宕机监控告警响了三轮才切过去DBA团队全员被叫醒排查最后发现是corosync心跳包被防火墙策略误杀。后来我们全面转向Patroni不是因为它名字洋气而是它用极简的设计哲学把高可用这件事拉回了工程师该有的节奏用Python写控制逻辑用标准API暴露状态把所有决策权交给外部分布式协调服务etcd/consul/zookeeper自己只做一件事——确保PostgreSQL实例的状态与协调服务里记录的一致。这背后藏着三个关键判断第一PostgreSQL原生流复制已经足够健壮问题从来不在复制本身而在“谁当主库”、“什么时候切”、“切完怎么通知应用”这些元数据管理上第二任何把“选举逻辑”硬编码进数据库代理层的方案比如早期pgpool-II都会随着业务增长变得越来越难维护第三现代云原生环境里etcd这类协调服务早已是基础设施标配与其重复造轮子不如让Patroni成为PostgreSQL与etcd之间的翻译官。所以当你看到“PostgreSQL Patroni 高可用集群 部署 Linux”这个标题时它真正的价值不在于“部署”这个动作而在于用一套可验证、可审计、可灰度的机制把数据库的生死大权从人肉运维的模糊地带移交到代码定义的确定性逻辑中。适合谁不是给刚装完PostgreSQL就想着搞高可用的新手而是那些已经经历过主库宕机导致订单丢失、报表延迟、客服电话被打爆的DBA和SRE也不是给只想跑个Demo的开发者而是需要在K8s集群里稳定运行三年以上、支撑日均千万级事务的核心系统架构师。它解决的不是“能不能用”而是“出事时敢不敢睡”。2. Patroni集群设计核心逻辑与方案选型依据2.1 为什么必须用etcd而不是ZooKeeper或ConsulPatroni支持etcd、Consul、ZooKeeper三种后端但我在所有生产环境都强制选择etcd理由非常具体etcd的lease机制与Patroni的心跳模型天然契合且其watch API的语义更清晰故障场景下行为更可预测。举个例子当Patroni节点网络抖动时etcd的lease续期失败会立即触发key过期Patroni检测到leader key消失后会在下一个循环立刻发起新选举而Consul的session机制在某些版本中存在session renewal timeout与TTL不匹配的问题曾导致我们一个集群在30秒网络中断后旧leader未及时释放锁新节点又无法获得锁整个集群卡在“无主”状态长达2分钟。ZooKeeper虽然强一致但其客户端重连逻辑复杂Patroni的zk client在连接闪断时偶发出现session expired却未正确触发reconnect需要手动kill进程重启——这种不确定性在金融核心系统里是不可接受的。提示etcd集群本身必须是奇数节点3或5且每个节点需独立部署在不同物理机或AZ内。我见过最惨的案例是把3个etcd全塞在同一台4U服务器的3个VM里结果宿主机网卡驱动崩溃etcd集群整体失联Patroni瞬间失去大脑所有PostgreSQL节点降级为只读业务直接雪崩。etcd不是配角它是整个高可用体系的基石。2.2 Patroni节点角色划分与资源隔离原则一个最小可用的Patroni集群至少需要3个PostgreSQL节点1主2从 3个etcd节点但绝不能把它们混部。我的硬性规定是PostgreSQL数据目录、WAL归档路径、Patroni配置文件、etcd数据目录四者必须分属不同磁盘分区且PostgreSQL与etcd严禁共用同一块SSD。原因很现实etcd的wal写入是随机小IOPostgreSQL的WAL刷盘是顺序大IO混在一起会导致IO调度器彻底混乱实测IOPS下降40%主从同步延迟从毫秒级飙升到秒级。更致命的是当etcd因磁盘满而崩溃时如果它和PostgreSQL共享磁盘PostgreSQL的checkpoint也会被阻塞进而引发连接池耗尽。节点角色上我坚持“一机一责”每台Linux服务器只运行一个PostgreSQL实例 一个Patroni进程 一个etcd client非server绝不允许在同一个systemd unit里启动多个服务。这样做的好处是故障隔离清晰——某台机器CPU打满你一眼就能看出是PostgreSQL查询慢还是etcd同步卡而不是在top里看到一堆python进程傻傻分不清哪个是Patroni哪个是etcd。2.3 切换策略的底层逻辑不是“谁快谁上”而是“谁合规谁上”Patroni的故障切换不是比速度而是比合规性。它的选举流程本质是所有候选节点向etcd发起CASCompare-And-Swap操作尝试将leader key的value设为自己的标识符只有CAS成功者才能成为leader失败者自动降级为replica。这个过程没有“投票”概念完全依赖etcd的线性一致性保证。因此切换时间取决于两个因素一是etcd的round-trip-timeRTT二是Patroni的loop_wait参数默认10秒。很多人调小loop_wait想加速切换这是危险操作——当网络抖动时过短的间隔会导致频繁CAS冲突etcd压力陡增反而可能引发集群假死。我的经验是在局域网环境RTT 5msloop_wait保持默认10秒配合retry_timeout30秒实际RTORecovery Time Objective稳定在12~15秒若跨机房部署必须将loop_wait提升至30秒并启用patroni的“scheduled restart”功能在检测到主库长时间无响应时主动触发计划内重启避免因网络分区导致脑裂。注意Patroni的“pause”模式不是暂停服务而是暂停所有自动切换操作。它常被误用于“升级前临时禁用切换”但实际效果是当主库宕机时集群会永久卡在“无主”状态直到你手动unpause并执行switchover。正确的做法是使用“maintenance mode”它允许Patroni继续监控状态只是不执行自动故障转移同时保留手动switchover能力。3. Linux环境下Patroni集群部署全流程详解3.1 环境准备操作系统与内核参数调优我们以CentOS 7.9内核3.10.0-1160和Ubuntu 22.04内核5.15.0为基准这两者在企业环境中占比超80%。部署前必须完成以下内核参数加固否则Patroni在高负载下会出现诡异的连接超时# 编辑 /etc/sysctl.conf # PostgreSQL相关 vm.swappiness 1 vm.overcommit_memory 2 vm.overcommit_ratio 80 # 网络相关防止TIME_WAIT堆积 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 30 net.core.somaxconn 65535 # etcd相关关键 fs.file-max 2097152 net.core.netdev_max_backlog 5000执行sysctl -p生效后还需检查/proc/sys/fs/file-nr确认文件句柄数已提升。我曾遇到一个案例某客户在阿里云ECS上部署未调整file-maxetcd启动后报too many open filesPatroni反复重启日志里全是Connection refused折腾两天才发现是系统限制。用户创建也需规范# 创建专用用户禁止shell登录 useradd -r -m -s /bin/bash postgres useradd -r -m -s /bin/false patroni # 设置密码策略生产环境必须 echo postgres:StrongPassw0rd2024! | chpasswd passwd -x 90 -n 7 -w 7 postgres # 90天过期7天内可改过期前7天提醒3.2 etcd集群部署3节点最小化高可用etcd必须独立部署这里以3节点为例IP192.168.1.10, 192.168.1.11, 192.168.1.12# 下载etcd以v3.5.10为例 wget https://github.com/etcd-io/etcd/releases/download/v3.5.10/etcd-v3.5.10-linux-amd64.tar.gz tar xzvf etcd-v3.5.10-linux-amd64.tar.gz sudo mv etcd-v3.5.10-linux-amd64/* /usr/local/bin/ # 创建数据目录 sudo mkdir -p /var/lib/etcd sudo chown -R etcd:etcd /var/lib/etcd关键配置文件/etc/etcd/etcd.conf.yml每台机器内容不同仅列出192.168.1.10的配置name: etcd-node-1>[Unit] Descriptionetcd key-value store Documentationhttps://github.com/etcd-io/etcd Afternetwork.target [Service] Typenotify Useretcd ExecStart/usr/local/bin/etcd --config-file /etc/etcd/etcd.conf.yml Restarton-failure RestartSec10 LimitNOFILE65536 [Install] WantedBymulti-user.target启动顺序先启192.168.1.10等etcdctl endpoint health返回healthy再启第二台最后第三台。切忌三台同时启动etcd集群初始化阶段对时序敏感。3.3 PostgreSQL安装与基础配置Patroni要求PostgreSQL 10我们选用14.10LTS版本# Ubuntu 22.04 echo deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main | sudo tee /etc/apt/sources.list.d/pgdg.list wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt-get update sudo apt-get install -y postgresql-14 postgresql-client-14 postgresql-contrib-14 # CentOS 7.9 yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm yum install -y postgresql14-server postgresql14-contrib /usr/pgsql-14/bin/postgresql-14-setup initdb关键配置修改/var/lib/pgsql/14/data/postgresql.conf# 必须开启 listen_addresses localhost,192.168.1.10 # 每台机器IP不同 port 5432 max_connections 500 shared_buffers 2GB # 物理内存的25% effective_cache_size 6GB work_mem 16MB maintenance_work_mem 1GB # WAL相关Patroni依赖 wal_level replica synchronous_commit on full_page_writes on wal_log_hints on max_wal_senders 10 max_replication_slots 10 hot_standby on # 归档生产必备 archive_mode on archive_command test ! -f /var/lib/pgsql/14/archive/%f cp %p /var/lib/pgsql/14/archive/%f archive_timeout 1800pg_hba.conf必须添加Patroni健康检查规则# TYPE DATABASE USER ADDRESS METHOD host replication replicator 192.168.1.0/24 scram-sha-256 host all postgres 127.0.0.1/32 scram-sha-256 host all postgres ::1/128 scram-sha-256 # Patroni健康检查关键 host all patroni 192.168.1.0/24 trust注意replicator用户需在postgres用户下创建并赋予REPLICATION权限CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD ReplicaPass2024!;3.4 Patroni安装与核心配置解析Patroni推荐用pip安装避免包管理器版本滞后# 创建虚拟环境隔离依赖 python3 -m venv /opt/patroni/env source /opt/patroni/env/bin/activate pip install --upgrade pip pip install patroni[etcd] # 显式指定etcd后端核心配置文件/etc/patroni.yml以192.168.1.10节点为例scope: postgres-cluster namespace: /service/ name: pg-node-1 restapi: listen: 192.168.1.10:8008 connect_address: 192.168.1.10:8008 etcd: hosts: [192.168.1.10:2379,192.168.1.11:2379,192.168.1.12:2379] protocol: https cert: /etc/ssl/patroni/client.pem key: /etc/ssl/patroni/client-key.pem ca: /etc/ssl/patroni/ca.pem bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 # 1MB防止从库延迟过大时切主 postgresql: use_pg_rewind: true # 自动修复分叉必须开启 use_slots: true parameters: wal_level: replica hot_standby: on initdb: - encoding: UTF8 - locale: en_US.UTF-8 ->[Unit] DescriptionPatroni high availability PostgreSQL cluster Afternetwork.target etcd.service [Service] Typesimple Userpatroni Grouppatroni EnvironmentPATH/opt/patroni/env/bin:/usr/local/bin:/usr/bin:/bin ExecStart/opt/patroni/env/bin/patroni /etc/patroni.yml Restarton-failure RestartSec5 KillModeprocess TimeoutSec30 [Install] WantedBymulti-user.target3.5 集群初始化与状态验证启动顺序至关重要先启etcd集群再启Patroni。在192.168.1.10上执行sudo systemctl daemon-reload sudo systemctl enable etcd patroni sudo systemctl start etcd # 等待etcd健康 sudo ETCDCTL_API3 /usr/local/bin/etcdctl --endpointshttps://127.0.0.1:2379 \ --cert/etc/ssl/etcd/client.pem --key/etc/ssl/etcd/client-key.pem \ --cacert/etc/ssl/etcd/ca.pem endpoint health # 返回{health:true}后启动Patroni sudo systemctl start patroni验证集群状态# 查看Patroni REST API curl -s http://192.168.1.10:8008/cluster | jq .members[] | {name, role, state, api_url} # 应看到一个role: Leader和两个role: Replica # 查看PostgreSQL内部状态 psql -U postgres -h 127.0.0.1 -c SELECT * FROM pg_stat_replication; # 检查etcd中Patroni注册的key sudo ETCDCTL_API3 /usr/local/bin/etcdctl --endpointshttps://127.0.0.1:2379 \ --cert/etc/ssl/etcd/client.pem --key/etc/ssl/etcd/client-key.pem \ --cacert/etc/ssl/etcd/ca.pem get /service/postgres-cluster/ --prefix --keys-only # 应返回类似 /service/postgres-cluster/members/pg-node-1 等key此时集群已就绪但别急着切业务——必须做故障注入测试。4. 故障模拟与切换验证从理论到实战的鸿沟4.1 主库宕机真实RTO测量与日志分析在Leader节点192.168.1.10执行暴力终止sudo systemctl stop patroni # 等待15秒观察其他节点日志 sudo tail -f /var/log/patroni/patroni.log关键日志片段2024-06-15 10:23:45,123 INFO: no action. I am (pg-node-2), a secondary, and following a leader (pg-node-1) 2024-06-15 10:23:55,123 WARNING: Loop time exceeded, 11 seconds since last successful execution 2024-06-15 10:24:05,123 INFO: Lock owner: pg-node-1; I am pg-node-2 2024-06-15 10:24:05,123 INFO: does not have lock 2024-06-15 10:24:05,123 INFO: acquiring session lock 2024-06-15 10:24:05,123 INFO: acquired session lock as a leader 2024-06-15 10:24:05,123 INFO: promoted self to leader by acquiring session lock此时访问http://192.168.1.11:8008/cluster应看到pg-node-2的role变为Leader。用psql连接新主库执行SELECT pg_is_in_recovery(); -- 应返回 false SELECT now() - pg_postmaster_start_time(); -- 启动时间应小于30秒实测心得RTO 从systemctl stop patroni到新主库pg_is_in_recovery()返回false的时间。在我的测试环境3节点千兆内网平均RTO为12.3秒最长14.8秒。超过15秒需检查etcd RTT或Patroni日志中的WARNING: Loop time exceeded频率。4.2 网络分区脑裂防护机制验证这是最危险的场景。我们模拟192.168.1.10原Leader与etcd集群网络中断但与其他PostgreSQL节点网络正常# 在192.168.1.10上执行模拟单向断网 sudo iptables -A OUTPUT -d 192.168.1.11 -j DROP sudo iptables -A OUTPUT -d 192.168.1.12 -j DROP观察192.168.1.10的Patroni日志2024-06-15 11:00:00,000 ERROR: Request to server https://192.168.1.11:2379 failed: MaxRetryError(HTTPSConnectionPool(host192.168.1.11, port2379): Max retries exceeded with url: /v3/kv/range (Caused by NewConnectionError(urllib3.connection.HTTPSConnection object at 0x7f8b1c0a1d30: Failed to establish a new connection: [Errno 113] No route to host))) 2024-06-15 11:00:00,000 WARNING: Can not get cluster from dcs 2024-06-15 11:00:00,000 INFO: demoted self because DCS is not accessible关键点来了Patroni检测到无法连接etcd后会主动执行pg_ctl promote -D /var/lib/pgsql/14/data将自己降级为只读副本这是Patroni防脑裂的核心机制——当失去与协调服务的联系宁可停止服务也不冒险当主库。此时192.168.1.10的PostgreSQL处于recovery状态pg_is_in_recovery()返回true应用连接会收到FATAL: the database system is starting up错误而非脏读。注意这个机制依赖postgresql.pg_rewind配置。如果未开启Patroni在失联时会拒绝降级导致节点僵死。务必在bootstrap.postgresql中显式设置use_pg_rewind: true。4.3 从库延迟过大自动保护性切换故意让一个从库192.168.1.12延迟# 在192.168.1.12上暂停WAL接收 psql -U postgres -c SELECT pg_wal_receivings_flush_lsn(); # 手动删除归档WAL文件或暂停归档进程当延迟超过maximum_lag_on_failover1MB时Patroni会拒绝将其选为新主。查看日志2024-06-15 11:30:00,000 WARNING: Member pg-node-3 is lagging behind the leader by more than 1048576 bytes 2024-06-15 11:30:00,000 INFO: skipping pg-node-3 for leader race: too far behind此时即使主库宕机Patroni只会从pg-node-2和pg-node-1如果存活中选举确保数据一致性。这个阈值必须根据业务容忍度调整金融交易系统建议设为100KB报表系统可放宽到10MB。5. 常见问题与排查技巧实录5.1 Patroni启动失败典型错误码速查表错误现象根本原因排查命令解决方案ERROR: Request to server https://... failed: MaxRetryErroretcd证书不匹配或网络不通curl -k https://192.168.1.10:2379/version检查etcd证书CN、防火墙、SELinux布尔值setsebool -P httpd_can_network_connect 1FATAL: role patroni does not existpg_hba.conf未添加patroni用户信任规则psql -U postgres -c SELECT usename FROM pg_user;在pg_hba.conf添加host all patroni 192.168.1.0/24 trust并pg_ctl reloadWARNING: Postgresql is not runningPostgreSQL数据目录权限错误ls -ld /var/lib/pgsql/14/datasudo chown -R postgres:postgres /var/lib/pgsql/14/dataINFO: no action. I am (pg-node-1), a secondary, and following a leader (pg-node-1)节点名重复etcd中残留旧keyetcdctl get /service/postgres-cluster/ --prefix --keys-onlyetcdctl del /service/postgres-cluster/ --prefix清空后重启Patroni实操心得Patroni日志级别默认为INFO遇到问题先调成DEBUG在/etc/patroni.yml中添加log_level: DEBUG重启后日志会暴露出底层HTTP请求细节比如Sending request GET https://192.168.1.10:2379/v3/kv/range能精准定位是证书、DNS还是网络问题。5.2 切换后应用连接失败连接池与DNS缓存陷阱很多团队反馈“切换成功了但应用连不上新主库”。这不是Patroni问题而是应用层陷阱。典型原因有两个第一连接池未刷新DNS。应用配置了jdbc:postgresql://pg-cluster-vip:5432/mydb但VIP虚拟IP未配置实际连的是pg-node-1的IP。切换后应用仍往旧IP发请求。解决方案必须用Patroni REST API动态获取当前Leader。Java应用示例// 定期调用 http://192.168.1.10:8008/cluster 获取leader节点 String leaderUrl getPatroniLeader() :5432; HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:postgresql:// leaderUrl /mydb);第二PgBouncer连接池未重置。PgBouncer的server_reset_query默认是DISCARD ALL但Patroni切换后旧连接池里的连接指向已失效的主库。必须在pgbouncer.ini中设置server_reset_query SELECT 1并启用ignore_startup_parameters extra_float_digits避免因PostgreSQL版本差异导致连接失败。5.3 性能瓶颈定位从Patroni日志到系统指标当集群响应变慢不要盲目调Patroni参数。按顺序检查etcd健康度etcdctl endpoint status --write-outtable关注Raft applied index是否停滞Patroni循环延迟日志中Loop time exceeded出现频率超过10%说明etcd压力过大PostgreSQL WAL压力SELECT * FROM pg_stat_replication;看pg_wal_lsn_diff(pg_last_wal_receive_lsn(), received_lsn)是否持续增长系统IO等待iostat -x 1await 50ms且%util接近100%时检查磁盘是否混部。我处理过一个典型案例客户报告切换后查询变慢。查Patroni日志发现Loop time exceeded高频出现但etcd状态正常。深入查iostat发现/var/lib/etcd和/var/lib/pgsql/14/data在同一块SSD上await高达120ms。迁移etcd数据目录到另一块NVMe盘后问题消失。5.4 安全加固生产环境必须做的5件事禁用Patroni REST API公网访问restapi.listen只绑定内网IP或用Nginx加Basic Authetcd强制TLS双向认证client-cert-auth: true且Patroni客户端证书需单独签发不能复用etcd server证书PostgreSQL密码加密存储Patroni配置中password字段必须用pg_encrypted_password函数加密而非明文Patroni进程降权运行Userpatroni且patroni用户对/var/lib/pgsql/14/data仅有读取权限无写入权审计日志开启在postgresql.conf中添加log_statement ddl和log_line_prefix %t [%p]: [%l-1] user%u,db%d,app%a,client%h 日志集中收集。最后分享一个小技巧Patroni的/health端点如http://192.168.1.10:8008/health返回HTTP 200表示节点健康但不保证是Leader。Zabbix或Prometheus监控时务必用/cluster端点解析JSON提取members[].state running且members[].role Leader的节点这才是真正的主库地址。我见过太多监控只看/health结果主库挂了却没告警的事故。
Patroni+etcd实现PostgreSQL高可用集群部署实战
发布时间:2026/7/9 20:00:13
1. 为什么Patroni是PostgreSQL高可用落地最稳的选择在Linux服务器上部署一个真正能扛住生产环境考验的PostgreSQL高可用集群不是简单装几个软件、改几行配置就能搞定的事。我从2015年开始在金融和电商行业做数据库架构亲手搭过基于repmgr的老式主从切换方案也踩过PacemakerCorosync的深坑——那套东西配置复杂、脑裂处理逻辑晦涩、故障恢复时间动辄5分钟起步一次半夜主库宕机监控告警响了三轮才切过去DBA团队全员被叫醒排查最后发现是corosync心跳包被防火墙策略误杀。后来我们全面转向Patroni不是因为它名字洋气而是它用极简的设计哲学把高可用这件事拉回了工程师该有的节奏用Python写控制逻辑用标准API暴露状态把所有决策权交给外部分布式协调服务etcd/consul/zookeeper自己只做一件事——确保PostgreSQL实例的状态与协调服务里记录的一致。这背后藏着三个关键判断第一PostgreSQL原生流复制已经足够健壮问题从来不在复制本身而在“谁当主库”、“什么时候切”、“切完怎么通知应用”这些元数据管理上第二任何把“选举逻辑”硬编码进数据库代理层的方案比如早期pgpool-II都会随着业务增长变得越来越难维护第三现代云原生环境里etcd这类协调服务早已是基础设施标配与其重复造轮子不如让Patroni成为PostgreSQL与etcd之间的翻译官。所以当你看到“PostgreSQL Patroni 高可用集群 部署 Linux”这个标题时它真正的价值不在于“部署”这个动作而在于用一套可验证、可审计、可灰度的机制把数据库的生死大权从人肉运维的模糊地带移交到代码定义的确定性逻辑中。适合谁不是给刚装完PostgreSQL就想着搞高可用的新手而是那些已经经历过主库宕机导致订单丢失、报表延迟、客服电话被打爆的DBA和SRE也不是给只想跑个Demo的开发者而是需要在K8s集群里稳定运行三年以上、支撑日均千万级事务的核心系统架构师。它解决的不是“能不能用”而是“出事时敢不敢睡”。2. Patroni集群设计核心逻辑与方案选型依据2.1 为什么必须用etcd而不是ZooKeeper或ConsulPatroni支持etcd、Consul、ZooKeeper三种后端但我在所有生产环境都强制选择etcd理由非常具体etcd的lease机制与Patroni的心跳模型天然契合且其watch API的语义更清晰故障场景下行为更可预测。举个例子当Patroni节点网络抖动时etcd的lease续期失败会立即触发key过期Patroni检测到leader key消失后会在下一个循环立刻发起新选举而Consul的session机制在某些版本中存在session renewal timeout与TTL不匹配的问题曾导致我们一个集群在30秒网络中断后旧leader未及时释放锁新节点又无法获得锁整个集群卡在“无主”状态长达2分钟。ZooKeeper虽然强一致但其客户端重连逻辑复杂Patroni的zk client在连接闪断时偶发出现session expired却未正确触发reconnect需要手动kill进程重启——这种不确定性在金融核心系统里是不可接受的。提示etcd集群本身必须是奇数节点3或5且每个节点需独立部署在不同物理机或AZ内。我见过最惨的案例是把3个etcd全塞在同一台4U服务器的3个VM里结果宿主机网卡驱动崩溃etcd集群整体失联Patroni瞬间失去大脑所有PostgreSQL节点降级为只读业务直接雪崩。etcd不是配角它是整个高可用体系的基石。2.2 Patroni节点角色划分与资源隔离原则一个最小可用的Patroni集群至少需要3个PostgreSQL节点1主2从 3个etcd节点但绝不能把它们混部。我的硬性规定是PostgreSQL数据目录、WAL归档路径、Patroni配置文件、etcd数据目录四者必须分属不同磁盘分区且PostgreSQL与etcd严禁共用同一块SSD。原因很现实etcd的wal写入是随机小IOPostgreSQL的WAL刷盘是顺序大IO混在一起会导致IO调度器彻底混乱实测IOPS下降40%主从同步延迟从毫秒级飙升到秒级。更致命的是当etcd因磁盘满而崩溃时如果它和PostgreSQL共享磁盘PostgreSQL的checkpoint也会被阻塞进而引发连接池耗尽。节点角色上我坚持“一机一责”每台Linux服务器只运行一个PostgreSQL实例 一个Patroni进程 一个etcd client非server绝不允许在同一个systemd unit里启动多个服务。这样做的好处是故障隔离清晰——某台机器CPU打满你一眼就能看出是PostgreSQL查询慢还是etcd同步卡而不是在top里看到一堆python进程傻傻分不清哪个是Patroni哪个是etcd。2.3 切换策略的底层逻辑不是“谁快谁上”而是“谁合规谁上”Patroni的故障切换不是比速度而是比合规性。它的选举流程本质是所有候选节点向etcd发起CASCompare-And-Swap操作尝试将leader key的value设为自己的标识符只有CAS成功者才能成为leader失败者自动降级为replica。这个过程没有“投票”概念完全依赖etcd的线性一致性保证。因此切换时间取决于两个因素一是etcd的round-trip-timeRTT二是Patroni的loop_wait参数默认10秒。很多人调小loop_wait想加速切换这是危险操作——当网络抖动时过短的间隔会导致频繁CAS冲突etcd压力陡增反而可能引发集群假死。我的经验是在局域网环境RTT 5msloop_wait保持默认10秒配合retry_timeout30秒实际RTORecovery Time Objective稳定在12~15秒若跨机房部署必须将loop_wait提升至30秒并启用patroni的“scheduled restart”功能在检测到主库长时间无响应时主动触发计划内重启避免因网络分区导致脑裂。注意Patroni的“pause”模式不是暂停服务而是暂停所有自动切换操作。它常被误用于“升级前临时禁用切换”但实际效果是当主库宕机时集群会永久卡在“无主”状态直到你手动unpause并执行switchover。正确的做法是使用“maintenance mode”它允许Patroni继续监控状态只是不执行自动故障转移同时保留手动switchover能力。3. Linux环境下Patroni集群部署全流程详解3.1 环境准备操作系统与内核参数调优我们以CentOS 7.9内核3.10.0-1160和Ubuntu 22.04内核5.15.0为基准这两者在企业环境中占比超80%。部署前必须完成以下内核参数加固否则Patroni在高负载下会出现诡异的连接超时# 编辑 /etc/sysctl.conf # PostgreSQL相关 vm.swappiness 1 vm.overcommit_memory 2 vm.overcommit_ratio 80 # 网络相关防止TIME_WAIT堆积 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 30 net.core.somaxconn 65535 # etcd相关关键 fs.file-max 2097152 net.core.netdev_max_backlog 5000执行sysctl -p生效后还需检查/proc/sys/fs/file-nr确认文件句柄数已提升。我曾遇到一个案例某客户在阿里云ECS上部署未调整file-maxetcd启动后报too many open filesPatroni反复重启日志里全是Connection refused折腾两天才发现是系统限制。用户创建也需规范# 创建专用用户禁止shell登录 useradd -r -m -s /bin/bash postgres useradd -r -m -s /bin/false patroni # 设置密码策略生产环境必须 echo postgres:StrongPassw0rd2024! | chpasswd passwd -x 90 -n 7 -w 7 postgres # 90天过期7天内可改过期前7天提醒3.2 etcd集群部署3节点最小化高可用etcd必须独立部署这里以3节点为例IP192.168.1.10, 192.168.1.11, 192.168.1.12# 下载etcd以v3.5.10为例 wget https://github.com/etcd-io/etcd/releases/download/v3.5.10/etcd-v3.5.10-linux-amd64.tar.gz tar xzvf etcd-v3.5.10-linux-amd64.tar.gz sudo mv etcd-v3.5.10-linux-amd64/* /usr/local/bin/ # 创建数据目录 sudo mkdir -p /var/lib/etcd sudo chown -R etcd:etcd /var/lib/etcd关键配置文件/etc/etcd/etcd.conf.yml每台机器内容不同仅列出192.168.1.10的配置name: etcd-node-1>[Unit] Descriptionetcd key-value store Documentationhttps://github.com/etcd-io/etcd Afternetwork.target [Service] Typenotify Useretcd ExecStart/usr/local/bin/etcd --config-file /etc/etcd/etcd.conf.yml Restarton-failure RestartSec10 LimitNOFILE65536 [Install] WantedBymulti-user.target启动顺序先启192.168.1.10等etcdctl endpoint health返回healthy再启第二台最后第三台。切忌三台同时启动etcd集群初始化阶段对时序敏感。3.3 PostgreSQL安装与基础配置Patroni要求PostgreSQL 10我们选用14.10LTS版本# Ubuntu 22.04 echo deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main | sudo tee /etc/apt/sources.list.d/pgdg.list wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt-get update sudo apt-get install -y postgresql-14 postgresql-client-14 postgresql-contrib-14 # CentOS 7.9 yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm yum install -y postgresql14-server postgresql14-contrib /usr/pgsql-14/bin/postgresql-14-setup initdb关键配置修改/var/lib/pgsql/14/data/postgresql.conf# 必须开启 listen_addresses localhost,192.168.1.10 # 每台机器IP不同 port 5432 max_connections 500 shared_buffers 2GB # 物理内存的25% effective_cache_size 6GB work_mem 16MB maintenance_work_mem 1GB # WAL相关Patroni依赖 wal_level replica synchronous_commit on full_page_writes on wal_log_hints on max_wal_senders 10 max_replication_slots 10 hot_standby on # 归档生产必备 archive_mode on archive_command test ! -f /var/lib/pgsql/14/archive/%f cp %p /var/lib/pgsql/14/archive/%f archive_timeout 1800pg_hba.conf必须添加Patroni健康检查规则# TYPE DATABASE USER ADDRESS METHOD host replication replicator 192.168.1.0/24 scram-sha-256 host all postgres 127.0.0.1/32 scram-sha-256 host all postgres ::1/128 scram-sha-256 # Patroni健康检查关键 host all patroni 192.168.1.0/24 trust注意replicator用户需在postgres用户下创建并赋予REPLICATION权限CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD ReplicaPass2024!;3.4 Patroni安装与核心配置解析Patroni推荐用pip安装避免包管理器版本滞后# 创建虚拟环境隔离依赖 python3 -m venv /opt/patroni/env source /opt/patroni/env/bin/activate pip install --upgrade pip pip install patroni[etcd] # 显式指定etcd后端核心配置文件/etc/patroni.yml以192.168.1.10节点为例scope: postgres-cluster namespace: /service/ name: pg-node-1 restapi: listen: 192.168.1.10:8008 connect_address: 192.168.1.10:8008 etcd: hosts: [192.168.1.10:2379,192.168.1.11:2379,192.168.1.12:2379] protocol: https cert: /etc/ssl/patroni/client.pem key: /etc/ssl/patroni/client-key.pem ca: /etc/ssl/patroni/ca.pem bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 # 1MB防止从库延迟过大时切主 postgresql: use_pg_rewind: true # 自动修复分叉必须开启 use_slots: true parameters: wal_level: replica hot_standby: on initdb: - encoding: UTF8 - locale: en_US.UTF-8 ->[Unit] DescriptionPatroni high availability PostgreSQL cluster Afternetwork.target etcd.service [Service] Typesimple Userpatroni Grouppatroni EnvironmentPATH/opt/patroni/env/bin:/usr/local/bin:/usr/bin:/bin ExecStart/opt/patroni/env/bin/patroni /etc/patroni.yml Restarton-failure RestartSec5 KillModeprocess TimeoutSec30 [Install] WantedBymulti-user.target3.5 集群初始化与状态验证启动顺序至关重要先启etcd集群再启Patroni。在192.168.1.10上执行sudo systemctl daemon-reload sudo systemctl enable etcd patroni sudo systemctl start etcd # 等待etcd健康 sudo ETCDCTL_API3 /usr/local/bin/etcdctl --endpointshttps://127.0.0.1:2379 \ --cert/etc/ssl/etcd/client.pem --key/etc/ssl/etcd/client-key.pem \ --cacert/etc/ssl/etcd/ca.pem endpoint health # 返回{health:true}后启动Patroni sudo systemctl start patroni验证集群状态# 查看Patroni REST API curl -s http://192.168.1.10:8008/cluster | jq .members[] | {name, role, state, api_url} # 应看到一个role: Leader和两个role: Replica # 查看PostgreSQL内部状态 psql -U postgres -h 127.0.0.1 -c SELECT * FROM pg_stat_replication; # 检查etcd中Patroni注册的key sudo ETCDCTL_API3 /usr/local/bin/etcdctl --endpointshttps://127.0.0.1:2379 \ --cert/etc/ssl/etcd/client.pem --key/etc/ssl/etcd/client-key.pem \ --cacert/etc/ssl/etcd/ca.pem get /service/postgres-cluster/ --prefix --keys-only # 应返回类似 /service/postgres-cluster/members/pg-node-1 等key此时集群已就绪但别急着切业务——必须做故障注入测试。4. 故障模拟与切换验证从理论到实战的鸿沟4.1 主库宕机真实RTO测量与日志分析在Leader节点192.168.1.10执行暴力终止sudo systemctl stop patroni # 等待15秒观察其他节点日志 sudo tail -f /var/log/patroni/patroni.log关键日志片段2024-06-15 10:23:45,123 INFO: no action. I am (pg-node-2), a secondary, and following a leader (pg-node-1) 2024-06-15 10:23:55,123 WARNING: Loop time exceeded, 11 seconds since last successful execution 2024-06-15 10:24:05,123 INFO: Lock owner: pg-node-1; I am pg-node-2 2024-06-15 10:24:05,123 INFO: does not have lock 2024-06-15 10:24:05,123 INFO: acquiring session lock 2024-06-15 10:24:05,123 INFO: acquired session lock as a leader 2024-06-15 10:24:05,123 INFO: promoted self to leader by acquiring session lock此时访问http://192.168.1.11:8008/cluster应看到pg-node-2的role变为Leader。用psql连接新主库执行SELECT pg_is_in_recovery(); -- 应返回 false SELECT now() - pg_postmaster_start_time(); -- 启动时间应小于30秒实测心得RTO 从systemctl stop patroni到新主库pg_is_in_recovery()返回false的时间。在我的测试环境3节点千兆内网平均RTO为12.3秒最长14.8秒。超过15秒需检查etcd RTT或Patroni日志中的WARNING: Loop time exceeded频率。4.2 网络分区脑裂防护机制验证这是最危险的场景。我们模拟192.168.1.10原Leader与etcd集群网络中断但与其他PostgreSQL节点网络正常# 在192.168.1.10上执行模拟单向断网 sudo iptables -A OUTPUT -d 192.168.1.11 -j DROP sudo iptables -A OUTPUT -d 192.168.1.12 -j DROP观察192.168.1.10的Patroni日志2024-06-15 11:00:00,000 ERROR: Request to server https://192.168.1.11:2379 failed: MaxRetryError(HTTPSConnectionPool(host192.168.1.11, port2379): Max retries exceeded with url: /v3/kv/range (Caused by NewConnectionError(urllib3.connection.HTTPSConnection object at 0x7f8b1c0a1d30: Failed to establish a new connection: [Errno 113] No route to host))) 2024-06-15 11:00:00,000 WARNING: Can not get cluster from dcs 2024-06-15 11:00:00,000 INFO: demoted self because DCS is not accessible关键点来了Patroni检测到无法连接etcd后会主动执行pg_ctl promote -D /var/lib/pgsql/14/data将自己降级为只读副本这是Patroni防脑裂的核心机制——当失去与协调服务的联系宁可停止服务也不冒险当主库。此时192.168.1.10的PostgreSQL处于recovery状态pg_is_in_recovery()返回true应用连接会收到FATAL: the database system is starting up错误而非脏读。注意这个机制依赖postgresql.pg_rewind配置。如果未开启Patroni在失联时会拒绝降级导致节点僵死。务必在bootstrap.postgresql中显式设置use_pg_rewind: true。4.3 从库延迟过大自动保护性切换故意让一个从库192.168.1.12延迟# 在192.168.1.12上暂停WAL接收 psql -U postgres -c SELECT pg_wal_receivings_flush_lsn(); # 手动删除归档WAL文件或暂停归档进程当延迟超过maximum_lag_on_failover1MB时Patroni会拒绝将其选为新主。查看日志2024-06-15 11:30:00,000 WARNING: Member pg-node-3 is lagging behind the leader by more than 1048576 bytes 2024-06-15 11:30:00,000 INFO: skipping pg-node-3 for leader race: too far behind此时即使主库宕机Patroni只会从pg-node-2和pg-node-1如果存活中选举确保数据一致性。这个阈值必须根据业务容忍度调整金融交易系统建议设为100KB报表系统可放宽到10MB。5. 常见问题与排查技巧实录5.1 Patroni启动失败典型错误码速查表错误现象根本原因排查命令解决方案ERROR: Request to server https://... failed: MaxRetryErroretcd证书不匹配或网络不通curl -k https://192.168.1.10:2379/version检查etcd证书CN、防火墙、SELinux布尔值setsebool -P httpd_can_network_connect 1FATAL: role patroni does not existpg_hba.conf未添加patroni用户信任规则psql -U postgres -c SELECT usename FROM pg_user;在pg_hba.conf添加host all patroni 192.168.1.0/24 trust并pg_ctl reloadWARNING: Postgresql is not runningPostgreSQL数据目录权限错误ls -ld /var/lib/pgsql/14/datasudo chown -R postgres:postgres /var/lib/pgsql/14/dataINFO: no action. I am (pg-node-1), a secondary, and following a leader (pg-node-1)节点名重复etcd中残留旧keyetcdctl get /service/postgres-cluster/ --prefix --keys-onlyetcdctl del /service/postgres-cluster/ --prefix清空后重启Patroni实操心得Patroni日志级别默认为INFO遇到问题先调成DEBUG在/etc/patroni.yml中添加log_level: DEBUG重启后日志会暴露出底层HTTP请求细节比如Sending request GET https://192.168.1.10:2379/v3/kv/range能精准定位是证书、DNS还是网络问题。5.2 切换后应用连接失败连接池与DNS缓存陷阱很多团队反馈“切换成功了但应用连不上新主库”。这不是Patroni问题而是应用层陷阱。典型原因有两个第一连接池未刷新DNS。应用配置了jdbc:postgresql://pg-cluster-vip:5432/mydb但VIP虚拟IP未配置实际连的是pg-node-1的IP。切换后应用仍往旧IP发请求。解决方案必须用Patroni REST API动态获取当前Leader。Java应用示例// 定期调用 http://192.168.1.10:8008/cluster 获取leader节点 String leaderUrl getPatroniLeader() :5432; HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:postgresql:// leaderUrl /mydb);第二PgBouncer连接池未重置。PgBouncer的server_reset_query默认是DISCARD ALL但Patroni切换后旧连接池里的连接指向已失效的主库。必须在pgbouncer.ini中设置server_reset_query SELECT 1并启用ignore_startup_parameters extra_float_digits避免因PostgreSQL版本差异导致连接失败。5.3 性能瓶颈定位从Patroni日志到系统指标当集群响应变慢不要盲目调Patroni参数。按顺序检查etcd健康度etcdctl endpoint status --write-outtable关注Raft applied index是否停滞Patroni循环延迟日志中Loop time exceeded出现频率超过10%说明etcd压力过大PostgreSQL WAL压力SELECT * FROM pg_stat_replication;看pg_wal_lsn_diff(pg_last_wal_receive_lsn(), received_lsn)是否持续增长系统IO等待iostat -x 1await 50ms且%util接近100%时检查磁盘是否混部。我处理过一个典型案例客户报告切换后查询变慢。查Patroni日志发现Loop time exceeded高频出现但etcd状态正常。深入查iostat发现/var/lib/etcd和/var/lib/pgsql/14/data在同一块SSD上await高达120ms。迁移etcd数据目录到另一块NVMe盘后问题消失。5.4 安全加固生产环境必须做的5件事禁用Patroni REST API公网访问restapi.listen只绑定内网IP或用Nginx加Basic Authetcd强制TLS双向认证client-cert-auth: true且Patroni客户端证书需单独签发不能复用etcd server证书PostgreSQL密码加密存储Patroni配置中password字段必须用pg_encrypted_password函数加密而非明文Patroni进程降权运行Userpatroni且patroni用户对/var/lib/pgsql/14/data仅有读取权限无写入权审计日志开启在postgresql.conf中添加log_statement ddl和log_line_prefix %t [%p]: [%l-1] user%u,db%d,app%a,client%h 日志集中收集。最后分享一个小技巧Patroni的/health端点如http://192.168.1.10:8008/health返回HTTP 200表示节点健康但不保证是Leader。Zabbix或Prometheus监控时务必用/cluster端点解析JSON提取members[].state running且members[].role Leader的节点这才是真正的主库地址。我见过太多监控只看/health结果主库挂了却没告警的事故。