如何分析redis中的高可用方案

50次阅读
没有评论

共计 14029 个字符,预计需要花费 36 分钟才能阅读完成。

丸趣 TV 小编今天带大家了解如何分析 redis 中的高可用方案,文中知识点介绍的非常详细。觉得有帮助的朋友可以跟着丸趣 TV 小编一起浏览文章的内容,希望能够帮助更多想解决这个问题的朋友找到问题的答案,下面跟着丸趣 TV 小编一起深入学习“如何分析 redis 中的高可用方案”的知识吧。

主从复制

用户可以通过 SLAVEOF 命令或者配置,让一个服务器去复制另一个服务器。被复制的服务器称为主服务器,进行复制的服务器称为从服务器。这样你在主服务器上增加键值,同时可以在从服务器上读取。【相关推荐:Redis 视频教程】

复制的过程又分为同步和命令传播两个步骤。

同步

同步将从服务器的数据库状态更新到主服务器当前的数据库状态。

客户端向从服务器发送 SLAVEOF 命令时,从服务器会向主服务器发生 SYNC 命令进行同步,步骤如下:

从服务器向主服务器发生 SYNC 命令。

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

主服务器的 BGSAVE 命令执行完毕后,主服务器将 BGSAVE 生成的 RDB 文件发送给从服务器,从服务器接收并载入这个 RDB 文件,将从服务器的数据库状态更新到主服务器执行 BGSAVE 命令时的数据库状态

主服务器将缓冲区的所有写命令发送给从服务器,从服务器执行这些写命令,将数据库状态更新至主服务器当前数据库状态。

命令传播

同步操作完成之后,主服务器和从服务器的数据库状态是一致的,但主服务器又接收到客户端写命令后,主从数据库之间又产生了数据不一致,这时通过命令传播达到数据库一致。

PSYNC 同步的优化

2.8 之前的同步每次都是全量同步,而如果是从服务器只是断开连接了一会,事实上是不用从头开始同步的,只需要将断开连接这会的数据同步即可。所以 2.8 版本开始使用 PSYNC 来代替 SYNC 命令。

PSYNC 分成全量同步和部分同步两种情况,全量同步就是处理初次同步的状态,而部分同步就是处理断线重连这种情况。

部分同步的实现

部分同步主要使用了以下三部分:

主服务器的复制偏移量和从服务器的复制偏移量

主服务器的复制积压缓冲区

服务器的运行 ID

复制偏移量

主服务器的复制偏移量:主服务器每次向从服务器传播 N 个字节的数据时,就将自己的复制偏移量 + N 从服务器的复制偏移量:从服务器每次收到主服务器传播的 N 个字节数据,就将自己的复制偏移量 +N
如果主从服务器处于一致状态,那么它们的偏移量总是相同的,如果偏移量不相等,那么说明它们处于不一致状态。

复制积压缓冲区

复制积压缓冲区由主服务器维护的一个固定长度的 FIFO 队列,默认大小 1MB,达到最大长度后,最先入队的会被弹出,给新入队的元素让位置。
redis 命令传播的时候不但会发送给从服务器,还会发送给复制积压缓冲区。

当从服务器重连上主服务器时,从服务器会通过 PSYNC 命令将自己的复制偏移量 offset 发送给主服务器,主服务器根据复制偏移量来决定使用部分同步还是全量同步。
如果 offset 偏移量之后的数据还在复制积压缓冲区,那么使用部分同步,反之使用全量同步。
(书上没说是怎么判断的,我猜测应该是拿主复制偏移量减去从复制偏移量,如果大于 1MB 就说明有数据不在缓冲积压区?)

服务器的运行 ID

服务器启动时会生成一个 40 位随机的字符作为服务器运行 ID。

从服务器对主服务器初次复制时,主服务器会将自己的运行 ID 传送给从服务器,而从服务器会将这个运行 ID 保存下来。从服务器断线重连的时候,会将保存的运行 ID 发送过去,如果从服务器保存的运行 ID 和当前主服务器的运行 ID 相同,那么会尝试部分同步,如果不同会执行全量同步。

PSYNC 的整体流程

心跳检测

