Redis系列之集群搭建

Posted by Jason Lee on 2020-12-04

Redis集群模式

Redis 主从集群

  • (1) 读写分离
    在redis主从架构中,Master节点负责处理写请求,Slave节点只处理读请求。对于写请求少,读请求多的场景,例如电商详情页,通过这种读写分离的操作可以大幅提高并发量,通过增加redis从节点的数量可以使得redis的QPS达到10W+。

  • (2) 主从同步
    Master节点接收到写请求并处理后,需要告知Slave节点数据发生了改变,保持主从节点数据一致的行为称为主从同步,所有的Slave都和Master通信去同步数据也会加大Master节点的负担,实际上,除了主从同步,redis也可以从从同步,我们在这里统一描述为主从同步。

Redis 主从集群原理

  1. 主从同步原理

  1. 命令传播

主从服务器为了保持一致,当主服务接受写操作的时候,将会同步给从服务器,保证两个服务器回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态

  1. 复制功能缺陷(SYNC)缺陷
    在Redis中,从服务器对主服务器的复制可以分为以下两种情况:
  • 初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同。
  • 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连上了主服务器,并继续复制主服务器。

每次执行SYNC命令,主从服务器需要执行以下动作:
1)主服务器需要执行BGSAVE命令来生成RDB文件,这个生成操作会耗费主服务器大量的CPU、内存和磁盘I/O资源。
2)主服务器需要将自己生成的RDB文件发送给从服务器,这个发送操作会耗费主从服务器大量的网络资源(带宽和流量),并对主服务器响应命令请求的时间产生影响。
3)接收到RDB文件的从服务器需要载入主服务器发来的RDB文件,并且在载入期间,从服务器会因为阻塞而没办法处理命令请求。
因为SYNC命令是一个如此耗费资源的操作,所以Redis有必要保证在真正有需要时才执行SYNC命令。

要理解这一情况,请看表15-2展示的断线后重复制例子。

上面给出的例子可能有一点理想化,因为在主从服务器断线期间,主服务器执行的写命令可能会有成百上千个之多,而不仅仅是两三个写命令。但总的来说,主从服务器断开的时间越短,主服务器在断线期间执行的写命令就越少,而执行少量写命令所产生的数据量通常比整个数据库的数据量要少得多,在这种情况下,为了让从服务器补足一小部分缺失的数据,却要让主从服务器重新执行一次SYNC命令,这种做法无疑是非常低效的。

  1. 复制功能缺陷(PSYNC)缺陷

PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:

  • ·其中完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。

  • ·而部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。

在了解了PSYNC命令的由来,以及部分重同步的工作方式之后,是时候来介绍一下部分重同步的实现细节了。

当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面

考虑以下这个例子:主从服务器当前的复制偏移量都为10086,但是就在主服务器要向从服务器传播长度为33字节的数据之前,从服务器A断线了,那么主服务器传播的数据将只有从服务器B和从服务器C能收到

在这之后,主服务器、从服务器B和从服务器C三个服务器的复制偏移量都将更新为10119,而断线的从服务器A的复制偏移量仍然停留在10086,这说明从服务器A与主服务器并不一致

假设从服务器A在断线之后就立即重新连接主服务器,并且成功,那么接下来,从服务器将向主服务器发送PSYNC命令,报告从服务器A当前的复制偏移量为10086,那么这时,主服务器应该对从服务器执行完整重同步还是部分重同步呢?如果执行部分重同步的话,主服务器将复制加压缓冲区的数据协会到从服务器当中。

Redis为复制积压缓冲区设置的默认大小为1MB,如果主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时间比较长,那么这个大小也许并不合适。如果复制积压缓冲区的大小设置得不恰当,那么PSYNC命令的复制重同步模式就不能正常发挥作用,因此,正确估算和设置复制积压缓冲区的大小非常重要。
复制积压缓冲区的最小大小可以根据公式secondwrite_size_per_second来估算:
·其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算)。
·而write_size_per_second则是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和)。
例如,如果主服务器平均每秒产生1 MB的写数据,而从服务器断线之后平均要5秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于5MB。
为了安全起见,可以将复制积压缓冲区的大小设为2
second*write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理。
至于复制积压缓冲区大小的修改方法,可以参考配置文件中关于repl-backlog-size选项的说明。

  • PSYNC命令的执行流程

