redis 系列(四)- Redis Cluster

问题

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

  1. 所有的写操作都集中到主服务器上,主服务器CPU压力比较大
  2. 不管是主服务器还是从服务器,它们都同样保存了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得到槽位,再对比关系图而得到的节点,如果节点发生变化了(即收到请求重定向),它也会更新这个关系图。

创建集群

  1. 准备节点,一个高可用的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,这里我们只要知道它很重要就可以了。

  1. 节点握手,打开客户端进入6379,然后依次运行cluster meet 139.199.168.61 6380cluster 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
  1. 分配槽
    以上只是建立了一个集群,但是其实集群还不能工作,可以用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指令,它会重新更新映射关系。那么这份映射关系保存在哪里呢?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342