在命令传播阶段,从服务器会默认以每秒一次的频率,向主服务器发送命令:
REPLICONF ACK replication_offset
其中 replication_offset 就是从服务器当前的复制偏移量。
发送 REPLICONF ACK 命令对于主从服务器有三个作用:

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

辅助实现 min-slaves 选项。

检测命令丢失。

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

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

辅助实现 min-slaves 选项

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

min-slaves-to-write 3
min-slaves-max-lag 10

如果配置如上,就表示如果从服务器的数量少于 3 个,或者 3 个从服务器的延迟都大于或等于 10 秒时,那么主服务器就将拒绝执行写命令。

检测命令丢失

如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么从服务器向主服务器发送 REPLICONF ACK 命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的偏移量,那么主服务器可以根据从服务器的复制偏移量,在复制缓冲区当中找到从服务器缺少的数据,将这些数据重写发送给从服务器。

主从复制总结

其实主从复制就是多备份了一份数据,因为即使有 RDB 和 AOF 进行持久化,但是可能主服务器上整个机器挂掉了,而主从复制可以将主从服务器部署在两台不同的机器上,这样即使主服务器的机器挂掉了,也可以手动切换到从服务器继续服务。

sentinel

主从虽然实现了数据的备份,但当主服务器挂掉时,需要手动的将从服务器切换成主服务器。而 sentinel 就可以实现当主服务器挂掉时,自动将从服务器切换成主服务器。

sentinel 系统可以监视所有的主从服务器,假设 server1 现在下线。当 server1 的下线时长超过用户设定的下线时长上限时,sentinel 系统就会对 server1 执行故障转移:

首先 sentinel 系统会挑选 server1 下的其中一个从服务器,并将这个选中的从服务器升级成新的主服务器。

之后,sentinel 系统会向 server1 属下的所有从服务器发送新的复制命令,让他们成为新主服务器的从服务器。当所有从服务器复制新的主服务器时,故障转移操作执行完毕。

另外,sentinel 还会监视已下线的 server1,在它重新上线时,将它设置为新的主服务器的从服务器。

初始化 sentinel 状态

struct sentinelState { char myid[CONFIG_RUN_ID_SIZE+1]; 
 //  当前纪元,用于实现故障转移
 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; 
 char *announce_ip; 
 int announce_port; 
 unsigned long simfailure_flags; 
 int deny_scripts_reconfig;
 char *sentinel_auth_pass; 
 char *sentinel_auth_user; 
 int resolve_hostnames; 
 int announce_hostnames; 
} sentinel;

初始化 sentinel 状态的 masters 属性

masters 记录了所有被 sentinel 监视的主服务器的相关信息,其中字典的键是被监视服务器的名字,而值是被监视服务器对应着 sentinelRedisInstance 结构。sentinelRedisInstance 被 sentinel 服务器监视的实例,可以是主服务器、从服务器或其他 sentinel 实例。

typedef struct sentinelRedisInstance {
 //  标识值,记录实例的类型,以及该实例的当前状态
 int flags; 
 //  实例的名字
 //  主服务器名字在配置文件中设置
 //  从服务器和 sentinel 名字由 sentinel 自动设置,格式是 ip:port
 char *name; 
 //  运行 id
 char *runid; 
 //  配置纪元,用于实现故障转移
 uint64_t config_epoch; 
 //  实例的地址
 sentinelAddr *addr; /* Master host. */
 //  实例无响应多少毫秒之后,判断为主观下线
 mstime_t down_after_period; 
 //  判断这个实例为客观下线所需的支持投票数量
 unsigned int quorum;
 //  执行故障转移,可以同时对新的主服务器进行同步的从服务器数量
 int parallel_syncs; 
 //  刷新故障迁移状态的最大时限
 mstime_t failover_timeout; 
 //  除了自己外,其他监视主服务器的 sentinel
 //  键是 sentinel 的名字,格式是 ip:port
 //  值是键对应的 sentinel 的实例结构
 dict *sentinels; 
 // ...
} sentinelRedisInstance;

创建连向主服务器的网络连接

初始化 sentinel 的最后一步是创建连向被监视主服务器的网络连接,会创建两个连向主服务器的连接。

命令连接:专门向主服务器发送命令,并接收命令回复。
订阅连接:专门用于订阅主服务器的_sentinel_:hello 频道。

