Redis系列文章
原理篇
源码篇
- 【Redis源码分析之基础流程】
- 【Redis源码分析之持久化】
- 【Redis源码分析之主从复制】
- 【Redis源码分析之哨兵】
- 【Redis源码分析之集群故障转移】
- 【Redis源码分析之集群Meet命令和请求路由】
问题分析
Redis原理之主从复制
我们都说Redis具有高可靠性,这里有两层含义:
一是数据尽量少丢失。AOF和RDB保证。
二是服务尽量少中断。增加副本冗余,将一份数据同时保存在多个实例上。即使有一个实例出现故障,其他实例也可以对外提供服务,不会影响业务使用。(以主从复制作为基础)
Redis提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。
读操作:主库、从库都可以接收。
写操作:首先到主库执行,然后主库将写操作同步到从库。
1. 主从库第一次同步
启动多个Redis实例的时候,它们相互之间可以通过slaveof命令(Redis5.0之后用replicaof)形成主库和从库的关系,之后会在主要的三个阶段完成数据的第一次同步。
例如,现在有实例 1(ip:172.16.19.3)和实例 2(ip:172.16.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据。
slaveof 172.16.19.3 6379
主从库键数据第一次同步的三个阶段,如下图所示。
1.1 第一个阶段
主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。
具体来说,从库给主库发送psync命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync包含了主库的runID和复制进度offset两个参数。
- runID:是每个Redis实例启动都会自动生成的一个随机ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的runnID,所以设置为“?”
- offset:此时设置为-1,表示第一次复制。
主库收到psync命令后,会用FULLRESYNC响应命令,并带上两个参数:主库的runID和主库目前的复制进度offset,返回给从库。从库接收到响应后,会记录下这两个参数。
FULLRESYNC响应表示第一次复制采用的是全量复制,主库会把当前所有的数据都复制给从库。
1.2 第二个阶段
主库将所有数据同步给从库,从库收到数据后,在本地完成数据加载。这个过程,依赖内存快照生成的RDB文件。
具体来说:主库执行bgsave命令,生成RDB文件,接着将文件发送给从库。从库接收到RDB文件后,会先清空当前数据库,然后加载RDB文件。这是因为slaveof命令开始和主库同步前,可能保存了其他数据,为了避免之前数据的影响,从库需要先把当前数据清空。
在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。但这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用replication buffer,记录RDB文件生成后收到的所有写操作。
replication buffer:其实就是客户端的buffer,因为这里的客户端为从库,所以也称为replication buffer。
1.3 第三个阶段
主库会把第二阶段新收到的写命令,再发送给从库。
具体来说:当主库完成RDB文件发送后,就会把此时replication buffer中修改操作发给从库,从库在重新执行这些操作。这样主从库就实现同步了。
主从库第一次同步完成后,后续主库中执行的命令,直接发送给从库,从库再去执行相对应的命令。
2. 主从级联分担全量复制压力
通过上面分析第一次数据分析同步的过程,可以看到,一次全量复制中,对于主库来说,需要完成两个耗时的操作:生成RDB和传输RDB文件。如果从库数量很多,都要和主库进行全量复制的话,会导致主库忙于fork子进程生成RDB文件,进行数据全量同步。这样会给主库带来压力。
可以通过“主—从—从”模式减少主库压力。
可以选择一个从库,用于级联其他的从库,然后再选择一些从库,在这些从库执行以下命令,让它们和刚才所选的从库,建立起主从关系。
slaveof 所选从库IP 6379
这样,从库就会知道,在进行同步时,不再和主库进行交互,只要和级联的从库进行写操作同步就行了,这样就可以减轻主库的压力。如下图所示:
一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操再在同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。
但是网络如果断开,主从库之间就无法进行命令传播了,从库的数据就没法和主库保持一致,客户端就可能在从库中读到旧数据。
3. 主从库间网络断开怎么办
在Redis2.8之前,如果主从库在命令传播时出现了网络闪断,那么从库就会和主库重新进行一次全量复制,开销非常大。
在Redis2.8之后,网络断开后,主从库采用增量复制的方式继续同步。增量复制只会把主从库网络断连期间,主库收到的命令,同步给从库。
主库所有写命令,除了传播给从库之外,都会写入repl_backlog这个缓冲区。该缓冲区是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
刚开始的时候,主库和从库的写读位置在一起,这算它们的起始位置。随着主库不断接受新的写操作,它在缓冲区的写位置会逐步偏离起始位置,通常用偏移量来衡量这个偏移距离的大小,对主库来说,对应的偏移量就是master_repl_offset。主库接受的新写操作越多,这个值就越大。
同理,从库在复制完写操作命令后,它在缓冲区中的读位置也开始逐渐偏移刚才的起始位置,此时从库已复制的偏移量slave_repl_offset也不断增加(注:执行完processCommand方法之后,更新c->reploff,此处记录了从库的偏移量)。正常情况下, 这两个偏移量基本相等。
主从库的连接恢复之后,从库首先会给主库发送psync命令,并把自己当前的slave_repl_offset发送给主库,主库会判断自己的master_repl_offset和slave_repl_offset之间的差距。
在网络断开连接期间,主库可能会受到新的写操作命令,所以,一般来说,master_repl_offset会大于slave_repl_offset,此时,主库只要把master_repl_offset到slave_repl_offset之间的命令操作同步给从库就行。
可以通过下图,来说明一下增量复制的流程。
repl_backlog是一个环形缓冲区,所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这种情况下从库将会进行全量复制。 (repl_backlog是所有从库共享,而slave_repl_offset是由从库自己记录,每个从库的复制进度不一定相同)。
因此,需要想办法避免这种情况,一般而言,可以调整repl_backlog_size这个参数。这个参数和所需的缓冲空间大小有关,缓冲空间的计算公式:
缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度* 操作大小。
实际应用中,可能会有一些突发的请求压力,通常把这个缓冲空间扩大一倍,即repl_backlog_size = 缓冲空间大小 * 2.
在实际应用上,需要注意repl_backlog_size这个配置参数,如果配置得过小,在增量复制阶段,可能会导致从库的复制赶不上主库,进而导致从库重新进行全量复制。
关于repl_backlog还有一些补充: 1)一个从库,如果和主库断连时间过长,造成它在主库repl_backlog的slave_repl_offset位置上的数据已经被覆盖了,此时从库和主库将会进行全量复制。
2)每个从库会记录自己的slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过psync命令把自己记录的slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。
4. 主从心跳
主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令。
主从节点心跳机制:
1)主从节点彼此都有心跳检测机制(主节点会判断从节点的连接有效性,从节点会判断主节点的连接有效性。),各自模拟成对方的客户端进行通信。通过client list命令查看复制相关客户端信息,主节点的连接状态为flags=M,从节点连接状态为flags=S。
2)主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可通过参数repl-ping-slave-period(Redis5.0之后,修改为repl-ping-replica-period参数)控制发送频率。
3)从节点在主线程中每隔1秒发送replconf ack {offset}命令,给主节点上报自身当前的复制偏移量。replconf命令主要作用如下:
3.1)实时检测主从节点网络状态。(向主服务器INFO replication命令,在列出的从服务器列表的lag一栏中,可以看到lag值,表示从服务器最后一次向主服务发送REPLCONF ACK命令据现在过去多久,一般情况lag的值应该在0到1秒之间)
3.2)上报自身复制偏移量,主节点上进行记录。
有些资料上会写到检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区拉取丢失数据。《Redis设计与实现》《Redis开发与运维》。但是个人从代码角度来看,没有看到相关逻辑.
3.3)实现保证从节点的数量和延迟性功能,通过min-slaves-to-write、min-slaves-max-lag参数配置定义,默认分别为0、10.(Redis5.0之后,参数修改为min-replicas-to-write、min-replicas-max-lag)。min-slaves-max-lag参数的意义为:主从节点通信时间是否超过该时间,超过则认为不是一个“好”的连接。如果配置了min-slaves-to-write,要求“好”的连接超过该配置的数量,才能允许数据写入。
假设
min-slaves-to-write 3
min-slaves-max-lag 10
那么在从服务器数量少于3个,或者三个从服务器的延迟(lag)值都大于等于10秒时,主服务器将拒绝执行写命令。
主节点根据replconf ack命令,判断从节点超时时间,体现在info replication统计中的lag信息,lag表示与从节点最好一次通信延迟的描述,正常延迟应该在0和1之间。如果repl-timeout配置的值(默认60秒),则判定从节点下线并断开复制客户端连接。
从节点根据主节点同步的命令,或者PING命令记录和主服务器交互的最后时间,超过repl-timeout配置的值(默认60秒),则会和主节点断开连接。
5. 附录
5.1 主从同步完整细节
前面阐述的第一次同步只是大致的步骤,更加完整的细节如下图:
流程再描述一下:
1)从节点发送psync命令进行数据同步,因为hi第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync ? -1
2)主节点根据psync -1 解析出当前为全量复制,恢复+FULLRESYNC响应。
3)从节点接收主节点的响应数据,保存主节点的运行ID和偏移量offset
4)主节点执行bgsave保存RDB文件到本地。
5)主节点发送RDB文件给从节点,从节点把接收到的RDB文件保存在本地,并直接作为从节点的数据文件。这边需要注意,对于数据量比较大的从节点,如生成RDB文件超过6G以上。传输文件的操作非常耗时,速度取决于主从节点之间的网络带宽。如果总时间超过repl_timeout所配置的时间(默认60s),从节点将放弃接收RDB文件并清理已经下载的临时文件,导致全量复制失败。 针对数据量比较大的节点,调大repl_timeout参数,防止出现全量同步数据时超时。
另外,为了降低主节点磁盘开销,Redis支持无盘复制,生成的RDB文件不保存到硬盘而是直接通过网络发送给从节点,通过repl-diskless-sync参数控制,默认关闭。无盘复制适用于主节点所在机器磁盘性能较差,但网络带宽较充裕的场景。
6)对于从节点接收RDB文件到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写明了数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成复制客户端缓冲区溢出。 默认配置client-output-buffer-limit slave 256MB 64MB 60。如果60秒内缓冲区消耗持续大于64MB,或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。
7)从节点接收完主节点传送来的全部数据后,会清空自身旧数据。
8)从节点清空数据后,开始加载RDB文件。对于较大的RDB文件,这一步操作仍然比较耗时。
9)从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof操作,为了保证全量复制后AOF持久化文件立刻可用。
5.2 主从全量同步使用RDB原因
主从全量同步使用RDB而不适用AOF原因:
1)RDB文件内容是经过压缩的二进制数据,文件很小。而AOF文件记录的是每一次操作的命令,写操作越多文件就会变得越大。在全量同步时,传输RDB文件可以尽量降低主库机器网络带宽的消耗,从库加载RDB文件时,一是文件小,读取整个文件的速度会很快。二是因为RDB文件存储是二进制数据,从库根据RDB协议解析还原数据即可,速度会很快。而AOF需要重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度会比RDB慢很多。
2)如果要使用AOF做全量同步,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会影响Redis性能。而RDB只有在需要定时备份和全量同步数据时才出发生成一次快照。在很多丢失数据不敏感的业务场景,其实不需要开启AOF。
5.3 主从复制数据同步方案
5.3.1 Redis2.8之前
每次从服务器向主服务器发送sync命令,主服务器都需要做持久化操作(bgsave),然后把数据全量同步给从服务器。
5.3.2 Redis2.8
从服务器会记录已经从主服务器接收到的数据量(复制偏移量);主服务器会维护一个复制缓冲区,记录已经执行且待发送给从服务器的命令请求,同时还需要记录复制缓存区第一个字节的复制偏移量。从服务器同步主服务器的命令也修改成psync。从服务器连接主服务器时,会向主服务器发送psync命令,带上已收到的复制偏移量,主服务器再根据该复制偏移量是否在复制缓冲区做操作。如果在,则直接向从服务器发送复制缓冲区中的命令,这部分可以称为部分重同步;如果不在,则需要执行持久化操作,同时将所有新执行的写命令缓存到复制缓冲区中,并重置复制缓冲区第一个字节的复制偏移量,这可以称为完成重同步。
执行部分重同步要求比较严格:1)runId必须相等。2)复制偏移量必须在复制缓冲区中。在实际情况下,经常会出现两种情况:1)从服务器重启,复制信息丢失。2)主服务器故障转移,主服务器运行ID发生改变。
5.3.3 redis4.0
对于上述问题进行优化,提出psync2协议。
方案1:持久化主从复制信息。 Redis服务器关闭时,将主从复制信息作为辅助字段存储在RDB文件中;Redis服务器启动加载RDB文件时,恢复主从复制信息,重新同步主服务器时携带。
方案2:存储上一个主服务器复制信息。
在server.h/redisServer 结构体上,存储上一个主服务器复制信息。
char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from master*/
long long second_replid_offset; /* Accept offsets up to this for replid2. */
从服务器在做主从切换的时候,会用replid2和sencon_replid_offset存储之前主服务器的运行ID和复制偏移量。
//replication.c#shiftReplicationId
void shiftReplicationId(void) {
memcpy(server.replid2,server.replid,sizeof(server.replid));
server.second_replid_offset = server.master_repl_offset+1;
changeReplicationId();
}
另外在判断是否能执行重同步的条件也修改:
//replication.c#masterTryPartialResynchronization
if (strcasecmp(master_replid, server.replid) &&
(strcasecmp(master_replid, server.replid2) ||
psync_offset > server.second_replid_offset))
{
...
goto need_full_resync;
}
6. 参考资料
- 《Redis核心技术与实战》——极客时间
- 《Redis开发与运维》
- 《Redis 5设计与源码分析》
- 《Redis设计与实现》