您的位置:首页 > 数据库 > Redis

初学乍练redis:复制与哨兵

2018-09-07 18:10 190 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wzy0623/article/details/82500593

 

目录

一、复制

1. 配置

2. 原理

3. 乐观复制

4. 无硬盘复制

5. 增量复制

二、哨兵

1. 什么是哨兵

2. 配置

3. 原理

4. 部署

三、脑裂

        大部分摘自Redis入门指南(第2版)。

一、复制

        持久化使redis在服务器重启情况下尽量少丢数据,但持久化生成的文件是存储在redis同一机器上的。为避免单点故障,redis提供复制(replication)功能,可以实现当一台数据库实例中的数据更新后,自动将更新的数据同步到其它数据库实例上。

1. 配置

        在复制的概念中,数据库分为主(master)从(slave)两类。主库可以进行读写操作,同时将数据变化同步给从库。从库缺省是只读的,接受主库同步过来的数据。同MySQL类似,一个主库可以拥有多个从库,但一个从库只能从属于一个主库。

        在redis中使用复制功能非常简单,只需要在从数据库的配置文件中加入“slaveof 主库IP 主库端口”即可,主库只要注意能让从库访问就可以了,比如主库如果设置了protected-mode yes,那么一定要将从库IP地址加到配置bind参数中。

        下面演示一个最简单的复制系统。在一台服务器上启动两个redis实例,监听不同端口,其中一个作为master,另一个作为slave。当slaveof命令完成后,master的数据会同步到slave。

[code][root@hdp4/var/redis/20010]#redis-cli -p 20009 keys "*"
1) "key"
2) "foo"
[root@hdp4/var/redis/20010]#redis-cli -p 20010 keys "*"
(empty list or set)
[root@hdp4/var/redis/20010]#redis-cli -p 20010 slaveof 127.0.0.1 20009
OK
[root@hdp4/var/redis/20010]#redis-cli -p 20010 keys "*"
1) "foo"
2) "key"
[root@hdp4/var/redis/20010]#

        这时使用info replication命令查看主从两个redis实例获取复制的相关信息。

[code][root@hdp4/var/redis/20010]#redis-cli -p 20009 info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=20010,state=online,offset=811,lag=1
master_replid:b1dbeb92601e742d2998e3573c03818a073b6da6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:811
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:811

        可以看到,20009实例的角色是master,同时已连接的slave的个数为1。slave的端口为20010,状态为在线。

[code][root@hdp4/var/redis/20010]#redis-cli -p 20010 info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:20009
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:825
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:b1dbeb92601e742d2998e3573c03818a073b6da6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:825
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:112
repl_backlog_histlen:714
[root@hdp4/var/redis/20010]#

        从库的复制信息显示,20010实例的角色是slave,对应的主库IP是127.0.0.1,端口为20009。此后master的任何数据变化会自动同步到slave中。

[code][root@hdp4/var/redis/20010]#redis-cli -p 20009 del key
(integer) 1
[root@hdp4/var/redis/20010]#redis-cli -p 20009 set key1 10
OK
[root@hdp4/var/redis/20010]#redis-cli -p 20010 keys "*"
1) "key1"
2) "foo"
[root@hdp4/var/redis/20010]#redis-cli -p 20010 get key1
"10"
[root@hdp4/var/redis/20010]#

        为了让slave下次启动依然作为slave,需要将slaveof命令加到配置文件中。

[code][root@hdp4/var/redis/20010]#more redis.conf 
daemonize yes
port 20010
dir "/var/redis/20010"
pidfile "/var/redis/20010/redis.pid"
logfile "/var/redis/20010/redis.log"
protected-mode no
save 900 1
save 300 10
save 60 10000
[root@hdp4/var/redis/20010]#redis-cli -p 20010 config rewrite
OK
[root@hdp4/var/redis/20010]#more redis.conf 
daemonize yes
port 20010
dir "/var/redis/20010"
pidfile "/var/redis/20010/redis.pid"
logfile "/var/redis/20010/redis.log"
protected-mode no
save 900 1
save 300 10
save 60 10000
# Generated by CONFIG REWRITE
slaveof 127.0.0.1 20009
[root@hdp4/var/redis/20010]#

        Slave服务器默认是只读模式,可以由slave-read-only参数控制。

