Redis源码阅读(二)高可用设计-复制

复制的概念:Redis的复制简单理解就是一个Redis服务器从另一台Redis服务器复制所有的Redis数据库数据,能保持两台Redis服务器的数据库数据一致。

使用场景:复制机制很实用,在客户端并发访问量很大,单台Redis扛不住的情况下,可以部署多台Redis复制相同的数据,共同对外提供服务,提高Redis并发访问处理能力。当然这种通过复制方式部署多台Redis以提高并发处理能力的方式只适用于客户端大部分访问为读数据请求的场景。此外,Redis从2.8版本以后支持的Sentinel高可用机制(热备)也需要依赖复制功能来同步几台机器的Redis数据。

个人在阅读源码时比较关心的几个复制问题如下:

  1. Redis复制时,机器之间传递的数据结构是怎样的?
  2. 为保证各节点的数据一致,应该要经常进行数据同步,Redis数据量很大时,每次同步都全量肯定不现实,那Redis中的增量同步是如何实现的?
  3. 如果从节点断线后重连到主节点,是否会触发全量同步,在网络状况不好的情况下可能会出现频繁的重连,如果每次重连再进行全量同步会加重网络负担,Redis是否在这种情况下做了增量同步?

接下来会介绍Redis复制的主要流程,并针对这几个问题来看Redis源码的实现。

1. 初次复制的流程

  1. 客户端发slaveof命令给从服务器,给从服务器指定需要复制的主服务器ip和端口
  2. 从服务器收到slaveof命令后,创建到主服务器的连接并发送PING来确认连接有效
  3. 从服务器接收到主服务器返回PONG,确认可以和主服务器通信后,向主服务器发送”PSYNC ? -1”命令,申请执行初次的全量同步
  4. 主服务器收到PSYNC,开始在内部执行BGSAVE命令,将数据库写到RDB文件中;注意,在执行BGSAVE期间,主服务器还可接收客户端的写命令,这些命令会向缓冲区中写一份,确保在执行BGSAVE期间发生的写命令也可以同步给从服务器
  5. 主服务器执行BGSAVE后,发送RDB文件给从服务器,从服务器导入RDB文件
  6. 发送RDB完成后,主服务器将缓冲区中的写命令也发送给从服务器,从服务器执行这些写命令

备注:这里介绍的是主要流程,忽略了身份认证的步骤

复制时机器间传递的数据结构是RDB文件;RDB是Redis持久化的一种方式;RDB文件中记录的是Redis数据库中所有的key-value对;这种持久方式在任意时刻开启都能保证持久化的数据是Redis中完整数据,与此相对的AOF持久化方式则是将所有写命令计入到文件中,这种文件保留的数据只是从开启AOF的那一刻开始,开启之前的数据是无法保存的,所以复制机制没有使用AOF文件。

源码中,服务器处理slaveof命令的处理器是slaveofCommand()函数

void slaveofCommand(redisClient *c) { //想让一个节点A通过slaveof成为另一个节点B的slave,则必须是B在单机模式下

    // .......
/* Check if we are already attached to the specified slave */
// 检查输入的 host 和 port 是否服务器目前的主服务器
// 如果是的话,向客户端返回 +OK ,不做其他动作
//说明之前已经slaveof ip port过,这次又执行该命令,说明之前已经连接建立成功过了
if (server.masterhost && !strcasecmp(server.masterhost,c->argv[]->ptr)
&& server.masterport == port) {
redisLog(REDIS_NOTICE,"SLAVE OF would result into synchronization with the master we are already connected with. No operation performed.");
addReplySds(c,sdsnew("+OK Already connected to specified master\r\n"));//向从服务器的redis-cli客户端发送该命令
return;
} //第一次执行slaveof ip port,新的ip或者port
/* There was no previous master or the user specified a different one,
* we can continue. */
// 没有前任主服务器,或者客户端指定了新的主服务器
// 开始执行复制操作
replicationSetMaster(c->argv[]->ptr, port);
redisLog(REDIS_NOTICE,"SLAVE OF %s:%d enabled (user request)",
server.masterhost, server.masterport);
}
addReply(c,shared.ok);
}

在该函数中通过replicationSetMaster()函数设置要连接的主服务器的IP和端口,并将服务器连接状态server.repl_state置为REDIS_REPL_CONNECT。从而会触发connectWithMaster()函数将连接状态置为REDIS_REPL_CONNECTING,继而触发slaveTryPartialResynchronization()函数来发送PSYNC命令。connectWithMaster函数创建连接套接字专门用于主备数据同步

