Loading

[09] Redis 分片集群

1. 集群方案比较

哨兵模式】在 Redis3.0 以前的版本要实现集群一般是借助哨兵 Sentinel 工具来监控 Master 节点的状态,如果 Master 节点异常,则会做主从切换,将某一台 Slave 作为 Master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率。

「主从」和「哨兵」可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:① 海量数据存储问题 ② 高并发写的问题。

高可用集群模式】Redis 集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis 集群不需要 Sentinel 哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过 1000 个节点)。并且随着数据量增大,单个 Master 复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只负责存储整个数据集的一部分,这就是 Redis 的集群,其作用是提供在多个 Redis 节点间共享数据的程序集。

2. 集群数据分片

分片是什么?

使用 Redis 集群时我们会将存储的数据分散到多台 Redis 机器上,这称为「分片」。简言之,集群中的每个 Redis 实例都被认为是整个数据的一个分片。

如何找到给定 key 的分片?

为了找到给定 key 的分片,我们对 key 进行 CRC16(key) 算法处理并通过对总分片数量(16384)取模。然后,使用确定性哈希函数,这意味着给定的 key 将多次始终映射到同一个分片,我们可以推断将来读取特定 key 的位置。

槽位映射,一般有 3 种解决方案

  • 哈希取余分区
  • 一致性哈希分区
  • 哈希槽分区*

哈希槽

Redis 集群没有使用一致性 hash,而是引入了「哈希槽」的概念。Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分 hash slot。举个例子,比如当前集群有 3 个节点:

在数据和节点之间又加入了一层,把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。slot 解决的是粒度问题,相当于把粒度变大了,这样便于数据移动;hash 解决的是映射问题,使用 key 的哈希值来计算所在的 slot,便于数据分配。

一个集群只能有 16384 个槽,编号 0-16383(0~2^14-1)。这些槽会分配给集群中的所有主节点,分配策略没有要求。

集群会记录节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对 key 求哈希值,然后对 16384 取模,余数是几 key 就落入对应的槽里。HASH_SLOT = CRC16(key) mod 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。

为什么 Redis 集群的最大槽数是 16384 个?

CRC16 算法产生的 hash 值有 16bit,该算法可以产生 2^16=65536 个值。换句话说值是分布在 0~65535 之间,有更大的 65536 不用为什么只用 16384 就够?

(1)如果槽位为 65536,发送心跳信息的消息头达 8k,发送的心跳包过于庞大;

在集群发送的心跳数据包中,消息头中最占空间的是 myslots[CLUSTER_SLOTS/8]。 当槽位为 65536 时,这块的大小是 65536÷8÷1024=8kb,而当槽位为 16384 时,这块的大小是 16384÷8÷1024=2kb。因为每秒钟,Redis 节点需要发送一定数量的 ping 消息作为心跳包,如果槽位为 65536,这个 ping 消息的消息头太大了,浪费带宽。

(2)Redis 的集群主节点数量基本不可能超过 1000 个;

集群节点越多,心跳包的消息体内携带的数据越多。如果节点过 1000 个,也会导致网络拥堵。因此 Redis 作者不建议 Redis Cluster 节点数量超过 1000 个。 那么,对于节点数在 1000 以内的 Redis Cluster 集群,16384 个槽位够用了,没有必要拓展到 65536 个。

(3)槽位越少,在节点少的情况下,压缩比高,容易传输;

Redis 主节点的配置信息中它所负责的哈希槽是通过 bitmap 的形式来保存的,在传输过程中会对 bitmap 进行压缩。bitmap 的填充率越低,压缩率越高(bitmap 填充率 = slots / 节点数)。所以,插槽数偏低的话, 填充率会降低,压缩率会升高。

综合下来,从心跳包的大小、网络带宽、心跳并发、压缩率等维度考虑,16384 个插槽更有优势且能满足业务需求。

【补充】消息头有一个名为 myslots 的 char 类型数组 unsigned char myslots[CLUSTER_SLOTS/8],该数组长度为 16384/8 = 2048 。底层存储其实是一个 bitmap,存储发送方提供服务的 slot 映射表(如果为从,则为该从所对应的主提供服务的 slot 映射表),每一个位代表一个槽,如果该位为 1 则表示这个槽是属于这个节点。

