# 九、Redis 集群分片

# 1. 集群介绍

集群(cluster)就是一组计算机,它们作为一个整体向用户提供一组网络资源,这些单个的计算机系统就是集群的节点(node)。集群提供了以下关键的特性:

  • 可扩展性
  • 高可用性
  • 负载均衡
  • 错误恢复

分布式与集群的联系与区别如下:

  • 分布式即各个业务分开部署,集群指的是几台服务器集中在一起实现同一个业务。
  • 分布式的每一个节点都可以做成集群,而集群并不一定就是分布式的。

集群主要分为三大类:

  • HA(High Availability Cluster):高可用集群
  • LBC(Load Balance Cluster):负载均衡集群
  • HPC(High Performance Computing Cluster):高性能集群

# 2. Redis 集群架构

# 2.1 架构简介

  • 并发量大了 → 主从复制解决 → 主从稳定性 → 哨兵解决 → 单节点存储能力 → 集群 Cluster 解决
  • Redis Cluster 集群模式具有高可用、可扩展性、分布式、容错等特性。

img

Redis Cluster 采用无中心结构,每个节点都可以保存数据和整个集群状态,每个节点都和其他所有节点连接,Cluster 至少为 6 个才能保证组成完成高可用集群,分为 3 主 3 从。主节点分配槽,处理客户端的命令请求,从节点可用在主节点故障后,顶替主节点。

为什么至少 6 台呢?

我们可以在 Redis 官方找到这么一句话:

Note that the minial cluster that works as expected requires to contain at least three master nodes.

因为最小的 Redis 集群,需要至少 3 个主节点(哨兵),既然有 3 个主节点,而一个主节点搭配至少一个从节点,因此至少得 6 台 Redis。

如上图所示,该集群中包括 6 个 Redis 节点,3 主 3 从,分别为 M1、M2、M3、S1、S2、S3。除了主从 Redis 节点之间进行数据复制外,所有 Redis 节点之间采用 Gossip 协议进行通信,交换维护节点元数据信息。

Gossip

# 2.2 主从模式

  • 一个主节点搭配一个或多个从节点,从而保证高可用。

# 2.3 优点

  • 去中心化
  • 可扩展性
  • 高可用性
  • 自动故障转移

# 2.4 缺点

  • 数据通过异步复制,无法保证数据强一致性
  • 集群环境搭建略微复杂

# 3. 数据分区方式

随着请求量和数据量的增加,一台机器已经无法满足需求,我们就需要把数据和请求分割到多台机器,这时候就需要引入分布式存储。分布式分成有以下几个特性:

  • 增强可用性
  • 维护方便
  • 均衡 IO
  • 改善查询性能

分布式存储首先要解决的就是把整个数据集安全分区规则映射到多个节点的问题。

下面介绍几个常见的分区算法。

# 3.1 范围分区

  • 优点:同一范围内的范围查询不需要跨节点,提升查询速度
  • 应用场景:MySQL、Oracle

# 3.2 节点取余分区

hash(object) % N

  • 优点:实现简单,数据均匀分摊
  • 缺点:当扩容或缩容节点时,需要迁移的数据量大(翻倍扩容可以相对减少迁移量)。

# 3.3 一致性哈希分区

  • 优点:相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。
  • 缺点:当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
  • 应用场景:Memcached

img

一致性哈希算法

参考:https://zhuanlan.zhihu.com/p/98030096

1. 普通 hash 算法在分布式系统中的缺陷

在分布式的存储系统中,要将数据存储到具体的节点上,如果我们采用普通的 hash 算法进行路由,将数据映射到具体的节点上,如 key%N,key 是数据的 key,N 是机器节点数,如果有一个机器加入或退出这个集群,则所有的数据映射都无效了,如果是持久化存储则要做数据迁移,如果是分布式缓存,则其他缓存就失效了。

2. 一致性哈希

