2

1. Redis集群概述

Redis作为当前非常热门的内存型数据结构存储,可用于数据存储,缓存和消息代理等。本文将讲解如何基于docker搭建Redis集群。

Redis的集群设计方案中,主从模式是基础,再在这个基础上发展:哨兵模式、集群模式。

1.1. 主从模式

主从复制在数据库中很常见,一般用来做读写分离,Redis中也是如此。要求只有1个Master(主节点),可以有N个slaver(从节点),而且Slaver也可以有自己的Slaver,由于这种主从的关系决定他们是在配置阶段就要指定他们的上下级关系,而不是Zookeeper那种平行关系是自主推优出来的。

读写分离,Master只负责写和同步数据给Slaver,Slaver承担了被读的任务,所以Slaver的扩容只能提高读效率不能提高写效率。

Slaver先将Master那边获取到的信息压入磁盘,再load进内存,client端是从内存中读取信息的。当一个新的Slaver加入到这个集群时,会主动找Master来拜码头,Master发现新的小弟后将全量数据发送给新的Slaver,数据量越大性能消耗也就越大,所以尽量避免在运行时做Slaver的扩容。

  • 优点:读写分离,通过增加Slaver可以提高并发读的能力。
  • 缺点:Master写能力是瓶颈,维护Slaver开销也总将会变成瓶颈。

1.2. 哨兵模式

哨兵模式的前提,就是基于主从模式。在Redis的服务中,可以有多台服务器,还可以配置主从服务器,通过配置使得从机能够从主机同步数据。在这种配置下,当主Redis服务器出现故障时,只需要执行故障切换(failover)即可,也就是作废当前出故障的主Redis服务器,将从Redis服务器切换为主Redis服务器即可。

这个过程可以由人工完成,也可以由程序完成,如果由人工完成,则需要增加人力成本,且容易产生人工错误,还会造成一段时间的程序不可用,所以一般来说,我们会选择使用程序完成。这个程序就是我们所说的哨兵(sentinel)

哨兵是一个程序进程,它运行于系统中,通过发送命令去检测各个Redis服务器(包括主从Redis服务器)。它会通过发送命令来监测各个Redis主从服务器是否可用。当主服务器出现故障不可用时,哨兵监测到这个故障后,就会启动故障切换机制,作废当前故障的主Redis服务器,将其中的一台Redis从服务器修改为主服务器,然后将这个消息发给各个从服务器,使得它们也能做出对应的修改,这样就可以保证系统继续正常工作了。通过这段论述大家可以看出,哨兵进程实际就是代替人工,保证Redis的高可用,使得系统更加健壮。

然而有时候单个哨兵也可能不太可靠,因为哨兵本身也可能出现故障,所以Redis还提供了多哨兵模式。多哨兵模式可以有效地防止单哨兵不可用的情况,即哨兵集群

多个哨兵会相互监控,使得哨兵模式更为健壮,在这个机制中,即使某个哨兵出现故障不可用,其他哨兵也会监测整个Redis主从服务器,使得服务依旧可用。不过,故障切换方式和单哨兵模式的完全不同,这里我们通过假设举例进行说明。

假设Redis主服务器不可用,哨兵1首先监测到了这个情况,这个时候哨兵1不会立即进行故障切换,而是仅仅自己认为主服务器不可用而已,这个过程被称为主观下线

因为Redis主服务器不可用,跟着后续的哨兵(如哨兵2和3)也会监测到这个情况,所以它们也会做主观下线的操作。如果哨兵的主观下线达到了一定的数量,各个哨兵就会发起一次投票,选举出新的Redis主服务器,然后将原来故障的主服务器作废,将新的主服务器的信息发送给各个从Redis服务器做调整,这个时候就能顺利地切换到可用的Redis服务器,保证系统持续可用了,这个过程被称为客观下线

哨兵是独立于redis之外的进程,也是需要单独部署的。如果需要安装哨兵插件,需要单独下载安装,如下:

sudo apt install redis-sentinel

sentinel最好不要和redis部署在同一台机器,不然redis的服务器挂了以后,sentinel也挂了。

1.3. 集群模式

哨兵模式只能说是保证主从模式的健壮性。但主从模式只能加强“读”的并发,并不能拓展“写”的并发。

而集群模式,即能拓展“写”并发,又融入了哨兵模式、主从模式的优点,算是加强版。不过因为是后面诞生的,redis集群是3.0版本之后才提供的集群模式。

1. 哈希Slot,“写”拓展

哈希Slot名字上可能不好理解,其实就是数据库中的“水平划分”。如果你之前有了解过数据库的表分区的话,就会发现下来对于哈希Slot的描述,就和数据库表分区里面的“HASH分区”原理上大致相同。

图片描述