获取主服务器信息

sentinel 默认会每 10 秒,通过命令连接向被监视的主服务器发送 INFO 命令,并通过回复获取主服务器当前的信息。回复可以获得以下信息。

主服务器的 run_id

主服务器下所有从服务器的信息。

根据这些信息可以更新 sentinelRedisInstance 下的 name 字典和 runid 字段。

获取从服务器信息

sentinel 也会创建连接到从服务器的命令连接和订阅连接。

sentinel 默认会每 10 秒,通过命令连接向从服务器发送 INFO 命令,并通过回复获取从服务器当前的信息。回复如下:

从服务器的运行 ID

从服务器的角色 role

主服务器的 ip 和端口

主服务器的连接状态 master_link_status

从服务器的优先级 slave_priority

从服务器的复制偏移变量

根据 info 的回复信息,sentinel 可以更新从服务器的实例结构。

向主服务器和从服务器的订阅连接发送信息

默认情况下,sentinel 会每 2 秒一次,向被监视的主服务器和从服务器发送命令。

s_ip:sentinel 的 ip 地址
s_port:sentinel 的端口号
s_runid:sentinel 的运行 id
s_epoch:sentinel 当前的配置纪元
m_name: 主服务器的名字
m_ip: 主服务器的 ip 地址
m_port: 主服务器的端口号
m_epoch: 主服务器当前的配置纪元
向 sentinel_:hello 频道发送信息,也会被监视同一个服务器的其他 sentinel 监听到(包括自己)。

创建连向其他 sentinel 的命令连接

sentinel 之间会互相创建命令连接。监视同一个嘱咐其的多个 sentinel 将形成相互连接的网络。

sentinel 之间不会创建订阅连接。

检测主观下线状态

sentinel 会每秒一次向所有与它创建了命令连接的实例(主服务器、从服务器、其他 sentinel)发送 ping 命令,通过实例的回复来判断实例是否在线。
有效回复:实例返回 +PONG、-LOADING、-MASTERDOWN 其中一种。
无效回复:以上三种回复之外的其他回复,或者指定时长内没回复。
某个实例在 down-after-milliseconds 毫秒内,连续向 sentinel 返回无效回复。那么 sentinel 就会修改这个实例对应的实例结构,在结构的 flags 属性中打开 SRI_S_DOWN 标识,标识该实例进入主观下线状态。(down-after-milliseconds 可以在 sentinel 的配置文件中配置)

检测客观下线状态

当 sentinel 将一个主服务器判断为主观下线后,为了确认这个主服务器是否真的下线,还会想其他同样监视这个主服务器的其他 sentinel 询问,看其他 sentinel 是否也认为该主服务器下线了。超过一定数量就将主服务器判断为客观下线。

询问其他 sentinel 是否同意该服务器下线

SENTINEL is-master-down-by-addr ip port current_epoch runid

通过 SENTINEL is-master-down-by-addr 命令询问,参数意义如下图:

接收 SENTINEL is-master-down-by-addr 命令

其他 sentinel 接收到 SENTINEL is-master-down-by-addr 命令后,会根据其中主服务器的 ip 和端口,检查主服务器是否下线,然后返回包含三个参数的 Multi Bulk 的回复。

如何分析 redis 中的高可用方案

sentinel 统计其他 sentinel 同意主服务器已下线的数量,达到配置的数量后,则将主服务器的 flags 属性的 SRI_O_DOWN 标识打开,表示主服务器已经进入客观下线状态。

选举领头 sentinel

当一个主服务器被判断成客观下线时,监视这个下线主服务器的各个 sentinel 就会协商选举一个新的领头 sentinel,由这个 sentinel 进行故障转移操作。

如何分析 redis 中的高可用方案

确认主服务器进入客观下线状态后,会再次发送 SENTINEL is-master-down-by-addr 命令来选举出领头 sentinel。

选举规则

监视同一个主服务器的多个在线 sentinel 中每一个都可能成为领头 sentinel。

每次进行领头 sentinel 选举之后,无论选举是否成功,所有 sentinel 的配置纪元(configuration epoch)的值都会自增一次。(配置纪元,其实就是一个计数器)

