Redis学习(四):Redis 集群

1. Redis Cluster

1. 呼唤集群

1. 并发量

  • 10万/每秒 -> 业务需要100万/每秒呢?

2. 数据量

  • 机器内存:16~256G -> 业务需要500G呢?

3. 解决方法

  • 配置"强悍"的机器:超大内存,牛X CPU等。
  • 正确的解决方法:分布式:简单的认为加机器。

2. 数据分布

1. 分布式数据库 - 数据分区

  • 顺序分区
    • 特点:
      • 数据分散度易倾斜
      • 键值业务相关
      • 可顺序访问
      • 不支持批量操作
    • 典型产品:
      • BigTable
      • HBase
  • 哈希分布(例如节点取模)
    • 特点:
      • 数据分散度高
      • 键值分布业务无关
      • 无法顺序访问
      • 支持批量操作
    • 典型产品:
      • 一致性哈希Memcache
      • Redis Cluster
      • 其他缓存产品
  • 哈希分区种类:
    • 节点取余分区
    • 一致性哈希分区
    • 虚拟槽分区

2. 节点取余

  • 优势:非常简单。
  • 客户端分片:哈希 + 取余。
  • 节点伸缩:数据节点关系变化,导致数据迁移。
  • 迁移数量和添加节点数量有关:建议翻倍扩容。

3. 一致性哈希

  • 客户端分片:哈希 + 顺时针(优化取余)。
  • 节点伸缩:只影响临近节点,但是还是有数据迁移。
  • 翻倍伸缩:保证最小迁移数据和负载均衡。

4. 虚拟槽分区

  • 预设虚拟槽:每个槽映射一个数据子集,一般比节点数大。
  • 良好的哈希函数:例如CRC16。
  • 服务端管理节点,槽,数据:例如Redis Cluster。

虚拟槽分配

3. 搭建集群

1. 基本架构

  • 分布式情况下,节点之间是相互通信的。
  • Redis Cluster架构
    • 节点:cluster-enabled:yes(是否是一个集群模式)
    • meet:A向B发送meet B返回pong,说明A和B已经进行消息的交流。如果A和C之前也是通的,说明B和C之间也应该能够交换消息(所有节点共享消息)
    • 指派槽:为了负载均衡,会给每个节点指派槽
    • 复制
  • Redis Cluster特性
    • 复制
    • 高可用
    • 分片

4. Redis Cluster安装配置

1. 原生命令安装 - 理解架构

  • 配置开启节点
port ${port}
daemonize yes
dir "/home/redis/data"
dbfilename "sump-${port}.rdb"
logfile "${port}.log"
cluster-enabled yes
cluster-config-file nodes-${port}.conf
  • meet
redis-cli -h 192.168.242.129 -p 7000 cluster meet 192.168.242.129 7001
redis-cli -h 192.168.242.129 -p 7000 cluster meet 192.168.242.129 7002
redis-cli -h 192.168.242.129 -p 7000 cluster meet 192.168.242.129 7003
redis-cli -h 192.168.242.129 -p 7000 cluster meet 192.168.242.129 7004
redis-cli -h 192.168.242.129 -p 7000 cluster meet 192.168.242.129 7005
  • Cluster节点主要配置
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file "nodes.conf"
cluster-require-full-coverage yes
  • 指派槽
cluster addslots slot [slot ...]

redis-cli -h 192.168.242.129 -p 7000 cluster addslots {0...5461}
redis-cli -h 192.168.242.129 -p 7000 cluster addslots {5462...10922}
redis-cli -h 192.168.242.129 -p 7000 cluster addslots {10923...16383}
  • 主从
cluster replicate node-id

redis-cli -h 192.168.242.129 -p 7003 cluster replicate {node-id-7000}
redis-cli -h 192.168.242.129 -p 7004 cluster replicate {node-id-7001}
redis-cli -h 192.168.242.129 -p 7005 cluster replicate {node-id-7002}

2. 搭建集群 - 具体安装

  • 添加7000节点配置
protected-mode no
port 7000
daemonize yes
dir "/home/redis/data"
logfile "7000.log"
dbfilename "dump-7000.rdb"
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-require-full-coverage no
  • 使用sed快速生成其他配置文件
[root@localhost config]# sed 's/7000/7001/g' redis-7000.conf > redis-7001.conf 
[root@localhost config]# sed 's/7000/7002/g' redis-7000.conf > redis-7002.conf 
[root@localhost config]# sed 's/7000/7003/g' redis-7000.conf > redis-7003.conf 
[root@localhost config]# sed 's/7000/7004/g' redis-7000.conf > redis-7004.conf 
[root@localhost config]# sed 's/7000/7005/g' redis-7000.conf > redis-7005.conf 
  • 启动所有节点并查看进程
[root@localhost redis]# redis-server config/redis-7000.conf 
[root@localhost redis]# redis-server config/redis-7001.conf 
[root@localhost redis]# redis-server config/redis-7002.conf 
[root@localhost redis]# redis-server config/redis-7003.conf 
[root@localhost redis]# redis-server config/redis-7004.conf 
[root@localhost redis]# redis-server config/redis-7005.conf 
[root@localhost redis]# ps -ef | grep redis-server
root       5034      1  0 18:43 ?        00:00:00 redis-server *:7000 [cluster]
root       5050      1  0 18:44 ?        00:00:00 redis-server *:7001 [cluster]
root       5058      1  0 18:44 ?        00:00:00 redis-server *:7002 [cluster]
root       5066      1  0 18:44 ?        00:00:00 redis-server *:7003 [cluster]
root       5074      1  0 18:44 ?        00:00:00 redis-server *:7004 [cluster]
root       5082      1  0 18:44 ?        00:00:00 redis-server *:7005 [cluster]
  • 我们登录一个节点并输入命令,发现系统提示集群属于下线状态