对象保存到Redis之前先经过CRC16哈希到一个指定的Node上,例如图中Object4最终Hash到了Node1上。

 每个Node被平均分配了一个Slot段,对应着0-16384,Slot不能重复也不能缺失,否则会导致对象重复存储或无法存储。

 Node之间也互相监听,一旦有Node退出或者加入,会按照Slot为单位做数据的迁移。例如Node1如果掉线了,0-5640这些Slot将会平均分摊到Node2和Node3上,由于Node2和Node3本身维护的Slot还会在自己身上不会被重新分配,所以迁移过程中不会影响到 5641-16384 Slot段的使用。

  • 优点:将Redis的写操作分摊到了多个节点上,提高写的并发能力,扩容简单。
  • 缺点:每个Node承担着互相监听、高并发数据写入、高并发数据读出,工作任务繁重。
2. 主从+哈希slot,读写双全

看到这里大家也就发现了,主从模式和哈希的设计优缺点正好是相互弥补的,将二者结合在一起,就是Redis集群的终极形态,先Hash分逻辑节点,然后每个逻辑节点内部是主从,如图:

图片描述

3. 替代哨兵监控

哨兵模式中,需要单独部署哨兵集群,用来监控各个redis节点健康状态,以及选举切换master。但集群模式就直接包含了哨兵的功能。

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

  1. slave发现自己的master变为FAIL
  2. 将自己记录的集群currentEpoch(选举轮次标记)加1,并广播信息给集群中其他节点
  3. 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送结果
  4. 尝试选举的slave收集master返回的结果,收到超过半数master的统一后变成新Master
  5. 广播Pong消息通知其他集群节点。

因为最终有投票权当只有master,且需要大于半数的集群master节点同意才能选举成功,所以推荐redis集群中节点数为奇数。至少3个,因为如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。

2. 集群安装

默认我们已经有了docker环境,现在开始基于docker安装Redis集群。redis 5.0 版本之前,网上有很多教程都是通过redis-trib.rb 来创建集群,但是redis 5.0 版本以后,就只能通过 redis-cli 来实现。本文即通过 redis-cli 创建集群,因为redis集群的节点选举方式是需要半数以上的master通过,所以建议创建奇数个节点。本例中创建3个Master节点,并为每个Master节点各分配1个Slave节点。

2.1. 宿主机环境

首先需要找一份原始的redis.conf文件,将其重命名为:redis-cluster.tmpl,并配置如下几个参数,此文件的目的是生成每一个redis实例的redis.conf:

[root@kerry2 redis]# wget https://raw.githubusercontent.com/antirez/redis/5.0/redis.conf
[root@kerry2 redis]# mv redis.conf redis-cluster.tmpl

vi redis-cluster.tmpl

# bind 127.0.0.1
protected-mode no
port ${PORT}
daemonize no
dir /data/redis
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 15000

然后执行下列脚本,给3个Master和3个Slave创建各自的挂载卷目录

# 创建 master 和 slave 文件夹
for port in `seq 7700 7705`; do
    ms="master"
    if [ $port -ge 7703 ]; then
        ms="slave"
    fi
    mkdir -p ./$ms/$port/ && mkdir -p ./$ms/$port/data \
    && PORT=$port envsubst < ./redis-cluster.tmpl > ./$ms/$port/redis.conf;
done

当前目录结构为

[root@kerry2 redis]# tree
.
├── master
│   ├── 7700
│   │   ├── data
│   │   └── redis.conf
│   ├── 7701
│   │   ├── data
│   │   └── redis.conf
│   └── 7702
│       ├── data
│       └── redis.conf
├── redis-cluster.tmpl
├── slave
     ├── 7703
     │   ├── data
     │   └── redis.conf
     ├── 7704
     │   ├── data
     │   └── redis.conf
     └── 7705
         ├── data
         └── redis.conf

2.2. 创建Redis节点

假设我们只考虑单纯的docker环境,并无docker-compose和k8s之类的服务编排,每个redis容器之间必须要保证通讯,可以通过创建docker network。(使用微服务编排的情况,后续再讨论)

[root@kerry2 redis]# docker network create redis-cluster-net

现在我们就可以运行docker redis 的 master 和 slave 实例了

# 运行docker redis 的 master 和 slave 实例
for port in `seq 7700 7705`; do
    ms="master"
    if [ $port -ge 7703 ]; then
        ms="slave"
    fi
    docker run -d -p $port:$port -p 1$port:1$port \
    -v $PWD/$ms/$port/redis.conf:/data/redis.conf \
    -v $PWD/$ms/$port/data:/data/redis \
    --restart always --name redis-$ms-$port --net redis-cluster-net \
    redis redis-server /data/redis.conf;
done

查看已创建的redis容器

