Analysis of the principle of redis cluster and analysis of high-frequency interview questions

Get into the habit of writing together! This is the 5th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

High Availability Cluster Architecture

In versions prior to redis3.0, to implement a cluster, the Sentinel tool is generally used to monitor the status of the master node. If the master node is abnormal, the master-slave switch will be performed, and one of the slaves will be upgraded to the master. The sentinel configuration is slightly complicated, and the performance It also performs in general in terms of high availability performance, especially when the master-slave switch is instantaneously interrupted, generally ranging from a few seconds to tens of seconds, and the sentinel mode has only one master node to provide external services, so there is no way to support very high Concurrent, and the memory of a single node should not be set too large, otherwise the persistent file will be too large, affecting the efficiency of data recovery or master-slave synchronization.

Cluster Architecture Overview

Redis cluster is a distributed server group composed of multiple master and slave nodes, which has the characteristics of replication, high availability and sharding. Redis Cluster does not require Sentinel Sentinels to complete node removal and failover functions.

Each node needs to be set to cluster mode. There is no central node in this cluster mode, which can support horizontal expansion. In theory, it can expand to tens of thousands of nodes, but the official recommendation is no more than 1,000 nodes.

The cluster slave node does not support reading, and the read from the slave node will be redirected to the corresponding master node.

The performance and high availability of redis cluster are better than the sentinel mode of the previous version, and the cluster configuration is simpler.

Cluster Architecture Diagram

image-20220328171928730

Cluster construction

A redis cluster requires at least three master nodes . Let us build three master nodes and configure a slave for each master as an example. A total of 3 machines are needed, each machine has 1 master and 1 slave, and a total of 6 nodes. The specific steps are as follows :

# 在 redis 根目录新建 cluster 目录,并且在 cluster 目录下创建 6379 和 6380 两个目录,用于存放配置文件
cd /usr/local/redis-6.2.6
mkdir cluster
cd cluster/
mkdir 6379
mkdir 6380
# 拷贝之前搭建环境时备份的配置文件,如果是第一次可直接拷贝redis.conf到6379目录
# 当前所在目录:/usr/local/redis-6.2.6/cluster
cp ../redis.conf_bak 6379/redis.conf
# 1. 修改后台启动
daemonize yes
# redis 运行端口
port 6379 
# pid号写入哪个配置文件
pidfile /var/run/redis_6379.pid
# 指定数据文件的存放位置,因为需要在一台机器启动两个实例,所以必须指定不同的位置,不然会丢失数据
dir /usr/local/redis-6.2.6/cluster/6379/
# 开启集群模式
cluster-enabled yes
# 集群节点信息,启动后会自动生成该文件,写入集群信息,如果所有节点都挂了,后续集群启动的时候读取此配置文件,恢复集群信息
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
# 注释上允许所有网卡访问,
# bind 127.0.0.1 -::1
# 关闭保护模式
protected-mode no
# 开启 AOF 存储
appendonly yes
# 设置redis 访问密码
requirepass mypass-bf
# 设置集群间访问密码,跟上面保持一致
masterauth mypass-bf
复制代码

After the above file configuration is completed, copy a copy to the 6380directory , and then replace all 6379with the new one6380

cd 6380
cp ../6379/redis.conf ./
vim redis.conf
# 批量替换,:%s/源字符串/目的字符串/g
:%s/6379/6380/g
# 或者使用sed命令替换 sed -i 's/oldstring/newstring/g' full-path-file
sed -i 's/6379/6380/g' redis.conf
# 注意:mac os下需要强制要求备份,如果不需要备份文件 sed -i '' 's/6379/6s80/g' redis.conf
复制代码

然后在另外两台节点也创建相同的目录,把第一台节点上的配置文件使用 scp 拷贝到对应目录

# 38.7&38.8 机器分别执行以下命令
mkdir -p /usr/local/redis-6.2.6/cluster/6379
mkdir -p /usr/local/redis-6.2.6/cluster/6380
# 在38.6 执行以下命令
scp 6379/redis.conf [email protected]:/usr/local/redis-6.2.6/cluster/6379/
scp 6379/redis.conf [email protected]:/usr/local/redis-6.2.6/cluster/6379/
scp 6380/redis.conf [email protected]:/usr/local/redis-6.2.6/cluster/6380/
scp 6380/redis.conf [email protected]:/usr/local/redis-6.2.6/cluster/6380/
复制代码

