Redis主从复制

目录

主从复制简介

Redis数据同步

使用主从复制

建立复制

实例

准备工作:启动两个节点

建立复制

观察效果

断开复制

主从复制的实现原理

连接建立阶段

步骤1:保存主节点信息

步骤2:建立socket连接

步骤3:发送ping命令

步骤4:身份验证

步骤5:发送从节点端口信息

数据同步阶段

命令传播阶段

延迟与不一致

全量复制

旧版复制功能的实现

同步

命令传播

旧版复制功能的缺陷

新版复制功能的实现

部分重同步的实现

复制偏移量

复制积压缓冲区

根据需要调整复制积压缓冲区的大小

服务器运行ID

PSYNC命令的实现 

部分复制演示

 复制的实现

步骤1 : 设置主服务器的地址和端口

步骤2 : 建立套接字连接

步骤3: 发送PING命令

步骤4: 身份验证

步骤5: 发送端口信息

步骤6: 同步

步骤7: 命令传播

心跳检测

主到从 PING

从到主 REPLCONF ACK

检测主从服务器的网络连接状态

辅助实现min-slaves配置选项

检测命令丢失

Redis 2.8版本以前的命令丢失

重点回顾

应用中的问题

读写分离及其中的问题

延迟与不一致问题

数据过期问题

故障切换问题

读写分离总结

复制超时问题

复制中断问题

复制缓冲区溢出

各场景下复制的选择及优化技巧

第一次建立复制

主节点重启

从节点重启

网络中断

复制相关的配置

与主从节点都有关的配置

主节点相关配置

从节点相关配置

单机内存大小限制

info Replication


主从复制简介

为啥要用主从这样的架构模式

单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊,不当人啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是 单向 的,只能由主节点到从节点。Redis 主从复制支持 主从同步 和 从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。

主从复制主要的作用

数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 (实际上是一种服务的冗余)

负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。

高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。

Redis数据同步

数据怎么同步的呢?

你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。

 

数据传输的时候断网了或者服务器挂了怎么办啊?

传输过程中有什么网络问题啥的,会自动重连的,并且连接之后会把缺少的数据补上的。

大家需要记得的就是,RDB快照的数据生成的时候,缓存区也必须同时开始接受新请求,不然你旧的数据过去了,你在同步期间的增量数据咋办?是吧?

在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务 器去复制(replicate)另一个服务器,我们称呼被复制 的服务器为主服务器(master), 而对主服务器进行复制 的服务器则被称为从服务器(slave), 如图所示。

假设现在有两个Redis服务器,地址分别为

127.0.0.1:6379和127.0.0.1:12345, 如果我们向服务器127.0.0.1:12345发送 以下命令:

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379 OK

那么服务器127.0.0.1:12345将成为127.0.0.1:6379的从服务器,而服务器 127.0.0.1:6379则会成为127.0.0.1:12345的主服务器。

进行复制中的主从服务器双方的数据库将保存相同的数据,概念上将这种现象称作“数 据库状态一致",或者简称“一致”。

比如说,如果我们在主服务器上执行以下命令:

127.0.0.1:6379> SET msg "hello world" OK

那么我们应该既可以在主服务器上获取msg键的值:

127.0.0.1:6379> GET msg "hello world"

又可以在从服务器上获取msg键的值:

127.0.0.1:12345> G:ET msg "hello world"

另一方面,如果我们在主服务器中删除了键msg:

127,0.0.1:6379> DEL msg (integer) 1

那么不仅主服务器上的msg键会被删除:

127.0.0.1:6379> EXISTS msg (integer) 0

从服务器上的msg键也应该会被删除:

127.0.0.1:12345> EXISTS msg (integer) 0

