问:海量数据(亿即)作为缓存如何实现(其他持久化组件也适用)
答:肯定是多个缓存实例
问:海量数据是如何存在于多个缓存实例的
答:使数据尽量均匀的落入多个缓存实例中,同时考虑缓存实例数量变更的场景
问:如何具体实现
答:顺次参考下面三个算法
通过公式
hash % N
计算数据落点,N 是缓存集群节点数量
优缺点
优点
缺点
步骤
示意图如下
优缺点
以下图为例,原始 hash 环上只有 4 台存储节点,其位置分别是 node 1-4
优点
缺点
数据倾斜问题
回到一开始只有 node 1-4 的状态,
可以发现 node-1 节点的任务明显更繁重,它负责了 hash 环上一半左右的数据
这会使不同存储节点上的负载不均衡,实际开发生产环境中可能导致雪崩效应
雪崩效应
回到一开始只有 node 1-4 的状态,因为数据倾斜的问题,node -1 节点任务繁重
假设 node-1 挂了,则原先在 node-1 上读写的数据都落入 node-2 上
加上 node-2 本身的数据,node-2 很可能也扛不住,挂了
以此类推,全集群都挂了
虚拟节点
可以通过添加虚拟节点的方式一定程度上解决数据倾斜问题
虚拟节点的添加有两个原则
如上图,在 hash 环上添加 4 个灰色节点 node-x-2 作为 node-x 的虚拟节点,则整个 hash 环大致被 8 等分
实际操作中,也不一定是添加一倍的虚拟节点,而只需要参考虚拟节点到真实节点的映射,最终达到负载均衡即可
此时,上图中,
每段有颜色的圆弧上的数据对应一个节点(可能是真实节点,也可能是虚拟节点)
每两端颜色相同的圆弧上的数据由一个真实节点负责,做到每个真实节点负载大致均衡
也可以这样理解,8 个节点实际上都是虚拟节点(上图的的 node-x 理解为 node-x-1 的虚拟节点),负责差不多范围的数据
所有落在虚拟节点上的数据均由对应的真实节点处理,对应的规则为 node-x-1 与 node-x-2 都由 node-x 处理
虚拟节点一致性 hash 的问题
hash 取余算法存在存在两个问题
原始一致性 hash 解决了这两个问题,但因为它存储存储节点落点是通过 hash 计算的,因此带来了新问题
数据倾斜的本质是存储节点负载不均匀,因此引入虚拟节点解决了这一问题
但是,如上图,在虚拟节点一致性 hash 的基础上再增加存储节点呢
即,原本是 8 虚拟节点 对应 4 真实存储节点,
但,现在是 5 个真实存储节点了,如何分配虚拟节点或增长虚拟节点呢
一种可行的方式是:预设足够多的虚拟节点
比如,预设 120 个虚拟节点
假设原始有 10 个真实存储节点,那每个节点映射 12 个虚拟节点
现在增加 2 个节点,那每个节点改为映射 10 个虚拟节点
不能均分的话也只是某几个真实存储节点相对其他节点增减一部分工作量,这部分工作量通常不会造成什么后果
但是这种方式依然有局限性,局限性在于:
若真实物理节点超过一定数量,导致不能给真实物理节点相对均匀的分配虚拟节点
就可能导致某些节点的工作量是其他节点的多倍(比如 100 虚拟节点,70 真实节点),这又有可能导致雪崩
因此又要考虑扩展虚拟节点,而对虚拟节点的扩展,约等于扩展 hash 取余算法中的集群数量
最少又是 50% 的数据迁移量
步骤
从带有虚拟节点的一致性 hash 理解 hash 槽
hash 槽可以看做带有虚拟节点的一致性hash 的变种,或 一致性 hash 与 hash 取余 的结合
将每个 hash 槽都视为一个虚拟节点
hash 槽就是一个预设了 16384 个虚拟节点的一致性 hash(当然,此时无所谓 hash 环不环了)
然后在去解决 16384 个虚拟节点和真实节点的映射问题
换句话说,本帖子讨论到的 分布式存储中数据分布的核心问题 其实只有两个
hash 取余 的等分思想(对几取余就是几等分)解决了 负载均衡,但在 节点数量变化 上瘸了
一致性 hash 的 hash 环解决了 节点数量变化(在环上拆补线段玩),但又不够 负载均衡
带有虚拟节点的一致性 hash 到是同时解决了二者,但存在局限性
虚拟节点大致实现了等分,虚拟节点到实际节点的映射解决 节点数量变化,同时兼顾负载均衡
整合上面的思路,
首先,设置足够多的区间段(类似虚拟节点):16384 个
每个区间段的大小完全一致,这就是实现了对整个 hash 值空间(此时是 16 位的 CYC16 空间了)的绝对等分
这就是 hash 槽的数据结构
然后,尽量均匀的完成 hash 槽对真实存储节点的映射
因为划分的碎片足够碎,并且每个碎片一样大,因此总能给每个真实存储节点分配近乎一样的工作量,达成负载均衡
因为划分的碎片足够多,所以每个碎片足够小,通过划拨碎片可以做到在 存储集群节点数量变化造成数据迁移 尽可能少
为什么 hash 槽有 16384 个槽位 TBC
16384 个槽位够用了
分配足够多的槽位是为了可以不破坏 负载均衡 的前提下尽量灵活的映射 槽位 和 存储节点
一个集群中,节点的数量几乎不会超过 1000
即使按 1000 算,16K+ 个槽位去分配 1K 个节点也足够兼顾灵活映射且尽量负载均衡
考虑心跳包数据大小
每个集群中的节点都需要定时发送心跳
心跳包中包含槽位的信息,应该是当前节点具体包含哪些槽位,每个槽位是否包含可以用 1 bit 传输
一共 16384 个槽位即需要 16384 bit = 16384 / 8 / 1024 = 2Kb 的数据量进行描述,可以接受
当 65536 个槽位且集群中节点很多时,就需要 8Kb * 节点数量的数据流量,只用于传输心跳,浪费
压缩率高(没看明白)
Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。