抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Redis集群方案

哨兵的问题

​ 虽然主从复制和哨兵模式完美的解决了Redis的单机问题,但是Redis仍然存在着以下两个问题:

  1. 所有的写操作都集中到主服务器上,主服务器CPU压力比较大
  2. 不管是主服务器还是从服务器,它们都同样保存了redis的所有数据,随着数据越来越多,可能会出现内存不够用的问题

Redis集群简介

​ redis从3.0开始支持集群功能。redis集群采用无中心节点方式实现,无需proxy代理,客户端直接与redis集群的每个节点连接,根据同样的hash算法计算出key对应的slot,然后直接在slot对应的redisj节点上执行命令。在redis看来,响应时间是最苛刻的条件,增加一层带来的开销是redis不能接受的。因此,redis实现了客户端对节点的直接访问,为了去中心化,节点之间通过gossip协议交换互相的状态,以及探测新加入的节点信息。redis集群支持动态加入节点,动态迁移slot,以及自动故障转移。

​ Redis Cluster的具体实现细节是采用了Hash槽的概念,集群会预先分配16384个槽,并将这些槽分配给具体的服务节点,通过对Key进行CRC16(key)%16384运算得到对应的槽是哪一个,从而将读写操作转发到该槽所对应的服务节点。当有新的节点加入或者移除的时候,再来迁移这些槽以及其对应的数据。在这种设计之下,我们就可以很方便的进行动态扩容或缩容,个人也比较倾向于这种集群模式。

Redis 集群目标

  1. 高性能
  2. 线性扩容
  3. 高可用

Redis 集群结构特点

  • 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。

  • 节点的fail是通过集群中超过半数的节点检测失效时才生效。

  • 客户端与redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

  • redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node、slot、value。

  • Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。

Redis 集群提供的功能

  • 数据自动分片

    ​ 集群中每个节点都会负责一定数量的slot,每个key会映射到一个具体的slot,通过这种方式就可能找到key具体保存在哪个节点上了。

  • 提供hash tags功能

    ​ 通过hash tag功能可以将多个不同key映射到同一个slot上,这样就能够提供multi-key操作,hash tag的使用的方式是在key中包含“{}”,这样只有在“{…}”中字串被用于hash计算。

  • 自动失效转移和手动失效转移

  • 减少硬件成本和运维成本。

Redis 集群功能限制

Redis集群相对单机在功能上有一定限制。

  • key批量操作支持有限。如:MSET``MGET,目前只支持具有相同slot值的key执行批量操作。

  • key事务操作支持有限。支持多key在同一节点上的事务操作,不支持分布在多个节点的事务功能。

  • key作为数据分区的最小粒度,因此不能将一个大的键值对象映射到不同的节点。如:hash、list。

  • 不支持多数据库空间。单机下Redis支持16个数据库,集群模式下只能使用一个数据库空间,即db 0。

  • 复制结构只支持一层,不支持嵌套树状复制结构。

Redis集群方案

​ 在redis集群中,key只能保存在按照某种规律计算得到的节点上,对该key的读取和更新也只能在该节点进行。比如redis集群一共有6个节点,现在我想执行 set name hello,这个key为name,常见的某种规律有哈希取余"name".hashcode() % 6 + 1得到节点的位置为4,所以就放在第四个的位置上,以后不管我是读取还是更新还是删除,我都到第四个节点上。如此一来,便完美解决了上述两个问题。

客户端分区方案

​ 指在客户端计算key得到将要保存的节点,然后客户端再连接该节点端口,进行数据操作。这种方案比较简单,但是一旦节点数发生变化,将要更新新的计算算法(比如取余这个6改成10)到所有客户端上,会比较麻烦。

​ 客户端分区方案 的代表为 Redis Sharding,Redis Sharding 是 Redis Cluster 出来之前,业界普遍使用的 Redis 多实例集群 方法。Java 的 Redis 客户端驱动库 Jedis,支持 Redis Sharding 功能,即 ShardedJedis 以及 结合缓存池 的 ShardedJedisPool。

优点

​ 不使用 第三方中间件,分区逻辑 可控,配置 简单,节点之间无关联,容易 线性扩展,灵活性强。

缺点

​ 客户端 无法 动态增删 服务节点,客户端需要自行维护 分发逻辑,客户端之间 无连接共享,会造成 连接浪费。