在一个配置纪元里,所有 sentinel 都有将某个 sentinel 设置成局部 sentinel 的机会,一旦设置在这个配置纪元里就不能再更改。

所有发现主服务器客观下线的 sentinel 都会要求其他 sentinel 将自己设置为局部领头 sentinel,也就是都会发送 SENTINEL is-master-down-by-addr 命令,尝试让其他 sentinel 将自己设置成局部领头 sentinel。

当一个 sentinel 向另一个 sentinel 发送 SENTINEL is-master-down-by-addr 命令时,如果 runid 参数的值不是 *,而是源 sentinel 的 runid, 就表示要目标 sentinel 将自己设置成领头 sentinel。

sentinel 设置局部领头的规则是先到先得,第一个设置为局部领头 sentinel 后,其他的请求都被拒绝。

目标 sentinel 在接收到一条 SENTINEL is-master-down-by-addr 命令后,将向源 sentinel 返回一个命令回复。回复中 leader_runid 参数和 leader_epoch 参数分别记录了目标 sentinel 的局部领头 sentinel 的 runid 和配置纪元。

源 sentinel 接收到回复之后,会比较返回的配置纪元是否和自己的配置纪元相同,如果一样再继续比较返回的局部领头 sentinel 的 runid 是否和自己的 runid 相同,如果一致就表示目标 sentinel 将自己设置成了局部领头 sentinel。

如果某个 sentinel 被半数以上的 sentinel 设置成了局部领头 sentinel,那么它就成为领头 sentinel。

领头 sentinel 需要半数以上支持,并且每个配置纪元内只能设置一次,那么一个配置纪元里,只会出现一个领头 sentinel

如果在一定时限内,每一个 sentinel 被选举成领头 sentinel(没人没获取半数以上选票),那么各个 sentinel 在一段时间之后再次选举,直到选出领头 sentinel

故障转移

故障转移包含以下三个步骤:

在已下线的主服务器下所有从服务器里,挑选出一个从服务器转换成主服务器。

让已下线的主服务器属下的所有从服务器改为复制新的主服务器。

将已经下线的主服务器设置为新服务器的从服务器,旧的主服务器重新上线后,它就成为新的主服务器的从服务器。

选出新的主服务器

已下线的主服务器下所有从服务器里,挑选出一个从服务器,向这个从服务器发送 SLAVEOF no one 命令,将这个从服务器转换成主服务器。

挑选新主服务器的规则

领头的 sentinel 会将已下线主服务器的所有从服务器保存到一个列表里面,然后对这个列表进行过滤,挑选出新的主服务器。

删除列表中所有处于下线或者断线状态的从服务器。

删除列表中所有最近五秒内没有回复过领头 sentinel 的 INFO 命令的从服务器

删除所有与已下线服务器连接断开超过 dwon-after-milliseconds * 10 毫秒的服务器

然后根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的服务器。

如果有多个相同最高优先级的从服务器,那么就根据复制偏移量进行排序,选出最大偏移量的从服务器(复制偏移量最大也代表它保存的数据最新)

如果复制偏移量也相同,那么就根据 runid 进行排序,选其中 runid 最小的从服务器

发送 slaveof no one 命令之后,领头 sentinel 会每秒一次向被升级的从服务器发送 info 命令(平常是每 10 秒一次),如果返回的回复 role 从原来的 slave 变成了 master,那么领头 sentinel 就知道从服务器已经升级成主服务器了。

修改从服务器的复制目标

通过 SLAVEOF 命令来使从服务器复制新的主服务器。当 sentinel 监测到旧的主服务器重新上线后,也会发送 SLAVEOF 命令使它成为新的主服务器的从服务器。

sentinel 总结

sentinel 其实就是一个监控系统,,而 sentinel 监测到主服务器下线后,可以通过选举机制选出一个领头的 sentinel,然后由这个领头的 sentinel 将下线主服务器下的从服务器挑选一个切换成主服务器,而不用人工手动切换。

集群

哨兵模式虽然做到了主从自动切换,但是还是只有一台主服务器进行写操作(当然哨兵模式也可以监视多个主服务器,但需要客户端自己实现负载均衡)。官方也提供了自己的方式实现集群。

节点

每个 redis 服务实例就是一个节点,多个连接的节点组成一个集群。