[code][root@hdp4/var/redis/20010]#redis-cli -p 20010 config get slave-read-only
1) "slave-read-only"
2) "yes"
[root@hdp4/var/redis/20010]#redis-cli -p 20010 set key1 20
(error) READONLY You can't write against a read only slave.
[root@hdp4/var/redis/20010]#redis-cli -p 20010 config set slave-read-only no
OK
[root@hdp4/var/redis/20010]#redis-cli -p 20010 set key1 20
OK
[root@hdp4/var/redis/20010]#redis-cli -p 20010 get key1
"20"
[root@hdp4/var/redis/20010]#

        因为对从库的任何修改都不会同步给任何其它数据库,并且一旦主库中更新了对应的数据就会覆盖从库的改动,所以通常场景下不应该设置从库可写,以免导致易被忽略的潜在应用逻辑错误。

[code][root@hdp4/var/redis/20010]#redis-cli -p 20009 set key1 10
OK
[root@hdp4/var/redis/20010]#redis-cli -p 20010 get key1
"10"
[root@hdp4/var/redis/20010]#redis-cli -p 20010 config set slave-read-only yes
OK
[root@hdp4/var/redis/20010]#

        配置多台从库的方法也是一样,在所有从库的配置文件中都加上slaveof参数,或者执行slaveof命令,指向同一个主库即可。如果该数据库已经是其它主库的从库了,slaveof命令会停止和原来主库的同步转而和新主库同步。此外对于对于从库来说,还可以使用slaveof no one命令来使当前从库停止接收其它数据库的同步并转为主库。

2. 原理

        当一个从库启动后,会向主库发送SYNC命令。主库接收到SYNC命令后会开始在后台保存快照(执行BGSAVE,即RDB持久化的过程),并将保存快照期间接收到的命令缓存起来。当快照完成后,redis会将快照文件和所有缓存的命令发送给从库。从库接收到后,会载入快照文件并执行收到的缓存的命令。以上过程称为复制初始化。复制初始化结束后,主库每当收到写命令时,就会将命令以redis统一请求通信协议的格式同步给从库,从而保证主从数据一致。

        当主从数据库之间的连接断开重连后,redis 2.6以及之前的版本会重新进行复制初始化,即使从库可能仅有几条命令没有接收到,主库也必须将数据库里的所有数据重新传给从库。显然主从库断线重连后的数据恢复过程效率很低。redis 2.8的一个重要改进就是支持有条件的增量数据传输,当从库断线重连主库后,主库只需要将断线期间执行的命令传送给从库,从而大大提高了redis复制的实用性。

        由于redis服务器使用TCP协议通信,所以可以使用telnet工具伪装成一个从库与主库通信。首先在命令行中连接主库:

[code][root@hdp4/var/redis/20010]#telnet 127.0.0.1 20009 
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.

        然后作为从库,先要发送ping命令确认主库是否可以连接:

[code]ping
+PONG

        主库回复+PONG。如果没有收到主库的回复,则提示错误,如果需要密码才能连接,还要发送auth命令进行验证。而后向主库发送replconf命令说明自己的端口号:

[code]replconf listening-port 20010
+OK

        这时就可以开始同步过程了。向主库发送sync命令开始同步,此时主库发送回快照文件和缓存的命令,快照文件是二进制格式,从第三行开始:

sync
$199
REDIS0008    redis-ver4.0.11
redis-bits ...

        从数据库会将收到的内容写入到硬盘的临时文件中,当写入完成后从库会用该临时文件替换RDB快照文件,之后的操作就和RDB持久化时启动恢复的过程一样了。复制初始化过程不会阻塞主库,即使有一个或多个从库正在进行复制初始化,主库也可以继续处理命令请求。复制初始化过程也不会阻塞从库,默认情况下,从库会用同步前的数据对命令进行响应。可以配置slave-serve-stale-data参数为no来使从库在同步完成前对所有命令(除了info和slaveof)都回复错误。不过,在从库删除旧版本数据集并载入新版本数据集的那段时间内,即用临时文件替换RDB快照文件的过程中,连接请求会被阻塞。

        复制初始化阶段后,主库执行的任何导致数据变化的命令都会异步地传送给从库,这一过程为复制同步阶段。同步的内容和redis通信协议一样,比如在主库中执行set foo hi,telnet接收到:

