Redis(十)集群:Redis Cluster

  一、数据分布

  1.数据分布理论

  2.Redis数据分区

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

  

  Redis虚拟槽分区的特点:

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

  3.集群功能限制

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

  二、搭建集群

  1.准备节点

  (1)节点配置文件(以其中一个为例):

// 节点端口
port 6381
// 开启守护进程
daemonize yes
// 日志文件
logfile "6381.log"
// 开启集群模式
cluster-enabled yes
// 节点超时时间,单位毫秒 cluster
-node-timeout 15000
// 集群内部配置文件
cluster-config-file "node-6381.conf"
// 持久化RDB文件
dbfilename "dump-6381.rdb"
// 文件生成路径
dir "/home/redis/data/node/"

  (2)启动节点:

root@myubuntu:/home/redis/conf# redis-server redis-6379.conf 
root@myubuntu:/home/redis/conf# redis-server redis-6380.conf 
root@myubuntu:/home/redis/conf# redis-server redis-6381.conf 
root@myubuntu:/home/redis/conf# redis-server redis-6382.conf 
root@myubuntu:/home/redis/conf# redis-server redis-6383.conf 
root@myubuntu:/home/redis/conf# redis-server redis-6384.conf 

  (3)查看日志及生成的配置文件:

root@myubuntu:/home/redis/data/node# ll
总用量 60
drwxr-xr-x 2 root root 4096 6月   6 17:49 ./
drwxr-xr-x 3 root root 4096 6月   6 17:37 ../
-rw-r--r-- 1 root root 4753 6月   6 17:49 6379.log
-rw-r--r-- 1 root root 1457 6月   6 17:49 6380.log
-rw-r--r-- 1 root root 1457 6月   6 17:49 6381.log
-rw-r--r-- 1 root root 1457 6月   6 17:49 6382.log
-rw-r--r-- 1 root root 1457 6月   6 17:49 6383.log
-rw-r--r-- 1 root root 1457 6月   6 17:49 6384.log
-rw-r--r-- 1 root root  114 6月   6 17:44 node-6379.conf
-rw-r--r-- 1 root root  114 6月   6 17:49 node-6380.conf
-rw-r--r-- 1 root root  114 6月   6 17:49 node-6381.conf
-rw-r--r-- 1 root root  114 6月   6 17:49 node-6382.conf
-rw-r--r-- 1 root root  114 6月   6 17:49 node-6383.conf
-rw-r--r-- 1 root root  114 6月   6 17:49 node-6384.conf

root@myubuntu:/home/redis/data/node# cat 6384.log 
4321:C 06 Jun 17:49:36.988 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
4321:C 06 Jun 17:49:36.989 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=4321, just started
4321:C 06 Jun 17:49:36.989 # Configuration loaded
4322:M 06 Jun 17:49:36.990 * Increased maximum number of open files to 10032 (it was originally set to 1024).
4322:M 06 Jun 17:49:36.990 * No cluster configuration found, I'm 1782fe58177626fc7bbf991d2c34b72b9a8baae2
4322:M 06 Jun 17:49:37.025 * Running mode=cluster, port=6384.
4322:M 06 Jun 17:49:37.025 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
4322:M 06 Jun 17:49:37.025 # Server initialized
4322:M 06 Jun 17:49:37.025 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
4322:M 06 Jun 17:49:37.025 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
4322:M 06 Jun 17:49:37.026 * Ready to accept connections

root@myubuntu:/home/redis/data/node# cat node-6384.conf 
1782fe58177626fc7bbf991d2c34b72b9a8baae2 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0

  (4)节点启动过程:

  

  (5)集群内部配置文件:

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

root@myubuntu:/home/redis/data/node# cat node-6384.conf 
1782fe58177626fc7bbf991d2c34b72b9a8baae2 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0

  文件内容记录了集群初始状态,这里最重要的是节点ID,它是一个40位16进制字符串,用于唯一标识集群内一个节点,之后很多集群操作都要借助于节点ID来完成。需要注意是,节点ID不同于运行ID。节点ID在集群初始化时只创建一次,节点重启时会加载集群配置文件进行重用,而Redis的运行ID每次重启都会变化。

  2.手动搭建集群

  (1)节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程。

127.0.0.1:6379> cluster meet 127.0.0.1 6380
OK

  节点握手的过程:

  • 节点6379本地创建6380节点信息对象,并发送meet消息。
  • 节点6380接受到meet消息后,保存6379节点信息并回复pong消息。
  • 之后节点6379和6380彼此定期通过ping/pong消息进行正常的节点通信。

  这里的meet、ping、pong消息是Gossip协议通信的载体,它的主要作用是节点彼此交换状态数据信息。

  (2)查看6379和6380节点是否感知到对方的存在:

127.0.0.1:6379> cluster nodes
895e2f6dd1b0d22912e9ecaed1e66df185ccb35c 127.0.0.1:6379@16379 myself,master - 0 0 1 connected
c3efbba60fd1b860f11313236a84e72945709a76 127.0.0.1:6380@16380 master - 0 1528279299745 0 connected

127.0.0.1:6380> cluster nodes
c3efbba60fd1b860f11313236a84e72945709a76 127.0.0.1:6380@16380 myself,master - 0 0 0 connected
895e2f6dd1b0d22912e9ecaed1e66df185ccb35c 127.0.0.1:6379@16379 master - 0 1528279324291 1 connected

  (3)执行meet命令将其他节点接入到集群中:

127.0.0.1:6379> cluster meet 127.0.0.1 6381
OK
127.0.0.1:6379> cluster meet 127.0.0.1 6382
OK
127.0.0.1:6379> cluster meet 127.0.0.1 6383
OK
127.0.0.1:6379> cluster meet 127.0.0.1 6384
OK
127.0.0.1:6379> cluster nodes
895e2f6dd1b0d22912e9ecaed1e66df185ccb35c 127.0.0.1:6379@16379 myself,master - 0 1528279459000 1 connected
1782fe58177626fc7bbf991d2c34b72b9a8baae2 127.0.0.1:6384@16384 master - 0 1528279459966 5 connected
bc1da14abc7f6b11a2aaeabf8304a585d91112a9 127.0.0.1:6383@16383 master - 0 1528279459000 4 connected
18b0d98920db2a1eb81636659dda3773c3e2546b 127.0.0.1:6381@16381 master - 0 1528279460000 2 connected
6081f5b4c72f64ea2c74f1079c2ca896b029d900 127.0.0.1:6382@16382 master - 0 1528279460973 3 connected
c3efbba60fd1b860f11313236a84e72945709a76 127.0.0.1:6380@16380 master - 0 1528279458956 0 connected

  只需要在集群内任意节点上执行cluster meet命令加入新节点,握手状态会通过消息在集群内传播,这样其他节点会自动发现新节点并发起握手流程。

  (4)节点建立握手之后集群还不能正常工作,因为这是集群处于下线状态,所有的数据读写都被禁止,例如:

root@myubuntu:/home/redis/conf# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> set hello clusters
(error) CLUSTERDOWN Hash slot not served

  出现这个错误是因为:虽然配置并启动了 Redis 集群服务,但是它们暂时还并不在一个集群中,互相直接发现不了,而且还没有可存储的位置,就是所谓的slot(槽)

  手动操作的话,接下来的操作是分配槽--分配从节点,但是这个错误还不能解决,所以只能通过Redis集群管理工具redis-trib.rb来搭建集群。

  3.用redis-trib.rb搭建集群

  redis-trib.rb是采用Ruby实现的Redis集群管理工具。内部通过Cluster相关命令帮助简化集群创建、检查、槽迁移和均衡等常见操作。

  (1)节点配置(以其中一个为例):

// 绑定节点端口
port 6481
// 开启守护进程
daemonize yes
// 开启AOF模式
appendonly yes
// 绑定IP地址
bind 127.0.0.1
// 日志文件
logfile "node-6481.log"
// 开启集群模式
cluster-enabled yes
// 节点超时时间
cluster-node-timeout 15000
// 集群内部配置文件
cluster-config-file "node-6481.conf"
// 文件生成位置
dir "/home/redis/clusters/data"
// 把pid写入指定文件,而不是默认redis.pid里
pidfile /var/run/redis_6481.pid

  (2)准备好节点配置并启动节点:

root@myubuntu:/home/redis/clusters/conf# redis-server redis-6481.conf 
root@myubuntu:/home/redis/clusters/conf# redis-server redis-6482.conf 
root@myubuntu:/home/redis/clusters/conf# redis-server redis-6483.conf 
root@myubuntu:/home/redis/clusters/conf# redis-server redis-6484.conf 
root@myubuntu:/home/redis/clusters/conf# redis-server redis-6485.conf 
root@myubuntu:/home/redis/clusters/conf# redis-server redis-6486.conf 

  (3)Ruby环境准备(这步很坑,这步很坑,这步很坑!)

  安装Ruby:

root@myubuntu:/home/redis/clusters# apt-get install ruby

  安装rubygem redis依赖:

root@myubuntu:/home/redis/clusters# apt-get install rubygems

 然后执行以下命令会出现错误(注意ubuntu和Centos的区别啊,网上好多帖子用的都是Centos的系统,坑死我了!):

root@bigjun:/usr/local/src# gem install redis
ERROR:  Loading command: install (LoadError)
    cannot load such file -- zlib
