UDN-企业互联网技术人气社区

板块导航

浏览  : 1271
回复  : 0

[讨论交流] Redis复制实现原理

[复制链接]
巡山霉少女的头像 楼主
发表于 2016-6-4 19:24:48 | 显示全部楼层 |阅读模式
  摘要

  我的前一篇文章《浅析Redis复制》已经介绍了Redis复制相关特性,这篇文章主要在理解Redis复制相关源码的基础之上介绍Redis复制的实现原理。

  Redis复制实现原理

  应用场景化

  为了更好地表达与理解,我们先举个实际应用场景例子来看看Redis复制是怎么工作的,我们先启动一台master:

  
  1. $ ./redis-server --port 8000
复制代码


  然后启动一个redis客户端和上面那台监听8000端口的Redis实例连接:

  
  1. $ ./redis-cli -p 8000
复制代码


  我们向redis写一个数据:

  
  1. 127.0.0.1:8000> set msg doni

  2.   OK

  3.   127.0.0.1:8000> get msg

  4.   "doni"
复制代码

  于是我们可以假设以下场景:

  
  1. 我们有一台master实例master,master已经处于正常工作的状态,接受读写请求,这个时候由于单台机器的压力过大,我们想再启动一个Redis实例来分担master的读压力,假设我们新启动的这个实例叫slave。

  2.   已知M1的IP为127.0.0.1,端口为:8000
复制代码


  首先我们先启动redis实例,同时启动一个客户端连接这个实例:

  
  1. $ ./redis-server --port 8001

  2.   $ ./redis-cli -p 8001
复制代码


  这个时候slave是没有数据的:

  
  1. 127.0.0.1:8001> get msg

  2.   (nil)
复制代码


  我们可以用下面命令来让slave和master进行复制:

  
  1. 127.0.0.1:8001> slaveof 127.0.0.1 8000
复制代码


  于是,slave就获得了master上写的数据了:

  
  1. 127.0.0.1:8001> get msg

  2.   "doni"
复制代码


  上面的例子和很直观也很简单,下面我们就在脑海中缓存这个应用场景,来看看redis是如何实现复制的。

  处理slaveof

  我们首先需要看看slave接收到客户端的slaveof命令是如何处理的,下面是slave接收到客户端的slaveof命令的处理流程图:

3.png

  slaveof命令处理流程图

  解释下上图,redis实例接收到客户端的slaveof命令后的处理流程大致如下:

  判断当前模式是否为cluster,如果是则不支持复制。

  判断命令是否为slave'of no one,如果是,这表明客户端把当前实例设置为master。

  如果客户端指定了host和port,则将host和port设置为当前的master信息。

  将当前实例的复制状态设置为REPL_STATE_CONNECT。

  除了上面的几个大步骤之外,在第二步和第三步之间还做了下面一些事情:

  释放之前被阻塞的客户端,这些通常是使用Redis阻塞列表而被阻塞的客户端。

  断开当前实例的所有slave。

  清除缓存的master信息。

  释放backlog,backlog是堆积环形缓冲区。

  取消正在进行的握手过程。

  上面就是Redis处理slaveof命令的大致流程,诶,好像并没有做关于复制的事情诶。别急,如果看过我的另一篇《Redis网络架构及单线程模型》文章的同学都应该知道redis的单线线程模型,这里slaveof命令处理关键的一步已经将当前redis实例的复制状态设置为了REPL_STATE_CONNECT状态,在redis的eventloop里面自然会对处于这个状态的redis实例进行处理。

  连接master

  复制异步处理的触发逻辑一方面是I/O事件驱动的一部分,另一方面就是eventloop对时间事件处理的一部分,其实也是定时任务,redis定时任务最外面一层是serverCron方法,serverCron方法囊括了其他几乎所有定时处理逻辑的入口,可以列个不完全列表如下:

  过期key处理。

  软件watchdog。

  更新统计信息。

  rehash。

  触发备份RDB文件或者AOF重写逻辑。

  客户端超时处理。

  复制逻辑。

  ……

  我们这里只关心复制逻辑,调用代码如下:

  
  1. run_with_period(1000) replicationCron();
复制代码


  run_with_period方法是redis封装的一个帮助方法,最然serverCron的调用频率很高,是1毫秒一次:

  
  1. if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {

  2.   serverPanic("Can't create event loop timers.");

  3.   exit(1);

  4.   }
复制代码


  但是redis通过run_with_period实现了可以并不是每隔1毫秒必须要执行所有逻辑,run_with_period方法指定了具体的执行时间间隔。上面可以看出,redis主进程大概是1000毫秒也就是1秒钟执行一次replicationCron逻辑,replicationCron做什么事情呢,它做的事情很多,我们只关心本文的主线逻辑:

 
  1.  if (server.repl_state == REPL_STATE_CONNECT) {

  2.   if (connectWithMaster() == C_OK) {

  3.   serverLog(LL_NOTICE,"MASTER <-> SLAVE sync started");

  4.   }

  5.   }