[root@localhost redis]# redis-cli -p 7000
127.0.0.1:7000> set hello world
(error) CLUSTERDOWN Hash slot not served
  • 我们可以简单看一下集群的信息,我们还没有分配槽
[root@localhost redis]# redis-cli -p 7000 cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:1
cluster_size:0
cluster_current_epoch:0
cluster_my_epoch:0
cluster_stats_messages_sent:0
cluster_stats_messages_received:0
  • 搭建集群 - meet
[root@localhost redis]# redis-cli -p 7000 cluster meet 192.168.242.129 7001
OK
[root@localhost redis]# redis-cli -p 7000 cluster nodes
53c2b87482d2e87e581d12553c4f5623113144f3 192.168.242.129:7001@17001 master - 0 1514026353438 1 connected
7a646c22385df7aeda43946777d2d2bc7f3b7315 192.168.242.129:7000@17000 myself,master - 0 0 0 connected
  • 发现7000已经和7001发生了握手,继续执行命令,最后我们发现7005能发现其他节点,说明他们都是互通的
[root@localhost redis]# redis-cli -p 7005 cluster nodes
d57b719f47a276d4b011c7b4c9a5ef7c03d97ad3 192.168.242.129:7003@17003 master - 0 1514026441443 0 connected
1c718959f6082ab013e7c4e17b5773d5c00e8ffe 192.168.242.129:7005@17005 myself,master - 0 1514026440000 5 connected
7a646c22385df7aeda43946777d2d2bc7f3b7315 192.168.242.129:7000@17000 master - 0 1514026442000 3 connected
c0a7b5ebf298b447f2a9decb198febbde35209d0 192.168.242.129:7004@17004 master - 0 1514026443455 4 connected
11c1f71f0b7840c9e6e031ca41366bca55910d98 192.168.242.129:7002@17002 master - 0 1514026441000 2 connected
53c2b87482d2e87e581d12553c4f5623113144f3 192.168.242.129:7001@17001 master - 0 1514026442450 1 connected
  • 搭建集群 - 分配槽
  • 我们不可能一个一个分配槽,编写一个简单的脚本进行分配
start=$1
end=$2
port=$3
for slot in `seq ${start} ${end}`
do
    echo "slot:${slot}"
    redis-cli -h 192.168.242.129 -p ${port} cluster addslots ${slot}
done
  • 执行脚本,发现槽分配完成
[root@localhost script]# sh addslots.sh 0 5461 7000

127.0.0.1:7000> cluster nodes
11c1f71f0b7840c9e6e031ca41366bca55910d98 192.168.242.129:7002@17002 master - 0 1514027407209 2 connected
7a646c22385df7aeda43946777d2d2bc7f3b7315 192.168.242.129:7000@17000 myself,master - 0 1514027404000 3 connected 0-5461
  • 继续分配其他两个节点
[root@localhost script]# sh addslots.sh 5462 10922 7001
[root@localhost script]# sh addslots.sh 10923 16383 7002
  • 我们看一下集群的状态,集群状态是OK的,而且槽都是成功分配的
[root@localhost script]# redis-cli -p 7000 cluster info
cluster_state: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:3
cluster_stats_messages_ping_sent:1301
cluster_stats_messages_pong_sent:1287
cluster_stats_messages_meet_sent:5
cluster_stats_messages_sent:2593
cluster_stats_messages_ping_received:1287
cluster_stats_messages_pong_received:1306
cluster_stats_messages_received:2593
  • 搭建集群 - 主从分配
  • 7003是7000从节点,执行cluster replicate
[root@localhost script]# redis-cli -p 7000 cluster nodes
11c1f71f0b7840c9e6e031ca41366bca55910d98 192.168.242.129:7002@17002 master - 0 1514027878252 2 connected 10923-16383
7a646c22385df7aeda43946777d2d2bc7f3b7315 192.168.242.129:7000@17000 myself,master - 0 1514027878000 3 connected 0-5461
53c2b87482d2e87e581d12553c4f5623113144f3 192.168.242.129:7001@17001 master - 0 1514027879267 1 connected 5462-10922
c0a7b5ebf298b447f2a9decb198febbde35209d0 192.168.242.129:7004@17004 master - 0 1514027879000 4 connected
d57b719f47a276d4b011c7b4c9a5ef7c03d97ad3 192.168.242.129:7003@17003 master - 0 1514027880275 0 connected
1c718959f6082ab013e7c4e17b5773d5c00e8ffe 192.168.242.129:7005@17005 master - 0 1514027878000 5 connected