关于复制的特性和用法还有很多,Redis官方网站上的《复制》文档(http://redis.io/ topics/replication)已经做了很详细的介绍,这里不再赘述。

使用主从复制

为了更直观的理解主从复制,在介绍其内部原理之前,先说明我们需要如何操作才能开启主从复制。

建立复制

需要注意,主从复制的开启,完全是在从节点发起的;不需要我们在主节点做任何事情。

从节点开启主从复制,有3种方式:

(1)配置文件

在从服务器的配置文件中加入:slaveof <masterip> <masterport>

(2)启动命令

redis-server启动命令后加入 --slaveof <masterip> <masterport>

(3)客户端命令

Redis服务器启动后,直接通过客户端执行命令:slaveof <masterip> <masterport>,则该Redis实例成为从节点。

上述3种方式是等效的,下面以客户端命令的方式为例,看一下当执行了slaveof后,Redis主节点和从节点的变化。

实例

准备工作:启动两个节点

方便起见,实验所使用的主从节点是在一台机器上的不同Redis实例,其中主节点监听6379端口,从节点监听6380端口;从节点监听的端口号可以在配置文件中修改:

启动后可以看到:

两个Redis节点启动后(分别称为6379节点和6380节点),默认都是主节点。

建立复制

此时在6380节点执行slaveof命令,使之变为从节点:

观察效果

下面验证一下,在主从复制建立后,主节点的数据会复制到从节点中。

(1)首先在从节点查询一个不存在的key:

(2)然后在主节点中增加这个key:

(3)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:

(4)然后在主节点删除这个key:

(5)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:

断开复制

通过slaveof <masterip> <masterport>命令建立主从复制关系以后,可以通过slaveof no one断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。

从节点执行slaveof no one后,打印日志如下所示;可以看出断开复制后,从节点又变回为主节点。

主节点打印日志如下:

主从复制的实现原理

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段;下面分别进行介绍。

连接建立阶段

该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。

步骤1:保存主节点信息

从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。

需要注意的是,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。

这个过程中,可以看到从节点打印日志如下:

步骤2:建立socket连接

从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。如果连接成功,则:

从节点:为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。

主节点:接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。

这个过程中,从节点打印日志如下:

步骤3:发送ping命令

从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。

从节点发送ping命令后,可能出现3种情况:

(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。

(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。

(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。

在主节点返回pong情况下,从节点打印日志如下:

步骤4:身份验证

如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。

如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。

步骤5:发送从节点端口信息

身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。

数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。

数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制,下面会有一章专门讲解这两种复制方式以及psync命令的执行过程,这里不再详述。

需要注意的是,在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。

命令传播阶段

数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。由于心跳机制的原理涉及部分复制,因此将在介绍了部分复制的相关内容后单独介绍该心跳机制。

延迟与不一致

需要注意的是,命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay配置等有关。

repl-disable-tcp-nodelay no:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP_NODELAY;默认no,即不禁止TCP_NODELAY。当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。

一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为yes;多数情况使用默认值no。

全量复制

Redis通过psync命令进行全量复制的过程如下:

(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;具体判断过程需要在讲述了部分复制原理后再介绍。

(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令

(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态

(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态

(5)如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态

下面是执行全量复制时,主从节点打印的日志;可以看出日志内容与上述步骤是完全对应的。

主节点的打印日志如下:

从节点打印日志如下图所示:

其中,有几点需要注意:从节点接收了来自主节点的89260个字节的数据;从节点在载入主节点的数据之前要先将老数据清除;从节点在同步完数据后,调用了bgrewriteaof。

通过全量复制的过程可以看出,全量复制是非常重型的操作:

(1)主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的;

(2)主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗

(3)从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗

旧版复制功能的实现

Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作:

同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。

命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状 态出现不一致时,让主从服务器的数据库重新回到一致状态。

同步

当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首 先需要执行同步操作,也即是,将从服务器的数据库状态更新至主服务器当前所处的数据库 状态。

从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成,以下是 SYNC命令的执行步骤:

1)从服务器向主服务器发送SYNC命令。

2)收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使 用一个缓冲区记录从现在开始执行的所有写命令。

3)当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB 文件发送给从服务器,从服务器接收并载人这个RDB文件,将自己的数据库状态更新至主 服务器执行BGSAVE命令时的数据库状态。

4)主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写 命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。

图展示了SYNC命令执行期间,主从服务器的通信过程。

展示了一个主从服务器进行同步的例子。

命令传播

在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,但这种一致并不 是一成不变的,每当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被 修改,并导致主从服务器状态不再一致。

举个例子,假设一个主服务器和一个从服务器刚刚完成同步操作,它们的数据库都保存了相同的五个键k1至k5, 如图所示。

如果这时,客户端向主服务器发送命令DEL k3, 那么主服务器在执行完这个DEL命 令之后,主从服务器的数据库将出现不一致:主服务器的数据库已经不再包含键k3, 但这 个键却仍然包含在从服务器的数据库里面,如图

为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作:主 服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执 行,当从服务器执行了相同的写命令之后,主从 服务器将再次回到一致状态。

在上面的例子中,主服务器因为执行了命令 DEL k3而导致主从服务器不一致,所以主服务器 将向从服务器发送相同的命令DEL k3。当从服务 器执行完这个命令之后,主从服务器将再次回到一 致状态,现在主从服务器两者的数据库都不再包含k3了

旧版复制功能的缺陷

在Redis中,从服务器对主服务器的复制可以分为以下两种情况:

初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主 服务器和上一次复制的主服务器不同。

断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从 服务器通过自动重连接重新连上了主服务器,并继续复制主服务器。

对于初次复制来说,旧版复制功能能够很好地完成任务,但对于断线后重复制来说,旧 版复制功能虽然也能让主从服务器重新回到一致状态,但效率却非常低。

要理解这一情况,请看表展示的断线后重复制例子。

在时间T10091, 从服务器终于重新连接上主服务器,因为这时主从服务器的状态巳经 不再一致,所以从服务器将向主服务器发送SYNC命令,而主服务器会将包含键k1至键 k10089的ROB文件发送给从服务器,从服务器通过接收和载人这个ROB文件来将自己的 数据库更新至主服务器数据库当前所处的状态。

虽然再次发送SYNC命令可以让主从服务器重新回到一致状态,但如果我们仔细研究这 个断线重复制过程,就会发现传送ROB文件这一步实际上并不是非做不可的:

主从服务器在时间T0至时间T10086中一直处于一致状态,这两个服务器保存的数 据大部分都是相同的。

从服务器想要将自己更新至主服务器当前所处的状态,真正需要的是主从服务器连 接中断期间,主服务器新添加的k10087、k10088、k10089三个键的数据。

可惜的是,旧版复制功能并没有利用以上列举的两点条件,而是继续让主服务器生 成并向从服务器发送包含键k1至键k10089的RDB文件,但实际上RDB文件包 含的键k1至键k10086的数据对于从服务器来说都是不必要的。

上面给出的例子可能有一点理想化,因为在主从服务器断线期间,主服务器执行的写命 令可能会有成百上千个之多,而不仅仅是两三个写命令。但总的来说,主从服务器断开的时 间越短,主服务器在断线期间执行的写命令就越少,而执行少量写命令所产生的数据量通常 比整个数据库的数据量要少得多,在这种情况下,为了让从服务器补足一小部分缺失的数 据,却要让主从服务器重新执行一次SYNC命令,这种做法无疑是非常低效的。

SYNC命令是一个非常耗费资源的操作 每次执行SYNC命令,主从服务器需要执行以下动作:

1)主服务器需要执行BGSAVE命令来生成RDB文件,这个生成操作会耗费主服务 器大量的CPU、内存和磁盘IO资源。

2)主服务器需要将自己生成的RDB文件发送给从服务器,这个发送操作会耗费主 从服务器大量的网络资源(带宽和流量),并对主服务器响应命令请求的时间产生影响。

3)接收到RDB文件的从服务器需要载入主服务器发来的RDB文件,并且在载入 期间,从服务器会因为阻塞而没办法处理命令请求。

因为SYNC命令是一个如此耗费资源的操作,所以Redis有必要保证在真正有需要 时才执行SYNC命令。

新版复制功能的实现

为了解决旧版复制功能在处理断线重复制情况时的低效问题,Redis从2.8版本开始, 使用PSYNC命令代替SYNC命令来执行复制时的同步操作。

PSYNC命令具有完整重同步和部分重同步两种模式:

其中完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执 行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器 发送保存在缓冲区里面的写命令来进行同步。

而部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务 器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送 给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务 器当前所处的状态。

PSYNC命令的部分重同步模式解决了旧版复制功能在处理断线后重复制时出现的低效 情况,表展示了如何使用PSYNC命令高效地处理上一节展示的断线后复制情况。

对比一下SYNC命令和PSYNC命令处理断线重复制的方法,不难看出,虽然SYNC命 令和PSYNC命令都可以让断线的主从服务器重新回到一致状态,但执行部分重同步所需的资源比起 执行SYNC命令所需的资源要少得多,完成同步的 速度也快得多。执行SYNC命令需要生成、传送和 载人整个RDB文件,而部分重同步只需要将从服 务器缺少的写命令发送给从服务器执行就可以了。

图展示了主从服务器在执行部分重同步

部分重同步的实现

在了解了PSYNC命令的由来,以及部分重同步的工作方式之后,是时候来介绍一下部 分重同步的实现细节了

部分重同步功能由以下三个部分构成:

主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。

主服务器的复制积压缓冲区(replication backlog)。

服务器的运行ID (run ID)。

复制偏移量

执行复制的双方一主服务器和从服务器会分别维护一个复制偏移量:

主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。

从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量 的值加上N。

在图所示的例子中,主从服务器的复制 偏移量的值都为10086。

如果这时主服务器向三个从服务器传播长度 为33字节的数据,那么主服务器的复制偏移量将更新为10086+33=10119, 而三个从服务器在接收到主服务器传播的数据之后,也会将复制   三个从服务器 偏移量更新为10119, 如图

通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态:

如果主从服务器处于一致状态,那么主从服务器两者的偏移量总是相同的。

相反,如果主从服务器两者的偏移量并不相同,那么说明主从服务器并未处于一致 状态。

考虑以下这个例子:假设如图所示,主从服务器当前的复制偏移量都为10086, 但是就在主服务器要向从服务器传播长度为33字节的数据之前,从服务器A断线了,那么 主服务器传播的数据将只有从服务器B和从服务器C能收到,在这之后,主服务器、从服 务器B和从服务器C三个服务器的复制偏移量都将更新为10119, 而断线的从服务器A的复制偏移量仍然停留在10086, 这说明从服务器A与主服务器并不一致,如图

假设从服务器A在断线之后就立即重新连接主服务器,并且成功,那么接下来,从服 务器将向主服务器发送PSYNC命令,报告从服务器A当前的复制偏移量为10086, 那么 这时,主服务器应该对从服务器执行完整重同步还是部分重同步呢?如果执行部分重同步的 话,主服务器又如何补偿从服务器A在断线期间丢失的那部分数据呢?以上问题的答案都 和复制积压缓冲区有关。

复制积压缓冲区

复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队 列,默认大小为1MB。

当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令人队到复制积压缓冲区里面,如图

因此,主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积 压缓冲区会为队列中的每个字节记录相应的复制偏移量,就像表展示的那样。

当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量 offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步 操作:

如果offset偏移量之后的数据(也即是偏移量offset+1 开始的数据)仍然存在 于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作。

相反,如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务 器将对从服务器执行完整重同步操作。

回到之前图展示的断线后重连接例子:

当从服务器A断线之后,它立即重新连接主服务器,并向主服务器发送PSYNC命 令,报告自己的复制偏移量为10086。

主服务器收到从服务器发来的Psmc命令以及偏移量10086之后,主服务器将检 查偏移量10086之后的数据是否存在于复制积压缓冲区里面,结果发现这些数据仍 然存在,于是主服务器向从服务器发送+CONTINUE回复,表示数据同步将以部分 重同步模式来进行。

接着主服务器会将复制积压缓冲区10086偏移量之后的所有数据(偏移量为10087 至10119)都发送给从服务器。

从服务器只要接收这33字节的缺失数据,就可以回到与主服务器一致的状态,如 图

根据需要调整复制积压缓冲区的大小

Redis为复制积压缓冲区设置的默认大小为1 MB, 如果主服务器需要执行大量写命 令,又或者主从服务器断线后重连接所需的时间比较长,那么这个大小也许并不合适。 如果复制积压缓冲区的大小设置得不恰当,那么PSYNC命令的复制重同步模式就不能 正常发挥作用,因此,正确估算和设置复制积压缓冲区的大小非常重要。

复制积压缓冲区的最小大小可以根据公式second• write_size_per_second 来估算:

其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算)。