一致性哈希提出了在动态变化的 Cache 环境中,哈希算法应该满足的 4 个适应条件:

  • 均衡性(Balance)

    平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

  • 单调性(Monotonicity)

    单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。(这段翻译信息有负面价值的,当缓冲区大小变化时一致性哈希(Consistent hashing)尽量保护已分配的内容不会被重新映射到新缓冲区。

  • 低分散性(Spread)

    在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。

  • 低负载(Load)

    负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

3. 一致性哈希算法实现

  • 环形 hash 空间

    按照常用的 hash 算法来将对应的 key 哈希到一个具有 232 次方个节点的空间中,即 0 ~ (232-1)的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。

    节点的个数可以自定义,整个 hash 环我们可以用 TreeMap 来实现,因为 TreeMap 是有序的,我们刚好可以利用上。

    img

  • 映射服务器节点

    将各个服务器使用 hash 进行一个哈希,具体可以选择服务器的 IP 或唯一主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。假设我们将四台服务器使用 IP 地址哈希后在环空间的位置如下:

    img
  • 映射数据

    现在我们将 objectA、objectB、objectC、objectD 四个对象通过特定的 hash 函数计算出对应的 key 值,然后散列到 hash 环上,然后从数据所在位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。

    img
  • 节点的删除与增加

    • 如果此时 NodeC 宕机了,此时 Object A、B、D 不会受到影响,只有 ObjectC 会重新分配到 NodeD 上面去,而其他数据对象不会发生变化。

    • 如果在环境中新增一台服务器 Node X,通过 hash 算法将 Node X 映射到环中,通过按顺时针迁移的规则,那么ObjectC 被迁移到了 NodeX中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。

      img
  • 虚拟节点

    到目前为止一致性哈希也可以算做完成了,但是有一个问题还需要解决,那就是平衡性。从下图我们可以看出,当服务器节点比较少的时候,会出现一个问题,就是此时必然造成大量数据集中到一个节点上面,极少数数据集中到另外的节点上面。

    img

    为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以先确定每个物理节点关联的虚拟节点数量,然后在 IP 或者主机名后面增加编号。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:

    img

    同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到 NodeA上。这样就解决了服务节点少时数据倾斜的问题。每个物理节点关联的虚拟节点数量就根据具体的生产环境情况在确定。

# 3.4 虚拟槽分区

Redis 内部内置了序号 0-16383 个槽位,每个槽位可以用来存储一个数据集合,将这些槽位按顺序分配到集群中的各个节点。每次新的数据到来,会通过哈希函数 CRC16(key) 算出将要存储的槽位下标,然后通过该下标找到前面分配的 Redis 节点,最后将数据存储到该节点中。

这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。使用哈希槽的好处就在于可以方便的添加或移除节点。

  1. 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
  2. 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了。
  • 优点:每个 node 均匀的分配了 slot,缩小增减节点影响的范围。
  • 缺点:需要存储 node 和 slot 的对应信息。
  • 应用场景:Redis Cluster。

image-20210806181759610

# 4. 集群环境搭建

为了方便,本节集群环境中准备 3 台虚拟机,每台虚拟机上跑 2 个 Redis,模拟 6 台机器,搭建 3 主 3 从集群。

# 4.1 环境准备

IP 端口 操作系统
172.16.58.200 6371, 6372 CentOS 7
172.16.58.201 6373, 6374 CentOS 7
172.16.58.202 6375, 6376 CentOS 7

# 4.2 安装 Redis

分别在三台机器上下载 Redis:Redis 安装

# 4.3 创建目录

在三台机器上都创建以下目录

mkdir -p /usr/local/redis/cluster/conf
mkdir -p /usr/local/redis/cluster/data
mkdir -p /usr/local/redis/cluster/log

# 4.4 创建配置文件

三台机器都创建两个配置文件,6371 的如下,其余的同理。

vim /usr/local/redis/cluster/conf/redis-6371.conf

内容如下:

# 放行 IP 限制
bind 0.0.0.0
# 端口
port 6371
# 后台启动
daemonize yes
# 日志存储目录以及日志文件名
logfile "/usr/local/redis/cluster/log/redis-6371.log"
# RDB 数据文件名
dbfilename dump-6371.rbd
# AOF 模式开启和 AOF 数据文件名
appendonly yes
appendfilename "appendonly-6371.aof"
# RDB 数据文件和 AOF 数据文件的存储目录
dir /usr/local/redis/cluster/data
# 设置密码
requirepass 123456
# 从节点访问主节点密码(必须与 requirepass 一致)
masterauth 123456
# 是否开启集群模式,默认是 no
cluster-enabled yes
# 集群节点信息文件,会保存在 dir 配置对应目录下
cluster-config-file nodes-6371.conf
# 集群节点连接超时时间,单位是毫秒
cluster-node-timeout 15000
# 集群节点 IP
cluster-announce-ip 172.16.58.200
# 集群节点映射端口
cluster-announce-port 6371
# 集群节点总线端口
cluster-announce-bus-port 16371

# 4.5 启动 Redis

# 172.16.58.200
/usr/local/redis/bin/redis-server /usr/local/redis/cluster/conf/redis-6371.conf 
/usr/local/redis/bin/redis-server /usr/local/redis/cluster/conf/redis-6372.conf 

# 172.16.58.201
/usr/local/redis/bin/redis-server /usr/local/redis/cluster/conf/redis-6373.conf 
/usr/local/redis/bin/redis-server /usr/local/redis/cluster/conf/redis-6374.conf 

# 172.16.58.202
/usr/local/redis/bin/redis-server /usr/local/redis/cluster/conf/redis-6375.conf 
/usr/local/redis/bin/redis-server /usr/local/redis/cluster/conf/redis-6376.conf 

# 4.6 启动 Redis Cluster

随便找一个节点,执行以下命令,就可以创建 Redis Cluster 了:

/usr/local/redis/bin/redis-cli -a 123456 --cluster create \
172.16.58.200:6371 172.16.58.200:6372 \
172.16.58.201:6373 172.16.58.201:6374 \
172.16.58.202:6375 172.16.58.202:6376 \
--cluster-replicas 1
  • --cluster create:创建集群
  • --cluster-replicas:主从比例,1 表示一主一从

输出:

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 172.16.58.201:6374 to 172.16.58.200:6371
Adding replica 172.16.58.202:6376 to 172.16.58.201:6373
Adding replica 172.16.58.200:6372 to 172.16.58.202:6375
M: 8306858f71a9a4f43c644bf4426ae2c6561b36a5 172.16.58.200:6371
   slots:[0-5460] (5461 slots) master
S: 8f18a0cf21cc4bcc3d96c0e672edf0949abefad1 172.16.58.200:6372
   replicates be0ddf4921a4a2f077b8c0a72504c8714c69b836
M: ad98c789913e6cb9db293379509d39d8856d41b2 172.16.58.201:6373
   slots:[5461-10922] (5462 slots) master
S: 3c95fde7335c16c7f3fe323bde9cfc9778ca1d38 172.16.58.201:6374
   replicates 8306858f71a9a4f43c644bf4426ae2c6561b36a5
M: be0ddf4921a4a2f077b8c0a72504c8714c69b836 172.16.58.202:6375
   slots:[10923-16383] (5461 slots) master
S: 7ba9475d72e3ff57b409e9ff2ee7ecbfce4a024f 172.16.58.202:6376
   replicates ad98c789913e6cb9db293379509d39d8856d41b2
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 172.16.58.200:6371)
M: 8306858f71a9a4f43c644bf4426ae2c6561b36a5 172.16.58.200:6371
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 8f18a0cf21cc4bcc3d96c0e672edf0949abefad1 172.16.58.200:6372
   slots: (0 slots) slave
   replicates be0ddf4921a4a2f077b8c0a72504c8714c69b836
