Redis

Redis 集群

概述

其实集群的概念很广泛。广义上,只要多个机器共同组成一个分布式系统,都可以叫集群。之前的主从复制、哨兵模式,也可以看成广义集群。Redis 这里说的集群,一般指 Redis Cluster。它主要解决的是存储空间不足的问题:把数据拆到多台机器上,每台机器只保存一部分数据。

哨兵模式解决的是可用性问题,但本质上还是主从节点都保存完整数据。只要数据全集超过单机能承受的内存,就需要引入分片。

分片

分片可以理解成把数据一片一片分开。分片就是把数据拆成多份,分别放到不同节点上。 所以每一台机器上面的数据都是不一样的。 比如有 1TB 数据,可以这样拆:

  • 2 台机器,每台大约存 512GB。
  • 3 台机器,每台大约存 300 多 GB。
  • 4 台机器,每台大约存 256GB。

机器越多,每台机器承担的数据越少。但是实际部署时,每个存数据的主节点通常还要配从节点,用来提高可用性。所以扩容的时候并不是只需要一台,可能是需要很多台机器。对于数据存放的思路,有以下这几种算法可以实现。

哈希求余

Hash modulo sharding

哈希求余的思路是:

hash(key) % N

N 是分片数量。Redis 的数据都是 key-value 结构,所以可以先对 key 做 hash,再对分片数量求余,得到应该放到哪个分片。查询时也一样:key 不变,hash 函数不变,算出来的分片编号也不变。

这种方式简单,但是扩容很麻烦。因为一旦分片数量 N 改了,hash(key) % N 的结果会整体变化,很多数据都要重新搬运。

一致性哈希

Consistent hashing

一致性哈希把 hash 空间看成一个环,比如范围是 0 ~ 2^32 - 1。分片也落在这个环上,key 计算 hash 后,按环上的位置往顺时针跑,找到第一个分片的分割点,那就属于是当前的分片。

就像图里的,原本只有分片 012,若新增了分片 3,则扩容时不用让所有 key 重新分布,只需要搬动新节点影响到的一段数据。图中的 keyC 原本是属于分片 0 的,现在顺时针跑遇到的第一个分片是 3,那么它就属于新分片 3 的范围咯~

优点:

  • 比哈希求余的数据搬运量小
  • 扩容成本更低

但有个问题是如果节点分布不好,分片上的数据量可能不均匀,专业数据叫做数据倾斜~

Redis 哈希槽

Redis hash slots

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 cluster nodes

Redis 分片后,每个分片的槽位都不同,如果在分片 1 中,写入的 key 刚好位于分片 2 的 slot,则会无法插入,如图(主节点与从节点同理) Redis cluster moved error without client mode 如需要插入 key 1,则需要手动切换至 172.30.0.102:6379 这个节点,非常麻烦,有没有办法能够既可以插入,又可以无需手动切换 redis 节点呢?有的有的,只需要在进入 redis-cli 中加入 -c 命令,Redis 客户端就会根据当前 key 实际算出来的槽位号,自动找到匹配的分片主机,也就是能够在分片 1 节点上插入属于其他分片的 key 值。 Redis cluster client mode redirects key write 这次就能帮你切换到应该的节点,成功。仅需输入一次 -c 即可

故障转移

这里的故障转移与Redis 哨兵 的不太一样,主要的区别在于投票选出新的主节点上

具体演示下,将 redis 1 主节点给手动停掉,命令 docker stop redis1 Stop redis1 container Redis cluster failover promotes replica 可以看到,101 主节点挂了后,原先 101 的从节点:105 被提拔为了新的主节点,101 原本的从节点 106 此刻也变成了 105 的从节点,那么我们再试试重启 101 节点,看看是什么变化(docker start redis1Restarted redis1 becomes replica 它也变成了 105 的从节点。 能看出集群也能够处理故障转移,而且这看起来跟哨兵的故障转移很像,那我们说的不同点在哪呢?不同在于过程~

过程

故障判定

  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 是从节点,那么不需要进行故障迁移 如果 B 是主节点,那么就会由 B 的从节点(比如 C 和 D)触发故障迁移了

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

Add redis10 as cluster master

添加成功后 cluster nodes 查看 redis-10 已经作为主节点进入集群,但还未分配到 slots Redis10 joined with no slots 重新分配 slots

# reshard (重新切分)后的地址是集群中的任意节点地址
redis-cli --cluster reshard 172.30.0.101:6379

执行后会进行交互操作,具体有

  1. 选择多少个 slots 分配到该节点(原先有 3 个分片,现扩容至 4 分片,16384 slots 平均到一个分片为 4096) Choose reshard slot count

  2. 选择将 slots 转移到哪个节点上(此处填写节点的 id,如图) Choose reshard target node

  3. 这些 slots 从哪些节点搬运过来(all 表示每个主节点都搬一点,若选择 done 则需手动填写主节点 id) Choose reshard source nodes

  4. 确定之后,会初步打印出搬运方案,让用户确认,之后就会进行集群的 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 的从节点 Redis10 slots and redis11 replica result

总结

Redis Cluster 的核心是分片。主从复制和哨兵主要解决可用性,集群模式进一步解决单机存储容量限制。部署后可以通过 redis-cli -c 让客户端自动跟随槽位跳转;扩容时则先添加主节点,再通过 reshard 分配 slots,最后把新从节点挂到对应主节点下面。