CLUSTER MEET ip port

向另一个节点发送 CLUSTER MEET 命令,可以让节点与目标节点进行握手,握手成功就能将该节点加入到当前集群。

启动节点

redis 服务器启动时会根据 cluster-enabled 配置选项是否为 yes 来决定是否开启服务器集群模式。

如何分析 redis 中的高可用方案

集群数据结构

每个节点都会使用一个 clusterNode 结构记录自己的状态,并为集群中其他节点都创建一个相应的 clusterNode 结构,记录其他节点状态。

typedef struct clusterNode {
 //  创建节点的时间
 mstime_t ctime; 
 //  节点的名称
 char name[CLUSTER_NAMELEN];
 //  节点标识
 //  各种不同的标识值记录节点的角色(比如主节点或从节点) //  以及节点目前所处的状态(在线或者下线) int flags; 
 //  节点当前的配置纪元,用于实现故障转移
 uint64_t configEpoch;
 //  节点的 ip 地址
 char ip[NET_IP_STR_LEN]; 
 //  保存建立连接节点的有关信息
 clusterLink *link; 
 
 list *fail_reports; 
 // ...
} clusterNode;

clusterLink 保存着连接节点所需的相关信息

typedef struct clusterLink {
 // ...
 //  连接的创建时间
 mstime_t ctime; 
 //  与这个连接相关联的节点,没有就为 null
 struct clusterNode *node; 
 // ...
} clusterLink;

每个节点还保存着一个 clusterState 结构,它记录了在当前节点视角下,集群目前所处的状态,例如集群在线还是下线,集群包含多少个节点等等。

typedef struct clusterState {
 //  指向当前节点 clusterNode 的指针
 clusterNode *myself; 
 //  集群当前的配置纪元,用于实现故障转移
 uint64_t currentEpoch;
 //  集群当前的状态,上线或者下线
 int state; 
 //  集群中至少处理一个槽的节点数量
 int size; 
 //  集群节点的名单(包括 myself 节点) //  字典的键是节点的名字,字典的值为节点对应的 clusterNode 结构
 dict *nodes; 
} clusterState;

CLUSTER MEET 命令的实现

CLUSTER MEET ip port

节点 A 会为节点 B 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典里面。

之后,节点 A 将根据 CLUSTER MEET 命令给定的 IP 地址和端口号,向节点 B 发送一条 MEET 消息。

如果一切顺利,节点 B 将接收到节点 A 发送的 MEET 消息,节点 B 会为节点 A 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典里面。

之后,节点 B 将向节点 A 返回一条 PONG 消息。

如果一切顺利,节点 A 将接收到节点 B 返回的 PONG 消息,通过这条 PONG 消息节点 A 可以知道节点 B 已经成功地接收到了自己发送的 MEET 消息。

之后,节点 A 将向节点 B 返回一条 PING 消息。

如果一切顺利,节点 B 将接收到节点 A 返回的 PING 消息,通过这条 PING 消息节点 B 知道节点 A 已经成功接收到自己返回的 PONG 消息,握手完成。

如何分析 redis 中的高可用方案

槽指派

集群的整个数据库被分为 16384 个槽,每个键都属于 16384 个槽的其中一个,集群中每个节点处理 0 个或 16384 个槽。当所有的槽都有节点在处理时,集群处于上线状态,否则就是下线状态。

CLUSTER ADDSLOTS

CLUSTER ADDSLOTS slot …
通过 CLUSTER ADDSLOTS 命令可以将指定槽指派给当前节点负责,例如:CLUSTER ADDSLOTS 0 1 2 3 4 可以将 0 至 4 的槽指派给当前节点

记录节点的槽指派信息

clusterNode 结构的 slots 属性和 numslot 属性记录了节点负责处理哪些槽:

typedef struct clusterNode {
 
 unsigned char slots[CLUSTER_SLOTS/8];
 
 int numslots;
 // ...
} clusterNode;

slots:是一个二进制数组,一共包含 16384 个二进制位。当二进制位的值是 1,代表节点负责处理该槽,如果是 0,代表节点不处理该槽 numslots:numslots 属性则记录节点负责处理槽的数量,也就是 slots 中值为 1 的二进制位的数量。