启动 redis 集群

# 在每台机器分别执行以下命令
/usr/local/redis-6.2.6/src/redis-server /usr/local/redis-6.2.6/cluster/6379/redis.conf
/usr/local/redis-6.2.6/src/redis-server /usr/local/redis-6.2.6/cluster/6380/redis.conf
复制代码

关闭所有机器的防火墙

systemctl stop firewalld # 临时关闭防火墙
systemctl disable firewalld # 禁止开机启动
# 或者使用 iptables 清除路由策略,重启失效
iptables -F
iptables -X
复制代码

创建集群,各个游离的节点建立关系

/usr/local/redis-6.2.6/src/redis-cli -a mypass-bf --cluster create 172.16.38.6:6379 172.16.38.7:6379 172.16.38.8:6379 172.16.38.6:6380 172.16.38.7:6380 172.16.38.8:6380 --cluster-replicas 1
# 看到如下信息已经成功一半了
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 172.16.38.7:6380 to 172.16.38.6:6379
Adding replica 172.16.38.8:6380 to 172.16.38.7:6379
Adding replica 172.16.38.6:6380 to 172.16.38.8:6379
M: 54896a853973172bb6cc98aecc0f6b3d7be8edf3 172.16.38.6:6379
   slots:[0-5460] (5461 slots) master
M: bbaf5d12e6ef3d76e35feb3327fa9294e1f36d88 172.16.38.7:6379
   slots:[5461-10922] (5462 slots) master
M: 69c6c4421bd8be4734bf1234f88884f9d57e3a50 172.16.38.8:6379
   slots:[10923-16383] (5461 slots) master
S: 951cb6c625465d9f61e19a4c057ba684ce59adc7 172.16.38.6:6380
   replicates 69c6c4421bd8be4734bf1234f88884f9d57e3a50
S: 1d414bdc318e943bbba519a5f3bc8878d77712cc 172.16.38.7:6380
   replicates 54896a853973172bb6cc98aecc0f6b3d7be8edf3
S: c5442da329cf568fde4648e6164e3e6034d43788 172.16.38.8:6380
   replicates bbaf5d12e6ef3d76e35feb3327fa9294e1f36d88
Can I set the above configuration? (type 'yes' to accept):
# 输入yes
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 172.16.38.6:6379)
M: 54896a853973172bb6cc98aecc0f6b3d7be8edf3 172.16.38.6:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: 69c6c4421bd8be4734bf1234f88884f9d57e3a50 172.16.38.8:6379
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 951cb6c625465d9f61e19a4c057ba684ce59adc7 172.16.38.6:6380
   slots: (0 slots) slave
   replicates 69c6c4421bd8be4734bf1234f88884f9d57e3a50
M: bbaf5d12e6ef3d76e35feb3327fa9294e1f36d88 172.16.38.7:6379
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 1d414bdc318e943bbba519a5f3bc8878d77712cc 172.16.38.7:6380
   slots: (0 slots) slave
   replicates 54896a853973172bb6cc98aecc0f6b3d7be8edf3
S: c5442da329cf568fde4648e6164e3e6034d43788 172.16.38.8:6380
   slots: (0 slots) slave
   replicates bbaf5d12e6ef3d76e35feb3327fa9294e1f36d88
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
复制代码

验证集群信息

# 连接任意一个客户端,./src/redis-cli -c -h -a
# -a 指定访问密码,-c集群模式,-h指定连接服务端的ip地址,-p 端口号
./src/redis-cli -c -h 127.0.0.1 -p 6379 -a mypass-bf
# 查看集群信息
cluster info
# 查看集群列表
cluster nodes
# 关闭节点可使用 shutdown 命令
复制代码

Redis 集群工作原理分析

Redis cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。

当 Redis cluster 的客户端来连接集群时,客户端也会得到一份集群的槽位配置信息并将其缓存在客户端本地 redis.clients.jedis.providers.ClusterConnectionProvider#initializeSlotsCache。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽位定位算法

Cluster 默认会对 key 值使用 CRC16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模得到具体的槽位。

JedisClusterCRC16.getSlot(key);
getCRC16(key) & 16383
复制代码

跳转重定向

当客户端向一个错误的节点发出指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个新的节点获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有的 key 将使用新的槽位映射表。