复制代码


  如果当前实例的复制状态为REPL_STATE_CONNECT,我们就会尝试着连接刚才slaveof指定的master,连接master的主要实现在connectWithMaster里面,connectWithMaster的逻辑相对简单一些,大致做了下面三件事情:

  和指定的master建立连接,获取master的socket句柄,即fd。

  注册fd的读写事件,事件处理器为syncWithMaster。

  设置当前实例的复制状态为REPL_STATE_CONNECTING。

  握手机制

  上面已经注册了当前实例和master的读写I/O事件即事件处理器,由于I/O事件分离相关逻辑都由系统框架完成,也就是eventloop,因此我们可以直接看当前实例针对master连接的I/O处理实现部分,也就是syncWithMaster处理器。

  syncWithMaster主要实现了当前实例和master之间的握手协议,核心是赋值状态迁移,我们可以用下面一张图表示:

2.png

  slave和msater的握手机制

  上图为slave在syncWithMaster阶段做的事情,主要是和master进行握手,握手成功之后最后确定复制方案,中间涉及到迁移的状态集合如下:

 
  1.  #define REPL_STATE_CONNECTING 2 /* 等待和master连接 */

  2.   /* --- 握手状态开始 --- */

  3.   #define REPL_STATE_RECEIVE_PONG 3 /* 等待PING返回 */

  4.   #define REPL_STATE_SEND_AUTH 4 /* 发送认证消息 */

  5.   #define REPL_STATE_RECEIVE_AUTH 5 /* 等待认证回复 */

  6.   #define REPL_STATE_SEND_PORT 6 /* 发送REPLCONF信息,主要是当前实例监听端口 */

  7.   #define REPL_STATE_RECEIVE_PORT 7 /* 等待REPLCONF返回 */

  8.   #define REPL_STATE_SEND_CAPA 8 /* 发送REPLCONF capa */

  9.   #define REPL_STATE_RECEIVE_CAPA 9 /* 等待REPLCONF返回 */

  10.   #define REPL_STATE_SEND_PSYNC 10 /* 发送PSYNC */

  11.   #define REPL_STATE_RECEIVE_PSYNC 11 /* 等待PSYNC返回 */

  12.   /* --- 握手状态结束 --- */

  13.   #define REPL_STATE_TRANSFER 12 /* 正在从master接收RDB文件 */

  14.   当slave向master发送PSYNC命令之后,一般会得到三种回复,他们分别是:

  15.   +FULLRESYNC:不好意思,需要全量复制哦。

  16.   +CONTINUE:嘿嘿,可以进行增量同步。

  17.   -ERR:不好意思,目前master还不支持PSYNC。
复制代码


  当slave和master确定好复制方案之后,slave注册一个读取RDB文件的I/O事件处理器,事件处理器为readSyncBulkPayload,然后将状态设置为REPL_STATE_TRANSFER,这基本就是syncWithMaster的实现。

  处理PSYNC

  全量还是增量

  我们已经知道slave是怎么同master建立连接,怎么和master进行握手的了,那么master那边是什么情况呢,master在与slave握手之后,对于psync命令处理的秘密都在syncCommand方法里面,syncCommand方法实际包括两个命令处理的实现,一个是sync,一个是psync。我们继续看看,master对slave的psync的请求处理,如果当前请求不满足psync的条件,则需要进行全量复制,满足psync的条件有两个,一个是slave带来的runid是否为当前master的runid:

  
  1. if (strcasecmp(master_runid, server.runid)) {

  2.   //如果slave带来的runid“?”,说明slave想要强制走全量复制

  3.   if (master_runid[0] != '?') {

  4.   serverLog(LL_NOTICE,"Partial resynchronization not accepted: "

  5.   "Runid mismatch (Client asked for runid '%s', my runid is '%s')",

  6.   master_runid, server.runid);

  7.   } else {

  8.   serverLog(LL_NOTICE,"Full resync requested by slave %s",

  9.   replicationGetSlaveName(c));

  10.   }

  11.   goto need_full_resync;

  12.   }
复制代码


  如果不是,则需要全量同步。第二个条件即当前slave带来的复制offset,master在backlog中是否还能找到:

  
  1. if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) !=

  2.   C_OK) goto need_full_resync;

  3.   if (!server.repl_backlog ||

  4.   psync_offset < server.repl_backlog_off ||

  5.   psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))

  6.   {

  7.   if (psync_offset > server.master_repl_offset) {

  8.   //警告:slave带过来的offset不满足增量复制的条件

  9.   }

  10.   goto need_full_resync;

  11.   }
