概述
其实集群的概念很广泛。广义上,只要多个机器共同组成一个分布式系统,都可以叫集群。之前的主从复制、哨兵模式,也可以看成广义集群。Redis 这里说的集群,一般指 Redis Cluster。它主要解决的是存储空间不足的问题:把数据拆到多台机器上,每台机器只保存一部分数据。
哨兵模式解决的是可用性问题,但本质上还是主从节点都保存完整数据。只要数据全集超过单机能承受的内存,就需要引入分片。
分片
分片可以理解成把数据一片一片分开。分片就是把数据拆成多份,分别放到不同节点上。 所以每一台机器上面的数据都是不一样的。 比如有 1TB 数据,可以这样拆:
- 2 台机器,每台大约存 512GB。
- 3 台机器,每台大约存 300 多 GB。
- 4 台机器,每台大约存 256GB。
机器越多,每台机器承担的数据越少。但是实际部署时,每个存数据的主节点通常还要配从节点,用来提高可用性。所以扩容的时候并不是只需要一台,可能是需要很多台机器。对于数据存放的思路,有以下这几种算法可以实现。
哈希求余
哈希求余的思路是:
hash(key) % N
N 是分片数量。Redis 的数据都是 key-value 结构,所以可以先对 key 做 hash,再对分片数量求余,得到应该放到哪个分片。查询时也一样:key 不变,hash 函数不变,算出来的分片编号也不变。
这种方式简单,但是扩容很麻烦。因为一旦分片数量 N 改了,hash(key) % N 的结果会整体变化,很多数据都要重新搬运。
一致性哈希
一致性哈希把 hash 空间看成一个环,比如范围是 0 ~ 2^32 - 1。分片也落在这个环上,key 计算 hash 后,按环上的位置往顺时针跑,找到第一个分片的分割点,那就属于是当前的分片。
就像图里的,原本只有分片 012,若新增了分片 3,则扩容时不用让所有 key 重新分布,只需要搬动新节点影响到的一段数据。图中的 keyC 原本是属于分片 0 的,现在顺时针跑遇到的第一个分片是 3,那么它就属于新分片 3 的范围咯~
优点:
- 比哈希求余的数据搬运量小
- 扩容成本更低
但有个问题是如果节点分布不好,分片上的数据量可能不均匀,专业数据叫做数据倾斜~
Redis 哈希槽
Redis Cluster 真正采用的是哈希槽方案,将哈希求余➕一致性哈希,结合起来了。
slot = CRC16(key) % 16384
Redis Cluster 固定有 16384 个槽位,也就是 2^14 个。key 不是直接映射到分片,而是先映射到槽位,再由槽位映射到分片。
流程可以理解为:key -> 槽位(slot) -> 分片
扩容或迁移时,不需要按单个 key 去重新分配,而是迁移一部分槽位。只有这些槽位对应的数据需要搬运,所以比简单的哈希求余更适合扩容。
例如 3 个分片时,可以大致这样分:
0 号分片:[0, 5461]
1 号分片:[5462, 10923]
2 号分片:[10924, 16383]
如果扩成 4 个分片,也可以从原来的几个分片中各拿出一部分槽位给新分片。这样每个分片大约持有 4096 个槽位。
槽位数量
Redis 使用 16384 个槽位,是一个折中的数量。可以简单的算一下它的大小的,看着挺多,其实没占多少空间
16384 bit = 2048 byte = 2KB
如果槽位太少,每个分片能分到的槽位少,数据均衡性不容易保证,某些槽位可能有很多 key,某些槽位也可能没有 key。
如果槽位太多,节点之间通信的元数据会变大。因为 Redis 节点之间需要通过心跳包通信,心跳包中包含了该节点持有哪些 slots,这个是使用位图这样的数据结构表示的。如果槽位数量变成 65536,位图就会变成 8KB,虽然看着也不大,但是在这种频繁发送心跳包的场景里,网络带宽负担还是比较明显。
slots 通常会用位图表示:第 i 位为 1,表示这个节点持有第 i 个槽位;第 i 位为 0,表示不持有。注意这里表示的是节点是否持有槽位,不是这个槽位里有没有 key。
集群部署
具体部署步骤单独整理到:Docker 部署 Redis 集群 。
部署集群后,可以通过 docker-compose ps 命令来查看各个节点的状态
若想查看它们之间的分片主从关系,可通过 cluster nodes 查看,这个命令要在 redis-cli 中执行
Redis 分片后,每个分片的槽位都不同,如果在分片 1 中,写入的 key 刚好位于分片 2 的 slot,则会无法插入,如图(主节点与从节点同理)
如需要插入 key 1,则需要手动切换至 172.30.0.102:6379 这个节点,非常麻烦,有没有办法能够既可以插入,又可以无需手动切换 redis 节点呢?有的有的,只需要在进入 redis-cli 中加入 -c 命令,Redis 客户端就会根据当前 key 实际算出来的槽位号,自动找到匹配的分片主机,也就是能够在分片 1 节点上插入属于其他分片的 key 值。
这次就能帮你切换到应该的节点,成功。仅需输入一次 -c 即可
故障转移
这里的故障转移与Redis 哨兵 的不太一样,主要的区别在于投票选出新的主节点上
具体演示下,将 redis 1 主节点给手动停掉,命令 docker stop redis1
可以看到,101 主节点挂了后,原先 101 的从节点:105 被提拔为了新的主节点,101 原本的从节点 106 此刻也变成了 105 的从节点,那么我们再试试重启 101 节点,看看是什么变化(docker start redis1)
它也变成了 105 的从节点。
能看出集群也能够处理故障转移,而且这看起来跟哨兵的故障转移很像,那我们说的不同点在哪呢?不同在于过程~
过程
故障判定
- 节点 A 给节点 B 发送 ping 包,B 就会给 A 返回一个 pong 包。ping 和 pong 除了 message type 属性之外,其他部分都是一样的。这里包含了集群的配置信息(该节点的 id,该节点从属于哪个分片,是主节点还是从节点,从属于谁,持有哪些 slots 的位图……)
- 每个节点,每秒钟,都会给一些随机的节点发起 ping 包,而不是全发一遍。这样设定是为了避免在节点很多的时候,心跳包也非常多(比如有 9 个节点,如果全发,就是 9 * 8 有 72 组心跳了,而且这是按照 N^2 这样的级别增长的)。
- 当节点 A 给节点 B 发起 ping 包,B 不能如期回应的时候,此时 A 就会尝试重置和 B 的 TCP 连接,看能否连接成功。如果仍然连接失败,A 就会把 B 设为 PFAIL 状态(相当于主观下线)
- A 判定 B 为 PFAIL 之后,会通过 Redis 内置的 Gossip 协议,和其他节点进行沟通,向其他节点确认 B 的状态。(每个节点都会维护自己的“下线列表”,由于视角不同,每个节点的下线列表也不一定相同)。
- 此时 A 发现其他很多节点,也认为 B 为 PFAIL,并且数目超过总集群个数的一半,那么 A 就会把 B 标记成 FAIL (相当于客观下线),并且把这个消息同步给其他节点(其他节点收到之后,也会把 B 标记成 FAIL)。
故障迁移
如果 B 是从节点,那么不需要进行故障迁移 如果 B 是主节点,那么就会由 B 的从节点(比如 C 和 D)触发故障迁移了
- 从节点判定自己是否具有参选资格。如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太大了,说明很久没有同步过数据),时间超过阈值,就失去竞选资格。
- 具有资格的节点,比如 C 和 D,就会先休眠一定时间。休眠时间 = 500 ms 基础时间 + [0, 500 ms] 随机时间 + 排名 * 1000 ms。(offset 越大,数据就越接近主节点,排名就要靠前,休眠时间就更短)
- 比如 C 的休眠时间到了,C 就会给其他所有集群中的节点,进行拉票操作。但是只有主节点才有投票资格(谁休眠时间短,大概率就是新的主节点了)
- 主节点就会把自己的票投给 C(每个主节点只有 1 票)。当 C 收到的票数超过主节点数目的一半,C 就会晋升成主节点(C 自己负责执行
slaveof no one,并且让 D 执行slaveof C)。 - 同时,C 还会把自己成为主节点的消息,同步给其他集群的节点。大家也都会更新自己保存的集群结构信息。
更多的时候,是为了选一个节点出来,至于选谁,没那么重要。所以你看,不同点在于:哨兵,是先竞选出 leader,leader 负责找一个从节点升级成主节点,这里是直接投票选出新的主节点
集群扩容
扩容主节点
这里将 redis-10 作为主节点,redis-11 作为从节点。
# 添加 redis-10 的地址(172.30.0.110:6379)到集群中的任意一个节点地址(172.30.0.101:6379)
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
添加成功后 cluster nodes 查看 redis-10 已经作为主节点进入集群,但还未分配到 slots
重新分配 slots
# reshard (重新切分)后的地址是集群中的任意节点地址
redis-cli --cluster reshard 172.30.0.101:6379
执行后会进行交互操作,具体有
-
选择多少个 slots 分配到该节点(原先有 3 个分片,现扩容至 4 分片,16384 slots 平均到一个分片为 4096)
-
选择将 slots 转移到哪个节点上(此处填写节点的 id,如图)
-
这些 slots 从哪些节点搬运过来(all 表示每个主节点都搬一点,若选择 done 则需手动填写主节点 id)
-
确定之后,会初步打印出搬运方案,让用户确认,之后就会进行集群的 key 搬运工作,主节点就配置好了
扩容从节点
仅需一行命令
# 添加 redis-11(172.30.0.111:6379)到集群中(集群任意一个节点地址)作为从节点 (--cluster-slave),后跟上要跟随的主节点 id
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [172.30.0.110 节点的 nodeId(主节点 id)]
执行完毕后,从节点就已经被添加完成了,可见 redis-10 占了其他三个分片的部分 slots,redis-11 也成功成为了 redis-10 的从节点
总结
Redis Cluster 的核心是分片。主从复制和哨兵主要解决可用性,集群模式进一步解决单机存储容量限制。部署后可以通过 redis-cli -c 让客户端自动跟随槽位跳转;扩容时则先添加主节点,再通过 reshard 分配 slots,最后把新从节点挂到对应主节点下面。