前言

Redis有主从架构、哨兵架构,哨兵架构可以在master节点宕机的情况下,通过选举的方式切换slave节点作为新的master主节点。

哨兵架构的情况下,只能提供一个master主节点,也就是说只能有一个“写”的节点,这种情况下redis的性能瓶颈问题就暴露出来了,它支撑不了很高的并发。

而且在哨兵架构下,倘若master宕机了,而新的slave升级为master,这个切换的时间段,短则几秒,长则十几秒甚至更长都是有可能的。

而且单个主节点master,也不适合设置过大的内存,否则redis启动时恢复数据,需要过长的时间。

高可用集群介绍

redis-cluster

Redis 集群是一个由多个小的主从节点群组,组成的分布式服务器群,它具有复制、高可用、分片特性。

Redis集群不需要哨兵也能完成节点移除和故障转移。

需要将每个节点设置为集群模式,这种集群模式没有中心节点,可以水平扩展。

redis集群环境下,可以提供多个master主节点,也就是可以提供多个“写”的节点,所以它的性能和高可用性均优于哨兵模式,而且它的配置还很简单。

- 复制 :跟主从架构一样,master会把数据同步到slave节点
- 高可用:当某一个master挂了的情况下,其他的master还在正常运行,而且其他的master会选举slave作为新的master。
- 分片:redis使用了crc16的算法,对key进行运算,然后将key分布到各个master节点当中

Redis高可用集群搭建

在centos上搭建集群

我这里准备了三个centos虚拟机,我准备在三个centos上都搭建一主二从。三个虚拟机ip如下:

- 10.211.55.6
- 10.211.55.7
- 10.211.55.8

假设已经下载好了redis在linux系统上,并且已编译。

先在第一台centos上进行操作部署。( 10.211.55.6 )

# 在 /root目录下创建文件夹 redis-cluster,并且在下面创建两个文件夹,以要运行的端口号为命名。
[root@localhost redis-cluster]# mkdir 8010 8011 8012
[root@localhost redis-cluster]# ls
8010  8011  8012

# 复制配置文件到各个端口文件夹下
cp /root/redis/redis.conf /root/redis-cluster/801*/redis.conf

# -----对每个redis.conf文件进行修改-----
# ps: 可以先修改一个redis.conf,其他的端口直接用vim的%s替换命令,替换端口即可。

# 设置后台守护进程运行
daemonize yes
# redis要运行的端口号
port 8010
# 指定数据文件存放位置,如果多个redis实例的存放位置一致的话,会导致数据丢失,以端口号命名
dir /root/redis-cluster/8010/
# 日志存放文件名,这里以端口号命名
logfile "8010.log"
# 开启集群模式 , 默认这个配置被注释了,解开就可以。
cluster-enabled yes
# 集群节点信息文件,这里的文件命名最好和port对应上,默认也注释掉了。
cluster-config-file nodes-8010.conf
# 集群节点的超时时间 单位毫秒 , 默认也被注释了
cluster-node-timeout 5000
# 去掉bind绑定访问ip信息
# bind 127.0.0.1

# 关闭保护模式
protected-mode no
# 开启aof数据备份
appendonly yes

# ------如果需要设置密码的话,增加以下配置------

# 设置redis访问密码
requirepass 666666

# 设置集群节点间的访问密码
masterauth 888888

# 在第一台centos上,运行三个个实例
[root@localhost redis-cluster]# /root/redis/src/redis-server /root/redis-cluster/8010/redis.conf
[root@localhost redis-cluster]# /root/redis/src/redis-server /root/redis-cluster/8011/redis.conf
[root@localhost redis-cluster]# /root/redis/src/redis-server /root/redis-cluster/8012/redis.conf


# 查看进程,可以看到三个进程已经启动,进程信息后面的[cluster],表示以集群方式运行。
[root@localhost redis-cluster]# ps -ef | grep redis
root     23081     1  0 22:15 ?        00:00:00 /root/redis/src/redis-server *:8010 [cluster]
root     23099     1  0 22:15 ?        00:00:00 /root/redis/src/redis-server *:8011 [cluster]
root     23114     1  0 22:15 ?        00:00:00 /root/redis/src/redis-server *:8012 [cluster]

进入第二台、第三台centos执行相同的操作,需要注意的是需要更改不同的redis端口。