M: be0ddf4921a4a2f077b8c0a72504c8714c69b836 172.16.58.202:6375
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
M: ad98c789913e6cb9db293379509d39d8856d41b2 172.16.58.201:6373
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 3c95fde7335c16c7f3fe323bde9cfc9778ca1d38 172.16.58.201:6374
   slots: (0 slots) slave
   replicates 8306858f71a9a4f43c644bf4426ae2c6561b36a5
S: 7ba9475d72e3ff57b409e9ff2ee7ecbfce4a024f 172.16.58.202:6376
   slots: (0 slots) slave
   replicates ad98c789913e6cb9db293379509d39d8856d41b2
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

# 5. 集群状态检查

# 5.1 检查集群状态

命令:check

redis-cli -a 123456 --cluster check 172.16.58.201:6373

输出:

# 主节点信息
172.16.58.201:6373 (ad98c789...) -> 0 keys | 5462 slots | 1 slaves.
172.16.58.200:6371 (8306858f...) -> 0 keys | 5461 slots | 1 slaves.
172.16.58.202:6375 (be0ddf49...) -> 0 keys | 5461 slots | 1 slaves.
# 主节点中都有多少 key
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.16.58.201:6373)
# 主从详细信息
M: ad98c789913e6cb9db293379509d39d8856d41b2 172.16.58.201:6373
	 # 分槽情况
   slots:[5461-10922] (5462 slots) master
   # 从节点个数
   1 additional replica(s)
