redis之复制之谜(一)

本文详细解读了Redis早期同步复制与0.100版本后引入的异步复制的工作流程,对比两者性能瓶颈,重点探讨了异步复制如何提高效率及存在的挑战,如内存消耗和命令缓存问题。
摘要由CSDN通过智能技术生成

    复制,表面意思就是一份拷贝,在redis中即为一个实例数据的备份,主要用于数据的跨主机备份,容灾处理,并且也是redis集群的基础。

redis复制的第一版(同步复制)

伴随着redis的诞生而诞生的,即第一个版本0.091就有复制功能。

整体处理逻辑:

  1. 当配置文件中配置了如下选项时,此redis实例将成为replica
    slaveof <masterip> <masterport>
  2. replica启动后,根据配置的master的IP和端口主动连接master
    syncWithMaster()
    {
    ...
        anetTcpConnect(NULL,server.masterhost,server.masterport);
    ...
    }
  3. 连接上master后,replica主动发送 SYNC命令,等待master的响应(阻塞读
    syncWithMaster()
    {
    ...
        syncWrite(fd,"SYNC \r\n",7,5)
    ...
        syncReadLine(fd,buf,1024,5) //读取文件的大小
    ...
    }
  4. master接收到SYNC命令后,进行内存数据持久化到文件(rdb)
    syncCommand()
    {
        ...
        rdbSave(server.dbfilename)
        ...
    }
  5. master读取rdb文件,获取文件大小,发送文件大小给replica
    syncCommand()
    {
    ... 
        fd = open(server.dbfilename, O_RDONLY);
        fstat(fd,&sb)
        len = sb.st_size;
    
        snprintf(sizebuf,32,"$%d\r\n",len);
        syncWrite(c->fd,sizebuf,strlen(sizebuf),5)
    ...
    }
  6. master读取rdb文件内容,发送内容给replica
    syncCommand()
    {
    ...
         while(len) {
            char buf[1024];
            int nread;
    
            if (time(NULL)-start > REDIS_MAX_SYNC_TIME) goto closeconn;
            nread = read(fd,buf,1024);
            if (nread == -1) goto closeconn;
            len -= nread;
            if (syncWrite(c->fd,buf,nread,5) == -1) goto closeconn;
        }
    ...
    }
  7. master发送结束符\r\n
    syncCommand()
    {
    ... 
        syncWrite(c->fd,"\r\n",2,5)
        close(fd);
    ...
    }
  8. master将replica入到replica队列
    syncCommand()
    {
    ...
         listAddNodeTail(server.slaves,c))//后续master遍历整个队列,知道有哪些replica
    ...
    }
  9. replica从网络上读取文件大小
    syncWithMaster()
    {
    ...
        syncReadLine(fd,buf,1024,5)
    
         dumpsize = atoi(buf+1);
    ...
    }
  10. replica读取文件内容,写本地临时文件,最终存储到rdb文件
    syncWithMaster()
    {
    ...
        //写入到临时文件中
        snprintf(tmpfile,256,"temp-%d.%ld.rdb",(int)time(NULL),(long int)random());
        dfd = open(tmpfile,O_CREAT|O_WRONLY,0644);
    
        ...
        while(dumpsize) {
            int nread, nwritten;
    
            //从master中读取
            nread = read(fd,buf,(dumpsize < 1024)?dumpsize:1024);
            ...
    
            //写入本地文件
            nwritten = write(dfd,buf,nread);
            ...
    
    
            dumpsize -= nread;
        }
        close(dfd);
    
        //用临时文件替换rdb文件
        rename(tmpfile,server.dbfilename)
    ...
    }
  11. replica清空本地数据库数据
    syncWithMaster()
    {
    ...
        emptyDb();
    ...
    }
  12. replica加载rdb文件到内存,恢复数据库
    syncWithMaster()
    {
    ...
        rdbLoad(server.dbfilename) 
    ...
    }
  13. master将出发数据更新的命令同步到replica
    processCommand()
    {
    ...
         /* Exec the command */
        dirty = server.dirty;
        cmd->proc(c); //如果此命令修改了数据,server.dirty将增加1
        if (server.dirty-dirty != 0 && listLength(server.slaves))
            replicationFeedSlaves(server.slaves,cmd,c->db->id,c->argv,c->argc); //将此命令发送给replicas
    ...
    }
    
     replicationFeedSlaves(list *slaves, struct redisCommand *cmd, int dictid, robj **argv, int argc) {
        listNode *ln = slaves->head;
        robj *outv[REDIS_MAX_ARGS*4]; /* enough room for args, spaces, newlines */
        int outc = 0, j;
        
        //构建命令
        for (j = 0; j < argc; j++) {
            if (j != 0) outv[outc++] = shared.space;
            ...
    
            outv[outc++] = argv[j];
        }
        outv[outc++] = shared.crlf;
    
        //发送数据
        while(ln) {
            redisClient *slave = ln->value;
            ...
    
            for (j = 0; j < outc; j++) addReply(slave,outv[j]);
            ln = ln->next;
        }
    }

     