[root@localhost script]#  redis-cli -h 192.168.242.129 -p 7003 cluster replicate 7a646c22385df7aeda43946777d2d2bc7f3b7315
OK
[root@localhost script]#  redis-cli -h 192.168.242.129 -p 7004 cluster replicate 53c2b87482d2e87e581d12553c4f5623113144f3
OK
[root@localhost script]#  redis-cli -h 192.168.242.129 -p 7005 cluster replicate 11c1f71f0b7840c9e6e031ca41366bca55910d98
OK
  • 看一下节点状态,发现7003,7004,7005分别是7000,7001,7002的从节点
[root@localhost script]# redis-cli -p 7000 cluster nodes
11c1f71f0b7840c9e6e031ca41366bca55910d98 192.168.242.129:7002@17002 master - 0 1514028084077 2 connected 10923-16383
7a646c22385df7aeda43946777d2d2bc7f3b7315 192.168.242.129:7000@17000 myself,master - 0 1514028083000 3 connected 0-5461
53c2b87482d2e87e581d12553c4f5623113144f3 192.168.242.129:7001@17001 master - 0 1514028083063 1 connected 5462-10922
c0a7b5ebf298b447f2a9decb198febbde35209d0 192.168.242.129:7004@17004 slave 53c2b87482d2e87e581d12553c4f5623113144f3 0 1514028082054 4 connected
d57b719f47a276d4b011c7b4c9a5ef7c03d97ad3 192.168.242.129:7003@17003 slave 7a646c22385df7aeda43946777d2d2bc7f3b7315 0 1514028081044 3 connected
1c718959f6082ab013e7c4e17b5773d5c00e8ffe 192.168.242.129:7005@17005 slave 11c1f71f0b7840c9e6e031ca41366bca55910d98 0 1514028082000 5 connected
  • 通过cluster slots查看信息
[root@localhost script]# redis-cli -p 7000 cluster slots
1) 1) (integer) 10923
    2) (integer) 16383
    3) 1) "192.168.242.129"
      2) (integer) 7002
      3) "11c1f71f0b7840c9e6e031ca41366bca55910d98"
    4) 1) "192.168.242.129"
      2) (integer) 7005
      3) "1c718959f6082ab013e7c4e17b5773d5c00e8ffe"
2) 1) (integer) 0
    2) (integer) 5461
    3) 1) "192.168.242.129"
      2) (integer) 7000
      3) "7a646c22385df7aeda43946777d2d2bc7f3b7315"
    4) 1) "192.168.242.129"
      2) (integer) 7003
      3) "d57b719f47a276d4b011c7b4c9a5ef7c03d97ad3"
3) 1) (integer) 5462
    2) (integer) 10922
    3) 1) "192.168.242.129"
      2) (integer) 7001
      3) "53c2b87482d2e87e581d12553c4f5623113144f3"
    4) 1) "192.168.242.129"
      2) (integer) 7004
      3) "c0a7b5ebf298b447f2a9decb198febbde35209d0"
  • 启动节点,发现可以写入数据了,集群安装成功
[root@localhost redis]# redis-cli -c -p 7000
127.0.0.1:7000> set hello world
OK

2. 官方工具安装

  • redis cluster官方提供了ruby的安装脚本

  • Ruby环境准备

    • 下载,编译,安装Ruby
    • 安装rubygem redis
    • 安装redis-trib.rb
  • 下载ruby

[root@localhost Thpffcj]# wget https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
  • 解压缩,编译
[root@localhost Thpffcj]# tar -xvf ruby-2.3.1.tar.gz 

[root@localhost ruby-2.3.1]# ./configure -prefix=/usr/local/ruby

[root@localhost ruby-2.3.1]# make && make install

[root@localhost ruby-2.3.1]# cd /usr/local/ruby/
[root@localhost ruby]# cp bin/ruby /usr/local/bin
[root@localhost ruby]# cp bin/gem /usr/local/bin

[root@localhost ruby-2.3.1]# ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]
  • 安装客户端
[root@localhost Thpffcj]# wget http://rubygems.org/downloads/redis-3.3.0.gem

[root@localhost Thpffcj]# gem install -l redis-3.3.0.gem 

[root@localhost Thpffcj]# gem list -- check redis gem
  • 遇见无法看懂的bug:https://stackoverflow.com/questions/27378519/gem-install-error-related-to-missing-library

  • 通过redis-trib.rb搭建集群

  • 启动各个节点,这里我为了学习方便,采用视频中使用的3.0.7版本,去掉配置文件中protected-mode no(3.2版本出现)

[root@localhost src]# cp redis-trib.rb /usr/local/bin

[root@localhost redis-3.0.7]# redis-server config/redis-7000.conf 
[root@localhost redis-3.0.7]# redis-server config/redis-7001.conf 
[root@localhost redis-3.0.7]# redis-server config/redis-7002.conf 
[root@localhost redis-3.0.7]# redis-server config/redis-7003.conf 
[root@localhost redis-3.0.7]# redis-server config/redis-7004.conf 
[root@localhost redis-3.0.7]# redis-server config/redis-7005.conf 
  • 执行命令
[root@localhost src]# ./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
  • 它会返回一个执行计划
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
127.0.0.1:7000
127.0.0.1:7001
127.0.0.1:7002
Adding replica 127.0.0.1:7003 to 127.0.0.1:7000
Adding replica 127.0.0.1:7004 to 127.0.0.1:7001
Adding replica 127.0.0.1:7005 to 127.0.0.1:7002
M: a6058b01c21ca99ffcdd18a38b14ab03330cf933 127.0.0.1:7000
    slots:0-5460 (5461 slots) master
