Bootstrap

Redis存储⑬Redis集群_扩展存储空间

目录

1. 集群的基本概念

2. 数据分片算法

2.1 哈希求余

2.2 一致性哈希算法

2.3 哈希槽分区算法

3. 主节点宕机处理流程

3.1 故障判定

3.2 故障迁移

本篇完。


1. 集群的基本概念

        前面学习的哨兵模式,提高了系统的可用性。但是真正用来存储数据的还是 master 和 slave 节点,所有的数据都需要存储在单个 master 和 slave 节点中。如果数据量很大,接近超出了 master / slave 所在机器的物理内存,就可能出现严重问题了。

        虽然硬件价格在不断降低,一些中大厂的服务器内存已经可以达到 TB 级别了,但是 1TB 在当前这个 “大数据” 时代,俨然不算什么,但有的时候我们确实需要更大的内存空间来保存更多的数据。

如何获取更大的空间?

        加机器即可,所谓 “大数据” 的核心其实就是一台机器搞不定了,用多台机器来搞。

        Redis 的集群就是在上述的思路之下,引入多组 Master / Slave,每⼀组 Master / Slave 存储数据全集的一部分,从而构成一个更大的整体,称为 Redis 集群(Cluster)。

  • 广义的 “集群”:只要是多个机器构成了分布式系统,都可以称为是一个 “集群”。前面学习的主从结构、哨兵模式,也可以称为是 “广义的集群”。
  • 狭义的 “集群”:Redis 提供的集群模式,在这个集群模式之下,主要是解决存储空间不足的问题(扩展存储空间)。

        假定整个数据全集是 1 TB,引入三组 Master / Slave 来存储,那么每一组机器只需要存储整个数据全集的 1/3 即可。

在上述图中:

  • Master1 和 Slave11 和 Slave12 保存的是同样的数据,占总数据的 1/3。
  • Master2 和 Slave21 和 Slave22 保存的是同样的数据,占总数据的 1/3。
  • Master3 和 Slave31 和 Slave32 保存的是同样的数据,占总数据的 1/3。

        这三组机器存储的数据都是不同的。每个 Slave 都是对应 Master 的备份(当 Master 挂了,对应的 Slave 会补位成 Master)。每个红框部分都可以称为是一个分片(Sharding)。如果全量数据进一步增加,只要再增加更多的分片即可解决。

数据量多了,使用硬盘来保存不行吗?

        硬盘只是存储空间大了,但是访问速度是比内存慢很多的。但实际上,还是存在很多的应用场景,既希望存储较多的数据,又希望有非常高的读写速度,比如搜索引擎。


2. 数据分片算法

Redis cluster 的核心思路是用多组机器来存数据的每个部分。

        给定一个数据(一个具体的 key),那么这个数据应该存储在哪个分片上? 读取的时候又应该去哪个分片读取呢?

围绕这个问题,业界有三种比较主流的实现方式:

2.1 哈希求余

        借鉴了哈希表的基本思想。借助 hash 函数,把一个 key 映射到整数,再针对数组的长度进行求余,就可以得到一个数组下标。

        设有 N 个分片,使用 [0, N-1] 这样序号进行编号。针对某个给定的 key,先计算 hash 值,再把得到的结果 % N,得到的结果即为分片编号。例如:N 为 3,给定的 key 为 "hello",对 "hello" 计算 hash 值(比如使用 md5 算法:针对一个字符串,将里面的内容进行一系列的数学变化,转换为整数。特点:md5 计算结果是定长的,其计算结果是分散的,计算结果是不可逆的),得到的结果为:bc4b2a76b9719d91,再把这个结果 % 3(hash(key) % N => 0),结果为 0,那么就把 hello 这个 key 放到 0 号分片上。

当然,实际工作中涉及到的系统,计算 hash 的方式不一定是 md5,但是思想是一致的。

        后续如果要取某个 key 的记录, 也是针对 key 计算 hash , 再对 N 求余, 就可以找到对应的分片编号了。

  • 优点:简单高效,数据分配均匀。
  • 缺点:一旦需要进行扩容,N 发生了改变,那么原有的映射规则被破坏,就需要让节点之间的数据相互传输,重新排列,以满足新的映射规则。此时需要搬运的数据量是比较多的,开销较大。

        N 为 3 的时候,[100, 120] 这 21 个 hash 值的分布(此处假定计算出的 hash 值是一个简单的整数,方便肉眼观察)

        当引入一个新的分片,N 从 3 => 4 时,大量的 key 都需要重新映射(某个 key % 3 和 % 4 的结果不一样,就映射到不同机器上了)。

        如上图可以看到,整个扩容一共 21 个 key,只有 3 个 key 没有经过搬运,其他的 key 都是搬运过的。