S: 3c95fde7335c16c7f3fe323bde9cfc9778ca1d38 172.16.58.201:6374
	 # 从节点无槽
   slots: (0 slots) slave
   # 从节点在复制哪个主节点
   replicates 8306858f71a9a4f43c644bf4426ae2c6561b36a5
S: 8f18a0cf21cc4bcc3d96c0e672edf0949abefad1 172.16.58.200:6372
   slots: (0 slots) slave
   replicates be0ddf4921a4a2f077b8c0a72504c8714c69b836
M: 8306858f71a9a4f43c644bf4426ae2c6561b36a5 172.16.58.200:6371
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: be0ddf4921a4a2f077b8c0a72504c8714c69b836 172.16.58.202:6375
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 7ba9475d72e3ff57b409e9ff2ee7ecbfce4a024f 172.16.58.202:6376
   slots: (0 slots) slave
   replicates ad98c789913e6cb9db293379509d39d8856d41b2
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

# 5.2 集群日志分析

由输出日志 Adding replica 172.16.58.201:6374 to 172.16.58.200:6371 我们可以知道 63716374 的主节点,我们就来分析这两个节点的日志。

  • 6371

    # 6371
    $ tail -f -n 1000 /usr/local/redis/cluster/log/redis-6371.log
    
    # 初始化 Redis 服务
    83479:M 05 Aug 2021 23:36:50.916 # Server initialized
    83479:M 05 Aug 2021 23:36:50.916 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').
    83479:M 05 Aug 2021 23:36:50.916 * Ready to accept connections
    83479:M 05 Aug 2021 23:52:38.570 # configEpoch set to 1 via CLUSTER SET-CONFIG-EPOCH
    # 从节点 6374 请求 SYNC 复制
    83479:M 05 Aug 2021 23:52:41.047 * Replica 172.16.58.201:6374 asks for synchronization
    83479:M 05 Aug 2021 23:52:41.047 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '2deebf1e2e8995cd7e561153ce643056c542f05e', my replication IDs are '9fed7c28df63218d0d29b9041a683ca7213dcad8' and '0000000000000000000000000000000000000000')
    # 创建 backlog 以及对应的复制 ID
    83479:M 05 Aug 2021 23:52:41.047 * Replication backlog created, my new replication IDs are '145f1a07ead94288b0324a632fcb1d1c2debbba7' and '0000000000000000000000000000000000000000'
    # bgsave
    83479:M 05 Aug 2021 23:52:41.047 * Starting BGSAVE for SYNC with target: disk
    83479:M 05 Aug 2021 23:52:41.047 * Background saving started by pid 89219
    89219:C 05 Aug 2021 23:52:41.052 * DB saved on disk
    89219:C 05 Aug 2021 23:52:41.052 * RDB: 0 MB of memory used by copy-on-write
    83479:M 05 Aug 2021 23:52:41.115 * Background saving terminated with success
    83479:M 05 Aug 2021 23:52:41.116 * Synchronization with replica 172.16.58.201:6374 succeeded
    83479:M 05 Aug 2021 23:52:43.522 # Cluster state changed: ok
    
  • 6374

    # 6374
    $ tail -f -n 1000 /usr/local/redis/cluster/log/redis-6374.log
    
    # 初始化 Redis 服务
    98575:M 05 Aug 2021 23:44:37.923 # Server initialized
    98575:M 05 Aug 2021 23:44:37.923 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').
    98575:M 05 Aug 2021 23:44:37.924 * Ready to accept connections
    98575:M 05 Aug 2021 23:52:38.749 # configEpoch set to 4 via CLUSTER SET-CONFIG-EPOCH
    # 在变为从节点之前,要做一些准备工作
    98575:S 05 Aug 2021 23:52:40.769 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
    # 集群环境准备 ok
    98575:S 05 Aug 2021 23:52:40.769 # Cluster state changed: ok
    # 连接主节点
    98575:S 05 Aug 2021 23:52:41.213 * Connecting to MASTER 172.16.58.200:6371
    # 开始主从复制
    98575:S 05 Aug 2021 23:52:41.213 * MASTER <-> REPLICA sync started
    # 触发了 SYNC 主从同步非阻塞命令
    98575:S 05 Aug 2021 23:52:41.214 * Non blocking connect for SYNC fired the event.
    # 主从 PING-PONG 通信
    98575:S 05 Aug 2021 23:52:41.216 * Master replied to PING, replication can continue...
    # 尝试一个部分同步
    98575:S 05 Aug 2021 23:52:41.221 * Trying a partial resynchronization (request 2deebf1e2e8995cd7e561153ce643056c542f05e:1).
    # 因为环境刚搭好,需要全量复制,所以被 master 拒绝了,开始全量复制
    98575:S 05 Aug 2021 23:52:41.223 * Full resync from master: 145f1a07ead94288b0324a632fcb1d1c2debbba7:0
    # 抛弃之前缓存的数据
    98575:S 05 Aug 2021 23:52:41.223 * Discarding previously cached master state.
    # 开始从主节点那拉数据
    98575:S 05 Aug 2021 23:52:41.291 * MASTER <-> REPLICA sync: receiving 175 bytes from master to disk
    98575:S 05 Aug 2021 23:52:41.292 * MASTER <-> REPLICA sync: Flushing old data
    98575:S 05 Aug 2021 23:52:41.292 * MASTER <-> REPLICA sync: Loading DB in memory
    # 加载 RDB
    98575:S 05 Aug 2021 23:52:41.293 * Loading RDB produced by version 6.0.9
    98575:S 05 Aug 2021 23:52:41.293 * RDB age 0 seconds
    98575:S 05 Aug 2021 23:52:41.293 * RDB memory usage when created 2.54 Mb
    98575:S 05 Aug 2021 23:52:41.293 * MASTER <-> REPLICA sync: Finished with success
    98575:S 05 Aug 2021 23:52:41.293 * Background append only file rewriting started by pid 100901
    # 加载 AOF
    98575:S 05 Aug 2021 23:52:41.318 * AOF rewrite child asks to stop sending diffs.
    100901:C 05 Aug 2021 23:52:41.318 * Parent agreed to stop sending diffs. Finalizing AOF...
    100901:C 05 Aug 2021 23:52:41.318 * Concatenating 0.00 MB of AOF diff received from parent.
    100901:C 05 Aug 2021 23:52:41.318 * SYNC append only file rewrite performed
    100901:C 05 Aug 2021 23:52:41.319 * AOF rewrite: 0 MB of memory used by copy-on-write
    98575:S 05 Aug 2021 23:52:41.414 * Background AOF rewrite terminated with success
    98575:S 05 Aug 2021 23:52:41.414 * Residual parent diff successfully flushed to the rewritten AOF (0.00 MB)
    98575:S 05 Aug 2021 23:52:41.414 * Background AOF rewrite finished successfully
    

