问题
虽然主从复制和哨兵模式完美的解决了Redis的单机问题,但是Redis仍然存在着以下两个问题:
- 所有的写操作都集中到主服务器上,主服务器CPU压力比较大
- 不管是主服务器还是从服务器,它们都同样保存了redis的所有数据,随着数据越来越多,可能会出现内存不够用的问题
解题思路
在redis集群中,key只能保存在按照某种规律计算得到的节点上,对该key的读取和更新也只能在该节点进行。比如redis集群一共有6个节点,现在我想执行 set name hello
,这个key为name,常见的某种规律有哈希取余"name".hashcode() % 6 + 1
得到节点的位置为4,所以就放在第四个的位置上,以后不管我是读取还是更新还是删除,我都到第四个节点上。如此一来,便完美解决了上述两个问题。
Redis 分区方案(在哪里按照某种规律计算)
1. 客户端分区方案
指在客户端计算key得到将要保存的节点,然后客户端再连接该节点端口,进行数据操作。这种方案比较简单,但是一旦节点数发生变化,将要更新新的计算算法(比如取余这个6改成10)到所有客户端上,会比较麻烦。
2. 代理分区方案
指在客户端和服务器之间加了一层代理层,客户端的命令先到代理层,代理层进行计算,再分配到它对应的节点上;这种方法挺好的,节点数发生变化,只需要修改代理层的计算算法即可,但是需要多一层转发,需要一定的耗时。
3. 查询路由方案
节点之间早就约定好哪些key是属于自己,哪些key是属于其它节点;客户端最开始随机把命令发给某个节点,节点计算并查看这个key是否属于自己的,如果是自己的就进行处理,并把结果发回去;如果是其它节点的,就会把那个节点的信息(ip + 地址)转发给客户端,让客户端重定向,这么一说感觉是有点像http协议中的3XX状态码。今天的主角Redis Cluster
就是基于查询路由方案。
数据分区(某种规律有哪些)
数据分区一遍有两种,哈希分区和顺序分区;哈希分区顾名思义,就是对key进行哈希计算然后分区;而顺序分区则是对按顺序对key进行分区。因为Redis cluster采用的是哈希分区,所以这里只讨论哈希分区。哈希分区也有很多规则,如下:
1. 节点取余分区
对key进行hash计算,然后用节点的个数去取余得到应该在哪个节点hash(key) % N
。这种分区方法比较方便。就是当节点数变化的时候,几乎所有的key都需要重新分配。
2. 一致性哈希分区
3. 虚拟槽分区
在Redis Cluster
中,约定了有16383个槽,我们对key进行CRC16(key) & 16383
计算后会得到这个key属于哪个槽,这16383个槽在集群创建之初,会自动或者手动的分配到不同的节点中,即key -> slot -> node
。添加或者删除新的节点的时候,只需要对对应的槽进行重新分配即可。
redis cluster 的大概流程
集群创建之初,我们可以自动或者手动给每个节点分配槽位。每个节点通过Gossip协议,会和其它节点交换槽信息,得到并且保存槽与节点的全局对应关系图。于是节点收到客户端发来的命令以后,对key进行CRC16(key) & 16383
的计算得到槽位,对比这个槽位是不是属于自己的,如果是自己的就进行处理,并把结果发回去;如果是其它节点的,就会把那个节点的信息(ip + 地址)转发给客户端,然后客户端再重新请求;当然客户端也不会那么傻,每次都是随机请求节点,客户端在启动的时候也会和服务器交换信息得到槽和节点的映射图,客户端请求的节点,也是客户端自己计算CRC16(key) & 16383
得到槽位,再对比关系图而得到的节点,如果节点发生变化了(即收到请求重定向),它也会更新这个关系图。
创建集群
- 准备节点,一个高可用的redis集群至少要有6个节点
# 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
如下:
8ba45af25feef061507831ca1b3ddf71a7574631 :0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
其中8ba45af25feef061507831ca1b3ddf71a7574631是6379redis的节点ID,这里我们只要知道它很重要就可以了。
- 节点握手,打开客户端进入6379,然后依次运行
cluster meet 139.199.168.61 6380
到cluster meet 139.199.168.61 6384
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
来查看集群中节点间的关系。
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
来查看集群状态:
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个槽分出去,集群才能正常工作,分配槽的命令如下:
/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}
PS:注意0和5641之间隔的是两个点,因为看书上写的是三个点,会报(error) ERR Invalid or out of range slot
的错误。
这样子就把所有的槽都分出去了,但是只用到了三个节点,剩下三个节点我们可以作为从节点,可以使用cluster replicate 主节点id
来把某个节点挂为某个节点的从节点。
139.199.168.61:6382> cluster replicate 8ba45af25feef061507831ca1b3ddf71a7574631
139.199.168.61:6383> cluster replicate a08f700001a5902dd82b51eb74b4ec8028202d75
139.199.168.61:6384> cluster replicate 0573105a355722bc6dd5ab29dea072ce1a6956df
最后我们来看一下节点状态:
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
再来查看一下节点关系:
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信息。
槽位信息在哪里?
typedef struct {
char sig[4]; /* 信号标示 */
uint32_t totlen; /* 消息总长度 */
uint16_t ver; /* 协议版本*/
uint16_t type; /* 消息类型,用于区分meet,ping,pong等消息 */
uint16_t count; /* 消息体包含的节点数量,仅用于meet,ping,ping消息类型*/
uint64_t currentEpoch; /* 当前发送节点的配置纪元 */
uint64_t configEpoch; /* 主节点/从节点的主节点配置纪元 */
uint64_t offset; /* 复制偏移量 */
char sender[CLUSTER_NAMELEN]; /* 发送节点的nodeId */
unsigned char myslots[CLUSTER_SLOTS/8]; /* 发送节点负责的槽信息 */
char slaveof[CLUSTER_NAMELEN]; /* 如果发送节点是从节点,记录对应主节点的nodeId */
uint16_t port; /* 端口号 */
uint16_t flags; /* 发送节点标识,区分主从角色,是否下线等 */
unsigned char state; /* 发送节点所处的集群状态 */
unsigned char mflags[3]; /* 消息标识 */
union clusterMsgData data /* 消息正文 */;
} clusterMsg;
我们来看一下消息的格式,这里面有个myslots的char数组,长度为16383/8,这其实是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的。节点计算出某个key的槽位以后,只需要对比一下这个bitmap的第几个位是否是1,如果是1则它可以处理这个key,如果不是则查找一下其他节点的myslots,直到找到匹配的节点,然后把节点信息返回给客户端。
redis cluster 的伸缩
redis cluster 的伸缩实际就是槽在各个节点之间的转移。
smart客户端
redis-cli
现在来做一个实例,打开redis-cli,连接6379,如果处理一个不属于这个节点的key:
139.199.168.61:6379> set name 11
(error) MOVED 5798 139.199.168.61:6380
可以看到节点6379返回一个重定向指令,name
这个key的槽为5798,这个槽在139.199.168.61:6380
这台服务器上。我们再去6380试试,可以看到可以正常处理。
139.199.168.61:6380> set name 11
OK
如果你想客户端自己帮我们重定向,可以在启动客户端的时候 加上 -c
:
[root@VM_90_159_centos redis-3.2.6]# /usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6379 -c
139.199.168.61:6379> set name 14
-> Redirected to slot [5798] located at 139.199.168.61:6380
OK
JedisCluster
先上一份Java JedisCluster 的代码:
Set<HostAndPort> jedisClusterNode = new HashSet<>();
//添加节点信息
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6379));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6380));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6381));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6382));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6383));
jedisClusterNode.add(new HostAndPort("139.199.168.61", 6384));
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
//初始化JedisCluster
JedisCluster jedisCluster = new JedisCluster(jedisClusterNode, 1000, 1000, 5, poolConfig);
logger.debug("name = {}", jedisCluster.get("name"));
jedisCluster.close();
前面我们说过,客户端也会保存一份槽与节点的映射关系图,当执行某个命令的时候,也会计算CRC16(key) & 16383
得到槽的位置,然后从映射关系中找到对应的节点信息,再向节点发送请求,如果节点信息返回的是moved指令,它会重新更新映射关系。那么这份映射关系保存在哪里呢?