一.复制的概念
当执行SLAVEOF命令时,那么发送复制命令的服务器将复制接收该命令的服务器,之后这两个服务器将保存相同的数据。我们称被复制的服务器为主服务器,而对主服务器进行复制的服务器称为从服务器。
二.旧版复制功能
1.总体过程
总体复制过程由两部分组成。第一部分称为同步,用于将从服务器状态更新至主服务器的当前状态,即同步用于达到一致状态。第二部分称为命令传播,用于当主服务器状态改变时,让主从服务器回到一致状态,即用于维持一致状态。
2.同步过程(达到一致)
当执行SLAVEOF命令时,从服务器会像主服务器发送SYNC命令,当主服务器收到SYNC命令后便会开始执行BGSAVE以产生一个RDB文件,并存储从此刻起执行的写命令。之后将RDB文件与缓冲中的写命令发送给从服务器。其过程如下图所示:
3.命令传播(维持一致)
当主从服务器状态达到一致后,主服务器状态可能仍在不断变化,为了让主从服务器仍能不断保持一致,主服务器将不断将执行的写命令发送给从服务器,以让主从服务器能维持一致状态。
3.旧版复制功能的缺陷
旧版复制功能在断线后重复制时存在效率问题,因为每次断线重连后从服务器都将像主服务器发送SYNC命令,之后又将重新进行一次之前RDB持久化等操作,而没有根据断线期间数据变化的多少来决定是否需要从头进行同步,因为当变动数据较少时只需更新这一部分变动即可。
三.新版复制功能
1.新版复制过程概述
新版复制功能采用PSYNC命令代替SYNC命令,并有两种同步方式:完整重同步与部分重同步。
- 完整重同步:用于初次重同步(从服务器第一次尝试复制或与上一次复制的主服务器不同),其同步方式与旧版复制过程相同。
- 部分重同步:当从服务器在断线后重连主服务器时,若条件允许则主服务器可将从服务器断线期间执行的写命令发送给从服务器,而无需重新同步全部数据。
2.部分重同步
部分重同步通过三个组件来实现:服务器运行ID,主服务器的复制积压缓冲区和服务器的复制偏移量。每个组件都用来解决部分重同步中遇到的一个问题。
1)服务器运行ID
为了实现部分重同步,首先要解决的问题就是如何判断是初次同步还是断线重连后尝试同步。为了解决这个问题,从服务器每次进行复制时都会保存主服务器的运行时ID(每个服务器都有一个自己的运行ID)。当开始复制一个服务器时都会将保存的运行ID发送给当前的主服务器,主服务器会将收到的ID与自己的运行ID进行比较,若相同则说明是断线重连,否则是初次连接。
2)复制积压缓冲区
部分重同步为了避免重新同步全部数据,必须记录下断线期间的写命令,这样在重连后才能进行同步。为此redis中维护了一个名为复制积压缓冲区的固定长度FIFO队列,默认大小为1M。该缓冲区中保存着最近一部分传播的写命令,且缓冲队列中的每个字节数据都对应了一个序号(复制偏移量),这个结构很像TCP/IP中的滑动窗口。当缓冲队列满时,每次到达的新数据都会导致旧数据出队列。
3)复制偏移量
现在即可以判断是否为重连,也有了断联期间的数据,现在剩下的问题是,在从服务器重新连接后如何判断从哪个字节的数据开始重传呢?因为复制积压缓冲区保存的是最近发送的数据,当重连后紧接着断连前的那部分数据可能在缓冲区的哪个部分,也可能已经被顶替了(断连期间发送的数据过多,造成部分数据已出队)。
为此主从服务器都维护着一个复制偏移量。主服务器每发送N字节的数据就会将复制偏移量增加N(这个偏移量也就是每个数据的编号,即缓冲区中的编号),从服务器每接收N个数据就会将自己的复制偏移量增加N。通过对比主从服务器的复制偏移量就可以知道二者是否处于同步状态。
每当断线重连时从服务器都会将自己的复制偏移量(offset)发送给主服务器,若编号为offset + 1的数据还存在于复制积压缓存中,那么便可以进行部分重同步,否则进行完整重同步。
【*注*】:这其实与TCP协议有着异曲同工之妙,都为每个字节进行编号,这样redis便可知道断连期间的数据是否丢失,以及续传点在哪。
四.从源码看过程
源码中整个复制过程可以用下图进行总结:
【注】:可以看到redis在同步的不同阶段,通过为套接字设置不同的回调函数来处理不同阶段的事件。
1.从服务器端
服务器的复制是从客户端发送SLAVEOF命令开始的,因此从从服务器收到SLAVEOF命令开始。
step1.SLAVEOF命令的接收与处理
1)查找命令表
当客户端发送SLAVEOF命令后,从服务器的对应套接字会变为可读状态,从而调用回调函数readQueryFromClient,再其中会将收到的数据通过查找命令表转化为相应的命令,redis命令集位于redis.c文件中,redis命令集实际上便是一个redisCommand类型的数组,其定义如下:
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
{"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
...
{"slaveof",slaveofCommand,3,"ast",0,NULL,0,0,0,0,0},
...
}
2)启动主从复制并回复OK
接着在命令表中查找到slaveofCommand函数并开始执行,该函数会从redisClient->argv中获取并检查命令的参数,之后调用replicationSetMaster函数进行一些清理和设置操作(比如将新的主服务器地址保存到服务器的相应属性中),并将复制状态标记为将连接REDIS_REPL_CONNECT,redis会在一个时间事件中检查该状态并进行相应处理。具体如下所示:
void slaveofCommand(redisClient *c) {
// 在集群模式下不允许SLAVEOF,因为集群模式下会使用主节点的当前地址自动配置复制
if (server.cluster_enabled) {
addReplyError(c,"SLAVEOF not allowed in cluster mode.");
return;
}
/* slaveof no one 表示去除主从同步关系
* 否则为slaveof ip port表示添加主从同步关系
*/
if (!strcasecmp(c->argv[1]->ptr,"no") &&
!strcasecmp(c->argv[2]->ptr,"one")) {
if (server.masterhost) {
replicationUnsetMaster();
sds client = catClientInfoString(sdsempty(),c);
redisLog(REDIS_NOTICE,
"MASTER MODE enabled (user request from '%s')",client);
sdsfree(client);
}
} else {
long port;
// 获取端口参数
if ((getLongFromObjectOrReply(c, c->argv[2], &port, NULL) != REDIS_OK))
return;
/* 如果当前的主服务器就是此次命令输入的主服务器,则不进行操作,仅返回提示已连接
*/
if (server.masterhost && !strcasecmp(server.masterhost,c->argv[1]->ptr)
&& server.masterport == port) {
redisLog(REDIS_NOTICE,"SLAVE OF would result into synchronization with the master we are already connected with. No operation performed.");
addReplySds(c,sdsnew("+OK Already connected to specified master\r\n"));
return;
}
// 开始执行复制操作
replicationSetMaster(c->argv[1]->ptr, port);
sds client = catClientInfoString(sdsempty(),c);
redisLog(REDIS_NOTICE,"SLAVE OF %s:%d enabled (user request from '%s')",
server.masterhost, server.masterport, client);
sdsfree(client);
}
// 回复OK
addReply(c,shared.ok);
}
// 在主从复制前进行一系列清理与设置操作
void replicationSetMaster(char *ip, int port) {
// 释放旧的主服务器地址,并将新的主服务器IP及端口保存到服务器的masterhost和masterport属性中
sdsfree(server.masterhost);
server.masterhost = sdsnew(ip);
server.masterport = port;
// 释放旧的用于表示主服务器的redisCliemt(即服务器中的master属性)
if (server.master) freeClient(server.master);
// 将当前阻塞在本服务器上的客户,全部断开,因为随后要进行新的主服务器的复制,继续阻塞已无意义
// 该函数先将客户端标记为REDIS_CLOSE_AFTER_REPLY,这样在写完发送缓存的数据后便会断开该客户端
disconnectAllBlockedClients(); /* Clients blocked in master, now slave. */
// 断开本服务器的所有从服务器,因为本服务器马上也会成为从服务器
disconnectSlaves(); /* Force our slaves to resync with us as well. */
// 清空可能有的 master 缓存的REDIS_MASTER标记
replicationDiscardCachedMaster(); /* Don't try a PSYNC. */
freeReplicationBacklog(); /* Don't allow our chained slaves to PSYNC. */
cancelReplicationHandshake();
// 将服务器标记为REDIS_REPL_CONNECT,表示将连接到主服务器
server.repl_state = REDIS_REPL_CONNECT;
server.master_repl_offset = 0; // 初始化复制偏移量为0
server.repl_down_since = 0;
}
step2:建立连接并定时处理主从复制
redis服务器在初始化时便会设置一个定期时间事件serverCron,该函数会处理一些redis定期要处理的事件,比如进行渐进式rehash等等,serverCron还会定期调用replicationCron函数检查服务器复制状态,并进行相应处理,比如:是否尝试连接超时,是否在传输RDB文件的过程中超时,在以上超时发生时进行一些处理并尝试重连;断开超时从服务器;若是自己从服务器还将发送ACK+复制偏移,等等。举个具体点的例子,当执行第一步的SLAVEOF命令后,服务器的同步状态便被标记为REDIS_REPL_CONNECT,之后在定期调用的replicationCron函数中就会看到该状态并为其连接主服务器。为每一个从服务器socket设置的回调函数为syncWithMaster。 源码如下:
#define run_with_period(_ms_) if ((_ms_ <= 1000/server.hz) || !(server.cronloops%((_ms_)/(1000/server.hz))))
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
{
...
// 定期调用replicationCron处理服务器复制
run_with_period(1000) replicationCron();
...
}
复制过程在redis中是一种状态机模式,从服务器每与主服务器每进行一步操作便会进入一个新的状态,复制过程的状态如下
/* Slave replication state. Used in server.repl_state for slaves to remember
* what to do next. */
#define REDIS_REPL_NONE 0 /* No active replication */
#define REDIS_REPL_CONNECT 1 /* Must connect to master */
#define REDIS_REPL_CONNECTING 2 /* Connecting to master */
/* --- Handshake states, must be ordered --- */
#define REDIS_REPL_RECEIVE_PONG 3 /* Wait for PING reply */
#define REDIS_REPL_SEND_AUTH 4 /* Send AUTH to master */
#define REDIS_REPL_RECEIVE_AUTH 5 /* Wait for AUTH reply */
#define REDIS_REPL_SEND_PORT 6 /* Send REPLCONF listening-port */
#define REDIS_REPL_RECEIVE_PORT 7 /* Wait for REPLCONF reply */
#define REDIS_REPL_SEND_CAPA 8 /* Send REPLCONF capa */
#define REDIS_REPL_RECEIVE_CAPA 9 /* Wait for REPLCONF reply */
#define REDIS_REPL_SEND_PSYNC 10 /* Send PSYNC */
#define REDIS_REPL_RECEIVE_PSYNC 11 /* Wait for PSYNC reply */
/* --- End of handshake states --- */
#define REDIS_REPL_TRANSFER 12 /* Receiving .rdb from master */
#define REDIS_REPL_CONNECTED 13 /* Connected to master */
/* --- End of handshake states --- */
#define REDIS_REPL_TRANSFER 12 /* Receiving .rdb from master */
#define REDIS_REPL_CONNECTED 13 /* Connected to master */
/* State of slaves from the POV of the master. Used in client->replstate.
* In SEND_BULK and ONLINE state the slave receives new updates
* in its output queue. In the WAIT_BGSAVE state instead the server is waiting
* to start the next background saving in order to send updates to it. */
#define REDIS_REPL_WAIT_BGSAVE_START 14 /* We need to produce a new RDB file. */
#define REDIS_REPL_WAIT_BGSAVE_END 15 /* Waiting RDB file creation to finish. */
#define REDIS_REPL_SEND_BULK 16 /* Sending RDB file to slave. */
#define REDIS_REPL_ONLINE 17 /* RDB file transmitted, sending just updates. */
replicationCron函数如下:
// 服务器复制的核心处理函数,每秒调用一次
void replicationCron(void) {
static long long replication_cron_loops = 0; // 记录该函数执行的次数
/* Non blocking connection timeout? */
// 服务器正在尝试连接到主服务器 或者服务器处于等待PING命令回复至等待PSYN回复这两个步骤之间(包含这两个步骤)
// 此时检测到超时则取消连接
if (server.masterhost &&
(server.repl_state == REDIS_REPL_CONNECTING ||
slaveIsInHandshakeState()) &&
(time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
redisLog(REDIS_WARNING,"Timeout connecting to the MASTER...");
undoConnectWithMaster(); // 取消连接
}
// 主从复制已进行到传输RDB的步骤,但此时发生了超时,此时停止传输并删除临时文件,
// 同时将复制状态重新标记为REDIS_REPL_CONNECT(即等待连接)
if (server.masterhost && server.repl_state == REDIS_REPL_TRANSFER &&
(time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
redisLog(REDIS_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value.");
// 停止传输并删除临时文件,同时将复制状态重新标记为REDIS_REPL_CONNECT,这样redis将会重连主服务器
replicationAbortSyncTransfer();
}
/* Timed out master when we are an already connected slave? */
// 服务器状态为已连接,但根据最后一次互动时间来看是处于超时的,
// 这说明服务器已连接上主服务器,但由于超时,应该断开主服务器
if (server.masterhost && server.repl_state == REDIS_REPL_CONNECTED &&
(time(NULL)-server.master->lastinteraction) > server.repl_timeout)
{
redisLog(REDIS_WARNING,"MASTER timeout: no data nor PING received...");
freeClient(server.master); // 释放当前主服务器对应的redisClient
}
/* Check if we should connect to a MASTER */
// 连接到主服务器
if (server.repl_state == REDIS_REPL_CONNECT) {
redisLog(REDIS_NOTICE,"Connecting to MASTER %s:%d",
server.masterhost, server.masterport);
// 创建非阻塞套接字连接主服务器,随后注册读写事件
if (connectWithMaster() == REDIS_OK) {
redisLog(REDIS_NOTICE,"MASTER <-> SLAVE sync started");
}
}
/* 在完成RDB文件同步与加载后才会发送ACK,因为此前在进行清理工作时已经释放了server.master,
* 待RDB文件处理完毕后,才会执行server.master = createClient(server.repl_transfer_s);再次创建。
* 定期发送ACK + 复制偏移量 给主服务器,这样做的原因有三点:
* 1.检测网络状态(心跳)
* 2.辅助实现min-slaves配置选项(检测延迟)
* 3.因为还发送了复制偏移量,因此还可检测命令数据丢失
*/
if (server.masterhost && server.master &&
!(server.master->flags & REDIS_PRE_PSYNC))
replicationSendAck();
listIter li;
listNode *ln;
robj *ping_argv[1];
/* First, send PING according to ping_slave_period. */
// 若存在从服务器(即为主服务器),会定期向其发送PING命令
if ((replication_cron_loops % server.repl_ping_slave_period) == 0) {
ping_argv[0] = createStringObject("PING",4);
replicationFeedSlaves(server.slaves, server.slaveseldb,
ping_argv, 1);
decrRefCount(ping_argv[0]);
}
/* 在预同步阶段“\n”至所有的从服务器(即发送RDB文件之前),更确切地说,
* 此时从服务器正在等待主服务器创建RDB文件,为了避免从服务器判断超时,
* 此阶段主服务器利用发送“\n”刷新从服务器地last-io 计时,从服务器将忽略“\n”,
* 并不进行其它处理。
*/
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = ln->value;
// 向正在请求创建RDB和等待创建RDB文件完成地从服务器写“\n”
// REDIS_REPL_WAIT_BGSAVE_START:需要创建一个新地RDB文件
// REDIS_REPL_WAIT_BGSAVE_END:正在等待一个RDB文件创建完毕
if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START ||
(slave->replstate == REDIS_REPL_WAIT_BGSAVE_END &&
server.rdb_child_type != REDIS_RDB_CHILD_TYPE_SOCKET))
{
if (write(slave->fd, "\n", 1) == -1) {
/* Don't worry, it's just a ping. */
}
}
}
/* 断开超时地从服务器
* 当进行到命令传播阶段(即完成了RDB同步主从服务器已达到一致状态后,只进行写命令同步更新即可)时,
* 主服务会定期便会检查从服务器定期发送来地ACK命令,若一段时间内没有接收到ACK命令,则说明网络连接有问题
*/
if (listLength(server.slaves)) {
listIter li;
listNode *ln;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = ln->value;
// REDIS_REPL_ONLINE:表示已经完成了RDB文件传送,正在发送更新
if (slave->replstate != REDIS_REPL_ONLINE) continue;
// 从服务器为低版本redis不检查
if (slave->flags & REDIS_PRE_PSYNC) continue;
// 根据从服务器最后一次发送ACK地时间判断从服务器网络状态是否超时,若超时则断开
if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)
{
redisLog(REDIS_WARNING, "Disconnecting timedout slave: %s",
replicationGetSlaveName(slave));
freeClient(slave);
}
}
}
/* 若在一段时间内一直未有从服务器,那么主服务器将释放复制积压缓存 */
if (listLength(server.slaves) == 0 && server.repl_backlog_time_limit &&
server.repl_backlog)
{
time_t idle = server.unixtime - server.repl_no_slaves_since;
if (idle > server.repl_backlog_time_limit) {
freeReplicationBacklog();
redisLog(REDIS_NOTICE,
"Replication backlog freed after %d seconds "
"without connected slaves.",
(int) server.repl_backlog_time_limit);
}
}
/* If AOF is disabled and we no longer have attached slaves, we can
* free our Replication Script Cache as there is no need to propagate
* EVALSHA at all. */
if (listLength(server.slaves) == 0 &&
server.aof_state == REDIS_AOF_OFF &&
listLength(server.repl_scriptcache_fifo) != 0)
{
replicationScriptCacheFlush();
}
/* 如果使用了diskless同步(无盘同步),且有服务器处于请求生成RDB文件状态(REDIS_REPL_WAIT_BGSAVE_START),
* 则检查所有从服务器中最早请求的时间到现在是否超过了规定的秒数,若是则进行无盘同步
*/
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
time_t idle, max_idle = 0;
int slaves_waiting = 0;
int mincapa = -1;
listNode *ln;
listIter li;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = ln->value;
if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) {
idle = server.unixtime - slave->lastinteraction;
if (idle > max_idle) max_idle = idle;
slaves_waiting++;
mincapa = (mincapa == -1) ? slave->slave_capa :
(mincapa & slave->slave_capa);
}
}
if (slaves_waiting && max_idle > server.repl_diskless_sync_delay) {
/* Start a BGSAVE. Usually with socket target, or with disk target
* if there was a recent socket -> disk config change. */
// 尝试进行diskless同步(会检查是否开启了diskless同步)
startBgsaveForReplication(mincapa);
}
}
/* Refresh the number of slaves with lag <= min-slaves-max-lag. */
// 统计网络状态处于良好的从服务器数量
refreshGoodSlavesCount();
replication_cron_loops++; /* Incremented with frequency 1 HZ. */
}
连接函数如下:
// 该函数先进行了连接后注册了相应事件却并不会出现问题,
// 这是因为连接成功后主服务并不会立刻给从服务器发送消息,而是要先等待从服务器发送PING命令,才会回复一个PONG
int connectWithMaster(void) {
int fd;
// 建立一个连接到主服务器的非阻塞socket描述符
fd = anetTcpNonBlockBestEffortBindConnect(NULL,
server.masterhost,server.masterport,REDIS_BIND_ADDR);
if (fd == -1) {
redisLog(REDIS_WARNING,"Unable to connect to MASTER: %s",
strerror(errno));
return REDIS_ERR;
}
// 为该socket文件描述符关注读写事件
if (aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) == AE_ERR)
{
close(fd);
redisLog(REDIS_WARNING,"Can't create readable event for SYNC");
return REDIS_ERR;
}
server.repl_transfer_lastio = server.unixtime;
server.repl_transfer_s = fd;
server.repl_state = REDIS_REPL_CONNECTING;
return REDIS_OK;
}
step3:同步前的预处理(PING检测网络,身份验证,端口信息,发送PSYNC判断能否进行部分重同步)
当建立连接后,redis会为从服务器中用于通信的socket套接字设置读写事件回调函数syncWithMaster,该函数会先发送一个PING命令至主服务器,已检测网络状态及主服务器可否正常处理命令。函数如下:
【注】:主服务器之所以要获取从服务器的监听端口,是因为在哨兵系统中,哨兵会发送INFO命令至主服务器以获取从服务器的信息,其中便包括从服务器的监听端口,这样哨兵才可以连接到这些从服务器
void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
char tmpfile[256], *err = NULL;
int dfd, maxtries = 5;
int sockerr = 0, psync_result;
socklen_t errlen = sizeof(sockerr);
REDIS_NOTUSED(el);
REDIS_NOTUSED(privdata);
REDIS_NOTUSED(mask);
/* If this event fired after the user turned the instance into a master
* with SLAVEOF NO ONE we must just return ASAP. */
/* 若执行了SLAVE NO ONE,状态将变为REDIS_REPL_NONE,此时应关闭套接字*/
if (server.repl_state == REDIS_REPL_NONE) {
close(fd);
return;
}
/* redis中使用的是非阻塞套接字调用connect,当连接成功时会变为可写,而出现错误时会变为即可读又可写,
* 因此使用getsockopt函数判断是否出现错误若无错误则说明连接成功
*/
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &sockerr, &errlen) == -1)
sockerr = errno;
if (sockerr) {
redisLog(REDIS_WARNING,"Error condition on socket for SYNC: %s",
strerror(sockerr));
goto error;
}
/* 当前处于REDIS_REPL_CONNECTING状态说明刚进行连接,还未开始同步,此时将发送一个PING命令给主服务器,
* 并等待主服务器回复PONG,已确认网络状态及主服务器可正常工作,以便之后正常进行RDB文件传输
*/
if (server.repl_state == REDIS_REPL_CONNECTING) {
redisLog(REDIS_NOTICE,"Non blocking connect for SYNC fired the event.");
/* 解注册可写事件,防止不断触发可写事件。
* 之后每收到一条回复后,立刻发送下一条信息,因此无需再注册可写事件
*/
aeDeleteFileEvent(server.el,fd,AE_WRITABLE);
// 将同步状态标记为等待PONG回复(REDIS_REPL_RECEIVE_PONG)
server.repl_state = REDIS_REPL_RECEIVE_PONG;
/* Send the PING, don't check for errors at all, we have the timeout
* that will take care about this. */
err = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PING",NULL);
if (err) goto write_error;
return;
}
/* 接收PONG回复 */
if (server.repl_state == REDIS_REPL_RECEIVE_PONG) {
/*调用sendSynchronousCommand读数据会循环等待读完指定大小的数据,并检测是否超时,是否出现其它发送错误。
* err为错误字符串,格式为:"-Writing to master: %s", strerror(errno)。
*/
err = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
// 接收到的数据只有两种可能:
// 第一种是 +PONG ,第二种是因为未验证而出现的 -NOAUTH 错误
if (err[0] != '+' &&
strncmp(err,"-NOAUTH",7) != 0 &&
strncmp(err,"-ERR operation not permitted",28) != 0)
{
redisLog(REDIS_WARNING,"Error reply to PING from master: '%s'",err);
sdsfree(err);
goto error;
} else {
redisLog(REDIS_NOTICE,
"Master replied to PING, replication can continue...");
}
sdsfree(err);
// 同步状态更改为发送身份验证
server.repl_state = REDIS_REPL_SEND_AUTH;
}
// 进行身份验证
if (server.repl_state == REDIS_REPL_SEND_AUTH) {
if (server.masterauth) { // 若从服务器设置了身份认证选项(即设置了密码masterauth)
// 发送身份验证信息 AUTH masterauth 至主服务器
err = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"AUTH",server.masterauth,NULL);
if (err) goto write_error;
// 更改状态为等待接收身份验证回复
server.repl_state = REDIS_REPL_RECEIVE_AUTH;
return;
} else {
// 若从服务器未设置身份验证,则直接开始发送监听端口信息
server.repl_state = REDIS_REPL_SEND_PORT;
}
}
// 等待接收身份验证回复
if (server.repl_state == REDIS_REPL_RECEIVE_AUTH) {
err = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
// 身份验证失败(可以注意到,错误回复都是以'-'开头的,这样可以快速判断)
if (err[0] == '-') {
redisLog(REDIS_WARNING,"Unable to AUTH to MASTER: %s",err);
sdsfree(err);
goto error;
}
sdsfree(err);
// 将状态更为发送端口信息
server.repl_state = REDIS_REPL_SEND_PORT;
}
// 发送端口信息,使得主服务器的 INFO 命令可以显示从服务器正在监听的端口。
// 若从服务啊未设置身份验证,会直接发送该信息,但若主服务器设置了身份验证,亦会返回“-xxx”
if (server.repl_state == REDIS_REPL_SEND_PORT) {
sds port = sdsfromlonglong(server.port);
err = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"REPLCONF",
"listening-port",port, NULL);
sdsfree(port);
if (err) goto write_error;
sdsfree(err);
server.repl_state = REDIS_REPL_RECEIVE_PORT;
return;
}
// 等待主服务器的端口信息回复
if (server.repl_state == REDIS_REPL_RECEIVE_PORT) {
err = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
/* Ignore the error if any, not all the Redis versions support
* REPLCONF listening-port. */
if (err[0] == '-') {
redisLog(REDIS_NOTICE,"(Non critical) Master does not understand "
"REPLCONF listening-port: %s", err);
}
sdsfree(err);
server.repl_state = REDIS_REPL_SEND_CAPA;
}
/* Inform the master of our capabilities. While we currently send
* just one capability, it is possible to chain new capabilities here
* in the form of REPLCONF capa X capa Y capa Z ...
* The master will ignore capabilities it does not understand. */
if (server.repl_state == REDIS_REPL_SEND_CAPA) {
err = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"REPLCONF",
"capa","eof",NULL);
if (err) goto write_error;
sdsfree(err);
server.repl_state = REDIS_REPL_RECEIVE_CAPA;
return;
}
/* Receive CAPA reply. */
if (server.repl_state == REDIS_REPL_RECEIVE_CAPA) {
err = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
/* Ignore the error if any, not all the Redis versions support
* REPLCONF capa. */
if (err[0] == '-') {
redisLog(REDIS_NOTICE,"(Non critical) Master does not understand "
"REPLCONF capa: %s", err);
}
sdsfree(err);
server.repl_state = REDIS_REPL_SEND_PSYNC;
}
/* 发送PSYNC runid offset 命令,尝试进程部分重同步,若不可则进行完全重同步。
* 就算是进行完全重同步也会记录下主服务器的运行ID,这样现次同步时便可进行部分重同步。
*/
if (server.repl_state == REDIS_REPL_SEND_PSYNC) {
// 调用slaveTryPartialResynchronization函数发送PSYNC + runid + offset命令尝试部分重同步
if (slaveTryPartialResynchronization(fd,0) == PSYNC_WRITE_ERROR) {
err = sdsnew("Write error sending the PSYNC command.");
goto write_error;
}
// 同步状态变为等待PSYNC回复
server.repl_state = REDIS_REPL_RECEIVE_PSYNC;
return;
}
/* 若运行到此处但同步状态不为等待PSYNC回复REDIS_REPL_RECEIVE_PSYNC,说明出错 */
if (server.repl_state != REDIS_REPL_RECEIVE_PSYNC) {
redisLog(REDIS_WARNING,"syncWithMaster(): state machine error, "
"state should be RECEIVE_PSYNC but is %d",
server.repl_state);
goto error;
}
/*调用slaveTryPartialResynchronization读取PSYNC回复*/
psync_result = slaveTryPartialResynchronization(fd,1);
/* 若返回的是PSYNC_WAIT_REPLY,说明还未收到PSYNC回复,收到的是‘\n’,因此先退出该函数,
* 随后主服务器发回回复后仍会触发本回调函数,由于此时同步状态为REDIS_REPL_RECEIVE_PSYNC,
* 因此会直接运行到此处。
*/
if (psync_result == PSYNC_WAIT_REPLY) return; /* Try again later... */
/* Note: if PSYNC does not return WAIT_REPLY, it will take care of
* uninstalling the read handler from the file descriptor. */
if (psync_result == PSYNC_CONTINUE) {
redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization.");
return;
}
/* PSYNC failed or is not supported: we want our slaves to resync with us
* as well, if we have any (chained replication case). The mater may
* transfer us an entirely different data set and we have no way to
* incrementally feed our slaves after that. */
disconnectSlaves(); /* Force our slaves to resync with us as well. */
freeReplicationBacklog(); /* Don't allow our chained slaves to PSYNC. */
/* Fall back to SYNC if needed. Otherwise psync_result == PSYNC_FULLRESYNC
* and the server.repl_master_runid and repl_master_initial_offset are
* already populated. */
if (psync_result == PSYNC_NOT_SUPPORTED) {
redisLog(REDIS_NOTICE,"Retrying with SYNC...");
if (syncWrite(fd,"SYNC\r\n",6,server.repl_syncio_timeout*1000) == -1) {
redisLog(REDIS_WARNING,"I/O error writing to MASTER: %s",
strerror(errno));
goto error;
}
}
/*最多尝试maxtries去创建一个用于存储接受到的RDB数据的文件*/
while(maxtries--) {
snprintf(tmpfile,256,
"temp-%d.%ld.rdb",(int)server.unixtime,(long int)getpid());
dfd = open(tmpfile,O_CREAT|O_WRONLY|O_EXCL,0644);
if (dfd != -1) break;
sleep(1);
}
if (dfd == -1) {
redisLog(REDIS_WARNING,"Opening the temp file needed for MASTER <-> SLAVE synchronization: %s",strerror(errno));
goto error;
}
// 注册新的可读事件回调readSyncBulkPayload,该函数用于处理RDB数据的接收与存储
if (aeCreateFileEvent(server.el,fd, AE_READABLE,readSyncBulkPayload,NULL)
== AE_ERR)
{
redisLog(REDIS_WARNING,
"Can't create readable event for SYNC: %s (fd=%d)",
strerror(errno),fd);
goto error;
}
server.repl_state = REDIS_REPL_TRANSFER;
server.repl_transfer_size = -1;
server.repl_transfer_read = 0;
server.repl_transfer_last_fsync_off = 0;
server.repl_transfer_fd = dfd;
server.repl_transfer_lastio = server.unixtime;
server.repl_transfer_tmpfile = zstrdup(tmpfile);
return;
error:
aeDeleteFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE);
close(fd);
server.repl_transfer_s = -1;
server.repl_state = REDIS_REPL_CONNECT;
return;
write_error: /* Handle sendSynchronousCommand(SYNC_CMD_WRITE) errors. */
redisLog(REDIS_WARNING,"Sending command to master in replication handshake: %s", err);
sdsfree(err);
goto error;
}
发送PSYNC命令尝试进行部分重同步,并接收主服务器回复的函数为slaveTryPartialResynchronization,具体如下:
int slaveTryPartialResynchronization(int fd, int read_reply) {
char *psync_runid;
char psync_offset[32];
sds reply;
/* 写部分,即发送PSYNC + runID + 复制偏移量 命令*/
if (!read_reply) {
/* 将 repl_master_initial_offset初始为-1,以标识当前主机ID和偏移量是无效的,随后
* 在进行PSYNC/SYNC命令发送并等待回复后才会设置为正确的值
*/
server.repl_master_initial_offset = -1;
/* 若之前连接过某个主服务器,那么cached_master将会保存其信息,此处用于获取前一个主服务器的运行ID和复制偏移量
*/
if (server.cached_master) {
psync_runid = server.cached_master->replrunid;
// 将与前一个主服务器的复制偏移量格式化为字符串
snprintf(psync_offset,sizeof(psync_offset),"%lld", server.cached_master->reploff+1);
redisLog(REDIS_NOTICE,"Trying a partial resynchronization (request %s:%s).", psync_runid, psync_offset);
} else {
redisLog(REDIS_NOTICE,"Partial resynchronization not possible (no cached master)");
psync_runid = "?";
memcpy(psync_offset,"-1",3);
}
// 发送 PSYNC + runid + 复制偏移量 命令,尝试进行部分重同步
reply = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PSYNC",psync_runid,psync_offset,NULL);
if (reply != NULL) {
redisLog(REDIS_WARNING,"Unable to send PSYNC to master: %s",reply);
sdsfree(reply);
aeDeleteFileEvent(server.el,fd,AE_READABLE);
return PSYNC_WRITE_ERROR;
}
// 返回新的状态为等待回复PSYNC
return PSYNC_WAIT_REPLY;
}
/* 读部分,等待并读取PSYNC回复 */
reply = sendSynchronousCommand(SYNC_CMD_READ,fd,NULL);
if (sdslen(reply) == 0) {
/* 当主服务器收到PSYNC命令后,会不断发送“\n”更新repl_transfer_lastio以保证连接存活,
* 因为从服务器在serverCron函数中会根据repl_transfer_lastio在同步处于REDIS_REPL_RECEIVE_PONG 至
* REDIS_REPL_RECEIVE_PSYNC状态期间是否发生超时。
*/
sdsfree(reply);
// 若收到是‘/n’则返回
return PSYNC_WAIT_REPLY;
}
// 运行到此处说明成功收到来自主服务器的PSYNC回复
aeDeleteFileEvent(server.el,fd,AE_READABLE);
// 若主服务器回复为进行完全重同步
if (!strncmp(reply,"+FULLRESYNC",11)) {
char *runid = NULL, *offset = NULL;
// 从命令中读取主服务器运行时ID和复制偏移量并保存到
runid = strchr(reply,' ');
if (runid) {
runid++;
offset = strchr(runid,' ');
if (offset) offset++;
}
if (!runid || !offset || (offset-runid-1) != REDIS_RUN_ID_SIZE) {
redisLog(REDIS_WARNING,
"Master replied with wrong +FULLRESYNC syntax.");
/* This is an unexpected condition, actually the +FULLRESYNC
* reply means that the master supports PSYNC, but the reply
* format seems wrong. To stay safe we blank the master
* runid to make sure next PSYNCs will fail. */
memset(server.repl_master_runid,0,REDIS_RUN_ID_SIZE+1);
} else {
memcpy(server.repl_master_runid, runid, offset-runid-1);
server.repl_master_runid[REDIS_RUN_ID_SIZE] = '\0';
server.repl_master_initial_offset = strtoll(offset,NULL,10);
redisLog(REDIS_NOTICE,"Full resync from master: %s:%lld",
server.repl_master_runid,
server.repl_master_initial_offset);
}
/* We are going to full resync, discard the cached master structure. */
replicationDiscardCachedMaster();
sdsfree(reply);
// 返回回复为完全重同步
return PSYNC_FULLRESYNC;
}
// 若收到的回复为部分重同步
if (!strncmp(reply,"+CONTINUE",9)) {
/* Partial resync was accepted, set the replication state accordingly */
redisLog(REDIS_NOTICE,
"Successful partial resynchronization with master.");
sdsfree(reply);
// 将cached_master转移到当master中(因为cached_master为前一次复制的主服务器),到套接字描述符使用新的。
replicationResurrectCachedMaster(fd);
// 返回回复为部分重同步
return PSYNC_CONTINUE;
}
/* If we reach this point we received either an error since the master does
* not understand PSYNC, or an unexpected reply from the master.
* Return PSYNC_NOT_SUPPORTED to the caller in both cases. */
if (strncmp(reply,"-ERR",4)) {
/* If it's not an error, log the unexpected event. */
redisLog(REDIS_WARNING,
"Unexpected reply to PSYNC from master: %s", reply);
} else {
redisLog(REDIS_NOTICE,
"Master does not support PSYNC or is in "
"error state (reply: %s)", reply);
}
sdsfree(reply);
replicationDiscardCachedMaster();
return PSYNC_NOT_SUPPORTED;
}
step4:接收主服务器发送RDB文件并重建数据库
可以看到从服务器最初的读写回调函数syncWithMaster在进行一系列预处理,并处理完PSYNC命令,创建用于存储接收到的RDB数据的临时文件后,会设置新的读回调函数readSyncBulkPayload,该函数用于处理主服务器发送的RDB同步数据,并根据有盘与无盘传输采用了两种判断文件结束的方式,在文件接收结束后还会载入RDB文件以创建新的数据库。该函数具体如下:
void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) {
char buf[4096];
ssize_t nread, readlen;
off_t left;
REDIS_NOTUSED(el);
REDIS_NOTUSED(privdata);
REDIS_NOTUSED(mask);
static char eofmark[REDIS_RUN_ID_SIZE]; // 用于存储文件尾结束符,在无盘传输时使用
static char lastbytes[REDIS_RUN_ID_SIZE]; // 存储最后收到的40个字节的数据,以便判断是否为文件结束符
static int usemark = 0; // 是否使用文件结束符,若使用则为1(无盘传输),否则为0(有盘)
/* 如果 repl_transfer_size == -1 说明尚未读取文件大小
*/
if (server.repl_transfer_size == -1) {
if (syncReadLine(fd,buf,1024,server.repl_syncio_timeout*1000) == -1) {
redisLog(REDIS_WARNING,
"I/O error reading bulk count from MASTER: %s",
strerror(errno));
goto error;
}
// 一下进行一些错误处理,如回复错误,读到的是‘\n’等
if (buf[0] == '-') {
redisLog(REDIS_WARNING,
"MASTER aborted replication with an error: %s",
buf+1);
goto error;
} else if (buf[0] == '\0') {
/* At this stage just a newline works as a PING in order to take
* the connection live. So we refresh our last interaction
* timestamp. */
server.repl_transfer_lastio = server.unixtime;
return;
} else if (buf[0] != '$') {
redisLog(REDIS_WARNING,"Bad protocol from MASTER, the first byte is not '$' (we received '%s'), are you sure the host and port are right?", buf);
goto error;
}
/* 关于文件的有效大小有两种格式进行传输,这两种格式分别对应于有盘和无盘传输
*
* - 对于有盘传输,因为RDB文件会先保存在主服务器的磁盘上,因此可以知道其大小,
* 使用格式$ <count>进行传输。
*
* - 对于无盘传输,其传输格式如下:
* $ EOF:<40字节结束符>,
* 在文件末尾传输该事先约定的结束符,因为该结束符足够长,且足够随机,因此可以忽略
* 与实际文件内容相冲突的概率。
*/
if (strncmp(buf+1,"EOF:",4) == 0 && strlen(buf+5) >= REDIS_RUN_ID_SIZE) {
// 主服务器以无盘方式传输,接收约定的文件结束符,并及那个其存储至eofmark
usemark = 1;
memcpy(eofmark,buf+5,REDIS_RUN_ID_SIZE);
memset(lastbytes,0,REDIS_RUN_ID_SIZE);
/* Set any repl_transfer_size to avoid entering this code path
* at the next call. */
// 将要读取的文件大小标记为0(无盘模式),以防下次再进入该代码段
server.repl_transfer_size = 0;
redisLog(REDIS_NOTICE,
"MASTER <-> SLAVE sync: receiving streamed RDB from master");
} else {
// 主服务器以有盘方式传输,接收文件大小,将其保存到repl_transfer_size中
usemark = 0;
server.repl_transfer_size = strtol(buf+1,NULL,10); // str to long
redisLog(REDIS_NOTICE,
"MASTER <-> SLAVE sync: receiving %lld bytes from master",
(long long) server.repl_transfer_size);
}
return;
}
if (usemark) {
// 无盘传输不知道还剩多少数据,因此每次调用read都最多读缓存大小的数据
readlen = sizeof(buf);
} else {
left = server.repl_transfer_size - server.repl_transfer_read;
readlen = (left < (signed)sizeof(buf)) ? left : (signed)sizeof(buf);
}
// 从套接字读取数据到缓存buf中
nread = read(fd,buf,readlen);
if (nread <= 0) {
redisLog(REDIS_WARNING,"I/O error trying to sync with MASTER: %s",
(nread == -1) ? strerror(errno) : "connection lost");
replicationAbortSyncTransfer();
return;
}
server.stat_net_input_bytes += nread; // 对从网路传读入总数据量进行计数
/* 当使用无盘传输时 eof_reached用于标记结束符是否到达(文件传输完毕),防止在进行无盘传输时将结束符也一起写入文件中
* 当使用有盘传输时 当文件传输完毕时被标记为1
*/
int eof_reached = 0;
if (usemark) {
// 若为无盘传输(启用了结束符),则每收到一批数据都要将最新的后40个字节保存在lastbytes中
if (nread >= REDIS_RUN_ID_SIZE) {
memcpy(lastbytes,buf+nread-REDIS_RUN_ID_SIZE,REDIS_RUN_ID_SIZE);
} else {
int rem = REDIS_RUN_ID_SIZE-nread;
memmove(lastbytes,lastbytes+nread,rem);
memcpy(lastbytes+rem,buf,nread);
}
// 若收到了结束符,则将eof_reached置1
if (memcmp(lastbytes,eofmark,REDIS_RUN_ID_SIZE) == 0) eof_reached = 1;
}
// 更新最后进行io通信的时间
server.repl_transfer_lastio = server.unixtime;
// 将同步数据写入文件中
if (write(server.repl_transfer_fd,buf,nread) != nread) {
redisLog(REDIS_WARNING,"Write error or short write writing to the DB dump file needed for MASTER <-> SLAVE synchronization: %s", strerror(errno));
goto error;
}
server.repl_transfer_read += nread; // 增加已读取的文件大小
// 若使用的是无盘模式,且已收到了40字节的结束符,那么将最后40个字节从文件中截断(ftruncate)
if (usemark && eof_reached) {
if (ftruncate(server.repl_transfer_fd,
server.repl_transfer_read - REDIS_RUN_ID_SIZE) == -1)
{
redisLog(REDIS_WARNING,"Error truncating the RDB file received from the master for SYNC: %s", strerror(errno));
goto error;
}
}
/* 每读取一定大小的数据,就进行一次将数据同步到磁盘,否则在传输结束时可能由于
* 要将大量数据同步到磁盘而造成很大的延迟
*/
if (server.repl_transfer_read >=
server.repl_transfer_last_fsync_off + REPL_MAX_WRITTEN_BEFORE_FSYNC)
{
off_t sync_size = server.repl_transfer_read -
server.repl_transfer_last_fsync_off;
rdb_fsync_range(server.repl_transfer_fd,
server.repl_transfer_last_fsync_off, sync_size);
server.repl_transfer_last_fsync_off += sync_size;
}
/* Check if the transfer is now complete */
if (!usemark) {
// 当使用有盘传输时通过已接收大小与总大小比较来判断是否传输完毕,若完毕则将eof_reached置1
if (server.repl_transfer_read == server.repl_transfer_size)
eof_reached = 1;
}
// 文件传输完毕
if (eof_reached) {
// 将临时文件名改为正规的RDB文件名
if (rename(server.repl_transfer_tmpfile,server.rdb_filename) == -1) {
redisLog(REDIS_WARNING,"Failed trying to rename the temp DB into dump.rdb in MASTER <-> SLAVE synchronization: %s", strerror(errno));
replicationAbortSyncTransfer();
return;
}
redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Flushing old data");
// 清空旧数据库
signalFlushedDb(-1);
emptyDb(replicationEmptyDbCallback);
/* Before loading the DB into memory we need to delete the readable
* handler, otherwise it will get called recursively since
* rdbLoad() will call the event loop to process events from time to
* time for non blocking loading. */
/*此处不太明了,似乎与rdbLoad()有关????????????????????????*/
aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE);
redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Loading DB in memory");
// 加载RDB文件
if (rdbLoad(server.rdb_filename) != REDIS_OK) {
redisLog(REDIS_WARNING,"Failed trying to load the MASTER synchronization DB from disk");
replicationAbortSyncTransfer();
return;
}
/* Final setup of the connected slave <- master link */
zfree(server.repl_transfer_tmpfile);
close(server.repl_transfer_fd);
// 回调函数将被设置为readQueryFromClient
server.master = createClient(server.repl_transfer_s);
server.master->flags |= REDIS_MASTER;
server.master->authenticated = 1;
server.repl_state = REDIS_REPL_CONNECTED; // 标记为已成功连接到主服务器(已完成RDB同步)
server.master->reploff = server.repl_master_initial_offset;
// 拷贝主服务器的运行时ID
memcpy(server.master->replrunid, server.repl_master_runid,
sizeof(server.repl_master_runid));
/* If master offset is set to -1, this master is old and is not
* PSYNC capable, so we flag it accordingly. */
if (server.master->reploff == -1)
server.master->flags |= REDIS_PRE_PSYNC;
redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Finished with success");
/* Restart the AOF subsystem now that we finished the sync. This
* will trigger an AOF rewrite, and when done will start appending
* to the new file. */
// 如果有开启 AOF 持久化,那么重启 AOF 功能,并强制生成新数据库的 AOF 文件
if (server.aof_state != REDIS_AOF_OFF) {
int retry = 10;
stopAppendOnly();
while (retry-- && startAppendOnly() == REDIS_ERR) {
redisLog(REDIS_WARNING,"Failed enabling the AOF after successful master synchronization! Trying it again in one second.");
sleep(1);
}
if (!retry) {
redisLog(REDIS_WARNING,"FATAL: this slave instance finished the synchronization with its master, but the AOF can't be turned on. Exiting now.");
exit(1);
}
}
}
return;
error:
replicationAbortSyncTransfer();
return;
}
2.主服务器端