// 以非阻塞方式连接主服务器
int connectWithMaster(void) {
int fd;
// 连接主服务器
fd = anetTcpNonBlockConnect(NULL,server.masterhost,server.masterport); if (fd == -) {
redisLog(REDIS_WARNING,"Unable to connect to MASTER: %s", strerror(errno));
return REDIS_ERR;
} // 监听主服务器 fd 的读和写事件,并绑定文件事件处理器
if (aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) ==
AE_ERR)
{
close(fd);
redisLog(REDIS_WARNING,"Can't create readable event for SYNC");
return REDIS_ERR;
} // 初始化统计变量
server.repl_transfer_lastio = server.unixtime;
server.repl_transfer_s = fd; // 将状态改为已连接
server.repl_state = REDIS_REPL_CONNECTING;
return REDIS_OK; }

看下slaveTryPartialResynchronization()的具体实现实现

int slaveTryPartialResynchronization(int fd) {
char *psync_runid;
char psync_offset[];
sds reply;
server.repl_master_initial_offset = -;
//例如从服务器之前和主服务器连接上,并同步了数据,中途端了,则连接断了后会在replicationCacheMaster把server.cached_master = server.master;
//表示之前有连接到过服务器
if (server.cached_master) {
// 缓存存在,尝试部分重同步
// 命令为 "PSYNC <master_run_id> <repl_offset>"
psync_runid = server.cached_master->replrunid;
snprintf(psync_offset,sizeof(psync_offset),"%lld", server.cached_master->reploff+);
redisLog(REDIS_NOTICE,"Trying a partial resynchronization (request %s:%s).", psync_runid, psync_offset);
} else {
// 缓存不存在
// 发送 "PSYNC ? -1" ,要求完整重同步
redisLog(REDIS_NOTICE,"Partial resynchronization not possible (no cached master)");
psync_runid = "?";
memcpy(psync_offset,"-1",); //
} /* Issue the PSYNC command */
// 向主服务器发送 PSYNC 命令
reply = sendSynchronousCommand(fd,"PSYNC",psync_runid,psync_offset,NULL);
// 接收到 FULLRESYNC ,进行 full-resync
if (!strncmp(reply,"+FULLRESYNC",)) {
char *runid = NULL, *offset = NULL;
/* FULL RESYNC, parse the reply in order to extract the run id
* and the replication offset. */
// 分析并记录主服务器的 run id
runid = strchr(reply,' '); if (runid) {
runid++;
offset = strchr(runid,' ');
if (offset) offset++;
}
// 检查 run id 的合法性
if (!runid || !offset || (offset-runid-) != REDIS_RUN_ID_SIZE) {
redisLog(REDIS_WARNING, "Master replied with wrong +FULLRESYNC syntax.");
// 主服务器支持 PSYNC ,但是却发来了异常的 run id
// 只好将 run id 设为 0 ,让下次 PSYNC 时失败
memset(server.repl_master_runid,,REDIS_RUN_ID_SIZE+);
} else {
// 保存 run id
memcpy(server.repl_master_runid, runid, offset-runid-);
server.repl_master_runid[REDIS_RUN_ID_SIZE] = '\0';
// 以及 initial offset
server.repl_master_initial_offset = strtoll(offset,NULL,);
// 打印日志,这是一个 FULL resync
redisLog(REDIS_NOTICE,"Full resync from master: %s:%lld",
server.repl_master_runid,
server.repl_master_initial_offset);
}
/* We are going to full resync, discard the cached master structure. */
// 要开始完整重同步,缓存中的 master 已经没用了,清除它
replicationDiscardCachedMaster();
sdsfree(reply);
// 返回状态
return PSYNC_FULLRESYNC;
} // 接收到 CONTINUE ,进行 partial resync
if (!strncmp(reply,"+CONTINUE",)) {
/* Partial resync was accepted, set the replication state accordingly */
redisLog(REDIS_NOTICE, "Successful partial resynchronization with master.");
sdsfree(reply);
// 将缓存中的 master 设为当前 master
replicationResurrectCachedMaster(fd); // 返回状态
return PSYNC_CONTINUE;
} // 接收到错误?
if (strncmp(reply,"-ERR",)) {
/* If it's not an error, log the unexpected event. */
redisLog(REDIS_WARNING,
"Unexpected reply to PSYNC from master: %s", reply);
} else {
redisLog(REDIS_NOTICE,
"Master does not support PSYNC or is in "
"error state (reply: %s)", reply);
} //如果从服务器发送PSYNC上去后或者其他原因返回错误,然后在serverCron中重新建立连接再次从新走一次PSYNC或者SYNC流程
sdsfree(reply);
replicationDiscardCachedMaster(); // 主服务器不支持 PSYNC
return PSYNC_NOT_SUPPORTED;
}

