一、Redis 集群概述

Redis 主从复制

到 目前 为止,我们所学习的 Redis 都是 单机版 的,这也就意味着一旦我们所依赖的 Redis 服务宕机了,我们的主流程也会受到一定的影响,这当然是我们不能够接受的。

所以一开始我们的想法是:搞一台备用机。这样我们就可以在一台服务器出现问题的时候切换动态地到另一台去:

Redis主从哨兵详解 redis主从 哨兵 集群_redis

幸运的是,两个节点数据的同步我们可以使用 Redis 的 主从同步 功能帮助到我们,这样一来,有个备份,心里就踏实多了。

Redis 哨兵

后来因为某种神秘力量,Redis 老会在莫名其妙的时间点出问题 (比如半夜 2 点),我总不能 24 小时时刻守在电脑旁边切换节点吧,于是另一个想法又开始了:给所有的节点找一个 管家,自动帮我监听照顾节点的状态并切换:

Redis主从哨兵详解 redis主从 哨兵 集群_Redis主从哨兵详解_02

这大概就是 Redis 哨兵(Sentinel) 的简单理解啦。什么?管家宕机了怎么办?相较于有大量请求的 Redis 服务来说,管家宕机的概率就要小得多啦. 如果真的宕机了,我们也可以直接切换成当前可用的节点保证可用.

Redis 集群化

好了,通过上面的一些解决方案我们对 Redis 的 稳定性 稍微有了一些底气了,但单台节点的计算能力始终有限,所谓人多力量大,如果我们把 多个节点组合 成 一个可用的工作节点,那就大大增加了 Redis 的 高可用、可扩展、分布式、容错 等特性:

Redis主从哨兵详解 redis主从 哨兵 集群_redis_03

二、主从复制

Redis主从哨兵详解 redis主从 哨兵 集群_Redis_04

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是 单向 的,只能由主节点到从节点。Redis 主从复制支持 主从同步 和 从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。

主从复制主要的作用

- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 (实际上是一种服务的冗余)。

- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。

- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。

快速体验

在 Redis 中,用户可以通过执行 SLAVEOF 命令或者设置 slaveof 选项,让一个服务器去复制另一个服务器,以下三种方式是 完全等效 的:

- 配置文件:在从服务器的配置文件中加入:slaveof  

- 启动命令:redis-server 启动命令后加入 --slaveof  

- 客户端命令:Redis 服务器启动后,直接通过客户端执行命令:slaveof  ,让该 Redis 实例成为从节点。

需要注意的是:主从复制的开启,完全是在从节点发起的,不需要我们在主节点做任何事情。

第一步:本地启动两个节点

在正确安装好 Redis 之后,我们可以使用 redis-server --port 的方式指定创建两个不同端口的 Redis 实例,例如,下方我分别创建了一个 6379 和 6380 的两个 Redis 实例:

# 创建一个端口为 6379 的 Redis 实例
redis-server --port 6379
# 创建一个端口为 6380 的 Redis 实例
redis-server --port 6380

此时两个 Redis 节点启动后,都默认为 主节点。

第二步:建立复制

我们在 6380 端口的节点中执行 slaveof 命令,使之变为从节点:

# 在 6380 端口的 Redis 实例中使用控制台
redis-cli -p 6380
# 成为本地 6379 端口实例的从节点
127.0.0.1:6380> SLAVEOF 127.0.0.1ø 6379
OK

第三步:观察效果

下面我们来验证一下,主节点的数据是否会复制到从节点之中:

- 先在 从节点 中查询一个 不存在 的 key:

127.0.0.1:6380> GET mykey
(nil)

- 再在 主节点 中添加这个 key:

127.0.0.1:6379> SET mykey myvalue
OK

- 此时再从 从节点 中查询,会发现已经从 主节点 同步到 从节点:

127.0.0.1:6380> GET mykey
"myvalue"

第四步:断开复制

通过 slaveof  命令建立主从复制关系以后,可以通过 slaveof no one 断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。

从节点执行 slaveof no one 之后,从节点和主节点分别打印日志如下:

# 从节点打印日志
61496:M 17 Mar 2020 08:10:22.749 # Connection with master lost.
61496:M 17 Mar 2020 08:10:22.749 * Caching the disconnected master state.
61496:M 17 Mar 2020 08:10:22.749 * Discarding previously cached master state.
61496:M 17 Mar 2020 08:10:22.749 * MASTER MODE enabled (user request from 'id=4 addr=127.0.0.1:55096 fd=8 name= age=1664 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=34 qbuf-free=32734 obl=0 oll=0 omem=0 events=r cmd=slaveof')
# 主节点打印日志
61467:M 17 Mar 2020 08:10:22.749 # Connection with replica 127.0.0.1:6380 lost.