2.2 一致性哈希算法

        交替出现导致搬运成本高,为了降低上述的搬运开销,能够更高效扩容,业界提出了 “一致性哈希算法”。

key 映射到分片序号的过程不再是简单求余了,而是改成以下过程:

(1)把 0 -> 2^32-1 这个数据空间映射到一个圆环上,数据按照顺时针方向增长。


(2)假设当前存在三个分片,就把分片放到圆环的某个位置上


(3)假定有一个 key,计算得到 hash 值 H,那么这个 key 映射到哪个分片呢?

规则很简单,就是从 H 所在位置顺时针往下找,找到的第一个分片,即为该 key 所从属的分片。

        这就相当于,N 个分片的位置把整个圆环分成了 N 个管辖区间。Key 的 hash 值落在某个区间内,就归对应区间管理。

        上述一致性哈希算法的过程类似于去高铁站取票。现在的高铁站都可以直接刷身份证了,但是以前的时候需要网上先购票,然后再去高铁站的取票机上把票取出来。

想象一下下列场景:

        假设一个人每次来高铁站,都会停车在同一个位置(不同的人停车位置不同)。每个人下车之后,都往右手方向走,遇到第一个取票机就进行取票。

在这个情况下,如果扩容一个分片,如何处理呢?

原有分片在环上的位置不动,只要在环上新安排一个分片位置即可。

        此时,只需要把 0 号分片上的部分数据搬运给 3 号分片即可,而 1 号分片和 2 号分片管理的区间都是不变的。

  • 优点:大大降低了扩容时数据搬运的规模,提高了扩容操作的效率。
  • 缺点:数据分配不均匀(有的多有的少,数据倾斜)。

2.3 哈希槽分区算法

        为了解决上述问题(搬运成本高和数据分配不均匀),Redis 集群引入了哈希槽(hash slots)算法。

hash_slot = crc16(key) % 16384

其中 crc16 也是一种 hash 算法。

        16384 其实是 16 * 1024,也就是 2^14(16k),相当于是把整个哈希值映射到 16384 个槽位上,也就是 [0, 16383]。然后再把这些槽位比较均匀的分配给每个分片,每个分片的节点都需要记录自己持有哪些分片。

假设当前有三个分片,一种可能的分配方式:

  • 0 号分片:[0, 5461],共 5462 个槽位。
  • 1 号分片:[5462, 10923],共 5462 个槽位。
  • 2 号分片:[10924, 16383],共 5460 个槽位。

        这里的分片规则是很灵活的,虽然每个分片持有的槽位也不一定连续,但此时这三个分片上的数据是比较均匀了。每个分片的节点使用位图来表示自己持有哪些槽位。对于 16384 个槽位来说,需要 2048 个字节(2KB)大小的内存空间表示。

        如果需要进行扩容,比如新增一个 3 号分片,就可以针对原有的槽位进行重新分配。比如可以把之前每个分片持有的槽位各拿出一点,分给新分片。

一种可能的分配方式:

  • 0 号分片:[0, 4095],共 4096 个槽位。
  • 1 号分片:[5462, 9557],共 4096 个槽位。
  • 2 号分片:[10924, 15019],共 4096 个槽位。
  • 3 号分片:[4096, 5461] + [9558, 10923] + [15019, 16383],共 4096 个槽位。

在上述过程中,只有被移动的槽位所对应的数据才需要搬运。 

        在实际使用 Redis 集群分片的时候,不需要手动指定哪些槽位分配给某个分片,只需要告诉某个分片应该持有多少个槽位即可,Redis 会自动完成后续的槽位分配,以及对应的 key 搬运的工作。 

Redis 集群是最多有 16384 个分片吗?
        并非如此,如果一个分片只有一个槽位,key 是要先映射到槽位,再映射到分片的,这对于集群的数据均匀其实是难以保证的。实际上 Redis 的作者建议集群分片数不应该超过 1000。而且,16000 这么大规模的集群,本身的可用性也是一个大问题。一个系统越复杂,出现故障的概率是越高的。

为什么是 16384 个槽位?

Redis 作者回答:why redis-cluster use 16384 slots? · Issue #2576 · redis/redis