而write_size_per_second则是主服务器平均每秒产生的写命令数据量 (协议格式的写命令的长度总和)。

例如,如果主服务器平均每秒产生1 MB的写数据,而从服务器断线之后平均要5 秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于5MB。

为了安全起见,可以将复制积压缓冲区的大小设为2 * second * write_size_ per_second, 这样可以保证绝大部分断线情况都能用部分重同步来处理。

至于复制积压缓冲区大小的修改方法,可以参考配置文件中关于repl-backlog­size选项的说明。

服务器运行ID

除了复制偏移量和复制积压缓冲区之外,实现部分重同步还需要用到服务器运行ID (run ID):

每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID。

运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成,例如53b9b 28df8042fdc9ab5e3fcbbbabffld5dce2b3。

当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器, 而从服务器则会将这个运行ID保存起来。

当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之 前保存的运行ID:

如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服 务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部 分重同步操作。

相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同, 那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服 务器将对从服务器执行完整重同步操作。

举个例子,假设从服务器原本正在复制一个运行ID为53b9b28df8042fdc9ab5e3f cbbbabffld5dce2b3的主服务器,那么在网络断开,从服务器重新连接上主服务器之后, 从服务器将向主服务器发送这个运行ID. 主服务器根据自己的运行ID是否53b9b28df80 42fdc9ab5e3fcbbbabffld5dce2b3来判断是执行部分重同步还是执行完整重同步。