image-20220329174117455

Redis 集群节点间通信机制

维护集群元数据(集群节点信息、主从角色、节点数量、各节点共享的数据等)有两种方式,集中式和 gossip。

集中式

**优点:**在于元数据的更新和读取时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以感知到;

**不足:**在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据存储的压力。

很多中间件都会借助 zookeeper 集中式存储元数据。

gossip

Redis cluster 节点间采取 gossip 协议进行通信。gossip 歇息包含多种消息,包括 ping、pong、meet、fail 等。

meet: 某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;

ping: 每个节点都会频繁的给其他节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换数据(类似自己感知到的集群节点增加和移除,hash slot 信息等);

pong: 对 ping 和 meet 消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;

fail: 某个节点判断另一个节点 fail 后,就发送 fail 给其他节点,通知其他节点,指定的节点宕机了。

**优点:**在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续的打到所有节点上去更新,有一定的延时,降低了压力;

**缺点:**在于元数据更新有延时,可能导致集群的一些操作会有一些滞后。

gossip

gossip 协议通信端口

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

网络抖动

真实世界的机房网络往往并不是风平浪静的,他们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。

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

Redis 集群选举原理分析

当 slave 发现自己的 master 变为 FAIL 状态时,便尝试进行 Failover 以期待成为新的 master。由于挂掉的 master 可能会有多个 slave,从而存在多个 slave 竞争成为 master 节点的过程,其过程如下:

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

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

延迟计算公式: 500ms + random(0~500ms) + SLAVE_RANK*1000ms

SLAVE_RANK: 表示此 slave 已经从 master 复制数据总量的 rank。rank 越小代表已复制的数据越新。这种方式下,理论上持有最新数据的 slave 将会首先发起选举。

currentEpoch: 这是一个集群状态相关的概念,可以当作记录集群状态变更的递增版本号。每个集群节点,都会通过 server.cluster->currentEpoch 记录当前的 currentEpoch。

集群节点创建时,不管是 master 还是 slave,都置 currentEpoch 为 0。当前节点接收到来自其他节点的包时,如果发送者的 currentEpoch(消息头部会包含发送者的 currentEpoch)大于当前节点的currentEpoch,那么当前节点会更新 currentEpoch 为发送者的 currentEpoch。因此,集群中所有节点的 currentEpoch 最终会达成一致,相当于对集群状态的认知达成了一致。

集群脑裂数据丢失问题

在哨兵架构中,redis的集群脑裂是某个master所在机器突然脱离了正常的网络,导致redis master节点跟redis slave节点和sentinel集群处于不同的网络分区,此时因为sentinel集群无法感知到master的存在,哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master,这个时候集群里就会有两个master,也就是所谓的脑裂。

出现集群脑裂后,如果客户端还在基于原来的master节点继续写入数据,那么新的master节点将无法同步这些数据,当网络问题解决之后,sentinel集群将原先的master节点降为slave节点,此时再从新的master中同步数据,将会造成大量的数据丢失。

一句话总结:一个集群出现多个主节点,主节点降为从节点时会从新的主节点复制数据。

规避方法可以在 redis 配置里加上参数,这种方法不可能百分之百避免数据丢失。

min-replicas-to-write 1 # 写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制的配置,比如集群总共三个节点可以配置1,加上了leader就是2,超过了半数
min-slaves-max-lag 10 # 数据复制和同步的延迟不能超过10秒,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求
复制代码

**注意:**这个配置在一定程度上会影响集群的可用性(AP),比如 slave 要是少于1个,这个集群就算 leader 正常也不能提供服务了,需要具体场景权衡选择。

集群是否完整才对外提供服务

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

Redis 集群为什么至少需要3个 master 节点,并且推荐节点数为奇数?

因为新 master 的选举需要大于半数的集群 master 节点同意才能选举成功,如果只有两个 master 节点,当其中一个挂了,是达不到选举新 master 的条件的。

部署四个节点,最多允许挂几个节点?

奇数个 master 节点可以在满足选举该条件的基础上节省一个节点,比如三个 master 节点和四个 master 节点的集群相比,大家如果都挂了一个 master 节点都能选举新 master 节点,如果挂了两个 master 节点都没法选举新 master 节点了。所以奇数的 master 节点更多的是从节省机器资源角度出发说的。