M: 9b90c1640f92e2bda9ba5c902ffdd4227d9344d0 127.0.0.1:7001
    slots:5461-10922 (5462 slots) master
M: 517410e8513cf74fca20cfdd9691cc397bf034bc 127.0.0.1:7002
    slots:10923-16383 (5461 slots) master
S: ac2b1c40f9ad64264a54055771dec751f049409c 127.0.0.1:7003
    replicates a6058b01c21ca99ffcdd18a38b14ab03330cf933
S: 71d65e320723bcbda60f9e6161a31ebc74e4e369 127.0.0.1:7004
    replicates 9b90c1640f92e2bda9ba5c902ffdd4227d9344d0
S: 0517e201b0d8a9bf71a3bf5f242bd0f66e33145b 127.0.0.1:7005
    replicates 517410e8513cf74fca20cfdd9691cc397bf034bc
Can I set the above configuration? (type 'yes' to accept): 
  • 完成构建集群

3. 总结

  • 原生命令安装
    • 理解Redis Cluster架构
    • 生产环境不使用
  • 官方工具安装
    • 高效,准确
    • 生产环境可以使用
  • 其他
    • 可视化部署

2. 深入Redis Cluster

1. 集群伸缩

1. 伸缩原理

  • 集群伸缩:槽和数据在节点之间的移动

2. 扩展集群

  • 准备新节点
    • 集群模式
    • 配置和其他节点统一
    • 启动后是孤儿节点
  • 加入集群
    • meet
    • 作用:
      • 为它迁移槽和数据实现扩容
      • 作为从节点负责故障转移
    • 建议使用redis-trib.rb能够避免新节点已经加入其他集群,造成故障
  • 迁移槽和数据
    • 槽迁移计划
      • 重新平均槽数量
      • 每个槽拿出一定数量到新节点
    • 迁移数据
      • 对目标节点发送:cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据
      • 对源节点发送:cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽的数据
      • 源节点循环执行:cluster getKeysinslot {slot} {count}命令,每次获取count个属于槽的键
      • 在源节点执行:migrate {targetIp} {targetPort} key 0 {timeout}命令把指定key迁移
      • 重复执行步骤3~4直到槽下所有的键数据迁移到目标节点
      • 向集群内所有主节点发送:cluster setslot {slot} node {targetnodeId}命令,通知槽分配给目标节点
      • pipeline migrate(since 3.0.6 3.2.8 fix)
    • 添加从节点

3. 集群扩容演示

[root@localhost redis]# redis-cli -p 7000 cluster nodes
244168ea672d881d6e1eee4a50afcea27dc65fb8 192.168.242.129:7004@17004 slave 5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 0 1514185802000 4 connected
63adbbda48fcf0a3de92082eb771ca0640989d5e 192.168.242.129:7000@17000 myself,master - 0 1514185804000 2 connected 1-5461
5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 192.168.242.129:7001@17001 master - 0 1514185807027 1 connected 5462-10922
2f6a0261427a8ffafe4262928ab59d920290c4e8 192.168.242.129:7005@17005 slave c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 0 1514185804000 5 connected
09e74cda7aa87aa73b42ec0e63d9f514a49ad9f4 192.168.242.129:7003@17003 slave 63adbbda48fcf0a3de92082eb771ca0640989d5e 0 1514185806015 3 connected
c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 192.168.242.129:7002@17002 master - 0 1514185805000 0 connected 10923-16383
  • 我们现在是三主三从的模式,现在要加入7006和7007,7007是7006的从,要从7000,7001,7002迁移一部分数据给7006
[root@localhost config]# sed 's/7000/7006/g' redis-7000.conf > redis-7006.conf 
[root@localhost config]# sed 's/7000/7007/g' redis-7000.conf > redis-7007.conf 

[root@localhost redis]# redis-server config/redis-7006.conf 
[root@localhost redis]# redis-server config/redis-7007.conf 
  • 将这两个节点加入集群
[root@localhost redis]# redis-cli -p 7000 cluster meet 192.168.242.129 7006
OK
[root@localhost redis]# redis-cli -p 7000 cluster meet 192.168.242.129 7007
OK
  • 查看节点信息并添加主从关系
[root@localhost redis]# redis-cli -p 7000 cluster nodes
244168ea672d881d6e1eee4a50afcea27dc65fb8 192.168.242.129:7004@17004 slave 5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 0 1514186155000 4 connected
63adbbda48fcf0a3de92082eb771ca0640989d5e 192.168.242.129:7000@17000 myself,master - 0 1514186154000 2 connected 1-5461
5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 192.168.242.129:7001@17001 master - 0 1514186156636 1 connected 5462-10922
a307acd083ce621cacca6757df8034d2b869ce86 192.168.242.129:7006@17006 master - 0 1514186154000 6 connected
2f6a0261427a8ffafe4262928ab59d920290c4e8 192.168.242.129:7005@17005 slave c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 0 1514186154446 5 connected
6461b46234a4ff7c0a02589964fabcd172aa9f74 192.168.242.129:7007@17007 master - 0 1514186155631 7 connected
09e74cda7aa87aa73b42ec0e63d9f514a49ad9f4 192.168.242.129:7003@17003 slave 63adbbda48fcf0a3de92082eb771ca0640989d5e 0 1514186155000 3 connected
c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 192.168.242.129:7002@17002 master - 0 1514186153440 0 connected 10923-16383

