type
status
date
slug
summary
tags
category
icon
password
1、概述
Redis 集群的发展历史
- redis 为了解决单机故障引入了主从模式,但主从模式存在一个问题:master 节点故障后服务,需要人为的手动将 slave 节点切换成为 maser 节点后服务才恢复。
- redis 为解决这一问题又引入了哨兵模式,哨兵模式能在 master 节点故障后能自动将 salve 节点提升成 master 节点,不需要人工干预操作就能恢复服务可用。
- 但是主从模式、哨兵模式都没有达到真正的数据sharding存储,每个redis实例中存储的都是全量数据,各大厂等不及了官方的 sharding 集群版本,陆陆续续开发了自己的 redis 数据分片集群模式,比如:Twemproxy、Codis 等。
- redis cluster 在2015年才正式发布,实现了真正的数据分片存储。
集群模式 | 描述 | 优点 | 缺点 |
主从复制模式 | 为了避免单点故障和提供读写分离功能,Redis 提供了复制(replication)功能实现master数据库中的数据更新后,会自动将更新的数据同步到其他slave数据库上。
1. master节点提供读写功能,一个master节点可以有多个salve节点。master节点通过主从复制向slave节点同步写操作
2. slave节点只提供读功能,一个salve节点可以有slave节点。 | 主从结构具有读写分离,提高效率、数据备份,提供多个副本等优点。 | 1. 不具备自动容错和恢复功能,主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预。
2. master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题 |
Sentinel(哨兵)模式 | 哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。哨兵本身就是一个运行在特殊模式下的Redis进程,不存储数据,只用来监控。哨兵本身也需要高可用,所以一般会搭建3个以上哨兵节点来组成哨兵集群。
哨兵集群的作用有三个:
1. 监控:判断主从下线。
2. 选主:选出新的主库。
3. 通知:通知从库与新主库实现数据同步,通知客户端与新主库连接。 | 相比起主从复制模式,哨兵模式下master挂掉可以自动进行切换,系统可用性更高 | 1. 仍然只有一个Redis主机来接收和处理写请求,写操作还是受单机瓶颈影响,没有实现真正的分布式架构。
2. 每台 Redis 服务器都存储全量数据,浪费内存。 |
Cluster模式 | redis3.0之后,redis官方开始支持cluster模式。cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。cluster模式搭建最少需要6个节点(3个master3个slave)。
1. 每个master节点存储一部分数据分片。
2. 每个master节点可以挂一个到多个slave节点,salve节点负责从master节点同步数据,当master节点挂了之后,slave节点会自动切换为master节点。
3. 完全去中心化,所有节点彼此互联(PING-PONG机制),集群内部通过gossip协议进行通讯,每个节点都保存整个cluster的状态。
4. 客户端与redis节点直连,而且不需要连接集群所有节点,只需要连接任何一个可用节点即可。 | 1. 部署简单
2. 可扩展性高
3. 自动故障转移 | 1. 不支持多个key的操作,keys命令也只会显示连接的当前节点的key数
2. 不支持多数据库空间,只支持0库
3. 客户端实现复杂,客户端必须以 cluster 的协议与 redis 进行通讯,目前仅 JedisCluster 相对成熟 |
集群方案的选择:
- redis cluster 主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用 redis cluster,redis cluster的性能和高可用性均优于sentinel模式。
- 如果数据量不是很大时,使用 sentinel 模式就够了。
- 主从复制模式不够稳定,一般只在测试环境使用。
2、主从复制模式
主从服务器之间采用的是「读写分离」的方式:主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。
也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。
主从复制过程分为两部分:
- 同步:从服务器和主服务器的第一次同步数据,主服务器会把全量数据发送给从服务器,从服务器接收全量数据,把数据状态恢复成和主服务器一致。
- 传播:后续主服务器发生写操作命令时,会把这些写操作命令作为增量数据发送给从服务器,从服务器重新执行这些写操作命令,通过这种方式保持和主服务器数据状态一致。
2.1 同步过程
我们可以使用
replicaof
(Redis 5.0 之前使用 slaveof)命令形成主服务器和从服务器的关系。比如,现在有服务器 A 和 服务器 B,我们在服务器 B 上执行下面这条命令:
接着,服务器 B 就会变成服务器 A 的「从服务器」,然后与主服务器进行第一次同步。
主从服务器间的第一次同步的过程可分为三个阶段:
- 第一阶段是建立链接、协商同步;
- 第二阶段是主服务器同步数据给从服务器;
- 第三阶段是主服务器发送新写操作命令给从服务器。
第一阶段:建立链接、协商同步
执行了 replicaof 命令后,从服务器就会给主服务器发送
psync
命令,表示要进行数据同步。psync 命令包含两个参数,分别是主服务器的 runID 和复制进度 offset。
- runID,每个 Redis 服务器在启动时都会自动生产一个随机的 ID 来唯一标识自己。当从服务器和主服务器第一次同步时,因为不知道主服务器的 run ID,所以将其设置为 "?"。
- offset,表示复制的进度,第一次同步时,其值为 -1。
主服务器收到 psync 命令后,会用
FULLRESYNC
作为响应命令返回给对方。并且这个响应命令会带上两个参数:主服务器的 runID 和主服务器目前的复制进度 offset。从服务器收到响应后,会记录这两个值。
FULLRESYNC 响应命令的意图是采用全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器。
所以,第一阶段的工作时为了全量复制做准备。
第二阶段:主服务器同步数据给从服务器
接着,主服务器会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。
从服务器收到 RDB 文件后,会先清空当前的数据,然后载入 RDB 文件。
这里有一点要注意,主服务器生成 RDB 这个过程是不会阻塞主线程的,因为 bgsave 命令是产生了一个子进程来做生成 RDB 文件的工作,是异步工作的,这样 Redis 依然可以正常处理命令。
但是,这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。
那么为了保证主从服务器的数据一致性,主服务器在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里:
- 主服务器生成 RDB 文件期间;
- 主服务器发送 RDB 文件给从服务器期间;
- 「从服务器」加载 RDB 文件期间;
第三阶段:主服务器发送新写操作命令给从服务器
在主服务器生成的 RDB 文件发送完,从服务器收到 RDB 文件后,丢弃所有旧数据,将 RDB 数据载入到内存。完成 RDB 的载入后,会回复一个确认消息给主服务器。
接着,主服务器将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,从服务器执行来自主服务器 replication buffer 缓冲区里发来的命令,这时主从服务器的数据就一致了。
至此,主从服务器的第一次同步的工作就完成了。
2.2 传播过程
主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接。
后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。
而且这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。
上面的这个过程被称为基于长连接的命令传播,通过这种方式来保证第一次同步后的主从服务器的数据一致性。
2.3 增量复制
主从服务器在完成第一次同步后,就会基于长连接进行命令传播。如果主从服务器间的网络连接断开了,那么就无法进行命令传播了。当主从服务器重新连接的时候,从服务器为了恢复和主服务器的数据一致,有两种方式:
- 和主服务器重新进行一次全量复制。在 Redis 2.8 版本以前确实是这么做,缺点就是资料开销太大了。
- 采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
网络恢复后的增量复制过程:
- 从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
- 主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
- 然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。
那么关键的问题来了,主服务器怎么知道要将哪些增量数据发送给从服务器呢?
答案藏在这两个东西里:
- repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;
- replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此 这个缓冲区里会保存着最近传播的写命令。
网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:
- 如果判断出从服务器要读取的数据还在 repl_backlog_buffer 缓冲区里,那么主服务器将采用增量同步的方式;
- 相反,如果判断出从服务器要读取的数据已经不存在 repl_backlog_buffer 缓冲区里,那么主服务器将采用全量同步的方式。
当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,这个缓冲区缓存将要传播给从服务器的命令。从服务器只需要接收 replication buffer 缓冲区里面的少量增量数据,即可恢复成和主服务器数据一致的状态,这就是增量复制的过程。
replication buffer 缓冲区 和 repl_backlog_buffer 缓冲区的区别:
- replication buffer 缓冲区:主从全量复制的缓冲区,每个从库对应一个。当主节点向从节点发送RDB文件时,如果又接收到了写命令操作,就会把它们暂存在 replication buffer 缓冲区。等RDB文件传输完成,并且在从节点加载完成后,主节点再把复制缓冲区中的写命令发给从节点,进行同步。
- repl_backlog_buffer 缓冲区:主从增量复制的缓冲区,是一个环形缓冲区,整个master进程中只会存在一个,所有从库和主库共享。主节点和从节点进行常规同步时,在把写命令写入 replication buffer 缓冲区后,会把写命令写入 repl_backlog_buffer 缓冲区。如果从节点和主节点间发生了网络断连,等从节点再次连接后,可以从 repl_backlog_buffer 缓冲区中同步尚未复制的命令操作。repl_backlog_buffer 缓冲区是一个环形队列,如果从节点和主节点间的网络断连时间过⻓,环形队列的旧数据可能被新写入的命令覆盖。此时,从节点就没有办法和主节点进行增量复制了,而是只能进行全量复制。
2.4 手动切换主库
当主数据库崩溃时,需要手动切换从数据库成为主数据库。
- 在从数据库中使用 SLAVE NO ONE 命令将从数据库提升成主数据继续服务。
- 启动之前崩溃的主数据库,然后使用 SLAVEOF 命令将其设置成新的主数据库的从数据库,即可同步数据。
3、Sentinel模式
哨兵(Sentinel)机制的主要作用是实现主从节点故障转移。哨兵其实是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。从“哨兵”这个名字也可以看得出来,它相当于是“观察者节点”,观察的对象是主从节点。
哨兵节点主要负责三件事情:
- 监控:监测主节点是否存活。
- 选主:如果发现主节点挂了,它就会选举一个从节点切换为主节点。
- 通知:选主结束后,把新主节点的相关信息通知给从节点和客户端。
3.1 初始化并启动Sentinel
当一个Sentinel启动时,它需要执行以下步骤:
- 初始化服务器,将普通Redis服务器使用的代码替换成Sentinel专用代码,包括载入 sentinel 模式专用的命令,包括:ping、sentinel、info、subscribe、unsubscribe、psubscribe、punsubscribe。
- 初始化Sentinel状态。
- 根据给定的配置文件,初始化Sentinel的监视主服务器列表。
- 创建连向主服务器的网络连接。对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。另一个是订阅连接,这个连接专门用于订阅主服务器的__sentinel__:hello频道。
3.2 哨兵集群间的互相发现
哨兵在部署的时候不会只部署一个节点,通常用多个节点部署成哨兵集群(最少需要三台机器来部署哨兵集群),通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。
每个哨兵只连接主库,通过 pub/sub 机制,通过订阅名为
__sentinel__:hello
的channel,来获取其他哨兵的信息。3.3 监控集群状态
每个哨兵与 master 和 slave 之间建立命令连接和订阅连接,与其他哨兵之间建立命令连接。哨兵的工作内容有:
- 哨兵间互相通信:定期向 master 和 slave 的_sentinel_:hello频道发送自己的信息。哨兵节点不会直接与其他哨兵节点建立连接,而是通过 pub/sub 机制向 master 的 _sentinel_:hello 频道发送消息来向其他 sentinel 告知自己的存在。其他订阅了该频道的 sentinel 都可以接收到,从而各个 sentinel 互知。
- 获取 master 和 slave 的相关信息:定期(一般10s一次,当 master 被标记为主观下线时,改为1s一次)向 master 和 slave 发送 INFO 命令。通过 INFO 命令可以实现新节点的自动发现,哨兵只需要配置 master 数据库信息就可以自动发现其 slave 信息,获取到 slave 信息后,哨兵也会与 slave 建立连接执行监控。
- 发送心跳判断实例是否在线:定期(1s一次)向 master、slave 和其他哨兵发送 PING 命令。
- 通知客户端:客户端通过 pub/sub 机制从哨兵订阅所有事件,可以获取主从库切换过程中发生的各个重要事件和主从切换后得到新主库的连接信息。
3.3.1 主观下线
哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。
如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。这个「规定的时间」是配置项
down-after-milliseconds
参数设定的,单位是毫秒。3.3.2 客观下线
当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。
当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。
例如,现在有 3 个哨兵,quorum 配置的是 2,那么一个哨兵需要 2 张赞成票,就可以标记主节点为“客观下线”了。这 2 张赞成票包括哨兵自己的一张赞成票和另外两个哨兵的赞成票。
PS:quorum 的值一般设置为哨兵个数的二分之一加 1,例如 3 个哨兵就设置 2。
哨兵判断完主节点客观下线后,哨兵就要开始在多个「从节点」中,选出一个从节点来做新主节点。
3.4 哨兵集群选主
判断 master 客观下线后,哨兵集群需要选出一个 leader 负责故障转移的工作。哨兵集群内部会进行一次选举,采用 Raft 算法选出一个哨兵执行主从切换。
- 发现 master 下线的哨兵节点(我们称他为A)向每个哨兵发送命令,要求对方选自己为领头哨兵
- 如果目标哨兵节点没有选过其他人,则会同意选举 A 为领头哨兵
- 如果有超过一半的哨兵同意选举 A 为领头,则 A 当选
- 如果有多个哨兵节点同时参选领头,此时有可能存在一轮投票无竞选者胜出,此时每个参选的节点等待一个随机时间后再次发起参选请求,进行下一轮投票竞选,直至选举出领头哨兵。
3.5 主从故障转移
主从故障转移操作包含以下四个步骤:
- 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点。
- 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;
- 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
- 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;
3.5.1 选出新的主节点
哨兵集群选出的 leader,从出现故障的 master 的 slave 节点中挑选一个来当选新的 master。按照三个规则依次进行三轮打分,这三个规则分别是从库优先级、从库复制进度以及从库ID号。只要在某一轮中,有从库得分最高,那么它就是主库了。
- 从库优先级:从库配置 slave-priority,优先级高的当选主库。
- 从库复制进度:主从同步有一个 repl_backlog_buffer 缓冲区,主库会用 master_repl_offset 记录当前的最新写操作,从库会用slave_repl_offset这个值记录当前的复制进度。slave_repl_offset 最接近 master_repl_offset 的当选主库。
- 从库ID号:从库ID小的当选主库。
3.5.2 执行主从切换
当新主节点出现之后,哨兵 leader 下一步要做的就是,让已下线主节点属下的所有「从节点」指向「新主节点」,这一动作可以通过向「从节点」发送
SLAVEOF
命令来实现。所有从节点指向新主节点后的拓扑图如下:
3.5.3 通知客户的主节点已更换
通过 Redis 的发布者/订阅者机制来实现的。每个哨兵节点提供发布者/订阅者机制,客户端可以从哨兵订阅消息。
哨兵提供的消息订阅频道有很多,不同频道包含了主从节点切换过程中的不同关键事件,几个常见的事件如下:
客户端和哨兵建立连接后,客户端会订阅哨兵提供的频道。主从切换完成后,哨兵就会向
+switch-master
频道发布新主节点的 IP 地址和端口的消息,这个时候客户端就可以收到这条信息,然后用这里面的新主节点的 IP 地址和端口进行通信了。通过发布者/订阅者机制机制,有了这些事件通知,客户端不仅可以在主从切换后得到新主节点的连接信息,还可以监控到主从节点切换过程中发生的各个重要事件。这样,客户端就可以知道主从切换进行到哪一步了,有助于了解切换进度。
3.5.4 将旧主节点变为从节点
故障转移操作最后要做的是,继续监视旧主节点,当旧主节点重新上线时,哨兵集群就会向它发送
SLAVEOF
命令,让它成为新主节点的从节点。4、Cluster模式
Cluster 即 集群模式,类似MySQL,Redis 集群也是一种分布式数据库方案,集群通过分片(sharding)模式来对数据进行管理,并具备分片间数据复制、故障转移和流量调度的能力。
Redis集群的做法是 将数据划分为 16384(2的14次方)个哈希槽(slots),如果你有多个实例节点,那么每个实例节点将管理其中一部分的槽位,槽位的信息会存储在各自所归属的节点中。以下图为例,该集群有4个 Redis 节点,每个节点负责集群中的一部分数据,数据量可以不均匀。比如性能好的实例节点可以多分担一些压力。
一个Redis集群一共有16384个哈希槽,你可以有1 ~ n个节点来分配这些哈希槽,可以不均匀分配,每个节点可以处理0个 到至多 16384 个槽点。
当16384个哈希槽都有节点进行管理的时候,集群处于online 状态。同样的,如果有一个哈希槽没有被管理到,那么集群处于offline状态。
上面图中4个实例节点组成了一个集群,集群之间的信息通过 Gossip协议 进行交互,这样就可以在某一节点记录其他节点的哈希槽(slots)的分配情况。
4.1 创建集群
集群是由一个个互相独立的节点(readis node)组成的, 所以刚开始的时候,他们都是隔离,毫无联系的。我们需要通过一些操作,把他们聚集在一起,最终才能组成真正的可协调工作的集群。
各个节点的联通是通过 CLUSTER MEET 命令完成的:
具体的做法是其中一个node向另外一个 node(指定 ip 和 port) 发送 CLUSTER MEET 命令,这样就可以让两个节点进行握手(handshake操作) ,握手成功之后,node 节点就会将握手另一侧的节点添加到当前节点所在的集群中。
4.2 集群间通讯
集群中的各个节点通过发送和接收消息(message)来进行通信,Redis集群中的各个节点通过Gossip协议来交换各自关于不同节点的状态信息。Gossip协议实现了无中心式,基本思想就是一个节点周期性地随机选择网络中的一些节点发送消息。Gossip协议由MEET、PING、PONG三种消息实现。
节点发送的消息总共有以下五种:
- MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
- PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
- PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
- FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
- PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。
4.3 数据分片
4.3.1 哈希槽(slots)的划分
整个 Redis 数据库划分为 16384 个哈希槽,集群中的每个节点可以处理 0 个到至多 16384 个槽点,所有节点把 16384个槽位瓜分完成(这意味着某些节点可以分配 0 个槽)。slot 和数据库的划分关系有两种配置方式:
一种是初始化的时候均匀分配 ,使用 cluster create 创建,会将 16384 个slots 平均分配在我们的集群实例上,比如你有 n 个节点,那每个节点的槽位就是 16384 / n 个了 。
另一种是通过 CLUSTER MEET 命令将 node1、node2、ndoe3、node4 4个节点联通成一个集群,刚联通的时候因为还没分配哈希槽,还是处于offline状态。我们使用
cluster addslots
命令来指定。指定的好处就是性能好的实例节点可以多分担一些压力。可以通过 addslots 命令指定哈希槽范围,比如下图中,我们哈希槽是这么分配的:实例 1 管理 0 ~ 7120 哈希槽,实例 2 管理 7121~9945 哈希槽,实例 3 管理 9946 ~ 13005 哈希槽,实例 4 管理 13006 ~ 16383 哈希槽。
4.3.2 哈希槽(slots)的映射
当我们向 Redis 发送一个 key 值,这个 key 必定会映射到上面 16384 个槽的其中一个。slots 与 Redis Key 的映射是通过以下两个步骤完成的:
- 使用 CRC16 算法计算键值对信息的Key,会得出一个 16 bit 的值。
- 将 第1步中得到的 16 bit 的值对 16384 取模,得到的值会在 0 ~ 16383 之间,映射到对应到哈希槽中。当然,可能在一些特殊的情况下,你想把某些key固定到某个slot上面,也就是同一个实例节点上。这时候可以用hash tag能力,强制 key 所归属的槽位等于 tag 所在的槽位。其实现方式为在key中加个{},例如test_key{1}。使用hash tag后客户端在计算key的crc16时,只计算{}中数据。如果没使用hash tag,客户端会对整个key进行crc16计算。下面演示下hash tag使用:
4.3.3 请求重定向
每个节点都有一份 ClusterState,它记录了所有槽和节点的对应关系。Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。所以客户端的连接协议需要实现cluster协议,否则不能进行这种重定向的操作。
4.4 故障转移
Cluster 模式的故障转移过程和 Sentinel 模式有点类似,是通过 ping/pong 消息传播机制实现的。
4.4.1 主观下线
集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。判断主观下线流程:
- 节点a发送ping消息给节点b,如果通信正常将接收到pong消息,节点a更新最近一次与节点b的通信时间。
- 如果节点a与节点b通信出现问题则断开连接,下次会进行重连。如果一直通信失败,则节点a记录的与节点b最后通信时间将无法更新。
- 节点a内的定时任务检测到与节点b最后通信时间超过cluster-nodetimeout时,更新本地对节点b的状态为主观下线(pfail)。
4.4.2 客观下线
客观下线(fail)标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。判断客观下线流程:
- 当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略。
- 找到pfail对应的节点结构,更新clusterNode内部下线报告链表。
- 根据更新后的下线报告链表告尝试进行客观下线。
- 首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出。
- 当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
- 向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。
4.4.3 集群选主
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。所有从节点经过类Raft协议的过程在整个集群内达到一致:
- 资格检查:每个节点都会检查与主节点断开的时间。如果这个时间超过了 cluster-node-timeout * cluster-slave-validity-factor(从节点有效因子,默认为 10),那么就没有故障转移的资格。也就是说这个从节点和主节点断开的太久了,很久没有同步主节点的数据了,不适合成为新的主节点,因为成为主节点以后其他的从节点回同步自己的数据。
- 触发选举,通过了上面资格的从节点都可以触发选举。但是出发选举是有先后顺序的,这里按照复制偏移量的大小来判断。这个偏移量记录了执行命令的字节数。主服务器每次向从服务器传播 N 个字节时就会将自己的复制偏移量+N,从服务在接收到主服务器传送来的 N 个字节的命令时,就将自己的复制偏移量+N。复制偏移量越大说明从节点延迟越低,也就是该从节点和主节点沟通更加频繁,该从节点上面的数据也会更新一些,因此复制偏移量大的从节点会率先发起选举。
- 投票选举:Redis没有直接使用从节点进行领导者选举,使用集群内所有持有槽的主节点进行领导者选举,这样保证即使只有一个从节点也可以完成选举过程。如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。
- 替换主节点:当从节点收集到足够的选票之后,触发替换主节点操作:执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。并向集群广播自己的pong消息,表明已经替换了故障从节点。
4.5 集群节点扩容缩容
Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。原理可抽象为slot和对应数据在不同节点之间灵活移动。假如原有3个master,每个master负责10384 / 3 ≈ 5461个slot;加入一个新的master之后,每个master负责10384 / 4 = 4096个slot。可以使用redis-trib.rb工具迁移,也可以手动完成slot迁移:将每个master将超过4096个slot的部分迁移到新的master中,以slot为单位进行迁移:
- 对目标节点发送:cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽数据。
- 对源节点发送:cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽数据。
- 源节点循环执行:cluster getkeysinslot {slot} {count}命令,每次获取count个属于槽的键。
- 在源节点上执行:migrate {targetIP} {targetPort} key 0 {timeout}命令,把指定的key迁移。
- 重复执行步骤3和步骤4,直到槽下所有的键值数据迁移到目标节点。
- 向集群内所有主节点发送:cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。
说明:
- 迁移过程是同步的,在目标节点执行restore指令到原节点删除key之间,原节点的主线程处于阻塞状态,直到key被删除成功
- 如果迁移过程突然出现网路故障,整个slot迁移只进行了一半,这时两个节点仍然会被标记为中间过滤状态,即"migrating"和"importing",下次迁移工具连接上之后,会继续进行迁移
- 在迁移过程中,如果每个key的内容都很小,那么迁移过程很快,不会影响到客户端的正常访问
- 如果key的内容很大,由于迁移一个key的迁移过程是阻塞的,就会同时导致原节点和目标节点的卡顿,影响集群的稳定性,所以,集群环境下,业务逻辑要尽可能的避免大key的产生
- Author:mcbilla
- URL:http://mcbilla.com/article/d3f19247-8e20-428b-9394-cf52266ff912
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!