代理分区方案

​ 指在客户端和服务器之间加了一层代理层,客户端的命令先到代理层,代理层进行计算,再分配到它对应的节点上;这种方法挺好的,节点数发生变化,只需要修改代理层的计算算法即可,但是需要多一层转发,需要一定的耗时。

代理分区 主流实现的有方案有 Twemproxy 和 Codis。

优点

​ 简化 客户端 的分布式逻辑,客户端 透明接入,切换成本低,代理的 转发 和 存储 分离。

缺点

​ 多了一层 代理层,加重了 架构部署复杂度 和 性能损耗。

查询路由方案

​ 节点之间早就约定好哪些key是属于自己,哪些key是属于其它节点;客户端最开始随机把命令发给某个节点,节点计算并查看这个key是否属于自己的,如果是自己的就进行处理,并把结果发回去;如果是其它节点的,就会把那个节点的信息(ip + 地址)转发给客户端,让客户端重定向,这么一说感觉是有点像http协议中的3XX状态码。今天的主角Redis Cluster就是基于查询路由方案。

优点

​ 无中心节点,数据按照 槽 存储分布在多个 Redis 实例上,可以平滑的进行节点 扩容/缩容,支持 高可用 和 自动故障转移,运维成本低。

缺点

​ 严重依赖 Redis-trib 工具,缺乏 监控管理,需要依赖 Smart Client (维护连接,缓存路由表,MultiOp 和 Pipeline 支持)。Failover 节点的 检测过慢,不如 中心节点 ZooKeeper 及时。Gossip 消息具有一定开销。无法根据统计区分 冷热数据。

数据分区

数据分布理论

分布式数据库 首先要解决把 整个数据集 按照 分区规则 映射到 多个节点 的问题,即把 数据集 划分到 多个节点 上,每个节点负责 整体数据 的一个 子集。

​ 数据分布通常有 哈希分区顺序分区 两种方式,对比如下:

​ 分区方式 特点 相关产品 哈希分区 离散程度好,数据分布与业务无关,无法顺序访问 Redis Cluster,Cassandra,Dynamo 顺序分区 离散程度易倾斜,数据分布与业务相关,可以顺序访问 BigTable,HBase,Hypertable 由于 Redis Cluster 采用 哈希分区规则,这里重点讨论 哈希分区。常见的 哈希分区 规则有几种,下面分别介绍:

节点取余分区

​ 使用特定的数据,如 Redis 的 键 或 用户 ID,再根据 节点数量 N 使用公式:hash(key)% N 计算出 哈希值,用来决定数据 映射 到哪一个节点上。

优点

​ 这种方式的突出优点是 简单性,常用于 数据库 的 分库分表规则。一般采用 预分区 的方式,提前根据 数据量 规划好 分区数,比如划分为 512 或 1024 张表,保证可支撑未来一段时间的 数据容量,再根据 负载情况 将 表 迁移到其他 数据库 中。扩容时通常采用 翻倍扩容,避免 数据映射 全部被 打乱,导致 全量迁移 的情况。

缺点

​ 当 节点数量 变化时,如 扩容 或 收缩 节点,数据节点 映射关系 需要重新计算,会导致数据的 重新迁移。

一致性哈希分区

​ 一致性哈希 可以很好的解决 稳定性问题,可以将所有的 存储节点 排列在 收尾相接 的 Hash 环上,每个 key 在计算 Hash 后会 顺时针 找到 临接 的 存储节点 存放。而当有节点 加入 或 退出 时,仅影响该节点在 Hash 环上 顺时针相邻 的 后续节点。

优点

​ 加入 和 删除 节点只影响 哈希环 中 顺时针方向 的 相邻的节点,对其他节点无影响。

缺点

​ 加减节点 会造成 哈希环 中部分数据 无法命中。当使用 少量节点 时,节点变化 将大范围影响 哈希环 中 数据映射,不适合 少量数据节点 的分布式方案。普通 的 一致性哈希分区 在增减节点时需要 增加一倍 或 减去一半 节点才能保证 数据 和 负载的均衡。

注意

​ 因为 一致性哈希分区 的这些缺点,一些分布式系统采用 虚拟槽 对 一致性哈希 进行改进,比如 Dynamo 系统。

