资讯动态

PostgreSQL主从复制、自动切换与负载均衡实战指南

发布时间:2026/10/17 12:49:19 来源:尧图企业网站定制
这几年做数据库运维大部分时间其实都花在“怎么让数据库不宕机”和“宕机了怎么少挨骂”这两件事上。PostgreSQL的主从复制切换和负载均衡看着是两个独立话题但真到了生产环境它们是一体的——复制是手段切换是兜底负载均衡才是那个让你白天能睡个安稳觉的日常保障。这篇文章不聊PPT上的架构图只讲我实际把一套PostgreSQL集群从单机改造为主从复制、再逐步接入切换和负载均衡的全过程。包括核心参数怎么配、切换为什么不能用“手动敲命令”糊弄、负载均衡到底该放在哪一层以及我在故障演练里踩过的三个坑。1. 为什么我最后选了流复制加自动切换这一套组合先说结论现在主流的PostgreSQL高可用方案基本都围绕原生流复制构建配合一个负责监控和仲裁的编排层。我这边最终落地的是三节点模式一个主库、一个同步备库、一个异步备库外加一套基于共识机制选主的自动化组件。1.1 复制方案横向对比别一上来就是那句“分布式”很多朋友一聊到主从复制第一反应是“用现有集群组件自己写同步逻辑”。但我把几种常见方案摆到一起做过对比实际差距比想象中明显。第一类叫文件级复制也就是把主库的数据文件定期拷贝到备库。这种方式实现简单但丢数据窗口太大而且文件拷贝期间主库继续写库两边文件根本无法保证一致。现在几乎没人在生产里用它做主从只适合做冷备。第二类叫逻辑复制。它能精确到表甚至行可以跨大版本同步数据还能做转换。但它的原理是解析WAL里记录的变更再重放DDL处理能力偏弱主备切换后序列、表结构这类对象容易出状态不一致的问题。如果只是做主从高可用逻辑复制带来的复杂度会超过收益。第三类是物理流复制也就是我们说的原生流复制。备库直接接收主库的WAL日志并重放整库保持一致跨大版本也没问题延迟通常只在亚秒级到秒级之间。主备切换时只需要把备库提升为新的主库老主库降级后重新接入这套模型就是为高可用设计的。我的选型逻辑很简单主从复制的首要目标是数据不丢、切换不慌那就选和数据库内核同源的方式不要自己造同步协议。所以物理流复制是基底主备切换交给自动化编排负载均衡在连接层和中间件层各放一道这是我最后的架构形态。1.2 数据一致性的底线同步复制和异步复制怎么搭PostgreSQL流复制支持两种模式。异步模式下主库提交事务不需要等备库确认性能好但主库突然断电时备库会缺一小段WAL这部分数据就丢了。同步模式下主库会等备库把WAL刷到磁盘并确认后才提交事务数据不丢但是主库的性能会被备库拖累。生产环境我采用混合策略一个同步备库担“数据不丢”的底线一个异步备库担“快速接管”的弹性。多一个异步备库还能在同步备库出问题时临时顶替不至于降低集群安全性。这里有个关键参数容易被人忽略synchronous_commit。默认值是on意思是主库等备库确认WAL落盘就返回提交成功。但我实测中发现如果业务对写延迟敏感可以设置成remote_apply这会让主库等备库真正重放完WAL再确认提交读流量切到备库后立即能看到数据只是延迟会进一步上升。到底选哪个取决于业务到底能不能接受“主库提交成功但备库查询还是旧值”的窗口。注意如果业务对“写后读一致”要求很高比如发消息后立即能查到建议用remote_apply。如果牺牲几毫秒无所谓就用默认的on。2. 主从复制配置落地参数、初始化、验证一条线配置流复制并不复杂但每一步都有隐藏条件。我按主库、备库、验证三块逐步拆顺便说一下哪些参数是我后来才知道必须改的。2.1 主库参数wal_level和复制槽是两件套主库首先要打开wal_level replica9.6及以上版本这是流复制的基本前提。很多文档会建议直接用logical因为逻辑复制也需要WAL输出。我实际用下来如果暂时没有逻辑复制的需求就用replica因为日志量更小不必要的日志输出会影响刷盘压力。主库还需要配置至少一个复制连接。这一步最容易被忽略的是复制槽。备库通过流复制协议接收WAL如果备库延迟很久而主库的WAL文件因为归档或回收被清理备库就断档了只能重建。复制槽的作用是告诉主库这些WAL我的备库还没收完你先留着别删。我从某个凌晨的故障里学到没有复制槽时主库的pg_wal目录增长基本可控一旦备库挂了几个小时后重新上线只要有复制槽在pg_wal就可能暴涨到几十GB甚至占满磁盘。所以我的标准答案是每个流复制备库都建一个持久复制槽再配合一个定期检查复制槽活跃度的监控脚本。2.2 备库初始化基础备份这一步怎么选备库有两种初始化方式一种是全量备份后恢复一种是基于pg_basebackup制作基础备份。生产中我基本只推荐pg_basebackup因为它能自动生成主库的配置参数还能保证数据文件的一致性。典型命令大致是这样pg_basebackup -h 主库IP -p 5432 -U repl -D /var/lib/postgresql/data \ -X stream -P -R -C -S standby_slot_01这里几个参数得单独解释一下。-X stream表示在备份过程中需要的WAL通过流复制实时拉取而不是等备份结束再用归档恢复这在备份期间有大事务时特别关键。-R会自动生成standby.signal文件和primary_conninfo配置少手工改一处配置就少一次出错机会。-C -S则是自动创建复制槽名的槽就叫standby_slot_01方便和备库一一对应。用pg_basebackup做完基础备份后备库目录里的postgresql.conf已经有primary_conninfo了但还需要确认几个点hot_standby on默认开启但如果之前做过其他配置覆盖要确认没被关掉。主库和备库的max_connections最好一致否则切换后容易出现连接数不足导致的服务抖动。max_standby_streaming_delay的默认值是30秒如果备库上有长时间运行的查询它可能等待WAL重放这个值在查询和同步要求之间需要权衡。2.3 验证复制真的“活”了而不是“看起来活着”我见过太多人配完主从后只执行一句SELECT pg_is_in_recovery();看到返回t就以为大功告成。实际上这个函数只能说明当前数据库处于恢复模式完全看不出复制状态是否健康。我验证复制会依次跑这几项SELECT client_addr, state, sync_state, sent_lsn, replay_lsn FROM pg_stat_replication;这个视图的关键是看sent_lsn和replay_lsn是否在持续增长。两个值长时间不动说明备库可能已经卡住了。sync_state如果是sync说明走的是同步复制链路如果是async就是异步链路多一个备库就多一条记录。再看备库视角的延迟SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;这个差值就是备库相对主库的重放延迟。持续超过1秒需要警惕如果是大事务导致的延迟几分钟都回正常都有可能。有些团队还会用pg_stat_wal_receiver看备库接收WAL的状态但日常巡检里pg_stat_replication配合重放时间戳查询基本够用了。真正验证切换能力仍然要靠定期演练这个后面专门说。3. 切换这件事手动和自动各有什么坑很多同学觉得“切换嘛把一个备库提升为主库就行”pg_ctl promote一下完事。如果业务可以接受停机这确实够用。但真正生产环境切换的核心不是“能不能升”而是“老主库怎么办、脑裂怎么防、数据怎么对齐”。3.1 手动切换的正确时序不是一行promote那么简单手动切换的正确顺序是这样的把主库的写入流量摘干净最好用防火墙或负载均衡把应用连接全部断掉确认没有新的写入。在主库上执行SELECT pg_switch_wal();把当前WAL切换并继续推送到备库。检查备库的replay_lsn是否已经追平主库的最新LSN。如果差太多强行切换会有数据丢失风险。对目标备库执行pg_ctl promote或在SQL里调用SELECT pg_promote();。把业务连接切到新主库。把老主库重新配置成新主库的备库用pg_basebackup重新拉一份基础备份。注意第六步不是“直接改primary_conninfo然后起来就行”。老主库在旧主库身份期间可能已经产生新的WAL这些WAL和新的主库是冲突的。安全做法永远是用pg_basebackup重新初始化老主库的数据目录别心疼那点重建时间数据一致性比什么都重要。有个细节很多人会忽略切换后旧主库上如果有archive_command指向同一套归档目录新旧主库可能会同时写归档导致归档目录出现冲突。切换前最好把归档命令改成临时目录切换后统一整理。3.2 自动切换不能只顾“提升备库”更要防“双主脑裂”我最早做自动切换时只写了一个脚本检测主库心跳失败就执行promote。后来在一次网络分区演练中主库和备库之间的网络断开了但主库其实还活着业务也还在写。这时候备库被脚本提升成了新主库两个库都在接受写入等网络恢复两边数据已经分叉了。这就是典型的脑裂。要防脑裂本质上是需要仲裁。PostgreSQL本身不会告诉你“你到底该听谁的”需要借助外部组件来做决定。我用的方案是当主库连接丢失时先尝试通过一个仲裁节点判断集群主节点的状态只有确认主库确实失联了才允许切换。如果仲裁也联系不上宁可保持只读状态等人工介入也不自动提升。你也见过“用SSH探测主库端口存活”的方案吧说实话这种方案在交换机故障、高负载假死、网络分区时都不太可靠。我踩过的坑是在高IO负载下SSH能连上但PostgreSQL已经长时间无响应这种场景特别迷惑人。所以判断要同时看“TCP端口是否可达”和“数据库能否响应简单查询”两层都挂了才认定主库失效这个机制我后来一直保留着。3.3 老主库回归不重新初始化就等于埋雷老主库恢复后如果直接启动它可能带着旧身份的数据又尝试和新的主库建立复制关系轻则复制不了重则把新主库的WAL流搞乱。很多人手忙脚乱时做的第一件事是改primary_conninfo这是大忌。正确姿势还是那个词重新初始化。无论多舍不得老主库上的本地数据只要它在宕机期间有过独立写入那些数据就是垃圾数据不能并入集群。重新pg_basebackup前要把旧的数据目录备份或清理干净避免残留配置文件干扰。我在实际操作中遇到过老主库回归后replay_lsn始终追不上的情况排查到最后发现是老主库曾经的archive_command配置不一致归档链路间接影响了流复制。所以回归前配置审计也是必做项不能只顾着刷数据目录。4. 负载均衡该放在哪一层每一层都解决什么问题主从复制搭好以后最自然的诉求就是“读流量分摊到备库上”但把所有流量都丢给数据库连接器去自动选择也不是最优解。我最后把负载均衡拆到了三层驱动层、连接池层、专用中间件层。每一层的职责边界非常清晰说清楚之后你就能根据自己业务做取舍。4.1 驱动层JDBC的多主机配置到底能不能用如果你全是Java应用PostgreSQL JDBC驱动本身就支持多主机连接通过host1,host2这样的格式指定主备节点配合targetServerType参数驱动的逻辑是targetServerTypeprimary只连主库适合写操作。targetServerTypepreferSecondary优先备库备库不可用时才连主库。targetServerTypeany任意节点但这种模式不能保证你连的一定是主库。例如jdbc:postgresql://192.168.1.10:5432,192.168.1.11:5432/mydb?targetServerTypeprimary这个方案的好处是不用额外部署中间件坏处是第一它只能做“选主库或备库”这种粗粒度决策做不了真正的按连接分配第二如果主库发生切换应用里的连接池并不知道新主库是谁还是按旧列表去连会造成临时性连接失败。所以我的结论是驱动层适合做主备连接的兜底但不适合作为唯一的负载均衡手段。尤其是应用数量多时配置散落在各个服务里一旦主库地址变化维护成本会直线上升。4.2 连接池层PgBouncer不是负载均衡器别指望它PostgreSQL社区里最常见的连接池是PgBouncer它的定位很清楚代理应用连接降低数据库连接开销。它本身基本不做读写分离也不负责把读请求路由到备库。但我在实际架构里仍然会用PgBouncer原因和“均衡”无关PostgreSQL的max_connections是有上限的应用如果每条连接都直连数据库很快连接数就会被打满。有了PgBouncer后应用连接都被复用数据库连接数可以做到远低于应用连接数。需要特别提醒的是如果用了PgBouncer你需要注意事务模式和语句模式的差异。语句模式下不同语句可能被分发到不同数据库连接上事务内的中间状态会丢失。我建议对生产环境统一用事务模式至少能保住事务边界。4.3 专用中间件层Pgpool-II和HAProxy的实战分工如果业务读多写少想真正实现读写分离我一般会在应用和数据库之间放一层中间件。两个常见的选型是HAProxy和Pgpool-II。HAProxy本质是四层TCP负载均衡器它可以把连接列表中的多个备库节点做加权轮询还能做健康检查。但HAProxy的缺陷很明显它不解析SQL所以完全不知道一个连接是来读还是来写你能做的只是让应用把写流量连接到一个端口读流量连接到另一个端口。Pgpool-II则会解析SQL可以自动识别读写语句把写SQL发给主库读SQL按权重分发给备库。它还支持在线扩展节点、缓存查询结果。副作用是引入了一层额外复杂度和性能损耗而且配置项非常多调优需要一点耐心。我最后的生产方案其实是三层叠加应用先连HAProxy虚拟IPHAProxy把流量按来源IP和端口分给Pgpool-II集群Pgpool-II再做SQL级读写分离。听着复杂但每一层都有明确边界后期加节点、切主备时不用改应用。4.4 读写分离后先要回答的“一致性问题”负载均衡最容易被忽略的是读请求被分到备库后如果备库有延迟用户可能会读到“旧数据”。这个问题没有统一解只能分级处理。我的做法是绝大多数读请求接受几十毫秒内的一致对于要求强一致的关键操作比如支付结果、订单状态这类查询强制走主库路由。在Pgpool-II里可以给指定表或函数设查询规则也可以让应用带上注释路由标记中间件解析注释后决定是否分发到备库。如果你不想引入中间件也可以直接在SQL层面处理在驱动配置里把“强一致读”的连接指向主库把“允许延迟读”的连接指向备库列表。两层方式并不矛盾选哪个取决于你的业务复杂度。5. 真实故障演练中的三个意外配置和原理讲再多都没法替代一次真实的拔网线演练。我在把这套集群推向生产前专门做了三轮故障演练结果每一轮都炸出了意外。这三个问题比任何文档都值得记下来。5.1 备库延迟飙升但不是因为负载第一次演练我模拟备库执行一个大查询同时主库保持高写入量。结果备库的重放延迟从几十毫秒一路飙到十几秒而备库的CPU使用率并不高。我一度以为系统卡死了后来查pg_stat_activity才发现是备库上有一个长时间运行的读事务阻塞了WAL重放进程对某些页面的应用。流复制备库虽然允许只读查询但如果备库上有长查询和WAL重放之间存在锁冲突。max_standby_streaming_delay默认30秒意思是重放最多等30秒超过后查询会被强制取消。这个机制不是给备库做大查询用的它本来就是为短查询设计的。那次之后我的处理策略改了备库上禁止跑报表型全表扫描这类任务要么规划到专用分析备库要么在凌晨低峰执行。如果实在无法避免就把max_standby_streaming_delay调大但延迟会继续累计监控会响两害相权你要想清楚。5.2 复制槽引发的磁盘警报第二次演练我故意让备库停机两个小时。按我的预期备库停机会导致WAL继续堆积但复制槽应该能保证不丢失。结果备库恢复后pg_wal目录已经涨到占满数据盘主库的WAL无法归档归档进程直接报错。问题不是复制槽本身而是我用了持久复制槽但没设置max_slot_wal_keep_size的合理上限。旧版本的PostgreSQL这个参数默认值很大多个复制槽叠加主库迟迟不清理WAL磁盘就爆了。现在我的做法是给每个持久复制槽单独建监控计算pg_current_wal_lsn()和pg_replication_slots里restart_lsn的差值当差值超过阈值时告警。同时每个数据盘要留出至少20%的余量这是给复制恢复用的缓冲。5.3 老主库带着“旧数据”回归的混乱第三次演练我在手动切换后故意不重建老主库直接启动旧节点尝试作为备库接入。结果不一会儿监控就显示复制中断应用侧开始出现某些行被旧值覆盖的情况。原因和前面说的机制一致老主库在失联期间可能已经生成了本地WAL这些WAL并不在新主库的复制流里。当它作为备库接入时postgresql.conf里配置的连接指向新主库但本地已存在的WAL会让节点认为它有更“新”的数据于是拒绝继续拉取形成循环僵持。这一次我没有删除数据目录而是把老主库的archive_status目录清空用pg_basebackup重新初始化存储目录才真正恢复到健康状态。事后我把“老主库回归必须先重建”写进了故障手册这应该成为所有主从复制操作的默认前提。6. 日常巡检和监控真正有用的几个数和我的检查脚本集群稳定运行后日常运维就是按部就班地看几个关键指标。PostgreSQL的监控视图和函数很丰富但真正每天都要看的其实不超过五个。6.1 看备库是不是“活着且在追”每天我到工位的第一件事不是看监控大屏而是跑这段检查SQLSELECT client_addr, state, sync_state, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytes, now() - pg_last_xact_replay_timestamp() AS replay_delay FROM pg_stat_replication;replay_lag_bytes大于0说明备库还没追平主库。真正需要关注的是它是不是“一直大于0”如果是说明备库的重放速度跟不上主库的写入速度这往往是备库上长查询或者IO能力不足导致的。如果replay_delay在几秒左右波动一般不用管如果稳定增长就要介入看备库负载。还有一种常见陷阱备库长时间处于catchup状态这多半是备库刚从停顿状态恢复正在追赶延迟。这时要观察延迟差值是否在下降如果持续不变就要考虑是复制槽或者网络问题。6.2 复制槽和WAL容量的健康检查复制槽的检查不能等到磁盘报警。我写了一个小脚本每小时跑一次SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes FROM pg_replication_slots;当lag_bytes超过比如10GB时就说明备库可能已经离线很久了需要人工介入判断是继续等备库恢复还是直接重建备库节点。这个阈值要根据你的业务写入量和磁盘容量动态调整没有一个通用数字。还有一个隐蔽风险如果某个逻辑复制槽长期没人消费WAL累积速度会非常快。这个检查逻辑同样适用只是消费方变成逻辑解码插件监控频率要更高一些。6.3 一键检查脚本日常巡检五分钟搞定如果不想人工盯着几个SQL可以做成一个简单的bash脚本把上面几个查询汇总输出#!/bin/bash psql -h 主库IP -U postgres -d postgres -c \ SELECT client_addr, state, sync_state, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes FROM pg_stat_replication; psql -h 主库IP -U postgres -d postgres -c \ SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS slot_lag FROM pg_replication_slots; psql -h 备库IP -U postgres -d postgres -c \ SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;这套脚本我每天早上和下午各跑一次同时接入告警系统。三行SQL覆盖了复制状态、复制槽健康、备库重放延迟三个核心维度比任何监控大屏都直观。6.4 监控之外每季度一次“真实”切换演练监控只能发现问题切换演练才是验证备库真正可用的手段。我坚持每季度至少做一次非业务的切换演练流程是暂停应用写入手动切换启动应用观察是否正常。重点不是走流程而是记录三个数字切换耗时、数据丢失量到底有没有、应用重连成功比例。演练中常见的问题应用配置的主库地址写死了切换后应用根本不认识新主库或者连接池还缓存着旧主库的连接迟迟不释放。这些在文档里写“支持高可用”都没用只有真实切一遍才能暴露。我个人的体会是演练后最容易修复的是配置最难修复的是“应用侧连接池对主备变化的感知迟钝”这个要提前在驱动配置里设好。另外建议演练选在业务低峰期最好还能模拟“只断主库连接但业务流量仍在”的场景。这种演练虽然压力大但能逼出很多真实问题比如前面说的脑裂和连接池重连问题都是在这种严苛条件下才浮出水面的。事后复盘这套方案现在稳不稳这套主从复制加负载均衡的架构跑到现在稳定性比我预期的要好。最直接的收益是某个业务库因为硬件维护导致的主库不可用自动切换用了不到20秒业务侧只是感知到一次连接抖动没有出现长时间不可用。复盘下来我感觉整个方案里性价比最高的决定其实是“别自己写切换脚本把仲裁交出去”。另一个高性价比决定是“负载均衡提前分层”而不是把所有流量一股脑扔给某一个组件。很多问题如果在架构设计阶段就做了职责切分后期维护成本会低很多。如果让我再从头做一次我会把更多精力放在监控和演练上因为配置一次成型后很少会坏真正让系统变脆弱的是备库悄悄延迟、复制槽悄悄堆积、老主库偷偷带数据回归这类日常小概率事件。最后一个小建议别等到集群已经上了生产再去做切换演练先在测试环境按这套流程完整切三轮以上。每轮换不同的故障点比如网络分区、磁盘慢IO、主库假死等你在测试环境解决过所有突发状况生产环境的切换就只是走流程而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