# 10.211.55.7 centos
[root@localhost redis-cluster]# ps -ef | grep redis
root     23787     1  0 22:19 ?        00:00:00 /root/redis/src/redis-server *:8020 [cluster]
root     23807     1  0 22:19 ?        00:00:00 /root/redis/src/redis-server *:8021 [cluster]
root     23822     1  0 22:19 ?        00:00:00 /root/redis/src/redis-server *:8022 [cluster]

# 10.211.55.8 centos
[root@localhost redis-cluster]# ps -ef | grep redis
root     24705     1  0 22:21 ?        00:00:00 /root/redis/src/redis-server *:8030 [cluster]
root     24720     1  0 22:21 ?        00:00:00 /root/redis/src/redis-server *:8031 [cluster]
root     24732     1  0 22:21 ?        00:00:00 /root/redis/src/redis-server *:8032 [cluster]

最后三台服务器结构图如下 redis-cluster-node-centos

创建集群

用redis-cli 创建整个redis集群,注意需要将防火墙关掉,或者暴露出redis的gossip 通讯端口。

redis的gossip通讯端口一般为数据端口+10000,例如8010端口,8010+10000 = 18010 (gossip 端口)

# -a 'requirepass' 用密码访问, ‐‐cluster create 创建集群 ‐‐cluster‐replicas 1 为每个master设置多少个slave节点  
/root/redis/src/redis‐cli ‐a requirepass ‐‐cluster create ‐‐cluster‐replicas 2 xxx:port ....

# 示例创建集群,在任意服务器运行即可。
/root/redis/src/redis-cli -a 666666 --cluster create --cluster-replicas 2 10.211.55.6:8010 10.211.55.6:8011 10.211.55.6:8012 10.211.55.7:8020 10.211.55.7:8021 10.211.55.7:8022 10.211.55.8:8030 10.211.55.8:8031 10.211.55.8:8032

# 这时候它会提示你给各个master分配slots槽位,输yes回车即可
......
Can I set the above configuration? (type 'yes' to accept): 

验证集群是否搭建成功,连接任意一个客户端 ( -c 表示集群模式 )

/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.6 -p 8010

查看节点列表 ( cluster nodes )

redis-cluster-nodes-info

查看集群信息 ( cluster info )

10.211.55.6:8010> cluster info
cluster_state:ok  
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:9
cluster_size:3
cluster_current_epoch:9
cluster_my_epoch:1
cluster_stats_messages_ping_sent:683
cluster_stats_messages_pong_sent:575
cluster_stats_messages_sent:1258
cluster_stats_messages_ping_received:567
cluster_stats_messages_pong_received:683
cluster_stats_messages_meet_received:8
cluster_stats_messages_received:1258

字段解释摘自:http://www.redis.cn/commands/cluster-info.html

- `cluster_state`: `ok`状态表示集群可以正常接受查询请求。`fail` 状态表示,至少有一个哈希槽没有被绑定(说明有哈希槽没有被绑定到任意一个节点),或者在错误的状态(节点可以提供服务但是带有FAIL 标记),或者该节点无法联系到多数master节点。.
- `cluster_slots_assigned`: 已分配到集群节点的哈希槽数量(不是没有被绑定的数量)。16384个哈希槽全部被分配到集群节点是集群正常运行的必要条件.
- `cluster_slots_ok`: 哈希槽状态不是`FAIL` 和 `PFAIL` 的数量.
- `cluster_slots_pfail`: 哈希槽状态是 `PFAIL`的数量。只要哈希槽状态没有被升级到`FAIL`状态,这些哈希槽仍然可以被正常处理。`PFAIL`状态表示我们当前不能和节点进行交互,但这种状态只是临时的错误状态。
- `cluster_slots_fail`: 哈希槽状态是`FAIL`的数量。如果值不是0,那么集群节点将无法提供查询服务,除非`cluster-require-full-coverage`被设置为`no` .
- `cluster_known_nodes`: 集群中节点数量,包括处于`握手`状态还没有成为集群正式成员的节点.
- `cluster_size`: 至少包含一个哈希槽且能够提供服务的master节点数量.
- `cluster_current_epoch`: 集群本地`Current Epoch`变量的值。这个值在节点故障转移过程时有用,它总是递增和唯一的。
- `cluster_my_epoch`: 当前正在使用的节点的`Config Epoch`值. 这个是关联在本节点的版本值.
- `cluster_stats_messages_sent`: 通过node-to-node二进制总线发送的消息数量.
- `cluster_stats_messages_received`: 通过node-to-node二进制总线接收的消息数量.
数据验证

