<返回更多

Redis如何高效可靠地实现主从复制?终于有人讲明白了

2019-10-28    
加入收藏
导读:本文内容主要包括redis主从复制功能的概述、作用和方案实施。

作者:李乐

来源:华章科技

Redis如何高效可靠地实现主从复制?终于有人讲明白了

 

Redis支持主从复制功能,用户可以通过执行slaveof命令或者在配置文件中设置slaveof选项来开启复制功能。例如,现在有两台服务器—127.0.0.1:6379和127.0.0.1:7000,向服务器127.0.0.1:6379发送下面命令:

127.0.0.1:6379>slaveof 127.0.0.1 7000
OK

此时服务器127.0.0.1:6379会成为服务器127.0.0.1:7000的从服务器(slaver),服务器127.0.0.1:7000会成为服务器127.0.0.1:6379的主服务器(master);通过复制功能,从服务器127.0.0.1:6379的数据可以和主服务器127.0.0.1:7000的数据保持同步。

为什么需要主从复制功能呢?

简单来说,主从复制功能主要有以下两点作用。

  1. 读写分离,单台服务器能支撑的QPS是有上限的,我们可以部署一台主服务器、多台从服务器,主服务器只处理写请求,从服务器通过复制功能同步主服务器数据,只处理读请求,以此提升Redis服务能力;另外我们还可以通过复制功能来让主服务器免于执行持久化操作:只要关闭主服务器的持久化功能,然后由从服务器去执行持久化操作即可。
  2. 数据容灾,任何服务器都有宕机的可能,我们同样可以通过主从复制功能提升Redis服务的可靠性;由于从服务器与主服务器数据保持同步,一旦主服务器宕机,可以立即将请求切换到从服务器,从而避免Redis服务中断。

对于本例来说slaveof命令的主要流程如下。

  1. 从服务器127.0.0.1:6379向主服务器127.0.0.1:7000发送sync命令,请求同步数据。
  2. 主服务器127.0.0.1:7000接收到sync命令请求,开始执行bgsave命令持久化数据到RDB文件,并且在持久化数据期间会将所有新执行的写入命令都保存到一个缓冲区。
  3. 当持久化数据执行完毕后,主服务器127.0.0.1:7000将该RDB文件发送给从服务器127.0.0.1:6379,从服务器接收该RDB文件,并将文件中的数据加载到内存
  4. 主服务器127.0.0.1:7000将缓冲区中的命令请求发送给从服务器127.0.0.1:6379。
  5. 每当主服务器127.0.0.1:7000接收到写命令请求时,都会将该命令请求按照Redis协议格式发送给从服务器127.0.0.1:6379,从服务器接收并处理主服务器发送过来的命令请求。

上述流程已经可以完成主从复制基本功能了,Redis 2.8以前就是这样实现的,但是注意到步骤2中存在持久化操作(bgsave),而这是一个非常耗费资源的操作。

Redis如何高效可靠地实现主从复制?终于有人讲明白了

 

举一个简单的例子。

主服务器和从服务器之间是通过TCP长连接交互数据的,假设某个时刻主从服务器之间的网络连接发生故障且时间比较短,在此期间主服务器只执行了很少的写命令请求。

待主从服务器之间的网络连接恢复后,从服务器会重新连接到主服务器,并发送sync命令请求同步数据。这时候主服务器还需要执行持久化操作吗?显然是可以避免的,只要主服务器能够缓存连接故障期间执行的写命令即可。

Redis 2.8提出了新的主从复制解决方案。从服务器会记录已经从主服务器接收到的数据量(复制偏移量);而主服务器会维护一个复制缓冲区,记录自己已执行且待发送给从服务器的命令请求,同时还需要记录复制缓冲区第一个字节的复制偏移量。从服务器请求同步主服务器的命令也改为了psync。