PSYNC命令的实现 

PSYNC命令的调用方法有两种:

如果从服务器以前没有复制过任何主服务器,或者之前执行过SLAVEOF no one 命令,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC ? -1命令, 主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步)。

相反地,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复 制时将向主服务器发送PSYNC <runid> <offset>命令:其中runid是上一次 复制的主服务器的运行ID, 而offset则是从服务器当前的复制偏移量,接收到这 个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作。

根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:

如果主服务器返回+FULLRESYNC <runid> <offset>回复,那么表示主服务 器将与从服务器执行完整重同步操作:其中runid是这个主服务器的运行ID, 从 服务器会将这个ID保存起来,在下一次发送PSYNC命令时使用;而offset则是 主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量。

如果主服务器返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分重 同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了。

如果主服务器返回-ERR回复,那么表示主服务器的版本低于Redis 2.8, 它识别不 了PSYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同 步操作。

流程图总结了PSYNC命令执行完整重同步和部分重同步时可能遇上的情况。

了熟悉/'SYNC命令的用法,让我们来看一个完整的复制一网络中断一重复制例子。 首先,假设有两个Redis服务器,它们的版本都是Redis 2.8, 其中主服务器的地址为 127.0.0.1:6379, 从服务器的地址为127.0.0.1:12345。

如果客户端向从服务器发送命令SLAVEOF 127. 0. 0 .1 6379, 并且假设从服务器是 第一次执行复制操作,那么从服务器将向主服务器发送PSYNC ? -1命令,请求主服务器 执行完整重同步操作。

主服务器在收到完整重同步请求之后,将在后台执行BGSAVE命令,并向从服务器返 回+FULLRESYNC 53b9b28df8042fdc9ab5e3fcbbbabffld5dce2b3 10086回复,其 中53b9b28df8042fdc9ab5e3fcbbbabffld5dce2b3是主服务器的运行ID, 而10086 则是主服务器当前的复制偏移量。

假设完整重同步成功执行,并且主从服务器在一段时间之后仍然保持一致,但是在复制 偏移量为20000的时候,主从服务器之间的网络连接中断了,这时从服务器将重新连接主 服务器,并再次对主服务器进行复制。

因为之前曾经对主服务器进行过复制,所以从服务器将向主服务器发送命令PSYNC 53b9b28df8042fdc9ab5e3fcbbbabffld5dce2b3 20000, 请求进行部分重同步。

主服务器在接收到从服务器的PSYNC命令之后,首先对比从服务器传来的运行ID53b 9b28df8042fdc9ab5e3fcbbbabffld5dce2b3和主服务器自身的运行ID, 结果显示该 ID和主服务器的运行ID相同,于是主服务器继续读取从服务器传来的偏移量20000, 检 查偏移最为20000之后的数据是否存在于复制积压缓冲区里面,结果发现数据仍然存在。

确认运行ID相同并且数据存在之后,主服务器将向从服务器返回+CONTINUE回复, 表示将与从服务器执行部分重同步操作,之后主服务器会将保存在复制积压缓冲区20000 偏移量之后的所有数据发送给从服务器,主从服务器将再次回到一致状态。

部分复制演示

在下面的演示中,网络中断几分钟后恢复,断开连接的主从节点进行了部分复制;为了便于模拟网络中断,本例中的主从节点在局域网中的两台机器上。

网络中断

网络中断一段时间后,主节点和从节点都会发现失去了与对方的连接(关于主从节点对超时的判断机制,后面会有说明);此后,从节点便开始执行对主节点的重连,由于此时网络还没有恢复,重连失败,从节点会一直尝试重连。

主节点日志如下:

从节点日志如下:

网络恢复

网络恢复后,从节点连接主节点成功,并请求进行部分复制,主节点接收请求后,二者进行部分复制以同步数据。

主节点日志如下:

从节点日志如下:

 复制的实现

通过向从服务器发送SLAVEOF命令,我们可以让一个从服务器去复制一个主服务器:

SLAVEOF <master_ip> <master_port>

本节将以从服务器127.0.0.1:12345接收到命令:

SLAVEOF 127.0.0.1 6379

为例,展示Redis2.8或以上版本的复制功能的详细实现步骤。

步骤1 : 设置主服务器的地址和端口

当客户端向从服务器发送以下命令时:

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379 OK

从服务器首先要做的就是将客户端给定的主服务器IP地址127.0.0.1以及端口6379 保存到服务器状态的masterhost属性和masterport属性里面:

图展示了SLAVEOF命令执行之后,从服务器的服务器状态。

SLAVEOF命令是一个异步命令,在完成masterhost属性和masterport属性的设 置工作之后,从服务器将向发送SLAVEOF命令的客户端返回OK, 表示复制指令已经被接 收,而实际的复制工作将在OK返回之后才真正开始执行。

步骤2 : 建立套接字连接

在SLAVEOF命令执行之后,从服务器将根据命令所设置的IP地址和端口,创建连向主服务器的套接字连接,如图所示。

如果从服务器创建的套接字能成功 连接(connect)到主服务器,那么从服 务器将为这个套接字关联一个专门用于 处理复制工作的文件事件处理器,这个 处理器将负责执行后续的复制工作,比 如接收RDB文件,以及接收主服务器 传播来的写命令,诸如此类。

而主服务器在接受(accept)从服 务器的套接字连接之后,将为该套接字创建相应的客户端状态,并将从服务器看作是一个连接到主服务器的客户端来对待,这时从 服务器将同时具有服务器(server)和客户端(client)两个身份:从服务器可以向主服务器 发送命令请求,而主服务器则会向从服务器返回命令回复,如图所示。

因为复制工作接下来的几个步骤都会以从服务器向主服务器发送命令请求的形式来进 行,所以理解“从服务器是主服务器的客户端”这一点非常重要。

