第15章 复制
在Redis中,可以通过执行SLAVEOF命令或者设置slaveof选项,让从服务器来备份主服务器上的数据。 Redis的复制功能主要分为同步和命令传播。 同步主要是指从服务器的状态更新为主服务器的状态。同步具体又细分为完整重同步和部分重同步(Redis 2.8以后的版本才有)。 #####完整重同步 一般是从服务器向主服务器发送命令请求,主服务向从服务器发送RDB文件及缓冲区保存的写命令,从服务器根据RDB文件和缓冲区保存的写命令恢复数据库状态。 具体步骤如下: 1)从服务器向主服务器发送SYNC命令。 2)收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。 3)当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。 4)主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。 如下图所示:
命令传播
在同步操作执行完毕之后的这个时间点,主从服务器两者的数据库将达到一致状态。但之后当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主从服务器状态不再一致,所以命令传播就是主服务器执行一条写命令后,也会把这条写命令发送给从服务器执行。
部分重同步(Redis2.8以后版本才有) 对于从服务器初次复制的场景来说,完整重同步+命令传播已经可以完美得满足需求了,但是如果从服务器是断线后重复制,因为从服务器本身拥有主服务器的数据,只是缺少断线期间的数据修改,采用完整重同步+命令传播会效率比较低。所以就有了部分重同步只会向从服务器发送断线期间的写命令。 部分重同步的实现由三部分组成,复制偏移量,复制积压缓冲区,服务器运行ID。 复制偏移量 指的是执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量: ·主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。 ·从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。 这样通过对比主从服务器的复制偏移量可以知道主从服务器目前的数据状态。 复制积压缓冲区 复制积压缓冲区是由主服务器维护的一个固定长度先进先出队列,保存了主服务器执行的写命令。默认大小为1MB。队列长度是固定的,当元素满了时,会将最先入队的元素弹出,再将新元素放入队列。 服务器运行ID 每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID。当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。 当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID: ·如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作。 ·相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务“·相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作。
PSYNC命令的实现
PSYNC有两种模式,可以进行完整重同步和部分重同步。从服务器在开始一次新的复制时将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步。如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC 命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作。具体流程图如下:
复制的实现
向从服务发送SLAVEOF命令,可以让一个从服务器复制主服务。 例如:向从服务器发送下面的命令,复制地址为127.0.0.1,端口为6379的主服务器的数据。
SLAVEOF 127.0.0.1 6379点击复制代码复制出错复制成功
实现步骤如下: 1.在从服务redisServer的设置主服务器的地址masterhost和端口masterport
struct redisServer { // 主服务器的地址 char *masterhost; // 主服务器的端口 int masterport; };点击复制代码复制出错复制成功
2.建立Socket连接 3.发送PING命令 4.身份验证 5.发送端口信息 6.同步 7.命令传播,心跳检测,检测主从服务器的网络连接状态,“辅助实现min-slaves配置选项,检测命令丢失。
第16章 哨兵
哨兵系统指的是由一个或多个哨兵实例组成的哨兵系统可以监视任意多个主服务器及属下的所有从服务器,在被监视的主服务器进入下线状态时,自动将某个从服务器升级为主服务器,并且由新的主服务代替旧的主服务器继续处理命令请求。 #####启动哨兵服务器 哨兵服务器本质上搜索一个运行在特殊模式下的Redis服务器,哨兵服务器启动的实现主要分为三步: 1.初始化服务器。与普通Redis服务器不同的是,初始化时不需要通过载入RDB文件或者AOF文件还原数据库状态。下图为哨兵服务器的主要功能: 2.使用哨兵专用代码。也就是将一部分普通Redis服务器使用的代码替换为哨兵服务器使用的代码。例如:普通Redis服务器使用redis.c/redisCommandTable作为服务器的命令表,因为SET,SBSIZE等很多命令哨兵服务器不会执行,所以哨兵服务器使用sentinel.c/sentinelcmds作为服务器的命令表,并且其中的INFO命令会使用Sentinel模式下的专用实现sentinel.c/sentinelInfoCommand函数,而不是普通Redis服务器使用的实现redis.c/infoCommand函数。 3.初始化哨兵服务器的状态。普通服务器的一般状态仍然由redis.h/redisServer结构保存,哨兵服务器会初始化一个sentinel.c/sentinelState结构,这个结构保存了服务器中所有和Sentinel功能有关的状态。
struct sentinelState { // 当前纪元,用于实现故障转移 uint64_t current_epoch; // 保存了所有被这个哨兵服务器监视的主服务器 // 字典的键是主服务器的名字 // 字典的值则是一个指向sentinelRedisInstance结构的指针 dict *masters; // 是否进入了TILT模式? int tilt; // 目前正在执行的脚本的数量 int running_scripts; // 进入TILT模式的时间 mstime_t tilt_start_time; // 最后一次执行时间处理器的时间 mstime_t previous_time; // 一个FIFO队列,包含了所有需要执行的用户脚本 list *scripts_queue; } sentinel;点击复制代码复制出错复制成功
保存了被监视的主服务器信息的sentinelRedisInstance结构
typedef struct sentinelRedisInstance { // 标识值,记录了实例的类型,以及该实例的当前状态 int flags; // 实例的名字 // 主服务器的名字由用户在配置文件中设置 // 从服务器以及Sentinel的名字由Sentinel自动设置 // 格式为ip:port,例如"127.0.0.1:26379" char *name; // 实例的运行ID char *runid; // 配置纪元,用于实现故障转移 uint64_t config_epoch; // 实例的地址 sentinelAddr *addr; // SENTINEL down-after-milliseconds选项设定的值 // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down) mstime_t down_after_period; // SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的quorum参数 // 判断这个实例为客观下线(objectively down )所需的支持投票数量 int quorum; // SENTINEL parallel-syncs <master-name> <number> 选项的值 // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量 int parallel_syncs; // SENTINEL failover-timeout <master-name> <ms> 选项的值 // 刷新故障迁移状态的最大时限 mstime_t failover_timeout; // ... } sentinelRedisInstance;点击复制代码复制出错复制成功
4.创建连向主服务器的网络连接。哨兵服务器会创建两个连向被监视的主服务器的异步网络连接,一个命令连接,用于向主服务器发送命令,并接受命令回复。另一个是订阅连接,用于订阅主服务器的sentinel:hello频道。通过这个频道,哨兵服务器可以通过主服务器了解其他哨兵服务器,从服务器等信息。
命令连接-获取主服务器信息
哨兵服务器会以每十秒一次的频率,通过命令链接向被监视的主服务器发送INFO命令,并根据回复来获取主服务器的当前信息。 能获取到的信息如下: 1.主服务器的相关信息(运行ID,角色等) 2.主服务器下属的所有从服务器的信息(服务器的角色,IP,端口,在线状态等) 根据这些信息,哨兵对象可以更新自身的sentinelRedisInstance结构中的主服务器和从服务器信息。(可以自动发现从服务器的信息)
命令连接-获取从服务器的信息
当哨兵对象发现新的从服务器出现时, 会为它创建实例结构,而且会创建连接到从服务器的命令连接和订阅连接。并且会以每十秒一次的频率通过命令连接向从服务器发送INFO命令,获取从服务器及它所属的主服务器的信息。主要获得的信息如下: 从服务器的运行ID,角色role。 主服务器的IP地址,端口号master_port,主从服务器的连接状态,从服务器的优先级slave_priority,从服务器的复制偏移量slave_repl_offset。 下图展示了根据上面的INFO命令回复对从服务器的实例结构进行更新之后的情况:
向主服务器和从服务器发送信息
在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送PUBLISH的命令,格式如下:
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>点击复制代码复制出错复制成功
参数包括哨兵服务器的IP,端口,运行ID,配置纪元。主服务器的名字,IP,端口,配置纪元。 以下是一条Sentinel通过PUBLISH命令向主服务器发送的信息示例:
"127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0,mymaster,127.0.0.1,6379,0点击复制代码复制出错复制成功
这个示例包含了以下信息: ·Sentinel的IP地址为127.0.0.1端口号为26379,运行ID为e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,当前的配置纪元为0。 ·主服务器的名字为mymaster,IP地址为127.0.0.1,端口号为6379,当前的配置纪元为0。
接收来自主服务器和从服务器的频道信息
当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令:
SUBSCRIBE __sentinel__:hello点击复制代码复制出错复制成功
Sentinel对sentinel:hello频道的订阅会一直持续到Sentinel与服务器的连接断开为止。对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的sentinel:hello频道发送信息,又通过订阅连接从服务器的sentinel:hello频道接收信息。当sentinel1向服务器的sentinel:hello频道发送一条信息时,所有订阅了sentinel:hello频道的Sentinel(包括sentinel1自己在内)都会收到这条信息,然后对相应主服务器的实例结构进行更新。
更新sentinels字典
Sentinel为主服务器创建的实例结构中的sentinels字典保存了除Sentinel本身之外,还保存了所有同样监视这个主服务器的其他Sentinel的信息。当一个Sentinel收到其他Sentinel发来的信息时,会对消息解析,更新sentinels字典。
·sentinels字典的键是其中一个Sentinel的名字,格式为ip:port,比如对于IP地址为127.0.0.1,端口号为26379的Sentinel来说,这个Sentinel在sentinels字典中的键就是"127.0.0.1:26379"。 ·sentinels字典的值则是键所对应Sentinel的实例结构,比如对于键"127.0.0.1:26379"来说,这个键在sentinels字典中的值就是IP为127.0.0.1,端口号为26379的Sentinel的实例结构。 #####“创建连向其他Sentinel的命令连接 当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:Sentinel A有连向Sentinel B的命令连接,而Sentinel B也有连向Sentinel A的命令连接,如下图所示: Sentinel之间不会创建订阅连接 Sentinel在连接主服务器或者从服务器时,会同时创建命令连接和订阅连接,但是在连接其他Sentinel时,却只会创建命令连接,而不创建订阅连接。因为Sentinel需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新Sentinel,所以才需要建立订阅连接,而相互已知的Sentinel只要使用命令连接来进行通信就足够了。
检测主观下线状态
在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。 ·有效回复:实例返回+PONG、-LOADING、-MASTERDOWN三种回复的其中一种。 ·无效回复:实例返回除+PONG、-LOADING、-MASTERDOWN三种回复之外的其他回复,或者在指定时限内没有返回任何回复。 Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线所需的时间长度:如果一个实例在down-after-milliseconds毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的flags属性中打开SRI_S_DOWN标识,以此来表示这个实例已经进入主观下线状态。
检测客观下线状态
当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel发送"SENTINEL is-master-down-by-addr”命令,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。 客观下线状态的判断条件 Sentinel配置中有一个quorum属性,当有quorum个数的Sentinel认为主服务进入下线状态时,Sentinel便将主服务器判定位客户下线。(每个Sentinel的quorum可以不同)。
选举领头Sentinel
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。 以下是Redis选举领头Sentinel的规则和方法: 1.每个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINEL is-master-down-by-addr命令,要求后者将前者设置为局部领头Sentinel,每个Sentinel设置局部领头Sentinel规则都是先到先得,最先向目标Sentinel发送设置要求的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝。 2.局部领头一旦设置,目标Sentinel会将配置纪元+1,并且给源Sentinel回复局部领头Sentinel的运行ID和配置纪元。 3.源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel。 4.如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。 5.如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。
故障转移
在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤: 1.在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。 挑选规则: 1)删除列表中所有处于下线或者断线状态的从服务器 2)删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器 3)删除所有与已下线主服务器连接断开超过down-after-milliseconds10毫秒的从服务器:down-after-milliseconds选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds10毫秒的从服务器,则可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。 之后,领头Sentinel将根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器。 2.领头Sentinel向从服务器发送SLAVEOF命令,让已下线主服务器属下的所有从服务器改为复制新的主服务器。 3.当这个旧的主服务器重新上线时,领头Sentinel向它发送SLAVEOF命令将已下线主服务器设置为新的主服务器的从服务器。