# 5.3 查看集群信息

随便找一个节点,连进 Redis Cluster,核心选项 -c,表示以集群的模式连接:

redis-cli -c -a 123456 -h 172.16.58.200 -p 6371

# 5.3.1 查看集群信息 CLUSTER INFO

172.16.58.200:6371> CLUSTER INFO
# 集群状态
cluster_state:ok
# 总槽数
cluster_slots_assigned:16384
# 分配好的槽
cluster_slots_ok:16384
# 可能失败的槽
cluster_slots_pfail:0
# 真实失败的槽
cluster_slots_fail:0
# 已知集群节点
cluster_known_nodes:6
# 集群主节点个数
cluster_size:3
# 当前集群最大最新的 epoch
cluster_current_epoch:6
# 当前节点所在 epoch,后面都会同步到跟 cluster_current_epoch 一样
cluster_my_epoch:1
# ping-pong 信息
cluster_stats_messages_ping_sent:1306
cluster_stats_messages_pong_sent:1304
cluster_stats_messages_sent:2610
cluster_stats_messages_ping_received:1299
cluster_stats_messages_pong_received:1306
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:2610

# 5.3.2 查看节点信息 CLUSTER NODES

172.16.58.200:6371> CLUSTER NODES
8f18a0cf21cc4bcc3d96c0e672edf0949abefad1 172.16.58.200:6372@16372 slave be0ddf4921a4a2f077b8c0a72504c8714c69b836 0 1628234206407 5 connected
be0ddf4921a4a2f077b8c0a72504c8714c69b836 172.16.58.202:6375@16375 master - 0 1628234205407 5 connected 10923-16383
ad98c789913e6cb9db293379509d39d8856d41b2 172.16.58.201:6373@16373 master - 0 1628234203400 3 connected 5461-10922
3c95fde7335c16c7f3fe323bde9cfc9778ca1d38 172.16.58.201:6374@16374 slave 8306858f71a9a4f43c644bf4426ae2c6561b36a5 0 1628234205000 1 connected
7ba9475d72e3ff57b409e9ff2ee7ecbfce4a024f 172.16.58.202:6376@16376 slave ad98c789913e6cb9db293379509d39d8856d41b2 0 1628234204000 3 connected
8306858f71a9a4f43c644bf4426ae2c6561b36a5 172.16.58.200:6371@16371 myself,master - 0 1628234205000 1 connected 0-5460