ERROR:  While executing gem ... (NoMethodError)
    undefined method `invoke_with_build_args' for nil:NilClass

  这里需要安装zlib依赖库,Centos系统上叫zlib-devel,但是,ubuntu上叫zlib1g这个库,就很坑!!!

  安装完成后,去Ruby源码下ext文件夹下的zlib文件夹里执行如下命令:

root@bigjun:/usr/local/src/ruby-2.3.3# cd ext/zlib/
root@bigjun:/usr/local/src/ruby-2.3.3/ext/zlib# ruby ./extconf.rb 
checking for deflateReset() in -lz... yes
checking for zlib.h... yes
checking for crc32_combine() in zlib.h... yes
checking for adler32_combine() in zlib.h... yes
checking for z_crc_t in zlib.h... yes
creating Makefile

root@bigjun:/usr/local/src/ruby-2.3.3/ext/zlib# make
make: Nothing to be done for 'all'.
root@bigjun:/usr/local/src/ruby-2.3.3/ext/zlib# make install
/usr/bin/install -c -m 0755 zlib.so /usr/local/ruby/lib/ruby/site_ruby/2.3.0/x86_64-linux

  都出现yes就说明可以了。

  然后继续执行gem install redis又出现了下面的错误:

ERROR:  While executing gem ... (Gem::Exception)
    Unable to require openssl, install OpenSSL and rebuild ruby (preferred) or use non-HTTPS sources

  解决办法:安装openssl库(参考:https://blog.csdn.net/huang930528/article/details/51027915)

  另外,还缺少一个libssl-dev库,又坑死我了!(WTF!搞了一天,气死了,果然科研需要坚持不懈!!!)

  前前后后搞了一整天,终于见到了想要的东西,我滴妈耶!!!

root@bigjun:/usr/local/src# gem install redis-4.0.1.gem 
Successfully installed redis-4.0.1
Parsing documentation for redis-4.0.1
Installing ri documentation for redis-4.0.1
Done installing documentation for redis after 0 seconds
1 gem installed

  (4)执行redis-trib.rb命令确认环境是否正确(完全正确!):

root@bigjun:/usr/local/src/redis/src# redis-trib.rb 
Usage: redis-trib <command> <options> <arguments ...>

  create          host1:port1 ... hostN:portN
                  --replicas <arg>
  check           host:port
  info            host:port
  fix             host:port
                  --timeout <arg>
  reshard         host:port
                  --from <arg>
                  --to <arg>
                  --slots <arg>
                  --yes
                  --timeout <arg>
                  --pipeline <arg>
...

  (5)创建集群,即包括完成节点握手和槽分配过程:

root@bigjun:/home/redis/clusters/conf# redis-trib.rb create --replicas 1 127.0.0.1:6481 127.0.0.1:6482 127.0.0.1:6483 127.0.0.1:6484 127.0.0.1:6485 127.0.0.1:6486 
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
127.0.0.1:6481
127.0.0.1:6482
127.0.0.1:6483
Adding replica 127.0.0.1:6485 to 127.0.0.1:6481
Adding replica 127.0.0.1:6486 to 127.0.0.1:6482
Adding replica 127.0.0.1:6484 to 127.0.0.1:6483
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:0-5460 (5461 slots) master
M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:5461-10922 (5462 slots) master
M: 65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483
   slots:10923-16383 (5461 slots) master
S: b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484
   replicates 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
S: 1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485
   replicates b511ba661b98cce2b2fbec6e5c9387af16768dbb
S: 618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486
   replicates 65918f60fde257a7922a36328c92b944ac83c58a
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join..
>>> Performing Cluster Check (using node 127.0.0.1:6481)
M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
S: b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484
   slots: (0 slots) slave
   replicates 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
S: 1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485
   slots: (0 slots) slave
   replicates b511ba661b98cce2b2fbec6e5c9387af16768dbb
S: 618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486
   slots: (0 slots) slave
   replicates 65918f60fde257a7922a36328c92b944ac83c58a
M: 65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

  所有的16384个槽全部被分配,集群创建成功。

  (6)集群完成性检查

  集群完成性检查是指所有的槽都分配到存活的主节点上,只要16384个槽中有一个没有分配给节点则表示集群不完整。

root@bigjun:/home/redis/clusters/conf# redis-trib.rb check 127.0.0.1:6481
>>> Performing Cluster Check (using node 127.0.0.1:6481)
M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
S: b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484
   slots: (0 slots) slave
   replicates 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
S: 1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485
   slots: (0 slots) slave
   replicates b511ba661b98cce2b2fbec6e5c9387af16768dbb
S: 618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486
   slots: (0 slots) slave
   replicates 65918f60fde257a7922a36328c92b944ac83c58a
M: 65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

  三、节点通信

  1.通信流程

  在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。

  常见的元数据维护方式分为:集中式和P2P方式。

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

  

  节点通信过程:

  • 集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。
  • 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
  • 接收到ping消息的节点用pong消息作为响应。

  集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。

  2.Gossip消息

  Gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip消息,了解这些消息有助于我们理解集群如何完成信息交换。
  常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等。

  

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

  3.节点选择

  虽然Gossip协议的信息交换机制具有天然的分布式特性,但它是有成本的。由于内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。Redis集群内节点通信采用固定频率(定时任务每秒执行10次)。因此节点每次选择需要通信的节点列表变得非常重要。

  通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少会降低集群内所有节点彼此信息交换频率,从而影响故障判定、新节点发现等需求的速度。因此Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销。

  

  根据通信节点选择的流程可以看出消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。

  • 集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性。每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。
  • 每个ping消息的数据量体现在消息头和消息体中,其中消息头主要占用空间的字段是myslots[CLUSTER_SLOTS/8],占用2KB,这块空间占用相对固定。消息体会携带一定数量的其他节点信息用于信息交换。

  四、集群伸缩

  1.伸缩原理

  Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。

  Redis集群可以实现对节点的灵活上下线控制。其中原理可抽象为槽和对应数据在不同节点之间灵活移动。

  

  图中每个节点把一部分槽和数据迁移到新的节点6385,每个节点负责的槽和数据相比之前变少了从而达到了集群扩容的目的。

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

  2.扩容集群

  当前集群拓扑为:

  

  (1)准备新节点

root@bigjun:/home/redis/clusters/conf# redis-server node-6487.conf 
root@bigjun:/home/redis/clusters/conf# redis-server node-6488.conf

  (2)加入集群

  新节点接入集群有两种方法:

127.0.0.1:6481> cluster meet 127.0.0.1 6487
127.0.0.1:6481> cluster meet 127.0.0.1 6488

  或者

redis-trib.rb add-node 127.0.0.1:6487 127.0.0.1:6482
redis-trib.rb add-node 127.0.0.1:6488 127.0.0.1:6481

  正式环境建议使用redis-trib.rb add-node命令加入新节点,该命令内部会执行新节点状态检查,如果新节点已经加入其他集群或者包含数据,则放弃集群加入操作。
  如果我们手动执行cluster meet命令加入已经存在于其他集群的节点,会造成被加入节点的集群合并到现有集群的情况,从而造成数据丢失和错乱,后果非常严重,线上谨慎操作。

  使用第二章方法来使新节点接入集群:

  加入之前集群的状态:

127.0.0.1:6481> cluster nodes
9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 myself,master - 0 1528420784000 1 connected 0-5460
65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483@16483 master - 0 1528420785824 3 connected 10923-16383
b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484@16484 slave 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 0 1528420782795 4 connected
1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485@16485 slave b511ba661b98cce2b2fbec6e5c9387af16768dbb 0 1528420782000 5 connected
b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482@16482 master - 0 1528420784818 2 connected 5461-10922
618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486@16486 slave 65918f60fde257a7922a36328c92b944ac83c58a 0 1528420783802 6 connected

  执行新节点加入集群命令:

root@bigjun:/home/redis/clusters/conf# redis-trib.rb add-node 127.0.0.1:6487 127.0.0.1:6481
>>> Adding node 127.0.0.1:6487 to cluster 127.0.0.1:6481
>>> Performing Cluster Check (using node 127.0.0.1:6481)
M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
M: 65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
S: b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484
   slots: (0 slots) slave
   replicates 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
S: 1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485
   slots: (0 slots) slave
   replicates b511ba661b98cce2b2fbec6e5c9387af16768dbb
M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486
   slots: (0 slots) slave
   replicates 65918f60fde257a7922a36328c92b944ac83c58a
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6487 to make it join the cluster.
[OK] New node added correctly.
root@bigjun:/home/redis/clusters/conf# redis-trib.rb add-node 127.0.0.1:6488 127.0.0.1:6481
>>> Adding node 127.0.0.1:6488 to cluster 127.0.0.1:6481
>>> Performing Cluster Check (using node 127.0.0.1:6481)
M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
M: 4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487
   slots: (0 slots) master
   0 additional replica(s)
M: 65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
S: b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484
   slots: (0 slots) slave
   replicates 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
S: 1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485
   slots: (0 slots) slave
   replicates b511ba661b98cce2b2fbec6e5c9387af16768dbb
M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486
   slots: (0 slots) slave
   replicates 65918f60fde257a7922a36328c92b944ac83c58a
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6488 to make it join the cluster.
[OK] New node added correctly.

  新节点加入集群之后的集群信息:

127.0.0.1:6481> cluster nodes
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 master - 0 1528420933000 7 connected
9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 myself,master - 0 1528420930000 1 connected 0-5460
65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483@16483 master - 0 1528420930000 3 connected 10923-16383
b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484@16484 slave 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 0 1528420933938 4 connected
74f1c0605e83dc60affa5a486fef87a3cbb4a2f5 127.0.0.1:6488@16488 master - 0 1528420932000 0 connected
1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485@16485 slave b511ba661b98cce2b2fbec6e5c9387af16768dbb 0 1528420931000 5 connected
b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482@16482 master - 0 1528420931000 2 connected 5461-10922
618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486@16486 slave 65918f60fde257a7922a36328c92b944ac83c58a 0 1528420932928 6 connected

  

  可以看出,新节点刚开始都是主节点状态,但是由于没有负责的槽,所以不能接受任何读写操作。

  对于新节点的后续操作我们一般有两种选择:

  • 为它迁移槽和数据实现扩容。
  • 作为其他主节点的从节点负责故障转移。

  (3)迁移槽和数据

  加入集群后需要为新节点迁移槽和相关数据,槽在迁移过程中集群可以正常提供读写服务,迁移过程是集群扩容最核心的环节。

  槽是Redis集群管理数据的基本单位,首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀。

  以以下迁移计划为例:

  

  数据迁移过程是逐个槽进行的,每个槽数据迁移的过程如下:

  

  • 对目标节点发送cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据。
  • 对源节点发送cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽的数据。
  • 源节点循环执行cluster getkeysinslot {slot} {count}命令,获取count个属于槽{slot}的键。
  • 在源节点上执行migrate {targetIp} {targetPort} "" 0 {timeout} keys {keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点。
  • 重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点。
  • 向集群内所有主节点发送cluster setslot{slot}node{targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。

   (4)手动将源节点6481负责的槽2359迁移到目标节点6487中:

  首先为了得到槽2359的数据,需要向6481节点上写入数据,加入我要写十万个数据,那么有什么办法呢?这里我想了两个办法:Lua脚本和Jedis。但是Lua脚本不是很熟,那么Jedis对于我来说还是非常熟悉的,那么就用Jedis来写入十万个数据。

  但是这里有一个问题,不能向集群中的某个节点写任意数据,具体原因后面会分析。

  于是,为了测试方便,经过一番尝试后,决定将槽2359迁移至目标节点中。

127.0.0.1:6481> cluster keyslot hello
(integer) 866
127.0.0.1:6481> set node:key:1
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6481> set node:key:1 node:value:1
(error) MOVED 10613 127.0.0.1:6482
127.0.0.1:6481> set node:key:2 node:value:2
(error) MOVED 6422 127.0.0.1:6482
127.0.0.1:6481> set node:key:3 node:value:4
OK
127.0.0.1:6481> set node:key:5 node:value:5
(error) MOVED 10737 127.0.0.1:6482
127.0.0.1:6481> set node:key:6 node:value:7
(error) MOVED 6546 127.0.0.1:6482
127.0.0.1:6481> set node:key:8 node:value:8
(error) MOVED 14428 127.0.0.1:6483
127.0.0.1:6481> keys *
1) "node:key:3"
2) "cluster:key\xef\xbc\x9ai"
3) "hello"
127.0.0.1:6481> get node:key:3
"node:value:4"
127.0.0.1:6481> cluster keyslot node:key:3 
(integer) 2359
  • 目标节点6487准备导入槽2359数据:
明确6481和6487两个节点对应的id:
127.0
.0.1:6481> cluster nodes 4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 master - 0 1528438304000 7 connected 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 myself,master - 0 1528438301000 1 connected 0-5460 ...

然后目标节点准备导入槽2359数据:
127.0.0.1:6487> cluster setslot 2359 importing 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 OK

确认槽2359导入状态开启
127.0.0.1:6487> cluster nodes
...
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 myself,master - 0 1528438400000 7 connected [2359-<-9ef1113c90d3cc1b8422d31b81e6315ea4183ce8]
  • 源节点6481准备导出槽2359数据:
6481节点准备导出到2359的数据到6487节点:
127.0.0.1:6481> cluster setslot 2359 migrating 4da067f5ef20f201e301bb2be98469d01f5f91fd
OK
确认2359导出状态开启:
127.0
.0.1:6481> cluster nodes 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 myself,master - 0 1528438565000 1 connected 0-5460 [2359->-4da067f5ef20f201e301bb2be98469d01f5f91fd] ...
  • 批量将槽2359的键从源节点转移到目的节点:
批量获取槽2359对应的键,这里获取到1个处于该槽的键:
127.0.0.1:6481> cluster getkeysinslot 2359 100
1) "node:key:3"

确认这三个键都存在于源节点6481中:
127.0.0.1:6481> mget node:key:3
1) "node:value:4"

批量迁移这一个键,migrate命令保证了每个键迁移过程的原子性:
127.0.0.1:6481> migrate 127.0.0.1 6487 "" 0 5000 keys node:key:3
OK

查询这一个键是否还在6481中:
127.0.0.1:6481> mget node:key:3
(error) ASK 2359 127.0.0.1:6487
  • 通知所有主节点槽2359已经指派给目标节点6487:
127.0.0.1:6481> cluster setslot 2359 node 4da067f5ef20f201e301bb2be98469d01f5f91fd
OK

127.0.0.1:6482> cluster setslot 2359 node 4da067f5ef20f201e301bb2be98469d01f5f91fd
OK

127.0.0.1:6483> cluster setslot 2359 node 4da067f5ef20f201e301bb2be98469d01f5f91fd
OK

127.0.0.1:6487> cluster setslot 2359 node 4da067f5ef20f201e301bb2be98469d01f5f91fd
OK
  • 确认源节点6481不再负责槽2359,而是目标节点6487负责:
127.0.0.1:6487> cluster nodes
9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 master - 0 1528439307073 1 connected 0-2358 2360-5460
...
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 myself,master - 0 1528439304000 7 connected 2359

127.0.0.1:6487> keys *
1) "node:key:3"
127.0.0.1:6487> get node:key:3
"node:value:4"

  从这里就可以看到,目标节点6487确实负责了槽2359的数据,手动执行槽迁移就完成了。

  (5)在实际操作时肯定设计大量槽并且每个槽对应非常多的键,因此redis-trib提供了槽重分片功能:

redis-trib.rb reshard host:port --from <arg> --to <arg> --slots <arg> --yes --timeout <arg> --pipeline <arg>
host:port:必传参数,集群内任意节点地址,用来获取整个集群信息。
--from:制定源节点的id,如果有多个源节点,使用逗号分隔,如果是all源节点变为集群内所有主节点,在迁移过程中提示用户输入。
--to:需要迁移的目标节点的id,目标节点只能填写一个,在迁移过程中提示用户输入。
--slots:需要迁移槽的总数量,在迁移过程中提示用户输入。
--yes:当打印出reshard执行计划时,是否需要用户输入yes确认后再执行reshard。
--timeout:控制每次migrate操作的超时时间,默认为60000毫秒。--pipeline:控制每次批量迁移键的数量,默认为10。

  reshard命令简化了数据迁移的工作量,其内部针对每个槽的数据迁移同样使用之前的流程。

  (6)使用reshard命令将剩下的槽迁入6487中:

确认6487节点的ID:
127.0.0.1:6487> cluster nodes
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 myself,master - 0 1528439772000 7 connected 2359

用redis-trib.rb完成槽数据迁移:
root@bigjun:~# redis-trib.rb reshard 127.0.0.1:6481
>>> Performing Cluster Check (using node 127.0.0.1:6481)
M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:0-2358,2360-5460 (5460 slots) master
   1 additional replica(s)
M: 4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487
   slots:2359 (1 slots) master
   0 additional replica(s)
M: 65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
S: b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484
   slots: (0 slots) slave
   replicates 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
M: 74f1c0605e83dc60affa5a486fef87a3cbb4a2f5 127.0.0.1:6488
   slots: (0 slots) master
   0 additional replica(s)
S: 1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485
   slots: (0 slots) slave
   replicates b511ba661b98cce2b2fbec6e5c9387af16768dbb
M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486
   slots: (0 slots) slave
   replicates 65918f60fde257a7922a36328c92b944ac83c58a
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

首先输入需要确认的槽数量,这里输入4096个:
How many slots do you want to move (from 1 to 16384)? 4096

然后输入目标节点ID:
What is the receiving node ID? 4da067f5ef20f201e301bb2be98469d01f5f91fd

然后输入源节点的ID,分别输入三个主节点6481 6482 6483的ID,最后用done表示:
Source node #1:9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
Source node #2:b511ba661b98cce2b2fbec6e5c9387af16768dbb
Source node #3:65918f60fde257a7922a36328c92b944ac83c58a
Source node #4:done

确认槽迁移计划:
Ready to move 4096 slots.
  Source nodes:
    M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:0-2358,2360-5460 (5460 slots) master
   1 additional replica(s)
    M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
    M: 65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
  Destination node:
    M: 4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487
   slots:2359 (1 slots) master
   0 additional replica(s)
  Resharding plan:
    Moving slot 5461 from b511ba661b98cce2b2fbec6e5c9387af16768dbb
    Moving slot 5462 from b511ba661b98cce2b2fbec6e5c9387af16768dbb
    Moving slot 5463 from b511ba661b98cce2b2fbec6e5c9387af16768dbb
 ...
   Moving slot 11359 from 65918f60fde257a7922a36328c92b944ac83c58a
    Moving slot 11360 from 65918f60fde257a7922a36328c92b944ac83c58a
    Moving slot 11361 from 65918f60fde257a7922a36328c92b944ac83c58a
...
   Moving slot 0 from 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
    Moving slot 1 from 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
    Moving slot 2 from 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
...
Do you want to proceed with the proposed reshard plan (yes/no)? yes
  
  查看节点6487负责的槽为:
127.0.0.1:6487> cluster nodes
9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 master - 0 1528440408000 1 connected 866-2358 2360-5460
65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483@16483 master - 0 1528440408130 3 connected 12288-16383
b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482@16482 master - 0 1528440409137 2 connected 6827-10922
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 myself,master - 0 1528440406000 7 connected 0-865 2359 5461-6826 10923-12287

  可以看到,节点6487负责的槽为:0-865 2359 5461-6826 10923-12287。

  (7)迁移之后需要检查节点之间槽的均衡性(迁移后所有主节点负责的槽数量差异在2%之内,集群节点数据相对均衡,无需调整。):

# redis-trib.rb rebalance 127.0.0.1:6481
>>> Performing Cluster Check (using node 127.0.0.1:6481)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
*** No rebalancing needed! All nodes are within the 2.0% threshold.

  (8)添加从节点

  节点6487迁移了部分槽和数据作为主节点,但相比其他主节点目前还没有从节点,因此该节点不具备故障转移能力。

  这时,要把节点6488作为6487的从节点,从而保证整个集群的高可用。

127.0.0.1:6488> cluster replicate 4da067f5ef20f201e301bb2be98469d01f5f91fd
OK
127.0.0.1:6488> cluster nodes
618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486@16486 slave 65918f60fde257a7922a36328c92b944ac83c58a 0 1528440975000 3 connected
1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485@16485 slave b511ba661b98cce2b2fbec6e5c9387af16768dbb 0 1528440971115 2 connected
b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484@16484 slave 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 0 1528440973000 1 connected
65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483@16483 master - 0 1528440975239 3 connected 12288-16383
9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 master - 0 1528440977256 1 connected 866-2358 2360-5460
b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482@16482 master - 0 1528440976246 2 connected 6827-10922
74f1c0605e83dc60affa5a486fef87a3cbb4a2f5 127.0.0.1:6488@16488 myself,slave 4da067f5ef20f201e301bb2be98469d01f5f91fd 0 1528440974000 0 connected
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 master - 0 1528440973227 7 connected 0-865 2359 5461-6826 10923-12287

  首先计算每个主节点负责的槽的数量:

6481:(2358-866+1)+(5460-2360+1)=4594
648210922-6827+1=4096
648316383-12288+1=4096
6487:(865-0+1)+(1)+(6826-5461+1)+(12287-10923+1)=3598

  集群现在的拓扑结构为:

  

  3.收缩集群

  收缩集群以为者缩减规模,需要从现有集群中安全下线部分节点。

  安全下线的流程是:

  

  • 首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性。
  • 当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭。

  假如要把主节点6483和其从节点6486下线:

  (1)首先确定两个节点的信息:

127.0.0.1:6488> cluster nodes
618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486@16486 slave 65918f60fde257a7922a36328c92b944ac83c58a 0 1528440975000 3 connected
65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483@16483 master - 0 1528440975239 3 connected 12288-16383

  (2)收缩正好和扩容迁移方向相反,6483变为源节点,其他主节点变为目标节点,源节点需要把自身负责的4096个槽均匀地迁移到其他主节点上。

  这里直接使用redis-trib.rb reshard命令完成槽迁移。由于每次执行reshard命令只能有一个目标节点,

  因此需要执行3次reshard命令,分别迁移1365个槽给6481、1365个槽给6482、1366个槽给6487:

How many slots do you want to move (from 1 to 16384)? 1365
What is the receiving node ID? 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
Please enter all the source node IDs.
  Type 'all' to use all the nodes as source nodes for the hash slots.
  Type 'done' once you entered all the source nodes IDs.
Source node #1:65918f60fde257a7922a36328c92b944ac83c58a
Source node #2:done
...

  迁移完成之后,查看集群信息:

127.0.0.1:6481> cluster nodes
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 master - 0 1528443103000 10 connected 0-865 2359 5461-6826 10923-12287 15018-16383
9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 myself,master - 0 1528443100000 8 connected 866-2358 2360-5460 12288-13652
65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483@16483 master - 0 1528443103000 3 connected
b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482@16482 master - 0 1528443103000 9 connected 6827-10922 13653-15017

  可以看到6483负责的一个槽都没有了。

  (3)忘记节点:

  由于集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。也就是说让其他节
不再与要下线节点进行Gossip消息交换。Redis提供了cluster forget {downNodeId}命令实现该功能。

  当节点接收到cluster forget{down NodeId}命令后,会把nodeId指定的节点加入到禁用列表中,在禁用列表内的节点不再发送Gossip消息。禁用列表有效期是60秒,超过60秒节点会再次参与消息交换。也就是说当第一次forget命令发出后,我们有60秒的时间让集群内的所有节点忘记下线节点。线上操作不建议直接使用cluster forget命令下线节点,需要跟大量节点命令交互,实际操作起来过于繁琐并且容易遗漏forget节点。

  建议使用redis-trib.rb del-node {host:port} {downNodeId}命令:

  先下线从节点再下线主节点,避免不必要的全量复制:

先确定从节点和主节点的ID:
127.0.0.1:6481> cluster nodes
65918f60fde257a7922a36328c92b944ac83c58a 127.0.0.1:6483@16483 master - 0 1528443809000 3 connected
618ba789383a87bbb77d66b1cd47121f55a0294f 127.0.0.1:6486@16486 slave 65918f60fde257a7922a36328c92b944ac83c58a 0 1528443810445 10 connected

然后先下线6486,再下线6483:
root@bigjun:~# redis-trib.rb del-node 127.0.0.1:6481 618ba789383a87bbb77d66b1cd47121f55a0294f
>>> Removing node 618ba789383a87bbb77d66b1cd47121f55a0294f from cluster 127.0.0.1:6481
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.
root@bigjun:~# redis-trib.rb del-node 127.0.0.1:6481 65918f60fde257a7922a36328c92b944ac83c58a
>>> Removing node 65918f60fde257a7922a36328c92b944ac83c58a from cluster 127.0.0.1:6481
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.

确认节点状态:
127.0.0.1:6481> cluster nodes
4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487@16487 master - 0 1528443965000 10 connected 0-865 2359 5461-6826 10923-12287 15018-16383
9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481@16481 myself,master - 0 1528443965000 8 connected 866-2358 2360-5460 12288-13652
b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484@16484 slave 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 0 1528443965000 8 connected
74f1c0605e83dc60affa5a486fef87a3cbb4a2f5 127.0.0.1:6488@16488 slave 4da067f5ef20f201e301bb2be98469d01f5f91fd 0 1528443967445 10 connected
1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485@16485 slave b511ba661b98cce2b2fbec6e5c9387af16768dbb 0 1528443966437 9 connected
b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482@16482 master - 0 1528443965429 9 connected 6827-10922 13653-15017

  此时的拓扑结构为:

  

  执行redis-trib.rb check命令查看是否正确;

root@bigjun:~# redis-trib.rb check 127.0.0.1:6481
>>> Performing Cluster Check (using node 127.0.0.1:6481)
M: 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8 127.0.0.1:6481
   slots:866-2358,2360-5460,12288-13652 (5959 slots) master
   1 additional replica(s)
M: 4da067f5ef20f201e301bb2be98469d01f5f91fd 127.0.0.1:6487
   slots:0-865,2359,5461-6826,10923-12287,15018-16383 (4964 slots) master
   1 additional replica(s)
S: b3f17821f17ab6d0ad2fcae777fe550804f88eff 127.0.0.1:6484
   slots: (0 slots) slave
   replicates 9ef1113c90d3cc1b8422d31b81e6315ea4183ce8
S: 74f1c0605e83dc60affa5a486fef87a3cbb4a2f5 127.0.0.1:6488
   slots: (0 slots) slave
   replicates 4da067f5ef20f201e301bb2be98469d01f5f91fd
S: 1fea45c1fbfa9cc1119b3cdb1f674f84e294fac3 127.0.0.1:6485
   slots: (0 slots) slave
   replicates b511ba661b98cce2b2fbec6e5c9387af16768dbb
M: b511ba661b98cce2b2fbec6e5c9387af16768dbb 127.0.0.1:6482
   slots:6827-10922,13653-15017 (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

  五、请求路由以及客户端操作集群

  Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。因此对于希望从单机切换到集群环境的应用需要修改客户端代码。

  首先创建集群:

>>> Performing Cluster Check (using node 192.168.131.132:7001)
M: 3a6bf0f8b7b01adb6d94aa743901f393f5168b9e 192.168.131.132:7001
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
S: 662746314e94e6e74a1a03b56c12dd73e23a6ee4 192.168.131.132:7004
   slots: (0 slots) slave
   replicates 269628e6801fba86a9fcf59cc8ff84551b5cfdce
S: e8dd125c16df5a5d4b24880358d4aabd0b708245 192.168.131.132:7005
   slots: (0 slots) slave
   replicates c8688dcdf350ed838f167de4c3d1919f77490906
M: c8688dcdf350ed838f167de4c3d1919f77490906 192.168.131.132:7003
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
S: e714976c9e9f6928452c0b069964bea25c73b80a 192.168.131.132:7006
   slots: (0 slots) slave
   replicates 3a6bf0f8b7b01adb6d94aa743901f393f5168b9e
M: 269628e6801fba86a9fcf59cc8ff84551b5cfdce 192.168.131.132:7002
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

  集群的拓扑为:

  1.请求重定向

  (1)在节点7001上执行set命令成功,这是因为键key:test:1对应槽5191正好位于7001节点负责的槽范围内:

192.168.131.132:7001> set key:test:1 value-1
OK
192.168.131.132:7001> cluster keyslot key:test:1
(integer) 5191

  (2)在节点7001上执行另一个set命令不成功,因为键key:test:2对应的槽9252位于节点7002负责的槽范围内:

192.168.131.132:7001> set key:test:2 value-2
(error) MOVED 9252 192.168.131.132:7002
192.168.131.132:7001> cluster keyslot key:test:2
(integer) 9252

  (3)在第(2)步的返回中包含了重定向的槽以及负责该槽的节点地址,在7002节点上可以成功执行之前的set命令:

192.168.131.132:7002> set key:test:2 value-2
OK

  (4)使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作,实际上是帮助客户端连接到正确的节点执行命令。

root@bigjun:/home/redis/clusters/conf# redis-cli -h 192.168.131.132 -p 7001 -c
192.168.131.132:7001> set key:test:2 value-2
-> Redirected to slot [9252] located at 192.168.131.132:7002
OK
192.168.131.132:7002> keys *
1) "key:test:2"
192.168.131.132:7002> get key:test:2
"value-2"

  这个过程是在redis-cli内部维护,实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发。

  

   键命令执行步骤主要分为两步:计算槽、查找槽所对应的节点。

  (5)计算槽

  Redis首先需要计算键所对应的槽。根据键的有效部分使用CRC16函数计算出散列值,再取对16383的余数,使每个键都可以映射到0~16383槽范围内。

  根据输入的键key,计算槽的代码为:

def key_hash_slot(key):
int keylen = key.length();
for (s = 0; s < keylen; s++):
if (key[s] == '{'):
break;
if (s == keylen) return crc16(key,keylen) & 16383;
for (e = s+1; e < keylen; e++):
if (key[e] == '}') break;
if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;
/* 使用 { 和 } 之间的有效部分计算槽 */
return crc16(key+s+1,e-s-1) & 16383;

  从代码可以看出:如果键key的内容包括“{”和“}”,那么计算槽的有效部分就是括号内的内容;否则采用键的全内容计算槽。

  cluster keyslot命令就是采用key_hash_slot(key)函数实现的,可以用来计算键key对应的槽。

192.168.131.132:7001> cluster keyslot key:test:111
(integer) 10050

  其中如果大括号内部是hash_tag,那么可以使不同的键具备相同的slot的功能。

192.168.131.132:7001> cluster keyslot key:{hash_tag}:111
(integer) 2515
192.168.131.132:7001> cluster keyslot key:{hash_tag}:222
(integer) 2515
192.168.131.132:7001> cluster keyslot key:{hash_tag}:333
(integer) 2515

  如果在集群中使用mget等命令批量调用时,键列表必须具有相同的slot,否则就无法执行。这个时候可以利用hash_tag让不同的键具有相同的slot,从而可以使用批量命令(这块有点困惑,没懂):

192.168.131.132:7001> mset node:250:7001 7001 node:250:7001 7002
(error) MOVED 10629 192.168.131.132:7002
192.168.131.132:7001> mset node:{250}:7001 7001 node:{250}:7001 7002
(error) MOVED 10230 192.168.131.132:7002
192.168.131.132:7001> cluster keyslot node:250:7001
(integer) 10629
192.168.131.132:7001> cluster keyslot node:250:7002
(integer) 6630
192.168.131.132:7001> cluster keyslot node:250:7003
(integer) 2503
192.168.131.132:7001> cluster keyslot node:{250}:7003
(integer) 10230
192.168.131.132:7001> cluster keyslot node:{250}:7003
(integer) 10230
192.168.131.132:7001> cluster keyslot node:{250}:7002
(integer) 10230
192.168.131.132:7001> cluster keyslot node:{250}:7001
(integer) 10230

root@bigjun:/home/redis/clusters/conf# redis-cli -h 192.168.131.132 -p 7002
192.168.131.132:7002> mset node:{250}:7001 7001 node:{250}:7001 7002
OK
192.168.131.132:7002> keys *
1) "node:{250}:7001"
2) "key:test:2"
192.168.131.132:7002> get node:{250}:7001
"7002"
192.168.131.132:7002> 
root@bigjun:/home/redis/clusters/conf# redis-cli -h 192.168.131.132 -p 7001
192.168.131.132:7001> keys *
1) "key:test:1"
192.168.131.132:7001> 
root@bigjun:/home/redis/clusters/conf# redis-cli -h 192.168.131.132 -p 7003
192.168.131.132:7003> keys *
(empty list or set)

  再来一个例子(重定向可以重定向多次):

root@bigjun:/home/redis/clusters/conf# redis-cli -h 192.168.131.132 -p 7001 -c
192.168.131.132:7001> set user:10086:friends friends
OK
192.168.131.132:7001> set user:10086:videos videos
OK
192.168.131.132:7001> set user:10086:wife wife
-> Redirected to slot [8331] located at 192.168.131.132:7002
OK
192.168.131.132:7002> set user:10086:laogong laogong
-> Redirected to slot [3522] located at 192.168.131.132:7001
OK
192.168.131.132:7001> set user:10086:lian lian
OK
192.168.131.132:7001> set user:10086:shen shen
-> Redirected to slot [14706] located at 192.168.131.132:7003
OK

  从上面可以看出来,每一个节点里对应着不同的槽的不同数据:7001(friends、videos、laogong、lian)、7002(wife)、7003(shen)

  • 7002去拿7001里面的键(加大括号后可以执行,但是没有值):
192.168.131.132:7002> mget user:10086:friends user:10086:videos user:10086:laogong user:10086:lian
(error) CROSSSLOT Keys in request don't hash to the same slot
192.168.131.132:7002> mget user:{10086}:friends user:{10086}:videos user:{10086}:laogong user:{10086}:lian
1) (nil)
2) (nil)
3) (nil)
4) (nil)
  • 7002去拿7001和7002里面的键(还是没有):
192.168.131.132:7002> mget user:10086:friends user:10086:videos user:10086:laogong user:10086:lian user:10086:wife
(error) CROSSSLOT Keys in request don't hash to the same slot
192.168.131.132:7002> mget user:{10086}:friends user:{10086}:videos user:{10086}:laogong user:{10086}:lian user:{10086}:wife
1) (nil)
2) (nil)
3) (nil)
4) (nil)
5) (nil)
  • 7001去拿自己的键(太乱了,慢慢看):
root@bigjun:/home/redis/clusters/conf# redis-cli -h 192.168.131.132 -p 7001 
192.168.131.132:7001> mget user:10086:friends user:10086:videos user:10086:laogong user:10086:lian
(error) CROSSSLOT Keys in request don't hash to the same slot
192.168.131.132:7001> mget user:{10086}:friends user:{10086}:videos user:{10086}:laogong user:{10086}:lian
(error) MOVED 5466 192.168.131.132:7002
192.168.131.132:7001> mget user:{2500}:friends user:{2500}:videos user:{2500}:laogong user:{2500}:lian
(error) MOVED 7186 192.168.131.132:7002
192.168.131.132:7001> keys *
1) "user:10086:videos"
2) "user:10086:laogong"
3) "key:test:1"
4) "user:10086:lian"
5) "user:10086:friends"
192.168.131.132:7001> mget user:10086:friends user:10086:videos user:10086:laogong user:10086:lian
(error) CROSSSLOT Keys in request don't hash to the same slot
192.168.131.132:7001> cluster keyslot user:{1}:friends
(integer) 9842
192.168.131.132:7001> cluster keyslot user:{2}:friends
(integer) 5649
192.168.131.132:7001> cluster keyslot user:{3}:friends
(integer) 1584
192.168.131.132:7001> mget user:{3}:friends user:{3}:videos user:{3}:laogong user:{3}:lian
1) (nil)
2) (nil)
3) (nil)
4) (nil)
192.168.131.132:7001> mset user:{3}:friends friends user:{3}:videos videos user:{3}:laogong laogong user:{3}:lian lian
OK
192.168.131.132:7001> mget user:{3}:friends user:{3}:videos user:{3}:laogong user:{3}:lian
1) "friends"
2) "videos"
3) "laogong"
4) "lian"
192.168.131.132:7001> cluster keyslot user:{3}:friends
(integer) 1584
192.168.131.132:7001> cluster keyslot user:{3}:videos
(integer) 1584
192.168.131.132:7001> cluster keyslot user:{3}:laogong
(integer) 1584
192.168.131.132:7001> cluster keyslot user:{3}:lian
(integer) 1584

  结论:即使是同一个节点,只要是键对应的slot值不一样,就不可以执行mget和mset方法,更不用说是不同节点了,就更不可能了。

     使用大括号可以使第一个下面的键key值对应的slot值和第一个键的slot值一样,这样就可以存在同一个节点的同一个槽下了,这当然可以在这个节点mget到了。

  (6)槽节点查找

  Redis计算得到键对应的槽后,需要查找槽所对应的节点。集群内通过消息交换每个节点都会知道所有节点的槽信息。

  槽节点查找的代码是:  

typedef struct clusterState {
clusterNode *myself; /* 自身节点 ,clusterNode 代表节点结构体 */
clusterNode *slots[CLUSTER_SLOTS]; /* 16384 个槽和节点映射数组,数组下标代表对应的槽 */
...
} clusterState;
slots数组表示槽和节点对应关系,实现请求重定向伪代码如下: def execute_or_redirect(key):
int slot = key_hash_slot(key); ClusterNode node = slots[slot]; if(node == clusterState.myself): return executeCommand(key); else: return '(error) MOVED {slot} {node.ip}:{node.port}';

  据伪代码看出节点对于判定键命令是执行还是MOVED重定向,都是借助slots[CLUSTER_SLOTS]数组实现。

  根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销,这不是Redis集群高效的使用方式。

  正因为如此通常集群客户端都采用另一种实现:Smart(智能)客户端。

  2.Smart客户端

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

  以Jedis为例介绍Smart客户端操作集群的流程:

  (1)启动节点并且搭建集群: 

root@bigjun:/home/redis/clusters/conf# redis-server node-7001.conf 
root@bigjun:/home/redis/clusters/conf# redis-server node-7002.conf 
root@bigjun:/home/redis/clusters/conf# redis-server node-7003.conf 
root@bigjun:/home/redis/clusters/conf# redis-server node-7004.conf 
root@bigjun:/home/redis/clusters/conf# redis-server node-7005.conf 
root@bigjun:/home/redis/clusters/conf# redis-server node-7006.conf 

  (2)在JedisCluster初始化时会选择一个运行节点,初始化槽和节点映射关系,使用cluster slots命令完成:

192.168.131.132:7001> cluster slots
1) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "192.168.131.132"
      2) (integer) 7002
      3) "f0c0bba3e13cccb529f4a9826fc10556c207da86"
   4) 1) "192.168.131.132"
      2) (integer) 7004
      3) "8fcab02224a1419c92db91350d81817fd47e8cc5"
2) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "192.168.131.132"
      2) (integer) 7003
      3) "fea178a9eba33948ce255afcdde49796b84fb93a"
   4) 1) "192.168.131.132"
      2) (integer) 7005
      3) "dcd2d62dd624b9d1cc483a5d8b92b977577d14ad"
3) 1) (integer) 0
   2) (integer) 5460
   3) 1) "192.168.131.132"
      2) (integer) 7001
      3) "7a19118645a20351fba3fbd93a12ee864ec0ffaf"
   4) 1) "192.168.131.132"
      2) (integer) 7006
      3) "dc3d6eb37a053befdabbd3fa6898885a2d6bed90"

  输出信息包括:开始槽范围、结束槽范围、主节点ip、主节点端口号、主节点ID、从节点ip、从节点端口号、从节点ID。

  (3)JedisCluster解析cluster slots结果缓存在本地,并为每个节点创建唯一的JedisPool连接池。映射关系在JedisClusterInfoCache类中:

public class JedisClusterInfoCache {
private Map<String, JedisPool> nodes = new HashMap<String, JedisPool>();
private Map<Integer, JedisPool> slots = new HashMap<Integer, JedisPool>();
...
}

  (4)JedisCluster执行键命令,源码如下:

public abstract class JedisClusterCommand<T> {
// 集群节点连接处理器
private JedisClusterConnectionHandler connectionHandler;
// 重试次数,默认 5 次
private int redirections;
// 模板回调方法
public abstract T execute(Jedis connection);
public T run(String key) {
if (key == null) {
throw new JedisClusterException("No way to dispatch this command to
Redis Cluster.");
}
return runWithRetries(SafeEncoder.encode(key), this.redirections, false,
false);
}
// 利用重试机制运行键命令
private T runWithRetries(byte[] key, int redirections, boolean tryRandomNode,
boolean asking) {
if (redirections <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redi
rections");
}
Jedis connection = null;
try {
if (tryRandomNode) {
// 随机获取活跃节点连接
connection = connectionHandler.getConnection();
} else {
// 使用 slot 缓存获取目标节点连接
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.
getSlot(key));
}
return execute(connection);
} catch (JedisConnectionException jce) {
// 出现连接错误使用随机连接重试
return runWithRetries(key, redirections - 1, true/* 开启随机连接 */, asking);
} catch (JedisRedirectionException jre) {
if (jre instanceof JedisMovedDataException) {
// 如果出现 MOVED 重定向错误 , 在连接上执行 cluster slots 命令重新初始化 slot 缓存
this.connectionHandler.renewSlotCache(connection);
}
// slot 初始化后重试执行命令
return runWithRetries(key, redirections - 1, false, asking);
} finally {
releaseConnection(connection);
}
}
}

  通过上面的源码可以看到,键命令执行的流程是:

  

  ①计算slot并根据slots缓存获取目标节点连接,发送命令。

  ②如果出现连接错误,使用随机连接重新执行键命令,每次命令重试对redi-rections参数减1。

  ③捕获到MOVED重定向错误,使用cluster slots命令更新slots缓存(renewSlotCache方法)。

  ④重复执行①~③步,直到命令执行成功,或者当redirections<=0时抛出Jedis ClusterMaxRedirectionsException异常。

  3.Smart客户端--JedisCluster使用方法

  (1)JedisCluster初始化

  public JedisCluster(Set<HostAndPort> jedisClusterNode, int connectionTimeout, int soTimeout,
      int maxAttempts, final GenericObjectPoolConfig poolConfig) {
    super(jedisClusterNode, connectionTimeout, soTimeout, maxAttempts, poolConfig);
  }
Set<HostAndPort> jedisClusterNode:所有Redis Cluster节点信息
connectionTimeout:连接超时
soTimeout:读写超时
maxAttempts:重试次数
GenericObjectPoolConfig poolConfig:连接池参

public class JedisClusterTest {
    public static void main(String[] args) {
        // 初始化所有节点
        Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7001));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7002));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7003));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7004));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7005));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7006));
        // 初始化common-pool连接池,并设置相关参数
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        @SuppressWarnings("resource")
        // 初始化JedisCluster
        JedisCluster jedisCluster = new JedisCluster(jedisClusterNode, 1000, 1000, 5, poolConfig);
        // 利用JedisCluster实现明星的调用
        jedisCluster.set("Hello", "world");
        System.out.println(jedisCluster.get("Hello"));
    }
}

  对于JedisCluster的使用需要注意:

  • JedisCluster包含了所有节点的连接池(JedisPool),所以建议JedisCluster使用单例。
  • JedisCluster每次操作完成后,不需要管理连接池的借还,它在内部已经完成。
  • JedisCluster一般不要执行close()操作,它会将所有JedisPool执行destroy操作。

  (2)多节点命令和操作

  Redis Cluster虽然提供了分布式的特性,但是有些命令或者操作,诸如keys、flushall、删除指定模式的键,需要遍历所有节点才可以完成。

  在Redis Cluster中删除制定模式的键的步骤是:

  • 通过jedisCluster.getClusterNodes()获取所有节点的连接池。
  • 使用info replication筛选第一步中的主节点。
  • 遍历主节点,使用scan命令找到指定模式的key,使用Pipeline机制删除。

  例如,每次遍历1000个key,将Redis Cluster中以user开头的key全部删除的代码是:

package jedisClusterDemo;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;

public class JedisClusterTest {
    public static void main(String[] args) {
        // 初始化所有节点
        Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7001));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7002));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7003));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7004));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7005));
        jedisClusterNode.add(new HostAndPort("192.168.131.132", 7006));
        // 初始化common-pool连接池,并设置相关参数
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        @SuppressWarnings("resource")
        // 初始化JedisCluster
        JedisCluster jedisCluster = new JedisCluster(jedisClusterNode, 1000, 1000, 5, poolConfig);
        // 给定匹配模式,以user开头的所有key
        String pattern = "user*";
        // 给定遍历key的个数
        int scanCounter = 1000;
        // 执行批量删除方法
        delRedisClusterByPattern(jedisCluster, pattern, scanCounter);
    }
        
        
    public static void delRedisClusterByPattern(JedisCluster jedisCluster, String pattern, int scanCounter) {
        // 获取所有节点的JedisPool
        Map<String, JedisPool> jedisPoolMap = jedisCluster.getClusterNodes();
        for (Entry<String, JedisPool> entry : jedisPoolMap.entrySet()) {
            // 获取每个节点的Jedis连接
            Jedis jedis = entry.getValue().getResource();
            // 只删除主节点数据
            if (!isMaster(jedis)) {
                continue;
            }
            // 使用Pipeline每次删除指定前缀的数据
            Pipeline pipeline = jedis.pipelined();
            // 使用scan扫描指定前缀的数据
            String cursor = "0";
            // 指定扫描参数,每次扫描个数个pattern
            ScanParams params = new ScanParams().count(scanCounter).match(pattern);
            while (true) {
                // 执行扫描
                ScanResult<String> scanResult = jedis.scan(cursor, params);
                // 删除的key列表
                List<String> keyList = scanResult.getResult();
                if (keyList != null && keyList.size() > 0) {
                    for (String key : keyList) {
                        pipeline.del(key);
                    }
                    // 批量删除
                    pipeline.syncAndReturnAll();
                }
                // 如果游标变为0,说明扫描完毕
                cursor = scanResult.getStringCursor();
                if ("0".equals(cursor)) {
                    break;
                }
            }
        }
    }

    // 判断当前Redis是否为Master主节点
    private static boolean isMaster(Jedis jedis) {
        String[] data = jedis.info("Replication").split("\r\n");
        for (String line : data) {
            if ("role:master".equals(line.trim())) {
                return true;
            }
        }
        return false;
    }
}

  (3)批量操作的方法

  Redis Cluster中,由于key分布到各个节点上,会造成无法实现mget、mset等功能。

  可以利用CRC16算法计算出key对应的slot,以及Smart客户端保存了slot和节点对应关系的特性,将属于同一个Redis节点的key进行归档,然后分别对每个节点对应的子key列表执行mget或者pipeline操作。

  (4)使用Lua、事务等特性的方法

  Lua和事务需要所操作的key,必须在一个节点上,不过Redis Cluster提供了hashtag,如果确实需要Lua和事务,可以将所要操作的key使用一个hashtag。

  步骤如下:  

  • 将事务中所有的key添加hashtag
  • 使用CRC16计算hashtag对应的slot
  • 获取指定slot对应的节点连接池JedisPool
  • 在JedisPool上执行事务

  4.ASK重定向

  (1)客户端ASK重定向

  Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。

  例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点,即

  

  当出现上述情况时,客户端键命令执行流程将发生变化,如下所示:

  • 客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
  • 如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:(error)ASK{slot}{targetIP}:{targetPort}。
  • 客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息  

  (2)ASK重定向整体流程:

  

  ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别。ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。

  六、故障转移

  Redis集群自身实现了高可用。高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。

  1.故障发现

  当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。

  Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观(pfail)和客观下线(fail)。

  • 主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
  • 客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。

  2.故障恢复

  故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。

  

  3.故障转移时间

  • 主观下线(pfail)识别时间=cluster-node-timeout。
  • 主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
  • 从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。

  根据以上分析可以预估出故障转移时间,

failover-time( 毫秒 ) ≤ cluster-node-timeout + cluster-node-timeout/2 + 1000

  因此,故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。

  4.故障转移演练

  (1)确认集群状态:

192.168.131.132:7001> cluster nodes
7a19118645a20351fba3fbd93a12ee864ec0ffaf 192.168.131.132:7001@17001 myself,master - 0 1528682877000 1 connected 0-5460
fea178a9eba33948ce255afcdde49796b84fb93a 192.168.131.132:7003@17003 master - 0 1528682880445 3 connected 10923-16383
dc3d6eb37a053befdabbd3fa6898885a2d6bed90 192.168.131.132:7006@17006 slave 7a19118645a20351fba3fbd93a12ee864ec0ffaf 0 1528682879439 6 connected
8fcab02224a1419c92db91350d81817fd47e8cc5 192.168.131.132:7004@17004 slave f0c0bba3e13cccb529f4a9826fc10556c207da86 0 1528682878434 4 connected
dcd2d62dd624b9d1cc483a5d8b92b977577d14ad 192.168.131.132:7005@17005 slave fea178a9eba33948ce255afcdde49796b84fb93a 0 1528682878000 5 connected
f0c0bba3e13cccb529f4a9826fc10556c207da86 192.168.131.132:7002@17002 master - 0 1528682878000 2 connected 5461-10922

  (2)3个主节点里的键:

192.168.131.132:7001> keys *
1) "test:7"
2) "Hello"
3) "test:3"

192.168.131.132:7002> keys *
1) "test:6"
2) "test:2"

192.168.131.132:7003> keys *
1) "test:4"
2) "test:8"

7001的从节点7006:
 192.168.131.132:7006> keys *
1) "test:3"
2) "Hello"
3) "test:7"

  (3)强制关闭7001进程,模拟7001主节点故障的场景

root@bigjun:~# ps -ef | grep redis-server | grep 7001
root       2464   1667  0 10:46 ?        00:00:00 redis-server 192.168.131.132:7001 [cluster]
root@bigjun:~# kill -9 2464

  (4)从节点7006与主节点7001复制中断:

cat redis-7006.log
2493:S 11 Jun 10:47:54.536 # Connection with master lost.
2493:S 11 Jun 10:47:54.536 * Caching the disconnected master state.
2493:S 11 Jun 10:47:55.051 * Connecting to MASTER 192.168.131.132:7001
2493:S 11 Jun 10:47:55.051 * MASTER <-> SLAVE sync started
2493:S 11 Jun 10:47:55.051 # Error condition on socket for SYNC: Connection refused

  (5)7002和7003两个主节点都标记7001为主观下线,超过半数因此标记为客观下线状态:

cat node-7002.log
2469:M 11 Jun 10:48:12.913 * Marking node 7a19118645a20351fba3fbd93a12ee864ec0ffaf as failing (quorum reached).

cat node-7003.log
2474:M 11 Jun 10:48:12.912 * Marking node 7a19118645a20351fba3fbd93a12ee864ec0ffaf as failing (quorum reached).

  (6)从节点识别正在复制的主节点进入客观下线后准备选举时间,选举在延迟551毫秒之后执行:

cat node-7006.log
2493:S 11 Jun 10:49:10.775 # Start of election delayed for 625 milliseconds (rank #0, offset 140).

  (7)延迟选举时间551毫秒后,从节点更新配置纪元并发起故障选举

cat node-7006.log
2493:S 11 Jun 10:49:11.480 # Starting a failover election for epoch 8.

  (8)7002和7003主节点为从节点7006投票:

cat node-7002.log
2469:M 11 Jun 10:49:11.482 # Failover auth granted to dc3d6eb37a053befdabbd3fa6898885a2d6bed90 for epoch 8

cat node-7003.log
2474:M 11 Jun 10:49:11.482 # Failover auth granted to dc3d6eb37a053befdabbd3fa6898885a2d6bed90 for epoch 8

  (9)从节点获取2个主节点投票后,超过半数执行替换主节点操作,从而完成故障转移

cat node-7006.log
2493:S 11 Jun 10:49:11.504 # Failover election won: I'm the new master.
2493:S 11 Jun 10:49:11.504 # configEpoch set to 8 after successful failover

  (10)从节点7006重置ID,并修改集群状态:

cat node-7006.log
2493:M 11 Jun 10:49:11.504 # Setting secondary replication ID to 86efad99696fb035d3eb62f304b7d4a6f4572b67, valid up to offset: 141. New replication ID is 6f4e8a586f16e3b85be84a495e3299466900179c
2493:M 11 Jun 10:49:11.504 * Discarding previously cached master state.
2493:M 11 Jun 10:49:11.504 # Cluster state changed: ok

  (11)查看集群状态,7006已经成为了主节点

192.168.131.132:7002> cluster nodes
f0c0bba3e13cccb529f4a9826fc10556c207da86 192.168.131.132:7002@17002 myself,master - 0 1528686428000 2 connected 5461-10922 [2109-<-7a19118645a20351fba3fbd93a12ee864ec0ffaf] [2233-<-7a19118645a20351fba3fbd93a12ee864ec0ffaf] [3030-<-7a19118645a20351fba3fbd93a12ee864ec0ffaf]
dcd2d62dd624b9d1cc483a5d8b92b977577d14ad 192.168.131.132:7005@17005 slave fea178a9eba33948ce255afcdde49796b84fb93a 0 1528686431000 5 connected
8fcab02224a1419c92db91350d81817fd47e8cc5 192.168.131.132:7004@17004 slave f0c0bba3e13cccb529f4a9826fc10556c207da86 0 1528686432760 2 connected
fea178a9eba33948ce255afcdde49796b84fb93a 192.168.131.132:7003@17003 master - 0 1528686431753 3 connected 10923-16383
dc3d6eb37a053befdabbd3fa6898885a2d6bed90 192.168.131.132:7006@17006 master - 0 1528686432000 8 connected 0-2108 2110-2232 2234-3029 3031-5460
7a19118645a20351fba3fbd93a12ee864ec0ffaf 192.168.131.132:7001@17001 master,fail - 1528685274545 1528685273742 1 disconnected 2109 2233 3030

  (11)成功完成故障转以后,对故障节点7001进行恢复,

root@bigjun:~# redis-server /home/redis/clusters/conf/node-7001.conf

  (12)故障节点7001重启后发现自己负责的槽指派给了另一个几点,则以现有集群配置为准,变为7006主节点的从节点:

cat node-7001.log
2597:M 11 Jun 11:09:21.649 # Configuration change detected. Reconfiguring myself as a replica of dc3d6eb37a053befdabbd3fa6898885a2d6bed90
2597:S 11 Jun 11:09:21.649 * Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
2597:S 11 Jun 11:09:21.649 # Cluster state changed: ok

  (13)集群内其他主节点和从节点接收到7001发来的ping消息,清空客观下线状态:

cat node-(7002 7003 7004 7005 7006).log
2469:M 11 Jun 11:09:21.709 * Clear FAIL state for node 7a19118645a20351fba3fbd93a12ee864ec0ffaf: is reachable again and nobody is serving its slots after some time.

  (14)7001节点变为从节点,对主节点7006发起复制流程

cat node-7001.log
2597:S 11 Jun 11:09:22.656 * Connecting to MASTER 192.168.131.132:7006
2597:S 11 Jun 11:09:22.656 * MASTER <-> SLAVE sync started
2597:S 11 Jun 11:09:22.656 * Non blocking connect for SYNC fired the event.
2597:S 11 Jun 11:09:22.656 * Master replied to PING, replication can continue...
2597:S 11 Jun 11:09:22.656 * Trying a partial resynchronization (request ed84c246b23fe3c0d1d743c057ee08a8900dbb6d:1).
2597:S 11 Jun 11:09:22.657 * Full resync from master: 6f4e8a586f16e3b85be84a495e3299466900179c:140
2597:S 11 Jun 11:09:22.657 * Discarding previously cached master state.
2597:S 11 Jun 11:09:22.714 * MASTER <-> SLAVE sync: receiving 222 bytes from master
2597:S 11 Jun 11:09:22.714 * MASTER <-> SLAVE sync: Flushing old data
2597:S 11 Jun 11:09:22.714 * MASTER <-> SLAVE sync: Loading DB in memory
2597:S 11 Jun 11:09:22.714 * MASTER <-> SLAVE sync: Finished with success

  (15)查看集群当前状态,可以看出7001确实变成了7006的从节点:

192.168.131.132:7002> cluster nodes
f0c0bba3e13cccb529f4a9826fc10556c207da86 192.168.131.132:7002@17002 myself,master - 0 1528687068000 2 connected 5461-10922 [2109-<-7a19118645a20351fba3fbd93a12ee864ec0ffaf] [2233-<-7a19118645a20351fba3fbd93a12ee864ec0ffaf] [3030-<-7a19118645a20351fba3fbd93a12ee864ec0ffaf]
dcd2d62dd624b9d1cc483a5d8b92b977577d14ad 192.168.131.132:7005@17005 slave fea178a9eba33948ce255afcdde49796b84fb93a 0 1528687070405 5 connected
8fcab02224a1419c92db91350d81817fd47e8cc5 192.168.131.132:7004@17004 slave f0c0bba3e13cccb529f4a9826fc10556c207da86 0 1528687068000 2 connected
fea178a9eba33948ce255afcdde49796b84fb93a 192.168.131.132:7003@17003 master - 0 1528687069400 3 connected 10923-16383
dc3d6eb37a053befdabbd3fa6898885a2d6bed90 192.168.131.132:7006@17006 master - 0 1528687067386 8 connected 0-2108 2110-2232 2234-3029 3031-5460
7a19118645a20351fba3fbd93a12ee864ec0ffaf 192.168.131.132:7001@17001 slave dc3d6eb37a053befdabbd3fa6898885a2d6bed90 0 1528687068391 8 connected

  七、集群运维  

  1.集群完整性

  为了保证集群完整性,默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。执行任何键命令返回(error)CLUSTERDOWNHash slot not served错误。这是对集群完整性的一种保护措施,保证所有的槽都指派给在线的节点。

  但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况,因此建议将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行,不会影响其他主节点的可用性。

  2.带宽消耗

  集群内Gossip消息通信本身会消耗带宽,官方建议集群最大规模在1000以内,也是出于对消息通信成本的考虑,因此单集群不适合部署超大规模的节点。在之前节点通信小节介绍到,集群内所有节点通过ping/pong消息彼此交换信息,节点间消息通信对带宽的消耗体现在以下几个方面:

  • 消息发送频率:跟cluster-node-timeout密切相关,当节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息。
  • 消息数据量:每个消息主要的数据占用包含:slots槽数组(2KB空间)和整个集群1/10的状态数据(10个节点状态数据约1KB)。
  • 节点部署的机器规模:机器带宽的上线是固定的,因此相同规模的集群分布的机器越多每台机器划分的节点越均匀,则集群内整体的可用带宽越高。

  例如,一个总节点数为200的Redis集群,部署在20台物理机上每台划分10个节点,cluster-node-timeout采用默认15秒,这时ping/pong消息占用带宽达到25Mb。如果把cluster-node-timeout设为20,对带宽的消耗降低到15Mb以下。
  集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗。

  因此搭建Redis集群时需要根据业务数据规模和消息通信成本做出合理规划:

  • 在满足业务需要的情况下尽量避免大集群。同一个系统可以针对不同业务场景拆分使用多套集群。这样每个集群既满足伸缩性和故障转移要求,还可以规避大规模集群的弊端。如笔者维护的一个推荐系统,根据数据特征使用了5个Redis集群,每个集群节点规模控制在100以内。
  • 适度提高cluster-node-timeout降低消息发送频率,同时cluster-node-timeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者的平衡。
  • 如果条件允许集群尽量均匀部署在更多机器上。避免集中部署,如集群有60个节点,集中部署在3台机器上每台部署20个节点,这时机器带宽消耗将非常严重。

  3.Pub/Sub广播问题

  Redis在2.0版本提供了Pub/Sub(发布/订阅)功能,用于针对频道实现消息的发布和订阅。但是在集群模式下内部实现对所有的publish命令都会向所有的节点进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重带宽负担。

  

  利用现有集群演示Pub/Sub广播问题,

  (1)对集群所有主从节点执行subscribe命令订阅cluster_pub_spread频道,用于验证集群是否广播消息:

192.168.131.132:7001> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1

192.168.131.132:7002> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1

192.168.131.132:7003> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1

192.168.131.132:7004> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1

192.168.131.132:7005> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1

192.168.131.132:7006> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1

  (2)在7001节点上发布频道为cluster_pub_spread的消息

192.168.131.132:7001> publish cluster_pub_spread message_body_1
(integer) 1

  (3)集群内所有订阅客户端全部收到了消息

192.168.131.132:7001> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1
1) "message"
2) "cluster_pub_spread"
3) "message_body_1"

192.168.131.132:7002> subscribe cluster_pub_spread
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cluster_pub_spread"
3) (integer) 1
1) "message"
2) "cluster_pub_spread"
3) "message_body_1"

...

  针对集群模式下publish广播问题,需要引起开发人员注意,当频繁应用Pub/Sub功能时应该避免在大量节点的集群内使用,否则会严重消耗集群内网络带宽。

  针对这种情况建议使用sentinel结构专门用于Pub/Sub功能,从而规避这一问题。

  4.集群倾斜

  集群倾斜指不同节点之间数据量和请求量出现明显差异,这种情况将加大负载均衡和开发运维的难度。

  (1)数据倾斜

  • 节点和槽分配严重不均。
  • 不同槽对应键数量差异过大。
  • 集合对象包含大量元素。
  • 内存相关配置不一致

  (2)请求倾斜

  集群内特定节点请求量/流量过大将导致节点之间负载不均,影响集群均衡和运维成本。常出现在热点键场景,当键命令消耗较低时如小对象的get、set、incr等,即使请求量差异较大一般也不会产生负载严重不均。但是当热点键对应高算法复杂度的命令或者是大对象操作如hgetall、smembers等,会导致对应节点负载过高的情况。

  避免方式如下:

  • 合理设计键,热点大集合对象做拆分或使用hmget替代hgetall避免整体读取。
  • 不要使用热键作为hash_tag,避免映射到同一槽。
  • 对于一致性要求不高的场景,客户端可使用本地缓存减少热键调用。

  5.集群读写分离

  (1)只读连接

  集群模式下从节点不接受任何读写请求,发送过来的键命令会重定向到负责槽的主节点上(其中包括它的主节点)。当需要使用从节点分担主节点读压力时,可以使用readonly命令打开客户端连接只读状态。之前的复制配置slave-read-only在集群模式下无效。

  当开启只读状态时,从节点接收读命令处理流程变为:如果对应的槽属于自己正在复制的主节点则直接执行读命令,否则返回重定向信息。

查看集群状态(7001是7006的从节点):
192.168.131.132:7001> cluster nodes
f0c0bba3e13cccb529f4a9826fc10556c207da86 192.168.131.132:7002@17002 master - 0 1528688487535 2 connected 5461-10922
fea178a9eba33948ce255afcdde49796b84fb93a 192.168.131.132:7003@17003 master - 0 1528688486000 3 connected 10923-16383
dc3d6eb37a053befdabbd3fa6898885a2d6bed90 192.168.131.132:7006@17006 master - 0 1528688486528 8 connected 0-5460
dcd2d62dd624b9d1cc483a5d8b92b977577d14ad 192.168.131.132:7005@17005 slave fea178a9eba33948ce255afcdde49796b84fb93a 0 1528688488539 5 connected
8fcab02224a1419c92db91350d81817fd47e8cc5 192.168.131.132:7004@17004 slave f0c0bba3e13cccb529f4a9826fc10556c207da86 0 1528688487000 4 connected
7a19118645a20351fba3fbd93a12ee864ec0ffaf 192.168.131.132:7001@17001 myself,slave dc3d6eb37a053befdabbd3fa6898885a2d6bed90 0 1528688487000 1 connected

默认连接状态为普通客户端:flags=N
192.168.131.132:7001> client list
id=4 addr=192.168.131.132:7006 fd=20 name= age=11457 idle=8 flags=M db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
id=7 addr=192.168.131.132:49880 fd=7 name= age=9534 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
主从节点复制后,从节点和主节点内容一致
192.168.131.132:7001> keys *
1) "Hello"
2) "test:7"
3) "test:3"
使用get时,命令重定向到主节点
192.168.131.132:7001> get Hello
(error) MOVED 3030 192.168.131.132:7006
打开当前连接客户端只读状态
192.168.131.132:7001> readonly
OK
客户端连接状态变为:flags=r
192.168.131.132:7001> client list
id=4 addr=192.168.131.132:7006 fd=20 name= age=11577 idle=7 flags=M db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
id=7 addr=192.168.131.132:49880 fd=7 name= age=9654 idle=0 flags=r db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
执行get操作可以相应读命令
192.168.131.132:7001> get Hello
"world"
执行readwrite可以关闭只读连接状态
192.168.131.132:7001> readwrite
OK
192.168.131.132:7001> client list
id=4 addr=192.168.131.132:7006 fd=20 name= age=11687 idle=6 flags=M db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
id=7 addr=192.168.131.132:49880 fd=7 name= age=9764 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

  (2)读写分离

  集群模式下的读写分离,同样会遇到:复制延迟,读取过期数据,从节点故障等问题,具体细节见6.5复制运维小节。针对从节点故障问题,客户端需要维护可用节点列表,集群提供了cluster slaves{nodeId}命令,返回nodeId对应主节点下所有从节点信息,数据格式同cluster nodes。

192.168.131.132:7002> cluster slaves dc3d6eb37a053befdabbd3fa6898885a2d6bed90
1) "7a19118645a20351fba3fbd93a12ee864ec0ffaf 192.168.131.132:7001@17001 slave dc3d6eb37a053befdabbd3fa6898885a2d6bed90 0 1528698463371 8 connected"

  解析以上从节点列表信息,排除fail状态节点,这样客户端对从节点的故障判定可以委托给集群处理,简化维护可用从节点列表难度。

  集群模式下读写分离成本比较高,可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离。

  集群读写分离有时用于特殊业务场景如:

  • 利用复制的最终一致性使用多个从节点做跨机房部署降低读命令网络延迟。
  • 主节点故障转移时间过长,业务端把读请求路由给从节点保证读操作可用。

  以上场景也可以在不同机房独立部署Redis集群解决,通过客户端多写来维护,读命令直接请求到最近机房的Redis集群,或者当一个集群节点故障时客户端转向另一个集群。

  6.手动故障转移

  Redis集群提供了手动故障转移功能:指定从节点发起转移流程,主从节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为它的从节点。

  

  在从节点上执行cluster failover命令发起转移流程,默认情况下转移期间客户端请求会有短暂的阻塞,但不会丢失数据,流程如下:

  • 从节点通知主节点停止处理所有客户端请求。
  • 主节点发送对应从节点延迟复制的数据。
  • 从节点接收处理复制延迟的数据,直到主从复制偏移量一致为止,保证复制数据不丢失。
  • 从节点立刻发起投票选举(这里不需要延迟触发选举)。选举成功后断开复制变为新的主节点,之后向集群广播主节点pong消息。
  • 旧主节点接受到消息后更新自身配置变为从节点,解除所有客户端请求阻塞,这些请求会被重定向到新主节点上执行。
  • 旧主节点变为从节点后,向新的主节点发起全量复制流程。

  7.数据迁移

  应用Redis集群时,常需要把单机Redis数据迁移到集群环境。redis-trib.rb工具提供了导入功能,用于数据从单机向集群环境迁移的场景,命令如下:

redis-trib.rb import host:port --from <arg> --copy --replace

  redis-trib.rb import命令内部采用批量scan和migrate的方式迁移数据。这种迁移方式存在以下缺点:

  • 迁移只能从单机节点向集群环境导入数据。
  • 不支持在线迁移数据,迁移数据时应用方必须停写,无法平滑迁移数据。
  • 迁移过程中途如果出现超时等错误,不支持断点续传只能重新全量导入。
  • 使用单线程进行数据迁移,大数据量迁移速度过慢。

  正因为这些问题,社区开源了很多迁移工具,这里推荐一款唯品会开发的redis-migrate-tool,该工具可满足大多数Redis迁移需求,特点如下:

  • 支持单机、Twemproxy、Redis Cluster、RDB/AOF等多种类型的数据迁移。
  • 工具模拟成从节点基于复制流迁移数据,从而支持在线迁移数据,业务方不需要停写。
  • 采用多线程加速数据迁移过程且提供数据校验和查看迁移状态等功能。

转载于:https://www.cnblogs.com/BigJunOba/p/9136332.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值