尝试进行数据操作,我这里进入8010端口执行,可以看到我虽然用8010端口发送命令,但是redis客户端给我转到了另一个服务器的8030服务器上,这是因为"zhangsan" 这个key的槽位solt,对应的是8030的这个服务器,下面原理分析有写关于槽位solt。

# set一个key,zhangsan,
10.211.55.6:8010> set zhangsan 666666
-> Redirected to slot [12767] located at 10.211.55.8:8030
OK
10.211.55.8:8030> 

在集群环境下,也就会意味着一些批量处理的命令将会无法使用。假设我这里设置三个key,可以看到执行失败了,这是因为它们三个的ket所计算出的solt不在一个master上,如果它们的solt都在一台master上,那么这条命令还是可以执行的。

10.211.55.8:8030> mset zhangsan 666666 lisi 666666 wangwu 666666
(error) CROSSSLOT Keys in request don't hash to the same slot
10.211.55.8:8030> 

在集群环境下,如果要使用批量处理的命令,可以包装一个大括号,如下.

当使用了大括号包装后,那么redis只会用大括号包装的那一部分来计算solt,而大括号之外的则不会参与solt计算,所以它们最终会落到一个master节点上。

但是可能产生数据倾斜的问题,假设:* 数据非常多的话,那么就会大量的数据存在某一个master节点上,还有就是hash数据结构,在集群环境下,某一个key的hash结构数据过大,也会出现数据倾斜。

10.211.55.8:8030> mset {user}:zhangsan 666666 {user}:lisi 666666 {user}:wangwu 666666
-> Redirected to slot [5474] located at 10.211.55.7:8020
OK
10.211.55.7:8020> 
10.211.55.7:8020> mget {user}:zhangsan {user}:lisi {user}:wangwu 
1) "666666"
2) "666666"
3) "666666"

SpringBoot 集成Redis群

引入相关依赖
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
SpringBoot项目核心配置
spring:
  redis:
    cluster:
      # 这里放集群节点,建议放入所有节点信息,包括slave节点,它会在你给的节点列表任意一个节点获取所有的master节点信息
      nodes: 10.211.55.6:8010
    timeout: 3000
    database: 0
    lettuce:
      pool:
        max-active: 100
        max-wait: 1000
        max-idle: 50
        min-idle: 10
    # 访问redis密码
    password: 666666
访问接口代码
    //死循环不停的写入数据到redis
    @RequestMapping("whileWriteData")
    public String whileWriteData() throws InterruptedException {
        for (int i = 0; ; i++) {
           try {
               stringRedisTemplate.opsForValue().set("zhangsan:" + i, String.valueOf(i));
               Thread.sleep(1000);
           }catch (Exception e){
               e.printStackTrace();
           }
        }
    }
数据验证

进入查看redis数据,可以看到数据被分散到多个节点上

# 进入8010查看
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.6 -p 8010
10.211.55.6:8010> keys *
1) "zhangsan:14"
2) "zhangsan:11"
3) "zhangsan:10"
4) "zhangsan:0"
5) "zhangsan:4"
6) "zhangsan:8"
......

# 进入8020查看,忽略之前set的部分数据,{user}:*
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.7 -p 8020
10.211.55.7:8020> keys *
 1) "zhangsan:2"
 2) "zhangsan:5"
 3) "{user}:zhangsan"
 4) "zhangsan:1"
 5) "{user}wangwu"
 6) "zhangsan:6"
 7) "{user}:wangwu"
 8) "zhangsan:9"
 9) "zhangsan:13"
10) "{user}:lisi"
......

# 进入8030查看
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.8 -p 8030
10.211.55.8:8030> keys *
1) "zhangsan:3"
2) "zhangsan:12"
3) "zhangsan:7"
4) "zhangsan"
......

Redis 水平扩容

查看当前集群节点状态

进入节点执行cluster nodes 命令

10.211.55.6:8010> cluster nodes

redis-cluster-nodes-slots-2