# 6. 集群环境测试

# 6.1 SET/GET

# 我们在 set 的时候,redis 首先会对 key 进行 hash,找到它对应的槽上
172.16.58.200:6372> set username hedon
# username 经过 hash 后需要放在 6375 对应的槽上,所以重定向到 6375 上
-> Redirected to slot [14315] located at 172.16.58.202:6375
OK
# 因为 username 在 6375 上,当前是 6375,所以在 6375 上可以直接拿
172.16.58.202:6375> get username
"hedon"
172.16.58.202:6375> set address beijing
-> Redirected to slot [3680] located at 172.16.58.200:6371
OK
172.16.58.200:6371> get username
# 因为 username 在 6375 上,当前是 6371,所以需要重定向到 6375 上拿
-> Redirected to slot [14315] located at 172.16.58.202:6375
"hedon"

# 6.2 从节点只读模式

# 6372 是一个从节点,所以命令会跳转到它对应的主节点上
172.16.58.200:6372> get username
-> Redirected to slot [14315] located at 172.16.58.202:6375
"zhangsan"

# 可以设置 6372 只读,可以通过 READWRITE 来恢复
172.16.58.200:6372> READONLY  
OK

# 加上只读模式,6372 读取它的主节点 6375 上的数据,就不需要重定向了
172.16.58.200:6372> get username
"zhangsan"

# 当时读取集群中不是它的主节点的数据,还是需要重定向,因为 6372 只复制 6375 的数据
172.16.58.200:6372> get address
-> Redirected to slot [3680] located at 172.16.58.200:6371
"beijing"

# 7. 集群性能测试

结果

很显然单机的性能肯定是更高的,因为:

  1. 集群需要对 key 做哈希运算;
  2. 集群在 SET/GET 的时候需要做转发;

