PaaS 篇(八):PostgreSQL 16 流复制主从架构实战
PaaS 篇八PostgreSQL 16 流复制主从架构实战系列《基于华为云 FlexusX 四节点集群的云计算全栈实操》—— PaaS 篇实测环境华为云 FlexusX 8vCPU/16GiB × 4 节点Ubuntu 24.04.4 LTS本文所有状态输出、复制数据均来自真实实验结果文件results/09_pg_master.txt、results/10_pg_slave.txt、results/11_pg_replication.txt未做篡改。一、引子对象存储解决文件放哪数据库解决状态放哪。到了 PaaS 层关系型数据库的高可用几乎等于一句话主库挂了从库能顶上主库写的从库读得到。上篇我们验证了对象存储的容错本篇落到有事务语义的 PostgreSQL 16。我会在 n3 起主库primary在 n4 起一个只读从库standby用 PostgreSQL 原生的**流复制Streaming Replication**把 WAL 实时传过去然后做三件事验证主库写入能从从库读到数据一致从库确实是只读写被拒复制通道状态是streaming / async实时异步。实测结果主库插 3 行后共 5 行从库秒级读到 5 行从库 INSERT 直接ERROR: cannot execute INSERT in a read-only transaction。二、背景与理论对照《深入浅出云计算》PaaS 篇《深入浅出云计算》PaaS 篇把托管数据库归为开箱即用的有状态服务强调它把备份、扩缩容、高可用从用户手里接走。但书里不会教你怎么从零搭一套主从——而这恰恰是自建 PaaS 的硬骨头。2.1 WAL 与物理复制PostgreSQL 的高可用根基是WALWrite-Ahead Log预写日志任何数据修改先写日志、再改页。流复制就是把主库的 WAL 实时推给从库从库回放replay这些日志从而保持与主库一致。这叫物理复制——复制的是磁盘块的变更不是逻辑 SQL。复制类型复制内容特点物理复制流复制WAL 字节流主从二进制级一致从库只读可整库热备逻辑复制解码后的 SQL/行变更可选择性发布表从库可写跨版本支持本篇用物理流复制因为它是做高可用/只读备库的标准答案。2.2 同步 vs 异步流复制可配同步synchronous_commit 同步备库或异步。本实验是异步async主库提交事务不等待从库确认吞吐高、主库不被从库拖慢代价是极端故障下可能丢最后几笔未传过去的写入。results/11_pg_replication.txt里主库看到的sync_state async就印证了这点。2.3 关键机制速记wal_levelreplicaWAL 记录级别流复制要求至少replica默认即此。pg_basebackup -R从主库拉一份基础备份-R会自动生成standby.signal和primary_conninfo让从库开箱即备。standby.signal文件存在 该实例以只读备库身份启动PostgreSQL 12 用信号文件取代recovery.conf。hot_standbyon允许备库在接受连接时提供只读查询。pg_stat_replication主库侧监控复制状态的系统视图write_lag/replay_lag反映延迟。三、架构图示主库 PRIMARY (n3) 192.168.0.241:5432 postgres:16 wal_levelreplica replicator 角色 (REPLICATION) t_demo 表: 初始 2 行 → 再插 3 行 5 行 │ WAL 流推送 (async, streaming) primary_conninfo → 192.168.0.241 │ ▼ 从库 STANDBY (n4) 192.168.0.150:5432 postgres:16 standby.signal 存在 → 只读 hot_standbyon 读到 5 行INSERT 被拒四、环境与准备节点弹性公网IP私有IP规格系统角色node1113.47.6.41192.168.0.2528vCPU/16GiBUbuntu 24.04.4 LTS本篇未用node2124.70.93.52192.168.0.648vCPU/16GiBUbuntu 24.04.4 LTS被 pam_faillock 锁未用node31.94.220.182192.168.0.2418vCPU/16GiBUbuntu 24.04.4 LTSPG 主库 PRIMARYnode4124.70.102.139192.168.0.1508vCPU/16GiBUbuntu 24.04.4 LTSPG 从库 STANDBY四节点同 VPC 子网192.168.0.0/24内网互通均装 Docker 华为云镜像加速。注本篇主从仅用 n3/n4 两节点与 MinIO 篇的 n1/n3/n4 不同——因为数据库主从是 1 主 1 从的经典两节点结构足够演示流复制。node2 仍因前篇的pam_faillock锁定问题未参与。五、实操步骤完整可复现命令5.1 主库搭建在 n3 执行脚本scripts/pg_master_setup.sh关键行SLAVE_IP192.168.0.150dockerrm-fpg-master2/dev/null||truerm-rf/root/pgdata-master;mkdir-p/root/pgdata-master# 启动主库容器开启流复制所需参数dockerrun-d--namepg-master--restartalways--nethost\-ePOSTGRES_PASSWORDPg2026pass\-v/root/pgdata-master:/var/lib/postgresql/data\postgres:16\-cwal_levelreplica-cmax_wal_senders10-cmax_replication_slots10\-chot_standbyon-clisten_addresses*等主库就绪后建复制用户、放开 pg_hba、建测试表# 创建复制专用角色dockerexecpg-master psql-Upostgres-c\CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD Rep2026pass;# 放行从库的内网复制连接 整个 VPC 的访问dockerexecpg-masterbash-c\echo host replication replicator${SLAVE_IP}/32 md5 /var/lib/postgresql/data/pg_hba.confdockerexecpg-masterbash-c\echo host all all 192.168.0.0/24 md5 /var/lib/postgresql/data/pg_hba.confdockerexecpg-master psql-Upostgres-cSELECT pg_reload_conf();# 测试表 初始 2 行dockerexecpg-master psql-Upostgres-c\CREATE TABLE t_demo(id serial primary key, msg text, ts timestamptz default now());dockerexecpg-master psql-Upostgres-c\INSERT INTO t_demo(msg) VALUES (master-init-row-1),(master-init-row-2);dockerexecpg-master psql-Upostgres-cSELECT count(*) AS rows_on_master FROM t_demo;要点--nethost同样用于数据库容器主从走内网 IP 直连避免端口映射带来的地址错乱。max_wal_senders/max_replication_slots是流复制的名额参数给足以便后续扩展更多从库。pg_hba.conf里host replication replicator 192.168.0.150/32 md5是安全边界——只放行从库 IP 用 replicator 做复制。5.2 从库搭建在 n4 执行脚本scripts/pg_slave_setup.sh核心是先pg_basebackup -R拉基础备份MASTER_IP192.168.0.241dockerrm-fpg-slave2/dev/null||truerm-rf/root/pgdata-slave;mkdir-p/root/pgdata-slave# 从主库拉基础备份-R 自动生成 standby.signal primary_conninfodockerrun--rm--nethost-ePGPASSWORDRep2026pass-v/root/pgdata-slave:/backup postgres:16\bash-cpg_basebackup -h${MASTER_IP}-p 5432 -U replicator -D /backup -Fp -Xs -R -P echo BASEBACKUP_OK# 向 primary_conninfo 注入复制密码pg_basebackup -R 生成的连接串不含密码sed-is|primary_conninfo |primary_conninfo passwordRep2026pass |\/root/pgdata-slave/postgresql.auto.conf# 启动从库容器dockerrun-d--namepg-slave--restartalways--nethost\-v/root/pgdata-slave:/var/lib/postgresql/data\postgres:16-clisten_addresses*pg_basebackup -R干的活非常关键-Xs流式拷贝 WAL保证基础备份结束点与 WAL 连续从库启动后能无缝接着追。-R自动写入standby.signal和带primary_conninfo的postgresql.auto.conf从库读这个就能知道去哪同步、用什么身份。我额外用sed把passwordRep2026pass塞进primary_conninfo因为-R生成的连接串不含密码不补上从库连不上主库。六、真实输出贴实测未篡改6.1 主库就绪results/09_pg_master.txt[1] 启动主库容器 (开启流复制参数) a8036cf54b85e56a0a2455c6d927be36d90aa201afb7fd9cf7f111cdd0e8cb0f [2] 等待主库就绪 /var/run/postgresql:5432 - accepting connections [3] 创建复制用户 replicator CREATE ROLE [4] 配置 pg_hba.conf 允许从库复制与内网访问 pg_reload_conf ---------------- t (1 row) [5] 创建测试表并写入初始数据 CREATE TABLE INSERT 0 2 rows_on_master ---------------- 2 (1 row) MASTER READY主库就绪测试表t_demo初始 2 行。6.2 从库成为只读备库results/10_pg_slave.txt[1] 从主库拉取基础备份 (pg_basebackup -R 生成 standby 配置) BASEBACKUP_OK [2] 向 primary_conninfo 注入复制密码 primary_conninfo passwordRep2026pass userreplicator passwordRep2026pass channel_bindingprefer host192.168.0.241 port5432 sslmodeprefer ... -rw------- 1 root root 0 Jul 26 16:04 /root/pgdata-slave/standby.signal standby.signal exists - 该实例将以只读备库启动 [3] 启动从库容器 bd146042d93aaa8f0af0ef9adac9484860c921f3efc3f8db767ea1baa042ad4f [4] 等待从库就绪 /var/run/postgresql:5432 - accepting connections [5] 确认从库处于恢复(备库)模式 is_standby ------------ t (1 row) SLAVE READYstandby.signal文件存在、且pg_is_in_recovery()返回t从库确认进入只读恢复模式。6.3 复制状态与数据一致性results/11_pg_replication.txt主库侧看复制通道 [主库] 查看复制状态 pg_stat_replication -[ RECORD 1 ]------------------ application_name | walreceiver client_addr | 192.168.0.150 state | streaming sync_state | async write_lag | replay_lag |client_addr 192.168.0.150正是 n4 从库的内网 IP证明连接来源正确。state streamingWAL 正在实时流式推送不是落后的归档恢复。sync_state async异步复制主库提交不阻塞等从库确认。write_lag/replay_lag为空说明延迟极小实测量级在毫秒内视图未采样到非零值。主库再插 3 行 → 共 5 行 [主库] 新写入3行 INSERT 0 3 master_rows ------------- 5 (1 row)从库读到的数据与主库完全一致 [从库] 读取数据(验证同步) slave_rows ------------ 5 (1 row) id | msg ----------------------- 1 | master-init-row-1 2 | master-init-row-2 3 | sync-test-A 4 | sync-test-B 5 | sync-test-C (5 rows)从库读到 5 行且 3 条新插入的sync-test-*与主库完全对应证明异步流复制在秒级内完成了同步。从库尝试写入被拒证实只读 [从库] 尝试写入(应被拒绝, 证明只读) ERROR: cannot execute INSERT in a read-only transaction七、深度解读重点7.1 异步复制下一致性的边界在哪实测里主库 INSERT 后从库立刻读到 5 行看着像同步但sync_stateasync告诉我们主库返回成功时并不保证从库已经收到这笔 WAL。本实验主从在同一 VPC、RTT 极低所以延迟小到write_lag/replay_lag采样为空。但你要清醒异步复制的语义是最终一致不是强一致。若主库刚提交就宕机且那批 WAL 还没传给从库切换后那几笔就丢了。是否要 sync取决于你业务的容丢数据窗口。7.2 pg_basebackup -R 为什么是银弹没有-R的年代搭从库要手工写recovery.conf、配primary_conninfo、建standby.signal。-R把这些一步打包它拉完基础备份直接把我是备库、去连主库的声明写进postgresql.auto.conf并放好standby.signal。我从库脚本里唯一补的一行sed只是把密码塞进连接串——因为安全起见-R不会把密码明文写进自动配置。这个细节很能说明 PostgreSQL 的安全设计取向连接信息自动生成敏感凭证留给你自己补。7.3 用 pg_stat_replication 监控延迟的正确姿势write_lag是主库已写出的 WAL 与从库已接收的 WAL 之差replay_lag是主库已写出的 WAL 与从库已回放的 WAL 之差。replay_lag 才是用户感知的从库数据落后多少。生产监控要把这两个字段配上告警阈值比如 replay_lag 30s 报警。本实验两字段皆空说明延迟在采样间隔内可忽略——这是同机房内网的常态跨 AZ/跨地域就会明显。7.4 hot_standby 与只读的价值hot_standbyon让备库能承接只读查询这正是读写分离的物理基础报表、聚合、备份导出全打到从库主库专注写入互不挤占。但这也引出约束——从库永远不能写实测 INSERT 直接报错所有写必须回主库。应用层要做连接路由读走从、写走主这块是业务改造的重点。八、踩坑与排障真实的坑8.1 pg_basebackup -R 生成的连接串不含密码这是本篇最实的坑pg_basebackup -R生成的primary_conninfo里有userreplicator host...但没有password。若直接用从库启动后连主库会因缺密码反复重试、卡在恢复态连不上。我用一行sed把passwordRep2026pass插到primary_conninfo 之后解决实测results/10_pg_slave.txt里那行primary_conninfo已经带着密码从库顺利进入streaming。8.2 不要在主库目录上直接跑 pg_basebackup 当从库pg_basebackup的目标目录/root/pgdata-slave必须是空目录或不存在它会自己初始化。我脚本里先rm -rf /root/pgdata-slave再建避免残留postgresql.auto.conf或旧standby.signal造成从库以为自己是主库的混乱。8.3 node2 锁定的连带影响node2124.70.93.52的pam_faillock锁定详见上篇让我们本轮只能用 n3/n4 两节点。本篇主从两节点刚好够但如果要演示一主多从或同步复制 多备仲裁少一个节点会很局促。结论不变批量运维脚本要控制 SSH 并发关键节点留带外通道。九、读写分离、故障切换promote与自建 vs 云 RDS9.1 读写分离怎么落主从搭好后典型架构应用写 → 主库 (n3:5432) 应用读 → 从库 (n4:5432) ← 报表/统计/备份实现方式有三种应用层路由代码里区分读/写数据源如 Spring 的AbstractRoutingDataSource。中间件Pgpool-II / HAProxy 做读负载均衡与故障探测。连接池PgBouncer 只管连接复用路由仍需上层配合。9.2 故障切换promote主库宕机时把从库提升为主# 在从库 n4 执行dockerexecpg-slave pg_ctl promote-D/var/lib/postgresql/data# 或 SQL: SELECT pg_promote();pg_promote()会让从库停止恢复、以主库身份接受写入。之后旧主恢复要当作新从库重新pg_basebackup拉数据——promote 是不可逆的脑裂风险要靠 fencing如关旧主、VIP 漂移规避。本实验未做自动 failover生产环境建议上repmgr或云厂商托管。9.3 自建 vs 云 RDS取舍与成本维度自建 PG 流复制本实验云 RDS for PostgreSQL高可用自己搭主从、自己 promote、自己防脑裂多可用区自动故障切换SLA 99.95%备份自己配 pg_dump / 归档 / PITR自动备份 时间点恢复运维版本升级、参数调优、磁盘扩容全自己来控制台一键托管升级成本付实例 云盘无额外 license 费实例费 备份存储费 可能的高可用副本费可控性全可控可改任何参数、接任何工具受托管边界限制部分参数只读适用成本敏感、需要深度调优/合规隔离、团队有 DBA要省心、要 SLA、要快速起量我的观点中小团队、核心业务、没人专职盯库——直接上云 RDS把高可用和备份交给厂商你买的是睡觉安稳。但如果你有 DBA、对参数和成本敏感、或数据合规要求不出私网自建流复制主从完全可行本篇就是可复现的范本。别两个极端来回跳要么全托管要么全自建但配齐监控与 failover 演练——最怕自建却当托管用出了事才发现有脑裂、没备份。十、小结本篇在华为云 FlexusX 的 n3/n4 两节点上跑通了 PostgreSQL 16 流复制主从主库wal_levelreplica建replicator角色pg_hba 放行从库 IP从库pg_basebackup -R拉基础备份补密码后standby.signal存在 → 只读备库启动is_standbyt主库pg_stat_replication显示client_addr192.168.0.150、statestreaming、sync_stateasync主库插 3 行共 5 行从库秒级读到 5 行数据完全一致且从库写入被拒ERROR: cannot execute INSERT in a read-only transaction。流复制不是黑盒它是 WAL 从主库流到从库并被回放的过程。异步给你吞吐同步给你安全选哪个是你的业务决策而pg_stat_replication的replay_lag是你判断从库到底落后多少的唯一真相来源。参考脚本本篇命令均来自仓库scripts/scripts/pg_master_setup.sh—— 主库搭建含 replicator、pg_hba、测试数据scripts/pg_slave_setup.sh—— 从库搭建pg_basebackup -R promote 准备实测结果results/09_pg_master.txt、results/10_pg_slave.txt、results/11_pg_replication.txtPaaS 篇至此覆盖对象存储与关系型数据库两大基石。后续可延伸到消息队列Kafka/RabbitMQ、缓存Redis 主从哨兵与 Kubernetes 编排——它们同样能在 FlexusX 集群上跑出真实数据。