当从服务器连接到主服务器时,会向主服务器发送psync命令请求同步数据,同时告诉主服务器自己已经接收到的复制偏移量,主服务器判断该复制偏移量是否还包含在复制缓冲区;如果包含,则不需要执行持久化操作,直接向从服务器发送复制缓冲区中命令请求即可,这称为部分重同步;如果不包含,则需要执行持久化操作,同时将所有新执行的写命令缓存在复制缓冲区中,并重置复制缓冲区第一个字节的复制偏移量,这称为完整重同步。

详情可参照Redis源码,方法masterTryPartialResynchronization用于判断是否可以执行部分重同步;方法replicationFeedSlaves用于向所有从服务器广播命令。

void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc){
 if (server.repl_backlog) {
 //将当前命令请求添加到复制缓冲区
 }
 while((ln = listNext(&li))) {
 //向所有从服务器同步命令请求
 }
}

另外,从服务器也会通过命令“REPLCONF ACK < reploff >”定时向主服务器汇报自己的复制偏移量;据此,主服务器一来可以检测从服务器是否有效,二来可以重新广播丢失的命令请求。

另外需要注意的是,每台Redis服务器都有一个运行ID,从服务器每次发送psync请求同步数据时,会携带自己需要同步主服务器的运行ID。

主服务器接收到psync命令时,需要判断命令参数运行ID与自己的运行ID是否相等,只有相等才有可能执行部分重同步。而当从服务器首次请求主服务器同步数据时,从服务器显然是不知道主服务器的运行ID,此时运行ID以“?”填充,同时复制偏移量初始化为-1。

从上面的分析我们可以得到psync命令格式为“psync <MASTER_RUN_ID> <OFFSET>”,主从复制初始化流程如图1所示。

从图1可以看到,当主服务器判断可以执行部分重同步时向从服务器返回“+CON-TINUE”;需要执行完整重同步时向从服务器返回“+FULLRESYNC RUN_ID OFFSET”,其中RUN_ID为主服务器自己的运行ID,OFFSET为复制偏移量。

Redis如何高效可靠地实现主从复制?终于有人讲明白了

▲图1 主从复制初始化流程图

可以看到执行部分重同步的要求还是比较严格的:

  1. RUN_ID必须相等;
  2. 复制偏移量必须包含在复制缓冲区中。

然而在生产环境中,经常会出现以下两种情况:

这时显然是无法执行部分重同步的,而这两种情况又很常见,因此Redis 4.0针对主从复制又提出了两点优化,提出了psync2协议。

Redis服务器关闭时,将主从复制信息(复制的主服务器RUN_ID与复制偏移量)作为辅助字段存储在RDB文件中;Redis服务器启动加载RDB文件时,恢复主从复制信息,重新同步主服务器时携带。持久化主从复制信息代码如下:

if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
 == -1) return -1;
 if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset)
 == -1) return -1;

当主服务器发生故障,自己成为新的主服务器时,使用变量server.replid2和server.second_replid_offset存储之前主服务器的运行ID与复制偏移量:

void shiftReplicationId(void) {
 memcpy(server.replid2,server.replid,sizeof(server.replid));
 server.second_replid_offset = server.master_repl_offset+1;
 changeReplicationId();
}

另外判断是否能执行部分重同步的条件也改变为:

if (strcasecmp(master_replid, server.replid) &&
 (strcasecmp(master_replid, server.replid2) ||
 psync_offset > server.second_replid_offset))
{
 goto need_full_resync;
}

假设m为主服务器(运行ID为M_ID),A、B和C为三个从服务器;某一时刻主服务器m发生故障,从服务器A升级为主服务器(同时会记录replid2=M_ID),从服务器B和C重新向主服务器A发送“psync M_ID psync_offset”请求;显然根据上面条件,只要psync_offset满足条件,就可以执行部分重同步。

关于作者:李乐,好未来php工程师,西安电子科技大学硕士,乐于钻研技术与源码研究,对Redis和Nginx有较深理解。合著书籍《Redis 5设计与源码分析》。

本文摘编自《Redis 5设计与源码分析》,经出版方授权发布。

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>