步骤3: 发送PING命令

从服务器成为主服务器的客户端之后,做的第 一件事就是向主服务器发送一个PING命令,如图

这个PING命令有两个作用:                                                     

虽然主从服务器成功建立起了套接字连接,但双方井未使用该套接字进行过任何通信,通过发送PING命令可以检查套接字的 读写状态是否正常。

因为复制工作接下来的几个步骤都必须在主服务器可以正常处理命令请求的状态下 才能进行,通过发送PING命令可以检查主服务器能否正常处理命令请求。

从服务器在发送PING命令之后将遇到以下三种情况的其中一种:

如果主服务器向从服务器返回了一个命令回复,但从服务器却不能在规定的时限 (timeout)内读取出命令回复的内容,那么表示主从服务器之间的网络连接状态不 佳,不能继续执行复制工作的后续步骤。当出现这种情况时,从服务器断开并重新 创建连向主服务器的套接字。

如果主服务器向从服务器返回一个错误,那么表示主服务器暂时没办法处理从服务 器的命令请求,不能继续执行复制工作的后续步骤。当出现这种情况时,从服务器断开并重新创建连向主服务器的套接字。比如说,如果主服务器正在处理一个超时 运行的脚本,那么当从服务器向主服务器发送PING命令时,从服务器将收到主服务器返回的BUSY Redisis busy running a script. SCRIPT KILL or SHUTDOWN NOSAVE. 错误。

如果从服务器读取到"PONG"回复,那么表示主从服务器 之间的网络连接状态正常, 并且主服务器可以正常处理 从服务器(客户端)发送的 命令请求,在这种情况下, 从服务器可以继续执行复制 工作的下个步骤。

流程图总结了从服务器在发送PING命令时可能遇到的情况, 以及各个情况的处理方式。

步骤4: 身份验证

从服务器在收到主服务器返回的"PONG"回复之后,下一步要做的就是决定是否进行身份验证:

如果从服务器设置了masterauth选项,那么进行身份验证。

如果从服务器没有设置masterauth选项,那么不进行身份验证。

在需要进行身份验证的情况下,从服务器将向主服务器发送一条AUTH命令,命令的 参数为从服务器masterauth选项的值

举个例子,如果从服务器masterauth 选项的值为10086, 那么从服务器将向主服务 器发送命令AUTH 10086, 如图所示。

从服务器在身份验证阶段可能遇到的情 况有以下几种:

如果主服务器没有设置requirepass选项,并且从服务器也没有设置masterauth 选项,那么主服务器将继续执行从服务器发送的命令,复制工作可以继续进行。

如果从服务器通过AUTH命令发送的密码和主服务器requirepass选项所设置的 密码相同,那么主服务器将继续执行从服务器发送的命令,复制工作可以继续进行。 与此相反,如果主从服务器设置的密码不相同,那么主服务器将返回一个invalid password错误。

如果主服务器设置了requirepass选项,但从服务器却没有设置masterauth 选项,那么主服务器将返回一个NOAUTH错误。另一方面,如果主服务器没有设置requirepass选项,但从服务器却设置了masterauth选项,那么主服务器将返 回一个no password is set错误。

所有错误情况都会令从服务器中止目前的复制工作,并从创建套接字开始重新执行复 制,直到身份验证通过,或者从服务器放弃执行复制为止。

流程图总结了从服务器在身份验证阶段可能遇到的情况,以及各个情况的处理方式。

步骤5: 发送端口信息

在身份验证步骤之后,从服务器将执行命令REPLCONF listening-port<port­number>, 向主服务器发送从服务器的监听端口号。

例如在我们的例子中,从服务器的监听端口为12345, 那么从服务器将向主服务器发 送命令REPLCONF listening-port 12345, 如图所示。

主服务器在接收到这个命令之后,会将端口号记录在从服务器所对应的客户端状态的slave_listening_port属性中

图展示了客户端状态设置slave_listening_port属性之后的样子。

slave _ listening _ port属性目前唯一的作用就是在主服务器执行INFO replication 命令时打印出从服务器的端口号。

以下是客户端向例子中的主服务器发送INFO replication命令时得到的回 复,其中slave0行的port域显示的就 是从服务器所对应客户端状态的slave_ listening_port属性的值

步骤6: 同步

在这一步,从服务器将向主服务器发送PSYNC命令,执行同步操作,并将自己的数据 库更新至主服务器数据库当前所处的状态。

值得一提的是,在同步操作执行之前,只有从服务器是主服务器的客户端,但是在执行 同步操作之后,主服务器也会成为从服务器的客户端:

如果PSYNC命令执行的是完整重同步操作,那么主服务器需要成为从服务器的客 户端,才能将保存在缓冲区里面的写命令发送给从服务器执行。

如果PSYNC命令执行的是部分重同步操作,那么主服务器需要成为从服务器的客 户端,才能向从服务器发送保存在复制积压缓冲区里面的写命令。

因此,在同步操作执行之后,主从服务器双方都是对方的客户端,它们可以互相向对方 发送命令请求,或者互相向对方返回命令回复,如图所示。

正因为主服务器成为了从服务器的客户端,所以主服务器才可以通过发送写命令来改变 从服务器的数据库状态,不仅同步操作需要用到这一点,这也是主服务器对从服务器执行命 令传播操作的基础。

步骤7: 命令传播

当完成了同步之后,主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己 执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就 可以保证主从服务器一直保持一致了。

以上就是Redis 2.8或以上版本的复制功能的实现步骤。

心跳检测

主到从 PING

每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。

PING发送的频率由repl-ping-slave-period参数控制,单位是秒,默认值是10s。

关于该PING命令究竟是由主节点发给从节点,还是相反,有一些争议;因为在Redis的官方文档中,对该参数的注释中说明是从节点向主节点发送PING命令,如下图所示:

但是根据该参数的名称(含有ping-slave),以及代码实现,我认为该PING命令是主节点发给从节点的。相关代码如下:

从到主 REPLCONF ACK

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK <replication_offset>

其中replication_offset是从服务器当前的复制偏移量。 发送REPLCONF ACK命令对于主从服务器有三个作用:

检测主从服务器的网络连接状态。

辅助实现min-slaves选项。

检测命令丢失。

检测主从服务器的网络连接状态

主从服务器可以通过发送和接收REPLCONF ACK命令来检查两者之间的网络连接是否 正常:如果主服务器超过一秒钟没有收到从服务器发来的REPLCONF ACK命令,那么主服 务器就知道主从服务器之间的连接出现问题了。

通过向主服务器发送INFO replication命令,在列出的从服务器列表的lag一栏中,我 们可以看到相应从服务器最后一次向主服务器发送REPLCONF ACK命令距离现在过了多少秒

在一般情况下,lag的值应该在0秒或者1秒之间跳动,如果超过1秒的话,那么说 明主从服务器之间的连接出现了故障。

辅助实现min-slaves配置选项

Redis的min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服 务器在不安全的情况下执行写命令。

举个例子,如果我们向主服务器提供以下设置:

那么在从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令,这里的延迟值就是上面提到的INFO replication命令的 lag值。

检测命令丢失

如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向 主服务器发送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自 己的复制偏移量,然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里 面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。

举个例子,假设有两个处于一致状态的主从服务器,它们的复制偏移 量都是200, 如图所示。

如果这时主服务器执行了命令 SET key value (协议格式的长度 为33字节),将自己的复制偏移量更新到了233, 并尝试向从服务器传播 命令SET key value, 但这条命令 却因为网络故障而在传播的途中丢失, 那么主从服务器之间的复制偏移量就 会出现不一致,主服务器的复制偏移量会被更新为233, 而从服务器的复制偏移量仍然为200, 如图所示。

在这之后,当从服务器向主服务器发送REPLCONF ACK命令的时候,主服务器会察觉 从服务器的复制偏移量依然为200, 而自己的复制偏移量为233, 这说明复制积压缓冲区里面复制偏移量为201至233的数据(也即是命令SET key value)在传播过程中丢失 了,于是主服务器会再次向从服务器传播命令SET key value, 从服务器通过接收并执 行这个命令可以将自己更新至主服务器当前所处的状态,如图所示。

注意,主服务器向从服务器补发缺失数据这一操作的原理和部分重同步操作的原理非常 相似,这两个操作的区别在于,补发缺失数据操作在主从服务器没有断线的情况下执行,而 部分重同步操作则在主从服务器断线并重连之后执行。

Redis 2.8版本以前的命令丢失

REPLCONF ACK命令和复制积压缓冲区都是Redis 1.8版本新增的,在Redis 1.8版 本以前,即使命令在传播过程中丢失,主服务器和从服务器都不会注意到,主服务器更 不会向从服务器补发丢失的数据,所以为了保证复制时主从服务器的数据一致性,最好 使用1.8或以上版本的Redis。

重点回顾

Redis 2.8以前的复制功能不能高效地处理断线后重复制情况,但Redis 2.8新添加的 部分重同步功能可以解决这个问题。

部分重同步通过复制偏移量、复制积压缓冲区、服务器运行ID三个部分来实现。

在复制操作刚开始的时候,从服务器会成为主服务器的客户端,并通过向主服务器 发送命令请求来执行复制步骤,而在复制操作的后期,主从服务器会互相成为对方 的客户端。

主服务器通过向从服务器传播命令来更新从服务器的状态,保持主从服务器一致, 而从服务器则通过向主服务器发送命令来进行心跳检测,以及命令丢失检测。

应用中的问题

读写分离及其中的问题

在主从复制基础上实现的读写分离,可以实现Redis的读负载均衡:由主节点提供写服务,由一个或多个从节点提供读服务(多个从节点既可以提高数据冗余程度,也可以最大化读负载能力);在读负载较大的应用场景下,可以大大提高Redis服务器的并发量。下面介绍在使用Redis读写分离时,需要注意的问题。

延迟与不一致问题

前面已经讲到,由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。

在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。

数据过期问题

在单机版Redis中,存在两种删除策略:

惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。

定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。

在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。

Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。

故障切换问题

在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。

读写分离总结

在使用读写分离之前,可以考虑其他方法增加Redis的读负载能力:如尽量优化主节点(减少慢查询、减少持久化等其他情况带来的阻塞等)提高负载能力;使用Redis集群同时提高读负载能力和写负载能力等。如果使用读写分离,可以使用哨兵,使主从节点的故障切换尽可能自动化,并减少对应用程序的侵入。

复制超时问题

主从节点复制超时是导致复制中断的最重要的原因之一,本小节单独说明超时问题,下一小节说明其他会导致复制中断的问题。

超时判断意义

在复制连接建立过程中及之后,主从节点都有机制判断连接是否超时,其意义在于:

(1)如果主节点判断连接超时,其会释放相应从节点的连接,从而释放各种资源,否则无效的从节点仍会占用主节点的各种资源(输出缓冲区、带宽、连接等);此外连接超时的判断可以让主节点更准确的知道当前有效从节点的个数,有助于保证数据安全(配合前面讲到的min-slaves-to-write等参数)。

(2)如果从节点判断连接超时,则可以及时重新建立连接,避免与主节点数据长期的不一致。

判断机制

主从复制超时判断的核心,在于repl-timeout参数,该参数规定了超时时间的阈值(默认60s),对于主节点和从节点同时有效;主从节点触发超时的条件分别如下:

(1)主节点:每秒1次调用复制定时函数replicationCron(),在其中判断当前时间距离上次收到各个从节点REPLCONF ACK的时间,是否超过了repl-timeout值,如果超过了则释放相应从节点的连接。

(2)从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:

如果当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过repl-timeout,则释放与主节点的连接;

如果当前处于数据同步阶段,且收到主节点的RDB文件的时间超时,则停止数据同步,释放连接;