[code]*2
$6
SELECT
$1
0
*3
$3
set
$3
foo
$2
hi

        复制同步阶段会贯穿整个主从同步过程中,直到主从关系终止为止。

        在复制初始化过程中,快照在主从库都起了很大的作用,只要执行复制初始化就会进行快照,即使关闭了RDB方式的持久化(通过将save参数置空)。

        从库不仅可以接收主库的同步数据,自己也可以同时作为主库存在,形成类似链式会树状的拓扑结构。复制功能可以单纯地用于数据冗余(data redundancy),也可以通过让多个从库处理只读命令请求来提升扩展性(scalability)。比如说繁重的SORT命令可以交给从库去运行。通过复制建立多个从库节点,主库负责只进行写操作,而从库负责读操作。这种一主多从的结构很适合读多写少的场景。

        持久化是一个相对耗时的操作。为了提高性能,可以通过复制功能来让主库免于执行持久化操作,只要关闭主库的持久化功能,然后由从库去执行持久化操作即可。当从库崩溃重启后,主库会自动将数据同步过来,所以无需担心数据丢失。然而当主库崩溃时,手工通过从库恢复主库数据时,需要严格按照以下两步进行。

  1. 在从库中使用saveof no one命令将从库提升成主库继续服务。
  2. 启动之前崩溃的主库,然后使用slaveof命令将其设置成新主库的从库,即可将数据同步回来。

        当主库崩溃后,切勿直接重启。因为当主库重启后,因为没有开启持久化功能,数据库中所有数据都被清空,这时从库依然会从主库中接收数据,使得所有从库也被清空,导致从库的持久化失去意义。redis提供了一种叫做“哨兵”的自动化方案实现主从切换过程,避免了手工维护的麻烦和容易出错的问题。

3. 乐观复制

        redis采用了乐观复制(optimistic replication)的复制策略,容忍在一定时间内主从数据库的内容是不同的,但是两者的数据会最终同步。redis的数据复制过程本身是异步的,这一特性保证了启用复制后主库的性能不会受到影响,但另一方面也会产生一个主从数据库不一致的时间窗口,当主库执行了一条写命令后,主库的数据已经发生变化,而在主库将该命令传送给从库之前,如果两个库之间断网,此时二者之间的数据就会是不一致的。从这个角度看,主库是无法得知某个命令最终同步给了多少个从库。不过redis提供了两个配置选项来限制只有当至少同步个指定数量的从库时,主库才是可写的。这一特性默认是关闭的:

[code][root@hdp4~]#redis-cli -p 20009 config get min*
1) "min-slaves-to-write"
2) "0"
3) "min-slaves-max-lag"
4) "10"
[root@hdp4~]#

        min-slaves-to-write为0表示无论有没有从库连接到主库,主库都是可写的。如果连接到主库的从库数量小于该参数值,写主库会返回错误,例如:

[code][root@hdp4~]#redis-cli -p 20009 set foo bar
(error) NOREPLICAS Not enough good slaves to write.
[root@hdp4~]#

        min-slaves-max-lag 表示允许从库最长失去连接的时间,单位是秒。如果从库最后与主库联系的时间小于这个值,则认为从库还在保持与主库的连接。

4. 无硬盘复制

        从前面介绍的复制原理可知,redis复制初始化是基于RDB方式的持久化实现的,即主库在后台保存RDB快照,从库则接收并载入快照文件。这样的实现优点是可以显著地简化逻辑,复用已有的代码,但是缺点也很明显。

  • 当主库禁用RDB快照时,如果执行了复制初始化操作,redis依然会生成RDB快照,所以下次启动后主库会以该快照恢复数据,结果就是恢复的是最近一次复制初始化时的数据,这显然是不合理的。
  • 因为复制初始化时需要在硬盘中创建RDB快照文件,这一过程会对性能产生影响。

        因此从2.8.18版本开始,redis引入了无盘复制选项,开启该选项时,redis在与从库进行复制初始化时将快照内容存储到硬盘上,而是直接通过网络发送给从库,避免的硬盘的性能瓶颈。这种功能类似于Linux的管道,特点在于不在本地落盘,通过网络直接传到远端。该功能缺省是关闭的,可以设置repl-diskless-sync参数为yes开启。

