JohnShen's Blog.

Redis-集群

字数统计: 3.3k阅读时长: 12 min
2021/03/24 Share

Redis Cluster 是 Redis 的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用 Cluster 架构方案达到负载均衡的目的。

1. 数据分布

分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集,常规的有:哈希分区、顺序分区。主要的哈希分区规则有:节点取余分区一致性哈希分区虚拟槽分区

节点取余分区

hash(key) % N计算出哈希值,用来决定数据映射到哪一个节点上。

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

优点:简单性,常用于数据库的分库分表规则。

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

一致性哈希分区

思路是为系统中每个节点分配一个token,范围一般在0~2^32,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。

优点:加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。

缺点:增删节点需手动处理或忽略部分数据,因此一致性哈希多用于缓存场景;节点较少时,节点的变化将大范围影响数据映射;增减节点时为保证数据和负载的均衡需增加一倍或减去一半节点。

虚拟槽分区

使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。

槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展,每个节点会负责一定数量的槽。

Redis数据分布

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

这种 Redis 虚拟槽分区的特点:

  • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
  • 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。

不过 Redis 集群存在功能限制:

  • key 批量操作支持有限。如 mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mget、mget等操作可能存在于多个节点上因此不被支持。
  • key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
  • key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。
  • 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
  • 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

2. 搭建集群

配置示例:

1
2
3
4
5
6
7
8
9
#节点端口
port 6379
# 开启集群模式
cluster-enabled yes
# 节点超时时间, 单位毫秒
cluster-node-timeout 15000
# 集群内部配置文件
cluster-config-file "nodes-6379.conf"
daemonize yes

启动:

1
2
3
4
5
6
redis-server redis-6379.conf
redis-server redis-6380.conf
redis-server redis-6381.conf
redis-server redis-6382.conf
redis-server redis-6383.conf
redis-server redis-6384.conf

第一次启动时如果没有集群配置文件,它会自动创建一份,文件名称采用cluster-config-file参数项控制,建议采用node-{port}.conf格式定义。

当集群内节点信息发生变化,如添加节点、节点下线、故障转移等。节点会自动保存集群状态到配置文件中。需要注意的是,Redis自动维护集群配置文件,不要手动修改,防止节点重启时产生集群信息错乱。

文件内容记录了集群初始状态,这里最重要的是节点ID。节点ID不同于运行ID。节点ID在集群初始化时只创建一次,节点重启时会加载集群配置文件进行重用,而Redis的运行ID每次重启都会变化。

使用meet命令握手:

1
2
3
4
5
127.0.0.1:6379>cluster meet 127.0.0.1 6380
127.0.0.1:6379>cluster meet 127.0.0.1 6381
127.0.0.1:6379>cluster meet 127.0.0.1 6382
127.0.0.1:6379>cluster meet 127.0.0.1 6383
127.0.0.1:6379>cluster meet 127.0.0.1 6384

查看集群节点:cluster nodes

查看集群状态: cluster info (此时发现 cluster_state:fail ,是因为未分配槽)

分配槽:

1
2
3
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0..5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5462..10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923..16383}

使用cluster replicate {nodeId}命令让一个节点成为从节点:

1
2
3
4
5
127.0.0.1:6382>cluster replicate <6379节点ID>
OK
127.0.0.1:6383>cluster replicate <6380节点ID>
OK
127.0.0.1:6384>cluster replicate <6381节点ID>

3. 节点通信

Redis Cluster 采用 P2P 的 Gossip(流言)协议来维护节点元数据信息,Gossip 协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。

  • 集群中的每个节点都会单独开辟TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。

  • 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。

  • 接收到ping消息的节点用pong消息作为响应

Gossip

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

  • meet 消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong消息交换。
  • ping 消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送 ping 消息,用于检测节点是否在线和交换彼此状态信息。ping 消息发送封装了自身节点和部分其他节点的状态数据。
  • pong 消息:当接收到 ping、meet 消息时,作为响应消息回复给发送方确认消息正常通信。pong 消息内部封装了自身状态数据。节点也可以向集群内广播自身的 pong 消息来通知整个集群对自身状态进行更新。
  • fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

节点选择

集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息。

如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。

4. 集群伸缩

集群的水平伸缩的上层原理:集群伸缩=槽和数据在节点之间的移动

扩容集群

A. 准备新节点

B. 加入集群

