二、redis高可用

一、声明

本文为整理的学习笔记,主要内容来自与redis学习中的那些参考资料。

这些学习资料里面最详细的是《redis核心技术与实战》和《可能是北半球最全面的Redis6.x系列文章》,都是很详细的讲解redis。

因为时间、以及文字表达能力有限,所以文中有很多内容就直接摘抄使用了原文。有一些地方有标记,有一些地方没有标记。

如果转载此文章,需要把参考资料中的所有链接都标记上。

二、主从集群原理

1.建立连接,协商同步

从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。

1.1 连接建立过程

1.1.1 从库发送psync命令给主库,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync命令包含了主库的runID和复制进度offset两个参数。

  • runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。
  • offset,此时设为 -1,表示第一次复制。

1.1.2 主库收到psync命令后,会用FULLRESYNC响应命令,并带上两个参数:主库runID和主库目前的复制进度offset,返回给从库。从库收到响应后,会记录下这两个参数。

  • FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

2.主库同步数据给从库 主库将所有数据同步给从库。

从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。

2.1 主库执行bgsave命令,生成RDB文件,接着将文件发给从库。

2.2 从库接收到RDB文件后,会先清空当前数据库,然后加载RDB文件。

3.主库发送新写命令给从库

主库生成RDB文件,以及将RDB文件同步给从库的过程中,主库还是会提供服务。这些请求中的写操作会记录在replication buffer中。

3.1 当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

4.主从网络中断后的数据同步

redis2.8版本之前,网络中断,从库重新连接上主库以后,主从数据同步采用全量复制。

redis2.8版本之后,网络中断,从库重新连接上主库以后,主从数据同步采用增量复制。


实现原理:

4.1 主从网络断开以后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。

4.2 刚开始的时候,主库和从库的写读位置在一起,这算是它们的起始位置。随着主库不断接收新的写操作,它在缓冲区中的写位置会逐步偏离起始位置,我们通常用偏移量来衡量这个偏移距离的大小,对主库来说,对应的偏移量就是 master_repl_offset。主库接收的新写操作越多,这个值就会越大。同样,从库在复制完写操作命令后,它在缓冲区中的读位置也开始逐步偏移刚才的起始位置,此时,从库已复制的偏移量 slave_repl_offset 也在不断增加。正常情况下,这两个偏移量基本相等。

4.3 主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。

在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。

三、基于haproxy实现高可用

1.环境

1.1 操作系统

centos7

1.2 redis版本

6.2.6

1.3 ip

master:192.168.1.1

backup:192.168.1.2

haproxy:192.168.1.3

2.配置

2.1 配置主节点

修改192.168.1.1的配置文件中的以下参数:

daemonize yes
bind 0.0.0.0
dbfilename dump.rdb              
requirepass password
logfile "redismaster.log"

2.2 配置从节点

修改192.168.1.2的配置文件中的以下参数:

daemonize yes
bind 0.0.0.0
dbfilename dump.rdb
requirepass password               #redis6.0 以后支持ACL,可以针对用户划分详细的权限。线上使用时,建议与开发沟通确定方案,配置详细的ACL规则。
slave-read-only yes
replicaof 192.168.1.1 6379
logfile "redisbackup.log"

2.3 启动redis

# 主 192.168.1.1
/opt/redis/redis-server /opt/redis/redis.conf

#从 192.168.1.2
/opt/redis/redis-server /opt/redis/redis.conf

2.4 使用客户端验证

/opt/redis/redis-cli -p 6379 -p password

[root@jumpserver-2-195 redis-6379-backup]# ./redis-cli -p 6379 -p password
192.168.1.1:6379> info Replication
# Replication
role:master                                                          #主节点
connected_slaves:1                                                   #连接的从的数量
slave0:ip=192.168.1.2,port=6379,state=online,offset=378,lag=0        #可以看到这已经有从节点连接了
master_failover_state:no-failover                                    #故障状态
master_replid:efc83c43dd5b78d649267f0a3ccbe5e20cfd9dc8
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:378
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:378

2.5 配置haproxy

2.5.1 haproxy安装

略过

2.5.2 haproxy配置文件方式一

global

    chroot      /usr/local/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     65000
    user        user
    group       user
    daemon
    nbproc 1
    # turn on stats unix socket
    stats socket /usr/local/haproxy/stats

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
    mode                    tcp
    log                     global
    option                  dontlognull
    option                  redispatch
    option                  tcplog
    retries                 3
    timeout http-request    20s
    timeout queue           20s
    timeout connect         20s
    timeout client          20s
    timeout server          20s
    timeout http-keep-alive 20s
    maxconn                 65000
    log         127.0.0.1   local3 info