实现原理简析

Redis主从哨兵详解 redis主从 哨兵 集群_服务器_05

为了节省篇幅,我把主要的步骤都 浓缩 在了上图中,其实也可以 简化成三个阶段:准备阶段-数据同步阶段-命令传播阶段。下面我们来进行一些必要的说明。

身份验证 | 主从复制安全问题

在上面的 快速体验 过程中,你会发现 slaveof 这个命令居然不需要验证?这意味着只要知道了 ip 和端口就可以随意拷贝服务器上的数据了?

那当然不能够了,我们可以通过在 主节点 配置 requirepass 来设置密码,这样就必须在 从节点 中对应配置好 masterauth 参数 (与主节点 requirepass 保持一致) 才能够进行正常复制了。

SYNC 命令是一个非常耗费资源的操作

每次执行 SYNC 命令,主从服务器需要执行如下动作:

1. 主服务器 需要执行 BGSAVE 命令来生成 RDB 文件,这个生成操作会 消耗 主服务器大量的 CPU、内存和磁盘 I/O 的资源;

2. 主服务器 需要将自己生成的 RDB 文件 发送给从服务器,这个发送操作会 消耗 主服务器 大量的网络资源(带宽和流量),并对主服务器响应命令请求的时间产生影响;

3. 接收到 RDB 文件的 从服务器 需要载入主服务器发来的 RBD 文件,并且在载入期间,从服务器 会因为阻塞而没办法处理命令请求;

特别是当出现 断线重复制 的情况是时,为了让从服务器补足断线时确实的那一小部分数据,却要执行一次如此耗资源的 SYNC 命令,显然是不合理的。

PSYNC 命令的引入

所以在 Redis 2.8 中引入了 PSYNC 命令来代替 SYNC,它具有两种模式:

1. 全量复制: 用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作;

2. 部分复制: 用于网络中断等情况后的复制,只将 中断期间主节点执行的写命令 发送给从节点,与全量复制相比更加高效。需要注意 的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制;

部分复制的原理主要是靠主从节点分别维护一个 复制偏移量,有了这个偏移量之后断线重连之后一比较,之后就可以仅仅把从服务器断线之后确实的这部分数据给补回来了。

三、Redis Sentinel 哨兵

Redis主从哨兵详解 redis主从 哨兵 集群_服务器_06

上图 展示了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点:

- 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;

- 数据节点:主节点和从节点都是数据节点;

在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下方是官方对于哨兵功能的描述:

- 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。

- 自动故障转移(Automatic failover):当 主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。

- 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。

- 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

快速体验

第一步:创建主从节点配置文件并启动

正确安装好 Redis 之后,我们去到 Redis 的安装目录 (mac 默认在 /usr/local/),找到 redis.conf 文件复制三份分别命名为 redis-master.conf/redis-slave1.conf/redis-slave2.conf,分别作为 1 个主节点和 2 个从节点的配置文件 (下图演示了我本机的 redis.conf 文件的位置)

Redis主从哨兵详解 redis主从 哨兵 集群_Redis主从哨兵详解_07

打开可以看到这个 .conf 后缀的文件里面有很多说明的内容,全部删除然后分别改成下面的样子:

#redis-master.conf
port 6379
daemonize yes
logfile "6379.log"
dbfilename "dump-6379.rdb"
#redis-slave1.conf
port 6380
daemonize yes
logfile "6380.log"
dbfilename "dump-6380.rdb"
slaveof 127.0.0.1 6379
#redis-slave2.conf
port 6381
daemonize yes
logfile "6381.log"
dbfilename "dump-6381.rdb"
slaveof 127.0.0.1 6379

然后我们可以执行 redis-server  来根据配置文件启动不同的 Redis 实例,依次启动主从节点:

redis-server /usr/local/redis-5.0.3/redis-master.conf
redis-server /usr/local/redis-5.0.3/redis-slave1.conf
redis-server /usr/local/redis-5.0.3/redis-slave2.conf

节点启动后,我们执行 redis-cli 默认连接到我们端口为 6379 的主节点执行 info Replication 检查一下主从状态是否正常:(可以看到下方正确地显示了两个从节点)

Redis主从哨兵详解 redis主从 哨兵 集群_Redis_08

第二步:创建哨兵节点配置文件并启动

按照上面同样的方法,我们给哨兵节点也创建三个配置文件。(哨兵节点本质上是特殊的 Redis 节点,所以配置几乎没什么差别,只是在端口上做区分就好)

# redis-sentinel-1.conf
port 26379
daemonize yes
logfile "26379.log"
sentinel monitor mymaster 127.0.0.1 6379 2
# redis-sentinel-2.conf
port 26380
daemonize yes
logfile "26380.log"
sentinel monitor mymaster 127.0.0.1 6379 2
# redis-sentinel-3.conf
port 26381
daemonize yes
logfile "26381.log"
sentinel monitor mymaster 127.0.0.1 6379 2

其中,sentinel monitor mymaster 127.0.0.1 6379 2 配置的含义是:该哨兵节点监控 127.0.0.1:6379 这个主节点,该主节点的名称是 mymaster,最后的 2 的含义与主节点的故障判定有关:至少需要 2 个哨兵节点同意,才能判定主节点故障并进行故障转移。

执行下方命令将哨兵节点启动起来:

redis-server /usr/local/redis-5.0.3/redis-sentinel-1.conf --sentinel
redis-server /usr/local/redis-5.0.3/redis-sentinel-2.conf --sentinel
redis-server /usr/local/redis-5.0.3/redis-sentinel-3.conf --sentinel

使用 redis-cil 工具连接哨兵节点,并执行 info Sentinel 命令来查看是否已经在监视主节点了:

# 连接端口为 26379 的 Redis 节点
➜  ~ redis-cli -p 26379
127.0.0.1:26379> info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

此时你打开刚才写好的哨兵配置文件,你还会发现出现了一些变化:

第三步:演示故障转移

首先,我们使用 kill -9 命令来杀掉主节点,同时 在哨兵节点中执行 info Sentinel 命令来观察故障节点的过程:

➜  ~ ps aux | grep 6379
longtao          74529   0.3  0.0  4346936   2132   ??  Ss   10:30上午   0:03.09 redis-server *:26379 [sentinel]
longtao          73541   0.2  0.0  4348072   2292   ??  Ss   10:18上午   0:04.79 redis-server *:6379
longtao          75521   0.0  0.0  4286728    728 s008  S+   10:39上午   0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn 6379
longtao          74836   0.0  0.0  4289844    944 s006  S+   10:32上午   0:00.01 redis-cli -p 26379
➜  ~ kill -9 73541

如果 刚杀掉瞬间 在哨兵节点中执行 info 命令来查看,会发现主节点还没有切换过来,因为哨兵发现主节点故障并转移需要一段时间:

# 第一时间查看哨兵节点发现并未转移,还在 6379 端口
127.0.0.1:26379> info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

一段时间之后你再执行 info 命令,查看,你就会发现主节点已经切换成了 6381 端口的从节点:

# 过一段时间之后在执行,发现已经切换了 6381 端口
127.0.0.1:26379> info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6381,slaves=2,sentinels=3

但同时还可以发现,哨兵节点认为新的主节点仍然有两个从节点(上方 slaves=2),这是因为哨兵在将 6381 切换成主节点的同时,将 6379 节点置为其从节点。虽然 6379 从节点已经挂掉,但是由于 哨兵并不会对从节点进行客观下线,因此认为该从节点一直存在。当 6379 节点重新启动后,会自动变成 6381 节点的从节点。

另外,在故障转移的阶段,哨兵和主从节点的配置文件都会被改写:

- 对于主从节点:主要是 slaveof 配置的变化,新的主节点没有了 slaveof 配置,其从节点则 slaveof 新的主节点。

- 对于哨兵节点:除了主从节点信息的变化,纪元(epoch) (记录当前集群状态的参数) 也会变化,纪元相关的参数都 +1 了。

新的主服务器是怎样被挑选出来的?

故障转移操作的第一步 要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送 slaveof no one 命令,将这个从服务器转换为主服务器。但是这个从服务器是怎么样被挑选出来的呢?

简单来说 Sentinel 使用以下规则来选择新的主服务器:

1. 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 淘汰。

2. 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰。

3. 在 经历了以上两轮淘汰之后 剩下来的从服务器中, 我们选出 复制偏移量(replication offset)最大 的那个 从服务器 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。

四、Redis 集群

Redis主从哨兵详解 redis主从 哨兵 集群_服务器_09

上图 展示了 Redis Cluster 典型的架构图,集群中的每一个 Redis 节点都 互相两两相连,客户端任意 直连 到集群中的 任意一台,就可以对其他 Redis 节点进行 读写 的操作。

基本原理

Redis主从哨兵详解 redis主从 哨兵 集群_Redis_10

集群的主要作用

1. 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制 存储容量大大增加;另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,bgsave 和 bgrewriteaof 的 fork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……

2. 高可用: 集群支持主从复制和主节点的 自动故障转移(与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。