今年年初我们数据库团队接了一个硬任务:把跑在单机房的磐维数据库,改成一套双中心容灾的流复制集群。当时方案选型、参数调优、切换演练加在一起差不多干了一个月,中间踩了不少坑。这篇文章我把整套搭建过程从头到尾理一遍——为什么选流复制、节点怎么规划、参数怎么配、切换怎么练,适合正在做国产数据库容灾改造的DBA和运维同学参考。如果你手里也是类似架构,主库在A机房、备库在B机房,想做到机房断电不丢数据、业务分钟级恢复,那这篇正好能用上。
1. 为什么双中心容灾更适合选流复制
1.1 先想清楚容灾目标再谈技术选型
做容灾之前,第一件事不是找工具,而是把目标量化。数据库容灾领域两个最基础指标:RTO和RPO。RTO是故障发生后业务恢复允许的时间,RPO是故障时可容忍丢失的数据量。单机房时代,我们靠备份恢复,RPO可能是上次备份到故障点之间所有数据,小时级,RTO取决于备份集大小和恢复人的手速,通常半小时往上。
双中心容灾要解决的正是这个痛点:生产中心彻底挂掉时,灾备中心能接住核心交易。目标定多少直接决定架构复杂度。我们的业务对账粒度比较细,领导给的预期是RPO接近0,也就是主库提交的事务至少不能丢;RTO控制在分钟级。明确这个预期之后,方案选型就顺理成章了。
这里有个常见误区:有人觉得只要每晚把数据文件复制到灾备机房也算容灾,这其实是备份,不是容灾。真正的容灾要求灾备端的数据实时或准实时追平主库,并且具备快速接管业务的能力。所以我们在需求评审阶段就明确,落地方案必须有持续的数据同步通道,而不是定时全量拷贝。
1.2 在流复制、逻辑复制、存储复制之间做取舍
当时我们重点比对了三条路线,各有适应场景。
- 流复制:主库把WAL日志通过replication协议实时发给备库,备库拿到WAL后重新应用。这是磐维数据库和PostgreSQL生态原生支持的能力,数据一致性最接近物理级,切换逻辑简单,不额外依赖硬件和中间件。
- 逻辑复制:把数据变更解析成逻辑记录再传递,可以异构、可以只同步部分表,但架构复杂,DDL处理有诸多限制,全库级容灾并不是它的强项。
- 存储复制:靠存储阵列在块设备层做镜像,数据库无感,RPO可以做到极低,但成本高出一大截,而且切换后还需要数据库一致性处理,落地周期长。
| 方案对比 | 数据一致性 | 架构复杂度 | 成本 | 适用场景 |
|---|---|---|---|---|
| 物理流复制 | 高,按WAL顺序回放 | 低,数据库原生 | 低 | 同构数据库全库容灾 |
| 逻辑复制 | 中,按SQL级逻辑 | 高,需小心DDL | 中 | 异构同步、部分表同步 |
| 存储复制 | 高,块级一致 | 高,依赖存储设备 | 高 | 核心库高级别容灾 |
结论很清楚:我们场景就是磐维数据库到磐维数据库,没有异构需求,没有跨库同步需求,流复制是最合适的。它不需要昂贵的存储阵列,不需要额外同步中间件,而且与备份、归档、只读查询天然配合良好,后续运维成本也可控。选型会开了一下午,最终落在“主备物理流复制+双中心”这个组合上。
2. 双中心容灾集群的节点规划与同步策略
2.1 中心A、中心B的角色划分与网络规划
双中心容灾,不是随便找两台服务器把复制配起来就行,节点角色和网络链路必须提前设计。我们用的是双机房对称布局:中心A是生产中心,部署主库节点;中心B是灾备中心,部署备库节点。两个机房之间用专线互通,业务流量和数据库复制流量都走这条链路。
网络设计上,我给DBA同学一个建议:复制流量最好能跟业务流量在逻辑上隔离,最理想是数据库节点单独划一个VLAN或走独立路由。原因很直接,业务高峰时流量抖动会影响WAL传输,备库延迟一旦拉大,容灾能力就打了折扣。我们当时给主备之间预留了千兆专线,按业务峰值WAL产生速率估算,余量在5倍以上,这后面会细说。
节点规划还有一层容易被忽视:仲裁节点。两节点的流复制集群做手动切换没问题,但想实现自动故障切换,必须有一个独立于两个中心之外的第三方仲裁,否则网络分区时就分不清该让谁接管。仲裁节点可以是一台小规格的云主机,也可以是一套分布式一致性服务,具体方案第5章展开讲。
2.2 同步流复制与异步流复制:RPO和可用性的权衡
流复制本身有两种工作模式:同步和异步。这对双中心容灾来说是最关键的一个分岔口。
异步模式下,主库提交事务不需要等备库确认,备库只是尽量追。好处是主库性能完全不受跨机房链路影响,坏处是主库宕机瞬间,已经提交但还没传到备库的事务会丢。RPO无法归零。如果你的业务能接受几秒到几十秒的数据丢失,异步模式最省心。
同步模式下,主库提交事务要等备库返回确认,确认粒度还可以分几档:备库收到WAL但没落盘、落盘但没回放、已经回放完成。回放完成的remote_apply一致性最强,RPO基本归零,但主库每一次写提交都要跟备库做一次“握手”,跨机房网络延迟越大,主库写入延迟越高。
我们最终选了同步模式,synchronous_commit设成remote_apply,结合同步备库列表限定等待对象。当时部门有同事质疑这会让写入变慢,实践证明在专线RTT稳定在5ms的情况下,性能损耗在可接受范围。真正危险的不是变慢,而是网络抖动时同步备迟迟不确认,主库写事务被阻塞——这个坑我后面单开一节讲怎么防御。
2.3 提前设计IP、目录和用户,避免搭建时手忙脚乱
搭建前把这些固定下来,后面命令才不会写乱:
| 项目 | 中心A(生产) | 中心B(灾备) |
|---|---|---|
| 数据库角色 | 主库 | 备库 |
| 服务器IP | 192.168.10.11 | 192.168.20.12 |
| 数据目录 | /data/pandb | /data/pandb |
| 归档目录 | /archives/pandb | /archives/pandb |
| 监听端口 | 5432 | 5432 |
| 心跳/仲裁 | 连接192.168.30.13 | 连接192.168.30.13 |
这里我特别强调目录和用户的一致性。磐维数据库安装包一般会自带初始化脚本,但核心要养成规范:数据目录单独挂盘、归档目录和数据目录分离、数据库运行用户独立。我们统一用pandb用户运行数据库,避免之后权限问题排查半天。
3. 搭建前的系统层准备与磐维数据库参数初始化
3.1 操作系统、时钟同步与文件系统注意事项
很多流复制问题不是数据库本身出的,而是系统层没准备好。我踩过最典型的坑是服务器时钟漂移:备库时间比主库慢了几分钟,日志里时间戳对不上,排查问题的时候一度以为是主备断连。双中心间直接用NTP或chrony统一时钟源,这个动作要加进搭建清单,别嫌基础。
文件系统层面,数据库数据目录所在的磁盘,建议用XFS或ext4,挂在独立逻辑卷上。为什么不建议根目录直接放数据?因为根目录一旦被日志、归档这些内容撑满,整个数据库实例都会被拖垮,太被动了。我们给数据目录和归档目录各划了独立卷,其中归档盘按WAL增长速度预留了双周冗余,后面备份脚本会定期清理。
操作系统还有几个老生常谈但必须做的:关闭透明大页、关闭NUMA潜在干扰、调整vm.swappiness。这些在安装文档里都有,但现场经常有人漏。漏一个通常看起来没事,但高并发下偶发性能抖动会非常难定位。所有系统调优做完,记得用os reboot验证自动拉起,确认服务正常再进下一步。
3.2 数据库层核心参数清单
磐维数据库参数体系与PostgreSQL生态一脉相承,我这里给一份我在生产环境实测过的核心参数组合,大家执行前先用show命令确认一下版本差异。
主库postgresql.conf关键配置:
wal_level = replica max_wal_senders = 10 max_replication_slots = 10 wal_keep_size = 4GB synchronous_commit = remote_apply synchronous_standby_names = 'FIRST 1 (standby_b)' hot_standby = on max_standby_streaming_delay = 30s archive_mode = on archive_command = 'test ! -f /archives/pandb/%f && cp %p /archives/pandb/%f'逐个说下为什么这样设:
- wal_level设为replica,备库才能拿到完整WAL做物理回放;如果要跑逻辑复制得设logical,但纯容灾场景replica足够。
- max_wal_senders决定了主库最多能有多少个walsender进程向外发WAL。直接数一数未来要连主库的备库数量,再留出归档、监控连接余量。我们两个中心各一台备库,另有可能临时拉一个全量,所以给了10。
- max_replication_slots对应复制槽上限。复制槽的作用后面专门讲,先记住它和max_wal_senders最好保持同一数量级。
- synchronous_commit用remote_apply是相对激进的选择,要求备库把WAL回放完成才返回。如果你对性能更敏感,可以先用remote_write,代价是备库OS崩溃时可能丢已写但未刷盘的数据。
- synchronous_standby_names用FIRST 1指定同步备库列表里最靠前的一个参与同步确认。我们只有一个备库,所以实际就是限定主库只等待standby_b确认。
备库这边参数相对简单,重点是hot_standby要打开,这样灾备中心平时可以挂一些只读查询,同时max_standby_streaming_delay别设太短,否则备库上有长查询时会不停中断回放。
3.3 归档日志:备库之外的第二道保险
有人会觉得有流复制了,归档日志就是多余的。我不这么看。流复制覆盖不了两个场景:第一,备库因为机房级故障或者误操作废掉时,需要全量备份加归档把数据追到一个历史时间点;第二,生产事故误删数据时,备份加归档是唯一能精确恢复到误删前一刻的手段。
archive_command里我用的是cp往本地归档目录拷贝,注意一点:归档目录千万别和数据目录放在同一块盘。不然主库数据盘被写满时,归档也同时In trouble,恢复手段全废。我们另外在灾备机房还有一份远程归档拷贝,代价是加大归档目录空间,换来的安全感非常值。
归档测试一定要做真实演练,光看archive_command返回成功不算完。我见过某套环境归档命令写错了文件路径,日志一直报错,但数据库服务正常,没人注意到,直到要做PITR才发现归档没生效。这个教训后面展开说。
4. 流复制集群搭建实录:主库配置与备库拉起
4.1 主库初始化与复制授权
主库配置分三步:初始化实例、改参数、配访问控制。
实例初始化我用的是磐维安装包自带的初始化流程,核心和传统PostgreSQL的initdb一致,指定数据目录和字符集。初始完成后,按第3章的参数清单改postgresql.conf,然后编辑pg_hba.conf,增加复制用户的访问规则:
host all pandb_admin 192.168.0.0/16 scram-sha-256 host replication replica_user 192.168.0.0/16 scram-sha-256第一行是日常管理账号访问库,第二行是replication专用账号,权限只给复制用。复制账号没必要开超级用户,最小权限原则:
CREATE ROLE replica_user LOGIN REPLICATION PASSWORD 'R3pl_Passwd_2024';REPLICATION权限在PostgreSQL生态里是独立权限,不是普通登录权限能替代的。配完后重启主库实例,pg_hba.conf变更不像参数一样可以reload一部分,稳妥起见直接重启。
4.2 用pg_basebackup一键拉起备库
备库初始数据不推荐手动去拷贝主库文件,首选pg_basebackup,它是官方推荐的物理备份工具,拉取过程中还会自动建立与主库的流复制连接关系。
备库服务器装好磐维数据库软件后,执行:
mkdir -p /data && chown pandb:pandb /data pg_basebackup -h 192.168.10.11 -p 5432 -U replica_user \ -D /data/pandb -X stream -P -R -C -S slot_a_to_b参数逐一说:
- -h -p -U指定主库地址、端口和复制账号。
- -D是备库数据目录。
- -X stream表示把当前WAL日志以流式方式一并拉取,避免初始化过程中主库新产生的WAL没有被包含,导致备库启动后接不上。
- -R会在数据目录生成standby.signal文件,并自动写入primary_conninfo连接主库的信息。这是备库身份的关键标志,没有它备库会以普通库身份启动。
- -C -S slot_a_to_b自动创建物理复制槽,并且自动写到主库侧,省去手工建槽。
执行完成后启动备库:
pg_ctl -D /data/pandb start写到这里要强调:pg_basebackup拉取过程中,主库会有短暂的额外负载,如果你在业务高峰期做初始化,务必控制并发。多台备库同时拉的话,建议错峰,别图省事一次性拉起三台。
4.3 复制槽:防止WAL被过早回收
复制槽这个概念,搭建时容易忽略,故障时才知道重要。主库生成WAL,备库消费WAL,正常情况下备库消费多少主库就清理多少。但备库如果停机或者网络断了,主库不知道备库已经在落后,会继续按保留策略清理旧WAL,等备库恢复想要追日志时,主库已经把需要的WAL删了,后果就是备库报废,必须重新全量。
复制槽就是为解决这个问题存在的。一个物理复制槽会把主库的WAL保留到该槽对应的消费位点,备库还没消费的WAL,主库不得清理。刚才-C -S参数已经自动创建了槽,可以用下面SQL确认:
SELECT slot_name, slot_type, active, restart_lsn FROM pg_replication_slots;active字段为t表示备库正在通过该槽消费。有个副作用要提前知道:假如备库永久下线,这个槽会一直拖着主库WAL不清理,时间一长主库磁盘就被撑爆。所以复制槽必须纳入监控,发现某个槽长期不active就要排查,确认备库不会回来后要及时删槽:
SELECT pg_drop_replication_slot('slot_a_to_b');4.4 主备同步状态验证
集群起来不是终点,验证才是。在主库上执行:
SELECT client_addr, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;正常情况下state是streaming,sync_state是sync,三个lag字段都接近零或毫秒级。write_lag表示WAL发送到备库的延迟,flush_lag是备库落盘延迟,replay_lag是备库回放延迟。其中replay_lag最贴近真实数据可见性,做容灾演练时主要盯它。
在备库上验证身份和进度:
SELECT pg_is_in_recovery(); -- 返回t说明是备库 SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();两个LSN差距越小越好。还可以做一个直观小实验:在主库建一张临时表,插入几行,马上在备库查,如果能立刻看到,说明同步链路工作正常。注意备库上表数据可见性受hot_standby回放影响,设了remote_apply后,主库事务返回成功时备库基本已经可见。
5. 容灾切换演练与脑裂防护:从计划内切换到故障切换
5.1 计划内切换(switchover)的标准动作
双中心集群最核心的价值演练就是计划内切换:平时主库在A中心,现在要把主库切到B中心,比如机房搬迁、硬件维护,整个过程要求业务影响最小化。
我的标准步骤:
- 确认备库同步状态,sync_state必须为sync,replay_lag必须为零或极小。
- 通知应用侧把连接切到B中心数据库VIP或读连接串。
- 在A中心主库执行提升备库操作:
pg_ctl promote -D /data/pandb或者SQL:
SELECT pg_promote();- 确认B中心节点已变成主库,pg_is_in_recovery()返回f。
- 把A中心原主库转成备库。这里有个关键动作容易被漏:ensure原主库作为新备库连接到新主库后,要同步修改synchronous_standby_names,让新主库等待的备库变成原来的A中心节点,否则下一次故障切换就没有同步备库可等了。
- 用第4章的lag查询确认新备库追平,切换完成。
整个过程听着不复杂,但现场演练时我们第一次花了15分钟,原因就是应用连接串的切换文档没更新,DBA以为切了,应用侧配置还指向A中心。现在我们的规约是:数据库切换完成后,必须由应用团队回一条“连接验证通过”的确认,才宣告切换成功。
5.2 故障切换与脑裂:为什么两个节点不能拍脑袋自动决定谁当家
故障切换比计划内切换危险得多,核心问题是脑裂。设想一个场景:A、B两中心之间专线断了,业务在B中心仍然可用,运维在B中心把备库提升为主库。此时A中心的主库其实还活着,也在接受写入。两边数据同时变化,等网络恢复,两个中心的数据已经分叉,谁都补不回对方,整个集群宣告报废。
这就是典型的双节点脑裂。两个节点都认为自己该当主,谁也不服谁。要防脑裂,必须靠一个双方都信任的第三方裁判。我们这次设计用了仲裁节点配合同步确认逻辑。
一种做法是把同步提交名单改成quorum方式:
synchronous_commit = remote_apply synchronous_standby_names = 'ANY 2 (node_a, node_b, node_arb)'含义是:一次事务提交必须得到3个节点中至少2个节点的确认。正常情况下A、B都确认,仲裁节点只作陪跑。一旦网络分区发生,比如A和仲裁不通,A节点无法凑齐2个确认,写入会自动阻塞;而B节点只要能连上仲裁,就有B和仲裁两个确认,可以继续提交。这样数据只会在B中心继续增长,A中心不会产生新数据,等网络恢复,A中心完完整整追备库即可。这个机制把“谁该接管”交给法定多数决定,从机制上消灭双主。
如果不想让每次写事务都等仲裁节点拖慢性能,可以只把仲裁节点用于选主判定,写入确认仍然只要同步备库一个。生产环境很多团队用Patroni加etcd实现这个逻辑,etcd本身要求多数派健康才允许选主,本质是同一套思想。
5.3 切换后的数据校验和回切路径
容灾切换后,不能只看数据库起来就宣布恢复。我要求必须做三件校验:
- 关键业务表行数对比:切换前在主库统计一张核心表行数,切换后在备库/新主库重新统计,数字一致是最基础的数据存在性验证。
- 业务抽样验证:应用侧跑几个典型查询,比如按订单号查一笔最近交易,确认数据时间戳是新写入的,而不是缓存。
- 主备双节点LSN差额复查:确认另一个中心正在同步追平,且追平时间在可预期范围内。
回切比切换更麻烦,因为原A中心在故障期间处于停摆或数据落后状态,必须等它完全追上并处于同步状态后,才建议再切回去。切回时同样走5.1的计划内流程,不要觉得从B切回A就是重复一遍所以可以随便操作,回切出问题的情况非常多。
6. 上线后我踩过的坑:WAL堆积、网络抖动与归档联调
6.1 备库WAL堆积:一场差点撑爆磁盘的故障
集群上线第二周,我们监控告警主库数据盘使用率突破85%,趋势还在涨。排查下来,原因是备库触发了一次自动重启,重启期间复制槽虽然还在,但备库始终没有恢复消费,主库侧WAL被复制槽钉住不能清理,越积越多。
WAL堆积的排查路径是先看复制槽状态:
SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes FROM pg_replication_slots;lag_bytes持续增长就说明备库消费停滞。再结合备库日志看是不是恢复进程有问题。当时备库自动重启后迟迟没拉起来,是因为磁盘管理的脚本没把数据目录权限恢复正确,pandb用户无法访问数据卷,纯属低级失误。修复权限后,备库重新启动,通过复制槽把积压日志追平,主库磁盘占用随之回落。
这个坑给我的教训:复制槽是保数据的,不是保磁盘的,两者之间必须有一个磁盘增长监控,按槽位lag设置告警,而不是等磁盘快满了才发现。
6.2 网络抖动对同步复制的影响与降级策略
第二坑来自跨机房链路的抖动。某天线上反馈核心交易偶发响应变慢,数据库侧看到主库有大量walsender在等待备库确认,业务写入被阻塞了几百毫秒到一秒。原因是专线某段路由短暂拥塞,RTT从5ms跳到200ms,而我们的synchronous_commit是remote_apply,每次提交都在等跨中心确认,网络一跳,业务就跟着被拖住。
这个坑的解法不是放弃同步,而是要做“可降级”的同步设计。现在我们的处理是分层:
- 正常时期,保持remote_apply,追求RPO归零。
- 监控发现跨中心RTT持续超过阈值或同步备确认超时,快速把synchronous_commit调到on,也就是备库落盘即返回,牺牲一点RPO保命。
- 如果网络长时间不稳定,把备库从同步备名单里拿掉,改成异步追,等网络恢复且备库追平后再加回同步名单。
参数调整大多可以reload生效,不用重启。这里给一个建议:把synchronous_standby_names的设置写成一个运维脚本,包含正常、降级、恢复三套配置,切换时一键执行,避免故障状态下手忙脚乱敲SQL。
6.3 归档、全量备份与大数据平台取数的联动
最后说一个容易被忽略的联调项:归档日志和备份策略要配合。我们当时只做了归档,没有配归档清理,归档目录在两周内涨了几个GB,没感觉,但全量备份开始之后,归档目录增长更快,最终差点把归档盘填满。现在归档清理策略是:保留最近7天归档加上最近一次全量备份点之前的归档,确认PITR窗口满足后,旧归档自动删除。
容灾集群稳定运行后,业务方自然会拿着更多需求来找你,最常见的是“把核心库的表同步到大数据平台做分析”。我的建议是,别直接在容灾备库上长期跑重查询,备库的角色是接故障,不是当分析师。可以让ETL工具从备库导出增量数据到消息队列,或者搭一个独立的只读逻辑副本,再流入spark、hadoop这类离线集群,这样容灾链路和数据加工链路各走各的,互不拖累。我们后来就是加了逻辑复制节点单独喂数,容灾备库压力一下就下来了。
这套集群我运行了小半年,最大的体会是:流复制本身不难,难的是把“RPO是多少、网络抖动怎么办、切换谁说了算”这些问题在设计阶段就想明白。如果你也正在搭,建议先把第3章的参数表贴到监控里,再逼自己完整走一遍第5章的脑裂演练,再考虑上线。那一次演练能暴露的问题,比看十遍文档都多。