listen Redis
        bind    :6379
        mode    tcp
        balance roundrobin
        tcp-request inspect-delay 30s
        option  tcpka
        server RedisMaster  192.168.1.1:6379 check inter 3s rise 1 fall 3
        server RedisBackup  192.168.1.2:6379 check backup inter 3s rise 1 fall 3

2.5.3 haproxy配置文件方式二

defaults REDIS
 mode tcp
 timeout connect  4s
 timeout server  30s
 timeout client  30s

frontend ft_redis
 bind 10.0.0.1:6379 name redis
 default_backend bk_redis

backend bk_redis
 option tcp-check
 tcp-check send PINGrn
 tcp-check expect string +PONG
 tcp-check send info replicationrn
 tcp-check expect string role:master
 tcp-check send QUITrn
 tcp-check expect string +OK
 server R1 10.0.0.11:6379 check inter 1s
 server R2 10.0.0.12:6379 check inter 1s

2.5.4 注意事项

  • timeout server 30s和timeout client 30s 这两个参数,如果指定时间内没有发送数据,haproxy会断开。如果使用的是redis的订阅,则需要客户端做数据连接保活,或者连接重试。
  • timeout server 30s和timeout client 30s 时间不建议设置太长。设置时间太长的话,服务宕掉的情况下可以快速切换,但服务器宕机则无法快速切换,需要等到超时时间以后才可以。
  • 基于haproxy的问题:当主宕机后,haproxy自动切换到备以后,如果主重新启用,则会导致主宕机的这一段时间的数据丢失。因为主宕机时间的数据都写到从机了,主机启动后,从机连接主做数据同步。这个需要手动的调整redis的主从,以及更新haproxy的配置。

四、基于哨兵实现高可用

1. 哨兵(sentinel)是什么?

Redis sentinel是Redis官方的高可用方案。由一个或多个Sentinel实例组成。sentinel主要有以下几个功能:

  • 监控(Monitoring):Sentinel会不断地检查你的主服务器和从服务器是否运作正常。
  • 周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
  • 通知(Notification):哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
  • 统一的配置管理:连接者询问sentinel取得主从的地址。
  • 故障迁移:当主服务器不能正常工作时,Sentinel会自动进行故障迁移,也就是主从切换。

2.哨兵集群原理

2.1 哨兵集群的网络建立

哨兵实例通过redis提供的pub/sub 机制来实现建立哨兵集群。

每个哨兵实例将自己的IP和端口发布到“__sentinel__:hello”频道上。此频道“__sentinel__:hello”上的每个哨兵实例获取此频道上“__sentinel__:hello”其它哨兵实例的ip和端口,并与其它哨兵建立网络连接。

2.2 哨兵集群与主库、从库如何建立连接?

主库连接 在哨兵的配置文件中指定

从库连接 哨兵集群通过对redis的主实例发送info命令来获取所有从库的连接。

2.4 哨兵集群检测redis实例过程(摘自程序员大佬超博客)

Sentinel 使用的算法核心是 Raft 算法,主要用途就是用于分布式系统,系统容错,以及Leader选举,每个Sentinel都需要定期的执行以下任务:

  • 每个 Sentinel 会自动发现其他 Sentinel 和从服务器,它以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令。
  • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线。 有效回复可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。
  • 如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有Sentinel要以每秒一次的频率确认主服务器的确进入了主观下线状态。
  • 如果一个主服务器被标记为主观下线, 并且有足够数量的Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。
  • 在一般情况下, 每个Sentinel会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。 当一个主服务器被Sentinel标记为客观下线时,Sentinel向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  • 当没有足够数量的Sentinel同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向Sentinel的 PING 命令返回有效回复时, 主服务器的主管下线状态就会被移除。

2.5 如何选定新主库?

2.5.1 两种下线

主观下线:哨兵检测到对主库或从库的PING命令超时,就将它标记为“主观下线”。

客观下线:多个哨兵实例(基于哨兵实例配置的数量)判断主库下线,即为客观下线。当有N个哨兵实例时,最好有N/2+1个实例判断主库为”主观下线”,才能最终判断主库为”客观下线”,这样可以减少误判的概率。

2.5.2 从库筛选

2.5.2.1 检查从库当前的在线状态

2.5.2.2 判断从库之前的网络连接状态

判断依据配置项中的down-after-milliseconds * 10。down-after-milliseconds 认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。


2.5.3 从库打分

按照从库优先级、从库复制进度以及从库 ID 号三个规则进行三轮打分。只要某一轮中,有从库得分最高,就被选为主库。

从库优先级

  • 可以通过设置slave-priority配置项,来设置从库优先级,数字越小从库优先级越高。
  • slave-priority适用Sentinel模块(unstable,M-S集群管理和监控),需要额外的配置文件支持。slave的权重值,默认100.当master失效后,Sentinel将会从slave列表中找到权重值最低(>0)的slave,并提升为master。如果权重值为0,表示此slave为”观察者”,不参与master选举。


