我们紧接上文,现在你使用了哨兵机制架构,业务的性能又上升了一个新的高度
你的Redis从最简单的只有一台实例,经过数据持久化机制、主从复制架构,再到哨兵架构,其性能和稳定性都越来越高,就算节点发生故障,也不用担心了。
基于哨兵架构,你的Redis基本上可以稳定运行很长一段时间了,随着时间的发展,你的业务流量迎来了爆炸式的增长,此时你的架构还能够承担这么大的流量吗?
我们思考一下:
- Redis节点故障宕机,我们有哨兵+slave,可以实现主从自动切换
- 大量读请求,我们有多个slave节点来分担压力,就算不够我们还可以增加多台slave
- 大量写请求,由于只有一台msater,而且只有master才能够提供写服务,所以master容易出现写性能瓶颈
问题出现了,当有大量的写请求时,你的架构只有一个msater能够提供服务,很有可能无法承担那么大的写流量
要想完美的解决这个问题,Redis第三个架构随之而来——分片集群(cluster)
cluster
- redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容
- 优点:将Redis的写操作分摊到了多个节点上,提高写的并发能力,扩容简单
- 缺点:每个Node承担着互相监听、高并发数据写入、高并发数据读出,工作任务繁重
特点:
- 高性能可拓展:可支持拓展到1000个节点,多个节点之间数据分片,采用异步复制模式完成主从同步,无代理方式完成重定向
- 可用性:具有自我故障检测、故障转移的特点
- redis cluster集群是为了降低主节点的压力,主主节点之间是不存在同步关系的,各主从之间的数据存在同步关系。有多少主节点,就会把16384个哈希槽(hash slot)平均分配到这些主节点上,当往redis里写入数据时,会根据哈希算法(CRC16(KEY)%16384)算出这个数的哈希槽来决定它放到哪一个主节点上,然后这个主节点的从节点去自动同步。在客户端随便连接一个主节点即可,主节点之间会进行内部跳转。当取对应数据时,各节点之间会自动跳转到所取数据所在的主节点上
- 多主多从,去中心化:从节点作为备用,复制主节点数据,不提供服务
- Redis Cluster does not support multiple databases like the standalone version of Redis. We only support database
0
; theSELECT
command is not allowed
cluster节点之间是如何进行通信的?
Redis集群中,节点之间通过建立TCP连接,使用gossip协议来传播集群的信息(节点通信端口=服务端口+10000)
所谓gossip协议,指的是一种消息传播的机制,类似人们传递八卦消息似的,一传十,十传百,直至所有人都知道这条八卦内容。Redis集群中各节点之间传递消息就是基于gossip协议,最终实现所有节点都会知道整个集群完整的信息。
gossip协议有4种常用的消息类型:PING、PONG、MEET、FAIL
- MEET:当需要向集群中加入新节点时,需要集群中的某个节点发送MEET消息到新节点,通知新节点加入集群。新节点收到MEET消息后,会回复PONG命令给发送者(redis5.0后 用add-node命令来添加节点)。
- PING:集群内每个节点每秒会向其他节点发送PING消息,用来检测其他节点是否正常工作,并交换彼此的状态信息。PING消息中会包括自身节点的状态数据和其他部分节点的状态数据。
- PONG:当接收到PING消息和MEET消息时,会向发送发回复PONG消息,PONG消息中包括自身节点的状态数据。节点也可以通过广播的方式发送自身的PONG消息来通知整个集群对自身状态的更新。
- FAIL:当一个节点判定另一个节点下线时,会向集群内广播一个FAIL消息,其他节点接收到FAIL消息之后,把对应节点更新为下线状态。
cluster是如何实现数据分片的
在cluster中,规定了数据和实例的对应规则——采用哈希槽(hash slot),来处理数据和实例之间的映射关系
在cluster中,一个切片集群共有16384个哈希槽(2^14),这些哈希槽类似于数据分区,每个键值对都会根据它的key,被映射到一个hash槽中
- 每当我们通过Redis Cluster对某个key执行操作时,接收请求的节点会首先对key执行计算,得到该key对应的哈希槽,然后再从哈希槽与节点的映射关系中找到负责该哈希槽的节点。如果是节点自身,则直接进行处理;如果是其他节点,则通过重定向告知客户端连接至正确的节点进行处理
- 每个Node被平均分配了一个Slot段,对应着0-16384,Slot不能重复也不能缺失,否则会导致对象重复存储或无法存储。
- Node之间也互相监听,一旦有Node退出或者加入,会按照Slot为单位做数据的迁移。例如Node1如果掉线了,0- 3276这些Slot将会平均分摊到Node2和Node3上,由于Node2和Node3本身维护的Slot还会在自己身上不会被重新分配,所以迁移过程中不会影响到3277-16384 Slot段的使用
- 为集群分配哈希槽
#将槽 0 ~ 槽5000 指派给 给 7001 :
127.0.0.1:7001> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
#将槽 5001 ~ 槽10000 指派给 给 7002 :
127.0.0.1:7002> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000
#将槽 10001~ 槽 16383 指派给 给 7003 :
127.0.0.1:7003> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383
通过哈希槽,就实现了数据到哈希槽,哈希槽再到实例的分配
但客户端如何知道要访问的数据在哪个实例上呢?
客户端如何定位数据在哪个节点上?
一般来说,客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的
Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了
客户端收到哈希槽信息之后,会把哈希槽信息缓存到本地,当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了
当客户端向节点请求键值对时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己
如果键所在的槽刚好指派给了当前节点,那么节点会直接执行这个命令;
如果没有指派给当前节点,那么节点会向客户端返回一个 MOVED 错误,然后重定向(redirect)到正确的节点,并再次发送之前待执行的命令
节点通过以下算法来定义 key
属于哪个槽:
crc16(key,keylen) & 0x3FFF;
crc16:用于计算 key 的 CRC-16 校验和
0x3FFF:换算成 10 进制是 16383
& 0x3FFF:用于计算出一个介于 0~16383 之间的整数作为 key 的槽号。
查看key属于哪个槽
127.0.0.1:7001> CLUSTER KEYSLOT <KEY>
例如:
比如:MOVED 10086 127.0.0.1:7002 表示,客户端请求的键值对所在的哈希槽 10086,实际是在 127.0.0.1:7002 这个实例上。
通过返回的 MOVED 命令,就相当于把哈希槽所在的新实例的信息告诉给客户端了。
这样一来,客户端就可以直接和 7002 连接,并发送操作请求了。
同时,客户端还会更新本地缓存,将该槽与 Redis 实例对应关系更新正确
当集群中的实例有新增或删除,redis需要重新分配哈希槽;为了负载均衡,redis需要把哈希槽在所有实例上重新分布一遍,而且分片过程中不需要下线
但这样会造成:当客户端有操作键值对的有关的命令,同时该键值对正好属于被迁移槽。并且被迁移槽的部分键值对还驻留在source节点中,另外部分键已经保存在target节点中;则会进行下列动作:
- 如果能在source节点找到对应的key,那么直接执行client的命令;
- 如果找不到该key,那很有可能就在target中,此时source节点会向client发送一个ASK错误,引导client转向正在导入槽的target节点,并再次发送之前想要执行的命令。
- ASK 和 MOVED 的区别
ASK 错误和 MOVED 错误都会导致客户端重定向,它们的区别在于:
1.MOVED 错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽 n 的 MOVED 错误之后,客户端每次遇到关于槽 n 的命令请求时,都可以直接将命令请求发送至 MOVED 错误指向的节点,因为该节点就是目前负责槽 n 的节点
2.而 ASK 只是两个节点迁移槽的过程中的一种临时措施:在客户端收到关于槽 n的 ASK 错误之后,客户端只会在接下来的一次命令请求中将关于槽 n 的命令请求发送到 ASK 错误指向的节点,但是 ,如果客户端再次请求槽 n 中的数据,它还是会给原来负责槽 n 的节点发送请求
3.ASK 命令的作用只是让客户端能给新实例发送一次请求,而且也不会更新客户端缓存的哈希槽分配信息。而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例
如何实现高可用?
- 故障检测
集群中每个节点都会定期向集群中的其他节点发送ping消息,以此检测对方是否在线
如果接受ping消息的节点没有在规定时间内返回pong消息,那么发送PING消息的节点就会将PONG消息节点标记为疑似下线(possible fail,PFAIL)
如果集群中超过半数以上的主节点都将某个节点X标记为PFAIL,则某个主节点就会将这个主节点X标记为下线(FAIL),并广播这条消息
- 故障转移
一个节点成为从节点后,就会告诉集群中其他节点自己正在开始复制所主节点
当一个从节点发现自己所主节点下线时,就会开始进行故障转移
1.在该下线主节点的所有从节点中,选择一个做主节点
2.被选中的从节点会执行SLAVEOF no one命令,成为新的主节点
3.新的主节点会撤销对所有对下线主节点的槽指派,并将这些槽全部派给自己
4.新的主节点向集群广播一条pong消息,告诉其他节点自己变成了主节点
5.新主节点开始提供服务,故障转移完成
命令相关
- 客户端命令
#集群相关
redis-cli -c -p 7001 cluster info :打印所有集群的信息
redis-cli -c -p 7001 cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息
#节点相关
redis-cli -c -p 7001 cluster meet <ip> <port> :将节点添加到集群当中,让它成为集群的一份子(随机分配角色。)
redis-cli -c -p 7001 cluster forget <node_id> :从集群中移除 node_id 指定的节点
redis-cli -c -p 7001 cluster replicate <master_node_id> :可以让接受命令的节点成为node_id所指定的从节点,并开始对主节点进行复制
redis-cli -c -p 7001 cluster saveconfig :将节点的配置文件保存到硬盘里面
#哈希槽相关
redis-cli -c -p 7001 cluster addslots <slot> [slot ...] :将一个或多个槽(slot)指派( assign)给当前节点
redis-cli -c -p 7001 cluster delslots <slot> [slot ...] :移除一个或多个槽对当前节点的指派。
redis-cli -c -p 7001 cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
redis-cli -c -p 7001 cluster setslot <slot> node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽,然后再进行指派
redis-cli -c -p 7001 cluster setslot <slot> migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中
redis-cli -c -p 7001 cluster setslot <slot> importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点
redis-cli -c -p 7001 cluster setslot <slot> stable :取消对槽 slot 的导入( import)或者迁移( migrate)
#key相关
redis-cli -c -p 7001 cluster keyslot <key> :计算键 key 应该被放置在哪个槽上
redis-cli -c -p 7001 cluster countkeysinslot <slot> :返回槽 slot 目前包含的键值对数量
redis-cli -c -p 7001 cluster getkeysinslot <slot> <count> :返回 count 个 slot 槽中的键
- 集群命令
#帮助文档
redis-cli --cluster help
#集群相关
redis-cli --cluster create --cluster-replicas 1 <host:port>创建集群
#添加主节点
redis-cli --cluster add-node <新主节点host:port> <集群任意节点host:port>
#添加从节点
redis-cli --cluster add-node --slave <从节点host:port> <主节点host:port> --cluster-slave
#重新分配slot
redis-cli --cluster reshard <节点host:port>
#平衡新增节点的slot
redis-cli --cluster rebalance --cluster-use-empty-masters
#查看集群情况
redis-cli --cluster check <节点host:port>
#移除节点(在删除的时候,需要保证节点的hash槽是空的,如果不是空的,需要把hash槽移走)
redis-cli --cluster del-node <节点host:port> <节点id>
#查看某一node信息
redis-cli --cluster info <host:port>
实例部署
- 前期准备(三台设备上)
cp /etc/redis/redis.conf /etc/redis_7001.conf
cp /etc/redis/redis.conf /etc/redis_8001.conf
mkdir /var/lib/redis/cluster
systemctl stop redis-server
- 配置文件
master1和slave1
#vim /etc/redis_7001.conf
bind 0.0.0.0
port 7001
daemonize yes
protected-mode no
dir /var/lib/redis/cluster
logfile /var/log/redis/redis_7001.log
pidfile /var/redis/redis_7001.pid
tcp-backlog 511
tcp-keepalive 300
timeout 0
supervised no
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb_7001
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
cluster-enabled yes
cluster-config-file nodes_7001.conf
cluster-node-timeout 15000
#vim /etc/redis_8001.conf
bind 0.0.0.0
port 8001
daemonize yes
protected-mode no
dir /var/lib/redis/cluster
logfile /var/log/redis/redis_8001.log
pidfile /var/redis/redis_8001.pid
tcp-backlog 511
tcp-keepalive 300
timeout 0
supervised no
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb_8001
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
cluster-enabled yes
cluster-config-file nodes_8001.conf
cluster-node-timeout 15000
启动redis
#redis-server /etc/redis_7001.conf && redis-server /etc/redis_8001.conf
master2和slave2
#vim /etc/redis_7001.conf
bind 0.0.0.0
port 7001
daemonize yes
protected-mode no
dir /var/lib/redis/cluster
logfile /var/log/redis/redis_7001.log
pidfile /var/redis/redis_7001.pid
tcp-backlog 511
tcp-keepalive 300
timeout 0
supervised no
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb_7001
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
cluster-enabled yes
cluster-config-file nodes_7001.conf
cluster-node-timeout 15000
#vim /etc/redis_8001.conf
bind 0.0.0.0
port 8001
daemonize yes
protected-mode no
dir /var/lib/redis/cluster
logfile /var/log/redis/redis_8001.log
pidfile /var/redis/redis_8001.pid
tcp-backlog 511
tcp-keepalive 300
timeout 0
supervised no
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb_8001
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
cluster-enabled yes
cluster-config-file nodes_8001.conf
cluster-node-timeout 15000
启动redis
#redis-server /etc/redis_7001.conf && redis-server /etc/redis_8001.conf
master3和slave3
#vim /etc/redis_7001.conf
bind 0.0.0.0
port 7001
daemonize yes
protected-mode no
dir /var/lib/redis/cluster
logfile /var/log/redis/redis_7001.log
pidfile /var/redis/redis_7001.pid
tcp-backlog 511
tcp-keepalive 300
timeout 0
supervised no
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb_7001
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
cluster-enabled yes
cluster-config-file nodes_7001.conf
cluster-node-timeout 15000vim /etc/redis_7001.conf
bind 0.0.0.0
port 7001
daemonize yes
protected-mode no
dir /var/lib/redis/cluster
logfile /var/log/redis/redis_7001.log
pidfile /var/redis/redis_7001.pid
cluster-enabled yes
cluster-config-file nodes_7001.conf
cluster-node-timeout 15000
#vim /etc/redis_8001.conf
bind 0.0.0.0
port 8001
daemonize yes
protected-mode no
dir /var/lib/redis/cluster
logfile /var/log/redis/redis_8001.log
pidfile /var/redis/redis_8001.pid
tcp-backlog 511
tcp-keepalive 300
timeout 0
supervised no
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb_8001
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
cluster-enabled yes
cluster-config-file nodes_8001.conf
cluster-node-timeout 15000
启动redis
redis-server /etc/redis_7001.conf && redis-server /etc/redis_8001.conf
- 创建集群
redis-cli --cluster create --cluster-replicas 1 192.168.149.128:7001 192.168.149.128:8001 192.168.149.129:7001 192.168.149.129:8001 192.168.149.130:7001 192.168.149.130:8001
也可以进行手动指定
- 手动配置主节点(系统会自动分配slot,master必须大于等于三个)
redis-cli --cluster create --cluster-replicas 0 192.168.149.128:7001 192.168.149.129:7001 192.168.149.130:7001
- 手动指定从节点
redis-cli --cluster add-node 192.168.149.129:8001 192.168.149.128:7001 --cluster-slave
redis-cli --cluster add-node 192.168.149.130:8001 192.168.149.129:7001 --cluster-slave
redis-cli --cluster add-node 192.168.149.128:8001 192.168.149.130:7001 --cluster-slave
验证
连接任意一台客户端即可
#redis-cli -c -p 7001
127.0.0.1:7001> CLUSTER NODES
db93ccf1d9ec043032b2a5c8a559f0aa0b77377b 192.168.149.129:7001@17001 master - 0 1660126817807 3 connected 5461-10922
6ebbefdc950de1b003ce49fdf5a6e8e853f4099a 192.168.149.129:8001@18001 slave e88d4393194f8990dfb9f6a958953e8a7a174a55 0 1660126819000 1 connected
9355eadf652557747690155b8acb2bd5a1fb7a8d 192.168.149.128:8001@18001 slave 8a36ec13af914d629df215bdf97e690167e3b67f 0 1660126818824 5 connected
8a36ec13af914d629df215bdf97e690167e3b67f 192.168.149.130:7001@17001 master - 0 1660126820857 5 connected 10923-16383
e88d4393194f8990dfb9f6a958953e8a7a174a55 192.168.149.128:7001@17001 myself,master - 0 1660126820000 1 connected 0-5460
eef9afb0222d6fc66bceddd1e95eaee64f2c4eb7 192.168.149.130:8001@18001 slave db93ccf1d9ec043032b2a5c8a559f0aa0b77377b 0 1660126820000 3 connected
可以看到192.168.149.128:7001是主,192.168.149.129:8001是其从
#在192.168.149.128:7001 写入数据
[root@master ~]# redis-cli -c -p 7001
127.0.0.1:7001> set name python
-> Redirected to slot [5798] located at 192.168.149.129:7001
OK
#在192.168.149.129:8001查看
[root@slave1 ~]# redis-cli -c -p 8001
127.0.0.1:8001> get name
-> Redirected to slot [5798] located at 192.168.149.129:7001
"python"
缩容案例
创建了四台master打算将其中一台master(192.168.149.128:8001)转换成slave
然后再分别为三台master分配slave 变成三主三从架构
#master:
192.168.149.128:7001
192.168.149.128:8001
192.168.149.129:7001
192.168.149.130:7001
- 将192.168.149.128:8001上的哈希槽(12288-16383)全部分配给192.168.149.128:7001
redis-cli --cluster reshard 192.168.149.128:7001
- 分配结束后把192.168.149.128:8001删除
redis-cli --cluster del-node 192.168.149.128:8001 caf9fbac8c701d6030f91fc2273a16e065808de7
- 现在只有三台主,我们添加从节点,每台主分配一台从
redis-cli --cluster add-node 192.168.149.129:8001 192.168.149.128:7001 --cluster-slave
redis-cli --cluster add-node 192.168.149.130:8001 192.168.149.129:7001 --cluster-slave
redis-cli --cluster add-node 192.168.149.128:8001 192.168.149.130:7001 --cluster-slave
- 再将哈希槽重新平均分配一下
redis-cli --cluster rebalance 192.168.149.128:7001
- 验证
#在192.168.149.129:7001写数据
[root@minion1 ~]# redis-cli -c -p 7001
127.0.0.1:7001> set age 18
-> Redirected to slot [741] located at 192.168.149.128:7001
OK
#重定向到192.168.149.128:7001了。而192.168.149.128:7001的从是192.168.149.129:8001
192.168.149.129:8001> get age
-> Redirected to slot [741] located at 192.168.149.128:7001
"18"
扩容案例
将192.168.149.128:8001转换成master,然后分配slot
#master:
192.168.149.128:7001
192.168.149.129:7001
192.168.149.130:7001
#slave:
192.168.149.129:8001
192.168.149.130:8001
192.168.149.128:8001
- 删除192.168.149.128:8001
redis-cli --cluster del-node 192.168.149.128:8001 caf9fbac8c701d6030f91fc2273a16e065808de7
- 增加192.168.149.128:8001为主节点
redis-cli --cluster ass-node 192.168.149.128:8001 192.168.149.128:7001
- 这时候128:8001还没有被分配slot,重新分配一下
#重新平均分配slot
redis-cli --cluster rebalance --cluster-use-empty-masters 192.168.149.128:8001