[code][root@hdp4~]#redis-cli -p 20009 config get repl-diskless-sync
1) "repl-diskless-sync"
2) "no"
[root@hdp4~]#

5. 增量复制

        redis 2.8版本最重要的更新之一是实现了主从断线重连情况下的增强复制。增量复制基于如下3点实现。

  1. 从库会存储主库的运行ID(run id)。每个redis运行实例均会拥有一个唯一的运行ID,每当实例重启后,就会自动生成一个新的运行ID。
  2. 在复制同步阶段,主库每将一个命令传送给从库时,都会同时把该命令存放到一个积压队列(backlog)中,并记录下该命令的偏移量。
  3. 同时,从库接收到主库传来的命令时,会记录下该命令的偏移量。

        这3点是实现增量复制的基础。2.8版本之前,当主从连接准备就绪后,从库会发送一条sync命令来告诉主库可以把所有数据同步过来了。而从2.8版本开始,不再发送sync命令,取而代之的是发送psync命令,格式为“psync 主库的运行ID 断开前最新的命令偏移量”。主库收到psync命令后,会执行以下判断来决定此次重连是否可以执行增量复制。

  1. 首先主库会判断从库传来的运行ID是否和自己的运行ID相同。这一步骤的意义在于确保从库之前确实是和自己同步的,以免从库拿到错误的数据(比如主库在断线期间重启过,会造成数据不一致)。
  2. 然后判断从库最后同步成功的命令偏移量是否在积压队列中,如果在则可以执行增量复制,并将积压队列中相应的命令发送给从库。

        如果此次重连不满足增量复制的条件,主库会进行一次全量同步。

        大部分情况下,增量复制的过程对开发者来说是完全透明的,开发者不需要关心增量复制的具体细节。2.8版本的主库可以正常地和旧版本的从库同步(通过接收sync命令),同样2.8版本的从库也可以与旧版的主库同步(通过发送sync命令)。唯一需要开发者设置的就是积压队列的大小。

        积压队列本质上是一个固定长度的循环队列,默认情况积压队列的大小为1MB,可以通过repl-backlog-size配置参数来调整。积压队列越大,其允许的主从数据库断线时间就越长。因为积压队列存储的内容是命令本身,所以估计积压队列的大小只需要估计主从数据库断线的时间中主库可能执行的命令的大小即可。

        与积压队列相关的量一个配置选项是repl-backlog-ttl,即当所有从库与主库断开连接后,经过多少时间可以释放积压队列的内存空间,缺省为1小时。

[code][root@hdp4~]#redis-cli -p 20009 config get repl-backlog*
1) "repl-backlog-size"
2) "1048576"
3) "repl-backlog-ttl"
4) "3600"

二、哨兵

1. 什么是哨兵

        redis的哨兵(Sentinel)系统用于管理多个redis主从实例(instance),该系统执行以下三个任务:

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

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

        虽然哨兵释出为一个单独的可执行文件redis-sentinel,但实际上它只是一个运行在特殊模式下的redis服务器,可以在启动一个普通redis服务器时通过给定 --sentinel 选项来启动哨兵。在一个一主多从的redis系统中,可以使用多个哨兵执行监控任务以保证系统足够稳健。此时不仅哨兵会同时监控主从库,哨兵之间也会互相监控。

2. 配置

        建立一个配置文件,如sentinel.conf,内容如下:

[code][root@hdp4/var/redis/sentinel]#more sentinel.conf
port 10001
protected-mode no
sentinel monitor mymaster 127.0.0.1 20009 1
[root@hdp4/var/redis/sentinel]#

        其中sentinel monitor设置监控的主库信息。mymaster表示要监控的自定义的主库名,必须仅由大小写字母、数字和“.-_”这3个字符组成。后面的两个参数表示主库的地址和端口。最后的1表示最低通过票数。配置哨兵监控一个主从系统时,只需要配置主库信息即可,哨兵会自动发现所有复制该主库的从库。接下来启动sentinel进程。

