PSYNC 与部分重同步
从库断线一次就全量同步一次,是 Redis 2.8 之前的梦魇。PSYNC 用复制 ID 加偏移量把"断线重连"变成毫秒级的增量补发。本文讲清判定逻辑、故障转移后的 replid2 机制与验证方法。
一、从 SYNC 到 PSYNC
旧版 SYNC 协议没有状态记忆:主库不记录从库同步到哪,从库一断线,唯一的选择就是重新走一遍全量。Redis 2.8 引入 PSYNC:
PSYNC <replid> <offset>
- replid:复制 ID,40 字符随机串,标记一条复制链的"血统",随 RDB 传播给从库;
- offset:从库已消费到的复制流偏移量。
主库判定:replid 一致且 offset 仍在 repl_backlog 覆盖范围内,回复 +CONTINUE 后只补发缺失区间;否则回复 +FULLRESYNC 进入全量流程。从库收到 -ERR(对端不支持 PSYNC)时回退到旧版 SYNC。
二、判定逻辑逐条拆解
从库带着 (replid, offset) 重连后,主库按顺序检查:
- replid 是否等于自己的 replid;不等,看第 2 步;
- replid 是否等于自己的 replid2,且 offset 不超过 second_replid_offset(故障转移场景,见下一节);都不满足,全量;
- offset 是否 ≥
repl_backlog_first_byte_offset;小于说明缺口已超出环形缓冲区,全量; - 全部通过,回复
+CONTINUE,从 backlog 中 offset 位置开始补发。
可见部分重同步的两个硬前提:血统可追溯、数据在缓冲区内。backlog 太小是实践中最常见的失败原因,协议判定本身只花微秒级时间。
判定失败时主库的回复会直接指明原因,读懂这些关键字能省掉大量猜测。+CONTINUE 表示部分重同步成功,后面可能跟着新的 replid(说明发生了主库切换);+FULLRESYNC <replid> <offset> 表示必须全量;日志里的 Unable to partial resync with replica ... for lack of backlog 说明第 3 步没过,缺口超出缓冲区;... for different master runid or replid 说明第 1、2 步没过,血统对不上。这两条日志对应的处置完全不同:前者要加大 repl-backlog-size,后者要检查是否发生过故障转移或主库重建。
协议层面还有一个细节:+CONTINUE 之后主库可能先发一行 +CONTINUE <new replid>,通知从库自己已经换了血统。从库收到后把 master_replid 更新为新值,同时把旧值保留到 master_replid2,这样下一次断线重连时携带的就是新血统,能命中第 1 步而不是第 2 步。这套"新血统前移、旧血统降级"的链式记忆,是 PSYNC2 能在多次切换后依然支持增量续传的原因。
三、replid2:故障转移后的续传
哨兵或 Cluster 把从库提升为主库后,新主库会做一件关键的事:把自己的 replid2 设为旧主库的 replid,second_replid_offset 记录切换时刻的偏移量。其他从库拿着旧主库的 replid 来请求 PSYNC,就能命中第 2 步的判定,从新主库的 backlog 续传,免于全量。
redis-cli INFO replication | grep replid
# master_replid:aaaaaaaa...(新链)
# master_replid2:bbbbbbbb...(旧主血统)
# second_replid_offset:88213904
同样的机制让主库重启后的重连也可能部分化:4.0 起主库把 replid 与 offset 存入 RDB,重启加载后,从库的 offset 未超出记录值即可继续增量。PSYNC2 这组改进共同决定了故障切换的速度——切换只影响新写入,不必为每个从库重传全量。
需要说清一个容易混淆的点:replid2 只保存"上一代"血统,长度为一次。如果短时间内连续发生两次主库切换,第一次切换带来的旧血统会被第二次切换覆盖,更早的从库再重连就只能全量。这也解释了为什么频繁误判的哨兵环境容易出现"全量风暴"——每次切换都在缩短可追溯的血统链,直到所有从库都掉出窗口。
second_replid_offset 的取值时机同样关键。它记录的是切换时刻的复制偏移量,新主库在此之后写入的数据从库一定拿不到旧链的位置信息,因此判定条件里必须同时满足"offset 不超过 second_replid_offset"。如果旧从库在切换前就已经落后很多,它的 offset 虽然小于该值,但缺口部分可能已经不在新主库的 backlog 里,此时第 3 步仍然会拒绝并转为全量。两个条件串联,缺一不可。
四、验证与演练
# 1. 记录当前 offset
redis-cli INFO replication | grep master_repl_offset
# 2. 主库上断开从库连接(演练环境通常只有一个从库)
redis-cli CLIENT KILL TYPE replica
# 3. 从库自动重连后,看主库日志
tail -f /var/log/redis/redis-server.log
# Partial resynchronization request ... accepted(部分重同步成功)
演练矩阵建议覆盖四种场景:闪断 10 秒(应部分重同步)、断开超过 backlog 覆盖时长(应全量)、手动 FAILOVER 后旧从库重连(应经 replid2 部分重同步)、主库重启(4.0 以上可部分重同步)。每种场景记录主从恢复耗时,作为高可用评估依据。
五、部分重同步救不了的情况
- backlog 被覆盖:断连太久,缺口超出环形缓冲区;
- 血统断裂:主库数据被从备份重建后 replid 变化,旧从库只能全量;
- replid2 范围外:故障转移后旧从库落后太多,offset 超过了切换时刻的记录值;
- 主库重启且从库领先:从库 offset 超过 RDB 中记录的值,缺口无源可补。
这些场景本质都是"缺口无源可补"。规划时的对策依然是:把 backlog 做大、缩短断连时间、failover 后尽快统一复制拓扑。
补一条容易被忽略的情况:从库自身发生阻塞也会导致全量。从库执行 BGSAVE、BGREWRITEAOF、DEBUG SLEEP 或加载大 key 时,主线程无法读取 socket,主库侧看到的输出缓冲区会持续增长;一旦超过 client-output-buffer-limit replica 的硬上限,主库会主动断开从库。断开的瞬间从库可能还在阻塞,等它恢复并重连时,主库已经写入了更多数据,如果 backlog 不够大就会退化为全量。所以从库的持久化配置同样要按"不阻塞传播"来设计,例如在从库上把 appendfsync 设为 everysec、把 save 条件放宽,而不是把主库的配置原样复制过去。
常见坑 / 最佳实践
- 全量频繁却只查网络。 先看
repl_backlog_size与repl_backlog_first_byte_offset,多数"网络问题"实际是 backlog 太小。 - 混淆 replid 与 runid。
INFO server里的 runid 标识实例进程,与复制链的 replid 无关,排障时别拿错字段。 - 升级前确认协议兼容。 PSYNC 与 replid2 分别要求 2.8 与 4.0,跨大版本组建主从前先核对两端版本。
- 故障演练纳入 PSYNC 场景。 只演练"主库挂了"不演练"从库闪断",会把 backlog 配置缺陷留到生产才暴露。
小结
- PSYNC 用 replid 加 offset 判定能否增量续传,条件是血统一致且缺口在 backlog 内。
- replid2 与 second_replid_offset 让故障转移后其他从库免于全量,是 4.0 起的重要改进。
- 主库重启后的部分重同步依赖 RDB 中保存的 replid 与 offset。
- 判定本身微秒级,失败原因集中在 backlog 容量与血统断裂两类。
- 用 CLIENT KILL 模拟闪断做演练,把四种场景的恢复耗时记录进高可用基线。
- 点赞
- 收藏
- 关注作者
评论(0)