Redis 集群对批量操作命令的支持

对于类似 mset、mget 这样的多个 key 的原生批量操作命令,redis 集群只支持所有 key 落在同一 slot 的情况,如果有多个 key 一定要用 mset 命令在 redis 集群操作,则可以在 key 的前面加上 {XX} ,这样参数数据分片 hash 计算的只有大括号里面的值,这样能确保不同的 key 能落到同一 slot 里去,实例如下:

mset {user}:1:name beifeng {user}:1:age 18
复制代码

Suppose the hash slot values ​​calculated by name and age are different, but this command is executed under the cluster. After redis, the user in the curly brackets is used for hash slot calculation, so the calculated slot values ​​must be the same, and they all end up in the same slot.

// redis.clients.jedis.ClusterCommandArguments#processKey(byte[])
// redis.clients.jedis.util.JedisClusterCRC16#getSlot(byte[])
public static int getSlot(byte[] key) {
    if (key == null) {
        throw new JedisClusterOperationException("Slot calculation of null is impossible");
    } else {
        int s = -1;
        int e = -1;
        boolean sFound = false;

        for(int i = 0; i < key.length; ++i) {
            // 123 左大括号 {
            if (key[i] == 123 && !sFound) {
                s = i;
                sFound = true;
            }
            // 125 有大括号 }
            if (key[i] == 125 && sFound) {
                e = i;
                break;
            }
        }

        return s > -1 && e > -1 && e != s + 1 ? getCRC16(key, s + 1, e) & 16383 : getCRC16(key) & 16383;
    }
}
// redis.clients.jedis.util.JedisClusterCRC16#getSlot(java.lang.String)
复制代码

code example

Jedis operates redis cluster

Code example:

package jedis;

import java.util.HashSet;
import java.util.Set;

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

/**
 * @author chenbing
 * @date 2022/3/29
 */
public class ClusterTest {
    public static void main(String[] args) throws InterruptedException {
        ConnectionPoolConfig poolConfig = new ConnectionPoolConfig();
        poolConfig.setMaxTotal(20);
        poolConfig.setMaxIdle(10);
        poolConfig.setMinIdle(5);
        Set<HostAndPort> jedisClusterNodes = new HashSet<>();
        jedisClusterNodes.add(new HostAndPort("172.16.38.6", 6379));
        jedisClusterNodes.add(new HostAndPort("172.16.38.7", 6379));
        jedisClusterNodes.add(new HostAndPort("172.16.38.8", 6379));
        jedisClusterNodes.add(new HostAndPort("172.16.38.6", 6380));
        jedisClusterNodes.add(new HostAndPort("172.16.38.7", 6380));
        jedisClusterNodes.add(new HostAndPort("172.16.38.8", 6380));
        int i = 0;
        // 死循环,验证集群内某个节点挂掉以后会自动切换 
        while (true) {
            i++;
            try (JedisCluster jedisCluster =
                new JedisCluster(jedisClusterNodes, 6000, 5000, 10, "mypass-bf", poolConfig)) {
                System.out.println(jedisCluster.set("key" + i, "value" + i));
                System.out.println(jedisCluster.get("key" + i));
            }
            Thread.sleep(1000);
        }

    }
}

上述代码写了个死循环,当程序启动后随机杀死一个 master 节点,可以观察到大概 1s 的时间程序会恢复正常。跟之前的哨兵架构对比主从切换时间大大缩短了。

复制代码

Spring boot operates redis cluster

The springboot integration is relatively simple, just need to modify the configuration file.

spring:
  redis:
    timeout: 3000
    #    host: 172.16.38.6
    lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000
      cluster:
        refresh:
          # https://blog.csdn.net/yuandengta/article/details/106770151
          adaptive: true # 集群拓扑动态感应
    database: 0
    password: mypass-bf
    cluster:
      nodes: 172.16.38.6:6379,172.16.38.7:6379,172.16.38.8:6379,172.16.38.6:6380,172.16.38.7:6380,172.16.38.8:6380
#    sentinel: # 哨兵配置
#      master: mymaster # 配置文件配置的集群名称
#      nodes: 172.16.38.6:26379,172.16.38.7:26379,172.16.38.8:26379 # 哨兵的ip和端口
复制代码

Guess you like

Origin juejin.im/post/7085674931874168863