Redis源码剖析——主从复制(2)

相关文章

Redis源码剖析——主从复制(1)
Redis源码剖析——主从复制(3)
Redis源码剖析——主从复制(4)
Redis源码剖析——主从复制(5)—共享复制缓冲区的方案

目录

主从复制——从节点

一、从节点属性

二、建立连接和握手过程

1.TCP建立连接

2.主从节点握手

3.接收RDB数据

4.命令传播

总结

主从复制——从节点

从从节点角度出发,分析在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,表示无需向输出缓存追加新数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值