一、定义
由前面几节可知,虽然redis通过某些方式实现了持久化、主从以及sentinel等功能,但仍存在单机照成的存储限制(大数据时无法承受)以及无法实现写操作的负载均衡(无法支撑高并发量)。所以必须通过集群部署(类似添加很多个机器,Redis 3.0开始引入的分布式存储方案)的方式将redis的数据按照一定的规则分配到多台机器,另外cluster可以实现主从和master重选功能,当然如果数据量不是很大使用sentinel即可。其中对数据的分配是指将key按照一定的规则进行计算,获取到key对应的redis分配规则节点,并将key对应的value分配到该redis实例上。集群由多个redis节点(node)组成,所有数据都分布在这些节点中。集群中的节点分为主节点和从节点,其中只有主节点负责读写请求和集群信息的维护,而从节点只进行主节点数据和状态信息的复制。
二、分区规则(数据分区/分片是集群最核心的功能)
集群将数据分散到多个节点,一方面突破了Redis单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。因此对于集群而言,如何分区就成了一个很重要的点。
1、数据分区是指在分布式数据库中,将数据按照分区规则分到不同的库中。其中主要分为顺序分区和哈希分区。
2、哈希分区又分三种:哈希取余分区、一致性哈希分区、虚拟槽分区
(1)哈希取余分区(hash(key)%nodes)
原理:对key进行hash运算,然后根据节点数量进行取余,计算出数据落在哪个节点。
存在问题:节点数量改变时会引起巨大的数据迁移。当添加一个节点的时候会发现有80%的数据会发生迁移,这样就容易丢失数据,所以做缓存的时候添加节点,有大部分数据将不再redis而需要去数据库查询再做缓存。(这种模式只能做缓存,做存储则在加节点时会有数据丢失,如key为5,3节点时,数据存在第三个节点,加一个节点,则下次查询的时候会去第二个节点查找,没办法找到)
优化(翻倍扩容):扩容时以倍数扩容,如3个添加到6个,则只有50%迁移。
(2)一致性哈希分区
原理:将哈希值空间组成一个0~2^32-1的圆环,按节点数在环上均分位置安放节点,数据key做hash运算后会分到某两个节点之间,再按顺时针行走找到最近的一个节点,即该数据置于该节点。
优点:当加入一个新节点,如加入新节点node4到node1,node2这两个节点之间,则只会有node2的部分数据需要迁移到新节点node4上,其他节点数据不动(删除节点类似),迁移量大大降低。虽然还是有数据迁移,但只影响邻近节点,保证最小迁移数据,但也一样只适合做缓存。
缺点:数据的存放不平衡,在增删节点时很可能会出现某些节点存放数据远大于其他节点。
(3)虚拟槽分区
原理:在集群中虚拟出16384个槽(slot),槽做为数据和节点之间沟通的媒介,每个节点会包含有一定数量(不重复)的槽,如三个节点(均分),则第一个节点分配0-5461的槽,第二个节点分配5462-10922的槽,第三个10923-16383的槽。数据使用了CRC16算法对key进行hash,再对16383进行取余运算,得到数据应该存放在哪个槽,再由槽得知在哪个节点。此时槽是数据管理和迁移的基本单位,redis集群就是用了该分区方式。
优点:在伸缩节点时数据迁移较少,且伸缩后数据在节点的分布还是较均衡的。如上面三个节点加多一个节点,则只要将原有节点分别拿出16384均分为4份之后多出的槽迁移到新节点即可,这时候只有这部分数据需要迁移。当由四个节点删一个节点,则只需将删除节点上的槽均分三份分别迁移到还在线的节点即可。
三、redis的集群搭建
集群搭建的方式有两种:使用原生redis命令搭建、使用官方工具ruby脚本搭建
下面以一个三主三从的集群做例子,其中三个主节点分别为 192.168.1.1 7000、192.168.1.2 7000、192.168.1.3 7000,三个从节点为 192.168.1.1 7001、192.168.1.2 7001、192.168.1.3 7001
1、 使用原生命令搭建
使用命令行搭建集群需要执行如下四个步骤:
配置节点,修改redis的配置文件启动使节点开启集群模式
对各节点进行meet操作(握手),使集群中的所有节点能够互相感知。
指派槽,将16384个槽分配给对应的主节点。
配置节点主从关系,从而实现高可用。
(1)配置文件(主要开启集群模式),这里以 192.168.1.1 7000 节点的配置为例,其他节点类似
port 7000
daemonize yes
dir /zhu/redis/data/
dbfilename "dump-7000.rdb"
logfile "7000.log"
cluster-enabled yes 开启集群模式
cluster-config-file "node-7000.conf" 集群信息文件名称,该文件含集群的节点信息
cluster-require-full-coverage no 该节点不可用,该集群仍可对外提供服务
cluster-node-timeout 15000 节点在15秒请求没返回则认为不可用
开启redis服务同之前一样: redis-server redis-7000.conf
然后通过如下命令查看进程,可以看到服务多了个cluster : ps -ef |grep redis
此时进入到该redis,进行set等操作会发现不可用,这是因为开启了集群必须在分配槽完成之后才可用
可通过命令查看该集群中的节点情况: redis-cli -p 7000 cluster nodes(info查看集群节点数等)
(2)meet操作(这里以192.168.1.1 7000的操作为例,即在该redis上进行如下操作,只需在一台redis进行该操作,其他redis就都能够互相获取对方信息)
redis-cli -p 7000 cluster meet 192.168.1.1 7001
redis-cli -p 7000 cluster meet 192.168.1.2 7000
redis-cli -p 7000 cluster meet 192.168.1.2 7001
redis-cli -p 7000 cluster meet 192.168.1.3 7000
redis-cli -p 7000 cluster meet 192.168.1.3 7001
此时执行 redis-cli -p 7000 cluster nodes 可以看到所有集群节点,包括节点的id
也可在对应路径的 /zhu/redis/data/nodes-7000.conf 看到自动生成的集群节点信息
(3)分配槽(共有19384个槽,因为只有三个主节点,所以均分为3份),只有当数据库的所有槽都分配了节点集群才处于可用状态,如果有一个没分配,则处于下线。
redis-cli -h 192.168.1.1 -p 7000 cluster addslots {0..5461}
redis-cli -h 192.168.1.2 -p 7000 cluster addslots {5462..10922}
redis-cli -h 192.168.1.3 -p 7000 cluster addslots {10923..16383}
(4)设置主从关系
redis-cli -h 192.168.1.1 -p 7001 cluster replicate xxxx //使 192.168.1.1 7001成为 192.168.1.1 7000的从节点,其中xxxx为对应主节点在集群中的id,可通过上面所讲命令获取
redis-cli -h 192.168.1.2 -p 7001 cluster replicate xxxx
redis-cli -h 192.168.1.3 -p 7001 cluster replicate xxxx
通过 redis-cli -p 7000 cluster slots 可以看到槽的分配情况,并且主从两个节点都分配在一个槽区间。至此,手动搭建集群完成。
2、使用官方工具ruby脚本搭建(推荐)
主要的搭建集群ruby脚本为redis-trib.rb,该文件在redis安装路径的src文件夹下。
(1)、安装ruby,centos(wget https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz)和ubuntu(apt-get install ruby)安装方式有些不同,自行百度。
(2)、安装rubygem redis(客户端),centos(wget http://rubygems.org/downloads/redis-3.3.0.gem)和ubuntu(
gem install redis)
(3)
、同上面一样启动节点,设置为集群模式
(4)
、通过执行 redis-trib.rb 脚本并添加一些参数即可搭建集群,命令如下:
./redis-trib.rb create --replicas 1 192.168.1.1:7000 192.168.1.2:7000 192.168.1.3:7000 192.168.1.1:7001 192.168.1.2:7001 192.168.1.3:7001
其中,create表示需要创建集群;(--replicas 1) 表示每个主节点都分配一个从节点;前三个ip、端口对应的节点做主节点,后面三个做从节点。另外需要注意,加入的节点不能在其他集群,脚本会对这些节点做判断,若已加入了集群的节点将无法加入。
四、集群伸缩
1、定义:集群伸缩的核心就是对节点上的槽(数据)进行迁移,从而修改槽与节点的对应关系。
伸(扩容):因业务量需要等因素向集群中加入新的主节点
缩:由于机器老化等各种原因需要对集群中的部分主节点进行下线操作
2、扩容集群步骤(以三个节点扩容到四个节点为例,由于三个节点时每个节点有5462(左右)个槽,当多加入一个主节点时,则原先的每个节点需要将 5462-(16384/4) 个槽迁移到新节点上)
(1)准备新节点:配置开启集群模式,然后启动节点(新节点一样有主从,即要起两个节点)
(2)加入集群:在集群中其他节点进行meet操作,将该节点加入集群。作用:为迁移槽和数据实现扩容。注意,我们一般是使用redis-trib.rb脚本对节点进行加入集群而不用meet命令,该脚本操作时能够避免新节点已经加入了其他集群或存在数据造成故障,因为该脚本加入前会做一些验证。主要执行的命令(redis-trib.rb add-node new_host:new_port existing_host:existing_port --slave --master-id
(3)迁移槽和数据
迁移步骤:
迁移数据主要包括如下:
示例扩容操作:
三个节点扩容到四个节点,将新节点加入集群,之后迁移操作如下(使用redis-trib.rb 脚本进行迁移操作,可以直接执行该脚本 ./redis-trib.rb 不带参数,将打印该脚本的使用参数,对集群扩容需要使用参数reshard对槽进行重新分配):
./redis-trib.rb reshard 192.168.1.1:7000 (在192.168.1.1:7000的节点上进行槽的重新分配,在其他集群节点也可(ip和port可以是集群中的任一节点)),执行完命令根据提示输入迁移多少槽到新节点(4096),新节点的id,输入哪些节点的槽要迁移到该新节点(这里我们为了均衡,输入all,即所有节点都迁移出一部分槽)。此时可以通过命令查看到集群中节点的槽分配情况,原来的三个节点槽都是连续的,而新节点的槽分为三段。
3、集群收缩操作(类似扩容)
定义:先查看下线的节点是否持有槽,存在则将槽迁移到其他节点再通知其他节点忘记该下线的节点,不存在则直接通知忘记操作,最后将节点关闭。
具体流程:
五、客户端访问
客户端访问集群主要有三种: moved重定向、ask重定向、smart客户端
1、moved重定向:
客户端发送操作命令给集群中的任意节点,该节点会计算操作命令所含key对应的槽和节点位置(通过cluster keyslot haha(key值) 命令计算key对应槽),当计算出的节点为自身,则执行客户端发送的命令并返回给客户端结果。若不在本节点,则回复一个moved异常给客户端,该moved含有该key所在槽和节点的信息(类似 moved 923 192.168.1.1:7000,923表示在哪个槽,后面为槽所在节点),客户端接收到该moved后重定向给对应的目标节点发送原先的命令。当使用如下命令登陆客户端 redis-cli -h 192.168.1.1 -p 7000,登录redis执行了key不在该节点的操作命令时会抛moved异常并结束。当使用如下命令 redis-cli -c -h 192.168.1.1 -p 7000,登录redis执行了key不在该节点的操作命令时会自动重定向登录到目标机器并执行该命令。说明:通过-c指定了集群模式,如果没有指定,redis-cli无法处理MOVED错误。
2、ask重定向:
槽和数据在进行迁移过程中(如集群在进行伸缩),此时客户端发送了命令给原先放该命令所含key的节点,但槽已经迁移到了新节点,只是还未通知其他节点槽进行了迁移,导致客户端请求失败,节点将回复ask转向给客户端。客户端再发送asking命令(先发)以及发送原来命令给获取到槽的新节点,新节点会返回执行结果。
3、smart客户端(追求性能)
定义:从集群中选一个可运行的节点,通过cluster slots命令获取到集群中槽和节点的映射关系并存放到本地,在执行命令时先根据本地的映射表找出该命令的key对应的目标节点,向目标节点发送命令并获取目标节点的返回结果。当连接目标节点出错时,则会随机向一个集群的节点发送该命令,如果该节点返回了moved异常给客户端,客户端就会重新刷新本地的映射表,然后向真正的目标节点发起请求。如果命令发送次数超过五次还未成功获取到执行结果,则抛出Too many cluster redirection 异常并停止执行。
总结:使用smart操作集群达到通信效率最大化,通过映射表快速定位到目标节点。只有在发生moved异常或者访问的节点4次不可达才会重新刷新内存中的槽、节点映射关系表。ASK错误说明数据正在迁移,不知道何时迁移完成,因此重定向是临时的,SMART客户端不会刷新本地映射表;MOVED错误重定向则是永久的,SMART客户端会刷新本地映射表。
smart客户端使用JedisCluster:
JedisPoolConfig poolConfig = new JedisPoolConfig(); //可进行相关设置
Set nodeList=new HashSet<>(HostAndPort); //将节点信息设置到nodeList 集合中
nodeList.add(new HostAndPort(host1,port1));
nodeList.add(new HostAndPort(host2,port2));
nodeList.add(new HostAndPort(host3,port3));
nodeList.add(new HostAndPort(host4,port4));
JedisCluster jedisCluster = new JedisCluster(nodeList,timeout,poolConfig); //timeout为超时时间毫秒,然后通过redis即可操作集群
jedisCluster.command... //get,set操作
获取所有节点的连接池(多节点获取):
Map jedisPoolMap = jedisCluster.getClusterNodes();
for(Entry entry:jedisPoolMap.entrySet()){
Jedis jedis = entry.getValue().getResource(); //获取每个节点的连接
}
六、集群故障发现和转移
1、定义:集群本身有故障发现机制(不依赖sentinel),集群中的各节点通过定时任务发送ping/pong消息来实现故障的发现。集群节点的故障发现同样存在主观下线和客观下线两个定义。其中主观下线指某个节点主观认为另一个节点存在故障不可用;而客观下线指当半数以上的持有槽主节点都标记某个节点主观下线,则对其进行客观下线,客观下线后将选取从节点进行故障转移。
2、主观下线流程示例:
3、客观下线流程:
当某节点接收到其他节点(如节点1)发送来的ping消息时,该消息带了他(节点1)对其他节点的看法(包含pfail),节点会将其维护在一个故障表中(该表有效期为2*cluster_node_timeout),当有一个节点被其他节点认为主观下线并把消息发给该节点时,该节点会根据故障表对主观下线的节点尝试进行客观下线。
尝试客观下线流程:节点主动对另一个节点(node1)尝试客观下线,会统计其他主节点对该节点(node1)的看法,当有超过一半的带槽主节点认为该节点(node1)下线,则将该节点(node1)设置为客观下线,并向集群广播下线节点的fail消息(通知集群内所有节点标记故障节点为客观下线、通知故障节点的从节点触发故障转移流程),否则退出操作。
4、故障恢复流程( 从节点故障时只会被下线,不会进行故障转移)
七、其他
1、高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
2、批量操作的实现:一条 mget/mset 批量操作命令的所有key需要在同一个槽才能执行成功
可以通过如下四种方法对批量操作进行优化: 串行mget、串行IO、并行IO、hash_tag
3、 hash_tag
原理:当一个key包含 {} 的时候,不对整个key做hash,而仅对 {} 包括的字符串做hash,如 {ab}:1,{ab}:2 这两个key都只会对ab进行hash,即这两个key的槽是一样的,执行命令会放在同一个槽中。
意义:hash_tag可以让不同的key拥有相同的hash值,即分配在同一个槽里;这样就能够使用批量操作命令 mget/mset 了。但其会导致数据倾斜(分配不均)
优化:(1)合理调整各节点中槽的数量,使数据尽量能均匀分布到各节点;(2)不要对热点数据(经常访问的)使用hash_tag,导致请求倾斜(很多请求访问到该节点)。
4、集群中每个节点都会对外暴露两个tcp端口:一个是开启redis服务使用的端口,如(6379),另一个是集群端口,主要用户节点之间的通信,该端口固定为服务端口+10000,如(16379)。
5、故障转移时间:指主节点发生故障直到从节点转移这个过程需要的时间,时间主要耗费在其他节点识别故障节点主观下线、将主观下线信息发送给其他节点以及从节点选举延迟。该时间与cluster-node-timeout 的大小有关,一般来说:故障转移时间(毫秒) ≤ 1.5 * cluster-node-timeout + 1000,而cluster-node-timeout默认为15s。
6、集群的带宽消耗:主要发生在节点间的ping/pong消息
具体消耗与如下三方面有关:
7、配置说明:
(1)cluster_node_timeout(默认值是15s),主要作用:
(2)cluster-require-full-coverage(默认yes,集群完整性判断,即是否全部槽分配完集群才可用)
该配置主要用于保证集群的完整性,如果设置为yes则只有所有槽分配完集群才能上线使用,这样会导致集群在发生故障转移集群不可用。如果设置为no,则当槽没有完全分配时,集群仍可以上线。生产环境建议设置为no。
8、数据发生倾斜(分配不均)的主要原因:节点和槽分配不均、不同槽对应键值数量差异较大、包含bigkey、内存相关配置不一致。
请求发生倾斜(某些节点请求量时间远大于其他节点)的主要原因:热点key(频繁访问)、bigkey(请求时数据量大导致请求时间长)
9、我们都知道单个redis可以使用16个数据库,但是集群模式下只支持一个,即db0。
10、集群中的节点之间通信主要由五种消息:meet、ping、pong、fail、publish