相关文章
Redis源码剖析——主从复制(1)
Redis源码剖析——主从复制(3)
Redis源码剖析——主从复制(4)
Redis源码剖析——主从复制(5)—共享复制缓冲区的方案
目录
主从复制——从节点
从从节点角度出发,分析在Redis源码中的主从复制部分。
一、从节点属性
在Redis源码中,表示Redis服务器的全局结构体struct redisServer server中,与主从复制相关的,从节点属性如下:
server.masterhost:记录从节点对应主节点的ip地址;
server.masterport:记录从节点对应主节点的端口号;
server.masterauth:如果设置了server.masterauth项,则需要向主节点发送"AUTH"命令,后跟"masterauth"选项的值进行密码认证;
server.repl_transfer_s:socket描述符,用于主从复制过程中,从节点与主节点之间的TCP通信,包括主从节点间的握手通信、接收RDB数据,以及后续的命令传播过程;
server.repl_transfer_fd:文件描述符,用于从节点将收到的RDB数据写到本地临时文件;
server.repl_transfer_tmpfile:从节点上,用于记录RDB数据的临时文件名;
server.repl_state:记录主从复制过程中,从节点的状态;
server.master:当从节点接受完主节点发来的RDB数据之后,进入命令传播过程。从节点就将主节点当成一个客户端看待。server.master就是redisClient结构的主节点客户端,从节点接收该server.master发来的命令,像处理普通客户端的命令请求一样进行处理,从而实现了从节点和主节点之间的同步;
server.master->reploff:从节点记录的复制偏移量,每次收到主节点发来的命令时,就会将命令长度增加到该复制偏移量上,以保持和主节点复制偏移量的一致;
server.master->replrunid:从节点记录的主节点运行ID;
server.cached_master:主从节点复制过程中(具体应该是命令传播过程中),如果从节点与主节点之间连接断掉了,会调用freeClient(server.master),关闭与主节点客户端的连接。为了后续重连时能够进行部分重同步,在freeClient中,会调用replicationCacheMaster函数,将server.master保存到server.cached_master。该redisClient结构中记录了主节点的运行ID,以及复制偏移。当后续与主节点的连接又重新建立起来的时候,使用这些信息进行部分重同步,也就是发送"PSYNC <runid> <offset>"命令;
server.repl_master_runid和server.repl_master_initial_offset:从节点发送"PSYNC <runid> <offset>"命令后,如果主节点不支持部分重同步,则会回复信息为"+FULLRESYNC <runid> <offset>",表示要进行完全重同步,其中<runid>表示主节点的运行ID,记录到server.repl_master_runid中,<offset>表示主节点的初始复制偏移,记录到server.repl_master_initial_offset中;
server.repl_transfer_size:记录完整复制时主节点发送来的RDB文件的大小;
server.repl_transfer_read:记录从节点在读取主节点发来的RDB文件时,已读内容字节数;
server.repl_transfer_last_fsync_off:由于从节点在读取RDB文件时,定期将读入的文件 fsync 到磁盘,以免缓冲区内内容太多,一下子写入时撑爆 IO,该属性记录从节点最近一次执行 fsync 时的偏移量;
server.repl_transfer_lastio:记录最近一次主从节点交互的时间;
二、建立连接和握手过程
从节点在收到客户端发来的”slaveof”命令时,或者在配置文件中配置了”slaveof”选项时,就会向主节点建链,开始主从复制过程。
在主节点将实际的RDB数据发送给从节点之前,还需要经历握手过程,这非常类似于TCP建链的三次握手。该过程由从节点主动发起,主节点作出相应的回应。握手过程如下:
该握手过程中,从节点的状态会发生转换,从REDIS_REPL_CONNECT状态起,一直到REDIS_REPL_RECEIVE_PSYNC状态期间,都算是握手过程。
1.TCP建立连接
在Redis源码中,使用server.repl_state记录从节点的状态。在Redis初始化时,该状态为REDIS_REPL_NONE。
当从节点收到客户端用户发来的”SLAVEOF” 命令时,或者在读取配置文件,发现了”slaveof”配置选项,就会将server.repl_state置为REDIS_REPL_CONNECT状态。该状态表示下一步需要向主节点发起TCP建链。其中SLAVEOF命令的执行函数代码如下:
void slaveofCommand(redisClient *c) {
// // 不允许在集群模式中使用
// if (server.cluster_enabled) {
// addReplyError(c,"SLAVEOF not allowed in cluster mode.");
// return;
// }
// SLAVEOF NO ONE 让从服务器转为主服务器
if (!strcasecmp(c->argv[1]->ptr,"no") &&
!strcasecmp(c->argv[2]->ptr,"one")) {
if (server.masterhost) {
// 让服务器取消复制,成为主服务器
replicationUnsetMaster();
//redisLog(REDIS_NOTICE,"MASTER MODE enabled (user request)");
}
} else {
long port;
// 获取端口参数
if ((getLongFromObjectOrReply(c, c->argv[2], &port, NULL) != REDIS_OK))
return;
// 检查输入的 host 和 port 是否服务器目前的主服务器
// 如果是的话,向客户端返回 +OK ,不做其他动作
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);
//redisLog(REDIS_NOTICE,"SLAVE OF %s:%d enabled (user request)", server.masterhost, server.masterport);
}
addReply(c,shared.ok);
}
关于不允许在集群模式中使用的原因,目前还没研究关于集群的内容,先搁置。
首先判断命令是否为"SLAVEOF no one",对于该命令的处理主要是调用replicationUnsetMaster函数让从服务器转为主服务器。
然后对"SLAVEOF host port"命令中的参数进行分析。检查输入的 host 和 port 是否服务器目前的主服务器,如果是的话,向客户端返回 +OK ,不做其他动作。
如果没有前任主服务器,或者客户端指定了新的主服务器,调用函数replicationSetMaster,将当前服务器设为指定地址的从服务器,并将从服务器的连接状态server.repl_state设置为REDIS_REPL_CONNECT。
其中函数replicationSetMaster的代码如下:
void replicationSetMaster(char *ip, int port) {
// 清除原有的主服务器地址(如果有的话)
sdsfree(server.masterhost);
// IP
server.masterhost = sdsnew(ip);
// 端口
server.masterport = port;
// 清除原来可能有的主服务器信息。。。
// 如果之前有其他地址,那么释放它
if (server.master) freeClient(server.master);
// 断开所有从服务器的连接,强制所有从服务器执行重同步
disconnectSlaves();
// 清空可能有的 master 缓存
replicationDiscardCachedMaster();
// 释放 backlog (如果存在的话)
freeReplicationBacklog();
// 取消之前的复制进程(如果有的话)
cancelReplicationHandshake();
// 进入连接状态(重点)
server.repl_state = REDIS_REPL_CONNECT;
server.master_repl_offset = 0;
}
在定时执行的函数serverCron中,会调用replicationCron函数检查主从复制的状态。该函数中,一旦发现当前的server.repl_state为REDIS_REPL_CONNECT,则会调用函数connectWithMaster,向主节点发起TCP建链请求,其代码如下:
int connectWithMaster(void) {
int fd;
// 向主服务器发起connect连接请求,获得已连接文件描述符fd
fd = anetTcpNonBlockConnect(NULL,server.masterhost,server.masterport);
if (fd == -1) {
//redisLog(REDIS_WARNING,"Unable to connect to MASTER: %s", strerror(errno));
return REDIS_ERR;
}
// 监听主服务器 fd 的读和写事件,并绑定文件事件处理器syncWithMaster,该函数用于处理主从节点间的握手过程
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;
// 将状态改为REDIS_REPL_CONNECTING,表示从节点正在向主节点建立连接
server.repl_state = REDIS_REPL_CONNECTING;
return REDIS_OK;
}
从节点根据其保存的主服务器对应ip地址server.masterhost和端口号server.masterport与主服务器建立连接。
然后注册socket描述符fd上的可读和可写事件,事件回调函数都为syncWithMaster,该函数用于处理主从节点间的握手过程;
然后更新最近一次主从节点的交互时间,并将socket描述符记录到server.repl_transfer_s中。置主从复制状态server.repl_state为REDIS_REPL_CONNECTING,表示从节点正在向主节点建链;
2.主从节点握手
当主从节点间的TCP建链成功之后,(怎么触发?)就会触发socket描述符server.repl_transfer_s上的可写事件,从而调用函数syncWithMaster。该函数处理从节点与主节点间的握手过程。也就是从节点在向主节点发送TCP建链请求,到接收RDB数据之前的过程。其代码如下:
void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
char tmpfile[256], *err;
int dfd, maxtries = 5;
int sockerr = 0, psync_result;
socklen_t errlen = sizeof(sockerr);
//无效参数,避免警告
REDIS_NOTUSED(el);
REDIS_NOTUSED(privdata);
REDIS_NOTUSED(mask);
// 如果从节点处于 SLAVEOF NO ONE 模式,说明从节点收到了客户端执行的"slave no one"命令,因此直接关闭socket描述符,然后返回
if (server.repl_state == REDIS_REPL_NONE) {
close(fd);
return;
}
// 检查套接字错误
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &sockerr, &errlen) == -1)
sockerr = errno;
if (sockerr) {
//删除对主服务器连接描述符上注册的可读和可写事件,移除对其的监听
aeDeleteFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE);
//redisLog(REDIS_WARNING,"Error condition on socket for SYNC: %s", strerror(sockerr));
goto error;
}
// 如果从服务器的复制状态为 CONNECTING ,那么在进行初次同步之前,
// 向主服务器发送一个非阻塞的 PONG
// 因为接下来的 RDB 文件发送非常耗时,所以我们想确认主服务器真的能访问
if (server.repl_state == REDIS_REPL_CONNECTING) {
//redisLog(REDIS_NOTICE,"Non blocking connect for SYNC fired the event.");
// 手动发送同步 PING ,暂时取消监听写事件
aeDeleteFileEvent(server.el,fd,AE_WRITABLE);
// 更新复制状态REDIS_REPL_RECEIVE_PONG,等待主服务PONG回复
server.repl_state = REDIS_REPL_RECEIVE_PONG;
// 同步发送 PING
syncWrite(fd,"PING\r\n",6,100);
// 返回,等待 PONG 到达
return;
}
// 接收 PONG 命令
if (server.repl_state == REDIS_REPL_RECEIVE_PONG) {
char buf[1024];
// 手动同步接收 PONG ,暂时取消监听读事件
aeDeleteFileEvent(server.el,fd,AE_READABLE);
// 尝试在指定时间限制内读取 PONG
buf[0] = '\0';
// 同步接收 PONG
if (syncReadLine(fd,buf,sizeof(buf),
server.repl_syncio_timeout*1000) == -1)
{
//redisLog(REDIS_WARNING, "I/O error reading PING reply from master: %s", strerror(errno));
goto error;
}
// 接收到的数据只有两种可能:
// 第一种是 +PONG ,第二种是因为未验证而出现的 -NOAUTH 错误
if (buf[0] != '+' &&
strncmp(buf,"-NOAUTH",7) != 0 &&
strncmp(buf,"-ERR operation not permitted",28) != 0)
{
// 接收到未验证错误
//redisLog(REDIS_WARNING,"Error reply to PING from master: '%s'",buf);
goto error;
} else {
// 接收到 PONG
// 更新复制状态REDIS_REPL_SEND_AUTH,准备发送密码认证
server.repl_state = REDIS_REPL_SEND_AUTH;
//redisLog(REDIS_NOTICE, "Master replied to PING, replication can continue...");
}
}
// 进行身份验证
if(server.repl_state == REDIS_REPL_SEND_AUTH){
if(server.masterauth) {
//如果配置了"masterauth"选项,则向主节点发送"AUTH"命令,后跟"masterauth"选项的值
err = sendSynchronousCommand(fd,"AUTH",server.masterauth,NULL);
//主服务器返回错误处理
if (err[0] == '-') {
//redisLog(REDIS_WARNING,"Unable to AUTH to MASTER: %s",err);
sdsfree(err);
goto error;
}
sdsfree(err);
}
// 更新复制状态REDIS_REPL_SEND_PORT,准备发送从服务器自己监听的端口给主服务器
server.repl_state = REDIS_REPL_SEND_PORT;
}
// 将从服务器的端口发送给主服务器,
// 使得主服务器的 INFO 命令可以显示从服务器正在监听的端口
if (server.repl_state == REDIS_REPL_SEND_PORT)
{
sds port = sdsfromlonglong(server.port);
//向主节点发送"REPLCONF listening-port <port>"命令
err = sendSynchronousCommand(fd,"REPLCONF","listening-port",port,
NULL);
sdsfree(port);
//如果回复信息的首字节为"-",说明主节点不认识该命令,这不是致命错误,只是记录日志而已
if (err[0] == '-') {
//redisLog(REDIS_NOTICE,"(Non critical) Master does not understand REPLCONF listening-port: %s", err);
}
sdsfree(err);
// 更新复制状态REDIS_REPL_SEND_PSYNC,准备向主服务器发送命令: "PSYNC <master_run_id> <repl_offset>"
server.repl_state = REDIS_REPL_SEND_PSYNC;
}
//向主服务器发送命令: "PSYNC <master_run_id> <repl_offset>"
if (server.repl_state == REDIS_REPL_SEND_PSYNC){
// 调用slaveTryPartialResynchronization读取主节点对于"PSYNC"命令的回复
psync_result = slaveTryPartialResynchronization(fd);
}
// 可以执行部分重同步
if (psync_result == PSYNC_CONTINUE) {
//redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization.");
// 返回
return;
}
// 主服务器不支持 PSYNC ,发送 SYNC
if (psync_result == PSYNC_NOT_SUPPORTED) {
//redisLog(REDIS_NOTICE,"Retrying with SYNC...");
// 向主服务器发送 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;
}
}
/*
* 如果执行到这里,说明不管函数返回PSYNC_FULLRESYNC,还是返回PSYNC_NOT_SUPPORTED
*都表示接下来要进行完全重同步过程
*/
// 打开一个临时文件,用于写入和保存接下来从主服务器传来的 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;
}
// 设置一个读事件处理器,来读取主服务器的 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;
}
// 设置复制状态为REDIS_REPL_TRANSFER,表示开始接收主节点的RDB数据
server.repl_state = REDIS_REPL_TRANSFER;
// 更新统计信息
//初始化RDB文件大小为-1,因为在读取RDB文件时会获取真正的文件大小
server.repl_transfer_size = -1;
//初始化已读 RDB 文件内容的字节数
server.repl_transfer_read = 0;
//初始化从服务器最近一次执行 fsync 时的偏移量
server.repl_transfer_last_fsync_off = 0;
//初始化保存 RDB 文件的临时文件的描述符
server.repl_transfer_fd = dfd;
//初始化保存 RDB 文件的临时文件名字
server.repl_transfer_tmpfile = zstrdup(tmpfile);
//初始化最近一次读入 RDB 内容的时间
server.repl_transfer_lastio = server.unixtime;
return;
//错误处理
error:
//关闭与主服务器建立连接的文件描述符
close(fd);
//从节点与主服务器建立连接的套接字置为-1
server.repl_transfer_s = -1;
//从服务器去复制状态设置为slaveof命令后的待连接状态
server.repl_state = REDIS_REPL_CONNECT;
return;
}
如果从节点处于 SLAVEOF NO ONE 模式,说明从节点收到了客户端执行的"slave no one"命令,因此直接关闭socket描述符,然后返回。
错误处理流程:关闭与主服务器建立连接的文件描述符,并将从节点与主服务器建立连接的套接字置为-1,置状态server.repl_state为REDIS_REPL_CONNECT,等待下次调用replicationCron时重连主节点;
调用函数getsockopt检查套接字是否存在错误,如果错误,则删除socket描述符上注册的可读和可写事件,并跳到错误处理流程。
如果从服务器的复制状态为 REDIS_REPL_CONNECTING,那么在进行初次同步之前,向主服务器发送一个非阻塞的 PING,因为接下来的 RDB 文件发送非常耗时,所以我们想确认主服务器真的能访问。此时会触发了描述符的可写事件,从而调用的该回调函数。这种情况下,先删除描述符上的可写事件,然后将状态设置为REDIS_REPL_RECEIVE_PONG,手动向主节点发送"PING"命令,然后返回;
如果当前的复制状态为REDIS_REPL_RECEIVE_PONG,则说明从节点收到了主节点对于"PING"命令的回复,触发了描述符的可读事件,从而调用的该回调函数。这种情况下,首先读取主节点的回复信息,正常情况下,主节点的回复只能有三种情况:"+PONG","-NOAUTH"和"-ERR operation not permitted"(老版本的redis主节点),如果收到的回复不是以上的三种,则直接进入错误处理代码流程。否则,将复制状态置为REDIS_REPL_SEND_AUTH(不返回);
当前的复制状态为REDIS_REPL_SEND_AUTH,如果配置了"masterauth"选项,则向主节点发送"AUTH"命令,后跟"masterauth"选项的值,不管从节点有没有配置"masterauth"选项,因为"AUTH"是同步命令,因此直接跳过REDIS_REPL_RECEIVE_AUTH状态,都将复制状态置为REDIS_REPL_SEND_PORT(不返回);
如果当前复制状态为REDIS_REPL_SEND_PORT,则向主节点发送"REPLCONF listening-port <port>"命令,告知主节点本身的端口号,因为"REPLCONF listening-port <port>"是同步命令,因此直接跳过REDIS_REPL_RECEIVE_PORT状态,直接将复制状态置为REDIS_REPL_SEND_PSYNC后返回;
如果复制状态为REDIS_REPL_SEND_PSYNC,则调用slaveTryPartialResynchronization函数,向主节点发送"PSYNC <psync_runid> <psync_offset>"命令。因为"PSYNC <psync_runid> <psync_offset>"是同步命令,因此REDIS_REPL_RECEIVE_PORT状态并不实际存在。
在slaveTryPartialResynchronization函数中,如果从节点缓存了主节点,说明该从节点之前与主节点的连接断掉了,现在是重新连接,因此尝试进行部分重同步。置psync_runid为保存的主节点ID,置psync_offset为保存的主节点复制偏移加1;如果从节点没有缓存主节点,说明需要进行完全重同步,则置psync_runid为"?",置psync_offset为"-1";
slaveTryPartialResynchronization返回主节点对于"PSYNC"命令的回复:
如果回复信息以"+CONTINUE"开头,说明主节点可以进行部分重同步,这种情况下,设置复制状态为REDIS_REPL_CONNECTED,后续将主节点当成一个客户端,接收该主节点客户端发来的命令请求,像处理普通客户端一样处理即可。因此函数该返回PSYNC_CONTINUE后,该函数直接返回即可;
如果回复信息以"+FULLRESYNC"开头,说明主节点虽然认识"PSYNC"命令,但是从节点发送的复制偏移psync_offset已经不在主节点的积压队列中了,因此需要进行完全重同步。解析出回复信息中的主节点ID,保存在server.repl_master_runid中;解析出主节点复制偏移初始值,保存在server.repl_master_initial_offset中;然后函数slaveTryPartialResynchronization返回PSYNC_FULLRESYNC;
如果回复信息不属于以上的情况,说明主节点不认识"PSYNC"命令,这种情况下,函数slaveTryPartialResynchronization返回PSYNC_NOT_SUPPORTED;
不管函数slaveTryPartialResynchronization返回PSYNC_FULLRESYNC,还是返回PSYNC_NOT_SUPPORTED,都表示接下来要进行完全重同步过程:
首先,创建保存RDB数据的临时文件"temp-<unixtime>.<pid>.rdb",该文件的描述符记录到server.repl_transfer_fd中;
然后,注册socket描述符server.repl_transfer_s上的可读事件,事件回调函数为readSyncBulkPayload;
最后,置复制状态为REDIS_REPL_TRANSFER,表示开始接收主节点的RDB数据。并初始化下列从节点属性。
// 更新统计信息
//初始化RDB文件大小为-1,因为在读取RDB文件时会获取真正的文件大小
server.repl_transfer_size = -1;
//初始化已读 RDB 文件内容的字节数
server.repl_transfer_read = 0;
//初始化从服务器最近一次执行 fsync 时的偏移量
server.repl_transfer_last_fsync_off = 0;
//初始化保存 RDB 文件的临时文件的描述符
server.repl_transfer_fd = dfd;
//初始化保存 RDB 文件的临时文件名字
server.repl_transfer_tmpfile = zstrdup(tmpfile);
//初始化最近一次读入 RDB 内容的时间
server.repl_transfer_lastio = server.unixtime;
slaveTryPartialResynchronization函数的具体代码如下:
int slaveTryPartialResynchronization(int fd) {
char *psync_runid;
char psync_offset[32];
sds reply;
/*
* 从节点发送"PSYNC <runid> <offset>"命令后,
* 如果主节点不支持部分重同步,则会回复信息为"+FULLRESYNC <runid> <offset>",表示要进行完全重同步,
* 其中<runid>表示主节点的运行ID,记录到server.repl_master_runid中,
* <offset>表示主节点的初始复制偏移,记录到server.repl_master_initial_offset中。
* */
server.repl_master_initial_offset = -1;
//该服务器不是第一个进行主从复制
if (server.cached_master) {
// 缓存存在,尝试部分重同步
// 命令为 "PSYNC <master_run_id> <repl_offset>"
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 {
// 缓存不存在
// 发送 "PSYNC ? -1" ,要求完整重同步
//redisLog(REDIS_NOTICE,"Partial resynchronization not possible (no cached master)");
psync_runid = "?";
memcpy(psync_offset,"-1",3);
}
// 向主服务器发送 PSYNC <runid> <offset> 命令,并获得主服务器的回复reply
reply = sendSynchronousCommand(fd,"PSYNC",psync_runid,psync_offset,NULL);
// 接收到 "+FULLRESYNC <runid> <offset>" ,进行完整重同步
if (!strncmp(reply,"+FULLRESYNC",11)) {
char *runid = NULL;
char*offset = NULL;
// 分析并记录主服务器的运行ID <runid>
runid = strchr(reply,' ');
if (runid) {
runid++;
// 分析并记录主服务器的初始复制偏移量
offset = strchr(runid,' ');
if (offset) offset++;
}
// 检查 run id 的合法性
if (!runid || !offset || (offset-runid-1) != REDIS_RUN_ID_SIZE) {
//redisLog(REDIS_WARNING, "Master replied with wrong +FULLRESYNC syntax.");
// 主服务器支持 PSYNC ,但是却发来了异常的 run id
// 只好将 run id 设为 0 ,让下次 PSYNC 时失败
memset(server.repl_master_runid,0,REDIS_RUN_ID_SIZE+1);
} else {
// 保存主服务器 runid
memcpy(server.repl_master_runid, runid, offset-runid-1);
server.repl_master_runid[REDIS_RUN_ID_SIZE] = '\0';
// 保存主服务器可用于复制的初始复制偏移量 initial offset
server.repl_master_initial_offset = strtoll(offset,NULL,10);
// 打印日志,这是一个 FULL resync
//redisLog(REDIS_NOTICE,"Full resync from master: %s:%lld", server.repl_master_runid, server.repl_master_initial_offset);
}
// 要开始完整重同步,不可能执行部分同步,缓存中的 master 已经没用了,清除它
replicationDiscardCachedMaster();
sdsfree(reply);
// 返回状态
return PSYNC_FULLRESYNC;
}
// 接收到”+CONTINUE“,进行部分重同步
if (!strncmp(reply,"+CONTINUE",9)) {
//redisLog(REDIS_NOTICE, "Successful partial resynchronization with master.");
sdsfree(reply);
// 将缓存中的 master 设为当前 master
replicationResurrectCachedMaster(fd);
// 返回状态
return PSYNC_CONTINUE;
}
// 接收到错误回复
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);
//执行到这说明已经不可能执行部分同步,缓存中的 master 已经没用了,清除它
replicationDiscardCachedMaster();
// 主服务器不支持 PSYNC
return PSYNC_NOT_SUPPORTED;
}
3.接收RDB数据
有硬盘复制的RDB数据,主节点将数据保存到RDB文件后,将文件内容加上"$<len>/r/n"的头部后,发送给从节点。
在syncWithMaster函数中,握手过程结束后,需要进行完全重同步时,从节点注册了socket描述符server.repl_transfer_s上的可读事件,事件回调函数为readSyncBulkPayload。从节点调用该函数接收主节点发来的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);
// 读取 RDB 文件的大小
// server.repl_transfer_size的值表示从节点要读取的RDB数据的总长度(仅对有硬盘复制的RDB数据而言)。
// 如果当前其值为-1,说明本次是第一次接收RDB数据。
if (server.repl_transfer_size == -1) {
// 调用读函数,在server.repl_syncio_timeout*1000时间内从fd中读取一行内容到buf
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;
}
// 如果读取到的内容为以‘-’开头,则是报错内容
if (buf[0] == '-') {
//redisLog(REDIS_WARNING, "MASTER aborted replication with an error: %s", buf+1);
goto error;
//如果读取到的内容为换行符【根据自定义的读取规则:如果读取到的内容只是换行符,去掉\n,加上结束符】
} else if (buf[0] == '\0') {
//在这个阶段,只是一个换行符作为PING工作,以使连接处于活动状态。
// 我们刷新了最近一次的交互时间戳
server.repl_transfer_lastio = server.unixtime;
return;
//主节点将数据保存到RDB文件后,将文件内容加上"$<len>/r/n"的头部,len表示RDB文件的大小
//如果读取到的内容既不是上述两种情况,也不是'$'开头,则说明读取的内容格式错误
} 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 文件大小
server.repl_transfer_size = strtol(buf+1,NULL,10);
//在日志中打印RDB文件的大小
//redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: receiving %lld bytes from master", (long long) server.repl_transfer_size);
return;
}
/*读数据*/
// 计算还有多少字节要读
left = server.repl_transfer_size - server.repl_transfer_read;
//计算本次可读取内容的长度
readlen = (left < (signed)sizeof(buf)) ? left : (signed)sizeof(buf);
// 从RDB文件中读取读取readlen长度内容到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;
}
// 更新最近一次从 RDB 读入内容的时间
server.repl_transfer_lastio = server.unixtime;
//将从RDB读取到的内容写入保存 RDB 文件的临时文件的描述符中
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;
}
// 更新已读 RDB 文件内容的字节数
server.repl_transfer_read += nread;
// 定期将读入的文件 fsync 到磁盘,以免 buffer 太多,一下子写入时撑爆 IO,至少每次同步8M内容
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;
fsync(server.repl_transfer_fd);
// rdb_fsync_range(server.repl_transfer_fd,
// server.repl_transfer_last_fsync_off, sync_size);
server.repl_transfer_last_fsync_off += sync_size;
}
// 检查 RDB 中的内容是否已经传送完毕
if (server.repl_transfer_read == server.repl_transfer_size) {
// 如果传送完毕,将临时文件改名为 dump.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);
// 先删除对主服务器的读事件监听,因为 rdbLoad() 函数也会监听读事件
// 从节点在加载RDB数据时,是不能处理主节点发来的其他数据的
aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE);
// 载入 RDB
if (rdbLoad(server.rdb_filename) != REDIS_OK) {
//redisLog(REDIS_WARNING,"Failed trying to load the MASTER synchronization DB from disk");
//如果载入RDB失败,则调用replicationAbortSyncTransfer,停止下载 RDB 文件,关闭与主服务器的连接,将从服务器状态调整至REDIS_REPL_CONNECT
replicationAbortSyncTransfer();
return;
}
// 关闭临时文件
zfree(server.repl_transfer_tmpfile);
// 关闭保存 RDB 文件的临时文件的描述符
close(server.repl_transfer_fd);
// 将主服务器设置成一个 redis client
// 注意 createClient 会为主服务器绑定事件,为接下来接收命令做好准备
server.master = createClient(server.repl_transfer_s);
// 标记这个客户端为主服务器
server.master->flags |= REDIS_MASTER;
// 标记它为已验证身份
server.master->authenticated = 1;
// 更新复制状态,表示主从节点已完成握手和接收RDB数据的过程;
server.repl_state = REDIS_REPL_CONNECTED;
// 设置主服务器的复制偏移量
server.master->reploff = server.repl_master_initial_offset;
// 保存主服务器的 RUN ID
memcpy(server.master->replrunid, server.repl_master_runid,
sizeof(server.repl_master_runid));
// 如果 offset 被设置为 -1 ,那么表示主服务器的版本低于 2.8
// 无法使用 PSYNC ,所以需要设置相应的标识值
if (server.master->reploff == -1)
server.master->flags |= REDIS_PRE_PSYNC;
//redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Finished with success");
// 首先调用stopAppendOnly
// 然后循环10次,调用startAppendOnly开始进行AOF转储,直到startAppendOnly返回REDIS_OK。
// 如果startAppendOnly失败次数超过10次,则直接exit退出
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:
//停止下载RDB文件,关闭与主服务器的连接,将从服务器状态调整至REDIS_REPL_CONNECT
replicationAbortSyncTransfer();
return;
}
server.repl_transfer_size的值表示要读取的RDB数据的总长度(仅对有硬盘复制的RDB数据而言)。如果当前其值为-1,说明本次是第一次接收RDB数据。因此,首先调用syncReadLine,读取主节点发来的第一行数据("\r\n"之前的内容)到buf中,读取的超时时间为5s,如果在5s之内还读不到"\n",则syncReadLine返回-1,跳到错误处理流程,调用函数replicationAbortSyncTransfer,终止本次复制过程,然后返回;
后续可读事件触发,再次调用该函数时,server.repl_transfer_size已不再是-1,开始接收真正的RDB数据了。接下来调用read,读取RDB数据内容到buf中,read返回值为nread。
如果nread小于等于0,要么说明发生了错误,要么说明主节点终止了链接,无论哪种情况,都是调用函数replicationAbortSyncTransfer,终止本次复制过程,然后返回;
如果nread大于0,最近一次主从节点交互的时间,将从RDB读取到的内容写入保存 RDB 文件的临时文件的描述符server.repl_transfer_fd中,然后将nread增加到server.repl_transfer_read中,该属性记录了当前已读到的RDB数据的长度;
每当读取了8M的数据后,都执行一次sync操作,保证临时文件内容确实写到了硬盘;
如果是有硬盘复制的RDB数据,且server.repl_transfer_read等于server.repl_transfer_size,则说明已经接收到所有数据。
如果所有的RDB数据已经接收完了,则首先将保存RDB数据的临时文件改名为配置的RDB文件名server.rdb_filename;然后调用signalFlushedDb,使得本实例的所有客户端感知到接下来要清空数据库了。然后就是调用emptyDb,清空所有数据,回调函数是replicationEmptyDbCallback,每当处理了字典哈希表中65535个bucket之后,就调用一次该函数,向主节点发送一个"\n",以向主节点证明本实例还活着;
然后先删除对主服务器的读事件监听,因为 rdbLoad() 函数也会监听读事件,从节点在加载RDB数据时,是不能处理主节点发来的其他数据的;
接下来就是调用rdbLoad加载RDB数据;
加载完RDB数据之后,就已经完成了完全重同步过程。接下来,从节点会将主节点当成客户端,像处理普通客户端那样,接收主节点发来的命令,执行命令以保证主从一致性。
加载完RDB数据之后,就已经完成了完全重同步过程。接下来,从节点会将主节点当成客户端,像处理普通客户端那样,接收主节点发来的命令,执行命令以保证主从一致性。
因此,首先关闭RDB临时文件描述符server.repl_transfer_fd,然后就使用socket描述符server.repl_transfer_s创建redisClient结构server.master,因此后续还是使用该描述符接收主节点客户端发来的命令;
将标记REDIS_MASTER记录到客户端标志中,以表明该客户端是主节点;
将复制状态置为REDIS_REPL_CONNECTED,表示主从节点已完成握手和接收RDB数据的过程;
主节点之前的发送"PSYNC"命令回复为"+FULLRESYNC"时,附带的初始复制偏移记录到了server.repl_master_initial_offset中,将其保存到server.master->reploff;附带的主节点ID记录到了server.repl_master_runid中,将其保存到server.master->replrunid中;如果server.repl_master_initial_offset为-1,说明主节点不认识"PSYNC"命令,因此将REDIS_PRE_PSYNC记录到客户端标志位中;
完成以上的操作之后,如果本实例开启了AOF功能,则首先调用stopAppendOnly,然后循环10次,调用startAppendOnly开始进行AOF转储,直到startAppendOnly返回REDIS_OK。如果startAppendOnly失败次数超过10次,则直接exit退出!!!
4.命令传播
当复制状态变为REDIS_REPL_CONNECTED后,表示进入了命令传播阶段。后续从节点将主节点当成一个客户端,接收该主节点客户端发来的命令请求,像处理普通客户端一样处理即可。
在读取客户端命令的函数recvData中,一旦从节点读到了追节点发来的同步命令,会将命令长度增加到从节点的复制偏移量server.master. reploff中:
if (nread) {
// 根据内容,更新查询缓冲区(SDS) free 和 len 属性
// 并将 '\0' 正确地放到内容的最后
sdsIncrLen(c->querybuf,nread);
// 记录服务器和客户端最后一次互动的时间
c->lastinteraction = server.unixtime;
// 如果客户端是主服务器 master 的话,更新它的复制偏移量
if (c->flags & REDIS_MASTER) c->reploff += nread;
}
这样,从节点的复制偏移量server.master. reploff就能与主节点保持一致了。
与普通客户端不同的是,主节点客户端发来的命令请求无需回复,因此,在函数prepareClientToWrite中,有下面的语句:
// 客户端是主服务器, 从服务器需要向主服务器发送REPLICATION ACK命令
// 在发送这个命令之前必须打开对应客户端的REDIS_MASTER_FORCE_REPLY标志
// 否则发送操作会被拒绝执行。
if ((c->flags & REDIS_MASTER) &&
!(c->flags & REDIS_MASTER_FORCE_REPLY)) return REDIS_ERR;
每次向客户端输出缓存追加新数据之前,都要调用函数prepareClientToWrite函数。如果该函数返回REDIS_ERR,表示无需向输出缓存追加新数据。
客户端标志中如果设置了REDIS_MASTER标记,就表示该客户端是主节点客户端server.master,并且在没有设置REDIS_MASTER_FORCE_REPLY标记的情况下,该函数返回REDIS_ERR,表示无需向输出缓存追加新数据。