虚拟槽分区

​ 虚拟槽分区 巧妙地使用了 哈希空间,使用 分散度良好 的 哈希函数 把所有数据 映射 到一个 固定范围 的 整数集合 中,整数定义为 槽(slot)。这个范围一般 远远大于 节点数,比如 Redis Cluster 槽范围是 0 ~ 16383。槽 是集群内 数据管理 和 迁移 的 基本单位。采用 大范围槽 的主要目的是为了方便 数据拆分 和 集群扩展。每个节点会负责 一定数量的槽,如图所示:

​ 当前集群有 5 个节点,每个节点平均大约负责 3276 个 。由于采用 高质量哈希算法,每个槽所映射的数据通常比较 均匀,将数据平均划分到 5 个节点进行 数据分区。Redis Cluster 就是采用 虚拟槽分区

节点1: 包含 0 到 3276 号哈希槽。

节点2:包含 3277 到 6553 号哈希槽。

节点3:包含 6554 到 9830 号哈希槽。

节点4:包含 9831 到 13107 号哈希槽。

节点5:包含 13108 到 16383 号哈希槽。

​ 这种结构很容易 添加 或者 删除 节点。如果 增加 一个节点 6,就需要从节点 1 ~ 5 获得部分 槽 分配到节点 6 上。如果想 移除 节点 1,需要将节点 1 中的 槽 移到节点 2 ~ 5 上,然后将 没有任何槽 的节点 1 从集群中 移除 即可。

​ 由于从一个节点将 哈希槽 移动到另一个节点并不会 停止服务,所以无论 添加删除或者 改变 某个节点的 哈希槽的数量 都不会造成 集群不可用 的状态.

Redis的数据分区

​ Redis Cluster 采用 虚拟槽分区,所有的 根据 哈希函数 映射到 0~16383 整数槽内,计算公式:slot = CRC16(key)& 16383。每个节点负责维护一部分槽以及槽所映射的 键值数据,如图所示:

Redis虚拟槽分区的特点
  • 解耦 数据 和 节点 之间的关系,简化了节点 扩容 和 收缩 难度。

  • 节点自身 维护槽的 映射关系,不需要 客户端 或者 代理服务 维护 槽分区元数据。

  • 支持 节点、槽、键 之间的 映射查询,用于 数据路由、在线伸缩 等场景。

Redis集群的功能限制

  • Redis 集群相对 单机 在功能上存在一些限制,需要 开发人员 提前了解,在使用时做好规避。

  • key 批量操作 支持有限。

  • 类似 mset、mget 操作,目前只支持对具有相同 slot 值的 key 执行 批量操作。对于 映射为不同 slot 值的 key 由于执行 mget、mget 等操作可能存在于多个节点上,因此不被支持。

  • key 事务操作 支持有限。

  • 只支持 多 key 在 同一节点上 的 事务操作,当多个 key 分布在 不同 的节点上时 无法 使用事务功能。

  • key 作为 数据分区 的最小粒度

  • 不能将一个 大的键值 对象如 hash、list 等映射到 不同的节点。

  • 不支持 多数据库空间

  • 单机 下的 Redis 可以支持 16 个数据库(db0 ~ db15),集群模式 下只能使用 一个 数据库空间,即 db0。

  • 复制结构 只支持一层

  • 从节点 只能复制 主节点,不支持 嵌套树状复制 结构。

Redis集群的大概流程

​ 集群创建之初,我们可以自动或者手动给每个节点分配槽位。每个节点通过Gossip协议,会和其它节点交换槽信息,得到并且保存槽与节点的全局对应关系图。于是节点收到客户端发来的命令以后,对key进行CRC16(key) & 16383的计算得到槽位,对比这个槽位是不是属于自己的,如果是自己的就进行处理,并把结果发回去;如果是其它节点的,就会把那个节点的信息(ip + 地址)转发给客户端,然后客户端再重新请求;当然客户端也不会那么傻,每次都是随机请求节点,客户端在启动的时候也会和服务器交换信息得到槽和节点的映射图,客户端请求的节点,也是客户端自己计算CRC16(key) & 16383得到槽位,再对比关系图而得到的节点,如果节点发生变化了(即收到请求重定向),它也会更新这个关系图。

创建集群

准备节点