从库和旧主库同步程度最接近

比较不同从库的slave_repl_offset,找出最大slave_repl_offset的从库选为主库。

从库ID号

每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号。目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。


2.5.4 哨兵集群执行主从切换

1.任何一个哨兵实例检测到redis实例主库下线后,会给其它哨兵实例发送is-master-down-by-addr命令。其它哨兵实例会根据自己与主库的连接情况,做出Y或者N的回应,Y是赞成票,N相当于反对票。

2.一个哨兵实例获得N个(N为 quorum的值)赞成票后,就将redis主实例标记为“客观下线”。 例如有3个哨兵实例,quorum配置为2。只要有两章赞成票,即可将主库标记为“客观下线”。这两张赞成票中,包含当前哨兵实例的那一张。即只有其它另外两个哨兵实例有一个的回应Y,即将redis主库标记为“客观下线”。

3.收到足够的赞成票以后,哨兵实例会给其它哨兵实例发送命令,希望自己执行主从切换,并让其它哨兵进行投票。这个投票过程称为“Leader 选举”。因为最终执行主从切换的哨兵称为 Leader,投票过程就是确定 Leader。

4.成为 Leader 的哨兵需要满足两个条件:

第一,拿到半数以上的赞成票;

第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。 以 3 个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。

3.哨兵集群搭建

3.1 操作系统

centos7

3.2 redis版本

6.2.6

3.3 ip地址

sentinel:192.168.2.150
sentinel:192.168.2.151
sentinel:192.168.2.152
redis:   192.168.2.153
redis:   192.168.2.154
redis:   192.168.2.155

3.4 搭建流程

3.4.1 redis主从配置

3.4.1.1 配置主节点(192.168.2.153)配置文件(修改配置文件中以下打参数如下配置)

daemonize yes
bind 0.0.0.0
dbfilename dump.rdb              
requirepass password
logfile "redismaster.log"
min-replicas-to-write 1             # min-slaves-to-write:与主库通信的从库数量大于该值时才允许主库的写操作
min-replicas-max-lag 20             # min-slaves-max-lag:主从同步时,从节点延迟时长超过这个值时,主节点拒绝写入 
上面两个参数主要是为了防止哨兵集群故障时导致脑裂。min-replicas-max-lag这个参数的值要比down-after-milliseconds的值大。

3.4.1.2 配置从节点(192.168.2.154和192.168.2.155)配置文件(修改配置文件中以下打参数如下配置)

daemonize yes
bind 0.0.0.0
dbfilename dump.rdb
requirepass password               #redis6.0 以后支持ACL,可以针对用户划分详细的权限。线上使用时,建议与开发沟通确定方案,配置详细的ACL规则。
slave-read-only yes
replicaof 192.168.2.153 6379
logfile "redisbackup.log"
min-replicas-to-write 1             # min-slaves-to-write:与主库通信的从库数量大于该值时才允许主库的写操作
min-replicas-max-lag 20             # min-slaves-max-lag:主从同步时,从节点延迟时长超过这个值时,主节点拒绝写入 
上面两个参数主要是为了防止哨兵集群故障时导致脑裂。min-replicas-max-lag这个参数的值要比down-after-milliseconds的值大。

3.4.2 启动redis(192.168.2.153-154)

/opt/redis/redis-server /opt/redis/redis.conf

3.4.4 配置sentinel

在192.168.2.150-152上面配置下面的配置文件。

port 26379

daemonize yes                                                     #后台运行

pidfile /var/run/redis-sentinel.pid

logfile "sentinel.log"                                            #sentinel日志

dir /tmp

sentinel monitor mymaster 192.168.2.153 6379 2                   # mymaster为redis主从集群名称。192.168.2.153为主redis  6379为端口 2为有两个哨兵判定主观下线,即进入客观下线。

sentinel down-after-milliseconds mymaster 10000                  # redis主节点超时时间。redis主节点超过这个时间没有回应哨兵,则哨兵判定主节点主观下线。默认30s。

acllog-max-len 128

sentinel parallel-syncs mymaster 1                               # 指定了在发生failover主备切换时,最多可以有多少个slave同时对新的master进行同步。
这个数字越小,完成failover所需的时间就越长;反之,但是如果这个数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为1,来保证每次只有一个slave,处于不能处理命令请求的状态。                      

sentinel failover-timeout mymaster 180000                        # 故障转移的超时时间failover-timeout,默认三分钟,可以用在以下这些方面:
# 1. 同一个sentinel对同一个master两次failover之间的间隔时间。  
# 2. 当一个slave从一个错误的master那里同步数据时开始,直到slave被纠正为从正确的master那里同步数据时结束。  
# 3. 当想要取消一个正在进行的failover时所需要的时间。
# 4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来同步数据了

