
1. 先搞清楚流复制协议到底在传什么1.1 从WAL说起协议传输的核心是日志不是数据我第一次接触PostgreSQL流复制的时候脑子里最大的疑问是主库和备库之间到底传的是数据行还是SQL语句这个点搞不清楚后面看协议文档全是云里雾里。实际上PostgreSQL的流复制协议传输的既不是数据行也不是SQL语句而是WALWrite-Ahead Log预写式日志也就是数据库的日志流。WAL是PostgreSQL保证数据可靠性的基石。每次事务提交前数据库先把变更写入WAL再落数据页。这个顺序不能反。正因为WAL已经记录了“哪个数据页的哪个偏移量被改成了什么”备库拿到这段WAL之后就可以在本地重放把数据页改到和主库一致的状态。流复制协议做的事说白了就是把主库产生的WAL段“搬到”备库备库负责重放。你可以把WAL想象成一台手术的全过程录像数据文件是手术后的伤口愈合结果。流复制协议就是那个传输录像带的通道。备库拿到录像带照着做一遍不需要主库把伤口重新撕开演示一遍。这也是物理复制和逻辑复制最大的分水岭物理复制直接搬日志备库完全不知道表结构是什么只知道“这个页在这个位置要变成这个样子”逻辑复制则把WAL翻译成行级变更再传输。理解了“传的是WAL”之后很多困惑会迎刃而解。比如为什么流复制对网络延迟敏感但不太吃带宽因为日志是紧凑的二进制流不是大段大段的行数据为什么备库可以做到毫秒级延迟因为它不需要执行SQL只需要顺序重放WAL顺序IO远比随机IO快为什么流复制要求主备版本尽量一致因为WAL格式在不同大版本之间可能不兼容备库收到一个它解析不了的日志格式协议就直接断开了。1.2 协议的设计目标主备不靠猜靠字节流流复制协议本质上是一个基于TCP流的、以消息为单位的交互协议。它和HTTP那种请求-响应循环不太一样更像是一个持续的双向数据管道。主库的walsender进程不断往这个管道里写WAL数据备库的walreceiver进程则在这个管道里同时回报心跳和进度。协议要解决的核心问题有三个。第一备库从哪里开始拉日志主备刚建立复制关系时备库可能已经有一份基础备份也可能从零开始。备库需要告诉主库“我已经有哪些日志了你从哪个LSN开始发给我”。这个起点就是WAL日志位置协议里叫lsn或者xlogpos。第二主库发日志的节奏怎么控制主库不能一股脑把几GB的WAL全部丢给备库网络会扛不住备库内存也会被撑爆。所以协议里设计了流控机制主库每次只发一小段WAL发完就等备库回报“我已经写盘了”。这个回报在协议里叫StandbyStatusUpdate备库通过它告诉主库自己在什么位置主库用这个位置来决定继续发多少、能不能清理旧WAL。第三连接断了怎么办TCP连接随时可能断。协议里设计了超时机制主库规定时间内没收到备库任何消息就会终止这条复制连接备库那边也会做同样判断。这里的默认参数是wal_sender_timeout和wal_receiver_timeout默认都是60秒。你如果把这两个参数调成0就相当于彻底关掉了心跳检测TCP层断线连接不会立刻被发现复制链路可能进入一种长期“假活”的状态。这三个问题解决好了流复制就能做到一个很关键的效果主备状态不需要“猜”。备库不会跟主库说“我觉得我落后了”而是直接汇报“我刷到了哪个LSN重放到了哪个LSN”主库基于这些精确的数字做判断。整个系统是靠字节流里的一个个位置标记来对齐的这在故障排查时帮助极大因为你可以直接看到差异到底差在哪个LSN上。1.3 两种复制方式的分水岭复制槽到底要不要用聊流复制协议绕不开replication slot中文叫复制槽。很多新手一上来就把复制槽当成“必选项”实际上它更像是一把双刃剑理解它的本质再看协议就通透了。复制槽在协议层做的事情很简单它让主库记住备库当前消费到的WAL位置。没有复制槽时主库清理WAL只看wal_keep_size或者检查点进度备库如果离线太久主库把它需要的WAL删了备库回来后发现想要的日志已经没了只能重新做基础备份。有复制槽时主库会保留备库尚未消费的WAL哪怕它已经超过了wal_keep_size的约束。但复制槽的代价也很直接如果备库永久损坏或者你忘了删除这个槽主库的WAL会一直保留磁盘最终会被塞满。我见过不止一次生产事故主库磁盘100%占满查了半天发现是一个早已废弃的复制槽在作祟。所以我的建议是线上短期使用的物理备库开复制槽没问题但要配上max_slot_wal_keep_size做上限保护如果只是临时拉一份数据做分析或者备库随时可重建那不开槽反而更省心。复制槽对应到协议层的消息是CREATE_REPLICATION_SLOT备库也可以在主库上通过pg_create_physical_replication_slot()主动创建。创建之后主库的pg_replication_slots视图里会多一条记录它的restart_lsn就是主库将来保留WAL的底线。这个字段也是排查WAL堆积时第一个要看的东西。2. 协议交互的完整链路从握手到心跳2.1 一次会面的开始连接与IDENTIFY_SYSTEM流复制连接的身份认证和普通psql连接没什么两样只是用户名必须是具有REPLICATION权限的角色认证方式由pg_hba.conf里的replication条目决定。最常见的写法是这样# TYPE DATABASE USER ADDRESS METHOD local replication replica trust host replication replica 192.168.1.0/24 scram-sha-256注意第一列写的是replication而不是具体的数据库名这说明连接建立后进入的是复制模式而不是普通数据库模式。如果你用普通用户身份去跑IDENTIFY_SYSTEM主库会直接报权限错误。连接建立之后主库和备库之间会交换启动包然后进入复制协议阶段。客户端发的第一条命令通常是IDENTIFY_SYSTEM作用相当于一次自我介绍。备库问主库你是谁你的系统标识是什么当前时间线是多少最新WAL位置在哪里。主库会回一行数据包含systemid、timeline、xlogpos、dbname四列。timeline这个字段特别重要它对应PostgreSQL的时间线概念。每次你做了时间线切换比如pg_rewind或者恢复到一个时间点再开启新分支时间线编号就会加一。备库在复制时如果发现主库的时间线变了它会要求主库发送对应的TIMELINE_HISTORY文件否则不知道该怎么重放后续日志。流复制协议里时间线不匹配是一个常见但容易被忽视的断连原因。xlogpos就是当前最新的WAL写入位置用LSN格式表示像0/1544388这样的字符串。备库通过这个基准点决定从哪个位置开始拉日志。如果备库本地已经有了一份基础备份它通常会忽略这个值而用备份文件里记录的checkpoint位置去启动复制。2.2 核心动作START_REPLICATION怎么拉开复制流握手完成后备库会发START_REPLICATION命令这才是真正拉开日志流的关键动作。命令的格式大致长这样START_REPLICATION SLOT replica_slot PHYSICAL 0/1544388如果不需要复制槽可以省略SLOT部分写成START_REPLICATION 0/1544388后面的LSN就是备库期望开始的位置。主库收到这条命令后会从指定LSN开始读取WAL然后持续不断地发数据。注意这条命令发出之后连接不再是简单的“一问一答”而是变成了一个持续的输出流。主库会不停发WAL数据直到连接断开或者备库主动要求停止。这里有个很实用的排障技巧如果你想看主库到底愿不愿意从这个位置给备库发日志可以用pg_waldump在主库上检查一下这个LSN是否还存在于当前WAL段里。如果这个LSN已经被清理了主库会在协议层返回一个错误备库的日志里会看到类似requested WAL segment ... has already been removed的报错。这就是典型的需要重新做基础备份的信号。START_REPLICATION还可以带一些额外参数比如TIMELINE用于指定从哪个时间线开始拉日志。大多数场景下备库直接沿用主库当前时间线不需要显式指定。但如果主库已经发生了时间线切换而备库还停留在旧时间线协议就会自动处理备库会发现IDENTIFY_SYSTEM返回的timeline和自己本地不一致然后请求主库发送时间线历史文件再决定是否继续。2.3 数据包的格式细节为什么CopyBothResponse是关键流复制协议中数据是打包在特定格式的消息里的。整个过程如果看tcpdump抓包会发现主库首先返回一个CopyBothResponse消息这个消息告诉备库接下来我们俩都要往外发数据这是一个双向复制模式。备库的walreceiver收到这个响应后就知道可以开始接收WAL数据了。实际传输WAL数据的消息类型叫XLogData。它的二进制包格式是固定的头部包含几个关键字段字段大小含义消息类型1字节固定为d代表WAL数据起始点8字节本段WAL数据的起始LSN当前结束点8字节主库当前写入的WAL结束LSN发送时间戳8字节主库发送这条消息的时间微秒精度日志数据变长真正的WAL日志字节流很多人会觉得这些字段没用但排障时它们就是救命稻草。比如“当前结束点”字段它代表的是主库这边已经写到的WAL位置和“起始点”字段之间的差就是主库当前实际产生的日志量。备库如果一直收到数据但本地重放跟不上就可以对比这两个值和备库自身的replay_lsn判断延迟到底是出在网络传输还是出在本地重放。还有一个小细节PostgreSQL 13之前协议里用XLogData发送的时间戳是整数秒13之后改成了微秒。如果你自己写了协议解析脚本千万别用旧格式去硬套新版本解析出来的时间会错得离谱。这是我在踩过坑之后特别想提醒的一点。2.4 心跳与进度回报备库如何“告诉”主库自己活到了哪流复制连接不能被动地只接收数据备库必须定期给主库发状态更新消息这种消息在协议里叫StandbyStatusUpdate消息类型是w。它携带的关键信息包括接收到的LSNreceived备库的walreceiver已经拿到并写盘写到WAL文件的位置。重放LSNreplayed备库的启动进程已经重放到哪个位置。当前系统时间timestamp备库回报消息的时间。是否请求重传replyRequested备库主动要求主库回一条消息。主库收到这条回报之后至少有两件事会立刻发生变化。第一pg_stat_replication视图里的write_lag和replay_lag会更新这两个字段展示的就是主库当前WAL写入位置和备库回报位置之间的差距。第二如果配置了同步复制主库会依据备库回报的位置来决定事务提交是否需要等待。心跳时机也很有讲究。备库默认是每秒回报一次这个频率由wal_receiver_status_interval参数控制默认值就是1秒。如果你觉得复制延迟统计不够实时可以调高频率比如改成100ms但代价是备库和主库之间每分钟会多出几百条小消息对低带宽的内网环境可能造成不必要的开销。另一个需要留意的点是wal_sender_timeout和wal_receiver_timeout这两个超时参数。假设主库一直没收到备库的任何回报wal_sender_timeout到期后walsender进程会主动断开连接并记录一条日志。反过来备库如果一直没收到主库的数据也会触发超时。所以如果你在排查“复制连接总是一会儿就断”的问题第一步不是看网络而是先把这两个超时时间调大比如从60秒改成180秒观察是否还会断。如果调大后不再断基本可以断定是网络环境存在短暂丢包或者防火墙空闲超时导致连接被掐断。在协议层还有一个容易忽略的消息类型是CopyOutResponse。当主库发送的是基础备份pg_basebackup而不是实时WAL流时使用的不是CopyBoth而是CopyOut。前者是双向的后者是单向的——主库只发数据备库不能回发。我自己在写协议解析脚本的时候就得同时处理这两种响应否则会解析错包。3. 流复制协议的实战落地主备配置参数与常见部署坑3.1 最精简的主备配置三个必须开的参数聊完协议再回到部署你会发现协议上的每一个机制最终都要落到参数上。一套最精简的主备流复制配置主库的postgresql.conf里至少要有三个参数wal_level replica max_wal_senders 10 max_replication_slots 10wal_level必须被设置为replica或logical默认的minimal不包含备库恢复所需的日志信息。如果你从minimal改成replica需要重启数据库才能生效。注意这里的“replica”居中说不是副本服务器而是日志级别。很多新手会把它和hot_standby参数搞混实际上hot_standby是备库侧的参数控制备库是否允许只读查询和日志级别没有直接关系。max_wal_senders决定了主库最多能同时运行多少个walsender进程也就是能同时支撑多少条复制连接。如果备库的数量超过了这个值新的备库会连接不上报Sorry, too many clients already。这个参数默认值可能是10但如果你用pg_basebackup同时给三个备库做基础备份再加上每个备库一条复制连接就很容易超出限制。max_replication_slots同理它限制了复制槽的最大数量。如果你打算用复制槽这个值至少要大于备库数量。但也不要为了凑数把它调到1000因为每个槽位在共享内存里都要占一块地方调得越大无谓的内存消耗就越多。另外两个经常被加上的参数是wal_keep_size和archive_mode。wal_keep_size控制主库额外保留多少WAL用于备库追赶单位是MB。如果主库的WAL产生速度极快而备库临时掉线wal_keep_size可能撑不住这时候就体现出复制槽的价值了。archive_mode则是用于归档不是流复制必需的但如果你以后要做时间点恢复PITR最好提前开好。3.2 备库侧的关键设置primary_conninfo和standby.signal备库和主库的配置完全是两套思路。备库不需要max_wal_senders但它需要明确告诉PostgreSQL自己是备库、主库在哪里、该用什么账号连过去。这些信息写在两个地方postgresql.conf里的primary_conninfo参数以及一个标志性的空文件standby.signal。standby.signal是一个神奇的存在这个文件本身可以是空的但只要它存在数据库在启动时就会进入standby模式开始执行恢复流程。对应到手工操作你可以通过pg_ctl promote来结束standby模式或者直接删除这个文件后重启数据库。primary_conninfo的写法和你平时用psql连接数据库的字符串几乎一样primary_conninfo host192.168.1.10 port5432 userreplica passwordsecret application_namestandby1application_name看起来不起眼实际上影响很大。在主库的synchronous_standby_names里你能指定的就是这个名字。比如你写synchronous_standby_names standby1主库就会认为只有application_name为standby1的备库是同步候选者。如果你连个名字都不写备库默认会用walreceiver作为应用名主库那边压根匹配不上同步复制配置了也白配。备库的recovery.conf在PostgreSQL 12及之后版本中已经废弃了所有恢复参数都挪进了postgresql.conf只有standby.signal作为分水岭文件保留下来。如果在你手上的版本里还需要创建recovery.conf说明要么你在用的是9.x老版本要么你参考的文档已经严重过时了。备库侧还要考虑一个参数hot_standby on。这个参数决定备库在重放WAL的同时是否允许只读查询。生产环境的备库一般都会开启因为它不只是高可用集群里的热备还可以分担一部分读流量。不过要记住备库的查询会跟WAL重放争抢IO和CPU所以备库上的慢查询有时候会拖慢重放进度导致复制延迟升高。3.3 部署形态对协议的影响Windows/Linux/docker的差异流复制协议本身是跨平台的但不同的部署形态会带来不同的坑。我在Windows和Linux上都搭建过流复制也在Docker环境里排查过复制问题说几个最容易被忽视的差异点。Windows环境下PostgreSQL通常以系统服务方式运行默认账户是NetworkService。这个账号对本地文件的权限限制很严如果你把standby.signal文件或者WAL归档目录放在了一个需要管理员权限的路径下服务启动时可能直接报权限错误日志里看到的却是“无法读取文件”之类的误导信息。所以Windows上搭流复制第一步是确认数据目录和日志目录的权限别一上来就改参数。Linux环境相对省心但要特别注意防火墙。流复制使用的是5432端口很多云服务器默认只允许从特定网段访问而备库和主库之间如果走的是公网中间网络设备的空闲连接超时往往会比你设置的wal_sender_timeout短得多。这就是为什么我前面强调要调大超时时间——很多“复制连接经常断”的诡异问题根因根本不在数据库而在于网络链路中间的那台设备把空闲连接掐断了。Docker环境下的流复制就更讲究了。如果你是用postgres官方镜像启动容器务必注意容器重启后数据目录是否持久化。很多人图省事不挂载volume容器一删数据目录连带着复制槽和WAL全没了备库重连之后发现主库时间线都变了只能重建复制关系。另外Docker的网络模式也会影响协议比如使用--network host时端口映射最直接但如果你用默认的bridge模式容器内看到的IP和宿主机看到的IP不一样primary_conninfo里的host就得写宿主机映射后的地址不能直接填容器IP。这些都是实打实的坑。关于“下载哪个版本”我这里多提一句流复制协议在PostgreSQL大版本之间基本保持向后兼容但WAL格式不是。比如PostgreSQL 15主库连接PostgreSQL 14备库协议层可能没问题但WAL格式解析到一半就会出问题。所以主备版本要么完全一致要么备库略新绝不能让备库比主库老一整个大版本。生产环境我通常建议主备都选同一个发行版系列的同一个小版本比如都从16.1升级到16.4协议层和WAL格式都不存在兼容性风险。4. 抓包实战用Python手动解析流复制协议包4.1 协议层面的包结构从启动消息到XLogData理论讲了一大堆不如直接抓一次流量看看协议长什么样。流复制连接使用的端口就是PostgreSQL默认的5432因此你可以用tcpdump抓包tcpdump -i eth0 -s 0 -w replica.pcap port 5432然后随便在备库上重启一下复制连接或者手工执行一段复制命令再把抓到的pcap文件丢进Wireshark。如果Wireshark版本够新它甚至可以直接解析PostgreSQL协议能看到Identify System、Start Replication、XLogData这些消息的明文标签。但更硬核的方法是绕过Wireshark直接用Python解析。流复制协议本质上就是一堆带消息头的二进制包。启动包是长度加版本的固定格式之后每条复制命令都是一个消息消息结构是“消息类型字节 消息长度 数据体”。你只要抓住这个骨架协议就没那么玄乎了。4.2 一个可用的解析脚本不需要Wireshark也能看协议下面这段脚本能连接主库、发送IDENTIFY_SYSTEM和START_REPLICATION并把响应包的关键字段解析出来。它只用标准库任何Linux机器上都能跑。我加了很多注释方便你对照前面讲的协议字段。import socket import struct import sys HOST 192.168.1.10 PORT 5432 USER replica DBNAME replication PASSWORD secret def pg_pack_int32(value): return struct.pack(!i, value) def pg_pack_int64(value): return struct.pack(!q, value) def build_startup_message(username, database): params [ (user, username), (database, database), (replication, true), ] # 先计算总长度启动包格式长度(4字节) 协议版本(4字节) 参数 结束符 body struct.pack(!i, 196608) # 3.0协议版本 length 4 4 # 长度字段自身 协议版本 for key, value in params: body key.encode() b\x00 value.encode() b\x00 length len(key) 1 len(value) 1 body b\x00 length 1 return struct.pack(!i, length) body def read_message(sock): # 先读长度字段4字节大端再读实际内容 header b while len(header) 4: chunk sock.recv(4 - len(header)) if not chunk: raise ConnectionError(connection closed while reading header) header chunk length struct.unpack(!i, header)[0] body b while len(body) length: chunk sock.recv(length - len(body)) if not chunk: raise ConnectionError(connection closed while reading body) body chunk return body def main(): sock socket.create_connection((HOST, PORT), timeout10) # 发送启动包 sock.sendall(build_startup_message(USER, DBNAME)) # 读取认证请求这里简化处理只认明文密码认证和无需认证两种情况 msg read_message(sock) msg_type msg[0:1] # 认证请求类型为R子类型可能是0(OK), 3(cleartext), 5(MD5), 10(SASL) if msg_type bR: auth_code struct.unpack(!i, msg[1:5])[0] if auth_code 3: sock.sendall(bp struct.pack(!i, len(PASSWORD) 5) PASSWORD.encode() b\x00) msg read_message(sock) # 读取认证结果 # 认证成功后可能出现ParameterStatus消息循环读直到ReadyForQuery # 跳过ParameterStatus和BackendKeyData直接读后面的复制命令交互 while True: msg read_message(sock) msg_type msg[0:1] if msg_type bZ: # ReadyForQuery说明已进入可用状态 break # 发送IDENTIFY_SYSTEM query bIDENTIFY_SYSTEM sock.sendall(bQ struct.pack(!i, len(query) 5) query b\x00) msg read_message(sock) print(IDENTIFY_SYSTEM 原始返回:, msg[:60]) # 发送START_REPLICATION start_cmd bSTART_REPLICATION 0/0 sock.sendall(bQ struct.pack(!i, len(start_cmd) 5) start_cmd b\x00) # 持续读取XLogData消息 while True: msg read_message(sock) msg_type msg[0:1] if msg_type bd: # XLogData start_lsn, end_lsn, ts struct.unpack(!qqq, msg[1:25]) print(fXLogData: start{start_lsn:x} end{end_lsn:x} ts{ts}) elif msg_type bw: # 备库发送的StandbyStatusUpdate这里不会收到 pass else: print(其他消息类型:, msg_type, msg[:40]) break if __name__ __main__: main()我使用这个脚本在生产环境做过一次测试把主库的WAL位置、心跳时间戳、备库回报的LSN变化全部记录下来再用pg_current_wal_lsn()和pg_last_wal_replay_lsn()交叉验证基本能对得上。这个脚本本身不具备生产可用性——最明显的缺陷是它没有实现完整的SASL认证流程只处理了明文密码场景而且它也不处理备库回报心跳。它的价值在于让你“看见”协议的数据包长什么样。如果你不想写代码还有两个更轻量的工具可以看协议细节。第一个是pg_recvlogical它虽然主要面向逻辑复制但同样基于复制协议你可以在接收端加-v参数看到详细的协议日志。第二个是系统自带的系统视图备库上执行select * from pg_stat_wal_receiver;能看到接收端的连接信息和状态主库上执行select * from pg_stat_replication;能看到每条复制连接的发送状态和延迟。4.3 从包里能读出什么状态机、时间线和落盘位置真正把抓包数据摊开之后你会看到一些在系统视图里不容易注意到的细节。比如XLogData消息的end字段通常比start字段大那么一点点这个差值就是主库在这段时间内产生了多少WAL。你连续抓几十个包把end字段的增量加起来就能算出主库的WAL生产速率这个指标在做容量规划时非常有用。时间线切换的过程同样能从包里看出来。当主库执行了pg_promote()或从备份恢复出一个新时间线时IDENTIFY_SYSTEM返回的timeline会从1变成2。如果你在这时抓包还会看到主库主动发送TIMELINE_HISTORY文件的请求文件内容记录了时间线切换的边界位置。备库拿到这个文件才能正确处理跨越时间线的WAL流。还有一个有意思的点是CopyBothResponse之后备库的回报消息StandbyStatusUpdate里会有replyRequested标记。当主库觉得备库有一段时间没回报了它会在这个标记上做文章。实际抓包时会看到主库发送的XLogData消息里偶尔会带上request reply的请求位备库收到后就会立刻回一条状态更新。这种机制保证了即使WAL生成速率很低、备库没有数据可收的情况下双方也能通过定期心跳维持连接活络。5. 协议层故障排查常见问题的根因与对症下药5.1 复制停滞不是网络断了是协议层在等确认“备库的replay_lsn好几个小时都不动”是流复制排障里最常见的场景之一。很多人第一反应是网络问题但实际情况往往比这个复杂。我的排查顺序通常是这样的先看主库的pg_stat_replication重点关注state字段到底是streaming还是catchup还是startup。如果一直是streaming说明TCP连接本身活着walsender还在正常发数据。接着看sent_lsn和write_lsn的差值如果sent_lsn一直增加而write_lsn停滞说明备库的walreceiver收到了数据但没写盘问题大概率出在备库的磁盘IO上。如果write_lsn和replay_lsn差值拉大说明walreceiver写盘正常但备库启动进程重放太慢这时候要去备库看pg_stat_activity里有没有长事务或慢查询在占资源。有一种比较隐蔽的停滞是同步复制模式下的“假死”。当synchronous_commit remote_apply时主库上的每个提交都需要等待备库回执。如果备库掉线了主库事务就会一直挂在提交阶段新的WAL也产生不出来整个系统表现为“主库写不了备库自然也没有新日志”。很多人在这种场景下查备库发现备库状态一切正常结果问题其实在主库的事务队列上。遇到这种情况先看主库有没有大量waiting for replication状态的会话有的话直接临时把synchronous_commit改成on让业务先恢复再处理备库。5.2 WAL堆积复制槽和max_slot_wal_keep_size的平衡复制槽导致的WAL堆积是流复制最经典的“自己埋雷”案例。你在主库上执行select slot_name, active, restart_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) as diff_bytes from pg_replication_slots;如果diff_bytes的数值一直在增大而active是false说明这个槽对应的备库早已断开但主库还在为它死守WAL。日积月累磁盘就告急了。解决思路有两个看场景用哪个。如果这个备库确实已经废弃直接删槽一条命令的事select pg_drop_replication_slot(dead_slot);如果备库还打算要只是暂时连不上那就给槽加个上限保护。PostgreSQL 13引入了max_slot_wal_keep_size参数你可以设成max_slot_wal_keep_size 40GB意思是单个槽位最多为主库多保留40GB的WAL超过之后WAL照样会被清理备库回来就只能重建。这相当于给物理复制槽加了一个“安全气囊”代价是备库长时间离线后需要重新做基础备份。权衡之后我觉得线上场景宁可让它超限放弃也不能让主库磁盘被打爆毕竟主库活着是底线。5.3 时间线分歧与主备切换TLI对不上的真实含义时间线分歧是主备切换之后最容易遇到的协议层问题。假设原来的主库A变成了备库原来的备库B被提升为主库。A重新向B发起复制时B的IDENTIFY_SYSTEM返回的timeline是2而A本地的时间线还停留在1怎么办协议的处理流程是A发现时间线不匹配后请求从B获取timeline history文件。B会把00000002.history文件发给AA读完这个文件才知道B是从哪个LSN开始切换到时间线2的。如果A本地在切换点之后产生了自己独有的WAL这些WAL不会被直接沿用而是被跳过。这就是为什么很多高可用方案在主备切换后要求旧主库做一次pg_rewind——它本质上就是借助协议层提供的信息把旧主库的本地数据回退到与时间线切换点一致的状态。我建议你在配置流复制的时候就把pg_rewind的用法和上下文提前踩一遍因为真到主备切换那一刻你不会还有心情去查文档。pg_rewind本身不是流复制协议的一部分但它依赖流复制协议来获取目标主库的时间线和WAL位置信息两者关系十分紧密。5.4 排查工具速查表最后把我常用的排查命令整理一下方便你遇到问题时直接抄作业。排查目标执行位置关键SQL/命令查看复制连接状态主库select client_addr, state, sent_lsn, write_lsn, replay_lsn from pg_stat_replication;查看复制槽状态主库select slot_name, active, restart_lsn from pg_replication_slots;查看主库当前WAL位置主库select pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn());查看备库接收状态备库select status, received_lsn, sender_host from pg_stat_wal_receiver;查看备库重放进度备库select pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();查看是否有同步等待主库select pid, state, wait_event_type, wait_event from pg_stat_activity where wait_event_type Replication;测试复制连接是否能建立主库psql host主库IP port5432 userreplica dbnamereplication -c IDENTIFY_SYSTEM;最后一行那个方法是很多DBA都会忽略的宝藏。你不需要真的在备库上操作直接在任意一台能连通主库的机器上用psql连接replication数据库执行IDENTIFY_SYSTEM;如果能返回结果说明网络、认证、权限都是通的。如果这一步都不通后面的协议层问题根本无从谈起。这也是我每次排查流复制问题时做的第一件事。一点收尾的个人心得做了这么多年PostgreSQL运维我最大的体会是流复制协议本身并不复杂它就是一个“日志搬运工”但就是这个搬运工串联起了主备一致性、高可用切换、故障恢复整套体系。很多看起来玄学的问题最后定位到协议层都会变得特别清晰——要么是LSN对不上要么是时间线不一致要么是心跳断掉了。如果你第一次接触流复制排障我建议你先别急着看日志花半天时间把pg_stat_replication和pg_stat_wal_receiver里的每个字段搞明白再配合抓包工具看一遍真实的数据包流动后面遇到任何问题都不会慌。希望这份笔记能帮你在流复制的路上少踩几个坑。