为什么设置了server.repl_state状态,就可以触发Redis服务器进行各种相应操作呢? 很明显Redis内部有定时轮询server.repl_state的逻辑存在。实现定时轮询其实就是Redis时间事件的一个实际的应用,感兴趣可以看下源码中ServerCron()函数的处理。

2. 增量同步

增量同步的两个常用场景:

  1. 初次全量同步之后,主服务器在收到写命令时,会将写命令传播给从服务器,而不是全量发送给从服务器(从服务器只读,不处理非主节点发来的写命令)
  2. 从服务器断线重连之后,从服务器与主服务器之间的数据差异只有断线期间主服务器接收到的写命令所执行的对数据的修改部分,此时只需要增量同步这部分数据即可。

写命令的传播比较容易理解,主服务器收到写命令,可能导致主从服务器数据不一致,那么主服务器在执行命令之外还会转发这个写命令给从服务器,让从服务器也执行相同的写命令,做到数据的一致。

另一个场景断线重连,Redis是由PSYNC命令来实现的,该功能也是在2.8版本后加入到Redis中的,老版本不兼容该命令。PSYNC的实现依赖于两个重要的概念:复制偏移量和复制积压缓冲区。

复制偏移量,指主服务器传播给从服务器的写命令的偏移量;主服务器和从服务器分别维护各自的偏移量。主服务器每传播N字节的写命令,偏移量就自增N;从服务器在接收到N字节写命令后也会将偏移量增N,这样就能保证主从节点的偏移量始终保持一致。(从服务器初始的复制偏移量是从哪里获取的呢?很明显应该是在初次全量同步时将主服务器的复制偏移量也同步给从服务器)

用示例图片来表示复制偏移量的变化

图1 主从服务器复制偏移量一致

开始阶段主从服务器的偏移量均为10086

图2 客户端发来写命令后,偏移量的变化

主服务器执行写命令后更新了自身的偏移量到10101;同时传播写命令给从服务器,从服务器接收命令后也会更新自己的同步偏移量使得主从保持一致。

有了这个概念,一旦从服务器断线重连时就可以在同步命令中带上偏移量,主服务器根据偏移量是否一致来判断是否需要增量同步,以及增量哪些写命令。偏移量在服务器启动时初始化为0,后面可以一直递增不用考虑循环,因为偏移量是long long类型的,上限很大很大(9223372036854775807),基本不会撑爆。

那需要增量的写命令时保存在哪里的呢?这里就引入了另一个概念:复制积压缓冲区(server.backlog)

复制积压缓冲区,在Redis中是一个先进先出的定长队列,默认长度是1M。数据结构就是一个带标签的字符串数组。每次写命令都会直接从index开始进行拷贝, 达到缓冲区尾部就从缓冲区头开始拷贝,对原有数据直接覆盖。

3. 心跳检测

心跳检测是由从服务器发起的,默认情况下是每隔1s发送一次;发送的命令是:

REPLCONF ACK <replication_offset>

Replication_offset是从服务器的复制偏移量;这样主服务器收到该命令后可以比较从服务器的Replication_offset是否和自身的一致,如果不一致这说明需要同步数据。主节点此时的处理方式类似于从服务器断线重连时的处理,将偏移量多出来的写命令发送给从节点执行,保持两者的数据一致。

由此可见心跳检测的一个重要功能就是防止命令传播过程中有消息未送达从服务器而导致的主从数据不一致。这也是2.8版本以后新增的异常处理逻辑,很大的提高了复制机制的可用性。

4. 总结

以上就是复制机制的一些关键点,总结一下主要包括以下几点:

  1. 复制机制下,只有主服务器才可以接收普通客户的写命令,从服务器只能接收普通客户端的读命令;且主服务器每接收到一个写命令,都会讲该命令传播给从服务器,保证主从数据一致
  2. Redis使用复制偏移量和复制积压缓冲区来实现PSYNC的增量复制,在从服务器断线重连后避免了进行全同步,效率提升明显。
  3. 心跳检测可以检查主从服务器数据是否一致,利用的也是从服务器发送给主服务器的复制偏移量,主服务器可以尽早的发现传播过程中丢掉的命令,并发起补充的命令传播。

Redis自带的高可用解决方案Sentinel也是以复制机制为基础的,增加了Sentinel监控节点,可以及时检查到失效的主服务器,在从服务器中选出新的主服务器。当原有的主服务器恢复后会成为从服务器加进来。

Redis源码阅读(二)高可用设计——复制的更多相关文章

  1. Redis源码阅读(五)集群-故障迁移(上)

    Redis源码阅读(五)集群-故障迁移(上) 故障迁移是集群非常重要的功能:直白的说就是在集群中部分节点失效时,能将失效节点负责的键值对迁移到其他节点上,从而保证整个集群系统在部分节点失效后没有丢失数 ...

  2. Redis源码阅读(一)事件机制

    Redis源码阅读(一)事件机制 Redis作为一款NoSQL非关系内存数据库,具有很高的读写性能,且原生支持的数据类型丰富,被广泛的作为缓存.分布式数据库.消息队列等应用.此外Redis还有许多高可 ...

  3. Redis源码阅读(六)集群-故障迁移(下)

    Redis源码阅读(六)集群-故障迁移(下) 最近私人的事情比较多,没有抽出时间来整理博客.书接上文,上一篇里总结了Redis故障迁移的几个关键点,以及Redis中故障检测的实现.本篇主要介绍集群检测 ...

  4. Redis源码阅读(四)集群-请求分配

    Redis源码阅读(四)集群-请求分配 集群搭建好之后,用户发送的命令请求可以被分配到不同的节点去处理.那Redis对命令请求分配的依据是什么?如果节点数量有变动,命令又是如何重新分配的,重分配的过程 ...

  5. Redis源码阅读(三)集群-连接初始化

    Redis源码阅读(三)集群-连接建立 对于并发请求很高的生产环境,单个Redis满足不了性能要求,通常都会配置Redis集群来提高服务性能.3.0之后的Redis支持了集群模式. Redis官方提供 ...

  6. Redis源码阅读-Adlist双向链表

    Redis源码阅读-链表部分- 链表数据结构在Adlist.h   Adlist.c Redis的链表是双向链表,内部定义了一个迭代器. 双向链表的函数主要是链表创建.删除.节点插入.头插入.尾插入. ...

  7. [Redis源码阅读]sds字符串实现

    初衷 从开始工作就开始使用Redis,也有一段时间了,但都只是停留在使用阶段,没有往更深的角度探索,每次想读源码都止步在阅读书籍上,因为看完书很快又忘了,这次逼自己先读代码.因为个人觉得写作需要阅读文 ...

  8. Redis源码阅读一:简单动态字符串SDS

    源码阅读基于Redis4.0.9 SDS介绍 redis 127.0.0.1:6379> SET dbname redis OK redis 127.0.0.1:6379> GET dbn ...

  9. Redis源码阅读笔记(1)——简单动态字符串sds实现原理

    首先,sds即simple dynamic string,redis实现这个的时候使用了一个技巧,并且C99将其收录为标准,即柔性数组成员(flexible array member),参考资料见这里 ...

随机推荐

  1. 解决Win10无法安装.Net Framework 3.5,错误代码0x800F081F

    重新安装了一遍Win10,但是不知怎的无法安装.net framework 3.5,即便是下载离线安装包也没法用. 网上有人说需要使用win10的ISO文件,个人感觉太麻烦,在这里分享一个很方便的操作 ...

  2. mariadb使用\s查看用户权限

    今天出现一个问题就是:给zabbix用户赋予权限 语句如下: grant all on zabbix.* to 'zabbix'@'%' identified by 'zabbix' 按照这样的说法应 ...

  3. 邮件客户端修改密码—OWA

    邮件客户端修改密码—OWA 1.登录OWA 2.输入用户名 3.点击选项 4.更改密码

  4. IE9 添加事件DOMContentLoaded,方法addEventListener

    IE9 新添加 事件DOMContentLoaded,方法addEventListener

  5. 在阿里云Windows Server 上部署ASP .NET CORE2.0项目

    近期使用ASP.NET Core2.0对博客进行了重写,在部署到服务器时遇到了一些问题,来记录一下留用. 配置环境 安装 .Net Framework3.5 在IIS管理器上直接开启,这里总是失败,上 ...

  6. 使用ubuntu过程中遇到的问题汇总

    使用ubuntu过程中遇到的问题汇总 1.使用图形界面设置免密码登录之后,改回密码登陆失效 解决方案: https://askubuntu.com/questions/211084/how-do-i- ...

  7. PyQt5--QPixmap

    # -*- coding:utf-8 -*- ''' Created on Sep 20, 2018 @author: SaShuangYiBing Comment: ''' import sys f ...

  8. css3自定义滚动条背景透明

    .editor{ overflow:hidden; height:640px; padding:0 45px; border: 0 none; outline: none; } .editor::-w ...

  9. 快速数论变换(NTT)

    刚学完FFT,干脆把NTT也学了算了 (一)预备知识 关于原根,这里说得蛮详细的百度百科 为什么使用原根呢?为什么原根可以替代\(\omega_{n}\)呢?想知道为什么就看here NTT用到的各种 ...

  10. python+selenium+chrome driver 64位环境配置

    新换了电脑,重新配置下环境,备忘下 1.python2安装 https://www.python.org/downloads/release/python-2714/ 下载64位Windows x86 ...