sentinel deny-scripts-reconfig yes                               # 查看sentinel配置文件

SENTINEL resolve-hostnames no                                    # 不适用主机名

SENTINEL announce-hostnames no                                   # 查看sentinel配置文件

#sentinel notification-script mymaster /opt/redis/script/notify.sh    #事件脚本。可以在sentinel有警告级别的时间发生调用这个脚本。最大执行时间60s,超时将被SIGKILL信号终止,之后重新执行。   
# 对于脚本的运行结果有以下规则:  
# 1. 若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10。
# 2. 若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。  
# 3. 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。

3.4.4 启动sentinel

在192.168.2.150-152上面执行以下命令

/opt/redis/redis-sentinel /opt/redis/sentinel.conf

3.4.5 连接验证验证

在sentinel上面执行./redis-cli -p 26379

./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=192.168.2.153:6379,slaves=2,sentinels=3 #配置如此,即为正确。

4.哨兵集群测试

4.1 停止192.168.2.153上面的redis

4.2 查看sentinel状态。可以看到address里面的地址已经变为192.168.2.154了。slaves=1,只有一个从库。

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=192.168.2.154:6379,slaves=1,sentinels=3 

4.3 启动192.168.2.153上面的redis。启动后等一段时间查看sentinel的状态。slaves=2,有两个从库。

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=192.168.2.154:6379,slaves=2,sentinels=3

五、客户端订阅哨兵集群

1.哨兵集群频道

哨兵集群关于redis实例切换的频道有以下几个

事件频道说明
主库下线事件+sdown实例进入”主观下线”状态
-sdown实例退出”主观下线”状态
+odown实例进入”客观下线”状态
-odown实例进入”客观下线”状态
从库重新配置事件+slave-reconf-sent哨兵发送SLAVEOF命令重新配置从库
-slave-reconf-inprog从库配置了新主库,但尚未进行同步
+slave-reconf-done从库配置了新主库,且和新主库完全同步
新主库切换+switch-master主库地址发生变化

2.python测试脚本

import sys
import getopt
import time  
from redis import StrictRedis


def redis_sub(ip, port, channel):
    redis = StrictRedis(host=ip, port=port)

    # redis 发布订阅
    pubsub = redis.pubsub()  

    # 监听通知
    pubsub.psubscribe(channel)

    # 开始消息循环
    print('Starting message loop')  
    while True:  
    # 获取消息
        message = pubsub.get_message()
        if message:
            print(message)
        else:
            time.sleep(0.01)


def get_argv():
    ip = None
    port = None
    channel = None

    if len(sys.argv) == 1 or sys.argv[1] == '-h':
        help() 
        sys.exit()
       
    argv = sys.argv[1:]

    try:
        opts, args = getopt.getopt(argv, "i:p:c",  
                                   ["ip=",
                                    "port=",
                                    "channel="])
    except:
        print("Error")

    for opt, arg in opts:
        if opt in ['-i', '--ip']:
            ip = arg
        elif opt in ['-p', '--port']:
            port = arg
        elif opt in ['-c', '--channel']:
            channel = arg
    
    redis_sub(ip, port, channel) 
   
def help():
    argv = sys.argv[0]
    print('''Redis Sub Test. version 1.0 2021/11/12\n'''.format(argv)) 
    print('''Usage : {0} <--host hostname> <--port port> <--channel channel>'''.format(argv)) 
    print('''        -h help;\n''') 
    print('''Supported channels: ''')
    print('''    all: 订阅哨兵集群的所有与主从切换相关的频道''')
    print('''    +sdown: redis实例进入"主观下线"状态 ''')
    print('''    -sdown: redis实例退出"主观下线"状态 ''')
    print('''    +odown: redis实例进入"客观下线"状态 ''')
    print('''    -sdown: redis实例退出"客观下线"状态 ''')
    print('''    +slave-reconf-sent: 哨兵发送SLAVEOF命令重新配置从库 ''')
    print('''    -slave-recon-inprog: 从库配置了新主库,但尚未进行同步 ''')
    print('''    +slave-reconf-done: 从库配置了新主库,且和新主库完全同步 ''')
    print('''    +switch-master: 主库地址发生变化 ''')
    print('''\nSample: ''')
    print('''订阅 redis实例进入"主观下线" 状态的消息: ''')
    print('''    {0} --host 192.168.2.1 --port 26379 --channel +sdown'''.format(argv))

def main():
    get_argv()

if __name__ == '__main__':
    main()

六、redis分片集群

我们目前的数量暂时没有使用到redis的分片集群,所以暂未整理redis的分片集群内容。

目前redis6.0版本以后增加了集群代理,官方目前暂时不建议在生产环境使用。等后续分片集群代理可以在生产使用的时候,有空就整理。

Previous Post

一、redis简介

Next Post

redis优化

Related Posts