[code]redis-sentinel /var/redis/sentinel/sentinel.conf > /var/redis/sentinel/sentinel.log 2>&1 &

        启动哨兵后,日志输出如下内容:

[code][root@hdp4/var/redis/sentinel]#more sentinel.log 
149920:X 07 Sep 15:22:25.176 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
149920:X 07 Sep 15:22:25.176 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=149920, just started
149920:X 07 Sep 15:22:25.176 # Configuration loaded
149920:X 07 Sep 15:22:25.179 * Running mode=sentinel, port=10001.
149920:X 07 Sep 15:22:25.179 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/soma
xconn is set to the lower value of 128.
149920:X 07 Sep 15:22:25.180 # Sentinel ID is 1885ea3c6a08cfc6a67c495da3c58f6a6aa0dd57
149920:X 07 Sep 15:22:25.180 # +monitor master mymaster 127.0.0.1 20009 quorum 1
149920:X 07 Sep 15:22:25.181 * +slave slave 127.0.0.1:20010 127.0.0.1 20010 @ mymaster 127.0.0.1 20009
[root@hdp4/var/redis/sentinel]#

        最后一行的+slave表示新发现了一个从库。现在哨兵已经监控20009和20010两个redis实例了。这时将20009的实例关闭:

[code][root@hdp4/var/redis/sentinel]#redis-cli -p 20009 shutdown

        等待30秒(可配置)后哨兵日志输出如下内容:

[code]149920:X 07 Sep 15:30:45.064 # +sdown master mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.064 # +odown master mymaster 127.0.0.1 20009 #quorum 1/1
149920:X 07 Sep 15:30:45.064 # +new-epoch 1
149920:X 07 Sep 15:30:45.064 # +try-failover master mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.065 # +vote-for-leader 1885ea3c6a08cfc6a67c495da3c58f6a6aa0dd57 1
149920:X 07 Sep 15:30:45.065 # +elected-leader master mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.065 # +failover-state-select-slave master mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.165 # +selected-slave slave 127.0.0.1:20010 127.0.0.1 20010 @ mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.165 * +failover-state-send-slaveof-noone slave 127.0.0.1:20010 127.0.0.1 20010 @ mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.249 * +failover-state-wait-promotion slave 127.0.0.1:20010 127.0.0.1 20010 @ mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.979 # +promoted-slave slave 127.0.0.1:20010 127.0.0.1 20010 @ mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:45.979 # +failover-state-reconf-slaves master mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:46.054 # +failover-end master mymaster 127.0.0.1 20009
149920:X 07 Sep 15:30:46.054 # +switch-master mymaster 127.0.0.1 20009 127.0.0.1 20010
149920:X 07 Sep 15:30:46.055 * +slave slave 127.0.0.1:20009 127.0.0.1 20009 @ mymaster 127.0.0.1 20010
149920:X 07 Sep 15:31:16.091 # +sdown slave 127.0.0.1:20009 127.0.0.1 20009 @ mymaster 127.0.0.1 20010

        +sdown表示哨兵主观认为主库停止服务了,而+odown表示哨兵客观认为主库停止服务了。此时哨兵开始执行故障恢复,即挑选一个从库,将其提升为主库。

        +try-failover表示哨兵开始进行故障恢复,+failover-end表示哨兵完成故障恢复。期间涉及的内容比较复杂,包括领头哨兵的选举、备选从库的选择等,后面详细介绍。+switch-master表示主库从20009端口迁移到20010端口,即20010的从库被升级为主库。+slave则列出了新主库的从库,端口为20009。哨兵会更新20009的信息,使得其重新加入后可以按照当前信息继续对外服务。当20009实例恢复服务后,会转变为20010的从库来运行。