从上图可以看到,整个集群运行正常,三个master节点和三个slave节点。

  • 8010端口存储槽位:0-5460
  • 8020端口存储槽位:5461-10922
  • 8030端口存储槽位:10923-16383

稍后我们进行水平扩容、水平缩容,都会对槽位进行重新分配,可多加留意。

启动新的节点

我这里在 10.211.55.6 服务器上新增两个节点,一个主,一个从。

  • 主master端口:8110
  • 从slave端口 :8120
# 创建两个文件夹,以端口命名
mkdir /root/redis-cluster/8110 /root/redis-cluster/8120

# 我这里直接从8010文件夹下复制文件到新的两个文件夹下,然后再用vim的%s命令替换端口,可自行查一下vim替换命令
cp /root/redis-cluster/8010/redis.conf /root/redis-cluster/8110/redis.conf
cp /root/redis-cluster/8010/redis.conf /root/redis-cluster/8120/redis.conf

# 启动两个服务
/root/redis/src/redis-server /root/redis-cluster/8110/redis.conf
/root/redis/src/redis-server /root/redis-cluster/8120/redis.conf

# 查看进程运行状态
[root@localhost ~]# ps -ef | grep redis
root      4533     1  0 22:18 ?        00:00:04 /root/redis/src/redis-server *:8010 [cluster]
root      4554     1  0 22:18 ?        00:00:05 /root/redis/src/redis-server *:8011 [cluster]
root      4858     1  0 22:19 ?        00:00:04 /root/redis/src/redis-server *:8012 [cluster]
root      9719     1  0 22:43 ?        00:00:00 /root/redis/src/redis-server *:8110 [cluster]
root      9737     1  0 22:43 ?        00:00:00 /root/redis/src/redis-server *:8120 [cluster]

需要注意的是这个时候,8110、8120进程只是启动完毕,但是并没有加入整个集群。

加入节点到集群

配置8110节点为master,并加入集群,Redis集群会把新加入的节点默认设为master主节点。

# 命令 newnode:port表示的是要加入的新节点  oldnode:port表示的是已经在集群的节点
redis-cli -a password --cluster add-node newnode:port oldnode:port
# 示例
/root/redis/src/redis-cli -a 666666 --cluster add-node 10.211.55.6:8110 10.211.55.7:8020

重新查看集群状态

# 进入节点
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.6 -p 8110
# 查看集群状态
10.211.55.6:8110> cluster nodes

从下图可以看到,8110端口已经添加到集群中,并且它是一个master节点,但是这个master它并没有任何solt槽位,也就意味着,这个master其实现在还是没用的阶段,我们需要为新的master节点手动分配solt槽位。redis-cluster-new-node

为新的master分配槽位

使用redis-cli命令为8110分配solt槽位,找到集群中的任意一个主节点,对其进行重新分片工作。

/root/redis/src/redis-cli -a 666666 --cluster reshard 10.211.55.7:8020

# 输出如下:
......
# 需要多少个solt槽位移动到指定节点上,直接设置,比如1000个solt槽位
How many slots do you want to move (from 1 to 16384)? 1000
# 接收这1000个槽位的redis节点id,我们填写8110端口的节点id
What is the receiving node ID? 0281fcbafef94b783ebf194f1b8c03440ed3af00

# 输入all,则表示从所有主节点中分别抽取响应的solt到指定的节点中,抽取的总solt数量为1000
Source node #1: all

# 输入yes确认开始执行分片任务
Do you want to proceed with the proposed reshard plan (yes/no)? yes

再次查看集群的最新状态

# 进入节点
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.6 -p 8110
# 查看集群状态
10.211.55.6:8110> cluster nodes

如下图所示,8110端口已经分配了solt,到目前为止,8110已经可以写入数据了。

  • 槽位1:0-332
  • 槽位2:5461-5794
  • 槽位3:10923-11255redis-cluster-newnode-revicesolt
为新的master分配slave节点

接下来为8110分配一个slave节点,也就是刚刚启动的8120端口。

# 命令 newnode:port表示的是要加入的新节点  oldnode:port表示的是已经在集群的节点
redis-cli -a password --cluster add-node newnode:port oldnode:port
# 示例 ,和添加8110端口的方法一样,但是添加进入后,因为是默认设置为master,所以后续要手动改为slave节点
/root/redis/src/redis-cli -a 666666 --cluster add-node 10.211.55.6:8120 10.211.55.7:8020