但是,集群的可用性更强,更加容灾。

# 7.1 redis-benchmark 命令

redis-benchmark 是一个测试 Redis 性能的工具,Redis 性能测试是通过同时执行多个命令实现的。

redis-benchmark [option] [option value]

可选参数:

选项 描述
-h 指定服务器主机名
-p 指定服务器端口
-s 指定服务器(socket 方式)
-c 指定并发连接数
-n 指定请求数
-d 以字节的形式指定 SET/GET 值的数据大小
-k 连接方式:1=keep alive(长连接); 0=reconnect
-r 指定 SET/GET/INCR 使用随机 key
-P 通过管道传输请求
-q 强制退出 Redis
--csv 以 CSV 格式输出
-l(小写的 L) 生成循环,永久执行测试
-t 仅运行以逗号分隔的测试命令列表
-I(大写的 i) Idle (空闲)模式,仅打开 N 个 Idle 连接并等待

# 7.2 单机测试

  • 准备单机配置文件 redis.conf

    # 备份
    cp /usr/local/redis/conf/redis.conf /usr/local/redis/conf/redis.conf.backup
    # 修改
    vim /usr/local/redis/conf/redis.conf
    

    内容:

    # 放行 IP
    bind 0.0.0.0
    # 后台启动
    daemonize yes
    # 日志存储目录及日志名
    logfile "/usr/local/redis/log/redis.log"
    # rdb 数据文件名
    dbfilename "dump.rdb"
    # aof 模式开启和 aof 数据文件名
    appendonly yes
    appendfilename "appendonly.aof"
    # rdb 数据文件和 aof 数据文件的存储目录
    dir "/usr/local/redis/data"
    # 设置密码
    requirepass 123456
    
  • 启动单机 Redis

    # 如果之前有运行单机 Redis,则杀死
    ps -ef | grep redis
    kill -9 PID
    # 启动
    /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf
    
  • 测试

    /usr/local/redis/bin/redis-benchmark -a 123456 -h 172.16.58.200 -p 6379 -t set,get -r 1000000 -n 1000000 -c 1000
    
  • 结果

    # SET 一百万次在 18.9s 内完成,每秒 52912.85 次
    ====== SET ======
      1000000 requests completed in 18.90 seconds
      1000 parallel clients
      3 bytes payload
      keep alive: 1
      host configuration "save": 
      host configuration "appendonly": yes
      multi-thread: no
    
    0.00% <= 3 milliseconds
    ...
    100.00% <= 36 milliseconds
    52912.85 requests per second
    
    # GET 一百万次在 19.92 内完成,每秒 50210.89 次
    ====== GET ======
      1000000 requests completed in 19.92 seconds
      1000 parallel clients
      3 bytes payload
      keep alive: 1
      host configuration "save": 
      host configuration "appendonly": yes
      multi-thread: no
    
    0.00% <= 2 milliseconds
    ...
    100.00% <= 47 milliseconds
    50210.89 requests per second
    
  • 总结

    • SET:一百万次在 18.9s 内完成,每秒 52912.85 次
    • GET:一百万次在 19.92 内完成,每秒 50210.89 次

# 7.3 集群测试

  • 随便先一个节点执行

    /usr/local/redis/bin/redis-benchmark -a 123456 -h 172.16.58.200 -p 6371 -t set,get -r 1000000 -n 1000000 -c 1000
    
  • 结果

    # SET 一百万次在 23.58s 内完成,每秒 42399.83 次
    ====== SET ======
      1000000 requests completed in 23.58 seconds
      1000 parallel clients
      3 bytes payload
      keep alive: 1
      host configuration "save": 
      host configuration "appendonly": yes
      multi-thread: no
    
    0.00% <= 3 milliseconds
    ....
    100.00% <= 113 milliseconds
    42399.83 requests per second
    
    # GET 一百万次在 29.20 内完成,每秒 34241.88 次
    ====== GET ======
      1000000 requests completed in 29.20 seconds
      1000 parallel clients
      3 bytes payload
      keep alive: 1
      host configuration "save": 
      host configuration "appendonly": yes
      multi-thread: no
    
    0.00% <= 3 milliseconds
    ...
    100.00% <= 238 milliseconds
    34241.88 requests per second
    
  • 总结

    • SET:一百万次在 23.58s 内完成,每秒 42399.83 次
    • GET:一百万次在 29.20 内完成,每秒 34241.88 次

