集群 cluster
基本概念
上述的 哨兵 模式, 提⾼了系统的可⽤性. 但是真正⽤来存储数据的还是 master 和 slave 节点. 所有的数据都需要存储在单个 master 和 slave 节点中.
如果数据量很⼤, 接近超出了 master / slave 所在机器的物理内存, 就可能出现严重问题了.
如何获取更⼤的空间? 加机器即可! 所谓 “⼤数据” 的核⼼, 其实就是⼀台机器搞不定了, ⽤多台机器来搞.
Redis 的集群就是在上述的思路之下, 引⼊多组 Master / Slave , 每⼀组 Master / Slave 存储数据全集的⼀部分, 从⽽构成⼀个更⼤的整体, 称为 Redis 集群 (Cluster).
在上述图中,
- Master1 和 Slave11 和 Slave12 保存的是同样的数据. 占总数据的 1/3
- Master2 和 Slave21 和 Slave22 保存的是同样的数据. 占总数据的 1/3
- Master3 和 Slave31 和 Slave32 保存的是同样的数据. 占总数据的 1/3
这三组机器存储的数据都是不同的.
每个 Slave 都是对应 Master 的备份(当 Master 挂了, 对应的 Slave 会补位成 Master).
每个红框部分都可以称为是⼀个 分⽚ (Sharding).
如果全量数据进⼀步增加, 只要再增加更多的分⽚, 即可解决.
数据分片算法
Redis cluster 的核⼼思路是⽤多组机器来存数据的每个部分. 那么接下来的核⼼问题就是, 给定⼀个数据 (⼀个具体的 key), 那么这个数据应该存储在哪个分⽚上? 读取的时候⼜应该去哪个分⽚读取?
围绕这个问题, 业界有三种⽐较主流的实现⽅式.
1) 哈希求余
针对某个给定的 key, 先计算 hash 值, 再把得到的结果 % N, 得到的结果即为分⽚编号.
例如, N 为 3. 给定 key 为 hello, 对 hello 计算 hash 值(⽐如使⽤ md5 算法), 得到的结果为bc4b2a76b9719d91 , 再把这个结果 % 3, 结果为 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) ⼀致性哈希算法
为了降低上述的搬运开销, 能够更⾼效扩容, 业界提出了 “⼀致性哈希算法”.
key 映射到分⽚序号的过程不再是简单求余了, ⽽是改成以下过程:
第⼀步, 把 0 -> 2^32-1 这个数据空间, 映射到⼀个圆环上. 数据按照顺时针⽅向增⻓.
第⼆步, 假设当前存在三个分⽚, 就把分⽚放到圆环的某个位置上.
第三步, 假定有⼀个 key, 计算得到 hash 值 H, 那么这个 key 映射到哪个分⽚呢? 规则很简单, 就是从 H所在位置, 顺时针往下找, 找到的第⼀个分⽚, 即为该 key 所从属的分⽚.
这就相当于, N 个分⽚的位置, 把整个圆环分成了 N 个管辖区间. Key 的 hash 值落在某个区间内, 就归对应区间管理
在这个情况下, 如果扩容⼀个分⽚, 如何处理呢?
原有分⽚在环上的位置不动, 只要在环上新安排⼀个分⽚位置即可.
此时, 只需要把 0 号分⽚上的部分数据, 搬运给 3 号分⽚即可. 1 号分⽚和 2 号分⽚管理的区间都是不变的.
优点: ⼤⼤降低了扩容时数据搬运的规模, 提⾼了扩容操作的效率.
缺点: 数据分配不均匀 (有的多有的少, 数据倾斜).
- 哈希槽分区算法 (Redis 使⽤)
为了解决上述问题 (搬运成本⾼ 和 数据分配不均匀), Redis cluster 引⼊了哈希槽 (hash slots) 算法.
16384 其实是 16 * 1024, 也就是 2^14
相当于是把整个哈希值, 映射到 16384 个槽位上, 也就是 [0, 16383].
然后再把这些槽位⽐较均匀的分配给每个分⽚. 每个分⽚的节点都需要记录⾃⼰持有哪些分⽚.
假设当前有三个分⽚, ⼀种可能的分配⽅式:
- 0 号分⽚: [0, 5461], 共 5462 个槽位
- 1 号分⽚: [5462, 10923], 共 5462 个槽位
- 2 号分⽚: [10924, 16383], 共 5460 个槽位
如果需要进⾏扩容, ⽐如新增⼀个 3 号分⽚, 就可以针对原有的槽位进⾏重新分配
⽐如可以把之前每个分⽚持有的槽位, 各拿出⼀点, 分给新分⽚.
⼀种可能的分配⽅式:
- 0 号分⽚: [0, 4095], 共 4096 个槽位
- 1 号分⽚: [5462, 9557], 共 4096 个槽位
- 2 号分⽚: [10924, 15019], 共 4096 个槽位
- 3 号分⽚: [4096, 5461] + [9558, 10923] + [15018, 16383], 共 4096 个槽位
此处还有两个问题:
问题⼀: Redis 集群是最多有 16384 个分⽚吗?
并⾮如此. 如果⼀个分⽚只有⼀个槽位, 这对于集群的数据均匀其实是难以保证的.
实际上 Redis 的作者建议集群分⽚数不应该超过 1000.
⽽且, 16000 这么⼤规模的集群, 本⾝的可⽤性也是⼀个⼤问题. ⼀个系统越复杂, 出现故障的概率是越⾼的.
问题⼆: 为什么是 16384 个槽位?
节点之间通过⼼跳包通信. ⼼跳包中包含了该节点持有哪些 slots. 这个是使⽤位图这样的数据结构表⽰的. 表⽰ 16384 (16k) 个 slots, 需要的位图⼤⼩是 2KB. 如果给定的 slots 数更多了, ⽐如 65536个了, 此时就需要消耗更多的空间, 8 KB 位图表⽰了. 8 KB, 对于内存来说不算什么, 但是在频繁的⽹
络⼼跳包中, 还是⼀个不⼩的开销的.
另⼀⽅⾯, Redis 集群⼀般不建议超过 1000 个分⽚. 所以 16k 对于最⼤ 1000 个分⽚来说是⾜够⽤的, 同时也会使对应的槽位配置位图体积不⾄于很⼤.