传播节点的槽指派信息

节点除了会将自己负责的槽记录在 clusterNode 中,还会将 slots 数组发送给集群中的其他节点,以此告知其他节点自己目前负责处理哪些槽。

typedef struct clusterState { clusterNode *slots[CLUSTER_SLOTS];
} clusterState;

slots 包含 16384 个项,每一个数组项都是指向 clusterNode 的指针,表示被指派给该节点,如果未指派给任何节点,那么指针指向 NULL。

CLUSTER ADDSLOTS 命令的实现

如何分析 redis 中的高可用方案

在集群中执行命令

客户端向节点发送与数据库有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查该槽是否指派给了自己。
如果指派给了自己,那么该节点直接执行该命令。如果没有,那么该节点会向客户端返回一个 MOCED 的错误,指引客户端转向正确的节点,并再次发送执行的命令。

如何分析 redis 中的高可用方案

计算键属于那个槽

如何分析 redis 中的高可用方案

CRC16(key)是计算出键 key 的 CRC16 的校验和,而 16383 就是取余,算出 0 -16383 之间的整数作为键的槽号。

判断槽是否由当前节点负责处理

计算出键所属的槽号 i 后,节点就能判断该槽号是否由自己处理。
如果 clusterState.slots[i]等于如果 clusterState.myself, 那么由自己负责该节点可以直接执行命令。
如果不相等,那么可以获取 clusterState.slots[i]指向如果 clusterNode 的 ip 和端口,向客户端返回 MOVED 错误,指引客户端转向负责该槽的节点。

集群模式下不会打印 MOVED 错误,而是直接自动转向。

重新分片

redis 集群重新分配可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,相关槽所属的键值对也会从源节点移动到目标节点。
重新分片操作是在线进行的,在重新分片的过程中,集群不用下线,源节点和目标节点都可以继续处理命令请求。
redis 集群的重新分片操作是由 redis-trib 负责执行。重新分片执行步骤如下:

redis-trib 对目标节点发送 CLUSTER SETSLOT slot IMPORTING source_id 命令,让目标节点准备好从源节点导入槽 slot 的键值对。

redis-trib 对源节点发送 CLUSTER SETSLOT slot MIGRTING target_id 命令,让源节点准备好将属于槽 slot 的键值对迁移至目标节点。

redis-trib 向源节点发送 CLUSTER GETKEYSINSLOT slot count 命令,获取最多 count 个属于槽的键值对的键名称。

对于步骤 3 获取的每个键名,redis-trib 都向源节点发送一个 MIGRTING target_ip target_port key_name 0 timeout 命令,将被选中的键值对从源节点迁移至目标节点。

重复执行步骤 3 和步骤 4,直到源节点保存的所以属于槽 slot 的键值对都被迁移至目标节点。

redis-trib 向集群中任何一个节点发送 CLUSTER SETSLOT slot NODE target_id 命令,将槽指派给目标节点。这一信息最终会通过消息发送至整个集群。

如何分析 redis 中的高可用方案

CLUSTER SETSLOT IMPORTING 命令实现

typedef struct clusterState {
 // ...
 clusterNode *importing_slots_from[CLUSTER_SLOTS];
} clusterState;

importing_slots_from 记录了当前节点正在从其他节点导入的槽。importing_slots_from[i]不为 null,则指向 CLUSTER SETSLOT slot IMPORTING source_id 命令,source_id 所代表的 clusterNode 结构。

CLUSTER SETSLOT MIGRTING 命令实现

typedef struct clusterState {
 // ...
 clusterNode *migrating_slots_to[CLUSTER_SLOTS];
} clusterState;

migrating_slots_to 记录了当前节点正在迁移至其他节点的槽。migrating_slots_to[i]不为 null,则指向迁移至目标节点所代表的 clusterNode 结构。

ASK 错误

在重新分片期间,源节点向目标节点迁移槽的过程中,可能属于这个槽的一部分键值对一部分保存在源节点当中,而另一部分保存在目标节点当中。
客户端向源节点发送一个与数据库键有关的命令,恰好这个槽正在被迁移。
源节点现在自己的数据库中查找指定的键,如果找到,直接执行。
如果没有找到,节点会检查 migrating_slots_to[i]查看键是否正在迁移,如果在迁移就返回一个 ask 错误,引导客户端转向目标节点。