如果当前处于命令传播阶段,且距离上次收到主节点的PING命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。

主从节点判断连接超时的相关源代码如下:

/* Replication cron function, called 1 time per second. */

void replicationCron(void) {

    static long long replication_cron_loops = 0;



    /* Non blocking connection timeout? */

    if (server.masterhost &&

        (server.repl_state == REDIS_REPL_CONNECTING ||

         slaveIsInHandshakeState()) &&

         (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)

    {

        redisLog(REDIS_WARNING,"Timeout connecting to the MASTER...");

        undoConnectWithMaster();

    }



    /* Bulk transfer I/O timeout? */

    if (server.masterhost && server.repl_state == REDIS_REPL_TRANSFER &&

        (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)

    {

        redisLog(REDIS_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value.");

        replicationAbortSyncTransfer();

    }



    /* Timed out master when we are an already connected slave? */

    if (server.masterhost && server.repl_state == REDIS_REPL_CONNECTED &&

        (time(NULL)-server.master->lastinteraction) > server.repl_timeout)

    {

        redisLog(REDIS_WARNING,"MASTER timeout: no data nor PING received...");

        freeClient(server.master);

    }



    //此处省略无关代码……



    /* Disconnect timedout slaves. */

    if (listLength(server.slaves)) {

        listIter li;

        listNode *ln;

        listRewind(server.slaves,&li);

        while((ln = listNext(&li))) {

            redisClient *slave = ln->value;

            if (slave->replstate != REDIS_REPL_ONLINE) continue;

            if (slave->flags & REDIS_PRE_PSYNC) continue;

            if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)

            {

                redisLog(REDIS_WARNING, "Disconnecting timedout slave: %s",

                    replicationGetSlaveName(slave));

                freeClient(slave);

            }

        }

    }



    //此处省略无关代码……



}

需要注意的坑

下面介绍与复制阶段连接超时有关的一些实际问题:

(1)数据同步阶段:在主从节点进行全量复制bgsave时,主节点需要首先fork子进程将当前数据保存到RDB文件中,然后再将RDB文件通过网络传输到从节点。如果RDB文件过大,主节点在fork子进程+保存RDB文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时;此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连……这是个悲伤的循环。为了避免这种情况的发生,除了注意Redis单机数据量不要过大,另一方面就是适当增大repl-timeout值,具体的大小可以根据bgsave耗时来调整。

(2)命令传播阶段:如前所述,在该阶段主节点会向从节点发送PING命令,频率由repl-ping-slave-period控制;该参数应明显小于repl-timeout值(后者至少是前者的几倍)。否则,如果两个参数相等或接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点发送数据,则从节点很容易判断超时。

(3)慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询(如keys *或者对大数据的hgetall等),导致服务器阻塞;阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。

复制中断问题

主从节点超时是复制中断的原因之一,除此之外,还有其他情况可能导致复制中断,其中最主要的是复制缓冲区溢出问题。

复制缓冲区溢出

前面曾提到过,在全量复制阶段,主节点会将执行的写命令放到复制缓冲区中,该缓冲区存放的数据包括了以下几个时间段内主节点执行的写命令:bgsave生成RDB文件、RDB文件由主节点发往从节点、从节点清空老数据并载入RDB文件中的数据。当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;这种情况可能引起全量复制->复制缓冲区溢出导致连接中断->重连->全量复制->复制缓冲区溢出导致连接中断……的循环。

复制缓冲区的大小由client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds}配置,默认值为client-output-buffer-limit slave 256MB 64MB 60,其含义是:如果buffer大于256MB,或者连续60s大于64MB,则主节点会断开与该从节点的连接。该参数是可以通过config set命令动态配置的(即不重启Redis也可以生效)。

当复制缓冲区溢出时,主节点打印日志如下所示:

需要注意的是,复制缓冲区是客户端输出缓冲区的一种,主节点会为每一个从节点分别分配复制缓冲区;而复制积压缓冲区则是一个主节点只有一个,无论它有多少个从节点。

各场景下复制的选择及优化技巧

在介绍了Redis复制的种种细节之后,现在我们可以来总结一下,在下面常见的场景中,何时使用部分复制,以及需要注意哪些问题。

第一次建立复制

此时全量复制不可避免,但仍有几点需要注意:如果主节点的数据量较大,应该尽量避开流量的高峰期,避免造成阻塞;如果有多个从节点需要建立对主节点的复制,可以考虑将几个从节点错开,避免主节点带宽占用过大。此外,如果从节点过多,也可以调整主从复制的拓扑结构,由一主多从结构变为树状结构(中间的节点既是其主节点的从节点,也是其从节点的主节点);但使用树状结构应该谨慎:虽然主节点的直接从节点减少,降低了主节点的负担,但是多层从节点的延迟增大,数据一致性变差;且结构复杂,维护相当困难。

主节点重启

主节点重启可以分为两种情况来讨论,一种是故障导致宕机,另一种则是有计划的重启。

主节点宕机

主节点宕机重启后,runid会发生变化,因此不能进行部分复制,只能全量复制。

实际上在主节点宕机的情况下,应进行故障转移处理,将其中的一个从节点升级为主节点,其他从节点从新的主节点进行复制;且故障转移应尽量的自动化,后面文章将要介绍的哨兵便可以进行自动的故障转移。

安全重启:debug reload

在一些场景下,可能希望对主节点进行重启,例如主节点内存碎片率过高,或者希望调整一些只能在启动时调整的参数。如果使用普通的手段重启主节点,会使得runid发生变化,可能导致不必要的全量复制。

为了解决这个问题,Redis提供了debug reload的重启方式:重启后,主节点的runid和offset都不受影响,避免了全量复制。

如下图所示,debug reload重启后runid和offset都未受影响:

但debug reload是一柄双刃剑:它会清空当前内存中的数据,重新从RDB文件中加载,这个过程会导致主节点的阻塞,因此也需要谨慎。

从节点重启

从节点宕机重启后,其保存的主节点的runid会丢失,因此即使再次执行slaveof,也无法进行部分复制。

网络中断

如果主从节点之间出现网络问题,造成短时间内网络中断,可以分为多种情况讨论。

第一种情况:网络问题时间极为短暂,只造成了短暂的丢包,主从节点都没有判定超时(未触发repl-timeout);此时只需要通过REPLCONF ACK来补充丢失的数据即可。

第二种情况:网络问题时间很长,主从节点判断超时(触发了repl-timeout),且丢失的数据过多,超过了复制积压缓冲区所能存储的范围;此时主从节点无法进行部分复制,只能进行全量复制。为了尽可能避免这种情况的发生,应该根据实际情况适当调整复制积压缓冲区的大小;此外及时发现并修复网络中断,也可以减少全量复制。

第三种情况:介于前述两种情况之间,主从节点判断超时,且丢失的数据仍然都在复制积压缓冲区中;此时主从节点可以进行部分复制。

复制相关的配置

这一节总结一下与复制有关的配置,说明这些配置的作用、起作用的阶段,以及配置方法等;通过了解这些配置,一方面加深对Redis复制的了解,另一方面掌握这些配置的方法,可以优化Redis的使用,少走坑。

配置大致可以分为主节点相关配置、从节点相关配置以及与主从节点都有关的配置,下面分别说明。

与主从节点都有关的配置

首先介绍最特殊的配置,它决定了该节点是主节点还是从节点:

1)   slaveof <masterip> <masterport>:Redis启动时起作用;作用是建立复制关系,开启了该配置的Redis服务器在启动后成为从节点。该注释默认注释掉,即Redis服务器默认都是主节点。

