主从复制:PSYNC2 协议与复制积压缓冲区
第16章 主从复制:PSYNC2 协议与复制积压缓冲区
Redis 的主从复制是构建高可用系统的基石。本章深入剖析 PSYNC2 协议的握手流程、replid 机制、全量与部分重同步的判定逻辑,以及复制积压缓冲区的设计原理与生产调优。
16.1 主从握手完整流程(7步)
从库启动后,执行以下握手序列与主库建立复制关系。每一步都有明确的协议语义,理解这些细节对排查复制故障至关重要。
第1步:PING
从库连接主库的 TCP 端口后,首先发送 PING 命令,验证主库存活且能正常响应:
From slave → master: *1\r\n$4\r\nPING\r\n
From master → slave: +PONG\r\n
如果主库未响应或响应超时(受 repl-timeout 控制,默认60秒),从库断开并重试。这一步也是从库判断网络连通性的基础检查。
第2步:AUTH
若主库配置了 requirepass,从库发送 AUTH 命令进行密码认证:
From slave → master: *2\r\n$4\r\nAUTH\r\n$6\r\nsecret\r\n
From master → slave: +OK\r\n
从库侧配置:
masterauth <password>
注意:Redis 6.0+ 引入 ACL,主从复制也可使用 masteruser + masterauth 组合进行 ACL 用户认证:
# redis.conf (slave)
masteruser replication_user
masterauth replication_password
第3步:REPLCONF listening-port
从库上报自己监听的端口,主库用于记录从库信息(INFO replication 中显示):
From slave → master: REPLCONF listening-port 6380
From master → slave: +OK\r\n
第4步:REPLCONF capa
从库声明自己支持的能力(capabilities),主库据此决定使用哪种协议:
From slave → master: REPLCONF capa eof capa psync2
From master → slave: +OK\r\n
eof:支持无盘复制(diskless replication),RDB 通过网络流传输,以 EOF 标记结束psync2:支持 PSYNC2 协议,即支持 replid2 的部分重同步
第5步:PSYNC
从库发送 PSYNC 命令,请求同步:
# 初次连接(无历史):
From slave → master: PSYNC ? -1
# 重连尝试部分重同步:
From slave → master: PSYNC <replid> <offset>
参数说明:
replid:从库保存的主库复制IDoffset:从库已处理到的复制偏移量
第6步:主库响应
主库根据请求类型返回两种响应之一:
# 全量同步:
+FULLRESYNC <replid> <offset>\r\n
# 部分重同步:
+CONTINUE <replid>\r\n
FULLRESYNC 响应中包含:
- 主库当前的
replid(40字节随机字符串) - 主库当前的
master_repl_offset
从库收到 FULLRESYNC 后,清空本地数据,准备接收 RDB。
第7步:传输 RDB + 积压命令
全量同步时:
- 主库触发 BGSAVE(或复用正在进行的 BGSAVE)
- 生成 RDB 期间,新写入命令同时写入 repl_backlog 和从库的 client output buffer
- RDB 生成完毕后,发送给从库:
- 磁盘模式:
$<rdb_size>\r\n<rdb_data> - 无盘模式:
$EOF:<40字节界定符>\r\n<rdb_stream><界定符>
- 磁盘模式:
- RDB 传输完毕后,发送积压的命令流
部分重同步时:
主库直接从 repl_backlog 中读取 [slave_offset, master_offset] 范围内的命令,通过已建立的连接发送给从库。
16.2 replid 与 replid2 机制
PSYNC2(Redis 4.0+)引入了双 replid 机制,解决了故障转移后无法部分重同步的问题。
replid 的生成
每个 Redis 实例在成为主库时生成一个新的 replid:
/* server.c */
void createReplicationId(void) {
getRandomHexChars(server.replid, CONFIG_RUN_ID_SIZE);
server.replid[CONFIG_RUN_ID_SIZE] = '\0';
}
replid 是40字节的十六进制随机字符串,例如:
8371b4fb1155b71f4a04d3e1bc3e18c4a990aeeb
replid2 与 second_replid_offset
当从库被提升为主库时(故障转移或手动 SLAVEOF NO ONE),发生以下转换:
旧主库: replid=A, offset=10000
从库提升: replid2=A, second_replid_offset=10001, replid=B(新生成), offset=10000
这意味着:新主库记住了"我曾经是谁的从库,以及我在哪个位置开始独立"。
部分重同步的 replid 匹配逻辑
当从库发送 PSYNC <rid> <offset> 时,主库检查:
/* replication.c - masterTryPartialResynchronization() */
if (strcasecmp(rid, server.replid) &&
(strcasecmp(rid, server.replid2) ||
psync_offset > server.second_replid_offset))
{
/* 无法部分重同步,触发全量 */
goto need_full_resync;
}
条件成立(可以部分重同步):
rid == replid:从库跟随的是当前主库(正常重连)rid == replid2且offset <= second_replid_offset:从库跟随的是旧主库,且偏移量在切换点之前(故障转移后重连)
这是 PSYNC2 相对于 PSYNC1 的核心改进:故障转移后其他从库可以直接跟随新主,无需全量同步。
查看当前 replid 状态
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
master_replid:8371b4fb1155b71f4a04d3e1bc3e18c4a990aeeb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1234567
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:104857600
repl_backlog_first_byte_offset:134568
repl_backlog_histlen:1099999
16.3 全量同步 vs 部分重同步
全量同步触发条件
以下任一条件满足时触发全量同步(FULLRESYNC):
- 从库首次连接主库(offset = -1)
- 从库的 replid 与主库的 replid 和 replid2 均不匹配
- 从库的 offset 落在 repl_backlog 覆盖范围之外
- 主库的 repl_backlog 被禁用(
repl-backlog-size 0)
全量同步代价高昂:
- 主库 CPU 和 I/O 压力(BGSAVE)
- 网络带宽消耗(传输整个 RDB)
- 从库需要清空所有数据并重新加载
部分重同步触发条件
同时满足以下所有条件时执行部分重同步(CONTINUE):
slave_replid == master_replid或slave_replid == master_replid2slave_offset在 repl_backlog 覆盖范围内:
master_repl_offset - repl_backlog_size <= slave_offset <= master_repl_offset
- 主库的 repl_backlog 已初始化(至少有一个从库连接过)
部分重同步的数据量估算
# 从库断连时间(秒)
disconnect_time = 30
# 主库写入速率(bytes/s)
write_rate = 50 * 1024 * 1024 # 50 MB/s
# 断连期间积累的数据量
missed_data = disconnect_time * write_rate # 1500 MB
# 若 repl_backlog_size = 100MB,则无法部分重同步
# 若 repl_backlog_size = 2000MB,则可以部分重同步
16.4 repl_backlog 环形缓冲区
数据结构
repl_backlog 是一个环形(circular)字节缓冲区,存储主库最近写入的命令序列:
/* server.h */
typedef struct {
char *buf; /* 环形缓冲区指针 */
long long histlen; /* 已存储的有效数据长度 */
long long idx; /* 下一个写入位置 */
long long offset; /* 对应 master_repl_offset 的起始偏移 */
size_t size; /* 缓冲区总大小(repl-backlog-size) */
} replicationBacklog;
写入逻辑:当 idx 到达末尾时,从头覆盖旧数据(环形)。
生产配置建议
# 默认值仅1MB,生产环境严重不足
repl-backlog-size 1mb # 默认,不推荐
# 生产推荐:根据公式计算
repl-backlog-size 512mb # 中等写入量(~10MB/s),可容忍约50秒断连
repl-backlog-size 1gb # 高写入量,建议值
# 积压缓冲区释放延迟:最后一个从库断开后,多久释放 backlog
repl-backlog-ttl 3600 # 默认3600秒,建议保持或调大
大小选择公式
repl_backlog_size = 最大可容忍断连时间(s) × 写入速率(bytes/s) × 2
乘以2是为了留有余量(COW 期间写入量可能翻倍)。
监控 backlog 使用率
redis-cli info replication | grep -E 'repl_backlog|offset'
# repl_backlog_size: 536870912 # 512MB
# repl_backlog_first_byte_offset: 134568
# repl_backlog_histlen: 536870912 # histlen == size,说明 backlog 已满(环形覆盖中)
# master_repl_offset: 2147483647
当 histlen == size 时,backlog 处于满载状态,旧数据正被覆盖。此时若从库 offset 落后过多,部分重同步将失败。
16.5 无盘复制(diskless replication)
传统磁盘模式 vs 无盘模式
| 维度 | 磁盘模式 | 无盘模式 |
|---|---|---|
| 流程 | BGSAVE → 写磁盘 → 发送文件 | BGSAVE → 直接通过 socket 发送 |
| 磁盘 I/O | 有(写 + 读) | 无 |
| 适用场景 | 磁盘快、网络慢 | 磁盘慢(如 EBS)、网络快 |
| 多从库并发 | 复用同一 RDB 文件 | 每个从库需要独立 fork(或等待) |
配置
# 启用无盘复制
repl-diskless-sync yes
# 等待时间(秒):等待更多从库连接后批量发送(减少 fork 次数)
repl-diskless-sync-delay 5
# 传输速率限制(0为不限制)
repl-diskless-sync-max-replicas 0
无盘模式下的多从库处理
时间线:
T=0: 从库A连接,主库等待 repl-diskless-sync-delay 秒
T=3: 从库B连接(在等待窗口内)
T=5: 等待结束,主库 fork 一次,同时向 A 和 B 发送 RDB 流
T=10: 从库C连接(错过窗口),等待下一次 BGSAVE
16.6 复制缓冲区(client output buffer)
与 repl_backlog 的区别
这是两个完全不同的缓冲区,经常被混淆:
| 缓冲区 | repl_backlog | client output buffer(replica) |
|---|---|---|
| 归属 | 全局(主库) | 每个从库连接独立 |
| 用途 | 支持部分重同步 | 主库向从库实时推送命令 |
| 满了之后 | 旧数据被覆盖(不影响复制) | 从库连接被强制关闭 |
| 大小配置 | repl-backlog-size | client-output-buffer-limit replica |
client output buffer 的三个限制
client-output-buffer-limit replica 256mb 64mb 60
格式:client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
hard limit:立即断开(256MB)soft limit:超过 64MB 持续 60 秒后断开- 从库断开后会触发全量重同步
恶性循环场景
主库大量写入(如 BGSAVE 期间 COW 导致 output buffer 积压)
→ 从库 client output buffer 溢出(>256MB)
→ 主库强制断开从库连接
→ 从库重连,发起 PSYNC,因 backlog 不足触发全量同步
→ 主库再次 BGSAVE(负载更高)
→ output buffer 再次溢出
→ 循环
解决方案:
# 1. 调大 output buffer 上限
client-output-buffer-limit replica 512mb 128mb 120
# 2. 使用无盘复制减少 BGSAVE 期间的写放大
repl-diskless-sync yes
# 3. 增大 repl-backlog-size(断连后能部分重同步)
repl-backlog-size 1gb
# 4. 限制主库写入速率(极端情况)
# 使用 CONFIG SET hz 和 slowlog 监控
16.7 复制相关配置汇总
# ============ 主库配置 ============
# 积压缓冲区大小(核心参数)
repl-backlog-size 512mb
# 无盘复制
repl-diskless-sync yes
repl-diskless-sync-delay 5
# 从库 output buffer 限制
client-output-buffer-limit replica 256mb 64mb 60
# 至少 N 个从库复制延迟 < M 秒,否则拒绝写入(见第17章)
min-replicas-to-write 1
min-replicas-max-lag 10
# ============ 从库配置 ============
# 主库地址
replicaof 192.168.1.1 6379
# 主库密码
masterauth your_password
# 从库是否只读(强烈建议 yes)
replica-read-only yes
# 从库连接超时
repl-timeout 60
# 从库优先级(Sentinel 选主时使用,越小优先级越高)
replica-priority 100
# ============ 共同配置 ============
# TCP 保活
repl-backlog-ttl 3600
tcp-keepalive 300
16.8 生产排查:复制中断诊断流程
步骤1:确认从库状态
redis-cli -h slave_ip info replication
# master_link_status: down ← 复制中断
# master_last_io_seconds_ago: 120 ← 多久没收到主库数据
# master_sync_in_progress: 1 ← 是否在全量同步中
步骤2:确认主库记录的从库状态
redis-cli -h master_ip info replication
# slave0:ip=192.168.1.2,port=6380,state=online,offset=1234567,lag=0
# slave1:ip=192.168.1.3,port=6381,state=sync,offset=0,lag=0
# ↑ state=sync 表示正在全量同步
步骤3:确认 backlog 是否足够
# 从库当前 offset
slave_offset=$(redis-cli -h slave_ip info replication | grep master_repl_offset | cut -d: -f2)
# 主库 backlog 起始位置
backlog_start=$(redis-cli -h master_ip info replication | grep repl_backlog_first_byte_offset | cut -d: -f2)
# 判断:slave_offset >= backlog_start → 可以部分重同步
echo "slave_offset: $slave_offset, backlog_start: $backlog_start"
步骤4:查看 slowlog 和 latency
redis-cli -h master_ip slowlog get 10
redis-cli -h master_ip latency history event
步骤5:确认 output buffer 使用情况
redis-cli -h master_ip client list | grep replica
# id=42 addr=192.168.1.2:43210 ... omem=67108864 ...
# omem 是 output buffer 已用内存(67MB,接近 soft limit 64MB)
16.9 Redis 7.x 复制改进
Redis 7.0 引入了 共享复制缓冲区(Shared Replication Backlog):
传统架构:
主库 → repl_backlog(全局)
主库 → client_output_buffer_slave1(per-slave)
主库 → client_output_buffer_slave2(per-slave)
问题:相同数据存了3份,内存浪费
Redis 7.0 新架构:
主库 → shared_replication_buf(全局共享)
slave1 和 slave2 都指向同一块共享内存中的不同位置
优点:内存使用从 O(N×size) 降为 O(size)
配置(7.0+):
# 共享复制缓冲区总大小限制
repl-backlog-size 512mb
# 注:7.0 中这个参数同时控制全局 backlog 和 per-slave 的共享缓冲区
本章小结
| 概念 | 关键参数/命令 | 生产建议 |
|---|---|---|
| 握手流程 | PING/AUTH/REPLCONF/PSYNC | 确保 masterauth 配置正确 |
| replid2 | INFO replication | 故障转移后确认 replid 切换正常 |
| 全量同步 | FULLRESYNC | 监控频率,避免频繁触发 |
| repl_backlog | repl-backlog-size | 生产至少 512MB,高写入量 1GB+ |
| 无盘复制 | repl-diskless-sync | 云环境强烈推荐开启 |
| output buffer | client-output-buffer-limit | 根据从库数量和写入量调整 |
掌握 PSYNC2 协议细节是深入理解 Redis 高可用架构的前提。下一章将分析复制延迟、数据一致性与数据丢失的具体场景。