redis学习(伍) -- cluster(集群)

一、定义

由前面几节可知,虽然redis通过某些方式实现了持久化、主从以及sentinel等功能,但仍存在单机照成的存储限制(大数据时无法承受)以及无法实现写操作的负载均衡(无法支撑高并发量)。所以必须通过集群部署(类似添加很多个机器,Redis 3.0开始引入的分布式存储方案)的方式将redis的数据按照一定的规则分配到多台机器,另外cluster可以实现主从和master重选功能,当然如果数据量不是很大使用sentinel即可。其中对数据的分配是指将key按照一定的规则进行计算,获取到key对应的redis分配规则节点,并将key对应的value分配到该redis实例上。集群由多个redis节点(node)组成,所有数据都分布在这些节点中。集群中的节点分为主节点和从节点,其中只有主节点负责读写请求和集群信息的维护,而从节点只进行主节点数据和状态信息的复制。

二、分区规则(数据分区/分片是集群最核心的功能)

集群将数据分散到多个节点,一方面突破了Redis单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。因此对于集群而言,如何分区就成了一个很重要的点。

1、数据分区是指在分布式数据库中,将数据按照分区规则分到不同的库中。其中主要分为顺序分区和哈希分区。

  • 顺序分区:将数据按一定顺序分区,如有三个节点,key为1到30的30个数据,则1到10分到第一个节点,11到20分第二个,以此类推。
  • 哈希分区:对数据的key先进行hash计算,再进行其他操作(根据不同方式操作不同,如取余等)来决定置于哪个节点。
  • 两种分区比较:哈希分区的数据分散度高(随机性强)但无法顺序访问,顺序分区的数据分散度易倾斜但可顺序访问。衡量数据分区方法好坏主要看数据分布是否均匀、增加或删减节点对数据分布的影响,集群的分区使用了哈希分区中的一种。

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、 使用原生命令搭建

使用命令行搭建集群需要执行如下四个步骤:

  1. 配置节点,修改redis的配置文件启动使节点开启集群模式
  2. 对各节点进行meet操作(握手),使集群中的所有节点能够互相感知。
  3. 指派槽,将16384个槽分配给对应的主节点。
  4. 配置节点主从关系,从而实现高可用。

(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 ),在这里执行命令如下:redis-trib.rb add-node 192.168.1.4:7000 192.168.1.1:7000

(3)迁移槽和数据
迁移步骤:

  1. 制定槽迁移计划:三个节点变四个节点时,16384个槽由原本的三份改为四份,即将其他三个节点多出来的槽迁移到新节点。
  2. 迁移数据
  3. 添加从节点(可在主节点加入集群就添加)

迁移数据主要包括如下:

  1. 客户端对目标节点发送:cluster setslot {slot} importing {sourceNodeId} 命令,让目标节点准备导入槽{slot}的数据。
  2. 客户端对源节点发送:cluster setslot {slot} migrating {targetNodeId} 命令,让源节点准备迁出槽{slot}的数据。
  3. 源节点循环执行 cluster getkeysinslot {slot} {count} 命令,每次从源节点获取count个属于槽{slot}的健(slot表示对应哪一个槽)。
  4. 在源节点上执行 migrate {targetIp} {targetPort} key 0 {timeout} 命令把制定key迁移(0表示在redis的第一个库)。
  5. 重复执行步骤3~4直到槽下所有的健数据迁移到目标节点。
  6. 向集群内所有主节点发送 cluster setslot {slot} node {targetNodeId} 命令,通知槽{slot}分配给目标节点。

示例扩容操作:

三个节点扩容到四个节点,将新节点加入集群,之后迁移操作如下(使用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、集群收缩操作(类似扩容)
定义:先查看下线的节点是否持有槽,存在则将槽迁移到其他节点再通知其他节点忘记该下线的节点,不存在则直接通知忘记操作,最后将节点关闭。

具体流程:

  1. 下线迁移槽:将要下线的节点上的槽均衡迁移到各个在线的节点,使用 redis-trib.rb 的reshard 来迁移槽。  ./redis-trib.rb reshard --from xxxxx(下线节点id) --to xxxx(将槽迁移到的节点id) --slots 1366(迁移的槽数量) 192.168.1.4:7000(集群中一个节点,表示在该节点执行命令)  : 表示将下线节点中的1366(一部分)个槽迁移到集群中其中一个节点, 其他槽迁移到另外两个节点的命令同上。
  2. 忘记节点:通知其他节点忘记下线节点命令:cluster forget xxxx(下线节点id),该操作需要对所有节点一一进行。
    这里使用 redis-trib.rb 的del-node 来忘记槽,./redis-trib.rb del-node 192.168.1.1:7000 xxxxx(下线从节点id) ,忘记主节点同上。注意应先下线从节点再下线主节点,因为若主节点先下线,从节点会被指向其他主节点,造成不必要的全量复制。
  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、主观下线流程示例:

  1. 节点1发送ping消息给节点2
  2. 当成功发送给节点2,则节点2会回复一个pong消息给节点1,节点1更新与节点2的最后通信时间
  3. 当消息发送失败时,通信连接断开,节点1与节点2的最后通信时间不变
  4. 节点1有个定时任务,定时查看自己与节点2的最后通信时间是否超过cluster_node_timeout,超过则认为节点2不可用并对其进行主观下线,标记为pfail。

3、客观下线流程:
当某节点接收到其他节点(如节点1)发送来的ping消息时,该消息带了他(节点1)对其他节点的看法(包含pfail),节点会将其维护在一个故障表中(该表有效期为2*cluster_node_timeout),当有一个节点被其他节点认为主观下线并把消息发给该节点时,该节点会根据故障表对主观下线的节点尝试进行客观下线。
尝试客观下线流程:节点主动对另一个节点(node1)尝试客观下线,会统计其他主节点对该节点(node1)的看法,当有超过一半的带槽主节点认为该节点(node1)下线,则将该节点(node1)设置为客观下线,并向集群广播下线节点的fail消息(通知集群内所有节点标记故障节点为客观下线、通知故障节点的从节点触发故障转移流程),否则退出操作。

4、故障恢复流程( 从节点故障时只会被下线,不会进行故障转移)

  1. 资格检查:每个从节点检查与故障主节点的断线时间,当超过cluster-node-timeout(默认15000ms)*cluster-slave-validity-factor(默认10s)长度则取消其选举资格。
  2. 准备选举时间:查看每个从节点的偏移量,按偏移量顺序由大到小向集群其他主节点发起选举。如从节点1偏移量为1000,从节点2偏移量为999,则若从节点1在1秒后向集群其他节点发起选举,则从节点2将会2秒后再向集群发起选举(比从节点1慢发起),因为偏移量越大说明数据越完整,即越希望其被选举为主节点,而集群每个主节点只能为从节点投一票,即越早发起选举越容易被选中。
  3. 选举投票:当集群有超过半数(n/2+1,包括故障主节点)的主节点投票给某从节点,则该从节点可替换主节点。
  4. 替换主节点:当前从节点取消复制变为主节点(slave no one)--> 执行cluster del slot撤销故障主节点负责的槽,并执行cluster add slot 把这些槽分配给从节点 --> 向集群广播主机的pong消息,表明已经替换了故障从节点

七、其他

1、高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。

2、批量操作的实现:一条 mget/mset 批量操作命令的所有key需要在同一个槽才能执行成功
可以通过如下四种方法对批量操作进行优化: 串行mget、串行IO、并行IO、hash_tag

  1. 串行mget:将mget命令在客户端进行处理,分成一个一个key命令发到redis集群处理。
  2. 串行IO:客户端将mget命令中的key进行计算,将同个节点上的key放在一起,然后将同个节点的key以一条命令的形式发送给节点,一次发送一批同节点key给集群。
  3. 并行IO:基本同串行IO,只是会并行将所有批次的key发送给集群。
  4. 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消息
具体消耗与如下三方面有关:

  1. 消息发送频率:节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息
  2. 消息数据量:slots槽数组和整个集群1/10的状态数据(10个节点状态数据约1kb)
  3. 节点部署的机器规模:集群发布的机器越多且每台机器划分的节点数越均匀,则集群内整体的可用带宽越高。

7、配置说明:

(1)cluster_node_timeout(默认值是15s),主要作用:

  1. 影响PING消息接收节点的选择:值越大对延迟容忍度越高,选择的接收节点越少,可以降低带宽,但会降低收敛速度;应根据带宽情况和应用要求进行调整。
  2. 影响故障转移的判定和时间:值越大,越不容易误判,但完成转移消耗时间越长;应根据网络状况和应用要求进行调整。

(2)cluster-require-full-coverage(默认yes,集群完整性判断,即是否全部槽分配完集群才可用)
该配置主要用于保证集群的完整性,如果设置为yes则只有所有槽分配完集群才能上线使用,这样会导致集群在发生故障转移集群不可用。如果设置为no,则当槽没有完全分配时,集群仍可以上线。生产环境建议设置为no。

8、数据发生倾斜(分配不均)的主要原因:节点和槽分配不均、不同槽对应键值数量差异较大、包含bigkey、内存相关配置不一致。
请求发生倾斜(某些节点请求量时间远大于其他节点)的主要原因:热点key(频繁访问)、bigkey(请求时数据量大导致请求时间长)

9、我们都知道单个redis可以使用16个数据库,但是集群模式下只支持一个,即db0。

10、集群中的节点之间通信主要由五种消息:meet、ping、pong、fail、publish

  • MEET消息:在新节点通过CLUSTER MEET命令加入集群的时候, 集群中对应节点会向新加入的节点发送MEET消息,请求新节点加入到当前集群;新节点收到MEET消息后会回复一个PONG消息。
  • PING消息:集群中的节点会定时向其他节点发送PING消息用于信息交流,该消息主要含对其他节点的看法以及自身信息,接收者收到消息后会回复一个PONG消息。接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:(1)随机找5个节点,在其中选择最久没有通信的1个节点(2)扫描节点列表,选择最近一次收到PONG消息时间大于cluster_node_timeout/2的所有节点,防止这些节点长时间未更新。
  • PONG消息:PONG消息封装了自身状态数据。可以分为两种:第一种是在接到MEET/PING消息后回复的PONG消息;第二种是指节点向集群广播PONG消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播PONG消息。
  • FAIL消息:当一个主节点判断另一个主节点进入FAIL状态时,会向集群广播这一FAIL消息;接收节点会将这一FAIL消息保存起来,便于后续的判断。
  • PUBLISH消息:节点收到PUBLISH命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该PUBLISH命令。

你可能感兴趣的:(redis)