复制 作为现代数据服务基本必备的功能,保证 数据的冗余备份,从而保障服务高可用和高可靠性,当然 Redis 这么伟大的服务也不例外
本章可能有点儿绕,做好心理准备…  ̄ω ̄=
Redis 复制( replicate ) 流程的核心在于 状态机 中状态的流转,连接各个逻辑运算,我根据 Redis ver4.0.11 的源码逻辑画了一个简单的 master-slave 状态机流转示意图,由于一般 从库的挂载都是由 slave 发起的,所以 slave 放在左边,大家可以先看一眼,我们后续进行每个步骤的详细讲解
Redis 复制模块 由于有些方法里的代码量比较大而且有点儿绕,所以我们分流程在文字详解的时候一块解析,同志们可以根据文章提示的代码位置 和 代码里面的关键词 在源码中搜素,可能数据结构一些元素 看不太懂什么意思,没关系,先混个脸熟,后面看完回头再看过来就明白了
状态机流转
发起建立 master-slave 关系
发起 master-slave 关系有几种方式,不过我们通常是在 conf 里面进行配置
redis.conf
文件中配置slaveof [masterip] [masterport]
选项,当然还是需要在服务启动的时候指定这个 redis.conf 配置redis-server
命令启动服务的时候指定参数--slaveof [masterip] [masterport]
client 模式
下通过slaveof [masterip] [masterport]
命令执行绑定主服务器
上面三种方式的主从复制,都是由 从服务器 主动执行的,最终命令使用的是 slaveof
命令的函数逻辑
我们还要看一下设置主服务器地址和端口的函数,同时也会有状态机的一部分,状态设置为 REPL_STATE_CONNECT
当前状态机状态为 REPL_STATE_CONNECT
master-slave 连接建立
slaveof 操作为一个异步操作,由 从服务发起 后直接返回响应结果,随后由 时间事件 回调函数 serverCron()
里面逻辑发起 调用 replicationCron()
函数
replicationCron()
每秒会被执行一次,如果发现 有主服务器信息 而且 状态机状态为 REPL_STATE_CONNECT,那么就会发起建立连接
上述逻辑中可以看到,调用 connectWithMaster()
函数 以非阻塞的方式连接主服务器
当前状态机状态为 REPL_STATE_CONNECTING
ping-pong 的发送/响应
刚才在上一个步骤中建立了与 主服务器的连接,而且建立了读写事件,所以会触发 AE_WRITABLE 事件,触发回调函数 syncWithMaster()
,发送 PING
给主服务器,并且等待 PONG
响应
主服务器在接到 PING 信息后 会返回 +PONG\r\n
,触发 从服务器写事件,调用 之前我们新建的从服务器读事件,回调 syncWithMaster() 进行处理
当前状态机状态为 REPL_STATE_SEND_AUTH
auth 权限认证
上一个状态的逻辑还没有 return,所以还会继续在 syncWithMaster() 函数中进行处理
根据上述逻辑,如果发送了认证信息,那么我们就需要等待 主服务器的认证结果返回,当前状态为 REPL_STATE_RECEIVE_AUTH
,主服务器接到认证信息后,返回结果,从服务读取结果,继续处理
当前状态机状态为 REPL_STATE_SEND_PORT
ip/port 发送
上一个状态中我们看到状态机状态转为发送 端口信息的时候 没有 return,所以继续运算处理,发送 端口信息,状态码状态改为 REPL_STATE_RECEIVE_PORT
我们注意到上面发送端口信息的时候,使用了一个 REPLCONF listening-port [port]
,主服务器读取到信息以后,调用了 replconfCommand()
命令函数进行处理,提取端口信息,保存至 c->slave_listening_port
中,然后响应 +OK\r\n
成功状态信息,通过 fd 句柄响应给 从服务器,从服务器读取主服务返回结果,状态码状态改为 REPL_STATE_SEND_IP
从服务器发送完端口号并且正确收到主服务器的回复后,状态机状态为 REPL_STATE_SEND_IP
,紧接着 syncWithMaster() 函数执行发送IP的代码,发送IP和发送端口号过程几乎一致,发送完成以后状态机状态修改为 REPL_STATE_RECEIVE_IP
发送 IP 地址信息给主服务器, 我们可以看到,跟发送 port 的时候一样逻辑,主服务器接收到了信息,调用 replconfCommand()
命令函数进行处理,提取 IP 信息,保存至 c->slave_ip
中,从服务器读取主服务返回结果,状态码状态改为 REPL_STATE_SEND_CAPA
当然如果从服务器里面没有保存需要传递给主服务的 IP 地址的时候,就直接跳转到下一个步骤
当前状态机状态为 REPL_STATE_SEND_CAPA
,发送 从服务器的数据处理能力(capability),当前版本通常代表为 能否解析出 RDB 文件的 EOF流 格式
capability 能力发送
发送 capability 能力(当前版本通常代表为 能否解析出 RDB 文件的 EOF流 格式)跟上面的 port 信息基本一致,主要为 发送 从服务的数据处理能力,而服务器接收到信息以后使用 replconfCommand() 函数进行提取,赋值 c->slave_capa
,然后回复 +OK\r\n
,从服务状态接收回复信息后状态机变更为 REPL_STATE_SEND_PSYNC
|
|
当前状态机状态为 REPL_STATE_SEND_PSYNC
发送同步信息给主服务器
psync 响应
从这里开始跟前面的各个状态机状态就不太一致了,而且比较绕 就体现在这个步骤
状态机处于 REPL_STATE_SEND_PSYNC
状态的时候,从服务器会尝试向主服务器发起同步请求,一般请求分为 全量同步 和 增量同步;首次一般都是 全量同步,而全量后的普通同步 和 由于网络等原因断开后重连的同步 会选择增量同步
秉着高效的原理,Redis v2.8 版本的时候引入了 PSYNC,主从可以增量同步,这样当主从链接短时间中断恢复后,无需做完整的RDB完全同步这种重量级操作
所以 从服务器在连接上主服务器后 首先尝试的是 增量同步,因为 有可能是 断线后重连的情况,如果判断发现不是重连的情况不能进行 增量同步,就进行一次全量同步
接状态机的上个步骤状态REPL_STATE_SEND_PSYNC
我们接下来看一下 尝试发送 全量同步的函数 slaveTryPartialResynchronization()
,需要注意的是 第二个参数传递的是0,代表 写命令,我们 摘取 写逻辑部分
上面逻辑可以看到 从服务器发送了一个 PSYNC ? -1
信息给 主服务器,代表申请全量同步,随后 主服务器调用 syncCommand()
命令函数进行处理,这个部分的逻辑稍微有点儿复杂,不像 之前几个步骤,交互比较单一,我们需要特地进行解析
然后紧接着开始 复制执行BGSAVE 操作,也就印证我们最上方流程图里面的函数 startBgsaveForReplication()
我们看到上面逻辑,rdbSaveBackground()
函数 RDB 文件保存完毕,而且会调用 replicationSetupSlaveForFullResync()
函数给从服务器发送 +FULLRESYNC
命令,而且要发送 主服务器的运行 id server.runid
和 主服务器的全局复制偏移量 server.master_repl_offset
至此主服务器端完成了 RDB 保存以及响应 从服务器完成信息,随后就轮到主服务器 将 缓存的数据发送给从服务器
发送缓冲区数据
上个步骤我们看到 主服务已经完成了 RDB 保存而且响应了从服务器,那么又到了状态机逻辑部分了
这次调用 slaveTryPartialResynchronization()
函数后面的参数传递的为 1,走读逻辑
返回 PSYNC_FULLRESYNC 状态后 syncWithMaster() 继续往下走
从上面看出主服务器发送 RDB 文件后 从服务器触发可读事件执行 readSyncBulkPayload()
函数,这个函数就会把主服务器发来的数据读到一个缓冲区中,然后将缓冲区的数据写到刚才打开的临时文件中,接着要载入到从服务器的数据库中,最后同步到磁盘中
但是主服务器同步 RDB 文件的逻辑是肿么样的呢,除了 最上面的流程图上,我们还没有具体提到过;现在主要难点在于 主服务器如何触发事件将 RDB 文件发往 从服务器,我们从 时间事件 逻辑上找到了对应逻辑,serverCron()->backgroundSaveDoneHandler()->backgroundSaveDoneHandlerDisk()->updateSlavesWaitingBgsave()
调用完成以后,至此 RDB 文件保存完毕,接下来 调用 sendBulkToSlave()
函数将 RDB 文件写入到 fd 中,触发从服务器的读事件,从服务器调用 readSyncBulkPayload()
函数,来将 RDB文件 的数据载入数据库中,我们看一下 sendBulkToSlave()
都干了啥
写完成后,又一次取消监听文件可写事件,等待下一次发送缓冲区数据时再监听触发,并且调用 putSlaveOnline()
函数将从服务器 client 的复制状态设置为 SLAVE_STATE_ONLINE
,表示已经发送 RDB文件 完毕,发送缓存更新
从服务器可读以后,触发函数 readSyncBulkPayload()
进行 RDB 文件读取
至此全量同步完成 .
命令传播机制
那么以后主服务器有写命令执行,主从的数据又不一致了,那么就需要一个 命令传播机制,传播的时候会通过 propagate()
函数调用 replicationFeedSlaves()
,会将执行的命令以协议的传输格式写到从服务器 client 的输出缓冲区中,这就是为什么主服务器会将从服务器 client 的输出缓冲区发送到从服务器,也会添加到 server.repl_backlog
中
增量同步
之前上次我们也提到了,中途断开的情况,比如 突然断电等,连接上以后,这个时候就可以尝试增量同步,如果可以增量,那么就不用动用全量这个重型操作
部分重同步在复制的过程中,相当于之前发送 PSYNC 命令的部分,其他所有的部分都要进行,他只是主服务器回复从服务器的命令不同,回复 +CONTINUE
则执行部分重同步,回复 +FULLRESYNC
则执行全量同步
那么主服务器如何发现从服务器掉线了呢,那这里就依赖与其他数据库主从一样的 心跳机制,在主从服务器建立连接后,他们之间都维护长连接并彼此发送心跳命令,其实主从服务器彼此都有心跳机制,各自模拟成对方的客户端进行通信;
- 主服务器心跳机制的频率由
server.repl_ping_slave_period 默认10秒
全局变量 控制 - 从服务每个 1秒 发送
REPLCONF ACK <offset>
信息,像主服务器报告当前复制偏移量
另外,我们之前反复提到了一个 全局变量 server.repl_backlog
,代表 复制积压缓冲区
;复制积压缓冲区 是一个大小为1M的循环队列,主服务器在命令传播时,不仅会将命令发送给所有的从服务器,还会将命令写入 复制积压缓冲区中,也就是说,复制积压缓冲区 最多可以备份1M大小的数据,如果主从服务器断线时间过长,复制积压缓冲区 的数据会被新数据覆盖,那么当从主从中断连接起,主服务器接收到的数据超过1M大小,那么从服务器就无法进行部分重同步,只能进行全量复制
刚才我们介绍的 syncCommand() 函数中,调用 masterTryPartialResynchronization()
函数会进行尝试部分重同步,在我们之前分析的第一次全量同步时,该函数会执行失败,然后返回 syncCommand()函数执行全量同步,而在进行恢复主从连接后,则会进行部分重同步
本文作者: wettper
本文链接: http://www.web-lovers.com/redis-source-replication.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!