# 使用redis-cli 命令进入这个8120节点
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.6 -p 8120

# 使用命令设置为某一个master的slave节点 : cluster replicate masterid
10.211.55.6:8120> cluster replicate 0281fcbafef94b783ebf194f1b8c03440ed3af00
OK

再次查看集群状态,可以看到8120已经设置为8110的slave节点,水平扩容到此完毕。

redis-cluster-newslave

Redis 水平缩容

移除slave节点

水平缩容无非就是把指定的主从移除集群,准备移除刚刚添加并且分配了solt的8110主节点,8120从节点。

删除从节点,8120端口

/root/redis/src/redis-cli -a 666666 --cluster del-node 10.211.55.6:8120 93230cc3e90e371e7a3950e4ebfd3146402122c3

Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Removing node 93230cc3e90e371e7a3950e4ebfd3146402122c3 from cluster 10.211.55.6:8120
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

查看集群状态

# 进入任意一个节点
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.6 -p 8010

#查看节点信息,可以看到8120端口节点已经不在了
10.211.55.6:8010> cluster nodes
529aacc697943d76146858ff08b29fc255bc0fc6 10.211.55.6:8010@18010 myself,master - 0 1592666806000 1 connected 333-5460
0281fcbafef94b783ebf194f1b8c03440ed3af00 10.211.55.6:8110@18110 master - 0 1592666805101 10 connected 0-332 5461-5794 10923-11255
b93be2ad14af5724ad016c20be87981324770f36 10.211.55.7:8020@18020 master - 0 1592666806000 4 connected 5795-10922
040ba74db58cd2912b5b9f6445ddfc90de31d230 10.211.55.8:8031@18031 slave 0281fcbafef94b783ebf194f1b8c03440ed3af00 0 1592666806509 10 connected
1b57c2aa088f84e68711e92eb715b395d55dadb1 10.211.55.6:8011@18011 slave 23c3a630c3173af822e20338e33b49ce5ef38207 0 1592666805000 7 connected
23c3a630c3173af822e20338e33b49ce5ef38207 10.211.55.8:8030@18030 master - 0 1592666806609 7 connected 11256-16383
8428e0da601f849d96edb63dc6571642eb6874fd 10.211.55.6:8012@18012 slave b93be2ad14af5724ad016c20be87981324770f36 0 1592666805503 4 connected
3900369ca0098df78972291d638cf3f88ed6c554 10.211.55.7:8022@18022 slave 23c3a630c3173af822e20338e33b49ce5ef38207 0 1592666806509 7 connected
3e2108060ecdcdc4a220ab14c8f04202932be2f1 10.211.55.8:8032@18032 slave b93be2ad14af5724ad016c20be87981324770f36 0 1592666805000 9 connected
4e9195825d14d2ec74b593e158c9c229cc682d8c 10.211.55.7:8021@18021 slave 529aacc697943d76146858ff08b29fc255bc0fc6 0 1592666806000 5 connected

# 查看redis进程
[root@localhost ~]# ps -ef | grep redis
root      4533     1  0 22:18 ?        00:00:11 /root/redis/src/redis-server *:8010 [cluster]
root      4554     1  0 22:18 ?        00:00:12 /root/redis/src/redis-server *:8011 [cluster]
root      4858     1  0 22:19 ?        00:00:12 /root/redis/src/redis-server *:8012 [cluster]
root      9719     1  0 22:43 ?        00:00:07 /root/redis/src/redis-server *:8110 [cluster]
root      9737     1  0 22:43 ?        00:00:04 /root/redis/src/redis-server *:8120 [cluster]

# 手动kill掉8120
kill 9737
重新分配solt槽位

删除主节点,8110端口

删除主节点需要先把solt槽位分配给其他节点,一开始在把新节点加进来时,手动为其分配了1000个solt槽位,现在要移除这个master,那么就需要把这1000个槽位分配到其他任意一个节点上,目前还做不了平均分配功能。

