首页 / 资讯中心 / 文章详情

Redis主从复制从PING到PSYNC:复制握手状态机与故障排查

Redis主从复制从PING到PSYNC:复制握手状态机与故障排查 ★ FEATURED ARTICLE
读 Redis 源码的时候“主从复制”是我觉得最值得啃的一块而真正把我卡住过的地方恰恰是主从建立连接之后的这段应用层“三次握手”。别看名字叫握手它跟 TCP 的三次握手不是一回事它发生在 TCP 连接已经建立、传输层可靠通道已经打通之后由 Redis 的主从节点在应用层各自发起的多轮请求确认。这篇文章我打算把 repl.c 里从节点从发起 replicaof 到发送 PSYNC 之间的完整状态机拆开顺便讲清楚主节点侧每个命令分别走了哪个处理函数最后再给出我实际排查握手问题时的观测方法和心得。如果你想搞懂主从复制里“主从是怎么互相认出对方”的那一段这篇应该正对胃口。1. 复制握手到底握的是什么先和TCP三次握手的误区说清楚1.1 两个握手分层不同解决的问题完全不同TCP 三次握手发生操作系统内核协议栈里解决的是“网络通不通、双方收发能力正不正常”的问题。等 TCP 连接建立好Redis 主从之间的字节流就是畅通的了理论上这时候可以发任何数据。但问题来了主节点面对的外部连接那么多它怎么知道新来的这个连接到底是一个普通客户端还是一个准备做复制的从节点复制握手就是在这一层补上“应用层身份确认”。它要回答三个问题对端是不是一个真正的、能正常响应 Redis 协议的 Redis 实例如果主节点配置了密码对端的身份凭证能不能通过认证对端声称自己是从节点那么它支持哪些复制协议特性它想从哪个历史偏移量开始同步这个角色很像公司入职流程。TCP 把大楼门禁给你打开了但你还得先在前台证明自己是今天约好的候选人PING然后提交学历证书和岗位确认单AUTH / REPLCONF最后到人力资源系统里完成报到PSYNC。门禁开了不代表你能进工位复制握手就是“走到工位”之前的那几步确认。1.2 说好的三次握手为什么源码里远远不止三个包这是很多朋友查资料时最容易犯迷糊的地方。严格看 repl.c 里从节点的状态机一次完整的握手流程包括PING、REPLCONF listening-port、REPLCONF capa、PSYNC如果配了 masterauth 还要先走 AUTH。报文数量根本不是三个。那“三次握手”这个说法到底从哪来的我的理解是它指的是三个功能关卡而不是三个包第一次PING 探活。确认对端是一个活着的、支持 Redis 协议的节点这种探活成本最低也最容易定位问题。第二次REPLCONF 元数据交换。从节点上报自己的监听端口、复制能力。虽然实际发送时被拆成 listening-port 和 capa 两次但它们本质都是在交换配置信息。第三次PSYNC 同步请求。从节点把自己掌握的 replid 和偏移量发给主节点由主节点裁决是部分重同步还是全量重同步。AUTH 是可选项REPLCONF 又被拆成两次所以严格说其实是四到五个包。源码角度反而比概念更清楚不要死记“三次”要记状态机的转折点。2. 从节点状态机PING、AUTH、REPLCONF、PSYNC 是如何一步步发出的2.1 一切从 replicationSetMaster 开始从节点执行REPLICAOF master_ip master_port命令后会进入复制初始化。核心函数是 replicationSetMaster逻辑非常直接记录主节点 IP 和端口把 server.masterhost、server.masterport 设好把复制状态置为 REPL_STATE_CONNECT然后立刻调用 connectWithMaster 发起连接。我按源码逻辑整理了一份简化伪代码void replicationSetMaster(char *ip, int port) { // 记录目标主节点地址 server.masterhost sdsnew(ip); server.masterport port; // 断开当前可能存在的复制关系并清空相关状态 disconnectSlaves(); cancelReplicationHandshake(); ... server.repl_state REPL_STATE_CONNECT; connectWithMaster(); }connectWithMaster 做的是创建非阻塞 TCP 连接注册读事件回调 syncWithMaster状态变成 REPL_STATE_CONNECTING。这一步不会立刻发送复制协议命要等连接真正可读之后由事件回调来推进。你可能会问为什么不在 connectWithMaster 里直接把 PING 发出去因为非阻塞 connect 的返回不能代表对端已经接受连接只有等 socket 可读可写才能确定 TCP 握手已经完成。所以 Redis 把发送时机推迟到事件循环通知这是异步编程里很典型的做法。2.2 syncWithMaster 就是一个握手状态机syncWithMaster 是这篇文章里最值得看的一个函数。它的核心是根据 server.repl_state 的值决定当前应该读取响应还是发送下一个请求。我把主干状态转移画成文本逻辑void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { // 1. 如果状态还是 CONNECTING先检查连接是否真正建立 if (server.repl_state REPL_STATE_CONNECTING) { if (连接失败) { serverLog(LL_WARNING, Unable to connect to MASTER: ...); return; // 等待 replicationCron 重试 } server.repl_state REPL_STATE_RECEIVE_PONG; // 发送握手第一条命令: PING } // 2. 读取主节点响应并按照状态机推进 if (server.repl_state REPL_STATE_RECEIVE_PONG) { if (响应是 PONG) { if (server.masterauth) { server.repl_state REPL_STATE_SEND_AUTH; // 发送 AUTH } else { server.repl_state REPL_STATE_SEND_PORT; } } } if (server.repl_state REPL_STATE_RECEIVE_AUTH) { if (响应是 OK) { server.repl_state REPL_STATE_SEND_PORT; } } if (server.repl_state REPL_STATE_RECEIVE_PORT) { if (响应是 OK) { server.repl_state REPL_STATE_SEND_CAPA; } } if (server.repl_state REPL_STATE_RECEIVE_CAPA) { if (响应是 OK) { server.repl_state REPL_STATE_SEND_PSYNC; } } }实际源码还包含对各种-ERR响应、EOF、超时等分支的处理上面只是主干。但这张表已经足够说明问题每发一条命令都要等主节点明确回 OK 或正确数据才进入下一步。没有一个命令被抢跑。2.3 每一步报文背后的设计意图PING最便宜的协议探活。它不携带任何复制语义哪怕是普通客户端发 PING 也能拿到 PONG。从节点用它验证“对端至少是一个 Redis 实例”。AUTH如果主节点需要密码从节点必须在复制开始前证明身份。注意 masterauth 配的是主节点的密码不是从节点自身的 requirepass这个方向别搞反。REPLCONF listening-port告诉主节点自己在监听哪个端口。主节点把它记录到 client 结构里后面 INFO replication 才能显示出从节点的 IP:PORT 拓扑关系。REPLCONF capa eof capa psync2这是能力协商。capa eof 表示从节点能识别 EOF 分隔符格式的 RDBcapa psync2 表示支持新版部分重同步。没有这个协商主节点只能用最保守的传输方式RDB 推进效率会受影响。PSYNC从节点带着 replid 和 offset 请求同步。如果是第一次复制通常发PSYNC ? -1意思是“我没有历史直接给我全量”。Redis 没有把 REPLCONF 的两个参数合并成一次发送是为了每一步都得到确认。这种设计在问题排查时非常有用握手中断了你能根据状态机精确判断是卡在了端口上报还是能力协商而不是面对一个打包命令的黑盒。3. 主节点接招拆招PING、REPLCONF、PSYNC 分别走了哪条处理路径3.1 PING 走的是 pingCommand不是 syncCommand新手读源码很容易把复制握手的命令都归到 syncCommand 里实际不是。从节点发出的 PING 在命令表里对应的处理函数是 pingCommand主节点收到后回PONG整个过程和普通客户端发 PING 没有任何区别。这也说明一个重要事实握手初期主节点并没有给这个连接任何特殊待遇。它只是一个普通客户端连接直到主节点收到 REPLCONF 或 PSYNC才开始把它往“从节点”方向标记。3.2 REPLCONF 由 replconfCommand 分类处理REPLCONF 是一个复合命令replconfCommand 根据第一个参数区分场景。握手阶段只有前两种listening-port主节点把从节点上报的端口写入 client-slave_listening_port。后续 INFO replication 展示 from节点地址时会用到。capa主节点按子串设置能力位例如可识别 “eof” 和 “psync2”。ACK从节点在命令传播阶段周期性上报已复制的偏移量对应复制心跳。GETACK主节点主动询问从节点的复制偏移量Redis 哨兵选主前常用。REPLCONF 的每种类型都回OK。这里有个容易忽略的点主节点并不会完全信任从节点上报的端口值它会对端口是否合法做基础校验。但生产环境中更常见的是 NAT 或容器环境从节点上报的是内网端口主节点能看到从节点的连接地址却不能用 INFO 里的地址直连从节点。这不代表握手有问题只是拓扑展示受网络环境限制。3.3 PSYNC 由 syncCommand 做全量还是增量的裁决syncCommand 才是真正的复制入口。主节点收到 PSYNC 后大致做这样的判断先看从节点带过来的 replid 是否等于主节点当前的 replid。如果相等再看 offset 是否仍然落在 repl_backlog 这个环形缓冲区范围内。落在范围内返回CONTINUE replid从节点直接从断点继续接收增量命令这就是“部分重同步”。replid 对不上或者 offset 已经滑出 backlog 范围说明从节点落后太多返回FULLRESYNC replid offset触发 BGSAVE 生成 RDB走全量同步。如果主节点不支持 PSYNC老版本会退化为 SYNC直接强制全量。rmepl_backlog 是这里的关键变量。它本质是一块环形缓冲区只保存写命令传播流。很多运维事故里从节点只是断了几分钟结果 offset 直接被后续流量冲出了 backlog重新连上来就触发全量同步。你可以把 repl_backlog 想成一根水管主流不停往里灌从节点断开的瞬间它接水位的位置决定了回来之后还能不能续上。要想减少不必要的全量同步就得把 repl-backlog-size 设置得足够大覆盖你预期中最长的断连峰值。4. 握手失败现场认证不对、半开连接、PSYNC 被拒的常见结局4.1 AUTH 不过导致的“复读机”循环最常见的握手失败场景就是密码不对。从节点日志里会出现反复的连接、断开、再连接表现很像“复读机”。原因是 syncWithMaster 在收到错误响应后会把连接关掉状态回到 CONNECTreplicationCron 再重新发起连接于是循环重来。排查时的第一步永远是核对两端配置主节点 requirepass 是什么从节点 masterauth 是什么。Redis 6.0 之后引入了 ACL如果主节点给复制用户单独配了密码从节点的 masterauth 要写成“用户名 密码”的空格分隔形式比如masterauth default MyPass123很多老配置迁移到 ACL 环境后还只填密码不带用户名结果 AUTH 一直失败。这个是升级后比较容易踩的坑。4.2 半开连接与 replicationCron 的兜底机制还有一种情况比较隐蔽TCP 连接处于半开状态从节点发出去的 PING 石沉大海既没有 PONG 也没有 RST。此时从节点不能无限等下去不然整个复制模块就被一个假连接卡死。Redis 的 replicationCron 每 1 秒跑一次它会检查主连接的空闲时间一旦发现读超时或者状态停滞就主动关闭连接并清理状态让握手回到起点。我在模拟一个弱网项目时遇到过类似问题从节点 INFO 一直显示 master_link_status:down但 Redis 日志非常干净没有任何认证失败的记录。最后定位到是中间网络设备对长连接做了静默超时TCP 层已经断了但应用层没有感知。这种问题只调 Redis 配置是没用的要去看 TCP 层的重传和丢包必要时调整网络设备层面的连接空闲参数。这个案例让我养成一个习惯握手现象排查先分清是应用层拒绝还是传输层静默。4.3 PSYNC 被拒的边界replid 和 backlog 的冷门细节日志里出现FULLRESYNC很多人会当成故障其实它是正常路径之一。只有 PSYNC 返回-ERR或者全量同步执行到一半反复失败才需要认真排查。真正容易出现误判的场景是主从切换之后。旧主节点重新以从节点的身份回来它 PSYNC 里携带的 replid 可能是旧主时代的 id。如果新主开启了部分重同步能力且 backlog 还覆盖了旧主断开的偏移量可能还能续上否则主节点会拒绝部分重同步要求全量。不要看到 FULLRESYNC 就认为一定是代码 bug它更多是数据落差的客观裁决。另外要注意repl_backlog 大小不是设置得越大越好。它占内存并且只对复制有用。实际生产里不少人直接给到 128MB 甚至 1GB结果内存压力变大。更合理的做法是先按写流量峰值估算比如主节点每秒产生 1MB 写命令希望容忍从节点断连 10 分钟那 backlog 至少要 600MB。这个值需要根据业务量动态调整而不是拍脑袋。5. 想看真实握手报文MONITOR、tcpdump 和 INFO 三件套的用法5.1 MONITOR 里一眼看全四句话想最快看到握手全过程可以在主节点上开一个redis-cli MONITOR然后在从节点触发REPLICAOF。MONITOR 会打印从节点发来的命令序列大概长这样PING REPLCONF listening-port 6380 REPLCONF capa eof capa psync2 PSYNC ? -1这条输出基本就是握手全过程的真实回放。注意 MONITOR 会把所有命令都打出来生产高流量实例上开它会导致输出爆炸建议在测试环境或者业务低峰期短时间观测。5.2 tcpdump 抓包能同时看到两个“三次握手”如果你想建立更立体的感官可以在主从之间的链路上抓包tcpdump -i eth0 -s 0 -w redis_repl.pcap host 192.168.1.10 and port 6379用 Wireshark 打开 pcap 文件你能清楚看到前面是 TCP 的 SYN、SYNACK、ACK后面紧跟着应用层的 PING、REPLCONF、PSYNC。两个“握手”一前一后分层关系一目了然。我在实际排查时会重点看两个东西一是 PING 到 PONG 之间有没有明显延迟如果有大概率不是 Redis 的问题而是网络链路二是 REPLCONF 之后的响应是不是OK如果这里收到别的字符串说明主节点对该客户端做了异常处理。5.3 排查三步法日志、INFO、CLIENT LIST最后分享一个我自己常用的排查顺序遇到握手问题不要一上来就抓包先看从节点日志。把 loglevel 临时调到 debug能拿到握手阶段更多细节redis-cli config set loglevel debug再看从节点的 INFO replication重点看 master_link_status 是 up 还是 downmaster_repl_offset 有没有往前推进。如果 offset 不动说明命令传播阶段也有问题。最后到主节点执行 CLIENT LIST看有没有 flags 为 S 的连接。如果握手已经完成主节点会把这个连接标记为从节点如果始终看不到 S 标志说明握手还没走完继续回到日志和网络层排查。这三步法能帮你快速缩小范围避免在已经完成握手的场景里白白浪费时间。最后再说一个读源码的小体会。我第一次看 repl.c 时试图从头到尾逐行读结果被状态机绕得晕头转向。后来换个方式先把从节点状态按“连接建立 - PING - AUTH(可选) - REPLCONF - PSYNC - 等待RDB”列成线性序列再回到源码里对照每个状态的分支处理效率一下子高了很多。复制握手只是主从复制的入口握手完成之后还有 RDB 传输、命令传播、断线重连这些大话题等后面有机会再继续写。
阅读完成 · 觉得有帮助?
咨询建站