目录
4.1 redisServer结构体中与主从复制相关的成员变量
0.阅读与引用
menwen-Redis 复制(replicate)源码详细解析
《Redis开发与运维》第6章 查看自己的电子书
《Redis5 设计与源码分析》第21章 主从复制 链接
《Redis深度历险》原理8 主从同步 查看电子书
1.简介
1.1 什么是主从复制?
主从复制是一种数据备份的方案.
简单来说,是使用两个或两个以上相同的数据库,将一个数据库当做主数据库,而另一个(或多个)数据库当
做从数据库. 在主数据库中进行相应操作时, 从数据库记录下所有主数据库的操作,使其二者一模一样.
这个过程就是主从复制.
1.2 为什么要主从复制?
为什么需要主从复制功能呢?简单来说,主从复制功能主要有以下两点作用。
1)读写分离,单台服务器能支撑的QPS是有上限的,我们可以部署一台主服务器、多台从服务器,主服务
器只处理写请求,从服务器通过复制功能同步主服务器数据,只处理读请求,以此提升Redis服务能力;
另外我们还可以通过复制功能来让主服务器免于执行持久化操作:只要关闭主服务器的持久化功能,然后
由从服务器去执行持久化操作即可。
2)数据容灾,任何服务器都有宕机的可能,我们同样可以通过主从复制功能提升Redis服务的可靠性;由
于从服务器与主服务器数据保持同步,一旦主服务器宕机,可以立即将请求切换到从服务器,从而避免
Redis服务中断。
1.3 Redis实现主从复制的三种方式
2.相关命令与配置
info replication 查看执行此命令的客户端对应的服务端的节点信息
slaveof 127.0.0.1 6379 将执行命令的客户端对应的服务端的节点设置成IP为127.0.0.1,端口为6379的主机对应的redisServer的从节点
3.Redis主从复制实现方案的历史变迁
3.1 Redis2.8以前
Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作:
❑同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态;
❑命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让
主从服务器的数据库重新回到一致状态.
3.1.1 同步
当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操
作,也即是,将从服务器的数据库状态更新至主服务器当前所处的数据库状态。从服务器对主服务器的
同步操作需要通过向主服务器发送SYNC命令来完成,以下是SYNC命令的执行步骤:
1)从服务器向主服务器发送SYNC命令。
2)收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开
始执行的所有写命令。
3)当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服
务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
4)主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数
据库状态更新至主服务器数据库当前所处的状态。
3.1.2 命令传播
在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,但这种一致并不是一成不变的,每
当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主从服务器状态不
再一致.
为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将自己执
行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同
的写命令之后,主从服务器将再次回到一致状态.
这个过程就是命令传播.
3.1.3 Redis2.8以前性能分析
在Redis中,从服务器对主服务器的复制可以分为以下两种情况:
❑初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制
的主服务器不同。
❑断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连
接重新连上了主服务器,并继续复制主服务器。
对于初次复制来说,旧版复制功能能够很好地完成任务,但对于断线后重复制来说,旧版复制功能虽然也
能让主从服务器重新回到一致状态,但效率却非常低.
缺点分析:
发送SYNC命令的确可以让主从服务器重新回到一致状态,但如果我们仔细研究这个断线重复制过程,就会
发现传送RDB文件这一步实际上并不是非做不可的,如果master中有1w条数据,slave有9990条数据,为了
那10条数据,我们需要全部重新开始吗?显然没有必要,要知道SYNC命令是一个非常耗费资源的操作.
3.1.4 须知-SYSC是很重的操作
SYNC命令是一个非常耗费资源的操作每次执行SYNC命令,主从服务器需要执行以下动作:
(1)主服务器需要执行BGSAVE命令来生成RDB文件,这个生成操作会耗费主服务器大量的CPU、内存和磁盘
I/O资源;
(2)主服务器需要将自己生成的RDB文件发送给从服务器,这个发送操作会耗费主从服务器大量的网络资源
(带宽和流量),并对主服务器响应命令请求的时间产生影响;
(3)接收到RDB文件的从服务器需要载入主服务器发来的RDB文件,并且在载入期间,从服务器会因为阻塞
而没办法处理命令请求.
由此可以看出SYNC命令是一个十分耗费资源的操作,所以Redis有必要保证在真正有需要时才执行SYNC命
令.对现有的模式做出革命性改变!
3.2 Redis2.8
3.2.1 PSYNC命令代替SYNC命令
为了解决旧版复制功能在处理断线重复制情况时的低效问题,Redis从2.8版本开始,使用PSYNC命令代替
SYNC命令来执行复制时的同步操作。
PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partialresynchronization)两种模式:
(1)完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都
是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。
(2)部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允
许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这
些写命令,就可以将数据库更新至主服务器当前所处的状态。PSYNC命令的部分重同步模式解决了旧版复
制功能在处理断线后重复制时出现的低效情况,
看看下面的截图:
对比一下SYNC命令和PSYNC命令处理断线重复制的方法,不难看出,虽然SYNC命令和PSYNC命令都可以让断
线的主从服务器重新回到一致状态,但执行部分重同步所需的资源比起执行SYNC命令所需的资源要少得
多,完成同步的速度也快得多。执行SYNC命令需要生成、传送和载入整个RDB文件,而部分重同步只需要
将从服务器缺少的写命令发送给从服务器执行就可以了.
3.2.2 版本2.8的主从复制初始化流程图
3.2.3 部分重同步的实现
部分重同步功能由以下三个部分构成:
(1)主服务器的复制偏移量(replication offset)和从服务器的复制偏移量;
(2)主服务器的复制积压缓冲区(replication backlog);
(3)服务器的运行ID(run ID).
详细解说:
1.复制偏移量
执行复制的双方(主服务器和从服务器)会分别维护一个复制偏移量,主服务器每次向从服务器传播N个字节
的数据时,就将自己的复制偏移量的值加上N, 从服务器每次收到主服务器传播来的N个字节的数据时,就
将自己的复制偏移量的值加上N.
通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态:如果主从服务
器处于一致状态,那么主从服务器两者的偏移量总是相同的; 相反,如果主从服务器两者的偏移量并不相
同,那么说明主从服务器并未处于一致状态.
思考一个场景,A和B分别为主从服务器,B突然断了下,然后又重连了,接下来,从服务器将向主服务器
发送PSYNC命令,A发现B的偏移量是10086,但此时自己的偏移量是10119,主服务器应该对从服务器执行
完整重同步还是部分重同步呢?如果执行部分重同步的话,主服务器又如何补偿从服务器A在断线期间丢
失的那部分数据呢?这两个问题就与复制积压缓冲区有关.
2.复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度先进先出(FIFO)队列,默认大小为1MB.
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压
缓冲区里面.因此,主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积压
缓冲区会为队列中的每个字节记录相应的复制偏移量.
这里要注意一点就是,资源是有限的,这个FIFO队列的大小是有限制的:
和普通先进先出队列随着元素的增加和减少而动态调整长度不同,固定长度先进先出队列的长度是固
定的,当入队元素的数量大于队列长度时,最先入队的元素会被弹出,而新元素会被放入队列.
当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务
器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作,由于FIFO队列的大小是有
限制,所以可能出现offset偏移量之后的数据已经不存在于复制积压缓冲区的情况,所以:
(1)如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里
面,那么主服务器将对从服务器执行部分重同步操作;
(2)如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整
重同步操作.
这里还需要注意的一点就是FIFO队列的大小值的设置,Redis为复制积压缓冲区设置的默认大小为
1MB,如果主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时间比较长,那么这
个大小也许并不合适。如果复制积压缓冲区的大小设置得不恰当,那么PSYNC命令的复制重同步模式就
不能正常发挥作用,因此,正确估算和设置复制积压缓冲区的大小非常重要。复制积压缓冲区的最小
大小可以根据公式second*write_size_per_second来估算:
(1)second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算);
(2)write_size_per_second是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和).
例如,如果主服务器平均每秒产生1MB的写数据,而从服务器断线之后平均要5秒才能重新连接上主服务
器, 那么复制积压缓冲区的大小就不能低于5MB。为了安全起见,可以将复制积压缓冲区的大小设为
2*second*write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理.至于
复制积压缓冲区大小的修改方法,可以参考配置文件中关于repl-backlog-size选项的说明.
3.服务器的运行ID
除了复制偏移量和复制积压缓冲区之外,实现部分重同步还需要用到服务器运行ID(run ID):
(1)每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID。
(2)运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成,例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3。
当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则
会将这个运行ID保存起来. 当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主
服务器发送之前保存的运行ID:
(1)如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制
的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作;
(2)如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前
复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作.
3.2.4 版本2.8中主从复制存在的问题
由3.2.3 可以看到执行部分重同步的要求还是比较严格的:
(1)RUN_ID必须相等;
(2)复制偏移量必须包含在复制缓冲区中.
然而在生产环境中,经常会出现以下两种情况:
(1)从服务器重启(复制信息丢失);
(2)主服务器故障导致主从切换(从多个从服务器重新选举出一台机器作为主服务器,主服务器运行ID发生
改变).
这时候显然是无法执行部分重同步的,而这两种情况又很常见,这怎么处理呀?于是机智的前辈们在版本
4.0中给出了优化方案.
3.3 Redis4.0
针对3.2.4节中出现的问题,Redis4.0针对主从复制又提出了两点优化,提出了psync2协议:
3.3.1 持久化主从复制信息
Redis服务器关闭时,将主从复制信息(复制的主服务器RUN_ID与复制偏移量)作为辅助字段存储在RDB文件
中;Redis服务器启动加载RDB文件时,恢复主从复制信息,重新同步主服务器时携带.持久化主从复制信
息代码如下:
int rdbSaveInfoAuxFields(rio *rdb, int rdbflags, rdbSaveInfo *rsi) {
...
if (rsi) {
if (rdbSaveAuxFieldStrInt(rdb,"repl-stream-db",rsi->repl_stream_db)
== -1) return -1;
if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
== -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset)
== -1) return -1;
}
...
}
3.3.2 存储上一个主服务器复制信息
1.redisServer 中添加两个变量
struct redisServer {
...
char replid2[CONFIG_RUN_ID_SIZE+1];
long long second_replid_offset;
...
}
初始化replid2为空字符串,second_replid_offset为-1;当主服务器发生故障,自己成为新的主服务器
时,便使用replid2和second_replid_offset存储之前主服务器的运行ID与复制偏移量.
2.备机变成主机的手要执行的函数
void replicationUnsetMaster(void) {
...
shiftReplicationId();
...
}
void shiftReplicationId(void) {
memcpy(server.replid2,server.replid,sizeof(server.replid));
server.second_replid_offset = server.master_repl_offset+1;
changeReplicationId();
serverLog(LL_WARNING,"Setting secondary replication ID to %s, valid up to offset: %lld. New replication ID is %s", server.replid2, server.second_replid_offset, server.replid);
}
3.判断是否能执行部分重同步的条件改变为
if (strcasecmp(master_replid, server.replid) &&
(strcasecmp(master_replid, server.replid2) ||
psync_offset > server.second_replid_offset))
{
...
goto need_full_resync;
}
假设m为主服务器(运行ID为M_ID), A、B和C为三个从服务器;某一时刻主服务器m发生故障,从服务
器A升级为主服务器(同时会记录replid2=M_ID),从服务器B和C重新向主服务器A发送“psync M_ID
psync_offset”请求;显然根据上面条件,只要psync_offset满足条件,就可以执行部分重同步.
3.3.3 这样便可以了吗?
在我查看【6.0.8】版本及【5.0.9】版本的时候,发现能否执行部分同步的条件似乎又发生了改变.
下面让我们在版本【6.0.8】中看看现在的主从复制是怎么做的吧.
4.源码解析
4.1 redisServer结构体中与主从复制相关的成员变量
struct redisServer {
...
/* Replication (master) */
/* 当前任期的master的运行Id*/
char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */
/* 上个任期的master的运行Id */
char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from master */
/* 当前任期的缓冲区最后一个字节的复制偏移量 */
long long master_repl_offset; /* My current replication offset */
/* 上一个任期的缓冲区最后一个字节的复制偏移量 */
long long second_replid_offset; /* Accept offsets up to this for replid2*/
int slaveseldb; /* Last SELECTed DB in replication output */
int repl_ping_slave_period; /* Master pings the slave every N seconds */
char *repl_backlog; /* Replication backlog for partial syncs */
long long repl_backlog_size; /* Backlog circular buffer size */
long long repl_backlog_histlen; /* Backlog actual data length */
long long repl_backlog_idx; /* Backlog circular buffer current offset,
that is the next byte will'll write to.*/
long long repl_backlog_off; /* Replication "master offset" of first
byte in the replication backlog buffer.*/
time_t repl_backlog_time_limit; /* Time without slaves after the backlog
gets released. */
time_t repl_no_slaves_since; /* We have no slaves since that time.
Only valid if server.slaves len is 0. */
int repl_min_slaves_to_write; /* Min number of slaves to write. */
int repl_min_slaves_max_lag; /* Max lag of <count> slaves to write. */
int repl_good_slaves_count; /* Number of slaves with lag <= max_lag. */
int repl_diskless_sync; /* Master send RDB to slaves sockets directly. */
int repl_diskless_load; /* Slave parse RDB directly from the socket.
* see REPL_DISKLESS_LOAD_* enum */
int repl_diskless_sync_delay; /* Delay to start a diskless repl BGSAVE. */
/* Replication (slave) */
char *masteruser; /* AUTH with this user and masterauth with master */
char *masterauth; /* AUTH with this password with master */
char *masterhost; /* Hostname of master */
int masterport; /* Port of master */
int repl_timeout; /* Timeout after N seconds of master idle */
client *master; /* Client that is master for this slave */
client *cached_master; /* Cached master to be reused for PSYNC. */
int repl_syncio_timeout; /* Timeout for synchronous I/O calls */
int repl_state; /* Replication status if the instance is a slave */
off_t repl_transfer_size; /* Size of RDB to read from master during sync. */
off_t repl_transfer_read; /* Amount of RDB read from master during sync. */
off_t repl_transfer_last_fsync_off; /* Offset when we fsync-ed last time. */
connection *repl_transfer_s; /* Slave -> Master SYNC connection */
int repl_transfer_fd; /* Slave -> Master SYNC temp file descriptor */
char *repl_transfer_tmpfile; /* Slave-> master SYNC temp file name */
time_t repl_transfer_lastio; /* Unix time of the latest read, for timeout */
int repl_serve_stale_data; /* Serve stale data when link is down? */
int repl_slave_ro; /* Slave is read only? */
int repl_slave_ignore_maxmemory; /* If true slaves do not evict. */
time_t repl_down_since; /* Unix time at which link with master went down */
int repl_disable_tcp_nodelay; /* Disable TCP_NODELAY after SYNC? */
int slave_priority; /* Reported in INFO and used by Sentinel. */
int slave_announce_port; /* Give the master this listening port. */
char *slave_announce_ip; /* Give the master this ip address. */
/* The following two fields is where we store master PSYNC replid/offset
* while the PSYNC is in progress. At the end we'll copy the fields into
* the server->master client structure. */
char master_replid[CONFIG_RUN_ID_SIZE+1]; /* Master PSYNC runid. */
long long master_initial_offset; /* Master PSYNC offset. */
int repl_slave_lazy_flush; /* Lazy FLUSHALL before loading DB? */
/* Replication script cache. */
dict *repl_scriptcache_dict; /* SHA1 all slaves are aware of. */
list *repl_scriptcache_fifo; /* First in, first out LRU eviction. */
unsigned int repl_scriptcache_size; /* Max number of elements. */
/* Synchronous replication. */
list *clients_waiting_acks; /* Clients waiting in WAIT command. */
int get_ack_from_slaves; /* If true we send REPLCONF GETACK. */
...
}
5.问题讨论
5.1 redis最多可以设置几个从节点?
5.2 一致性要求高的时候可以采用读写分离的方式吗?
不可以.