[root@kerry2 redis]# docker ps |grep redis
010f295922e3        redis                                                                                                  "docker-entrypoint..."   41 seconds ago       Up 36 seconds       0.0.0.0:7705->7705/tcp, 6379/tcp, 0.0.0.0:17705->17705/tcp   redis-slave-7705
b5d89f0469ee        redis                                                                                                  "docker-entrypoint..."   45 seconds ago       Up 40 seconds       0.0.0.0:7704->7704/tcp, 6379/tcp, 0.0.0.0:17704->17704/tcp   redis-slave-7704
f710e805fe96        redis                                                                                                  "docker-entrypoint..."   50 seconds ago       Up 45 seconds       0.0.0.0:7703->7703/tcp, 6379/tcp, 0.0.0.0:17703->17703/tcp   redis-slave-7703
b187603aec65        redis                                                                                                  "docker-entrypoint..."   55 seconds ago       Up 50 seconds       0.0.0.0:7702->7702/tcp, 6379/tcp, 0.0.0.0:17702->17702/tcp   redis-master-7702
ea635bd8b3dc        redis                                                                                                  "docker-entrypoint..."   About a minute ago   Up 55 seconds       0.0.0.0:7701->7701/tcp, 6379/tcp, 0.0.0.0:17701->17701/tcp   redis-master-7701
f02a468572ca        redis                                                                                                  "docker-entrypoint..."   About a minute ago   Up About a minute   0.0.0.0:7700->7700/tcp, 6379/tcp, 0.0.0.0:17700->17700/tcp   redis-master-7700

2.3. 构建集群

通过redis-cli 命令构建集群,随便找一个redis容器,运行redis-cli --cluster create --cluster-replicas 1 ip:port 命令即可

[root@kerry2 redis]# docker exec -it redis-master-7700 redis-cli --cluster create 宿主ip:7700 宿主ip:7701 宿主ip:7702 宿主ip:7703 宿主ip:7704 宿主ip:7705 --cluster-replicas 1

# 提示输入yes后,构建集群成功

记住构建集群时,要保证节点redis数据为空,否则会出现下列错误。

[ERR] Node 172.18.0.2:7700 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.

2.4. 集群验证

集群搭建完成后,我们通过 redis-cli 命令连接集群节点验证一下。redis 集群节点的连接命令是通过 redis-cli -c -h ${ip} -p ${port}

[root@kerry2 ~]# docker exec -it redis-master-7700 redis-cli -c -h 宿主机ip  -p 7700
宿主机ip :7700> 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:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:23838
cluster_stats_messages_pong_sent:24283
cluster_stats_messages_sent:48121
cluster_stats_messages_ping_received:24278
cluster_stats_messages_pong_received:23838
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:48121
宿主机ip :7700> cluster nodes
056b99fe5993510c264e3e9a1fd1a04144da6a7b 172.18.0.2:7700@17700 myself,master - 0 1558578383000 1 connected 0-5460
73376f00b2837309d77b82d98984715f44eb2dcf 宿主机ip:7704@17704 slave 056b99fe5993510c264e3e9a1fd1a04144da6a7b 0 1558578388562 5 connected
20e4b509a54fb17ed8d0f6c21bbc8693ab715ee7 宿主机ip:7705@17705 slave 1bcb0a6ac770e261c5b0de21cfe26b0bd614590e 0 1558578386658 6 connected
1bcb0a6ac770e261c5b0de21cfe26b0bd614590e 宿主机ip:7701@17701 master - 0 1558578386579 2 connected 5461-10922
07a4c19848d578ac339bfaf741e1edfd0b010b08 宿主机ip:7702@17702 master - 0 1558578388661 3 connected 10923-16383
506271ed3f0657f05f439108d9372b638d2c4571 宿主机ip:7703@17703 slave 07a4c19848d578ac339bfaf741e1edfd0b010b08 0 1558578386000 4 connected

可以看到通过 "cluster info"命令看到集群的基本信息,所有的slot (16384) 都分配完毕。然后通过 "cluster nodes" 命令查看到每个master节点的slot分配的区域。至此,redis集群基本安装成功。

3. 后期运维

3.1. 基本命令

集群

cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。

节点

cluster meet <ip> <port> :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget <node_id> :从集群中移除 node_id 指定的节点。
cluster replicate <node_id> :将当前节点设置为 node_id 指定的节点的从节点。
cluster saveconfig :将节点的配置文件保存到硬盘里面。

槽(slot)

cluster addslots <slot> [slot ...] :将一个或多个槽( slot)指派( assign)给当前节点。
cluster delslots <slot> [slot ...] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot <slot> node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给
另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
cluster setslot <slot> migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot <slot> importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot <slot> stable :取消对槽 slot 的导入( import)或者迁移( migrate)。
键
cluster keyslot <key> :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot <slot> :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot <slot> <count> :返回 count 个 slot 槽中的键  

3.2. 常见问题

(1)redis-cluster 把所有的物理节点映射到[ 0 ~ 16383 ]个slot(哈希槽)上,cluster负责维护 node<->slot<->value。

(2)集群任意一个节点中,如果master挂掉,但是还有slaver,slave将自动升为 master,系统正常。

(3)集群任意一个节点中,如果master挂掉,并且没有slaver,集群将进入fail状态。

(4)如果集群超过半数以上节点的master挂掉,不管是否有slaver,集群都将进入fail状态。

(5)节点判断是否失效的选举,是集群中所有的master参与的,如果半数以上的master节点与当前被检测的master节点通讯检测超时(cluster-node-timerout),就认为当前master节点挂掉了。

4. 脚本和yaml

参考文档


KerryWu
641 声望159 粉丝

保持饥饿