因为是同步复制,所以会有很多性能问题。

  • replica每次重启(或者和master链接断开后重连),都需要全量同步

     每次replica连接上master同步时,master将阻塞在将内存数据全量写到磁盘文件上,然后再读取文件,最后从磁盘上读取文件,将文件内存发送给replica,在整个处理阶段master是不能响应任何请求的。

    replica每次重启时,如果replica开启了持久化,是否可以断点续传?

  • 部署多个replica时,master需要为每个replica生成rdb文件

      部署多个replica时,master会为每个replica生成rdb文件,即使master没有客户端请求或者内存的数据没有修改。

      多个replica在一定时间范围内同时连接上master请求同步,master是否共享一份rdb文件给这些replica

  • 每次生成rdb文件都是同步的

     每个replica连接到master请求同步时,master都是同步的生成rdb文件,replica大部分都是用来备份数据,不一定需要强一致性,一般都是最终一致性。

    master在响应replica同步请求生成rdb文件的过程是否可以异步?

  • 每次同步都需要读写文件

     每次replica连接上master请求同步时,master都需要将内存数据写到磁盘文件,然后又从磁盘文件读取到内存,发送给replica,经历了两次IO处理。

     replica也是将master发送的数据写入本地磁盘文件,然后再读取磁盘文件加载到内存恢复数据,同样经历了两次IO处理。

    在全量同步时,master是否可以不写文件,直接将内存数据发送给replica。

    在全量同步时,replica接收到master发送的数据时,是否不写文件,直接在内存恢复数据。

  • replica和master的状态不能动态切换

     replica只能通过配置文件中slaveof进行设置,其中设置了此项的为replica,没有设置的默认为master, 不能动态的通过命令进行切换。

      是否可以通过命令进行动态的切换master和replica角色。

redis复制第二版 (异步复制)

redis的0.100版本开始,引入了异步复制。

  • master异步生成rdb文件进行发送
  • master将在一段时间内同时请求同步的replica,应用同一份rdb文件(即只生成一次rdb)