Redis 主从集群搭建

    1. 目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@CentOS7 ~]# 
#root 目录下
redis
├── confs
│   ├── 6379
│   │   ├── dump.rdb
│   │   ├── redis_6379.pid
│   │   ├── redis.conf
│   │   └── redislog.log
│   ├── 6380
│   │   ├── redis_6380.pid
│   │   ├── redis.conf
│   │   └── redislog.log
│   ├── 6381
│   │   ├── dump.rdb
│   │   ├── redis_6381.pid
│   │   ├── redis.conf
│   │   └── redislog.log
│   └── redis.conf
    1. 修改配置文件

在配置之后,我们可以关闭防火墙。

1
2
3
4
5
bind 0.0.0.0                                # 监听外网端口
port 6379 # 端口
pidfile /root/redis/confs/6379/redis_6379.pid #pid 文件
logfile "/root/redis/confs/6379/redislog.log" #日志文件
daemonize yes #后台启动
    1. 启动服务器

在三个目录下分别执行下,然后启动服务器

1
2
3
[root@CentOS7 6380]# pwd
/root/redis/confs/6380
[root@CentOS7 6380]# redis-server ./redis.conf
    1. 执行主从命令

在6380 和 6381 执行一下命令

1
2
3
[root@CentOS7 6380]# redis-cli -p 6380
127.0.0.1:6380> SLAVEOF 192.168.100.10 6379
OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 6379 日志如下
18217:M 04 Dec 11:19:23.211 * Slave 192.168.100.10:6380 asks for synchronization
18217:M 04 Dec 11:19:23.211 * Partial resynchronization not accepted: Replication ID mismatch (Slave asked for '33469ac4d687096890a7b2b238f2d54104618166', my replication IDs are 'f9730ab8455c298502f5d876da0b47ae8c016cb2' and '0000000000000000000000000000000000000000')
18217:M 04 Dec 11:19:23.211 * Starting BGSAVE for SYNC with target: disk
18217:M 04 Dec 11:19:23.212 * Background saving started by pid 18331
18331:C 04 Dec 11:19:23.216 * DB saved on disk
18331:C 04 Dec 11:19:23.217 * RDB: 0 MB of memory used by copy-on-write
18217:M 04 Dec 11:19:23.308 * Background saving terminated with success
18217:M 04 Dec 11:19:23.309 * Synchronization with slave 192.168.100.10:6380 succeeded

## 6380 or 8381 日志如下:

18247:S 04 Dec 11:19:23.208 * Connecting to MASTER 192.168.100.10:6379
18247:S 04 Dec 11:19:23.209 * MASTER <-> SLAVE sync started
18247:S 04 Dec 11:19:23.210 * Non blocking connect for SYNC fired the event.
18247:S 04 Dec 11:19:23.210 * Master replied to PING, replication can continue...
18247:S 04 Dec 11:19:23.211 * Trying a partial resynchronization (request 33469ac4d687096890a7b2b238f2d54104618166:1).
18247:S 04 Dec 11:19:23.213 * Full resync from master: e2b2649c877aa6ff55ead56cf96e398975ee71aa:0
18247:S 04 Dec 11:19:23.213 * Discarding previously cached master state.
18247:S 04 Dec 11:19:23.309 * MASTER <-> SLAVE sync: receiving 4573 bytes from master
18247:S 04 Dec 11:19:23.309 * MASTER <-> SLAVE sync: Flushing old data
18247:S 04 Dec 11:19:23.309 * MASTER <-> SLAVE sync: Loading DB in memory
18247:S 04 Dec 11:19:23.309 * MASTER <-> SLAVE sync: Finished with success

主从结构如下:

1
2
3
    6379  master 
/ \
6380 6381 slaver

当集群完成主从之后,从服务器只允许读数据,不允许写数据。

  1. wait 命令(扩展,redis-3.0新增)