1
2
3
4
5
6
7
8
# 准备节点
redis-server conf/redis-6385.conf
redis-server conf/redis-6386.conf
# 加入集群
127.0.0.1:6379> cluster meet 127.0.0.1 6385
127.0.0.1:6379> cluster meet 127.0.0.1 6386
# 查看状态
cluster nodes

C. 迁移槽和数据

迁移槽

槽是Redis集群管理数据的基本单位,首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。

迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀。例如,在集群中加入6385节点。加入6385节点后,原有节点负责的槽数量从6380变为4096个。

迁移数据
  1. 对目标节点发送cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据。
  2. 对源节点发送cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽的数据。
  3. 源节点循环执行cluster getkeysinslot {slot} {count}命令,获取count个属于槽{slot}的键。
  4. 在源节点上执行migrate {targetIp} {targetPort} "" 0 {timeout} keys {keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
  5. 重复执行步骤3和步骤4直到槽下所有的键值数据迁移到目标节点。
  6. 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def move_slot(source,target,slot):
# 目标节点准备导入槽
target.cluster("setslot",slot,"importing",source.nodeId);
# 源节点准备全出槽
source.cluster("setslot",slot,"migrating",target.nodeId);
while true :
# 批量从源节点获取键
keys = source.cluster("getkeysinslot",slot,pipeline_size);
if keys.length == 0:
# 键列表为空时, 退出循环
break;
# 批量迁移键到目标节点
source.call("migrate",target.host,target.port,"",0,timeout,"keys",keys);
# 向集群所有主节点通知槽被分配给目标节点
for node in nodes:
if node.flag == "slave":
continue;
node.cluster("setslot",slot,"node",target.nodeId);

生成环境实际操作时涉及大量槽并且每个槽对应非常多的键使用,一般使用redis-trib.rb

添加从节点

6385、6386节点加入到集群,节点6385迁移了部分槽和数据作为主节点,需把6386作为6385的从节点。

在集群模式下 slaveof 添加从节点操作不再支持,需使用cluster replicate进行操作。

5. 请求路由

客户端发送键命令至任意节点,会计算槽和对应节点,若发现指向自己则执行命令,若不是自己则回复MOVED。

1
2
127.0.0.1:6379> set key:test:2 value-2
(error) MOVED 9252 127.0.0.1:6380

cluster keyslot key:test:1: 返回该key值对应的槽

cluster nodes:可以看到槽对应的节点

redis-cli使用-c参数时支持自动重定向,但实质是客户端接到MOVED信息后再次发送请求,并不是在redis节点中完成请求转发。

注意:如果键内容包含{}大括号字符,则计算槽的有效部分是括号内的内容;否则采用键的全内容计算槽。这提供了不同的键具备相同slot的功能,常用于Redis IO优化,如使用mget等命令优化批量调用。

1
2
3
4
127.0.0.1:6379> cluster keyslot key:{hash_tag}:111
(integer) 2515
127.0.0.1:6379> cluster keyslot key:{hash_tag}:222
(integer) 2515

Smart客户端

Dummy(傀儡)客户端:根据MOVED重定向机制,客户端随机连接集群内任一Redis获取键所在节点。它优点是代码实现简单,对客户端协议影响较小。弊端很明显,每次执行键命令额外增加了IO开销,这不是Redis集群高效的使用方式。

Smart客户端:通过在内部维护slot→node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot→node映射。

以Jedis为例:

初始化:JedisCluster 会选择一个运行节点,使用cluster slots初始化槽和节点映射关系,缓存结果,并为每个节点创建唯一的JedisPool连接池。

执行键命令:

CATALOG
  1. 1. 1. 数据分布
    1. 1.1. 节点取余分区
    2. 1.2. 一致性哈希分区
    3. 1.3. 虚拟槽分区
      1. 1.3.0.1. Redis数据分布
  • 2. 2. 搭建集群
  • 3. 3. 节点通信
    1. 3.1. Gossip
      1. 3.1.1. 节点选择
  • 4. 4. 集群伸缩
    1. 4.1. 扩容集群
      1. 4.1.1. A. 准备新节点
      2. 4.1.2. B. 加入集群
      3. 4.1.3. C. 迁移槽和数据
        1. 4.1.3.1. 迁移槽
        2. 4.1.3.2. 迁移数据
        3. 4.1.3.3. 添加从节点
  • 5. 5. 请求路由
    1. 5.1. Smart客户端