1. redis 主从架构原理详解
(1) 读写分离
在redis主从架构中,Master节点负责处理写请求,Slave节点只处理读请求。对于写请求少,读请求多的场景,例如电商详情页,通过这种读写分离的操作可以大幅提高并发量,通过增加redis从节点的数量可以使得redis的QPS达到10W+。
(2) 主从同步
Master节点接收到写请求并处理后,需要告知Slave节点数据发生了改变,保持主从节点数据一致的行为称为主从同步,所有的Slave都和Master通信去同步数据也会加大Master节点的负担,实际上,除了主从同步,redis也可以从从同步,我们在这里统一描述为主从同步。
A. 主从同步的方法
- 增量同步
redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一边向主节点反馈自己同步到哪里了 (偏移量,这是redis-2.8之后才有的特性)。从节点同步数据的时候不会影响主节点的正常工作,也不会影响自己对外提供读服务的功能,从节点会用旧的数据来提供服务,当同步完成后,需要删除旧数据集,加载新数据,这个时候才会暂停对外服务。
因为内存的 buffer 是有限的,所以 redis 主节点不能将所有的指令都记录在内存 buffer 中。redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满
了,就会从头开始覆盖前面的内容。
- 快照同步
如果节点间网络通信不好,那么当从节点同步的速度不如主节点接收新写请求的速度时,buffer 中会丢失一部分指令,从节点中的数据将与主节点中的数据不一致,此时将会触发快照同步。
快照同步是一个非常耗费资源的操作,它首先需要在主节点上进行一次 bgsave 将当前内存的数据全部快照到RDB文件中,然后再将快照文件的内容全部传送到从节点。从节点将RDB文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步。
在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环。所以需要配置一个合适的复制 buffer 大小参数,避免快照复制的死循环。
- 无盘复制
主节点在进行快照同步时,会进行大量的文件 IO 操作,特别是对于非 SSD 磁盘存储时,快照会对系统的负载产生较大影响。特别是当系统正在进行 AOF 的 fsync 操作时如果发生快照复制,fsync 将会被推迟执行,这就会严重影响主节点的服务效率。
从 Redis 2.8.18 版开始支持无盘复制。所谓无盘复制是主节点会一边遍历内存,一遍将序列化的内容发送到从节点,而不是生成完整的 RDB 文件后才进行 IO 传输从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。
B. 主从同步的详细流程
(1) 在从节点的配置文件中的slaveof
配置项中配置了主节点的IP和port后,从节点就知道自己要和那个主节点进行连接了。
(2) 从节点内部有个定时任务,会每秒检查自己要连接的主节点是否上线,如果发现了主节点上线,就跟主节点进行网络连接。注意,此时仅仅是取得连接,还没有进行主从数据同步。
(3) 从节点发送ping命令给主节点进行连接,如果设置了口令认证(主节点设置了requirepass),那么从节点必须发送正确的口令(masterauth)进行认证。
(4) 主从节点连接成功后,主从节点进行一次快照同步。事实上,是否进行快照同步需要判断主节点的run id
,当从节点发现已经连接过某个run id
的主节点,那么视此次连接为重新连接,就不会进行快照同步。相同IP和port的主节点每次重启服务都会生成一个新的run id
,所以每次主节点重启服务都会进行一次快照同步,如果想重启主节点服务而不改变run id
,使用redis-cli debug reload
命令。
(5) 当开始进行快照同步后,主节点在本地生成一份rdb快照文件,并将这个rdb文件发送给从节点,如果复制时间超过60秒(配置项:repl-timeout),那么就会认为复制失败,如果数据量比较大,要适当调大这个参数的值。主从节点进行快照同步的时候,主节点会把接收到的新请求命令写在缓存 buffer 中,当快照同步完成后,再把 buffer 中的指令增量同步到从节点。如果在快照同步期间,内存缓冲区大小超过256MB,或者超过64MB的状态持续时间超过60s(配置项:client-output-buffer-limit slave 256MB 64MB 60
),那么也会认为快照同步失败。
(6) 从节点接收到RDB文件之后,清空自己的旧数据,然后重新加载RDB到自己的内存中,在这个过程中基于旧的数据对外提供服务。如果主节点开启了AOF,那么在快照同步结束后会立即执行BGREWRITEAOF,重写AOF文件。
(7) 主节点维护了一个backlog文件,默认是1MB大小,主节点向从节点发送全量数据(RDB文件)时,也会同步往backlog中写,这样当发送全量数据这个过程意外中断后,从backlog文件中可以得知数据有哪些是发送成功了,哪些还没有发送,然后当主从节点再次连接后,从失败的地方开始增量同步。这里需要注意的是,当快照同步连接中断后,主从节点再次连接并非是第一次连接,所以进行增量同步,而不是继续进行快照同步。
(8) 快照同步完成后,主节点后续接收到写请求导致数据变化后,将和从节点进行增量同步,遇到 buffer 溢出则再触发快照同步。
(9) 主从节点都会维护一个offset,随着主节点的数据变化以及主从同步的进行,主从节点会不断累加自己维护的offset,从节点每秒都会上报自己的offset给主节点,主节点也会保存每个从节点的offset,这样主从节点就能知道互相之间的数据一致性情况。从节点发送psync runid offset
命令给主节点从而开始主从同步,主节点会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,也可能是CONTINUE触发增量复制。
(10) 主从节点因为网络原因导致断开,当网络接通后,不需要手工干预,可以自动重新连接。
(11) 主节点如果发现有多个从节点连接,在快照同步过程中仅仅会生成一个RDB文件,用一份数据服务所有从节点进行快照同步。
(12) 从节点不会处理过期key,当主节点处理了一个过期key,会模拟一条del命令发送给从节点。
(13) 主从节点会保持心跳来检测对方是否在线,主节点默认每隔10秒发送一次heartbeat,从节点默认每隔1秒发送一个heartbeat。
(14) 建议在主节点使用AOF+RDB的持久化方式,并且在主节点定期备份RDB文件,而从节点不要开启AOF机制,原因有两个,一是从节点AOF会降低性能,二是如果主节点数据丢失,主节点数据同步给从节点后,从节点收到了空的数据,如果开启了AOF,会生成空的AOF文件,基于AOF恢复数据后,全部数据就都丢失了,而如果不开启AOF机制,从节点启动后,基于自身的RDB文件恢复数据,这样不至于丢失全部数据。RDB和AOF机制可以参考详解 redis-4.x 持久化机制
2. redis 主从架构搭建
使用3个虚拟机搭建一主二从的redis主从架构集群。首先参考redis-4.0.12单节点安装在每台机器上安装redis,然后修改redis配置文件,其中一个master节点的配置如下(未列出的保持默认即可):
# basic
daemonize yes
port 6379
logfile /home/hadoop/logs/redis/6379/redis_6379.log
pidfile /home/hadoop/pid/redis/6379/redis_6379.pid
dir /home/hadoop/data/redis/6379
# aof
# 主节点打开AOF机制
appendonly yes
# master
# 绑定本台机器的IP,否则主从节点无法通信
bind 192.168.239.101
# 设置master的认证口令为redis
requirepass redis
# backlog大小
repl-backlog-size 1mb
# 快照同步的超时时间
repl-timeout 60
# 开启无盘复制
repl-diskless-sync yes
# 无盘复制的延迟默认为5s,是为了等待更多的slave连接
repl-diskless-sync-delay 5
# 是否开启主从节点复制数据的延迟机制
# 当关闭时,主节点产生的命令数据无论大小都会及时地发送给从节点,这样主从之间延迟会变小
# 但增加了网络带宽的消耗。适用于主从之间的网络环境良好的场景
# 当开启时,主节点会合并较小的TCP数据包从而节省带宽。
# 默认发送时间间隔取决于Linux的内核,一般默认为40毫秒。
# 这种配置节省了带宽但增大主从之间的延迟。适用于主从网络环境复杂或带宽紧张的场景
repl-disable-tcp-nodelay no
# 触发快照同步的条件
# 如果增量同步的缓存大于256MB,或者超过60s大于64MB,则触发快照同步
client-output-buffer-limit slave 256mb 64mb 60
# 主从节点进行心跳的时间间隔
repl-ping-slave-period 10
两个slave节点的配置如下:
# basic
daemonize yes
port 6379
logfile /home/hadoop/logs/redis/6379/redis_6379.log
pidfile /home/hadoop/pid/redis/6379/redis_6379.pid
dir /home/hadoop/data/redis/6379
# slave
# 绑定本机的IP,另一个为192.168.239.103
bind 192.168.239.102
# 绑定master的ip和port
slaveof 192.168.239.101 6379
# 从节点只读
slave-read-only yes
# 从节点在处于快照同步期间是否对外提供服务
slave-serve-stale-data yes
# 如果 master 检测到 slave 的数量小于这个配置设置的值,将拒绝对外提供服务,0 代表,无论 slave 有几个都会对外提供服务
min-slaves-to-write 0
# 如果 master 发现大于等于 ${min-slaves-to-write} 个 slave 与自己的心跳超过此处配置的时间(单位s)
# 就拒绝对外提供服务
min-slaves-max-lag 10
# master的认证口令
masterauth redis
启动3个redis服务:
[hadoop@node01 ~]$ redis-server ~/apps/redis-4.0.12/redis_6379.conf
[hadoop@node01 ~]$ redis-server ~/apps/redis-4.0.12/redis_6379.conf
[hadoop@node01 ~]$ redis-server ~/apps/redis-4.0.12/redis_6379.conf
查看master日志:
# master 已经准备就绪
7810:M 15 Feb 18:08:54.108 * Ready to accept connections
# slave 92.168.239.102:6379 请求主从同步
7810:M 15 Feb 18:08:54.770 * Slave 192.168.239.102:6379 asks for synchronization
# slave 92.168.239.102:6379 请求快照同步
7810:M 15 Feb 18:08:54.770 * Full resync requested by slave 192.168.239.102:6379
# master 开始写 RDB 文件到本地磁盘
7810:M 15 Feb 18:08:54.771 * Starting BGSAVE for SYNC with target: disk
# 子进程(7816)开始写 RDB 文件到磁盘
7810:M 15 Feb 18:08:54.771 * Background saving started by pid 7816
# RDB 文件写到本地磁盘成功
7816:C 15 Feb 18:08:54.774 * DB saved on disk
7816:C 15 Feb 18:08:54.774 * RDB: 6 MB of memory used by copy-on-write
7810:M 15 Feb 18:08:54.812 * Background saving terminated with success
# master 和 slave 192.168.239.102:6379 主从同步成功
7810:M 15 Feb 18:08:54.813 * Synchronization with slave 192.168.239.102:6379 succeeded
# slave 92.168.239.103:6379 请求快照同步
7810:M 15 Feb 18:08:55.564 * Slave 192.168.239.103:6379 asks for synchronization
7810:M 15 Feb 18:08:55.564 * Full resync requested by slave 192.168.239.103:6379
7810:M 15 Feb 18:08:55.564 * Starting BGSAVE for SYNC with target: disk
7810:M 15 Feb 18:08:55.564 * Background saving started by pid 7817
7817:C 15 Feb 18:08:55.567 * DB saved on disk
7817:C 15 Feb 18:08:55.567 * RDB: 6 MB of memory used by copy-on-write
7810:M 15 Feb 18:08:55.619 * Background saving terminated with success
# master 和 slave 192.168.239.103:6379 主从同步成功
7810:M 15 Feb 18:08:55.620 * Synchronization with slave 192.168.239.103:6379 succeeded
看日志发现一个问题,我们在原理中介绍说:
主节点如果发现有多个从节点连接,在快照同步过程中仅仅会生成一个RDB文件,用一份数据服务所有从节点进行快照同步。
然而这里master的日志显示写了两次RDB文件,这里我查一些资料再来更新。(!!!待完善)
查看slave日志(这里只列出一个slave的日志):
7112:S 15 Feb 18:08:54.796 * DB loaded from disk: 0.027 seconds
7112:S 15 Feb 18:08:54.796 * Ready to accept connections
# 连接到 master 192.168.239.101:6379
7112:S 15 Feb 18:08:54.796 * Connecting to MASTER 192.168.239.101:6379
# 开始主从同步
7112:S 15 Feb 18:08:54.796 * MASTER <-> SLAVE sync started
7112:S 15 Feb 18:08:54.797 * Non blocking connect for SYNC fired the event.
7112:S 15 Feb 18:08:54.797 * Master replied to PING, replication can continue...
7112:S 15 Feb 18:08:54.798 * Partial resynchronization not possible (no cached master)
# 与run id 为 1b9f6081fa8cfd0d1c3771daa5224de2a734c5e5:0 的 master 进行快照同步
7112:S 15 Feb 18:08:54.800 * Full resync from master: 1b9f6081fa8cfd0d1c3771daa5224de2a734c5e5:0
7112:S 15 Feb 18:08:54.841 * MASTER <-> SLAVE sync: receiving 176 bytes from master
# 删除旧数据
7112:S 15 Feb 18:08:54.841 * MASTER <-> SLAVE sync: Flushing old data
# 加载 RDB 到内存中
7112:S 15 Feb 18:08:54.841 * MASTER <-> SLAVE sync: Loading DB in memory
# 同步成功
7112:S 15 Feb 18:08:54.841 * MASTER <-> SLAVE sync: Finished with success
测试主从架构:
(1) 使用redis-cli
访问3个redis服务
# 这里由于我们配置的时候设置了认证口令,所以 redis-cli 连接服务也需要认证,不认证可以进入命令行,但是无法进行操作,比如 set
[hadoop@node01 ~]$ redis-cli -h 192.168.239.101 -p 6379 -a redis
192.168.239.101:6379>
[hadoop@node02 redis-4.0.12]$ redis-cli -h 192.168.239.102 -p 6379 -a redis
192.168.239.102:6379>
[hadoop@node03 bin]$ redis-cli -h 192.168.239.103 -p 6379 -a redis
192.168.239.103:6379>
(2) 在 master 节点上 set 一个数据
192.168.239.101:6379> set name tom
OK
(3) 从节点上获取数据
192.168.239.102:6379> get name
"tom"
192.168.239.103:6379> get name
"tom"
(4) 尝试在slave上写入数据
192.168.239.103:6379> set age 20
(error) READONLY You can't write against a read only slave.
redis主从架构搭建成功!
3. wait 命令(扩展,redis-3.0新增)
wait m t
wait 提供两个参数,第一个参数是从节点的数量 m,第二个参数是时间 t,以毫秒
为单位。它表示等待 wait 指令之前的所有写操作同步到 n 个子节点 (也就是确保
m 个子节点的同步没有滞后),最多等待时间 t。如果时间 t=0,表示无限等待直到
N 个从库同步完成达成一致。
假设此时某个子节点与主节点网络断开,wait 指令第二个参数时间 t = 0,主从同步无法继续
进行,wait 指令会永远阻塞,redis 服务器将丧失可用性。
node01:6379> set name bob
OK
node01:6379> wait 2 5000
(integer) 2