wait 提供两个参数,第一个参数是从节点的数量 m,第二个参数是时间 t,以毫秒
为单位。它表示等待 wait 指令之前的所有写操作同步到 n 个子节点 (也就是确保
m 个子节点的同步没有滞后),最多等待时间 t。如果时间 t=0,表示无限等待直到
N 个从库同步完成达成一致。
假设此时某个子节点与主节点网络断开,wait 指令第二个参数时间 t = 0,主从同步无法继续
进行,wait 指令会永远阻塞,redis 服务器将丧失可用性

1
2
3
4
5
127.0.0.1:6379> set name jason
OK
127.0.0.1:6379> wait 2 5000
(integer) 2 # 同步2个实例
127.0.0.1:6379>

redis 哨兵集群

Sentinel 的作用

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。

虽然 Redis Sentinel 释出为一个单独的可执行文件 redis-sentinel , 但实际上它只是一个运行在特殊模式下的 Redis 服务器, 你可以在启动一个普通 Redis 服务器时通过给定 –sentinel 选项来启动 Redis Sentinel 。

Sentinel模式下,Redis服务器不能执行诸如SET、DBSIZE、EVAL等等这些命令,因为服务器根本没有在命令表中载入这些命令。PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE这七个命令就是客户端可以对Sentinel执行的全部命令了。

Sentinel 的故障迁移

  • 用双环图案表示的是当前的主服务器server1。
  • 用单环图案表示的是主服务器的三个从服务器server2、server3以及server4。
  • server2、server3、server4三个从服务器正在复制主服务器server1,而Sentinel系统则在监视所有四个服务器。

假设这时,主服务器server1进入下线状态,那么从服务器server2、server3、server4对主服务器的复制操作将被中止,并且Sentinel系统会察觉到server1已下线,如图所示(下线的服务器用虚线表示)。

当server1的下线时长超过用户设定的下线时长上限时,Sentinel系统就会对server1执行故障转移操作:

  • 首先,Sentinel系统会挑选server1属下的其中一个从服务器,并将这个被选中的从服务器升级为新的主服务器。
  • 之后,Sentinel系统会向server1属下的所有从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。
  • 另外,Sentinel还会继续监视已下线的server1,并在它重新上线时,将它设置为新的主服务器的从服务器。

举个例子,图16-3展示了Sentinel系统将server2升级为新的主服务器,并让服务器server3和server4成为server2的从服务器的过程。

之后,如果server1重新上线的话,它将被Sentinel系统降级为server2的从服务器,如图所示。

Sentinel 实现原理

  1. 创建网络连接

初始化Sentinel的最后一步是创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息。

对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:

  • 一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。
  • 另一个是订阅连接,这个连接专门用于订阅主服务器的__sentinel__:hello频道。

在Redis目前的发布与订阅功能中,被发送的信息都不会保存在Redis服务器里面,如果在信息发送时,想要接收信息的客户端不在线或者断线,那么这个客户端就会丢失这条信息。因此,为了不丢失__sentinel__:hello频道的任何信息,Sentinel必须专门用一个订阅连接来接收该频道的信息。
另一方面,除了订阅频道之外,Sentinel还必须向主服务器发送命令,以此来与主服务器进行通信,所以Sentinel还必须向主服务器创建命令连接。因为Sentinel需要与多个实例创建多个网络连接,所以Sentinel使用的是异步连接。

  1. Sentinel 可以监控多个master

  1. 获取主服务器信息

Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。

通过分析主服务器返回的INFO命令回复,Sentinel可以获取以下两方面的信息:

  • 一方面是关于主服务器本身的信息,包括run_id域记录的服务器运行ID,以及role域记录的服务器角色;
  • 另一方面是关于主服务器属下所有从服务器的信息,每个从服务器都由一个"slave"字符串开头的行记录,每行的ip=域记录了从服务器的IP地址,而port=域则记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器。
  • 当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构之外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。
  1. 获取从服务器连接

在创建命令连接之后,Sentinel在默认情况下,会以每十秒一次的频率通过命令连接向从服务器发送INFO命令,并获得类似于以下内容的回复:

1
2
3
4
5
6
7
8
9
10
11
12
# Server
...
run_id:32be0699dd27b410f7c90dada3a6fab17f97899f
...
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
slave_repl_offset:11887
slave_priority:100
# Other sections