replica的整个流程依然是同步阻塞的,而master断进行了异步操作

  1. replica主动连接master
    #首先需要配置master的IP,port,此实例将成为replica,主动连接master
    slaveof <masterip> <masterport>
    //解析配置文件
    loadServerConfig()
    {
    ...
           else if (!strcasecmp(argv[0],"slaveof") && argc == 3) {
                server.masterhost = sdsnew(argv[1]);
                server.masterport = atoi(argv[2]);
                server.replstate = REDIS_REPL_CONNECT;
            } 
    ...
    }
    serverCron()
    {
    ...
     if (server.replstate == REDIS_REPL_CONNECT) {
           ...
           syncWithMaster()
           ...
        }
    ...
    }
    syncWithMaster()
    {
    ...
      int fd = anetTcpConnect(NULL,server.masterhost,server.masterport);
    ...
    }
  2. replica连接上master后,replica发送SYNC命令
    syncWithMaster()
    {
    ...
        syncWrite(fd,"SYNC \r\n",7,5)
    ...
    }
  3. replica等待读(阻塞),可以发现超时等待时间从5秒变成了3600秒
    syncWithMaster()
    {
    ...
    syncReadLine(fd,buf,1024,3600)
    ...
    }
  4. master接受replica连接后,处理replica的SYNC命令
  5. master生成子进程,后台生成rdb文件
    syncCommand()
    {
    ...
     if (server.bgsaveinprogress) { //正在生成rdb过程中
            
            redisClient *slave;
            listNode *ln;
             //遍历replica对象链表,查找可以共用一份rdb数据的对象
            listRewind(server.slaves);
            while((ln = listYield(server.slaves))) {
                slave = ln->value;
                if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) break;
            }
            if (ln) {
                ...
                //找到共享replica对象,将此对象在rdb生成过程中缓存的新的写命令拷贝一份
                listRelease(c->reply);
                c->reply = listDup(slave->reply);
                ...
                c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
               ...
            } else {
                //没有找到共用replica对象,需要等待此次rdb生成完成后,再进行rdb的生成
                c->replstate = REDIS_REPL_WAIT_BGSAVE_START;
               ...
            }
        } else { //没有rdb在生成,可以即可进行rdb的生成
            ...
            rdbSaveBackground(server.dbfilename) //创建子进程进行处理
            ...
            c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
        }
    ...
    }
  6. 当master的子进程rdb文件生成完成后,master主进程将注册异步调用函数sendBulkToSlave进行文件的发送
    serverCron()
    {
    ...
        if (server.bgsaveinprogress) {//有后台子进程生成
            ...
            if (wait4(-1,&statloc,WNOHANG,NULL)) {//捕获子进程结束状态
               //子进程结束
                 ...
    
                server.bgsaveinprogress = 0;
    
                
                updateSalvesWaitingBgsave(exitcode == 0 ? REDIS_OK : REDIS_ERR);
            }
        } 
    ...
    }
    
    updateSalvesWaitingBgsave(int bgsaveerr) {
        listNode *ln;
        int startbgsave = 0;
    
        //遍历所有的replica对象
        listRewind(server.slaves);
        while((ln = listYield(server.slaves))) {
            redisClient *slave = ln->value;
    
            if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) { //如果是等待rdb开始
                startbgsave = 1; //后续将开始rdb的生成
                slave->replstate = REDIS_REPL_WAIT_BGSAVE_END;
            } else if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) {//等待rdb生成结束
                struct stat buf;
    
                 ...
                //打开rdb文件
                slave->repldbfd = open(server.dbfilename,O_RDONLY)
                ...
    
                //获得文件大小
                fstat(slave->repldbfd,&buf)
                ...
    
                slave->repldboff = 0;
                slave->repldbsize = buf.st_size;
                slave->replstate = REDIS_REPL_SEND_BULK;
    
                //注册异步回调函数sendBulkToSlave,并且设置fd可写,使epoll进行调用
                aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE);
                aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave, NULL)
                ...
        }
    
        //需要生成rdb,开始异步生成rdb
        if (startbgsave) {
            rdbSaveBackground(server.dbfilename)
            ...
        }
    }
    
  7. 当master异步调用注册的发送函数,将文件发送完成后,注册异步回调函数sendReplyToClient,将发送文件期间新的修改数据的命令发送给replica
    sendBulkToSlave(aeEventLoop *el, int fd, void *privdata, int mask)
     {
        redisClient *slave = privdata;
       ...
        char buf[REDIS_IOBUF_LEN];
        ssize_t nwritten, buflen;
    
        if (slave->repldboff == 0) {//首次发送,先发送文件大小
            ...
    
            bulkcount = sdscatprintf(sdsempty(),"$%lld\r\n",(unsigned long long)
                slave->repldbsize);
            write(fd,bulkcount,sdslen(bulkcount)
            ...
        }
    
    
        lseek(slave->repldbfd,slave->repldboff,SEEK_SET); //偏移到已发送位置
        buflen = read(slave->repldbfd,buf,REDIS_IOBUF_LEN); //从文件中读取数据
        ...
    
        //将读取的文件内容发送给replica
        nwritten = write(fd,buf,buflen)
        ...
    
        //累加发送字节
        slave->repldboff += nwritten;
    
    
        if (slave->repldboff == slave->repldbsize) { //文件发送完成
            close(slave->repldbfd); //关闭文件
            ...
    
            //注册异步回调函数sendReplyToClient,将缓存的命令发送给replica
            aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE);
            slave->replstate = REDIS_REPL_ONLINE;
            aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE,
                sendReplyToClient, slave, NULL) 
            ...
        }
    }
    
  8. mater将从生成rdb到发送rdb结束期间产生的修改数据的命令发送给replica
    sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
        redisClient *c = privdata;
        int nwritten = 0, totwritten = 0, objlen;
        robj *o;
        ...
    
        while(listLength(c->reply)) {
            o = listNodeValue(listFirst(c->reply));
            objlen = sdslen(o->ptr);
    
            if (objlen == 0) {
                listDelNode(c->reply,listFirst(c->reply));
                continue;
            }
    
            if (c->flags & REDIS_MASTER) { //如果自己是replica,则不发送响应给master
                nwritten = objlen - c->sentlen;
            } else {
                //master 发送给 replica
                nwritten = write(fd, ((char*)o->ptr)+c->sentlen, objlen - c->sentlen);
                ...
            }
            c->sentlen += nwritten;
            totwritten += nwritten;
            /* If we fully sent the object on head go to the next one */
            if (c->sentlen == objlen) {
                listDelNode(c->reply,listFirst(c->reply));
                c->sentlen = 0;
            }
        }
        ...
        
        //如果已经全部发送完,则删除对应句柄的写事件,epoll将不在调用此函数
        if (listLength(c->reply) == 0) {
            c->sentlen = 0;
            aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
        }
    }
    

 

  1. 虽然master已经异步了,但是replica依然是同步的,这样replica作用不大
  2. master在对replica缓存命令的时候,每个replica对象都维护了一个链表,如果replica很多,并且同步期间修改数据的命令很多,将非常耗内存

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值