引言
Redis高可用部署有以下两种方式:哨兵模式(sentinel) 和集群模式。
一、哨兵模式
哨兵模式由一个或多个哨兵实例组成的哨兵系统,可以监视任意多个主服务器,以及从属主服务器的所有从服务器。当主服务器进入下线状态(宕机、下线)时,哨兵系统自动将该主服务器的某个从服务器提升为新的主服务器,代替原来的主服务器继续接收并处理命令请求。
1、哨兵启动初始化
启动一个哨兵可使用命令redis-sentinel /path/sentinel.conf
或者命令redis-server /path/sentinel.conf --sentinel
。一个sentinel启动时,会经过以下步骤:
- 初始化服务器:初始化为一个普通的Redis服务器。所以sentinel本质上是一个运行在特殊模式下的Redis服务器。但是与普通Redis服务器不同的是,sentinel不会执行载入RDB或AOF等操作
- 使用sentinel专用代码:将一部分普通Redis服务器使用的代码替换成sentinel专用代码
- 初始化sentinel状态:替换了sentinel专用代码之后,初始化一个保存所有和sentinel功能相关状态的sentinelState结构,代码如下:
strunct sentinelState{
// 当前纪元,用于实现故障转移
uint64_t current_epoch;
// 保存了所有被这个sentinel监视的主服务器
// 字典的键是主服务器的名字
// 字典的值是一个指向sentinelRedisInstance结构的指针
dict *masters;
// 是否进入TILT模式
int tilt;
// 目前正在执行脚本的数量
int running_scripts;
// 进入TILT模式的时间
mstime_t tilt_start_time;
// 最后一次处理时间处理器的时间
mstime_t previous_time;
// 一个FIFO队列,包含了所有需要执行的用户脚本
list *scripts_queue;
} sentinel;
- 初始化sentinel状态的masters属性:字典类型,记录所有被sentinel监视的主服务器的信息。其中字典的key为主服务器的名字,value为sentinelRedisinstance结构
- 创建连向主服务器的网络连接:创建连向主服务器的网络连接后,sentinel成为主服务器的客户端。通过向主服务器发送命令,获取相关信息。sentinel会创建两个异步连接:命令连接和订阅连接
2、心跳检测&故障转移
sentinel默认每隔10秒,向主服务器发送INFO命令。通过命令恢复,一方面获取服务器本身信息(run_id等),另一方面获取该主服务器树属下的所有从服务器信息。
在发现有新的从服务器加入时,sentinel会创建新的从服务器实例结构,同时为这个从服务器创建命令连接和订阅连接并与主服务器相同的频率,即每10秒向从服务器发送INFO命令。
默认情况下,sentinel每隔2秒钟,向其监视的所有主服务器和从服务器发送PUBLISH _sentinel_:hello
命令,这个命令向_sentinel_:hello频道发送自己的信息以表示存活。
在sentinel与主服务器或从服务器建立订阅连接后,sentinel会发送SUBSCRISE _sentinel_:hello
命令。对于监视同一个服务器的多个sentinel来说,一个sentinel发送的消息会被其他sentinel接收到。通过接收到的这些信息,更新本身的sentinels字典用于创建与其他sentinel的命令连接。
2.1、主观下线
sentinel会每隔1秒向与其建立命令连接的实例(主服务、从服务器、sentinel实例)发送PING
命令,判断是否在线。如果一个实例在down-after-milliseconds
毫秒内连续向sentinel返回无效恢复,那么sentinel会对这个实例的结构标识SRI_S_DOWN
,表示这个实例进入主观下线状态。
2.2、客观下线
当sentinel将一个主服务器判断为主观下线后,会询问监视这个主服务器的其他sentinel,得到足够数量的下线状态后(主观或者客观下线),sentinel会将从服务器判定为客观下线,并对主服务器进行故障转移。
3、选举领头Sentinel
当一个主服务器被判定为客观下线时,监视这个主服务器的各个sentinel会进行协商,选举出一个领头sentinel,由领头sentinel对该主服务器执行故障转移操作。选举领头sentinel规则和方法如下:
- 所有在线的sentinel都有被选为领头sentinel的资格
- 每次进行领头选举后,不管选举成功与否,所有sentinel配置纪元的值都会自增一次
- 在一个配置纪元内,所有sentinel都有一次将某个sentinel设置为局部领头sentinel的机会,并且一旦设置之后,在这个纪元内不能再更改
- 一个sentinel(源)向另一个sentinel(目标)发送
SENTINEL is-master-down-by-addr <源run_id>
命令时,表示源sentinel要求目标sentinel将源sentinel设置为局部领头 - 局部领头sentinel遵循先到先得的原则,即目标sentinel接受设置一个局部领头之后接收的设置要求都会拒绝
- 目标sentinel接收到设置要求后,会返回目标sentinel记录的局部领头sentinel的run_id和配置纪元
- 如果某个sentinel被半数以上的sentinel设置成局部领头,那么这个sentinel 成为领头sentinel。由于需要半数以上支持,所有在一个配置纪元里面,只会产生一个领头sentinel
- 如果在规定时间内没有选出领头sentinel,那么所有的sentinel将在一段时间后重新开始选举,直到选出领头sentinel
4、故障转移
选举完领头sentinel后,领头sentinel开始对主服务器执行故障转移操作。步骤如下:
- 在下线主服务器的从属服务器中,挑选出一个,将其转换为主服务器
- 让其他从服务器改为复制新的主服务器
- 将下线的主服务设置为新主服务器的从服务器,当这个下线主服务器重新上线时,就会成为新主服务器的从服务器
4.1 新主服务挑选规则
挑选步骤如下:
- 领头sentinel将下线的主服务的所有从服务器保存到一个列表里面
- 删除列表中已下线或者断线的从服务器
- 删除最近5秒内没有回复过领头sentinel的INFO命令的从服务器
- 删除与下线主服务断开连接超过
down-after-milliseconds * 10
毫秒的从服务器 - 根据从服务器的优先级从高到低进行排序;如果优先级一样,则按复制最大偏移量从大到小排序;复制偏移量还一样的话,按照服务器运行ID从小到大排序
- 排完序后,选取第一个作为新主服务器
二、集群模式
Redis集群是Redis提供的分布式数据库方案,集群的整个数据库被分为16384个槽(slot),每个数据库节点负责处理0个或多个槽(最多16384)
1、新增节点&指派槽
Redis节点在启动时,会设置cluster-enables
为yes来表示该节点开启集群模式。开启了之后,节点除了继续使用serverCron函数,还会在serverCron函数中调用集群模式独有的clusterCron函数。节点启动后,节点会使用clusterNode结构来记录自己的状态,包括节点的ip、端口、创建时间、当前的配置纪元
通过向节点A发送命令CLUSTER MEET <ip> <port>
可以将节点B加入A所在的集群,执行步骤如下:
- 节点A为节点B创建一个clusterNode结构,并添加到自己的clusterState.nodes字典属性中
- 给节点B发送MEET消息
- 节点B收到A的MEET消息后,为A创建clusterNode结构,并添加到自己的clusterState.nodes字典属性中
- B返回给A一条PONG消息
- A接到PONG消息后,返回PING消息,握手完成
新节点加入集群后,与其先建立握手通过gossip协议传播给其他节点,其他节点也与其进行握手。
新加的节点,在集群中的状态还是下线,需要指派槽后才能上线。通过命令CLUSTER ADDSLOTS <slot>
命令指派槽位。节点信息clusterNode结构的slots数组记录了节点负责处理哪些槽位:
- 如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理处理槽位i
- 如果slots数组在索引i上的二进制位的值为0,那么表示节点不负责处理处理槽位i
指派槽位后,clusterState结构中的slots数组对应槽位指针的位置,会指向一个clusterNode结构。
2、在集群中的过程
如上图,当客户端向集群中某个节点发送一个命令(如get 查询等)时,响应的流程如下:
- 节点使用
CRC16(key)&16383
计算出key对应槽位 i - 通过clusterState.slots数组,查询得到负责处理该槽位的节点信息clusterNode
- 如果clusterNode与clusterState.myself相等,说明key所在的槽由当前节点管理,那么直接执行这个命令
- 如果不相等,则返回一个MOVED错误,同时带上clusterNode中的信息,指引客户端重定向至正确的节点执行命令
MOVED错误在集群模式下不会打印出来,而是根据返回的节点信息,打印出转向节点的信息。只有在客户端识别不了MOVED错误才会直接打印出来。
3、重新分片and故障转移
3.1、重新分片
Redis集群的重新分片有集群管理软件redis-trib负责,分片步骤如下:
- redis-trib对目标发送
CLUSTER SETSLOT <slot> IMORTING <soutce_ip>
,让目标节点准备从源节点导入槽slot的键值对 - redis-trib对源节点发送
CLUSTER SETSLOT <slot> MIGRATING <targer_id>
,让源节点准备好将槽slot的键值对迁移至目标节点 - redis-trib向源节点发送
CLUSTERGETKEYSINSLOT <slot> <count>
,获得最多count个键值对的键名 - 根据第3步中获得的每个键名, redis-trib都向源节点发送一个
MIGATE <target_ip> <target_port> <key_name> 0 <timeout>
命令原子的将键迁移到目标节点 - 重复步骤3、4,直到源节点所有槽位都迁移至目标节点
- redis-trib向集群中任意一个几点发送
CLUSTER SETSLOT <slot> NODE <target_id>
命令,将迁移后的槽位管理信息发送至集群,最终集群所有节点同步此信息
重新分片期间,客户端向源节点发送命令时,因为key有可能还在源节点也有可能迁移到目标节点了,所以对于命令的处理逻辑如下:
- 如果在源节点中查询到这个key,直接返回结果
- 如果查不到,返回ASK错误,指引客户端向目标节点再次发送命令
3.2、故障转移
Redis集群分为中节点和从节点,主节点负责处理槽,从节点用于复制主节点,在主节点下线/宕机时,代替主节点。
3.2.1、节点故障检测过程
- 节点会定期向其他节点发送PING消息,如果在规定时间内没有收到PONG消息,那么这个节点会被发送PING消息的节点标记为疑似下线(在clusterState.nodes中查找到对应节点的clusterNode,记录到自己clusterNode.fail_reports中)
- 集群中,如果半数以上的主节点都将同一个主节点标记为疑似下线(PFAIL),那么这个主节点会被标记为下线(FAIL)
3.1.2、故障转移过程
故障转移过程如下:
- 从下线的主节点的所有从节点里面,选择一个从节点
- 选中的从节点执行
SLAVEOF no one
命令,成为新主节点 - 新主节点将下线主节点的所有处理的槽位全部指派给自己
- 新主节点向集群发送一条PONG消息,告诉集群中其他节点新主节点负责处理下线主节点的所有槽
3.1.3、新主节点选举规则
- 集群的配置纪元是一个自增计数器,初始为0;每当某个节点开始一次故障转移,集群配置纪元自增加一
- 对于每个配置纪元,集群每个负责处理槽的主节点都有一次投票机会,而第一个向主节点要求投票的从节点将获得主节点的投票
- 从节点发现自己复制主节点进入下线状态时,向集群广播一条
CLUSTERMSG_TYPE_FALLOVER_AUTH_REQUEST
消息,具有投票权的主节点向这个从节点投票 - 具有投票权的主节点尚未投票,那么将向从节点返回
CLUSTERMSG_TYPE_FALLOVER_AUTH__ACK
,表示支持该从节点成为新主节点 - 从节点统计自己获得多少主节点的支持
- 当从节点收到的支持大于等于
集群节点数n/2 + 1
时,从节点成为新主节点。因为每个配置纪元里,主节点只能投一次票,所以n/2 +1
确保了新主节点只会有一个 - 如果一个配置纪元里面没有从节点收到足够多的投票支持,那么进入新的配置纪元重新开始投票,直到选出新的主节点为止
总结
1、sentinel是运行在特殊模式下的Redis服务器
2、sentinel会与主/从服务器建立命令连接和订阅连接,命令连接用于发送命令请求,订阅连接用于接收指定频道的消息;而sentinel之间只建立命令连接
3、sentinel每隔10秒钟向主从服务器发送INFO命令(故障转移时改为每秒1次);sentinel每隔2秒向_sentinel_:hello频道发送自己的信息向其他sentinel宣告自己的存在;sentinel每隔1秒钟向其他实例(主/从服务器、sentinel)来检测其是否需要判定为主观下线,当超过半数sentinel认为一个实例主观下线后,其被判定为客观下线
4、Redis集群中分为16384个槽,每个节点负责处理一部分槽
5、节点之间通过gossip协议,将自己的信息(ip、port、槽位管理信息等)同步给其他节点
6、redis-trib负责将Redis集群中的重新分配工作
7、如果节点处理的槽正在迁移到目标节点,客户端发送的命令在节点中没查询到结果时,节点会返回ASK错误指引客户端向目标节点重新发送命令