根据INFO命令的回复,Sentinel会提取出以下信息:

  • 从服务器的运行ID run_id。
  • 从服务器的角色role。
  • 主服务器的IP地址master_host,以及主服务器的端口号master_port。
  • 主从服务器的连接状态master_link_status。
  • 从服务器的优先级slave_priority。
  • 从服务器的复制偏移量slave_repl_offset。
  1. 向主服务器和从服务器发送信息
    在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:
1
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"

这条命令向服务器的__sentinel__:hello频道发送了一条信息,信息的内容由多个参数组成:

  • 其中以s_开头的参数记录的是Sentinel本身的信息,各个参数的意义如表16-2所示。
  • 而m_开头的参数记录的则是主服务器的信息,各个参数的意义如表所示。如果Sentinel正在监视的是主服务器,那么这些参数记录的就是主服务器的信息;如果Sentinel正在监视的是从服务器,那么这些参数记录的就是从服务器正在复制的主服务器的信息。

  1. 接收来自主服务器和从服务器的频道信息

Sentinel既通过命令连接向服务器的__sentinel__:hello频道发送信息,又通过订阅连接从服务器的__sentinel__:hello频道接收信息

对于监视同一个服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,这些信息会被用于更新其他Sentinel对发送信息Sentinel的认知,也会被用于更新其他Sentinel对被监视服务器的认知。

举个例子,假设现在有sentinel1、sentinel2、sentinel3三个Sentinel在监视同一个服务器,那么当sentinel1向服务器的__sentinel__:hello频道发送一条信息时,所有订阅了__sentinel__:hello频道的Sentinel(包括sentinel1自己在内)都会收到这条信息,如图1所示。

当一个Sentinel从__sentinel__:hello频道收到一条信息时,Sentinel会对这条信息进行分析,提取出信息中的Sentinel IP地址、Sentinel端口号、Sentinel运行ID等八个参数,并进行以下检查:

·如果信息中记录的Sentinel运行ID和接收信息的Sentinel的运行ID相同,那么说明这条信息是Sentinel自己发送的,Sentinel将丢弃这条信息,不做进一步处理。

·相反地,如果信息中记录的Sentinel运行ID和接收信息的Sentinel的运行ID不相同,那么说明这条信息是监视同一个服务器的其他Sentinel发来的,接收信息的Sentinel将根据信息中的各个参数,对相应主服务器的实例结构进行更新。

  1. 创建连向其他Sentinel的命令连接

当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:Sentinel A有连向Sentinel B的命令连接,而Sentinel B也有连向Sentinel A的命令连接。

Sentinel之间不会创建订阅连接 Sentinel在连接主服务器或者从服务器时,会同时创建命令连接和订阅连接,但是在连接其他Sentinel时,却只会创建命令连接,而不创建订阅连接。

这是因为Sentinel需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新Sentinel,所以才需要建立订阅连接,而相互已知的Sentinel只要使用命令连接来进行通信就足够了。

Sentinel 故障迁移和选主流程

  1. 发送监控命令

在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。

  1. 命令回复

实例对PING命令的回复可以分为以下两种情况:

  • 有效回复:实例返回+PONG、-LOADING、-MASTERDOWN三种回复的其中一种。
  • 无效回复:实例返回除+PONG、-LOADING、-MASTERDOWN三种回复之外的其他回复,或者在指定时限内没有返回任何回复。
  1. 检查客观下线状态
    当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。

  2. 确定下线状态

Sentinel使用:

1
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

命令询问其他Sentinel是否同意主服务器已下线

当一个Sentinel(目标Sentinel)接收到另一个Sentinel(源Sentinel)发来的SENTINEL is-master-down-by命令时,目标Sentinel会分析并取出命令请求中包含的各个参数,并根据其中的主服务器IP和端口号,检查主服务器是否已下线,然后向源Sentinel返回一条包含三个参数的Multi Bulk回复作为SENTINEL is-master-down-by命令的回复:

1
2
3
1) <down_state>
2) <leader_runid>
3) <leader_epoch>

  1. 其他Sentinel 确认下线状态

根据其他Sentinel发回的SENTINEL is-master-down-by-addr命令回复,Sentinel将统计其他Sentinel同意主服务器已下线的数量,当这一数量达到配置指定的判断客观下线所需的数量时, 那么该Sentinel就会认为主服务器已经进入客观下线状态。

  1. 选主流程(Raft算法)

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。

  • 所有Sentinel 选举地位平等
  • 头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元(configuration epoch)的值都会自增一次
  • 在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改
  • 每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel
  • Sentinel设置局部领头Sentinel的规则是先到先得:最先向目标Sentinel发送设置要求的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝
  • 如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel
  • 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止

  1. 故障迁移

在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:

  • 1)在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
  • 2)让已下线主服务器属下的所有从服务器改为复制新的主服务器。
  • 3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。
  1. 选出新的主服务器

故障转移操作第一步要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送SLAVEOF no one命令,将这个从服务器转换为主服务器。
领头Sentinel会将已下线主服务器的所有从服务器保存到一个列表里面,然后按照以下规则,一项一项地对列表进行过滤:

  • 1)删除列表中所有处于下线或者断线状态的从服务器,这可以保证列表中剩余的从服务器都是正常在线的。
  • 2)删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器,这可以保证列表中剩余的从服务器都是最近成功进行过通信的。
  • 3)删除所有与已下线主服务器连接断开超过down-after-milliseconds10毫秒的从服务器:down-after-milliseconds选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds10毫秒的从服务器,则可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。
  • 之后,领头Sentinel将根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器。
  • 如果有多个具有相同最高优先级的从服务器,那么领头Sentinel将按照从服务器的复制偏移量,对具有相同最高优先级的所有从服务器进行排序,并选出其中偏移量最大的从服务器(复制偏移量最大的从服务器就是保存着最新数据的从服务器)。
  • 最后,如果有多个优先级最高、复制偏移量最大的从服务器,那么领头Sentinel将按照运行ID对这些从服务器进行排序,并选出其中运行ID最小的从服务器。

在发送SLAVEOF no one命令之后,领头Sentinel会以每秒一次的频率(平时是每十秒一次),向被升级的从服务器发送INFO命令,并观察命令回复中的角色(role)信息,当被升级服务器的role从原来的slave变为master时,领头Sentinel就知道被选中的从服务器已经顺利升级为主服务器了。

例如,在图16-22展示的例子中,领头Sentinel会一直向server2发送INFO命令,当server2返回的命令回复从:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Replication
role:slave
...
# Other sections
...
变为:


# Replication
role:master
...
# Other sections
...

的时候,领头Sentinel就知道server2已经成功升级为主服务器了。

  1. 修改从服务器的复制目标

当新的主服务器出现之后,领头Sentinel下一步要做的就是,让已下线主服务器属下的所有从服务器去复制新的主服务器,这一动作可以通过向从服务器发送SLAVEOF命令来实现。

下图展示了在故障转移操作中,领头Sentinel向已下线主服务器server1的两个从服务器server3和server4发送SLAVEOF命令,让它们复制新的主服务器server2的例子。

server3和server4成为server2的从服务器之后,各个服务器以及领头Sentinel的样子

  1. 将旧的主服务器变为从服务器

当server1重新上线时,Sentinel就会向它发送SLAVEOF命令,让它成为server2的从服务器。

Sentinel 的搭建过程

  1. Sentinel 配置说明
    redis-sentinel.conf配置项说明如下:
配置项目 配置说明 示例
port sentinel监听端口,默认是26379,可以修改。
sentinel monitor 告诉sentinel去监听地址为ip:port的一个master,这里的master-name可以自定义,quorum是一个数字,指明当有多少个sentinel认为一个master失效时,master才算真正失效。master-name只能包含英文字母,数字,和“.-_”这三个字符需要注意的是master-ip 要写真实的ip地址而不要用回环地址(127.0.0.1)。 sentinel monitor mymaster 192.168.0.5 6379 2
sentinel auth-pass 设置连接master和slave时的密码,注意的是sentinel不能分别为master和slave设置不同的密码,因此master和slave的密码应该设置相同。 sentinel auth-pass mymaster 0123passw0rd
sentinel down-after-milliseconds 这个配置项指定了需要多少失效时间,一个master才会被这个sentinel主观地认为是不可用的。 单位是毫秒,默认为30秒 sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 sentinel parallel-syncs mymaster 1
sentinel failover-timeout
failover-timeout 可以用在以下这些方面:
1. 同一个sentinel对同一个master两次failover之间的间隔时间。
2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
3. 当想要取消一个正在进行的failover所需要的时间。
4. 当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了。
sentinel failover-timeout mymaster1 20000
sentinel notification-script sentinel的notification-script和reconfig-script是用来配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。对于脚本的运行结果有以下规则:
若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
sentinel notification-script mymaster /var/redis/notify.sh
sentinel client-reconfig-script 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。以下参数将会在调用脚本时传给脚本: 目前总是“failover”, 是“leader”或者“observer”中的一个。 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的。这个脚本应该是通用的,能被多次调用,不是针对性的。 sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

2. 搭建集群

文件位置

```bash
.
├── 16379
│   ├── redis_sentinel_16379.pid
│   ├── redis_sentinel_log.log
│   └── sentinel.conf
├── 16380
│   ├── redis_sentinel_16380.pid
│   ├── redis_sentinel_log.log
│   └── sentinel.conf
└── 16381
├── redis_sentinel_16381.pid
├── redis_sentinel_log.log
└── sentinel.conf

配置文件模板 :

1
2
3
4
5
6
7
8
9
10
bind 0.0.0.0
port 16379
daemonize yes
pidfile /root/redis/confs/sentinel/16379/redis_sentinel_16379.pid
logfile "/root/redis/confs/sentinel/16379/redis_sentinel_log.log"
dir ./
sentinel monitor mymaster 192.168.100.10 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

分别启动

1
redis-sentinel ./sentinel.conf #16379/sentinel.conf

=

  1. 查看
    故障转移后 client 怎么知道新的master地址?sentinel 就像是一个服务注册中心,可以请求 sentinel 获取当前的 master 信息。

例如:

1
2
3
[root@CentOS7 ~]# redis-cli -p 16379 sentinel get-master-addr-by-name mymaster
1) "192.168.100.10"
2) "6379"

client 使用的 Redis 客户端需要支持 sentinel,那么就可以自动拿到 master 的地址了。

redis 集群模式

哨兵模式解决了主从复制不能自动故障转移,达不到高可用的问题,但还是存在难以在线扩容,Redis容量受限于单机配置的问题。Cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。

槽位

在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。

  • 为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点当其它主节点,.
  • ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了
  • Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

在集群中执行命令

在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。

  • 当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:
  • 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。
  • 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。

图展示了这两种情况的判断流程。

重新分片

Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。

Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。

redis-trib对集群的单个槽slot进行重新分片的步骤如下(如果重新分片涉及多个槽,那么redis-trib将对每个给定的槽分别执行上面给出的步骤):

  • 1)redis-trib对目标节点发送CLUSTER SETSLOTIMPORTING<source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
  • 2)redis-trib对源节点发送CLUSTER SETSLOTMIGRATING<target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点。
  • 3)redis-trib向源节点发送CLUSTER GETKEYSINSLOT命令,获得最多count个属于槽slot的键值对的键名(key name)。
  • 4)对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE<target_ip><target_port><key_name>0命令,将被选中的键原子地从源节点迁移至目标节点。
  • 5)重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。每次迁移键的过程如图所示。
  • 6)redis-trib向集群中的任意一个节点发送CLUSTER SETSLOTNODE<target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点

复制与故障转移

Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。

举个例子,对于包含7000、7001、7002、7003四个主节点的集群来说,我们可以将7004、7005两个节点添加到集群里面,并将这两个节点设定为节点7000的从节点

表记录了集群各个节点的当前状态,以及它们正在做的工作。

如果这时,节点7000进入下线状态,那么集群中仍在正常运作的几个主节点将在节点7000的两个从节点——节点7004和节点7005中选出一个节点作为新的主节点,这个新的主节点将接管原来节点7000负责处理的槽,并继续处理客户端发送的命令请求。

例如,如果节点7004被选中为新的主节点,那么节点7004将接管原来由节点7000负责处理的槽0至槽5000,节点7005也会从原来的复制节点7000,改为复制节点7004,如图17-33所示(图中用虚线包围的节点为已下线节点)。

如果在故障转移完成之后,下线的节点7000重新上线,那么它将成为节点7004的从节点,如图所示。

故障转移

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果节点7001向节点7000发送了一条PING消息,但是节点7000没有在规定的时间内,向节点7001返回一条PONG消息,么节点7001就会让节点7000进入了疑似下线状态,状态有三种
在线状态、疑似下线状态(PFAIL),还是已下线状态(FAIL)。

如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

举个例子,对于图17-38所示的下线报告来说,主节点7002和主节点7003都认为主节点7000进入了下线状态,并且主节点7001也认为主节点7000进入了疑似下线状态(代表主节点7000的结构打开了REDIS_NODE_PFAIL标识),综合起来,在集群四个负责处理槽的主节点里面,有三个都将主节点7000标记为下线,数量已经超过了半数,所以主节点7001会将主节点7000标记为已下线,并向集群广播一条关于主节点7000的FAIL消息,如图17-39所示。

总结

故障转移,当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:

  • 1)复制下线主节点的所有从节点里面,会有一个从节点被选中。
  • 2)被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
  • 3)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  • 4)新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
  • 5)新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举新的主节点新的主节点是通过选举产生的。以下是集群选举新的主节点的方法:

  • 1)集群的配置纪元是一个自增计数器,它的初始值为0。
  • 2)当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
  • 3)对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
  • 4)当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  • 5)如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
  • 6)每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
  • 7)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
  • 8)因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
  • 9)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

集群搭建

  1. 目录结构
端口 主从 复制节点
7000 7003
7001 7004
7002 7005
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.
├── 7000
│   └── redis.conf
├── 7001
│   └── redis.conf
├── 7002
│   └── redis.conf
├── 7003
│   └── redis.conf
├── 7004
│   └── redis.conf
├── 7005
│   └── redis.conf
├── redis_7000.pid
├── redis_7001.pid
├── redis_7002.pid
├── redis_7003.pid
├── redis_7004.pid
├── redis_7005.pid
├── redislog_7000.log
├── redislog_7001.log
├── redislog_7002.log
├── redislog_7003.log
├── redislog_7004.log
└── redislog_7005.log
  1. 配置文件
1
2
3
4
5
6
7
8
# 端口号 其他配置参见其他章节这里只保留集群模式
# 开启集群模式
cluster-enabled yes
# 集群的配置,配置文件首次启动自动生成
# 这里只需指定文件名即可,集群启动成功后会自动在data目录下创建
cluster-config-file "nodes-6379.conf"
# 请求超时,设置10秒
cluster-node-timeout 10000
1
[root@CentOS7 cluster]# redis-server ./700{0,1,2,3,4,5}/redis.conf

登录7000 查看集群信息

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@CentOS7 cluster]# redis-cli -p 7000
127.0.0.1:7000> CLIENT info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:1
cluster_size:0
cluster_current_epoch:0
cluster_my_epoch:0
cluster_stats_messages_sent:0
cluster_stats_messages_received:0
  1. 组成集群
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
root@CentOS7 cluster]# redis-cli -p 7001
127.0.0.1:7001> CLUSTER MEET 127.0.0.1 7000
OK
127.0.0.1:7001> CLUSTER MEET 127.0.0.1 700
[root@CentOS7 cluster]# redis-cli -p 7002
127.0.0.1:7002> CLUSTER MEET 127.0.0.1 7000
OK
127.0.0.1:7002>
[root@CentOS7 cluster]# redis-cli -p 7003
127.0.0.1:7003> CLUSTER MEET 127.0.0.1 7000
OK
127.0.0.1:7003>
[root@CentOS7 cluster]# redis-cli -p 7004
127.0.0.1:7004> CLUSTER MEET 127.0.0.1 7000
OK
127.0.0.1:7004>
[root@CentOS7 cluster]# redis-cli -p 7005
127.0.0.1:7005> CLUSTER MEET 127.0.0.1 7000
OK
127.0.0.1:7005>
[root@CentOS7 cluster]# redis-cli -p 7000
127.0.0.1:7000> CLUSTER info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:0
cluster_current_epoch:5
cluster_my_epoch:1
cluster_stats_messages_ping_sent:45
cluster_stats_messages_pong_sent:50
cluster_stats_messages_meet_sent:1
cluster_stats_messages_sent:96
cluster_stats_messages_ping_received:44
cluster_stats_messages_pong_received:46
cluster_stats_messages_meet_received:6
cluster_stats_messages_received:96
127.0.0.1:7000> get 1
(error) CLUSTERDOWN Hash slot not served
127.0.0.1:7000>

查看节点

1
2
3
4
5
6
7
127.0.0.1:7000> CLUSTER nodes
14bd4c6a0d82425637d10ac11238426bbf7d5e9f 127.0.0.1:7000@17000 myself,master - 0 1607679088000 1 connected
8e1da11c7da154ed2515c6a9b683ebb58d23618d 127.0.0.1:7002@17002 master - 0 1607679093630 2 connected
f52ae1c178ef712d7c5eac2f4112fba924013dc6 127.0.0.1:7003@17003 master - 0 1607679094631 3 connected
7c33f7bb44fb9d93a74cc3264604d67ff6866df6 127.0.0.1:7004@17004 master - 0 1607679093000 4 connected
2e25473d824a0598cd56b4ce846f099593bfd867 127.0.0.1:7005@17005 master - 0 1607679094000 5 connected
fca142b994747c432858c131c97d6e36f597d0af 127.0.0.1:7001@17001 master - 0 1607679092000 0 connected
  1. 设立主从
1
2
3
4
5
6
7
8
9
10

127.0.0.1:7000> CLUSTER REPLICATE <node_id> 分别在 7003 7004 7005 执行
得到如下配置

8e1da11c7da154ed2515c6a9b683ebb58d23618d 127.0.0.1:7002@17002 master - 0 1607680045949 2 connected
14bd4c6a0d82425637d10ac11238426bbf7d5e9f 127.0.0.1:7000@17000 master - 0 1607680045000 1 connected
fca142b994747c432858c131c97d6e36f597d0af 127.0.0.1:7001@17001 master - 0 1607680048957 0 connected
2e25473d824a0598cd56b4ce846f099593bfd867 127.0.0.1:7005@17005 slave 8e1da11c7da154ed2515c6a9b683ebb58d23618d 0 1607680047000 5 connected
f52ae1c178ef712d7c5eac2f4112fba924013dc6 127.0.0.1:7003@17003 slave 14bd4c6a0d82425637d10ac11238426bbf7d5e9f 0 1607680048000 3 connected
7c33f7bb44fb9d93a74cc3264604d67ff6866df6 127.0.0.1:7004@17004 myself,slave fca142b994747c432858c131c97d6e36f597d0af 0 1607680043000 4 connected
  1. 配置槽位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@CentOS7 cluster]# redis-cli -p 7000 cluster addslots {0..5460}
OK
[root@CentOS7 cluster]# redis-cli -p 7001 cluster addslots {5461..10000}
OK
[root@CentOS7 cluster]# redis-cli -p 7002 cluster addslots {10001..16383}
# 查看槽位
127.0.0.1:7000> 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:5
cluster_my_epoch:1
cluster_stats_messages_ping_sent:1481
cluster_stats_messages_pong_sent:1455
cluster_stats_messages_meet_sent:1
cluster_stats_messages_sent:2937
cluster_stats_messages_ping_received:1449
cluster_stats_messages_pong_received:1482
cluster_stats_messages_meet_received:6
cluster_stats_messages_received:2937
  1. 查看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 单击模式 登录
[root@CentOS7 cluster]# redis-cli -p 7000
127.0.0.1:7000> get 1
(error) MOVED 9842 127.0.0.1:7001 # 返回错误
127.0.0.1:7000> set 1
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:7000> set 1 1
(error) MOVED 9842 127.0.0.1:7001
127.0.0.1:7000> get 1
(error) MOVED 9842 127.0.0.1:7001
127.0.0.1:7000>
# 集群模式登录
[root@CentOS7 cluster]# redis-cli -c -p 7000
127.0.0.1:7000> get 1
-> Redirected to slot [9842] located at 127.0.0.1:7001

参考



支付宝打赏 微信打赏

赞赏一下