本文内容为《redis设计与实现》一书学习笔记。本文主要概述十五到十六章内容。
第十五章 复制
可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器。如:
127.0.0.1:12345> SLAVEOF 127.0.0.1:6379
OK
服务器127.0.0.1:12345将成为127.0.0.1:6379的从服务器。
进行复制中的主从服务器双方的数据库将保存相同的数据,这种现象即“数据库状态一致”。
15.1 旧版复制功能的实现
Redis的复制功能分为同步(sync)和命令传播(commandpropagate)两个操作。
同步:用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。 当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
从服务器通过向主服务器发送SYNC命令来完成同步操作,以下是SYNC命令的执行步骤:
- 从服务器向主服务器发送SYNC命令;
- 收到SYNC命令的主服务器开始执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令;
- 当主服务器的BGSAVE命令执行完成时,主服务器会将生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件;
- 主服务器将记录在缓存区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的状态更新至主服务器当前的状态。
命令传播:在执行同步后,当主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。 主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。
15.2 旧版复制功能的缺陷
在Redis中,从服务器对主服务器的复制可以分为以下两种情况:
- 初次复制:从服务器以前没用复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同;
- 断线后重新复制:处于命令传播阶段的主从服务器因为网络原因中断了复制,但从服务器通过自动重连连接上了主服务器,并继续复制主服务器;
对于初次复制来说,旧版复制没有问题;但是对于断线重连后的复制,旧版复制效率非常低。断线重连后,主从服务器状态不再一致,从服务器向主服务器发送SYNC命令;但主从服务器断开的时间越短,主服务器在断线期间执行的写命令就越少,而执行少量写命令所产生的数据量通常比整个数据库的量要少得多。为了让从服务器补足一小部分缺失的数据,却要让主从服务器重新执行一次SYNC命令(重新发送所有数据),这种做法是很低效的。
15.3 新版复制功能的实现
为了解决旧版复制功能在处理断线重复制情况时的低效问题,Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:
- 完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。
- 部分重同步用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
15.4 部分重同步的实现
部分重同步功能由以下三个部分构成:
- 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。
主服务器和从服务器会分别维护一个复制偏移量(记录发送、收到的字节数)。通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态。 - 主服务器的复制积压缓冲区(replication backlog)。
由主服务器维护的一个固定长度先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面。
因此,复制积压缓冲区会保持这一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量。从服务器断线重连后会通过PSYNC命令将自己的复制偏移量发送给主服务器,主服务器会根据这个偏移量来决定执行何种同步:
- 如果offset偏移量之后的数据(即offset+1开始的数据)仍然存在于复制积压缓冲区里面,那么主服务器会对从服务器执行部分重同步操作;
- 否则,就会执行完整重同步操作,这和SYNC命令是一样的;
复制积压缓冲区的最小大小可以根据公式second*write_size_per_second来估算,其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算);write_size_per_second是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和)。
- 服务器的运行ID(run ID)。
每个Redis服务器,不论主服务器还是从服务器,都会有自己的运行ID。当从服务器初次复制主服务器时,主服务器会将自己的运行ID传送给从服务器,而从服务器会将这个ID保存起来。当从服务器断线并重新连接上主服务器时,从服务器向当前连接的主服务器发送这个ID:如果主服务器发现这个ID和自己一样,那么可以执行部分重同步功能,否则说明这是一个新的从服务器,需要执行完整重同步。
15.5 PSYNC命令的实现
PSYNC命令的调用方法有两种:
- 如果从服务器以前没有复制过任何主服务器,那么在开始一次新的复制时将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步。
- 如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC <runid> <offset>命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作:
- 主服务器返回+FULLRESYNC <runid> <offset>回复,表示主服务器将与从服务器执行完整重同步操作:runid是这个主服务器的运行ID,从服务器会将这个ID保存起来,下一次发送PSYNC命令时使用;offset是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量。
- 主服务器返回+CONTINUE回复,表示主服务器将与从服务器执行部分重同步操作,从服务器等待主服务器将自己缺少的那部分数据发送过来就可以了。
-
主服务器返回-ERR回复,表示主服务器的版本低于Redis 2.8,识别不了PSYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。
PSYNC命令执行完整重同步和部分重同步流程图如下:
关于PSYNC命令,书中给出了一个便于理解的完整例子,但原文较长所以此处省略。
15.7 心跳检测
略
第十六章 Sentinel
Sentinel(哨岗、哨兵)是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
16.1 启动并初始化Sentinel
启动一个Sentinel可以使用命令
$ redis-sentinel /path/to/your/sentinel.conf
或者:
$ redis-server /path/to/your/sentinel.conf --sentinel
Sentinel启动时,需要执行以下步骤:
初始化服务器。Sentinel本质上只是一个运行在特殊模式下的Redis服务器(默认26379端口)。所以启动Sentinel的第一步,就是初始化一个普通的Redis服务器,但是不需要像普通Redis服务器那样载入RDB文件之类的操作,因为它不需要使用Redis数据库功能;
将普通Redis服务器使用的代码替换成Sentinel专用的代码。在Sentinel模式下,Redis服务器不能执行诸如SET、DBSIZE、EVAL等等这些命令,因为服务器根本没有在命令表中载入这些命令。PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE这七个命令就是客户端可以对Sentinel执行的全部命令。
初始化Sentinel状态。
根据给定的配置文件,初始化Sentinel的监视主服务器列表
Sentinel状态中的masters字典记录了所有被Sentinel监视的主服务器的相关信息,字典的键是被监视主服务器的名字,字典的值是被监视主服务对应的实例结构创建连向主服务器的网络连接
Sentinel将成为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关的信息。对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个面向主服务器的异步网络连接:
- 命令连接,专门用于向主服务器发送命令,并接收命令回复。
-
订阅连接,专门用于订阅主服务器的_sentine _l: hello频道。
16.2 获取主服务器信息
Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。
- 一方面是关于主服务器本身的信息,比如runID;
- 另一方面是关于该主服务器下的从服务器的信息,根据这些信息,Sentinel就可以自动发现从服务器;
16.3 获取从服务器信息
当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构之外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。
创建命令连接之后,Sentinel默认也会以十秒一次的频率通过命令连接向从服务器发送INFO命令,以获取从服务器信息。
16.4 向主服务器和从服务器发送信息
在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接,向所有被监视的Redis服务器发送一个命令,向服务器的sentinel:hello频道发送一条信息来向其他Sentinel宣告自己的存在;
PUBLISH _sentinel _:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
16.5 接收来自主服务器和从服务器的频道信息
对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的__sentinel __:hello频道发送信息,又通过订阅连接从服务器的__sentinel __:hello频道接收信息,如下图。
如果一个Redis服务器被多个Sentinel实例监视,那么一个Sentinel向某个被监视的Redis服务器发送的频道信息,会被其他所有监视这个Redis服务器的Sentinel实例接收到,这些Sentinel接收到不是自己发送的频道信息之后,会对其他Sentinel发送的频道信息进行解析,如下图。
Sentinel只会和主服务器和从服务器创建命令连接和订阅连接,Sentinel之间则只创建命令连接。
16.6 检测主观下线状态
在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。
在Sentinel配置文件中有一个配置:down-after-milliseconeds,如果一个实例(包括主服务器、从服务器、其他Sentinel在内)在down-after-milliseconeds毫秒后依然没有返回有效的PING回应,那么Sentinel就会判断该实例为下线;
16.7 检测客观下线状态
当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。
16.8 选举领头Sentinel
当一个主服务器被判定为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,负责执行故障转移操作:
- 所有在线的Sentinel都有资格成为领头Sentinel;
- 每次选举之后,不论选举是否成功,所有Sentinel的配置纪元(configuration epoch,就是一个计数器)都会增加1
- 每个发现主服务器进入客观下线的Sentinel(我的理解:根据16.7,不是所有Sentinel都认为主服务器客观下线,即不是所有Sentinel都参与选举)都会要求别人选举自己成为领头Sentinel
- 最先向目标Sentinel发送选举自己要求的Sentinel将获得选举,其他后来的选举要求将被目标Sentinel拒绝;
- 如果某个Sentinel被半数以上的Sentinel设置为领头Sentinel,那么这个Sentinel将成为领头Sentinel;
- 如果在给定的时间内没有选举出一个领头Sentinel,那么就会过一段时间再继续选举,直到产生领头Sentinel。
Redis中领头Sentinel选举过程其实就是Raft协议应用的一个例子,可以参考此链接。
16.9 故障转移
在选举产生领头Sentinel之后,领头Sentinel就会对已下线的主服务器执行故障转移操作:
- 在已下线的主服务器的所有从服务器里,挑选出一个从服务器,并将其转换为新的主服务器;
- 让已下线的主服务器的所有从服务器改为复制新的主服务器;
- 将已下线服务器作为新的主服务器的从服务器,当这个旧的主服务器开始重新上线时,他就会成为新的主服务器的从服务器;
- 选出新的主服务器
故障转移的第一步就是需要在故障的主服务器的所有从服务器中挑选一个状态良好、数据完整的从服务器,然后向这个从服务器发送SLAVEOF no one命令,将这个从服务器转换成主服务器;
领头Sentinel会将已下线的Sentinel服务器的所有从服务器保存在一个列表里面,然后按照下面的规则过滤:
- 删除列表中所有下线或者断线状态的服务器,保证剩余的从服务器都是正常在线的。
- 删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器,保证剩余的从服务器都是最近成功进行过通信的。
- 删除所有与已下线服务器连接断开超过 down-after-milliseconds * 10 毫秒的从服务器,保证剩余的从服务器没有过早的与主服务器断开连接,即保证剩余的从服务器保存的数据都是比较新的。
之后,领头Sentinel对列表中剩余的从服务器进行排序,选出优先级最高的从服务器。排序的规则是:
首先看服务器优先级(优先级高),然后是复制偏移量(偏移量大),最后是服务器运行ID(ID小)。
- 修改从服务器的复制目标
- 将旧的主服务器变为从服务器