[root@localhost redis]# redis-cli -h 192.168.242.129 -p 7007 cluster replicate a307acd083ce621cacca6757df8034d2b869ce86
OK
  • 使用redis-trib.rb
[root@localhost redis]# src/redis-trib.rb reshard 192.168.242.129:7000
...
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 
  • 我们选择移动4096个槽去7006,并选择从所有节迁移
How many slots do you want to move (from 1 to 16384)? 4096
What is the receiving node ID? a307acd083ce621cacca6757df8034d2b869ce86
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:all
  • 我们看到迁移速度还是比较快的,实际上槽里有数据的话可以速度会慢一些

  • 我们看见7006节点是由三部分组成的

[root@localhost redis]# redis-cli -p 7000 cluster nodes | grep master
63adbbda48fcf0a3de92082eb771ca0640989d5e 192.168.242.129:7000@17000 myself,master - 0 1514187027000 2 connected 1366-5461
5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 192.168.242.129:7001@17001 master - 0 1514187029843 1 connected 6827-10922
a307acd083ce621cacca6757df8034d2b869ce86 192.168.242.129:7006@17006 master - 0 1514187028000 8 connected 0-1365 5462-6826 10923-12287
c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 192.168.242.129:7002@17002 master - 0 1514187027000 0 connected 12288-16383

3. 收缩集群

  • 下线迁移槽
  • 忘记节点
    • redis->cli > cluster forget {downNodeId}
  • 关闭节点

4. 集群收缩演示

  • 我们希望将7006和7007进行下线
[root@localhost redis]# redis-cli -p 7000 cluster nodes
244168ea672d881d6e1eee4a50afcea27dc65fb8 192.168.242.129:7004@17004 slave 5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 0 1514198031000 4 connected
5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 192.168.242.129:7001@17001 master - 0 1514198033155 1 connected 6827-10922
63adbbda48fcf0a3de92082eb771ca0640989d5e 192.168.242.129:7000@17000 myself,master - 0 1514198031000 2 connected 1366-5461
6461b46234a4ff7c0a02589964fabcd172aa9f74 192.168.242.129:7007@17007 slave a307acd083ce621cacca6757df8034d2b869ce86 0 1514198032146 8 connected
a307acd083ce621cacca6757df8034d2b869ce86 192.168.242.129:7006@17006 master - 0 1514198030000 8 connected 0-1365 5462-6826 10923-12287
09e74cda7aa87aa73b42ec0e63d9f514a49ad9f4 192.168.242.129:7003@17003 slave 63adbbda48fcf0a3de92082eb771ca0640989d5e 0 1514198032000 3 connected
c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 192.168.242.129:7002@17002 master - 0 1514198030000 0 connected 12288-16383
2f6a0261427a8ffafe4262928ab59d920290c4e8 192.168.242.129:7005@17005 slave c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 0 1514198032000 5 connected
  • 进行槽转移
[root@localhost redis]# src/redis-trib.rb reshard --from a307acd083ce621cacca6757df8034d2b869ce86 --to 63adbbda48fcf0a3de92082eb771ca0640989d5e --slots 1366 192.168.242.129:7006
[root@localhost redis]# src/redis-trib.rb reshard --from a307acd083ce621cacca6757df8034d2b869ce86 --to 5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 --slots 1365 192.168.242.129:7006
[root@localhost redis]# src/redis-trib.rb reshard --from a307acd083ce621cacca6757df8034d2b869ce86 --to c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 --slots 1365 192.168.242.129:7006
  • 现在7006上已经没有任何槽了,现在我们要进行遗忘
[root@localhost redis]# src/redis-trib.rb del-node 192.168.242.129:7000 6461b46234a4ff7c0a02589964fabcd172aa9f74
[root@localhost redis]# src/redis-trib.rb del-node 192.168.242.129:7000 a307acd083ce621cacca6757df8034d2b869ce86
  • 发现两个节点已经不属于集群了
[root@localhost redis]# redis-cli -p 7000 cluster nodes244168ea672d881d6e1eee4a50afcea27dc65fb8 192.168.242.129:7004@17004 slave 5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 0 1514199050672 10 connected
5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1 192.168.242.129:7001@17001 master - 0 1514199051679 10 connected 5462-10922
63adbbda48fcf0a3de92082eb771ca0640989d5e 192.168.242.129:7000@17000 myself,master - 0 1514199050000 9 connected 0-5461
09e74cda7aa87aa73b42ec0e63d9f514a49ad9f4 192.168.242.129:7003@17003 slave 63adbbda48fcf0a3de92082eb771ca0640989d5e 0 1514199049661 9 connected
c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 192.168.242.129:7002@17002 master - 0 1514199048000 11 connected 10923-16383
2f6a0261427a8ffafe4262928ab59d920290c4e8 192.168.242.129:7005@17005 slave c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2 0 1514199049000 11 connected

2. 客户端路由

1. moved 重定向

[root@localhost redis]# redis-cli -c -p 7000
127.0.0.1:7000> cluster keyslot hello
(integer) 866
127.0.0.1:7000> set hello world
OK
127.0.0.1:7000> cluster keyslot php
(integer) 9244
127.0.0.1:7000> set php best
-> Redirected to slot [9244] located at 192.168.242.129:7001
OK