# 调用重新分配命令,后面ip:port 指定任意一个master节点
/root/redis/src/redis-cli -a 666666 --cluster reshard 10.211.55.6:8010
#输出如下
......
#要移动多少槽位: 1000
How many slots do you want to move (from 1 to 16384)? 1000
# 接收槽位的master节点id,我这里写8010的id 
What is the receiving node ID? 529aacc697943d76146858ff08b29fc255bc0fc6
# 从哪个节点id抽取,这里填写8110的节点id
Source node #1: 0281fcbafef94b783ebf194f1b8c03440ed3af00
# 然后输入done 准备迁移计划
Source node #2: done
# 输入yes开始迁移
Do you want to proceed with the proposed reshard plan (yes/no)? yes

查看当前集群情况

# 进入任意节点
/root/redis/src/redis-cli -a 666666 -c -h 10.211.55.6 -p 8010
# 查看节点信息
10.211.55.6:8010> cluster nodes

如下图所示,我们的8110主节点,已经没有任何的solt槽位了,这时候已经迁移成功了。 redis-cluster-node-remove

移除master节点

接下来就是大胆的删除这个节点

# 执行删除命令
/root/redis/src/redis-cli -a 666666 --cluster del-node 10.211.55.6:8110 0281fcbafef94b783ebf194f1b8c03440ed3af00

>>> Removing node 0281fcbafef94b783ebf194f1b8c03440ed3af00 from cluster 10.211.55.6:8110
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

# 查看集群信息,已经没有8110这个节点了,水平缩容也到此结束,别忘了最后kill掉8110的进程
10.211.55.6:8010> cluster nodes
529aacc697943d76146858ff08b29fc255bc0fc6 10.211.55.6:8010@18010 myself,master - 0 1592667896000 11 connected 0-5794 10923-11255
b93be2ad14af5724ad016c20be87981324770f36 10.211.55.7:8020@18020 master - 0 1592667896000 4 connected 5795-10922
040ba74db58cd2912b5b9f6445ddfc90de31d230 10.211.55.8:8031@18031 slave 529aacc697943d76146858ff08b29fc255bc0fc6 0 1592667897744 11 connected
1b57c2aa088f84e68711e92eb715b395d55dadb1 10.211.55.6:8011@18011 slave 23c3a630c3173af822e20338e33b49ce5ef38207 0 1592667897000 7 connected
23c3a630c3173af822e20338e33b49ce5ef38207 10.211.55.8:8030@18030 master - 0 1592667897000 7 connected 11256-16383
8428e0da601f849d96edb63dc6571642eb6874fd 10.211.55.6:8012@18012 slave b93be2ad14af5724ad016c20be87981324770f36 0 1592667896000 4 connected
3900369ca0098df78972291d638cf3f88ed6c554 10.211.55.7:8022@18022 slave 23c3a630c3173af822e20338e33b49ce5ef38207 0 1592667898044 7 connected
3e2108060ecdcdc4a220ab14c8f04202932be2f1 10.211.55.8:8032@18032 slave b93be2ad14af5724ad016c20be87981324770f36 0 1592667897542 9 connected
4e9195825d14d2ec74b593e158c9c229cc682d8c 10.211.55.7:8021@18021 slave 529aacc697943d76146858ff08b29fc255bc0fc6 0 1592667897039 11 connected

Redis 集群原理分析

Redis的slots槽位,分片

在redis集群中,它会给每一个master分配一定的槽位,在创建集群时第一次分配,具体为0-16383。比如我现在是三个master,那么就会把16383分配给我的三个master,每个节点负责一部分槽位,槽位的信息会存储于每个节点中。

redis客户端在连接上集群后,会获得所有master信息,包含它的solt槽位信息并且缓存在本地,当客户端要执行某个命令时,会对key进行solt运算,这样就可以立即定位到目标节点。

假设现在有一个命令 "set zhangsan 666 " ,那么redis客户端会对这个 "zhangsan" 这个key进行crc16运算得到一个数,再然后用这个数 对 16384进行取模运算,最后得到的solt必然是 16384 以内的数字。

客户端在获得这个solt后,就会把命令发送到这个solt所对应的redis服务,假设 "zhangsan"这个key的solt为10999,那么redis客户端会把命令发送到图下的最右边的master节点中。

我这里简单的画了一个图redis-cluster-solts

跳转重定位

当redis集群出现水平扩容,或者水平缩容等情况,那么redis各个服务之间的sol会有所变动。