ASKING

客户端收到 ask 错误之后,会先执行 ASKING 命令,再向目标节点发送命令。ASKING 命令就是打开发送该命令的客户端的 REDIS_ASKING 标识。一般来说客户端发送的键如果不属于自己负责会返回 MOVED 错误(槽只迁移部分,这时槽还不属于目标节点负责),但还会检查 importing_slots_from[i],如果显示节点正在导入槽 i,并且发送命令的客户端带有 REDIS_ASKING 标识,那么它就会破例执行一次该命令。

如何分析 redis 中的高可用方案

集群的故障转移

集群的故障转移效果和哨兵模式类似,也是将从节点升级成主节点。旧的主节点重新上线后将会成为新主节点的从节点。

故障检测

集群中每个节点会定期的向集群中其他节点发送 PING 消息,检测对方是否在线,如果指定时间内没有收到 PONG 消息,那么就将该节点标记为疑似下线。clusterState.nodes 字典中找到该节点的 clusterNode 结构,将 flags 属性修改成 REDIS_NODE_PFAIL 标识。
集群中各个节点会互相发送消息来交换集群中各个节点的状态,例如:主节点 A 得知主节点 B 认为主节点 C 进入了疑似下线状态,主节点 A 会在 clusterState.nodes 字典中找到节点 C 的 clusterNode 结构,并将主节点 B 的下线报告添加到 clusterNode 结构的 fail_reports 链表当中。
每一个下线报告由一个 clusterNodeFailReport 结构表示

typedef struct clusterNodeFailReport {
 struct clusterNode *node; 
 //  最后一次收到下线报告的时间
 mstime_t time; 
} clusterNodeFailReport;

如果一个集群当中,半数以上负责处理槽的主节点都将某个主节点 X 报告为疑似下线。那么这个主节点 X 将被标记为已下线。将主节点 X 标记成已下线的节点会向集群广播一条关于主节点 X 的 FAIL 消息。所有收到这条 FAIL 消息的节点都会将主节点 X 标记成已下线。

故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态,从节点将开始对下线主节点进行故障转移。

复制下线主节点的所有从节点,会有一个主节点被选中。

被选中的从节点会执行 SLAVEOF no one 命令,成为新的主节点。

新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。

新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成主节点。这个主节点已经接管了已下线节点负责处理的槽。

新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举新的主节点

新的主节点通过选举产生

集群的配置纪元是一个自增计数器,它的初始值为 0。

当集群的某个节点开始一次故障转移操作,集群的配置纪元的值加 1。

对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,第一个想主节点要求投票的从节点将获得主节点的投票。

当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息,并具有投票权的主节点向这个从节点投票。

如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。

每个参与选举的从节点都会接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。

如果集群里有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 N / 2 + l 张支持票时,这个从节点就会当选为新的主节点。

因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N 个主节点进行投票,那么具有大于等于 N / 2 + l 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。

如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进人一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

主节点选举的过程和选举领头 sentinel 的过程非常相似。

数据丢失

主从复制数据丢失

主从复制之间是异步执行的,有可能 master 的部分数据还没来得及同步到从数据库,然后 master 就挂了,这时这部分未同步的数据就丢失了。

脑裂

脑裂就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master,这个时候,集群里面就会有 2 个 master,也就是所谓的脑裂。
此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 的写数据。
master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据将会清空,重新从新的 master 复制数据,导致数据丢失。

减少数据丢失的配置

min-slaves-to-writ 1
min-slaves-max-lag 10

上述配置表示,如果至少有 1 个从服务器超过 10 秒没有给自己 ack 消息,那么 master 不再执行写请求。

主从数据不一致

当从数据库因为网络原因或者执行复杂度高命令阻塞导致滞后执行同步命令,导致数据同步延迟,造成了主从数据库不一致。

感谢大家的阅读,以上就是“如何分析 redis 中的高可用方案”的全部内容了,学会的朋友赶紧操作起来吧。相信丸趣 TV 丸趣 TV 小编一定会给大家带来更优质的文章。谢谢大家对丸趣 TV 网站的支持!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-15发表,共计14029字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)