3. 集群搭建测试

3.1 节点启动

Redis 集群需要至少 3 个 master 节点,我们这里搭建 3 个 master 节点,并且给每个 master 再搭建一个 slave 节点,总共 6 个 Redis 节点,这里用 3 台机器部署 6 个 Redis 实例,每台机器一主一从,搭建集群的步骤如下:

(1)设计说明

(2)修改配置文件 /myredis/cluster/redisCluster____.conf;

bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/myredis/cluster/cluster6381.log"
pidfile /myredis/cluster6381.pid
dir /myredis/cluster
dbfilename dump6381.rdb
appendonly yes
appendfilename "appendonly6381.aof"
requirepass 111111
masterauth 111111

cluster-enabled yes
cluster-config-file nodes-6381.conf
cluster-node-timeout 5000

可以用:%s/源字符串/目的字符串/g 来做批量替换(图文无关):

(3)启动 6 台 Redis 主机实例;

redis-server /myredis/cluster/redisCluster6381.conf
...
redis-server /myredis/cluster/redisCluster6386.conf

3.2 构建集群

(1)通过 redis-cli 命令为 6 台机器构建集群关系;

# --cluster-replicas 1 表示为每个master创建一个slave节点
redis-cli -a 111111 --cluster create --cluster-replicas 1
    192.168.111.185:6381 192.168.111.185:6382
    192.168.111.172:6383 192.168.111.172:6384
    192.168.111.184:6385 192.168.111.184:6386
# 分配原则尽量保证每个主数据库运行在不同的 IP 地址,每个从库和主库不在一个 IP 地址上。

(2)查看目录结构,会发现新增了集群相关的文件;

(3)以 6381 作为切入点,查看并检验集群状态:

CLUSTER nodes

CLUSTER info

3.3 集群读写

对 6381 新增两个 key,看看效果如何~

一定注意槽位的范围区间,需要路由到位!

【解决办法】加入参数 -c 优化路由

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。

CLUSTER KEYSLOT <key>                  # 计算键 key 应该被放置在哪个槽上
CLUSTER COUNTKEYSINSLOT <slot>         # 返回槽 slot 目前包含的键值对数量
CLUSTER GETKEYSINSLOT <slot> <count>   # 返回 count 个 slot 槽中的键

3.4 容错切换

(1)6381#Master 假如宕机了,6384 是否会上位成为了新的 Master?

6384 成功上位并正常使用:

(2)随后 6381 原来的主机回来了,是否会上位?

不会上位并以从节点形式回归。

(3)Redis 集群不保证强一致性,这意味着在特定的条件下,集群可能会丢掉一些被系统收到的写入请求命令。

(4)手动故障转移 or 节点从属调整该如何处理(还想让 6381 做主)?

3.5 主从扩容

(1)新建 6387、6388 两个服务实例配置文件

(2)启动 6387/6388 两个新的节点实例,此时他们自己都是 Master;

redis-server /myredis/cluster/redisCluster6387.conf
redis-server /myredis/cluster/redisCluster6388.conf

(3)将新增的 6387 节点(空槽号)作为 Master 节点加入原集群;

# redis-cli -a <pwd> --cluster add-node <new-IP>:<new-Port> <node-IP>:<node-Port>
# - newIP:newPort 就是将要作为 Master 的新增节点
# - node-IP:node-Port 就是原来集群节点里面的领路人,相当于 6387 依靠 6381 从而找到组织加入集群
redis-cli -a 111111  --cluster add-node 192.168.111.174:6387 192.168.111.185:6381

可以发现,新加入集群的这个节点并没有分配到槽。

(4)重新分派槽号 rehash

# redis-cli -a <pwd> --cluster reshard <IP>:<Port>
redis-cli -a 111111 --cluster reshard 192.168.111.185:6381

命令执行过程中需手动输入部分:

(5)通过命令 redis-cli -a <pwd> --cluster check <IP>:<Port> 查看集群情况;