翻译:
        节点之间通过心跳包通信。心跳包中包含了该节点持有哪些 slots,这个是使用位图这样的数据结构表示的,表示 16384(16k)个 slots,需要的位图大小是 2KB。如果给定的 slots 数更多了,比如 65536 个了,那么此时就需要消耗更多的空间,8 KB 位图表示了。8 KB 对于内存来说不算什么,但是在频繁的网络心跳包中,还是一个不小的开销的。
        另一方面,Redis 集群一般不建议超过 1000 个分片,所以 16k 对于最大 1000 个分⽚来说是足够用的, 同时也会使对应的槽位配置位图体积不至于很大。


3. 主节点宕机处理流程

        如果集群中的从节点挂了,是没有什么影响的。但如果挂了的节点是主节点,此时就会产生影响力,因为主节点才能处理写操作,从节点是不能写的。

3.1 故障判定

集群中的所有节点都会周期性的使用心跳包进行通信。

  1. 节点 A 给节点 B 发送 ping 包,B 就会给 A 返回一个 pong 包。ping 和 pong 除了 message type 属性之外,其他部分都是一样的。这里包含了集群的配置信息(该节点的 id,该节点从属于哪个分片,是主节点还是从节点,从属于谁,持有哪些 slots 的位图...)。
  2. 每个节点,每秒钟都会给一些随机的节点发起 ping 包,而不是全发一遍。这样设定是为了避免在节点很多的时候,心跳包也非常多(比如有 9 个节点,如果全发,就是 9 * 8 有 72 组心跳了,而且这是按照 N^2 这样的级别增长的)。
  3. 当节点 A 给节点 B 发起 ping 包,B 不能如期回应的时候,此时 A 就会尝试重置和 B 的 tcp 连接,看能否连接成功。如果仍然连接失败,A 就会把 B 设为 PFAIL 状态(相当于主观下线)。
  4. A 判定 B 为 PFAIL 之后,会通过 redis 内置的 Gossip 协议和其他节点进行沟通,向其他节点确认 B 的状态(每个节点都会维护一个自己的 “下线列表”,由于视角不同,每个节点的下线列表也不一定相同)。
  5. 此时 A 发现其他很多节点也认为 B 为 PFAIL,并且数目超过总集群个数的一半,那么 A 就会把 B 标记成 FAIL(相当于客观下线),并且把这个消息同步给其他节点(其他节点收到之后,也会把 B 标记成 FAIL)。

至此,B 就彻底被判定为故障节点了。

某个或者某些节点宕机,有的时候会引起整个集群都宕机(称为 fail 状态)。

以下三种情况会出现集群宕机:

  1. 某个分片,所有的主节点和从节点都挂了。(该分片就无法提供数据服务了)
  2. 某个分片,主节点挂了,但是没有从节点。(该分片就无法提供数据服务了)
  3. 超过半数的 master 节点都挂了。(情况严重)

核心原则是保证每个 slots 都能正常⼯工作(存取数据)。 


3.2 故障迁移

上述例子中,B 故障且 A 把 B FAIL 的消息告知集群中的其他节点。

  1. 如果 B 是从节点,那么不需要进行故障迁移。
  2. 如果 B 是主节点,那么就会由 B 的从节点(比如 C 和 D) 触发故障迁移了。

所谓故障迁移,就是指把从节点提拔成主节点,继续给整个 redis 集群提供支持。

具体流程如下:

  1. 从节点判定自己是否具有参选资格。如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太大了),时间超过阈值就失去竞选资格。
  2. 具有资格的节点,比如 C 和 D 就会先休眠一定时间,休眠时间 = 500ms 基础时间 + [0, 500ms] 随机时间 + 排名 * 1000ms。offset 的值越大,则排名越靠前(越小)。
  3. 比如 C 的休眠时间到了,C 就会给其他所有集群中的节点,进行拉票操作,但是只有主节点才有投票资格。
  4. 主节点就会把自己的票投给 C(每个主节点只有 1 票)。当 C 收到的票数超过主节点数目的一半,C 就会晋升成主节点(C 自己负责执行 slaveof no one,并且让 D 执行 slaveof C)。
  5. 同时,C 还会把自己成为主节点的消息,同步给其他集群的节点,大家也都会更新自己保存的集群结构信息。

        上述选举的过程称为 Raft 算法,是一种在分布式系统中广泛使用的算法。在随机休眠时间的加持下,基本上就是谁先唤醒,谁就能竞选成功。


本篇完。

下一篇是Redis存储⑭Redis的应用_缓存。

;