如果这时候客户端计算key的solt后,用它已有的缓存去定位solt所对应的节点,有可能这个节点已经不对应这个solt了,当客户端向这个错误的节点发出了指令,该节点会发现指令的key所在的槽位不归属于自己管理,这时候节点会反馈给客户端一个特殊的跳转指令,并携带目标操作的节点地址,客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位缓存,后续所有的key将会使用新的槽位缓存。

Redis 集群节点间的通信机制

redis cluster 节点间采取gossip协议进行通信,各个节点之间会保持连接。

集中式维护集群元数据

​ 有点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以感知到;不足自安于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。

gossip维护集群元数据

​ gossip协议包含多种消息,包括ping,pong,meet,fail等等。

	- ping : 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
	- pong:返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新;
	- fail:某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了;
	- meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信,不需要发送形成网络所需的所有cluster meet命令,发送cluster meet消息以便每个节点能够达到其他每个节点只需通过一条已知的节点链就够了,由于在心跳包中会交换gossip信息,将会创建节点间确实的链接。

gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他介个节点收到ping消息滞后会返回pong消息。

网络抖动

真实环境中,机房网络往往会出现各种小问题,比如常见的网络抖动,可能突然造成部分连接的不可访问,然后很快又恢复正常。

为解决这种问题,Redis Clister 提供了一个参数 cluster-node-timeout配置,标识当某个节点持续timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换,如果没有这个配置,网络抖动会导致主从频繁切换(数据的重新复制)。

Redis集群选举原理

当slave发现自己的master节点不可用时,便尝试发送消息到其他节点,告知master节点已不可用,请求升级为新的master节点,由于挂掉的master可能会有多个slave,那就存在多个slave节点竞争成为master节点的可能。

  1. slave发现自己的master不可用,变为了fail
  2. 将自己记录的集群currentEpoch加1,并且广播FAILOVER_AUTH_REQUEST 信息
  3. 其他节点收到信息,只有master响应,先判断请求者的合法性,再发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
  4. 尝试请求升级为master的slave节点,会收集其他master节点反馈的FAILOVER_AUTH_ACK
  5. slave收到超过半数的master的ack后就会变成新的master(如果只有两个master主节点,当其中一个挂了,只剩一个主节点,那么是不能选举成功的,所以集群至少应该要有三个主节点)
  6. 广播pong消息通知其他集群节点

从节点并不是在主节点一进入FAIL状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其他master或许尚未意识到FAIL状态,可能会拒绝投票

  • 延迟计算公式:
    • DELAY = 500ms + random(0-500ms) + SLAVE_RANK * 1000ms
      • SLAVE_RANK 所表示的是此slave已经从master复制数据的总量的Rank,Rank越小代表已复制的数据越新,这种方式下,持有最新数据的slave将会首先发起选举(理论上),这样可以极大程度的避免多个slave竞争。如果多个slave竞争选举,并且所获得的master投票一致,那么将会重新发起选举。
集群是否完整才对外提供服务

当redis.conf的配置cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线并且没有对应的从库进行故障恢复时,集群的其他master节点仍然可用,如果为yes则集群不可用。

集群为什么至少需要三个master节点

因为新master的选举需要大于半数的集群master节点同意才能选举成功,假设只有两个master节点,当其中一个挂了的情况下,slave获得的投票是达不到升级为master的条件的。

奇数个master节点可以在满足选举选举条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点,大家都能选举出新的master节点,如果都挂了两个master节点,那么都没法选举新master节点,因为slave获取不到半数的投票,所以master节点部署为奇数的个数更多的是从节省机器资源角度来考虑,当然如果奇数不能满足性能要求,偶数刚好合适,那么部署偶数也可以。

哨兵leader 选举流程

当一个master服务被某个sentinel认为下线状态,不可用状态后,该sentinel会与其他sentinel协商选出sentinel的leader进行故障转移工作。每个发现master服务不可用的sentinel都可以要求其他sentinel选自己为sentinel的leader,选举是先到先得。

同时每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel的leader。

如果所有超过一半的sentinel选举某个sentinel作为leader,之后由该sentinel进行故障转移操作,从存活的slave节点中选举出新的master,这个选举过程跟集群的master选举类似。

即使哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,可以正常选举新的master。

一般为了高可用推荐至少部署三个哨兵节点,为什么推荐奇数为个数部署哨兵节点,原理与集群奇数个master节点类似。