PostgreSQL流复制实现主从数据同步

环境准备

角色 IP 端口 数据库版本
Master,主服务器 10.0.0.18 5432 PostgreSQL 17.11
Standby,后备服务器 10.0.0.28 5432 PostgreSQL 17.11

主从数据同步的整体模型

html 复制代码
主服务器 10.0.0.18
  事务提交 → 写 WAL
            → walwriter 刷盘
            → walsender 读 WAL
            → TCP 流送给备库

后备服务器 10.0.0.28
  walreceiver 收 WAL → 写 pg_wal
  startup 进程重放 WAL
  hot_standby=on → 可接受只读查询

#总体流程
10.0.0.18 Primary
  wal_level=replica
  max_wal_senders=10
  replicator 用户
  standby28 复制槽
        │
        │  WAL 流(TCP 5432)
        ▼
10.0.0.28 Standby
  standby.signal
  primary_conninfo -> 10.0.0.18
  primary_slot_name = standby28
  hot_standby = on
  startup 重放 WAL
  walreceiver 收 WAL

实现的是:
✅ 主从物理数据同步
✅ 备库热备只读
✅ 断线可恢复(复制槽 / wal_keep_size / 归档)
✅ 主库宕机可手动 promote
但不是:
❌ 自动选主
❌ 防脑裂
❌ 应用自动找新主

官方定义:

流复制允许一台后备服务器比基于文件的日志传送更能保持最新状态。

主服务器在 WAL 记录产生时即将它们以流式传送给后备服务器,而不必等到 WAL 文件被填充。

一、 Master节点配置

bash 复制代码
#创建复制的用户并授权
postgres=# CREATE ROLE replicator WITH REPLICATION LOGIN ENCRYPTED PASSWORD 'Repl@Strong2026.';

#修改访问控制文件 pg_hba.conf
# 允许来自备库IP 10.0.0.28的专用复制账号连接,认证方式使用md5
[root@Master ~]#cat /apps/pgsql/data/pg_hba.conf | grep -v "^#" | grep -v "^$"
local   all             all                                     md5
host    all             all             127.0.0.1/32            md5
host    all             all             ::1/128                 md5
local   replication     all                                     md5
host    replication     all             127.0.0.1/32            md5
host    replication     all             ::1/128                 md5
host    replication     replicator      10.0.0.28/32            md5

#修改核心配置文件 postgresql.conf
[root@Master ~]#vim /apps/pgsql/data/postgresql.conf
# 开启归档
archive_mode = on
#主服务器的归档位置
archive_command = 'test ! -f /apps/pgsql/archive/%f && cp %p /apps/pgsql/archive/%f'
#WAL 日志的详细程度
wal_level = replica
#配置主库最大并发WAL发送进程数,避免备库连接时被拒绝
max_wal_senders = 10
#如果后续要使用复制槽保障WAL不被提前回收,需要提前配置该参数
max_replication_slots = 10
#主库至少额外保留 1GB WAL 给所有备库兜底
wal_keep_size = 1024MB
#如果用了复制槽,单个槽最多堆 512MB WAL,超了就强制删
max_slot_wal_keep_size = 512MB
#监听所有网络地址,允许备库发起复制连接
listen_addresses = '*'
#开启热备支持,允许备库在恢复模式下提供只读查询
hot_standby = on
#主库侧复制连接存活检测,60 秒没收到备库响应就断开 wal sender
wal_sender_timeout = 60s
#PostgreSQL 最多允许 30 个客户端同时连接
max_connections = 30
#PostgreSQL 的核心数据缓存区
shared_buffers = 512MB
#用 POSIX 共享内存(/dev/shm)做并行查询的动态内存分配
dynamic_shared_memory_type = posix
#备库在 WAL 重放和只读查询冲突时的"缓冲时间"
max_standby_streaming_delay = 30s
#让备库把读状态反馈给主库,阻止主库 VACUUM 清理旧行
hot_standby_feedback = on
#让备库每 10 秒向主库汇报一次复制进度和读状态
wal_receiver_status_interval = 10s
#让 PostgreSQL 把页面 hint bit 的修改也写入 WAL
wal_log_hints = on
#WAL 写到 512MB 就触发自动 checkpoint
max_wal_size = 512MB
#checkpoint 后至少保留 128MB WAL 文件复用
min_wal_size = 128MB
#日志时间戳显示时区
log_timezone = 'Asia/Shanghai'
#会话默认时区
timezone = 'Asia/Shanghai'
#日期时间格式、年月日顺序
datestyle = 'iso, ymd'
#PostgreSQL 的所有系统消息(报错、警告、日志、psql 提示)统一用英文输出
lc_messages = C
#让 PostgreSQL 格式化货币值时使用美式英语的货币格式
lc_monetary = C
#数字格式化统一,API/程序友好
lc_numeric = C
#让 PostgreSQL 的星期名、月份名等时间文本用英文输出
lc_time = C
#全文检索的默认语言配置
default_text_search_config = 'pg_catalog.english'
#让 PostgreSQL 把日志写到标准错误,由 systemd/journald 或 Docker 日志驱动接管落盘
log_destination = 'stderr'
#让 PostgreSQL 自己启动 logger 进程,把 stderr 日志拦截写成磁盘文件
logging_collector = on
#PostgreSQL 日志文件落盘的具体路径
log_directory = '/apps/pgsql/log'
#让 日志文件按天命名,简洁直观,运维友好
log_filename = 'postgresql-%Y-%m-%d.log'
#让 PostgreSQL 日志文件只有 postgres 用户能读写,其他用户完全不可见
log_file_mode = 0600

[postgres@Master ~]$pg_ctl restart -mf -D /apps/pgsql/data/