2. ask 重定向

  • moved和ask
    • 两者都是客户端重定向
    • moved:槽已经确定迁移
    • ask:槽还在迁移中

3. smart 客户端

  • smart客户端实现原理
    • 追求性能
      • 从集群中选一个可运行的节点,使用cluster slots初始化槽和节点映射。
      • 将cluster slots的结果映射到本地,为每个节点创建JedisPool。
      • 准备执行命令。

执行命令

  • smart客户端使用:JedisCluster

    • 单例:内置了所有节点的连接池
    • 无需手动借还连接池
    • 合理设置commons-pool
  • spring整合redis

  • 写一个jediscluster工厂类

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
  * Created by Thpffcj on 2017/12/25.
  */
public class JedisClusterFactory {

    private JedisCluster jedisCluster;

    private List<String> hostPortList;

    private int timeout;

    public void init() {
        // 这里可以设置相关参数
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        Set<HostAndPort> nodeSet = new HashSet<>();
        for (String hostPort : hostPortList) {
            String[] arr = hostPort.split(":");
            if (arr.length != 2) {
                continue;
            }
            nodeSet.add(new HostAndPort(arr[0], Integer.parseInt(arr[1])));
        }
        jedisCluster = new JedisCluster(nodeSet, timeout, jedisPoolConfig);
    }

    public void destory() {
        if (jedisCluster != null) {
            try {
                jedisCluster.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public JedisCluster getJedisCluster() {
        return jedisCluster;
    }

    public void setHostPortList(List<String> hostPortList) {
        this.hostPortList = hostPortList;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
}
  • 随便写一个spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="jedisClusterFactory" class="JedisClusterFactory"
          init-method="init" destroy-method="destory">
        <property name="hostPortList">
            <list>
                <value>192.168.242.129:7000</value>
                <value>192.168.242.129:7001</value>
                <value>192.168.242.129:7002</value>
                <value>192.168.242.129:7003</value>
                <value>192.168.242.129:7004</value>
                <value>192.168.242.129:7005</value>
            </list>
        </property>
        <property name="timeout" value="1000" />
    </bean>

    <bean id="jedisCluster" factory-bean="jedisClusterFactory"
          factory-method="getJedisCluster">
    </bean>

    <bean id="redisClusterService" class="RedisClusterImpl"/>
</beans>
  • 添加service层
import redis.clients.jedis.JedisCluster;

import javax.annotation.Resource;

/**
  * Created by Thpffcj on 2017/12/25.
  */
public class RedisClusterImpl implements RedisService {

    @Resource(name = "jedisCluster")
    private JedisCluster jedisCluster;

    @Override
    public String set(String key, String value) {
        return jedisCluster.set(key, value);
    }

    @Override
    public String get(String key) {
        return jedisCluster.get(key);
    }
}
  • 添加测试用例
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertNotNull;

/**
  * Created by Thpffcj on 2017/12/25.
  */
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring配置文件
@ContextConfiguration({ "classpath:spring-test.xml"})
public class JedisClusterTest {

    @Resource(name = "redisClusterService")
    private RedisService redisClusterService;

    @Test
    public void testNotNull() {
        assertNotNull(redisClusterService);
        redisClusterService.set("hello", "world");
        assertTrue("world".equals(redisClusterService.get("hello")));
    }
}

3. 多节点命令实现

  • 假如说我们要执行scan操作,我们知道scan是扫描节点所有的键值,rediscluster不能支持扫描所有节点,我们要求在所有节点分别执行命令
// 获取所有节点的JedisPool
Map<String, JedisPool> jedisPoolMap = jedisCluster.getClusterNodes();
for (Entry<String, JedisPool> entry : jedisPoolMap.entrySet()) {
    // 获取每个节点的Jedis连接
    Jedis jedis = entry.getValue().getResource();
    // 只删除主节点数据
    if (!isMaster(jedis)) {
        continue;
    }
    // finally close
}

4. 批量操作怎么实现

mget mset 必须在一个槽

四种批量优化的方法

  • 串行mget
    • 我们执行mget,写一个for循环执行每一个get
    • n次网络时间
    • 优点:编程简单,少量keys满足需求
    • 缺点:大量keys请求延迟严重
  • 串行IO
    • 我在客户端本地做一个内聚,本地有槽和节点的对应关系,就按节点对get进行分组,执行n次pipeline就可以完成对应的mget
    • nodes次网络时间
    • 优点:编程简单,少量节点满足需求
    • 缺点:大量node延迟严重
  • 并行IO
    • 就是多线程进行串行IO
    • 1次网络时间
    • 优点:利用并行特性,延迟取决于最慢的节点
    • 缺点:编程复杂,超时定位问题难
  • hash_tag
    • mget(hash_tag)
    • 1次网络时间
    • 优点:性能最高
    • 缺点:读写增加tag的维护成本,tag分布易出现数据倾斜

5. 故障转移

1. 故障发现

  • 通过ping/pong消息实现故障发现:不需要sentinel
  • 主观下线和客观下线
    • 主观下线:
      • 定义:某个节点认为另一个节点不可用,“偏见”。
      • 流程:ping成功更新最后通信时间。如果ping失败,通信失败就会通信异常断开连接,有一个定时任务,如果节点最后通信时间超过node-timeout标记pfail状态。
    • 客观下线:
      • 当半数以上持有槽的主节点都标记某节点主观下线。
      • 流程:主节点接收ping消息,消息包含其他pfail节点,他会将主观下线消息添加到故障链表中。
      • 尝试客观下线:计算有效下线报告数量,如果大于槽节点总数一半,更新为客观下线,并向集群广播下线节点的fail消息。
      • 通知集群内所有节点标记故障节点为客观下线。
      • 通知故障节点的从节点触发故障转移流程。

2. 故障恢复

  • 资格检查
    • 每个从节点检查与故障主节点的断线时间。
    • 超过cluster-node-timeout * cluster-slave-validity-factor取消资格。
    • cluster-slave-validity-factor:默认是10。
  • 准备选举时间
  • 选举投票
  • 替换主节点
    • 当前从节点取消复制变为主节点(slaveof no one)。
    • 执行clusterDelSlot撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽分配给自己。
    • 向集群广播自己的pong消息,表明已经替换了故障从节点。

3. 故障演练

  • 执行kill-9节点模拟宕机

  • 观察客户端故障恢复时间

  • 观察各个节点的日志

  • 先写一个死循环便于查看故障

@Test
public void test() throws InterruptedException {
    int i = 0;
    while (true) {
        try {
            String key = "key" + i;
            String value = "value" + i;
            redisClusterService.set(key, value);
            TimeUnit.SECONDS.sleep(1);
            i++;
            if (i%100 == 0) {
                System.out.println(i);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
  • 杀死进程,发现出现很多异常
[root@localhost redis]# redis-cli -p 7000 info server | grep process_id
process_id:3868
[root@localhost redis]# kill -9 3868

Caused by: java.net.SocketTimeoutException: connect timed out
  at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
  at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
  at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
  at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
  at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
  at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
  at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
  at java.net.Socket.connect(Socket.java:589)
  at redis.clients.jedis.Connection.connect(Connection.java:184)
  ... 46 more
  • 一段时间后故障消除重新启动7000节点,发现7000已经成为了从节点
[root@localhost redis]# redis-cli -p 7005 cluster slots
1) 1) (integer) 5462
    2) (integer) 10922
    3) 1) "192.168.242.129"
      2) (integer) 7001
      3) "5b39b23994f32b8d75590bdc9ab7c8df4ddc05d1"
    4) 1) "192.168.242.129"
      2) (integer) 7004
      3) "244168ea672d881d6e1eee4a50afcea27dc65fb8"