一个高可用的redis集群至少要有6个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# redis-6379.conf
port 6379
daemonize yes
protected-mode no
logfile "6379.log"
dbfilename "dump-6379.rdb"
cluster-enabled yes # 开启集群
cluster-node-timeout 15000 #节点超时时间,15s
cluster-config-file "nodes-6379.conf" #集群内部配置文件

# redis-6380.conf
port 6380
daemonize yes
protected-mode no
logfile "6380.log"
dbfilename "dump-6380.rdb"
cluster-enabled yes # 开启集群
cluster-node-timeout 15000 #节点超时时间,15s
cluster-config-file "nodes-6380.conf" #集群内部配置文件

# redis-6381.conf
port 6381
daemonize yes
protected-mode no
logfile "6381.log"
dbfilename "dump-6381.rdb"
cluster-enabled yes # 开启集群
cluster-node-timeout 15000 #节点超时时间,15s
cluster-config-file "nodes-6381.conf" #集群内部配置文件

# redis-6382.conf
port 6382
daemonize yes
protected-mode no
logfile "6382.log"
dbfilename "dump-6382.rdb"
cluster-enabled yes # 开启集群
cluster-node-timeout 15000 #节点超时时间,15s
cluster-config-file "nodes-6382.conf" #集群内部配置文件

# redis-6383.conf
port 6383
daemonize yes
protected-mode no
logfile "6383.log"
dbfilename "dump-6383.rdb"
cluster-enabled yes # 开启集群
cluster-node-timeout 15000 #节点超时时间,15s
cluster-config-file "nodes-6383.conf" #集群内部配置文件

# redis-6384.conf
port 6384
daemonize yes
protected-mode no
logfile "6384.log"
dbfilename "dump-6384.rdb"
cluster-enabled yes # 开启集群
cluster-node-timeout 15000 #节点超时时间,15s
cluster-config-file "nodes-6384.conf" #集群内部配置文件

6个节点启动成功后,我们可以在redis目录下看到生成的cluster-config-file文件,打开nodes-6379.conf如下:

1
2
8ba45af25feef061507831ca1b3ddf71a7574631 :0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0

其中8ba45af25feef061507831ca1b3ddf71a7574631是6379redis的节点ID,这里我们只要知道它很重要就可以了。

节点握手

打开客户端进入6379,然后依次运行cluster meet 139.199.168.61 6380cluster meet 139.199.168.61 6384

1
2
3
4
5
6
7
8
9
10
139.199.168.61:6379> cluster meet 139.199.168.61 6380
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6381
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6382
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6383
OK
139.199.168.61:6379> cluster meet 139.199.168.61 6384
OK

cluster meet 两个节点互相感知对方存在,发起节点发送发送Gossip协议中的meet消息给接收节点,接收节点收到meet消息后,保存发起节点的信息,然后通过返回pong消息把自己的信息也返回回去,之后两个节点会定期ping/pong进行节点通信。我们可以把它理解为把某个节点拉到一个集群里面,如果把其它节点也拉进来以后,集群里面的节点两两之间都会互相握手。等所有节点都拉到集群以后,我们可以执行cluster nodes来查看集群中节点间的关系。

1
2
3
4
5
6
7
139.199.168.61:6379> cluster nodes
8ba45af25feef061507831ca1b3ddf71a7574631 10.104.90.159:6379 myself,master - 0 0 1 connected
0573105a355722bc6dd5ab29dea072ce1a6956df 139.199.168.61:6381 master - 0 1540711564922 2 connected
a08f700001a5902dd82b51eb74b4ec8028202d75 139.199.168.61:6382 master - 0 1540711562919 3 connected
dc7a392e05e8b9840164bb21ef662168e28d71b4 139.199.168.61:6380 master - 0 1540711563919 0 connected
ba7816cc7ed0f5c0360708048a68c29b6bf66193 139.199.168.61:6383 master - 0 1540711565924 4 connected
d0585a4fda8ab743bd5b3448f26f46f1e09c19c9 139.199.168.61:6384 master - 0 1540711561916 5 connected

分配槽

以上只是建立了一个集群,但是其实集群还不能工作,可以用cluster info来查看集群状态:

1
2
3
4
5
6
7
8
9
10
11
12
139.199.168.61:6379> cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:0
cluster_current_epoch:5
cluster_my_epoch:1
cluster_stats_messages_sent:288
cluster_stats_messages_received:288

