一、声明
本文为整理的学习笔记,主要内容来自与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版本以后增加了集群代理,官方目前暂时不建议在生产环境使用。等后续分片集群代理可以在生产使用的时候,有空就整理。