2) 1) (integer) 0
    2) (integer) 5461
    3) 1) "192.168.242.129"
      2) (integer) 7003
      3) "09e74cda7aa87aa73b42ec0e63d9f514a49ad9f4"
    4) 1) "192.168.242.129"
      2) (integer) 7000
      3) "63adbbda48fcf0a3de92082eb771ca0640989d5e"
3) 1) (integer) 10923
    2) (integer) 16383
    3) 1) "192.168.242.129"
      2) (integer) 7002
      3) "c59973c6c5083dbe1cef3f2bde681f5bbde2dbe2"
    4) 1) "192.168.242.129"
      2) (integer) 7005
      3) "2f6a0261427a8ffafe4262928ab59d920290c4e8"
  • 这里我们可以看日志文件进一步了解故障各个阶段,不过和上一篇博客中redis sentinel中故障日志类似,这里就不重复粘贴日志

6. Redis Cluster开发运维常见问题

1. 集群完整性

  • cluster-require-full-coverage默认为yes

    • 集群中16384个槽全部可用:保证集群完整性。
    • 节点故障或者正在故障转移:(error) CLUSTERDOWN The cluster is down。
  • 大多数业务无法容忍,cluster-require-full-coverage建议设置为no

  • 使用9000~9005六个节点

protected-mode no
port 9000
daemonize yes
dir "/home/Thpffcj/redis/data"
logfile "9000.log"
dbfilename "dump-9000.rdb"
cluster-enabled yes
cluster-config-file nodes-9000.conf
cluster-require-full-coverage yes
  • 其他节点配置相似,启动所有节点
  • 启动集群
[root@localhost redis]# src/redis-trib.rb create --replicas 1 127.0.0.1:9000 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003 127.0.0.1:9004 127.0.0.1:9005
  • 发现节点状态已经失败
[root@localhost redis]# redis-cli -p 9000 shutdown
[root@localhost redis]# redis-cli -p 9003 shutdown
[root@localhost redis]# redis-cli -p 9002
127.0.0.1:9002> cluster info
cluster_state:fail
cluster_slots_assigned:16384
cluster_slots_ok:10923
cluster_slots_pfail:0
cluster_slots_fail:5461
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_ping_sent:953
cluster_stats_messages_pong_sent:151
cluster_stats_messages_meet_sent:5
cluster_stats_messages_fail_sent:6
cluster_stats_messages_sent:1115
cluster_stats_messages_ping_received:151
cluster_stats_messages_pong_received:134
cluster_stats_messages_fail_received:2
cluster_stats_messages_received:287
  • 集群是一个下线状态
127.0.0.1:9002> set hello world
(error) CLUSTERDOWN The cluster is down