可以看到此时集群的状态是fail,失败的,我们需要把这16383个槽分出去,集群才能正常工作,分配槽的命令如下:

1
2
3
/usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6379 cluster addslots {0..5461}
/usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6380 cluster addslots {5462..10922}
/usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6381 cluster addslots {10923..16383}

注意:0和5641之间隔的是两个点,因为看书上写的是三个点,会报(error) ERR Invalid or out of range slot的错误。

​ 这样子就把所有的槽都分出去了,但是只用到了三个节点,剩下三个节点我们可以作为从节点,可以使用cluster replicate 主节点id来把某个节点挂为某个节点的从节点。

1
2
3
139.199.168.61:6382> cluster replicate 8ba45af25feef061507831ca1b3ddf71a7574631
139.199.168.61:6383> cluster replicate a08f700001a5902dd82b51eb74b4ec8028202d75
139.199.168.61:6384> cluster replicate 0573105a355722bc6dd5ab29dea072ce1a6956df

最后我们来看一下节点状态:

1
2
3
4
5
6
7
8
9
10
11
12
139.199.168.61:6379> cluster info
cluster_state:ok #状态OK
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:5
cluster_my_epoch:1
cluster_stats_messages_sent:7325
cluster_stats_messages_received:7325

再来查看一下节点关系:

1
2
3
4
5
6
7
139.199.168.61:6379> cluster nodes
8ba45af25feef061507831ca1b3ddf71a7574631 10.104.90.159:6379 myself,master - 0 0 1 connected 0-5461
0573105a355722bc6dd5ab29dea072ce1a6956df 139.199.168.61:6381 master - 0 1540714889809 2 connected 10923-16383
a08f700001a5902dd82b51eb74b4ec8028202d75 139.199.168.61:6382 slave 8ba45af25feef061507831ca1b3ddf71a7574631 0 1540714890811 3 connected
dc7a392e05e8b9840164bb21ef662168e28d71b4 139.199.168.61:6380 master - 0 1540714885803 0 connected 5462-10922
ba7816cc7ed0f5c0360708048a68c29b6bf66193 139.199.168.61:6383 slave dc7a392e05e8b9840164bb21ef662168e28d71b4 0 1540714891813 4 connected
d0585a4fda8ab743bd5b3448f26f46f1e09c19c9 139.199.168.61:6384 slave 0573105a355722bc6dd5ab29dea072ce1a6956df 0 1540714888807 5 connected

节点id,节点ip/端口,是否是主节点,节点的槽位分配一览无余。至此,一个完整的redis cluster集群创建成功。

故障转移原理

节点通信(Gossip协议)

Gossip协议

常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息。

meet消息

​ 用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。

ping消息

​ 集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息,ping消息发送封装了自身节点和部分其他节点的状态数据。

pong消息

​ 当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。

fail消息

​ 当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

通信过程

我们知道集群中的节点为了交换自身的槽位信息,节点与节点之间会不停的进行通信。通信采用Gossip协议,工作原理是节点彼此不停的通信交换信息,一段时间后所有的节点都会知道集群的完整信息,有点类似流言传播。节点ping其它节点的时候,也会把其它节点的信息带上,接收方会记录这些节点的信息,然后再向这些节点发送ping信息。

故障发现

​ 当集群少量节点出行故障时,能通过自动化故障转移保证集群的高可用。那是怎么发现故障节点的呢?

​ 包括两个环节:主观下线pfail (单个节点认为另一个节点下线,将它标记为pfail) 、客观下线 fail(节点彼此之间通过信息交换,大家达成共识了,都认为该节点下线,标记为fail)

这里所说的“大家达成共识了”,指的是主节点投票超过半数以上,就是说如果是3主3从集群,至少要有2个主节点认为该节点下线,从节点没资格参与投票。

如果是主节点,就要进行故障转移了

故障转移

​ 主节点发生故障了,从节点收到fail广播消息,从节点会尝试发起选举

​ 其他主节点接收到选举消息,会进行投票,超过半数以上通过才可以完成选举。

(详细过程请阅读redis开发与运维.pdf)

​ 故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2 个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1个主节点选票将导致故障转移失败。这个问题也适用于故障发现环 节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点 问题。

评论