[code][root@hdp4/var/redis/sentinel]#redis-cli -p 20010 info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=20009,state=online,offset=110925,lag=1
master_replid:2e9e886f8603f91c227c5889d4045cc18db8f586
master_replid2:3a14b6758913d299f11bee7bcca0f0ad6a436aa0
master_repl_offset:110925
second_repl_offset:34204
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:110925
[root@hdp4/var/redis/sentinel]#redis-cli -p 20009 info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:20010
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:111609
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:2e9e886f8603f91c227c5889d4045cc18db8f586
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:111609
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:109669
repl_backlog_histlen:1941
[root@hdp4/var/redis/sentinel]#more /var/redis/20009/redis.conf 
daemonize yes
port 20009
dir "/var/redis/20009"
pidfile "/var/redis/20009/redis.pid"
logfile "/var/redis/20009/redis.log"
protected-mode no
save 900 1
save 300 10
save 60 10000
# Generated by CONFIG REWRITE
slaveof 127.0.0.1 20010
[root@hdp4/var/redis/sentinel]#

        重启20009实例时,首先哨兵会监控到这一变化,并输出:

[code]149920:X 07 Sep 15:49:41.704 # -sdown slave 127.0.0.1:20009 127.0.0.1 20009 @ mymaster 127.0.0.1 20010
149920:X 07 Sep 15:49:51.619 * +convert-to-slave slave 127.0.0.1:20009 127.0.0.1 20009 @ mymaster 127.0.0.1 20010

        -sdown表示实例20009已经恢复服务了(与+sdown相反),+convert-to-slave表示将20009端口的实例设置为20010实例的从库。

3. 原理

        一个哨兵进程启动时会读取配置文件的内容,通过如下配置找出需要监控的主库:

[code]sentinel monitor master-name ip redis-port quorum

        因为考虑到故障回复后当前监控的系统的主数据库的地址和端口会产生变化,所以哨兵提供了命令可以通过主库的名字获取当前系统的主库地址和端口号。

        一个哨兵节点可以同时监控多个redis主从系统,只需要提供多个sentinel monitor配置,例如:

[code]sentinel monitor master1 127.0.0.1 20001 2
sentinel monitor master2 127.0.0.1 20002 3

        同时多个哨兵也可以同时监控同一个redis主从系统。配置文件中还可以定义其它监控相关的参数,每个配置选项都包含主库的名字使得监控不同主库时可以使用不同的配置参数,例如:

[code]sentinel down-after-milliseconds master1 5000
sentinel down-after-milliseconds master2 6000

        哨兵启动后,会与要监控的主库建立两条连接,这两个连接的建立方式与普通的redis客户端无异。其中一条连接用来订阅该主库的__sentinel__:hello频道以获取其它同样监控该数据库的哨兵节点的信息,另外哨兵也需要定期向主库发送INFO等命令来获取主数据库本身的信息,因为当客户端的连接进入订阅模式时就不能在执行其它命令了,所以这时哨兵会使用另外一条连接来发送这些命令。

        和主库的连接建立完成后,哨兵会定时执行下面3个操作。

  • 每10秒哨兵会向主库和从库发送INFO命令。
  • 每2秒哨兵会向主库和从库的__sentinel__:hello频道发送自己的信息。
  • 每1秒哨兵会向主库、从库和其它哨兵节点发送PING命令。

        这3个操作贯穿哨兵进程的整个生命周期中。首先发送INFO命令使得哨兵可以获得当前数据库的相关信息,包括运行ID、复制信息等,从而实现新节点的自动发现。哨兵向主库发送INFO命令,通过解析返回结果得知从库列表,而后对每个从库建立同样两个连接。在此之后,哨兵每10秒定时向已知的所有主从库发送INFO命令来获取信息更新并进行相应操作,比如对新增从库建立连接并加入监控列表,对主从库的角色(由故障恢复操作引起)变化进行信息更新等。

        接下来哨兵向主从库的__sentinel__:hello频道发送信息来与同样监控该数据库的哨兵分享自己的信息。当其它哨兵收到消息后,会判断发消息的哨兵是不是新发现的哨兵。如果是则将其加入已发现的哨兵列表中并创建一个到其的连接。与数据库不同,哨兵之间只会创建一条连接用来发送PING命令,而不需要创建另外一条连接来订阅频道,因为哨兵只需要订阅数据库的频道即可实现自动发现其它哨兵。同时哨兵会判断信息中主库的配置版本,如果该版本比当前记录的主库版本高,则更新主库数据。

        实现了自动发现从库和其它哨兵节点后,哨兵要做的就是定时监控这些数据库和节点有没有停止服务。这是通过每隔一定时间向这些节点发送PING命令实现的。时间间隔与down-after-milliseconds选项有关,当down-after-milliseconds的值小于1秒时,哨兵会每隔down-after-milliseconds指定的时间发送一次PING命令,当down-after-milliseconds的值大于1秒时,哨兵会每隔1秒发送一次PING命令。

        当超过down-after-milliseconds选项指定时间后,如果被PING的数据库仍未回复,则哨兵认为其主观下线(subjectively down)。主观下线表示从当前的哨兵来看,该节点已下线。如果该节点是主库,则哨兵会进一步判断是否需要对其进行故障恢复:哨兵发送SENTINEL is-master-down-by-addr命令询问其它哨兵节点以了解它们是否也认为该主库主观下线,如果达到指定数量时,哨兵会认为其客观下线(objectively down),并选举领头的哨兵节点对主库系统发起故障恢复。这个指定数量即为quorum参数。接下来进行选举领头哨兵的步骤。

        虽然当前哨兵节点发现了主库客观下线,需要故障恢复,但故障恢复需要由领头的哨兵来完成,这样可以保证同一时间只有一个哨兵节点来执行故障恢复。选举领头哨兵的过程使用了raft算法。选出领头哨兵后,领头哨兵开始对主库进行故障恢复,过程如下。

        首先领头哨兵将从停止服务的主库的从库中挑选一个来充当新主库,挑选依据如下:

  1. 所有在线的从库中,选择优先级最高的从库。优先级通过slave-priority选项设置,值越小优先级越高。
  2. 如果有多个最高优先级的从库,则复制的命令偏移量越大(即复制越完整)越优先。
  3. 如果以上条件都一样,则选择运行ID较小的从库。

        选出一个从库后,领头哨兵将向该从库发送slave no one命令使其提升为主库。而后领头哨兵向其它从库发送slaveof命令来使其成为新主库的从库。最后一步则是更新内部的记录,将已经停止服务的旧的主库更新为新主库的从库,使得其恢复时自动以从库身份继续服务。