2. 带宽消耗

  • 官方建议:<=1000个节点
  • ping/pong消息
  • 不容忽视的带宽消耗
  • 三个方面:
    • 消息发送频率:节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息。
    • 消息数据量:slots槽数组(2KB空间)和整个集群1/10的状态数据(10个节点状态数据约1KB)。
    • 节点部署的机器规模:集群分布的机器越多且每台机器划分的节点数越均匀,则集群内整体的可用带宽越高。
  • 一个例子:
    • 规模:节点200个,20台物理机(每台10各节点)
    • cluster-node-timeout = 15000,ping/pong带宽约为25MB。
    • cluster-node-timeout = 20000,ping/pong带宽低于15MB。
  • 优化:
    • 避免"大"集群:避免多业务使用一个集群,大业务可以多集群。
    • cluster-node-timeout:带宽和故障转移速度的均衡。
    • 尽量均匀分配到多机器上:保证高可用和带宽。

3. Pub/Sub广播

  • 任何一个节点执行publish,它会将这个消息在集群中发布。
  • 问题:publish在集群每个节点广播:加重带宽。
  • 解决:单独"走"一套Redis Sentinel。

4. 集群倾斜

  • 数据倾斜:内存不均
    • 节点和槽分配不均。
      • redis-trib.rb info ip:port查看节点,槽,键值分布。
      • redis-trib.rb rebalance ip:port进行均衡(谨慎使用)。
    • 不同槽对应键值数量差异较大。
      • CRC16正常情况下比较均匀。
      • 可能存在hash_tag。
      • cluster countkeysinslot {slot}获取槽对应键值个数。
    • 包含bigkey。
      • bigkey:例如大字符串,几百万的元素的hash,set等
      • 从节点:redis-cli --bigkeys。
      • 优化:优化数据结构。
    • 内存相关配置不一致。
      • hash-max-ziplist-value,ser-max-intset-entries等。
      • 优化:定期"检查"配置一致性。
  • 请求倾斜:热点
    • 热点key:重要的key或者bigkey。
    • 优化:
      • 避免bigkey。
      • 热键不要用hash_tag。
      • 当一致性不高时,可以使用本地缓存 + MQ。
[root@localhost redis]# src/redis-trib.rb info 127.0.0.1:7000
127.0.0.1:7000 (63adbbda...) -> 0 keys | 5462 slots | 1 slaves.
192.168.242.129:7002 (c59973c6...) -> 0 keys | 5461 slots | 1 slaves.
192.168.242.129:7001 (5b39b239...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.

5. 读写分离

  • 只读连接:集群模式的从节点不接受任何读写请求
    • 重定向到负责槽的主节点。
    • readonly命令可以读:连接级别命令。
[root@localhost redis]# redis-cli -c -p 7000
127.0.0.1:7000> set hello world
OK
127.0.0.1:7000> exit
[root@localhost redis]# redis-cli -c -p 7003
127.0.0.1:7003> get hello
-> Redirected to slot [866] located at 192.168.242.129:7000
"world"

[root@localhost redis]# redis-cli -c -p 7003
127.0.0.1:7003> readonly
OK
127.0.0.1:7003> get hello
"world"
  • 读写分离:更加复杂
    • 同样的问题:复制延迟,读取过期数据,从节点故障。
    • 修改客户端:cluster slaves {nodeId}。

6. 数据迁移

  • 官方迁移工具:redis-trib.rb import
    • 只能从单机迁移到集群。
    • 不支持在线迁移:source需要停写。
    • 不支持断点续传。
    • 单线程迁移:影响速度。
  • 在线迁移:
    • 唯品会:redis-migrate-tool。
    • 豌豆荚:redis-port。

7. 集群vs单机

  • 集群限制:
    • key批量操作支持有限:例如mget,mset必须在一个slot。
    • key事务和Lua支持有限:操作的key必须在一个节点。
    • key是数据分区的最小粒度:不支持bigkey分区。
    • 不支持多个数据库:集群模式下只有一个db 0。
    • 复制只支持一层:不支持树形复制结构。
  • 分布式Redis不一定好
    • Redis Cluster:满足容量和性能的扩展性,很多业务"不需要"。
      • 大多数时客户端性能会"降低"。
      • 命令无法跨界点使用。
      • Lua和事务无法跨节点使用。
      • 客户端维护更复杂:SDK和应用本身消耗(例如更多的连接池)。
    • 很多场景Redis Sentinel已经足够好。

7. 集群总结

1. Redis cluster数据分区规则采用虚拟槽方式(16384个槽),每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡

2. 搭建集群划分为四个步骤:准备节点,节点握手,分配槽,复制。redis-trib.rb工具用于快速搭建集群

3. 集群伸缩通过在节点之间移动槽和相关数据实现

  • 扩容时根据槽迁移计划把槽从源节点迁移到新节点。
  • 收缩时如果下线的节点有负责的槽需要迁移到其他节点,再通过cluster forget命令让集群内所有节点忘记被下线节点。

4. 使用smart客户端操作集群达到通信效率最大化,客户端内部负责计算维护键->槽->节点的映射,用于快速定位到目标节点

5. 集群自动故障转移过程分为故障发现和节点恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性

6. 开发运维常见问题包括:超大规模集群带宽消耗,pub/sub广播问题,集群倾斜问题,单机和集群对比等

最后

大家可以关注我的微信公众号一起学习进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值