主从集群
在搭建主从集群前,我们先把Redis安装起来:
#解压Redis压缩包
[root@master lf]# tar -zxvf redis-6.2.1.tar.gz
……
#安装gcc
[root@master redis-6.2.1]# yum install gcc
……
[root@master lf]# cd redis-6.2.1
#进入解压好的Redis目录,进行编译和安装
[root@master redis-6.2.1]# make
……
之后,我们在Redis目录下创建config和data目录,config目录用于存放Redis的配置文件,data用于存放Redis数据:
[root@master redis-6.2.1]# mkdir config
[root@master redis-6.2.1]# mkdir data
[root@master redis-6.2.1]# cp redis.conf config/
[root@master redis-6.2.1]# cd config/
[root@master config]# mv redis.conf redis-6379.conf
修改redis-6379.conf文件:
#默认端口号为6379,
port 6379
#关闭保护模式,开启的话,只有本机才可以访问redis
protected-mode no
#redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes时,启用守护进程
daemonize yes
#redis以守护进程方式运行时,系统默认会把pid写入/var/run/redis.pid,可以通过pidfile指定pid文件
pidfile /var/run/redis-6379.pid
#指定日志文件名
logfile "6379.log"
#工作目录,日志、db和aof文件会存放在指定的工作目录下
dir /home/lf/redis-6.2.1/data
#设置Redis密码
requirepass 123456
#bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可
#bind 127.0.0.1 -::1
#指定存储数据的文件名
dbfilename dump-6379.rdb
然后我们回到Redis目录,并根据config下redis-6379.conf配置,启动一个Redis进程。之后我们启动一个Redis客户端,在校验密码后,再在Redis上设置一个<hello,world>的键值对:
[root@master redis-6.2.1]# src/redis-server config/redis-6379.conf
[root@master redis-6.2.1]# src/redis-cli
127.0.0.1:6379> AUTH 123456
OK
127.0.0.1:6379> SET hello world
OK
127.0.0.1:6379> GET hello
"world"
Redis可以正常的存储数据,代表我们第一个Redis进程启动成功,6379端口的Redis进程,即为我们的主节点。下面,我们要启动两个从节点,我们向Redis的主节点写数据,主节点会向Redis从节点同步数据,我们可以向从节点读数据,从而减轻Redis主节点的读压力。
我们重新拷贝一份原先的redis配置,并命名为redis-6380.conf:
[root@master config]# cp redis-6379.conf redis-6380.conf
之后,我们对redis-6380.conf做如下修改:
port 6380
protected-mode no
daemonize yes
pidfile /var/run/redis-6380.pid
requirepass 123456
logfile "6380.log"
dir /home/lf/redis-6.2.1/data
dbfilename dump-6380.rdb
#如果主节点有设置密码,从节点也需要配置主节点的校验密码
masterauth 123456
#从本机6379端口的Redis进程同步数据,Redis 5.0之前使用slaveof
replicaof 192.168.6.86 6379
#配置从节点为只读
replica-read-only yes
修改完毕后,我们根据redis-6380.conf重新拷贝一份redis-6381.conf,并将原先配置为6380的地方改为6381,然后我们根据这两个配置再次启动两个端口号分别为6380和6381的从节点:
[root@master config]# cp redis-6380.conf redis-6381.conf
……
[root@master redis-6.2.1]# src/redis-server config/redis-6380.conf
[root@master redis-6.2.1]# src/redis-server config/redis-6381.conf
然后我们查看Redis进程,可以看到目前启动了3个Redis进程:
[root@master redis-6.2.1]# ps -ef | grep redis
root 119821 1 0 Apr06 ? 00:08:30 src/redis-server *:6379
root 123856 1 0 Apr06 ? 00:07:25 src/redis-server *:6380
root 123862 1 0 Apr06 ? 00:07:41 src/redis-server *:6381
启动从节点后,我们在主节点里面查看从节点信息,可以看到6379服务当前的角色为主节点(master),并且看到两个从节点(slave0和slave1)的信息:
127.0.0.1:6379> INFO
……
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.6.86,port=6380,state=online,offset=576,lag=1
slave1:ip=192.168.6.86,port=6381,state=online,offset=576,lag=0
master_failover_state:no-failover
master_replid:7ee936b581b51f4b389b68cc3dcfcb3737468d5c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:576
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:576 # CPU
used_cpu_sys:0.956209
used_cpu_user:0.681712
used_cpu_sys_children:0.001589
used_cpu_user_children:0.000000
used_cpu_sys_main_thread:0.949240
used_cpu_user_main_thread:0.670907
……
我们启动一个Redis客户端连接到从节点,测试从节点是否有同步主节点的数据,并尝试在从节点进行写入操作:
[root@master redis-6.2.1]# src/redis-cli -p 6380
127.0.0.1:6380> AUTH 123456
OK
#从节点将把主节点的数据同步过来。
127.0.0.1:6380> KEYS *
1) "hello"
127.0.0.1:6380> GET hello
"world"
#尝试设置<hello,spring>的键值对,从节点报错:不能对只读副本进行写入操作。
127.0.0.1:6380> SET hello spring
(error) READONLY You can't write against a read only replica.
于是,我们尝试在主节点设置<hello,tomcat>键值对,并在从节点获取hello对应的value,可以看到主节点设置完毕后,依旧会将最新的值同步到从节点:
127.0.0.1:6379> SET hello tomcat
OK
127.0.0.1:6380> GET hello
"tomcat"
至此,主从集群算是搭建完毕,由于笔者服务器有限,所以主节点和从节点都在一台机器上,大家如果有多余的服务器,也可以在多台服务器上搭建主从集群。
Redis主从工作原理
如果你为主节点(master)配置了一个从节点(slave),不管这个从节点是否是第一次连接上主节点,它都会发送一个PSYNC命令给主节点请求复制数据。
主节点收到PSYNC命令后,会在后台进行数据持久化通过BGSAVE生成最新的rdb快照文件,持久化期间,主节点会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,主节点会把这份rdb文件数据集发送给从节点,从节点会把接收到的数据进行持久化生成rdb再加载到内存中。之后,主节点再将之前缓存在内存中的命令发送给从节点。
当主节点与从节点之间的连接由于某些原因而断开厚,从节点重新连上主节点,如果主节点收到了多个从节点并发连接请求,它只会进行一次持久化,而不是一个连接持久化一份rdb文件再单独把每个连接持久化下来的rdb文件发送给多个重连的从节点。
数据部分复制
当主节点和从节点断开重连后,一般都会对整份数据进行复制。但从Redis2.8版本开始,Redis改用可以支持部分数据复制的命令PSYNC去主节点同步数据,从节点与主节点能够在网络连接断开后重连只进行断线后数据变化的增量更新。
主节点会在内存中创建一个缓存队列,缓存最近一段时间更新数据的命令,主节点和它所有的从节点都维护了更新数据命令的下标offset和主节点的进程ID。当网络连接断开后,从节点根据下标向主节点同步断线期间未执行的命令。如果主节点进程ID变化了,或者从节点数据下标offset太旧,已经不在主节点的缓存队列里了,那么将会进行一次全量数据的复制。
主从复制(部分复制,断点续传)流程图:
如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),可以做如 下架构,让部分从节点与从节点(与主节点同步)同步数据:
Redis哨兵高可用架构
哨兵(sentinel)是特殊的Redis服务,不提供读写,主要用来监控Redis实例节点。
哨兵架构下的客户端在第一次从哨兵获得Redis主节点后,后续就直接访问Redis的主节点,不需要每次都通过哨兵访问Redis主节点。当Redis主节点有变化时,哨兵会第一时间感知到,并且将新的Redis主节点通知给客户端(这里面Redis客户端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)。
下面,我们来搭建哨兵集群,我们先将sentinel.conf复制一份到config目录下,并修改其命名:
[root@master redis-6.2.1]# cp sentinel.conf config/
[root@master redis-6.2.1]# cd config/
[root@master config]# cp sentinel.conf sentinel-26379.conf
之后我们修改sentinel-26379.conf的配置,其中大部分配置和原先redis.conf的配置一样,这里就不再针对各个配置作解释了:
protected-mode no
#<1>
port 26379
daemonize yes
#<2>
pidfile /var/run/redis-sentinel-26379.pid
#<3>
logfile "26379.log"
dir /home/lf/redis-6.2.1/data
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
#<ip>:哨兵集群监听的主节点IP
#<redis-port>:监听主节点IP的端口号
#<master-name>:哨兵集群可以监控多个Redis主从集群,设定Redis主节点便于客户端访问
#<quorum>:指明有多少个哨兵认为主节点失效时才算真正失效,值一般为:sentinel总数/2 +1
sentinel monitor mymaster 192.168.6.86 6379 2
#配置访问主节点的密码
sentinel auth-pass mymaster 123456
修改完sentinel-26379.conf后,我们复制两份配置分别命名为:sentinel-26380.conf和sentinel-26381.conf,并将上述<1>、<2>、<3>处26379分别改为26380和26381:
[root@master config]# cp sentinel-26379.conf sentinel-26380.conf
[root@master config]# cp sentinel-26379.conf sentinel-26381.conf
之后,我们根据这三份配置启动Redis哨兵服务:
[root@master redis-6.2.1]# src/redis-sentinel config/sentinel-26379.conf
[root@master redis-6.2.1]# src/redis-sentinel config/sentinel-26380.conf
[root@master redis-6.2.1]# src/redis-sentinel config/sentinel-26381.conf
启动完毕后,配置文件会自动补充主节点名称对应从节点的IP端口以及其他哨兵进程的IP和端口:
[root@master config]# cat sentinel-26379.conf
……
sentinel known-replica mymaster 192.168.6.86 6381
sentinel known-replica mymaster 192.168.6.86 6380
sentinel known-sentinel mymaster 192.168.6.86 26381 55d5eb86a5f220d802d08b1fff2d02f3433023ff
sentinel known-sentinel mymaster 192.168.6.86 26380 dc3444e290b9be39413364211a316404a8914e9c
[root@master config]# cat sentinel-26380.conf
……
sentinel known-replica mymaster 192.168.6.86 6381
sentinel known-replica mymaster 192.168.6.86 6380
sentinel known-sentinel mymaster 192.168.6.86 26379 6c62a2a7a9da058792b041ec5f7fb488c8674c74
sentinel known-sentinel mymaster 192.168.6.86 26381 55d5eb86a5f220d802d08b1fff2d02f3433023ff
[root@master config]# cat sentinel-26381.conf
……
sentinel known-replica mymaster 192.168.6.86 6381
sentinel known-replica mymaster 192.168.6.86 6380
sentinel known-sentinel mymaster 192.168.6.86 26380 dc3444e290b9be39413364211a316404a8914e9c
sentinel known-sentinel mymaster 192.168.6.86 26379 6c62a2a7a9da058792b041ec5f7fb488c8674c74
到目前为止,我们终于搭建完主从集群还有哨兵集群,现在,我们尝试根据主节点名称从哨兵集群获取主节点和从节点信息:
from redis.sentinel import Sentinel sentinel = Sentinel([('192.168.6.86', 26379),
('192.168.6.86', 26380),
('192.168.6.86', 26381),
],
socket_timeout=0.5)
master = sentinel.discover_master('mymaster')
print("master:", master)
slave = sentinel.discover_slaves('mymaster')
print("slave:", slave)
执行结果:
master: ('192.168.6.86', 6379)
slave: [('192.168.6.86', 6380), ('192.168.6.86', 6381)]
之后,我们尝试用向主节点写数据,再通过从节点读取数据:
from redis.sentinel import Sentinel sentinel = Sentinel([('192.168.6.86', 26379),
('192.168.6.86', 26380),
('192.168.6.86', 26381),
],
socket_timeout=0.5)
master = sentinel.discover_master('mymaster')
slave = sentinel.discover_slaves('mymaster')
master = sentinel.master_for('mymaster', socket_timeout=2, password='123456', db=0)
# 向主节点接数据
w_ret = master.set('hello', 'python')
slave = sentinel.slave_for('mymaster', socket_timeout=2, password='123456', db=0)
# 通过从节点读取数据
r_ret = slave.get('hello')
print(r_ret)
执行结果:
b'python'
最后,我们要验证哨兵集群的特性,就是如果主节点下线,哨兵集群会选举从节点成为主节点,我们在<1>、<2>处打印原先的主节点和从节点,在<3>处有一个循环往Redis主节点设置<fooN,barN>键值对,每设置完一次不管是否有抛出异常,都会休眠1s,这里会循环120次,总共休眠120s,在这120s内,我们将杀死Redis主节点,再看看哨兵集群是否会自动帮我们选取主节点,如果有选取出最新的主节点,在最后的<4>、<5>处会重新打印主节点和从节点信息:
from redis.sentinel import Sentinel
import time sentinel = Sentinel([('192.168.6.86', 26379),
('192.168.6.86', 26380),
('192.168.6.86', 26381),
],
socket_timeout=0.5)
master = sentinel.discover_master('mymaster')
print("origin master:", master) # <1>
slave = sentinel.discover_slaves('mymaster')
print("origin slave:", slave) # <2>
master = sentinel.master_for('mymaster', socket_timeout=2, password='123456', db=0)
for i in range(120): # <3>
try:
foo = 'foo' + str(i)
bar = 'bar' + str(i)
master.set(foo, bar)
print("set %s %s" % (foo, bar))
time.sleep(1)
except Exception as e:
print("Exception:", e)
time.sleep(1) master = sentinel.discover_master('mymaster')
print("new master:", master) # <4>
slave = sentinel.discover_slaves('mymaster')
print("new slave:", slave) # <5>
运行上面的代码,在10多秒时我们杀死端口号为6379的进程,即我们的Redis主节点:
[root@master redis-6.2.1]# ps -ef | grep redis
root 117641 1 0 08:23 ? 00:02:18 src/redis-server *:6379
root 117647 1 0 08:23 ? 00:02:16 src/redis-server *:6380
root 117654 1 0 08:24 ? 00:02:21 src/redis-server *:6381
root 117665 16267 0 08:24 pts/1 00:00:00 src/redis-cli
root 117666 22426 0 08:24 pts/0 00:00:00 src/redis-cli -p 6380
root 130214 1 0 16:56 ? 00:00:06 src/redis-sentinel *:26379 [sentinel]
root 130220 1 0 16:56 ? 00:00:06 src/redis-sentinel *:26380 [sentinel]
root 130226 1 0 16:57 ? 00:00:06 src/redis-sentinel *:26381 [sentinel]
root 130523 48986 0 17:08 pts/3 00:00:00 grep --color=auto redis
[root@master redis-6.2.1]# kill -9 117641
执行结果:
origin master: ('192.168.6.86', 6379)
origin slave: [('192.168.6.86', 6380), ('192.168.6.86', 6381)]
set foo0 bar0
set foo1 bar1
……
set foo17 bar17
Exception: Timeout connecting to server
Exception: Timeout connecting to server
……
Exception: Timeout connecting to server
set foo28 bar28
……
set foo117 bar117
set foo118 bar118
set foo119 bar119
new master: ('192.168.6.86', 6380)
new slave: [('192.168.6.86', 6381)]
现在我们来分析上面的运行结果,我们在打印了Redis主从节点的信息后,开始循环向Redis设置键值对,每次设置会休眠1s,在循环执行到第17次的时候,杀死端口号为6379的进程,之后代码再尝试向Redis主节点设置键值对就开始抛异常:Timeout connecting to server表示连接Redis服务超时,抛异常后会休眠1s,大致在几十秒内,哨兵集群判定Redis主节点下线,并重新选举出新的主节点,我们才能将<foo28,bar28>存储进新的Redis主节点。
最后程序重新打印Redis主从节点信息,可以看到主从节点信息已经跟以前不一样了,端口号为6380的从节点在哨兵集群判定主节点下线后,被选举为主节点,端口号6381的Redis服务依然为从节点。
我们连接6380Redis服务,查看Redis信息,可以看到6380服务当前的角色为主节点,有一个6381的从节点正连接到它。
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.6.86,port=6381,state=online,offset=227449,lag=1
master_failover_state:no-failover
master_replid:2cb865fd2e68bb7b6cd9a3905cab2bc9aa115c02
master_replid2:7ee936b581b51f4b389b68cc3dcfcb3737468d5c
master_repl_offset:227588
second_repl_offset:185259
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:227588
如果大家还有印象,之前我们在redis-6380.conf和redis-6381.conf两份配置里,配置了主节点的信息,如果打印这两份配置的主节点信息,我们会发现,之前配置在redis-6380.conf的主节点信息已经不存在了,因为6380服务已经成为主节点,而redis-6381.conf从原先的192.168.6.86 6379改为192.168.6.86 6380:
[root@master config]# cat redis-6380.conf | grep replicaof
# Master-Replica replication. Use replicaof to make a Redis instance a copy of
[root@master config]# cat redis-6381.conf | grep replicaof
# Master-Replica replication. Use replicaof to make a Redis instance a copy of
replicaof 192.168.6.86 6380
而哨兵集群监控的主节点,也从我们原先设定的6379切换成6380
[root@master config]# cat sentinel-26379.conf | grep mymaster
sentinel monitor mymaster 192.168.6.86 6380 2
……
[root@master config]# cat sentinel-26380.conf | grep mymaster
sentinel monitor mymaster 192.168.6.86 6380 2
……
[root@master config]# cat sentinel-26381.conf | grep mymaster
sentinel monitor mymaster 192.168.6.86 6380 2
……
至此,笔者带大家了解了如何搭建主从&哨兵集群,以及哨兵集群的特性。大家私下也可以试着搭建一下主从&哨兵集群,有任何疑问也可以对照下载笔者的配置文件进行配置。