[root@Master ~]#chmod 644 /apps/pgsql/data/postgresql.conf.bak.20260916
[root@Master ~]#chown -R postgres:postgres /apps/pgsql/data

二、Standby 节点配置

bash 复制代码
# 停库
[postgres@Standby ~]$pg_ctl stop -mf -D /apps/pgsql/data/

# 删除数据及归档日志
[postgres@Standby ~]$rm -rf /apps/pgsql/data/*
[postgres@Standby ~]$rm -rf /apps/pgsql/archive/*
[postgres@Standby ~]$rm -rf /apps/pgsql/backup/*

#备份主库数据到备库
[postgres@Standby ~]$pg_basebackup -D /apps/pgsql/backup/ -Ft -Pv -Ureplicator -h 10.0.0.18 -p 5432 -R

#还原备份的数据,实现初始的主从数据同步
[postgres@Standby ~]$tar xf /apps/pgsql/backup/base.tar -C /apps/pgsql/data
[postgres@Standby ~]$tar xf /apps/pgsql/backup/pg_wal.tar -C /apps/pgsql/data/pg_wal/

# 修改权限
[root@Standby ~]#chmod 700 /apps/pgsql/data
[root@Standby ~]#chown -R postgres:postgres /apps/pgsql/data


#修改postgresql.conf文件
[postgres@Standby ~]$vim /apps/pgsql/data/postgresql.conf
primary_conninfo = 'host=10.0.0.18 port=5432 user=replicator password=Repl@Strong2026.'

#开启热备支持,允许备库在恢复模式下提供只读查询
hot_standby = on
#恢复时自动追到最新的 timeline
recovery_target_timeline = 'latest'
#PostgreSQL 最多允许 30 个客户端同时连接
max_connections = 30
#备库在 WAL 重放和只读查询冲突时的"缓冲时间"
max_standby_streaming_delay = 30s
#让备库每 10 秒向主库汇报一次复制进度和读状态
wal_receiver_status_interval = 10s
#让备库把读状态反馈给主库,阻止主库 VACUUM 清理旧行
hot_standby_feedback = on
#配置主库最大并发WAL发送进程数,避免备库连接时被拒绝
max_wal_senders = 10
#让 PostgreSQL 自己启动 logger 进程,把 stderr 日志拦截写成磁盘文件
logging_collector = on
#PostgreSQL 日志文件落盘的具体路径
log_directory = '/apps/pgsql/log'
#让 日志文件按天命名,简洁直观,运维友好
log_filename = 'postgresql-%Y-%m-%d.log'
#监听所有网络地址,允许备库发起复制连接
listen_addresses = '*'
#PostgreSQL 的核心数据缓存区
shared_buffers = 512MB
#用 POSIX 共享内存(/dev/shm)做并行查询的动态内存分配
dynamic_shared_memory_type = posix
#WAL 日志的详细程度
wal_level = replica
#让 PostgreSQL 把页面 hint bit 的修改也写入 WAL
wal_log_hints = on
#WAL 写到 512MB 就触发自动 checkpoint
max_wal_size = 512MB
#checkpoint 后至少保留 128MB WAL 文件复用
min_wal_size = 128MB
# 开启归档
archive_mode = on
#主服务器的归档位置
archive_command = 'test ! -f /apps/pgsql/archive/%f && cp %p /apps/pgsql/archive/%f'
#配置主库最大并发WAL发送进程数,避免备库连接时被拒绝
max_wal_senders = 10
#如果后续要使用复制槽保障WAL不被提前回收,需要提前配置该参数
max_replication_slots = 10
#主库至少额外保留 1GB WAL 给所有备库兜底
wal_keep_size = 1024MB
#如果用了复制槽,单个槽最多堆 512MB WAL,超了就强制删
max_slot_wal_keep_size = 512MB
#主库侧复制连接存活检测,60 秒没收到备库响应就断开 wal sender
wal_sender_timeout = 60s
#备库在 WAL 重放和只读查询冲突时的"缓冲时间"
max_standby_streaming_delay = 30s
#让备库每 10 秒向主库汇报一次复制进度和读状态
wal_receiver_status_interval = 10s
#让备库把读状态反馈给主库,阻止主库 VACUUM 清理旧行
hot_standby_feedback = on
#日志时间戳显示时区
log_timezone = 'Asia/Shanghai'
#日期时间格式、年月日顺序
datestyle = 'iso, ymd'
#会话默认时区
timezone = 'Asia/Shanghai'
#PostgreSQL 的所有系统消息(报错、警告、日志、psql 提示)统一用英文输出
lc_messages = C
#让 PostgreSQL 格式化货币值时使用美式英语的货币格式
lc_monetary = C
#数字格式化统一,API/程序友好
lc_numeric = C
#让 PostgreSQL 的星期名、月份名等时间文本用英文输出
lc_time = C
#全文检索的默认语言配置
default_text_search_config = 'pg_catalog.english'
#让 PostgreSQL 把日志写到标准错误,由 systemd/journald 或 Docker 日志驱动接管落盘
log_destination = 'stderr'
#让 PostgreSQL 自己启动 logger 进程,把 stderr 日志拦截写成磁盘文件
logging_collector = on
#PostgreSQL 日志文件落盘的具体路径
log_directory = '/apps/pgsql/log'
#让 日志文件按天命名,简洁直观,运维友好
log_filename = 'postgresql-%Y-%m-%d.log'
#让 PostgreSQL 日志文件只有 postgres 用户能读写,其他用户完全不可见
log_file_mode = 0600

#启动从库
[root@Standby ~]#su - postgres
[postgres@Standby ~]$pg_ctl start -mf -D /apps/pgsql/data/

三、监控同步状态

3.1在主库查看状态

bash 复制代码
[root@Master ~]#pg_controldata
pg_control version number:            1700
Catalog version number:               202406281
Database system identifier:           7675300641084329406	
Database cluster state:               in production		#主库正常运行
pg_control last modified:             Sat 17 Oct 2026 01:21:02 AM CST
Latest checkpoint location:           0/280000B8	
Latest checkpoint's REDO location:    0/28000060
Latest checkpoint's REDO WAL file:    000000010000000000000028
Latest checkpoint's TimeLineID:       1
Latest checkpoint's PrevTimeLineID:   1
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID:          0:823
Latest checkpoint's NextOID:          32769
Latest checkpoint's NextMultiXactId:  1
Latest checkpoint's NextMultiOffset:  0
Latest checkpoint's oldestXID:        731
Latest checkpoint's oldestXID's DB:   1
Latest checkpoint's oldestActiveXID:  823
Latest checkpoint's oldestMultiXid:   1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint:            Sat 17 Oct 2026 01:21:02 AM CST
Fake LSN counter for unlogged rels:   0/3E8
Minimum recovery ending location:     0/0
Min recovery ending loc's timeline:   0
Backup start location:                0/0
Backup end location:                  0/0
End-of-backup record required:        no
wal_level setting:                    replica		#支持复制
wal_log_hints setting:                on
max_connections setting:              30
max_worker_processes setting:         8
max_wal_senders setting:              10
max_prepared_xacts setting:           0
max_locks_per_xact setting:           64
track_commit_timestamp setting:       off
Maximum data alignment:               8
Database block size:                  32768
Blocks per segment of large relation: 32768
WAL block size:                       8192
Bytes per WAL segment:                16777216
Maximum length of identifiers:        64
Maximum columns in an index:          32
Maximum size of a TOAST chunk:        8140
Size of a large-object chunk:         8192
Date/time type storage:               64-bit integers
Float8 argument passing:              by value
Data page checksum version:           1		#开了数据校验
Mock authentication nonce:            2fe4b4d8a89fa2be457455b19c8c831772616a1b419896ff0664226da2ed2997

#主库建复制槽(防 WAL 被回收)
postgres=# SELECT pg_create_physical_replication_slot('standby28');
 pg_create_physical_replication_slot 
-------------------------------------
 (standby28,)

postgres=# SELECT slot_name, active, restart_lsn FROM pg_replication_slots;
 slot_name | active | restart_lsn 
-----------+--------+-------------
 standby28 | t      | 0/28000168

postgres=# SELECT client_addr, state, sync_state, sent_lsn, write_lsn, flush_lsn, replay_lsn FROM pg_stat_replication; client_addr |   state   | sync_state |  sent_lsn  | write_lsn  | flush_lsn  | replay_lsn 
-------------+-----------+------------+------------+------------+------------+------------
 10.0.0.28   | streaming | async      | 0/28000168 | 0/28000168 | 0/28000168 | 0/28000168

postgres=# SELECT pid,state,client_addr,sync_priority,sync_state FROM pg_stat_replication;
 pid  |   state   | client_addr | sync_priority | sync_state 
------+-----------+-------------+---------------+------------
 2197 | streaming | 10.0.0.28   |             0 | async

postgres=# SELECT pg_current_wal_insert_lsn(),* FROM pg_stat_replication; 
 pg_current_wal_insert_lsn | pid  | usesysid |  usename   | application_name | client_addr | client_hostname | client_
port |         backend_start         | backend_xmin |   state   |  sent_lsn  | write_lsn  | flush_lsn  | replay_lsn | 
write_lag | flush_lag | replay_lag | sync_priority | sync_state |          reply_time           
---------------------------+------+----------+------------+------------------+-------------+-----------------+--------
-----+-------------------------------+--------------+-----------+------------+------------+------------+------------+-
----------+-----------+------------+---------------+------------+-------------------------------
 0/28000168                | 2197 |    32768 | replicator | walreceiver      | 10.0.0.28   |                 |       6
1454 | 2026-10-17 10:04:32.125342+08 |              | streaming | 0/28000168 | 0/28000168 | 0/28000168 | 0/28000168 | 
          |           |            |             0 | async      | 2026-10-17 10:37:12.966612+08

#主库当前写入位置 = 备库回放位置,完全一致,零字节延迟
pg_current_wal_insert_lsn = 0/28000168
sent_lsn = write_lsn = flush_lsn = replay_lsn = 0/28000168

#服务器查看数据库是否为备库,f表主库, t表示为备库
postgres=# SELECT * FROM pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 f
 
 postgres=# SELECT application_name,client_addr,sync_state FROM pg_stat_replication;
 application_name | client_addr | sync_state 
------------------+-------------+------------
 walreceiver      | 10.0.0.28   | async

#如果连接被成功建立,将在后备服务器中看到一个 walreceiver,并且在主服务器中有一个相应的 walsender 进程
[root@Master ~]#ps aux |grep walsender
postgres    2197  0.0  0.6 690112 12092 ?        Ss   10:04   0:00 postgres: walsender replicator 10.0.0.28(61454) streaming 0/28000168
root        2285  0.0  0.0  12220  1096 pts/2    R+   11:04   0:00 grep --color=auto walsender

#新建数据库
postgres=# CREATE DATABASE testdb;
postgres=# \l
                                                List of databases
   Name    |  Owner   | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules |   Access privileges   
-----------+----------+----------+-----------------+---------+-------+--------+-----------+-----------------------
 postgres  | postgres | UTF8     | libc            | C       | C     |        |           | 
 template0 | postgres | UTF8     | libc            | C       | C     |        |           | =c/postgres          +
           |          |          |                 |         |       |        |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | C       | C     |        |           | =c/postgres          +
           |          |          |                 |         |       |        |           | postgres=CTc/postgres
 testdb    | postgres | UTF8     | libc            | C       | C     |        |           | 

postgres=# SELECT pg_current_wal_lsn();
 pg_current_wal_lsn 
--------------------
 0/28109130

postgres=# SELECT application_name,state,sent_lsn,pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS unsent,sync_state FROM pg_stat_replication;
 application_name |   state   |  sent_lsn  | unsent | sync_state 
------------------+-----------+------------+--------+------------
 walreceiver      | streaming | 0/28109130 |      0 | async

#量化主备复制延迟
postgres=# SELECT client_addr,
postgres-#        pg_wal_lsn_diff(pg_current_wal_insert_lsn(), replay_lsn) AS lag_bytes,
postgres-#        pg_wal_lsn_diff(pg_current_wal_insert_lsn(), replay_lsn) / 1024 / 1024 AS lag_mb
postgres-# FROM pg_stat_replication;
 client_addr | lag_bytes |         lag_mb         
-------------+-----------+------------------------
 10.0.0.28   |         0 | 0.00000000000000000000

#主库当前最新写入位置:pg_current_wal_insert_lsn()
#备库已回放到哪:replay_lsn
#主库最新位置 − 备库回放位置 = 延迟量,0/28109130 − 0/28109130 = 0

postgres=# SELECT pg_current_wal_insert_lsn(),pg_current_wal_lsn(),pg_current_wal_flush_lsn();
 pg_current_wal_insert_lsn | pg_current_wal_lsn | pg_current_wal_flush_lsn 
---------------------------+--------------------+--------------------------
 0/28109130                | 0/28109130         | 0/28109130

#一般监控用 pg_current_wal_insert_lsn() 最准

#主库上直接看备库回放位置(不用登备库)
postgres=# SELECT replay_lsn FROM pg_stat_replication;
 replay_lsn 
------------
 0/28109130

postgres=# SELECT
postgres-#     client_addr AS standby_ip,
postgres-#     pg_current_wal_insert_lsn() AS primary_lsn,
postgres-#     replay_lsn AS standby_replay_lsn,
postgres-#     pg_wal_lsn_diff(pg_current_wal_insert_lsn(), replay_lsn) AS lag_bytes
postgres-# FROM pg_stat_replication;
 standby_ip | primary_lsn | standby_replay_lsn | lag_bytes 
------------+-------------+--------------------+-----------
 10.0.0.28  | 0/28109130  | 0/28109130         |         0

#standby_ip:备库 IP
#primary_lsn:主库当前最新写入位置
#standby_replay_lsn:备库已回放到这个位置
#lag_bytes:差值 = 0 字节延迟
字段 值 含义
pid 2197 主库 walsender 进程
usename replicator 复制用户
client_addr 10.0.0.28 备库 IP
backend_start 10:04:32 从 restart 后连的,稳定运行
state streaming 流式复制
sync_state async 异步模式(没配 sync,正常)
reply_time 10:37:12 30 秒前还在回包,连接活跃
write_lag / flush_lag / replay_lag 全空 零延迟,所以没值
application_name walreceiver 备库上 walreceiver 进程连主库时,默认用这个名字注册

查询结果解读

列 值 含义
client_addr 10.0.0.28 备库 IP
lag_bytes 0 主备延迟 0 字节
lag_mb 0 主备延迟 0 MB
想查什么 在哪查 用什么
主库写到哪了 主库 pg_current_wal_insert_lsn()
备库收到没 主库 pg_stat_replication.sent_lsn
备库落盘没 主库 pg_stat_replication.flush_lsn
备库回放没 主库 replay_lsn 或 备库 pg_last_wal_replay_lsn() 两者都行
延迟多少 主库 pg_wal_lsn_diff(主, 备)

3.2在从库查看状态

bash 复制代码
#备库加配置
[postgres@Standby ~]$vim /apps/pgsql/data/postgresql.conf
primary_slot_name = 'standby28'

#让复制槽生效(需要 restart)
[postgres@Standby ~]$pg_ctl restart -D /apps/pgsql/data

[postgres@Standby ~]$pg_controldata
pg_control version number:            1700
Catalog version number:               202406281
Database system identifier:           7675300641084329406	#跟主库一模一样,同一个集群
Database cluster state:               in archive recovery	#备库正常状态,正在恢复模式
pg_control last modified:             Sat 17 Oct 2026 10:04:32 AM CST
Latest checkpoint location:           0/280000B8	#跟主库 checkpoint 一致
Latest checkpoint's REDO location:    0/28000060
Latest checkpoint's REDO WAL file:    000000010000000000000028	#跟主库同一个 WAL 段
Latest checkpoint's TimeLineID:       1
Latest checkpoint's PrevTimeLineID:   1
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID:          0:823
Latest checkpoint's NextOID:          32769
Latest checkpoint's NextMultiXactId:  1
Latest checkpoint's NextMultiOffset:  0
Latest checkpoint's oldestXID:        731
Latest checkpoint's oldestXID's DB:   1
Latest checkpoint's oldestActiveXID:  823
Latest checkpoint's oldestMultiXid:   1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint:            Sat 17 Oct 2026 01:21:02 AM CST
Fake LSN counter for unlogged rels:   0/3E8
Minimum recovery ending location:     0/28000168	#跟主库当前 LSN 一致
Min recovery ending loc's timeline:   1	  #跟主库同一个 timeline
Backup start location:                0/0
Backup end location:                  0/0
End-of-backup record required:        no
wal_level setting:                    replica	#支持复制
wal_log_hints setting:                on
max_connections setting:              30	#跟主库一致
max_worker_processes setting:         8
max_wal_senders setting:              10
max_prepared_xacts setting:           0
max_locks_per_xact setting:           64
track_commit_timestamp setting:       off
Maximum data alignment:               8
Database block size:                  32768
Blocks per segment of large relation: 32768
WAL block size:                       8192
Bytes per WAL segment:                16777216
Maximum length of identifiers:        64
Maximum columns in an index:          32
Maximum size of a TOAST chunk:        8140
Size of a large-object chunk:         8192
Date/time type storage:               64-bit integers
Float8 argument passing:              by value
Data page checksum version:           1
Mock authentication nonce:            2fe4b4d8a89fa2be457455b19c8c831772616a1b419896ff0664226da2ed2997

#备库确认在恢复模式
postgres=# SELECT * FROM pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 t
 
 # 试试读,只读查询正常
 postgres=# SELECT count(*) FROM pg_class;
 count 
-------
   481

# 试写(应该被拒),写操作被正确拒绝
postgres=# CREATE TABLE test(n int);
ERROR:  cannot execute CREATE TABLE in a read-only transaction

postgres=# \x auto
Expanded display is used automatically.
postgres=# SELECT * FROM pg_stat_wal_receiver;
-[ RECORD 1 ]---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
pid                   | 2986	#walreceiver 进程 PID
status                | streaming	#正在流式接收 WAL
receive_start_lsn     | 0/28000000	#本次连接开始接收的 LSN(28 段起始)
receive_start_tli     | 1	#开始时 timeline = 1
written_lsn           | 0/28000168	#已写到 pg_wal/ 的最新位置
flushed_lsn           | 0/28000000	#已 fsync 到磁盘的位置
received_tli          | 1	#当前 timeline
last_msg_send_time    | 2026-10-17 11:56:05.065391+08	#主库最后发消息的时间
last_msg_receipt_time | 2026-10-17 11:56:05.062545+08	#备库收到的时间
latest_end_lsn        | 0/28000168	#主库告知的终点 LSN
latest_end_time       | 2026-10-17 10:04:32.140417+08	#备库达到一致状态(consistent recovery state)的时间
slot_name             | standby28	#用的是主库上创建的复制槽
sender_host           | 10.0.0.18	#主库 IP
sender_port           | 5432	#主库端口
conninfo              | user=replicator password=******** channel_binding=prefer dbname=replication host=10.0.0.18 port=5432 fallback_application_name=walreceiver sslmode=prefer sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=disable krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable

#查看进程
[postgres@Standby ~]$ps auxf |grep postgres
root        1672  0.0  0.3 143932  6676 pts/0    S    Oct16   0:00  |           \_ su - postgres
postgres    1673  0.0  0.2  16620  5392 pts/0    S    Oct16   0:00  |               \_ -bash
postgres    2953  0.0  0.4  60552  8108 pts/0    S+   09:43   0:00  |                   \_ psql -h 127.0.0.1 -p 5432 -U postgres
root        2890  0.0  0.3 143932  6656 pts/1    S    09:31   0:00              \_ su - postgres
postgres    2891  0.0  0.2  16752  5672 pts/1    S    09:31   0:00                  \_ -bash
postgres    3057  0.0  0.1  47880  3740 pts/1    R+   12:12   0:00                      \_ ps auxf
postgres    3058  0.0  0.0  12220  1092 pts/1    S+   12:12   0:00                      \_ grep --color=auto postgres
postgres    2981  0.0  1.2 688004 25492 ?        Ss   10:04   0:00 /apps/pgsql/bin/postgres -D /apps/pgsql/data
postgres    2982  0.0  0.2 144120  5328 ?        Ss   10:04   0:00  \_ postgres: logger 
postgres    2983  0.0  0.3 688168  5968 ?        Ss   10:04   0:00  \_ postgres: checkpointer 
postgres    2984  0.0  0.3 688168  7312 ?        Ss   10:04   0:00  \_ postgres: background writer 
postgres    2985  0.0  0.4 690004  8312 ?        Ss   10:04   0:00  \_ postgres: startup recovering 000000010000000000000028
postgres    2986  0.0  0.5 691076 10696 ?        Ss   10:04   0:00  \_ postgres: walreceiver 
postgres    3036  0.0  0.9 690860 18568 ?        Ss   11:41   0:00  \_ postgres: postgres postgres 127.0.0.1(61760) idle

postgres=# SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
 pg_last_wal_receive_lsn | pg_last_wal_replay_lsn 
-------------------------+------------------------
 0/28000000              | 0/28000168

#主库 pg_current_wal_insert_lsn() = 0/28000168
#备库 pg_last_wal_replay_lsn()   = 0/28000168

postgres=# SELECT pg_wal_lsn_diff(pg_last_wal_replay_lsn(), pg_last_wal_receive_lsn());
 pg_wal_lsn_diff 
-----------------
             360
#备库回放位置比接收统计超前 360 字节,说明:
WAL 数据早就写到 pg_wal/ 里了(walreceiver 写的)
startup 进程已经回放了一部分
receive_lsn 那个统计值还停在 0/28000000,实际数据已经到 0/28000168
360 字节 = 不到一个 WAL record 的大小,就是统计视图刷新滞后的正常误差。

postgres=# SELECT datname FROM pg_database;
  datname  
-----------
 postgres
 testdb	     #testdb 已经同步到备库,主库建的,备库自动出现
 template1
 template0

#主备完全一致,复制链路,有写入就推,推完就追平,零延迟
postgres=# SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
 pg_last_wal_receive_lsn | pg_last_wal_replay_lsn 
-------------------------+------------------------
 0/28109130              | 0/28109130

postgres=# SELECT pg_wal_lsn_diff(pg_last_wal_replay_lsn(), pg_last_wal_receive_lsn());
 pg_wal_lsn_diff 
-----------------
               0
#0 字节差值,完美追平


#备库:已回放到哪
postgres=# SELECT pg_last_wal_replay_lsn(),pg_last_wal_receive_lsn(),pg_is_in_recovery();
 pg_last_wal_replay_lsn | pg_last_wal_receive_lsn | pg_is_in_recovery 
------------------------+-------------------------+-------------------
 0/28109130             | 0/28109130              | t

#pg_last_wal_replay_lsn():已回放的最新 LSN
#pg_last_wal_receive_lsn():已接收的最新 LSN
#pg_is_in_recovery():是否还在恢复模式(t=备库)
指标 值 判断
差值 360 bytes 极小,正常
备库状态 in archive recovery -
复制状态 streaming -
实际 LSN 0/28000168 = 主库 追平
指标 值 状态
主备 LSN 0/28109130 一致 ✅
receive vs replay 差值 0 ✅
复制状态 streaming ✅
复制槽 standby28 active ✅
备库模式 in archive recovery ✅
DDL 同步 testdb 已同步 ✅
DML 同步 写入正常推送 ✅
备库只读 写操作被拒 ✅
心跳 < 3ms ✅

主从复制从搭建到全链路验证,全部完工,零问题

四、切换主从

4.1将从库切换为主库

bash 复制代码
[postgres@Standby ~]$pg_ctl promote
[postgres@Standby ~]$pg_controldata
pg_control version number:            1700
Catalog version number:               202406281
Database system identifier:           7675300641084329406
Database cluster state:               in production
pg_control last modified:             Sat 17 Oct 2026 03:14:13 PM CST
Latest checkpoint location:           0/281091C0
Latest checkpoint's REDO location:    0/28109168
Latest checkpoint's REDO WAL file:    000000020000000000000028
Latest checkpoint's TimeLineID:       2
Latest checkpoint's PrevTimeLineID:   2
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID:          0:824
Latest checkpoint's NextOID:          40961
Latest checkpoint's NextMultiXactId:  1
Latest checkpoint's NextMultiOffset:  0
Latest checkpoint's oldestXID:        731
Latest checkpoint's oldestXID's DB:   1
Latest checkpoint's oldestActiveXID:  824
Latest checkpoint's oldestMultiXid:   1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint:            Sat 17 Oct 2026 03:14:13 PM CST
Fake LSN counter for unlogged rels:   0/3E8
Minimum recovery ending location:     0/0
Min recovery ending loc's timeline:   0
Backup start location:                0/0
Backup end location:                  0/0
End-of-backup record required:        no
wal_level setting:                    replica
wal_log_hints setting:                on
max_connections setting:              30
max_worker_processes setting:         8
max_wal_senders setting:              10
max_prepared_xacts setting:           0
max_locks_per_xact setting:           64
track_commit_timestamp setting:       off
Maximum data alignment:               8
Database block size:                  32768
Blocks per segment of large relation: 32768
WAL block size:                       8192
Bytes per WAL segment:                16777216
Maximum length of identifiers:        64
Maximum columns in an index:          32
Maximum size of a TOAST chunk:        8140
Size of a large-object chunk:         8192
Date/time type storage:               64-bit integers
Float8 argument passing:              by value
Data page checksum version:           1
Mock authentication nonce:            2fe4b4d8a89fa2be457455b19c8c831772616a1b419896ff0664226da2ed2997
字段 promote 前 promote 后 含义
Database cluster state in archive recovery in production ✅ 变成主库了
TimeLineID 1 2 ✅ 切换到新 timeline
PrevTimeLineID 1 2 ✅
REDO WAL file 00000001... 00000002... ✅ 新 timeline 的 WAL
Minimum recovery ending location 0/28000168 0/0 ✅ 不再需要恢复终点
NextOID 32769 40961 ✅ 重新分配 OID 段
Latest checkpoint 之前时间 03:14:13 PM ✅ 刚做的 checkpoint

Timeline 是 PG 复制里非常重要的概念:

html 复制代码
Timeline 1: 原主库一路写 WAL
    ↓
  主库挂了 / promote 备库
    ↓
Timeline 2: 新主库(原备库)从分叉点开始写新 WAL


WAL 文件名前缀 00000002 就是 timeline 2 的段文件。这防止了 timeline 1 和 timeline 2 的 WAL 互相覆盖。

Minimum recovery ending location: 0/0
之前是 0/28000168(告诉 PG "至少回放到这个位置才能 open"),现在变成 0/0 表示不再有恢复终点限制,数据库完全 open 了。

NextOID: 40961
PG 的 OID 是按段分配的(每 8192 个一段)。promote 后跳到下一个段起始,避免跟 timeline 1 时代的 OID 冲突。

Latest checkpoint location: 0/281091C0
promote 时会做一个 end-of-recovery checkpoint,这是备库变成主库后的第一个 checkpoint,也是 timeline 2 的第一个 WAL 记录。

4.2验证新主库

bash 复制代码
# 在 Standby 上
postgres=# SELECT pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 f	#不在恢复模式 = 主库(可读写)

#pg_ctl promote 之后,Standby 已经正式切换成主库,不再从任何人那里接收 WAL

postgres=# CREATE TABLE post_failover_test(id int);	
#没报错 = 写操作被接受,说明新主库可以正常执行 DDL

postgres=# INSERT INTO post_failover_test VALUES(1);	
#DML 也能正常写入,数据会写进新 timeline(timeline 2)的 WAL

#Standby 已经彻底变成主库,可读可写,failover 成功


[postgres@Standby ~]$pg_ctl restart -mf -D /apps/pgsql/data/
#恢复正常模式()
postgres=# SELECT pg_wal_replay_resume();

postgres=# SELECT pg_current_wal_insert_lsn();
 pg_current_wal_insert_lsn 
---------------------------
 0/290001E0

#promote 后新主库已经产生了新的 WAL 段(29 段),timeline 2 在正常工作

postgres=# SELECT pg_current_wal_insert_lsn(),pg_current_wal_lsn(),pg_current_wal_flush_lsn();
 pg_current_wal_insert_lsn | pg_current_wal_lsn | pg_current_wal_flush_lsn 
---------------------------+--------------------+--------------------------
 0/2C013EE0                | 0/2C013EE0         | 0/2C013EE0

#28(主库)三个 LSN 完全一致,全部停在 0/2C013EE0

postgres=# SELECT replay_lsn FROM pg_stat_replication;
 replay_lsn 
------------
 0/2C013EE0

#主库写入点 = 备库接收点 = 备库回放点 = 完全一致,零延迟

postgres=# SELECT client_addr AS standby_ip, pg_current_wal_insert_lsn() AS primary_lsn, replay_lsn AS standby_replay_lsn, pg_wal_lsn_diff(pg_current_wal_insert_lsn(), replay_lsn) AS lag_bytes
postgres-# FROM pg_stat_replication;
 standby_ip | primary_lsn | standby_replay_lsn | lag_bytes 
------------+-------------+--------------------+-----------
 10.0.0.18  | 0/2C013EE0  | 0/2C013EE0         |         0

#standby_ip:备库地址
#primary_lsn:主库当前写入点
#standby_replay_lsn:备库已回放点
#lag_bytes:零字节延迟

#28上
postgres=# SELECT datname FROM pg_database;
  datname  
-----------
 postgres
 testdb
 template1
 template0

postgres=# CREATE TABLE IF NOT EXISTS t1(id serial, ts timestamp default now()); INSERT INTO t1 DEFAULT VALUES; SELECT * FROM t1;
CREATE TABLE
INSERT 0 1
 id |             ts             
----+----------------------------
  1 | 2026-10-17 18:56:27.663438

4.3原主库状态

bash 复制代码
# 去原主库上确认
#如果原主库还在跑着,它不知道自己被取代了。如果应用还往它写数据,就会出现脑裂。

postgres=# SELECT pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 f

# 返回 f,说明它还在当主库 → 脑裂风险!
#现在的风险:如果应用还在往 18 写数据,就出现脑裂了------两个主库各自写各自的,数据永远合不回来

注意:生产环境 failover 的标准操作是:先关原主库,再 promote 备库。

4.4原主库切换为从库

用 pg_rewind 把 10.0.0.18 重做成 10.0.0.28 的备库:

bash 复制代码
# 10.0.0.18 上
[root@Master ~]#su - postgres 
[postgres@Master ~]$pg_ctl stop -mf -D /apps/pgsql/data/

#确认 28 的 pg_hba.conf 已加规则
# 10.0.0.28 的 pg_hba.conf
[postgres@Standby ~]$vim /apps/pgsql/data/pg_hba.conf
host    replication     replicator      10.0.0.18/32            trust

[postgres@Standby ~]$vim /apps/pgsql/data/postgresql.conf
#primary_conninfo = 'host=10.0.0.18 port=5432 user=replicator password=Repl@Strong2026.'
#primary_slot_name = 'standby28'

[postgres@Standby ~]$pg_ctl restart -mf -D /apps/pgsql/data/

#从 18 上跑 pg_rewind
# 18 上(确认已停库)
[postgres@Master ~]$pg_rewind -D /apps/pgsql/data --source-server='host=10.0.0.28 port=5432 user=postgres'

# 在 10.0.0.18 上,创建 standby.signal
[postgres@Master ~]$touch /apps/pgsql/data/standby.signal

#standby.signal的作用:PG 启动时检测到 standby.signal 存在 → 进入 recovery 模式(备库),从 primary_conninfo 指向的主库拉 WAL,没有这个文件 → PG 启动后直接进入 in production(主库),那就又变成双主了

#配 primary_conninfo 指向新主库(28)
[postgres@Master ~]$vim /apps/pgsql/data/postgresql.conf
primary_conninfo = 'host=10.0.0.28 port=5432 user=replicator password=Repl@Strong2026. application_name=standby18'
primary_slot_name = 'standby18'

[postgres@Master ~]$sed -i 's/host=10.0.0.18/host=10.0.0.28/g' /apps/pgsql/data/postgresql.auto.conf
[postgres@Master ~]$grep "host=" /apps/pgsql/data/postgresql.auto.conf
primary_conninfo = 'user=replicator password=''Repl@Strong2026.'' channel_binding=prefer host=10.0.0.28 port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=disable krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'

[postgres@Master ~]$pg_ctl restart -mf -D /apps/pgsql/data/
postgres=# SHOW primary_conninfo;
                                                                                                                      
                                primary_conninfo                                                                      
                                                                                
----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 user=replicator password='Repl@Strong2026.' channel_binding=prefer host=10.0.0.28 port=5432 sslmode=prefer sslnegotia
tion=postgres sslcompression=0 sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=disable krbsrvna
me=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable

[postgres@Master ~]$tail -5 /apps/pgsql/log/postgresql-2026-10-17.log
2026-10-17 18:03:44.366 CST [4730] LOG:  redo starts at 0/290000D8
2026-10-17 18:03:44.366 CST [4730] LOG:  consistent recovery state reached at 0/290001E0
2026-10-17 18:03:44.366 CST [4730] LOG:  invalid record length at 0/290001E0: expected at least 24, got 0
2026-10-17 18:03:44.366 CST [4726] LOG:  database system is ready to accept read-only connections
2026-10-17 18:03:44.380 CST [4731] LOG:  started streaming WAL from primary at 0/29000000 on timeline 2
#18 正在从 28 流式接收 WAL,复制链路完全建立

#在 28(新主库)上建复制槽
# 10.0.0.28 上执行
postgres=# SELECT pg_create_physical_replication_slot('standby18');
 pg_create_physical_replication_slot 
-------------------------------------
 (standby18,)

postgres=# SELECT client_addr, application_name, state, replay_lsn FROM pg_stat_replication;
 client_addr | application_name |   state   | replay_lsn 
-------------+------------------+-----------+------------
 10.0.0.18   | walreceiver      | streaming | 0/2C0001E0

#28 上确认 18 在流式同步,state=streaming,LSN 在推进(已经从 0/290001E0 涨到 0/2C0001E0)

postgres=# CREATE TABLE test_sync(id int); INSERT INTO test_sync VALUES(1);
CREATE TABLE
INSERT 0 1



#验证
# 18 上
postgres=# SELECT pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 t	#10.0.0.18 已成功变回备库,t = 在恢复模式 = 备库

postgres=# SELECT * FROM test_sync;
 id 
----
  1

postgres=# SELECT pg_wal_lsn_diff(pg_last_wal_replay_lsn(), pg_last_wal_receive_lsn());
 pg_wal_lsn_diff 
-----------------
               0
##0 字节差值,完美追平,lsn_diff=0,零延迟,接收和回放完全同步

postgres=# SELECT pg_last_wal_replay_lsn(),pg_last_wal_receive_lsn(),pg_is_in_recovery();
 pg_last_wal_replay_lsn | pg_last_wal_receive_lsn | pg_is_in_recovery 
------------------------+-------------------------+-------------------
 0/2C013EE0             | 0/2C013EE0              | t

#receive_lsn:已接收到的最新 WAL
#replay_lsn:已回放到的最新 WAL
#pg_is_in_recovery():t,18 确实在备库模式


[postgres@Master ~]$pg_controldata
pg_control version number:            1700
Catalog version number:               202406281
Database system identifier:           7675300641084329406
Database cluster state:               in archive recovery	#备库模式
pg_control last modified:             Sat 17 Oct 2026 05:21:30 PM CST
Latest checkpoint location:           0/29000130
Latest checkpoint's REDO location:    0/290000D8
Latest checkpoint's REDO WAL file:    000000020000000000000029	#timeline 2 的 WAL
Latest checkpoint's TimeLineID:       2	 #跟 28 同一个 timeline
Latest checkpoint's PrevTimeLineID:   2
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID:          0:826
Latest checkpoint's NextOID:          40964
Latest checkpoint's NextMultiXactId:  1
Latest checkpoint's NextMultiOffset:  0
Latest checkpoint's oldestXID:        731
Latest checkpoint's oldestXID's DB:   1
Latest checkpoint's oldestActiveXID:  826
Latest checkpoint's oldestMultiXid:   1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint:            Sat 17 Oct 2026 04:09:28 PM CST
Fake LSN counter for unlogged rels:   0/3E8
Minimum recovery ending location:     0/290001E0	#有明确的恢复终点
Min recovery ending loc's timeline:   2
Backup start location:                0/0
Backup end location:                  0/0
End-of-backup record required:        no
wal_level setting:                    replica
wal_log_hints setting:                on
max_connections setting:              30
max_worker_processes setting:         8
max_wal_senders setting:              10
max_prepared_xacts setting:           0
max_locks_per_xact setting:           64
track_commit_timestamp setting:       off
Maximum data alignment:               8
Database block size:                  32768
Blocks per segment of large relation: 32768
WAL block size:                       8192
Bytes per WAL segment:                16777216
Maximum length of identifiers:        64
Maximum columns in an index:          32
Maximum size of a TOAST chunk:        8140
Size of a large-object chunk:         8192
Date/time type storage:               64-bit integers
Float8 argument passing:              by value
Data page checksum version:           1
Mock authentication nonce:            2fe4b4d8a89fa2be457455b19c8c831772616a1b419896ff0664226da2ed2997

#18上
postgres=# SELECT datname FROM pg_database;
  datname  
-----------
 postgres
 testdb
 template1
 template0

postgres=# SELECT * FROM t1;
 id |             ts             
----+----------------------------
  1 | 2026-10-17 18:56:27.663438
相关推荐
IvorySQL2 小时前
PostgreSQL 日报| PG19 主备切换挂起修复(10 月 17 日)
数据库·postgresql
ly76893 小时前
PostgreSQL 流复制与高可用切换:主从延迟、同步提交与 Spring Boot 读写分离的边界
数据库·spring boot·postgresql·读写分离·流复制·高可用切换
骑着蜗牛撵大象3276 小时前
ToolJet + PostgreSQL:后台表格、查询和流程,一次搭成可远程访问的业务系统
数据库·低代码·docker·postgresql·内部工具·tooljet
wno70414 小时前
postgresSQL压缩版安装与基本命令
数据库·postgresql
瀚高PG实验室17 小时前
瀚高数据库常用系统表查询入门指南(附常用SQL)
数据库·sql·postgresql·瀚高数据库
像风一样自由202020 小时前
56.实战PostgreSQL-pgvector-FastAPI构建轻量级RAG
数据库·人工智能·postgresql·大模型·fastapi·智能体
ss2731 天前
AI时代首先数据库-PostgreSQL实战笔记 | 11 PostgreSQL安装部署常见问题排查与解决
数据库·笔记·postgresql
ss2731 天前
AI时代首先数据库-PostgreSQL实战笔记 | 12 PostgreSQL数据目录与重要文件结构
数据库·笔记·postgresql
一木 之林1 天前
LangChain 第8章 记忆系统:短期/长期记忆与 PostgreSQL 持久化
数据库·postgresql·langchain