4. 部署

        哨兵以独立进程的方式对一个主从系统进行监控,监控的效果好坏取决于哨兵的视角是否具有代表性。如果一个主从系统中配置的哨兵较少,哨兵对整个系统判断的可靠性就会降低。当只有一个哨兵时,哨兵本身就可能发生单点故障。整体来讲,相对稳定的哨兵部署方案是使得哨兵的视角尽可能与每个节点的视角一致,即:

  • 为每个节点(无论是主库还是从库)部署一个哨兵。
  • 使每个哨兵与其对应的节点的网络环境相同或相近。

        这样的部署方案可以保证哨兵的视角拥有较高的代表性和可靠性。同时设置quorum的值为N/2取整 + 1,其中N为哨兵节点数量。这样只有当大部分哨兵节点同意后才会进行故障恢复。

        当节点较多时,考虑到每个哨兵都会和系统中的所有节点建立连接,为每个节点分配一个哨兵会产生较多连接,尤其是当进行客户端分片时使用多个哨兵节点监控多个主库会因为redis不支持连接复用而产生大量冗余连接,具体可以参见https://github.com/antirez/redis/issues/2257,同时如果redis节点负载较高,会在一定程度上影响哨兵的回复以及与其哨兵与其它节点的通信。所以配置哨兵时还需要根据实际的生产环境情况进行选择。

三、脑裂

        对于redis主从架构,master接受到请求之后执行完会立刻返回给client,然后会异步复制给其它master,此时会出现两种问题:

  • 当集群节点间网络或其它问题导致异步复制延时很高,如果此时master宕机了,毫无疑问会丢失延时的这段时间的数据。
  • 当网络分区变化导致master和slave节点之间无法正常通信时,sentinel哨兵集群会选举slave为master,此时与之前master连接的client一直发送数据,当我们进行恢复将原master当做新master的slave节点的时候,那么后来一直发送到原master内存的数据会丢失。

        解决上述两种数据丢失的问题,redis配置文件中有以下两行:

[code]min-slaves-to-write 3
min-slaves-max-lag 10

        意味着至少要有3个slave节点与master保持10秒钟以内的数据同步,否则master就不会接受新的写请求。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: