HA(高可用性)是数据库的最基本需求,而主备冗余则是HA最基础的解决方案。Redis里面,主备通常使用Master-Replica来表述。

通用主备方案的实现,涉及到以下几个关键问题:

  • 主备感知:主备之间要建立某种关联(主备信令通道),并且要明确判断主备角色(主备裁决)。
  • 数据同步:

首先要明确一点,主备之间的数据同步不是必须的,这取决于系统的HA的要求有多高。这又可以分为:热备(hot-standby)、冷备。先说冷备,备份节点不需要同步任何主节点的数据,待备升主之后,新的主节点再想办法恢复数据。恢复数据的方法就要看应用场景了(比如通信设备软件会从上下游相邻节点恢复数据);再说热备,热备就是主节点要实时把数据同步给备节点,保证主备数据随时一致,备升主后可以直接使用。

衡量主备HA质量的一个关键指标,是主备倒换的黑障期,也就是备升主后要多长时间能接入新业务。很显然,热备远好于冷备。那为啥还要有冷备呢?实现简单啊。有些对HA要求低的场景,冷备也够用。另外,热备也是有黑障期的,但是黑障期很小,这个黑障期就是拼技术了,看谁能做得足够小。

  • 故障检测和倒换:快速发现故障,并触发主备倒换。故障恢复的时长取决于检测时间+倒换时间。

通用的主备方案必须解决以上三个问题,否则不能称之为完整的主备解决方案,但是这里面的水分很大,实现可能差别很大。

下面我们看Redis的主备方案(注意我下面讲的是纯主备部署,不考虑哨兵和cluster的情况)。

1、主备模型

Redis的主备模式是一主多备,并且备节点还可以有备节点,所谓的备节点级联,代码里面有些地方也叫slave chain。

这很好理解,和我们常见的主备模型没啥区别。

2、主备协商

Redis自己定义了一套私有的主备协商协议,如下:

当我们新增加一个备节点时(replicasof 127.0.0.1 7000),它会去主动发起和master(127.0.0.1 7000)的协商(代码中也叫握手- handshake流程)。上面的图就是完整的handshake流程,handshake结束后,备节点进入connected状态,可以正常从master接受实备的command。

handshake很简单,主要包括PING\PONG、REPLCONF配置port\IP\CAPA、psync、full-sync。比较复杂的psync和full-sync我们后面单独讲。

需要注意的是,redis协商并没有像cluster之间协商那样定义专门的MSG(meet),而是将自己看做master的一个client,向master发送command。比如上面的PING\REPLCONF\PSYNC\SYNC,其实都是真实的command。这是主备之间比较有意思的一点,包括后面我们讲的主备间数据同步,他们本质上都是在发送command,而不是协议消息。

Redis主备之间没有区分信令通道和数据通道,也就是说它的协商和数据同步都是在一个socket上面完成的。它的协商本质上就是发送command,而后面我们也会讲到它的数据同步本质上也是发送command。

3、实备-实时同步

如下图所示:

Redis的Slave是ReadOnly的,也就是说在Slave只能执行查询命令,不是Set\Del等。这很好理解,既然是备胎,那你就老老实实从Master同步数据就好了,如果主备都能改DB,那会出现大量冲突,咋同步呢?想想都乱。

所以,Slave的数据都来自于Master,从数据流来说,Master是生产者,slave是消费者。

Client在master修改了DB,master自己执行成功了,会把这个command再分发给所有的slave。slave把master当做client,执行command的流程,如果有级联的slave(sub-slave),再依次分发下去。这就是redis主备之间的实时同步流程。

对于一个通用的主备实时同步方案,我们需要考虑两部分数据,一部分是配置数据、一部分是状态数据。

配置数据好理解,状态数据是系统动态生成的,比如路由器的路由表信息。刚开始看redis代码的时候,我预先设想了各种复杂方案,最后发现redis只备份了配置数据,没有备份状态数据,所有状态数据都是每个节点自己生成的。很简单!

4、PSYNC - 部分同步

如果主备之间的链路出现闪断(断掉一会儿又连上),主备之间的数据就可能出现不同步(闪断期间用户在主上面执行了command)。所以,备需要向主请求一次同步。但这个场景下我们是没有必要全量同步的,在网络环境不好的情况下,这太耗费性能了。最理想的,就是把闪断期间的那部分修改同步给备,这就是PSYNC要达到的效果。

要实现部分同步,很显然我们必须有个地方把实时同步的状态记录下来,要知道哪些数据同步过了,哪些没有同步。

我能想到两种方案:

1、master里面对所有数据增加一个备份标记,已经备份的清空标记,被修改了还没有备份的置上标记。

2、主备都设置一个command的buf,每执行一个command,都写到这个buf里面。同时,我们就一个offset,来表明我当前执行到的位置。通过比较主备之间的offset,就能知道buf里面那些command没有同步。

redis采用第2种方案,它有一个buf,叫backlog-buf,是一个循环写入的buf。同时,它还记录了一系列的值。master和slave节点都存在这样一套机制。它们共同实现一个目的,就是如果如果Master的有些command没有备份到slave,我们可以识别出来,并通过psync实现同步。下面我们详细描述这套机制:

1、系统分配了一个backlog-buf,它的长度是repl_backlog_size,可以通过conf文件配置大小。由于buf不可能无限大,所以当buf写满后,会循环从头开始写入,依次覆盖原来的数据。这就是所谓的循环buf。

2、系统定义了几个值,用于描述这个buf的状态(注意,网上很少有文章对这几个状态值进行介绍的,有也是直接翻译了源码中的注释,根本搞不清是啥意思。这几个状态值是关键。)

repl_backlog_idx:这个很好理解,它指向buf中最新写入的位置。如果我们要写入一个command,就是从它指向的位置写入。当达到repl_backlog_size时,它又指向0。

下面三个值配合使用:

master_repl_offset:当前累积写入buf的总偏移量,注意它是累积的,不受repl_backlog_size的限制。

repl_backlog_histlen:当前的有效buf长度。如果buf还没有写满,那么这个值就等于实际写入的buf长度。如果写满了,那么就一直等于repl_backlog_size。这很好理解,因为buf被循环写入覆盖,所以有效长度肯定一直就是repl_backlog_size了。

repl_backlog_off:它指向当前有效buf的起始偏移量。注意,这个值并不是buf的真实偏移,因为buf是循环的,而这个值是和master_repl_offset相关的,是一个累积的偏移。

它们三者的关系是: repl_backlog_off = master_repl_offset - repl_backlog_histlen + 1

因为repl_backlog_off指向第一个字节,所以加了1。

了解了上面的机制,我们来看看主备节点之间如何配合从而实现psync的呢?

我们前面讲过,redis的数据库是通过command主备双发执行来实现实时同步的,当备节点接收到command时,它也会递增它自己的保存的一个offset(commandProcessed),这个offset就是master client里面的reploff。

如果主备节点是正常同步状态,那么reploff的值和master节点中的master_repl_offset应该是相等的。

如果主备链路闪断,主节点的一些command没有备份到备节点,会怎么样呢?很显然,备节点的reploff会小于主节点的master_repl_offset。比如下图:

在主备链路重新连接时,会触发handshake过程,就是我们前面讲的。slave节点会在psync请求命令中,将自己的reploff值带给主节点,主节点比较这个值和自己存的master_repl_offset,就知道该怎么样做psync了。

大家试想,如果reploff不在主节点的repl_backlog_off和master_repl_offset范围内,会怎么样?结论是没法做psync了,因为buf没有办法记录下所有丢失的command,这时候只能触发全量同步。

另外还要注意一点,我们知道,redis是支持slave级联的,slave-chain。所以slave本身也会维护一个backlog-buf,把自己当做master,和自己的slave进行数据同步。我们不细讲了!

前面我们讲了psync最难理解的部分,但不是全部,还有两个点:

1、repliId

它是一个随机的字符串,像这样:dade19682a24f8dfe4e15039c458ee95b7a03c65。它唯一标识一个同步数据源。我们把主备之间的数据同步,想象为一个单向的数据流,master是这个数据流的唯一入口(只有master可写,slave都只读),那么这个master和它的所有slave,已经slave下面再级联的所有slave,它们的数据源都是同一个repliID。它有什么用呢?

我们在做psync的时候,首先就是判断repliID是否一致,如果不一致就直接去做全量同步了。如果数据源都不一致,那我们还做啥psync呢?比如我们将一个redis实例由A的slave变成了B的slave,很显然repliID是不一致的。

2、cached-master

这是redis4.0之后增加的特性,之后的psync也叫psync2。我们知道,psync解决了主备之间同步丢失造成的全量同步问题,提升了性能。但是,在某些场景下它依然会触发全量同步,更加恐怖的是,这些场景还是比较常见的。

比如,管理员可能对slave节点做维护性的重启,重启后,repliid和offset信息会完全丢失,触发全量同步。

又比如,主实例发生了主备倒换,slave跟新的主实例握手建联,repliid和offset信息也会完全丢失,触发全量同步。

这些问题的本质原因,都是slave没有把原来的repliid和offset给保存下来。所以,新版本的redis增加了一个cached-master,它是一个client结构,用于保存老的信息。

我们看看以下两个场景:

场景一、slave重启

当slave被shutdown或者restart时,系统会自动保存rdb,在rdb的头部AUX信息中,会把repliid和reploffset给记录下来。注意,aof文件虽然在rewrite之后也可以包含和rdb类似的头部AUX信息,但是它不会保存repliid和reploffset。

slave启动后,在DB恢复阶段(loadDataFromDisk),会从rdb文件中读取这两个值,然后生成一个cached-master(replicationCacheMasterUsingMyself)。

场景二、master发生了主备倒换

slave监测到和master断链后,会将master对应的client free(freeclient)掉,这时会把对应的client赋值给cached-master(replicationCacheMaster)。

以上场景,我们都能把原来的repliid和reploffset给记录下来。在slave发起psync时,如果cached-master有效,则会使用里面的replid和reploffset来发起psync。

通过cached-master,能避免这些常见的场景之下触发全量同步,进一步提升了redis的性能。

PSYNC到此其实已经讲得差不多了,当然还有一些细节,比如结合cluster该如何处理?只要理解了上面的内容,啃起代码来就容易得多。

5、FULL-SYNC 全同步

不是所有场景都能PSYNC的,比如,一个全新的slave节点,当然就得全同步啦。下面我们分析全同步流程。

全同步其实比部分同步要简单,它只需要生产者把自己的数据一股脑全扔给消费者即可。我们要搞清的就是,Redis是如何把数据扔过去的?

全同步也是在handshake流程中触发的,如果psync失败,那么master会自动向slave发起全同步。

全同步有两种方式:diskless(不使用磁盘)、use-disk(使用磁盘)。取决于conf文件中的两个配置:

一、diskless full sync

这种方式不会把rdb保存到本地磁盘,而是直接把rdb通过socket发送给slave,所以叫做diskless。

由于考虑到db数据可能会海量,如果在主进程处理,会占用大量的cpu性能,所以redis fork了一个子进程来处理。但是呢,master和slave之间的socket又是主进程绑定的,所以子进程只负责读取db,格式化等处理,真正的socket发送,还是主进程处理的。说实话,挺绕的,看图。

这些操作可以看代码函数rdbSaveToSlavesSockets。

diskless的方式,如果判断结束呢,它是以下面的格式传输的:

$EOF:<40 bytes eofmark>

data

data

data

...

<40 bytes eofmark>

eofmark是一个40字节的随机数,在第一行会携带,接收端只要再次读取到eofmark,就认为结束。

二、disk full sync

这种方式会备份一个dump.rdb文件,其过程和普通的save一模一样,这里不展开,后面有时间可以专门写一章关于redis持久化的介绍。我们需要关注的是,这个rdb文件如果传递给slave。

在save子进程结束之后,master会读取这个文件,并且通过socket将文件内容全部传给slave(sendBulkToSlave)。

注意,它会先写一个前导符:"$<length>\r\n",表示这个文件的大小。

上面只讲了master如何把全量数据传输给slave,那么slave接受到数据后怎么恢复呢?所有处理都在函数readSyncBulkPayload中。

在接收端,也就是slave节点,也分为diskless和use disk两种方式(conf配置项 repl_diskless_load),需要注意的是,这和master发送方式是完全独立的。两端可以任意选择一种方式,不需要一致。

对于use disk的情况,slave会把接收到的数据先保存在一个本地临时的rdb文件中,然后在调用rdb_load做数据恢复。

对于diskless的情况,为了简便,slave会把socket先给block住,调用rdbLoadRio从socket的fd中做数据恢复、。

注意,在做数据恢复之前,会把本地的db全部清空。

FULL-SYNC之后,master-client就可以做正常的command实时同步了。

6、故障检测和倒换

这篇文章是不考虑哨兵模式和cluster的,大家需要注意。

主备故障检测,通常应该包含两个层面:链路、业务。

链路,就是说主备之间的连接是否故障。

业务,就是说对端实例是否还能正常提供服务。

业务层故障了,链路层不一定故障。反之,链路层正常,业务层不一定可用。所以,对这两层都得有检测机制。

redis在链路层,是通过socket来感知故障的,如果read一个fd,socket故障会直接返回错误。

而在业务层,则是master会每个一个周期(默认10s)向所有slave发送PING。超时的标准是,60S内没有收到任何SLAVE的数据。这时会把slave的client强制释放掉,slave需要重新handshake。

单纯的主备模式,故障感知能力是比较弱的。同时,它还不支持自动触发倒换,得手动触发。所以,这个模式几乎没法用。

得结合哨兵模式或者cluster。

后续我们再讲。

Redis源码详解 - Replication(主备)流程相关推荐

  1. Redis从精通到入门——数据类型Zset实现源码详解

    Redis数据类型之Zset详解 Zset简介 Zset常用操作 应用场景 Zset实现 源码阅读 Zset-ziplist实现 图解Zset-ziplist Zset-字典(dict) + 跳表(z ...

  2. AidLux“换脸”案例源码详解 (Python)

    "换脸"案例源码详解 (Python) faceswap_gui.py用于换脸,可与facemovie_gui.py身体互换源码(上一篇文章)对照观看 打开faceswap_gui ...

  3. Vue-Watcher观察者源码详解

    源码调试地址 https://github.com/KingComedy/vue-debugger 什么是Watcher Watcher是Vue中的观察者类,主要任务是:观察Vue组件中的属性,当属性 ...

  4. 【java】LinkedList1.8源码详解

    目录 前言 概要 属性 构造方法 核心方法 get(int index) set(int index, E element) add(int index, E element) addAll(Coll ...

  5. 【JAVA秘籍心法篇-Spring】Spring XML解析源码详解

    [JAVA秘籍心法篇-Spring]Spring XML解析源码详解 所谓天下武功,无坚不摧,唯快不破.但有又太极拳法以快制慢,以柔克刚.武功外式有拳打脚踢,刀剑棍棒,又有内功易筋经九阳神功.所有外功 ...

  6. OpenstackSDK 源码详解

    OpenstackSDK 源码详解 openstacksdk是基于当前最新版openstacksdk-0.17.2版本,可从 GitHub:OpenstackSDK 获取到最新的源码.openstac ...

  7. W601温湿度监测与邮件报警系统 — 源码详解(邮件报警模块)

    本项目中的邮件报警模块在用户在网页激活后会自动监测当前的温度,并且与用户设置的温度阈值做比较,一旦检测到当前温度超过用户设定的温度阈值,系统便会向用户所指定的邮箱发送一封报警邮件.当然,你也可以接入各 ...

  8. Rocksdb Compaction源码详解(二):Compaction 完整实现过程 概览

    文章目录 1. 摘要 2. Compaction 概述 3. 实现 3.1 Prepare keys 过程 3.1.1 compaction触发的条件 3.1.2 compaction 的文件筛选过程 ...

  9. Extreme Drift赛车游戏C#源码详解(1)

    Extreme Drift赛车游戏C#源码详解(1) C#我只是一个萌新,由于搞过Java,还是可以看懂C#的 偶然间得到赛车游戏Extreme Drift的源码 接下来我会花一段时间来解读,这是一个 ...

最新文章

  1. 对装饰器@wraps的解释(一看就懂)-- 并对装饰器详解
  2. 强势推荐8个功能强大,鲜为人知的实用软件
  3. [JS] 动态修改ckPlayer播放器宽度
  4. 成功解决FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `ar
  5. c语言08,C语言08 -- 指针
  6. 【Maven学习】Nexus OSS私服仓库的备份与迁移
  7. Javascript 原型和继承(Prototypes and Inheritance)
  8. 计算机与操作系统小结
  9. SpringCloud常见组件有哪些?
  10. Lombok常用注解和功能
  11. 行政编码json_基于FME国内县级及以上网络公开行政区划边界的获取
  12. 安川g7接线端子图_图解西门子S7-300plc模拟量模块接线方法
  13. 进阶 vue,需要掌握哪些知识?
  14. 微信用久了,越来越占内存怎么办?
  15. 如何在恢复模式下启动 Mac?
  16. Visual Studio 2005中的Windows Mobile模拟器
  17. 深度学习(二):传统神经网络
  18. shell的logo含义_华为logo的寓意是什么
  19. 最新最快的HTTP代理服务器,国内外HTTP代理服务器,游戏代理服务器,Q代理服务器,代理IP...
  20. 【C语言初阶】求最小公倍数的三种方法

热门文章

  1. mybatis的2种缓存机制(1)
  2. django进入admin报错ORA-00918:column ambiguously defined
  3. three.js问题记录---MeshLambertMaterial材质颜色失效
  4. 华为交换机配置ssh登录远程管理交换机
  5. 解决游戏画面撕裂问题谁家显示器更强?
  6. Office 365:如何有效管理会议详细信息和会议纪要
  7. 判断具有多个属性的行的连通性
  8. 华为模拟器ENSP——DNS域名解析实验
  9. 使用Element-UI中的Table表格组件制作多级表头
  10. python什么时候需要加引号_Python学习笔记(八)-Python中的引号用法总结