为什么 6387 是 3 个新的区间,以前的都还是连续的呢?

因为重新分配成本太高,所以前 3 片各自匀出来一部分,从 6381/6383/6385 三个旧节点分别匀出 1364 个坑位给新节点 6387。

(6)为主节点 6387 分配从节点 6388

# redis-cli -a <pwd> --cluster add-node <new-IP>:<new-Port> <node-IP>:<node-Port> --cluster-slave --cluster-master-id <MasterID>
redis-cli -a 111111 --cluster add-node 192.168.111.184:6388 192.168.111.184:6387 --cluster-slave --cluster-master-id xxx...xxx

执行完成后,再次查看集群情况:

3.6 主从缩容

目的:6387 和 6388 下线

(1)将从节点 6388 删除

# redis-cli -a <pwd> --cluster del-node <del-IP>:<del-Port> <delNodeID>
redis-cli -a 111111 --cluster del-node 192.168.111.184:6388 xxx...xxx

再次查看集群状态,会发现 6388 被删除了,只剩下 7 台机器:

(2)将 6387 的槽号清空,重新分配,本例将清出来的槽号都给 6381;

redis-cli -a 111111 --cluster reshard 192.168.111.185:6381

(3)再次查看集群情况

(4)将 6387 删除

4. 扩展问题

4.1 通信机制

Redis Cluster 节点间采取 gossip 协议进行通信:

维护集群的元数据(集群节点信息、主从角色、节点数量、各节点共享的数据等)有两种方式:集中式和 gossip。

(1)集中式

  • 优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;
  • 不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 很多中间件都会借助 zookeeper 集中式存储元数据。

(2)gossip

  • gossip 协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;
  • 缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

gossip 协议包含多种消息,包括 ping、pong、meet、fail 等等。

  • meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;
  • ping:每个节点都会频繁给其他节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据(类似自己感知到的集群节点增加和移除,hash slot 信息等);
  • pong:对 ping 和 meet 消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;
  • fail: 某个节点判断另一个节点 fail 之后,就发送 fail 给其他节点,通知其他节点,指定的节点宕机了。

gossip 通信的 10000 端口】每个节点都有一个专门用于节点间 gossip 通信的端口,就是自己提供服务的端口号 + 10000,比如 7001,那么用于节点间通信的就是 17001 端口。 每个节点每隔一段时间都会往另外几个节点发送 ping 消息,同时其他几点接收到 ping 消息之后返回 pong 消息。

4.2 网络抖动

真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。

为解决这种问题,Redis Cluster 提供了一种选项 cluster­node­timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。

4.3 脑裂数据丢失问题

Redis 集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失。

规避方法可以在 Redis 配置里加上参数 min‐replicas‐to‐write 1,这个配置的意思是:写数据成功最少同步的 slave 数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置 1,加上 leader 就是 2,超过了半数。

【注意】这种方法不可能百分百避免数据丢失,参考集群 leader 选举机制。并且这个配置在一定程度上会影响集群的可用性,比如 slave 要是少于 1 个,这个集群就算 leader 正常也不能提供服务了,需要具体场景权衡选择。

4.4 集群是否完整才能对外提供服务

默认 YES。当 redis.conf 的配置 cluster-require-full-coverage 为 no 时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用,如果为 yes 则集群不可用。

4.5 对批量操作命令的支持

对于类似 mset、mget 这样的多个 key 的原生批量操作命令,Redis 集群只支持所有 key 落在同一 slot 的情况。如果有多个 key 一定要用 mset 命令在 Redis 集群上操作,则可以在 key 的前面加上 {XX},这样参数数据分片 hash 计算的只会是大括号里的值,这样能确保不同的 key 能落到同一 slot 里去,示例如下:

mset {user1}:1:name tree6x7 {user1}:1:age 24

假设 name 和 age 计算的 hash slot 值不一样,但是这条命令在集群下执行,Redis 只会用大括号里的 user1 做 hash slot 计算,所以算出来的 slot 值肯定相同,最后都能落在同一 slot。

posted @ 2020-09-05 12:21  tree6x7  阅读(213)  评论(0编辑  收藏  举报