在Redis Sentinel实现原理分析这篇文。Sentinel是为主从复制服务的,所以在这篇文章里面,我们反过来讲一下主从复制的实现。
主从复制涉及到RDB等机制,其中持久化部分在Redis持久化机制实现中介绍。
Redis主从复制流程简介
Redis Sentinel 是对主从复制流程而言的,所以先要理解主从复制的大概流程。这里需要注意,主从复制并不是 Redis Cluster。
- Slave 接受到 SLAVEOF 命令
- Slave 连接 Master
- Slave PING Master
- 鉴权
- Slave 发送 SYNC/PSYNC 命令
PSYNC命令用法
PSYNC 命令如下所示,其中:
- replicationid 表示我们断线重连前 Master 服务器的 id
- offset 表示 Slave 接受到最后命令的偏移量,以字节计算
1 | // 旧 |
这里还有个特殊用法,表示我们要触发一次全量复制。
1 | PSYNC ? -1 |
在 Redis 2.8后,提供了 PSYNC,这个命令能够支持全量复制和部分复制。这样在 Slave 断线重连之后,就可以部分复制,从而节省 Master 的计算资源和带宽。
在 Redis 4.0 版本后,优化了增量复制,主要包括:
- 重启后,也可以进行部分复制
之前这种情况,重启后会丢失 runid,从而触发PSYNC ? -1
- 当 Slave 被 promote 称为 Master 后,其他 Slave 可以从新 Master 处复制
主要流程
主要类以及常数
在服务器类中定义了 masterhost
,表示 Master 节点的地址。如果是 NULL,表示自己就是 Master。所以这个字段会被用来判断是 Master 还是 Slave。
1 | // server.h |
介绍下repl_state
的状态:
REPL_STATE_NONE 0
表示现在是SLAVEOF NO ONE的REPL_STATE_CONNECT 1
在replicationCron
判断,如果处于这个状态,表示现在要去尝试连接Master了。REPL_STATE_CONNECTING 2
下面的状态是握手过程中的状态:
REPL_STATE_RECEIVE_PONG 3
REPL_STATE_SEND_AUTH 4
REPL_STATE_RECEIVE_AUTH 5
REPL_STATE_SEND_PORT 6
REPL_STATE_RECEIVE_PORT 7
REPL_STATE_SEND_IP 8
REPL_STATE_RECEIVE_IP 9
REPL_STATE_SEND_CAPA 10
REPL_STATE_RECEIVE_CAPA 11
REPL_STATE_SEND_PSYNC 12
REPL_STATE_RECEIVE_PSYNC 13
下面状态是握手完毕的状态:
REPL_STATE_TRANSFER 14
REPL_STATE_CONNECTED 15
连接建立流程
- connectWithMaster
Full Sync流程
Partial Sync流程
代码解释 Slave部分
connectWithMaster: 建立套接口连接
connConnect
系列函数,以及connection
类封装了有关网络连接的库。
实际上connConnect
通过anetTcpNonBlockBestEffortBindConnect
尝试建立一个非阻塞的套接字,此时connect
函数可能返回EINPROGRESS
表示连接还在建立过程中,但我们其实可以不用等。通过aeCreateFileEvent
将这个Socket描述符加入到事件循环里面,等到这个套接字可以写之后,会触发对应的回调。
等到连接建立后,回调会通过connSocketEventHandler
被唤起。
1 | int connectWithMaster(void) { |
syncWithMaster: 握手以及准备传输RDB
在连接完毕后,connectWithMaster
会回调syncWithMaster
,此时状态是REPL_STATE_CONNECTING
。
1 | void syncWithMaster(connection *conn) { |
检查一下,如果现在又是SLAVEOF NO ONE了,就把这个连接关掉。
1 | ... |
因为是非阻塞的连接,所以我们要检查一下现在连接的状态。如果失败,就goto error,里面内容是重置状态,例如,server.repl_state
会被重置为REPL_STATE_CONNECT
。
1 | ... |
下面是一个状态机的实现。我们在Sentinel中已经见过类似的了,Redis中状态机的实现就是,对于状态X,表示状态X前一个状态已经处理完了,目前正在处理状态X的工作。当状态机处理完一个状态后,在最后将状态设置为下一个要做的事情。也就是我们不用类似X_FINISHED
这样的状态,因为X_FINISHED
根据完成的情形不同,可能有多种状态转移。
【REPL_STATE_CONNECTING】这个状态下,我们尝试发送一个同步命令PING,然后直接修改状态到REPL_STATE_RECEIVE_PONG
。如果这个同步命令发送有问题,就直接goto error了,不会走到下面流程。
1 | ... |
【REPL_STATE_RECEIVE_PONG】我们只要收到对PING的回复,就进入了REPL_STATE_RECEIVE_PONG
状态,但这个回复未必是PONG,也可能是一个AUTH错误。
1 | ... |
【REPL_STATE_SEND_AUTH】如果需要AUTH认证,我们就发送AUTH,进入REPL_STATE_RECEIVE_AUTH
。否则直接进入REPL_STATE_SEND_PORT
。
1 | ... |
【REPL_STATE_RECEIVE_AUTH】如果验证通过,就进入REPL_STATE_SEND_PORT
。
1 | ... |
【REPL_STATE_SEND_PORT】下面一步,我们需要发送我们当前的端口,进入REPL_STATE_RECEIVE_PORT
状态。
在发送完之后,我们在主节点执行INFO replication
,会在其中显示我们反馈的port。
【Q】slave_announce_port
和port
的区别是什么?
1 | ... |
【REPL_STATE_RECEIVE_PORT】接下来,我们用类似的办法发送IP,这里注意,如果没有指定slave_announce_ip
就直接跳转到REPL_STATE_SEND_CAPA
,否则跳转到REPL_STATE_SEND_IP
。
1 | ... |
【REPL_STATE_SEND_CAPA】发送IP的过程很类似,我们就不说了。下面这一对状态是REPL_STATE_SEND_CAPA
,用来发送Slave的容量。这一对状态结束之后,进入REPL_STATE_SEND_PSYNC
状态。
【Q】这个容量指的是什么?
1 | ... |
【REPL_STATE_SEND_PSYNC】下面,我们尝试PSYNC。主要就是调用若干次slaveTryPartialResynchronization
:第一次传0进去,让它发PSYNC指令,并且设置状态为REPL_STATE_RECEIVE_PSYNC
;后面就不断地传1进去,查询结果。
1 | ... |
下面查看返回值
1 | ... |
如果PSYNC能支持,我们前面就返回了,下面对于不支持的情况,我们就得用老的SYNC方法。在开始传输后,进入REPL_STATE_TRANSFER
状态。
1 | ... |
如果不支持无盘加载,那么就要在磁盘上创建一个临时文件。
查看函数useDisklessLoad
,无盘加载需要满足:
repl_diskless_load
配置- 所有的模块都能处理读错误
1 | ... |
下面非阻塞地进行SYNC,设置读取SYNC数据的回调readSyncBulkPayload
,如果成功就切换状态为REPL_STATE_TRANSFER
。
这里,我们设置了repl_transfer_size
为1,
1 | ... |
下面是错误处理,需要将状态重置为等待连接的REPL_STATE_CONNECT
。
1 | ... |
slaveTryPartialResynchronization: PSYNC分支
1 |
|
PSYNC_WRITE_ERROR 0
套接口不可写。PSYNC_WAIT_REPLY 1
需要read_erply
设置为1,并调用函数。PSYNC_CONTINUE 2
PSYNC_FULLRESYNC 3
表示虽然支持PSYNC,但现在仍然需要一次Full SYNC。在这情况下,我们需要保存Master的runid和offset。PSYNC_NOT_SUPPORTED 4
不支持PSYNC。PSYNC_TRY_LATER 5
暂时连不上Master,要重试。
1 | int slaveTryPartialResynchronization(connection *conn, int read_reply) { |
首先,是写部分。这里的写,指的是往连接里面发送PSYNC
指令:
- 如果我们缓存了
server.master
到server.cached_master
通常是在replicationCacheMaster
中设置的 - 如果是第一次连
发送1
PSYNC ? -1
1 | /* Writing half */ |
读出Master的回复,如果是空,我们就返回继续等待PSYNC_WAIT_REPLY
。
1 | ... |
如果回复是+FULLRESYNC
,表示需要一次Full SYNC。
1 | ... |
1 | ... |
否则,我们可以部分同步。
1 | ... |
这里new表示Master端传来的runid。如果和我们当前的server.replid
不一样,我们要重新设置一下,并且将老的server.replid
复制给server.replid2
。
【Q】这里涉及到三个replid,他们的区别是什么呢?
server.replid
server.replid2
server.cached_master->replid
1 | ... |
如果当前Slave有Sub Slave,全部断开,让他们重新走PSYNC流程。
1 | ... |
readSyncBulkPayload: SYNC分支 接受RDB
1 | /* Asynchronously read the SYNC payload we receive from a master */ |
主事件循环
replicationCron
主要代码位于replication.c中。
主函数replicationCron
被serverCron
触发,每隔一秒钟触发一次。
1 | run_with_period(1000) replicationCron(); |
下面查看主函数。
1 | // replication.c |
首先,下面是几个超时判断:
- 建立连接过程中超时
- 传输过程中超时
- 心跳/数据超时
1 | ... |
判断是否需要连接Master。connectWithMaster
这个函数会将状态设置为REPL_STATE_CONNECTING
,并设置回调syncWithMaster
。
1 | ... |
如果Master支持PSYNC,就定期发送ACK。
这个ACK的作用是发送一个REPLCONF ACK
命令给Master,从而通知自己当前的复制偏移。
1 | ... |
下面,我们对所有Slave发送PING。根据注释,如果我们连接了Slave(是不是说明当前节点是Master?),就按时PING它们。这样Slave们能够维护到Master的显式的超时时间,从而在TCP连接并没有真正丢失的时候,检查一个断线的情况。
1 | ... |
1 | ... |
1 | ... |
如果SLAVEOF自己会怎么样?
Reference
- http://cbsheng.github.io/posts/redis%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1servercron/
- https://www.cnblogs.com/kismetv/p/9236731.html
- https://youjiali1995.github.io/redis/replication/
- https://wenfh2020.com/2020/05/31/redis-replication-next/
有注释 - https://redis.io/commands/psync
- https://zhuanlan.zhihu.com/p/44105707
- https://zhuanlan.zhihu.com/p/86617437
讲解sub slave