2)   repl-timeout 60:与各个阶段主从节点连接超时判断有关,见前面的介绍。

主节点相关配置

1)   repl-diskless-sync no:作用于全量复制阶段,控制主节点是否使用diskless复制(无盘复制)。所谓diskless复制,是指在全量复制时,主节点不再先把数据写入RDB文件,而是直接写入slave的socket中,整个过程中不涉及硬盘;diskless复制在磁盘IO很慢而网速很快时更有优势。需要注意的是,截至Redis3.0,diskless复制处于实验阶段,默认是关闭的。

2)   repl-diskless-sync-delay 5:该配置作用于全量复制阶段,当主节点使用diskless复制时,该配置决定主节点向从节点发送之前停顿的时间,单位是秒;只有当diskless复制打开时有效,默认5s。之所以设置停顿时间,是基于以下两个考虑:(1)向slave的socket的传输一旦开始,新连接的slave只能等待当前数据传输结束,才能开始新的数据传输 (2)多个从节点有较大的概率在短时间内建立主从复制。

3)   client-output-buffer-limit slave 256MB 64MB 60:与全量复制阶段主节点的缓冲区大小有关,见前面的介绍。

4)   repl-disable-tcp-nodelay no:与命令传播阶段的延迟有关,见前面的介绍。

5)   masterauth <master-password>:与连接建立阶段的身份验证有关,见前面的介绍。

6)   repl-ping-slave-period 10:与命令传播阶段主从节点的超时判断有关,见前面的介绍。

7)   repl-backlog-size 1mb:复制积压缓冲区的大小,见前面的介绍。

8)   repl-backlog-ttl 3600:当主节点没有从节点时,复制积压缓冲区保留的时间,这样当断开的从节点重新连进来时,可以进行部分复制;默认3600s。如果设置为0,则永远不会释放复制积压缓冲区。

9)   min-slaves-to-write 3与min-slaves-max-lag 10:规定了主节点的最小从节点数目,及对应的最大延迟,见前面的介绍。

从节点相关配置

1)   slave-serve-stale-data yes:与从节点数据陈旧时是否响应客户端命令有关,见前面的介绍。

2)   slave-read-only yes:从节点是否只读;默认是只读的。由于从节点开启写操作容易导致主从节点的数据不一致,因此该配置尽量不要修改。

单机内存大小限制

fork操作对Redis单机内存大小的限制。实际上在Redis的使用中,限制单机内存大小的因素非常之多,下面总结一下在主从复制中,单机内存过大可能造成的影响:

(1)切主:当主节点宕机时,一种常见的容灾策略是将其中一个从节点提升为主节点,并将其他从节点挂载到新的主节点上,此时这些从节点只能进行全量复制;如果Redis单机内存达到10GB,一个从节点的同步时间在几分钟的级别;如果从节点较多,恢复的速度会更慢。如果系统的读负载很高,而这段时间从节点无法提供服务,会对系统造成很大的压力。

(2)从库扩容:如果访问量突然增大,此时希望增加从节点分担读负载,如果数据量过大,从节点同步太慢,难以及时应对访问量的暴增。

(3)缓冲区溢出:(1)和(2)都是从节点可以正常同步的情形(虽然慢),但是如果数据量过大,导致全量复制阶段主节点的复制缓冲区溢出,从而导致复制中断,则主从节点的数据同步会全量复制->复制缓冲区溢出导致复制中断->重连->全量复制->复制缓冲区溢出导致复制中断……的循环。

(4)超时:如果数据量过大,全量复制阶段主节点fork+保存RDB文件耗时过大,从节点长时间接收不到数据触发超时,主从节点的数据同步同样可能陷入全量复制->超时导致复制中断->重连->全量复制->超时导致复制中断……的循环。

此外,主节点单机内存除了绝对量不能太大,其占用主机内存的比例也不应过大:最好只使用50%-65%的内存,留下30%-45%的内存用于执行bgsave命令和创建复制缓冲区等。

info Replication

在Redis客户端通过info Replication可以查看与复制相关的状态,对于了解主从节点的当前状态,以及解决出现的问题都会有帮助。

主节点:

从节点:

对于从节点,上半部分展示的是其作为从节点的状态,从connectd_slaves开始,展示的是其作为潜在的主节点的状态。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值