复制代码


  如果找不到,不好意思,还是需要全量复制的,如果两个条件都满足,master会告诉slave可以增量复制,回复+CONTINUE消息。

  复制是否正在进行

  如果在当前slave执行复制请求之前,恰好已经有其他的slave已经请求过了,且master这个时候正在进行子进程传输(包括RDB文件备份和socket传输),那么分下面两种情况处理:

  如果复制方式是RDB disk方式,则找到当前master状态为SLAVE_STATE_WAIT_BGSAVE_END的slave,复制这个slave的offset到当前slave的offset,这是为了当子进程完成RDB文件备份之后, 当前请求复制的slave可以和之前的slave一起进行master的复制操作。

  如果复制方式是Diskless方式,则当前进来的slave并不会向上面那个slave这么幸运了,因为基于socket的复制已经正在进行了,当前slave只能参与下一轮的子进程复制,且状态为SLAVE_STATE_WAIT_BGSAVE_START。

  如果没有子进程正在复制,这里针对RDB disk方式和diskless方式,又要分两种情况讨论:

  如果是RDB disk方式,则启动子进程进行RDB文件备份。

  如果是diskless方式,则等待一段时间,也是为了尽可能让后面的具有复制请求的slave一起进来,参与这一轮复制,复制开始由定时任务异步启动复制。

  子进程结束后处理

  RDB disk方式,当子进程备份RDB文件完毕,什么时候开始发送给slave的呢?diskless方式当子进程传输完毕,接下来又做什么呢?对于RDB disk的方式,这里涉及到一个I/O事件注册的过程,也是由serverCron驱动的,当子进程结束之后,主进程会得知,然后通过backgroundSaveDoneHandler处理器来进行处理,针对RDB disk类型和diskless类型的复制,处理逻辑是不一样的,我们分别来看看。

  RDB disk方式后处理

  对于RDB disk复制方式,后处理主要是注册向slave发送RDB文件的处理器sendBulkToSlave:

  
  1. if (aeCreateFileEvent(server.el, slave->fd,

  2.   AE_WRITABLE, sendBulkToSlave, slave) == AE_ERR) {

  3.   freeClient(slave);

  4.   continue;

  5.   }
复制代码


  然后RDB的文件发送由sendBulkToSlave处理器来完成,master对于RDB文件发送完毕之后会把slave的状态设置为:online。这里需要注意的是,在把slave设置为online状态之后会注册写处理器,将堆积在reply的数据发送给slave:

  
  1. if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE,

  2.   sendReplyToClient, slave) == AE_ERR) {

  3.   freeClient(slave);

  4.   return;

  5.   }
复制代码


  这部分的内容即为RDB文件开始备份到发送给slave结束这段时间的增量数据,因此需要注册I/O事件处理器,将这段时间累积的内容发送给slave,最终保持数据一致。

  diskless方式后处理

  diskless方式的后处理不同的是当子进程结束的时候,其实RDB文件已经传输完成了,而且其中做了些事情:

  当slave通过接受完RDB文件之后发送一个REPLCONF ACK给master。

  master接收到slave的REPLCONF ACK之后,开始将缓存的增量数据发送给slave。

  因此这里不会注册sendBulkToSlave处理器,只需要将slave设置为online即可。我们还可以发现不同的一点,对于累积部分的数据处理,RDB disk方式是由master主动发送给slave的,而对于diskless方式,master收到slave的REPLCONF ACK之后才会将累积的数据发送出去,这点有些不同。

  当子进程结束,后处理的过程中还要考虑到一种情况:

  
  1. 无论是RDB disk方式还是diskless方式,如果复制已经开始了,后来的slave需要同master复制,这部分的slave怎么办呢
复制代码


  怎么办呢,对于这类slave,slave的复制状态为SLAVE_STATE_WAIT_BGSAVE_START,语义上表示当前slave等待复制的开始,对于这种情况,Redis会直接启动子进程开始预备下一轮复制。

  RDB文件传输协议

  上面握手机制部分提到,当slave和master握手完毕之后注册了个readSyncBulkPayload处理器,用于读取master发送过来的RDB文件,RDB文件通过TCP连接传输,本质上是一个数据流,slave端是如何区分当前传输方式是RDB disk方式还是diskless方式的呢?实际上对于不同的复制方式,数据传输协议也是不同的,假设我们把这个长长的RDB文件流称为RDB文件报文,我们来看看两种方式的不同协议格式:

1.png

  RDB文件传输协议

  上面有两种报文协议,第一种为RDB disk方式的RDB文件报文传输协议,TCP流以"$"开始,然后紧跟着报文的长度,以换行符结束,这样slave客户端读取长度之后就知道要从TCP后续的流中读取多少内容就算结束了。第二种为diskless复制方式的RDB文件报文传输协议,以"$EOF:"开头,紧跟着40字节长度的随机16进制字符串,RDB文件结尾也紧跟着同样的40字节长度的随机16进制字符串。slave客户端分别由TCP数据流的头部来判断复制类型,然后根据不同的协议去解析RDB文件,当RDB文件传输完成之后,slave会将RDB文件保存在本地,然后载入,这样slave就基本和master保持同步了。

  总结

  本文主要在了解Redis复制源码的基础之上介绍Redis复制的实现原理及一些细节,希望对大家有帮助。

原文作者:佚名 来源:开发者头条
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部