# 8. 集群原理总结

# 8.1 哈希槽

Redis Cluster 并没有选用一致性哈希算法,而是采用了哈希槽(SLOT),主要原因是一致性哈希算法对于数据分布、节点位置的控制并不是很友好,一致性哈希算法不太适合节点数目较少的情景。

哈希槽其实是两个概念:

  1. 哈希算法:Redis Cluster 的哈希算法并不是简单的 hash(),而是 CRC16 算法,这是一种校验算法;

    CRC16

    所以 CRC,就是循环冗余校验法,可以参考:计算机网络丨链路层丨差错检测丨循环冗余校验码

  2. 槽位的概念:空间分配的规则。

注意

对于槽位的转移和分派,Redis Cluster 是不会自动进行的,而是需要人工配置的。所以 Redis Cluster 的高可用是依赖于节点的主从复制与主从间的自动故障转移。

# 8.2 16384 个 slot

Redis Cluster 没有单机的那种 16 个数据库(0-15)的概念,而是分成了 16384 个 slot,每个节点负责其中的一部分 slots。

为什么 Redis Cluster 会设计成 16384 个槽位呢?

正常的心跳包携带一个节点的完整配置,可以用幂等的方式替换旧的配置,以便更新旧的配置。这意味着它们以原始形式包含节点的槽配置,该节点使用 2k 的空间和 16k 的槽,但使用 65k 的槽将使用 8k 的空间。

同时,由于其他的设计权衡,Redis Cluster 不太可能扩展到超过 1000 个 master 节点。

所以 16k 是在正确的范围内,以确保每个主机有足够的插槽,最大 1000 个master,但足够小的数字传播插槽配置作为一个原始位图容易。注意,在小集群中,位图将很难压缩,因为当 N 很小时,位图将设置槽 N 位,这是设置的位的很大百分比。

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

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

2. Redis 的集群主节点数量基本不可能超过 1000 个。

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

3. 槽位越小,节点少的情况下,压缩率高。

Redis 主节点的配置信息中,它所负责的哈希槽是通过一张 bitmap 的形式来保存的,在传输过程中,会对 bitmap 进行压缩,但是如果bitmap 的填充率 slots / N 很高的话(N 表示节点数),bitmap 的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap 的压缩率就很低。

# 8.3 槽位定位算法

Redis Cluster 默认会对 key 值使用 CRC16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体的槽位。

即:HASH_SLOT = CRC16(key) mod 16384

image-20210806181754810

# 8.4 扩容原理

# 8.4.1 添加主节点

  1. 准备节点 M4;
  2. 启动节点 M4;
  3. 使用 add-node 将节点加入到集群当中;
  4. 使用 reshard 对集群重新分配,给 M4 分配槽;
image-20210810103452785

# 8.4.2 添加从节点

  1. 准备节点 S4;
  2. 启动节点 S4;
  3. 使用 add-node 将节点加入到集群当中,并用 --cluater-salve 声明该节点为从节点,且使用 --cluster-master-id 指定主节点 ID;

# 8.5 缩容原理

# 8.5.1 删除从节点

  1. 使用 del-node 将节点从集群中移除;
  2. 使用 kill 或 SHUTDOWN 停止节点进程。

# 8.5.2 删除主节点

删除主节点稍微麻烦一点,因为主节点分配了槽,所以必须先把槽分配给其他可用节点后,才可以移除节点,不然会造成数据丢失。

  1. 使用 reshard 将要删除的主节点 M4 的槽分配给其他节点;
  2. 使用 del-node 将 M4 从集群中移除;
  3. 停止 M4 进程。
image-20210810103747994
上次更新: 10/12/2021, 4:28:38 PM