「Redis源码解读」—多机数据库(一)主从

  • 主动数据库配置用来降低单个redis的压力(主要是master)。通常的方案是master用做数据写入,slave用做数据读取。



    主从复制,服务器双方数据库将保存相同的数据,这种现象称为“数据库状态一致”

127.0.0.1:6380> slaveof 127.0.0.1 6379

旧版复制功能的实现(2.8以前的版本)

复制功能都分为两个基本步骤:同步命令传播
1.同步:将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
2.命令传播:主服务器的数据库状态被修改,导致主从服务器的数据库状态不一致,让主从服务器数据库重新回到一致状态。

同步

当客户端向从服务器发送slaveof命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,也就是将从服务器的数据库状态更新至主服务器当前所处的数据库状态。而从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成。

  • 从服务器发送SYNC命令的执行步骤:
    1.从服务器向主服务器发送SYNC命令。
    2.收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
    3.当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收接收并载入这个RBD文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
    4.主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器当前所处的状态。


命令传播

  • 在执行完同步操作以后,如果客户端又再次向主服务器发送写命令,如果此时该命令没有传播到从服务器,那么主从服务器的数据库状态必然会不一样,因此,在执行完同步操作以后,还必须得执行命令传播,用来传播主服务器接收到的新的命令请求。
  • 为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的那条写命令,发送给从服务器,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。

旧版复制存在的缺陷

从服务器对主服务器的复制分为以下两种:
1.初次复制:从服务器没有复制任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同。
2.断线后重复制:处理命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连接上主服务器,并继续复制主服务器。


  • 当主从服务器断开以后,从服务器通过自动重连连上主服务器,然后从服务器向主服务器发送SYNC命令,进行同步操作,但是主服务器此时会将数据库状态写入到RDB文件中,如上述红色方框(重复复制了许多键值对),这部分就是旧版复制存在的缺陷。

旧版复制问题的解决方案

为了解决旧版复制功能在处理断线重复复制情况时的低效问题,redis从2.8以后,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。

  • psync命令具有完整重同步和部分重同步两种模式。
    1.完整重同步:用以解决初次复制的问题。执行操作与sync一模一样。
    2.部分重同步:用于处理断线后重复制情况:当从服务器在断线后重新连上主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据更新至主服务器当前所处的状态。

  • PSYNC命令的部分重同步解决了旧版复制功能在处理断线后重复复制时出现的低效情况。



部分重同步的实现

  • 要实现部分重同步,必须解决以下三个问题:
    1.当前主从服务器各复制了多少数据??
    2.如果主从服务器断线以后,主服务器新接收到的命令请求,该如何处理?
    3.如果在一个集群系统中,如何找到上一次复制的那个主服务器呢?

部分重同步功能由以下三个部分构成:
  a.主服务器的复制偏移量和从服务器的复制偏移量
  b.主服务器的复制积压缓冲区
  c.服务器的运行ID

t/*
 * 客户端结构
 *
 * 为每个连接到服务器的客户端保存维持一个该结构的映射,
 * 从而实现多路复用。
 */
typedef struct redisClient {

    // socket 文件描述符
    int fd;

    // 指向当前目标数据库的指针
    redisDb *db;

    // 当前目标数据库的号码
    int dictid;

    // 查询缓存
    sds querybuf;
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */

    // 参数的个数
    int argc;

    // 字符串表示的命令,以及命令的参数
    robj **argv;

    // 命令,以及上个命令
    struct redisCommand *cmd, *lastcmd;

    // 回复类型
    int reqtype;
    int multibulklen;       /* number of multi bulk arguments left to read */
    long bulklen;           /* length of bulk argument in multi bulk request */

    // 保存回复的链表
    list *reply;
    // 链表中保存的所有回复的总字节大小
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */

    // 统计数据
    int sentlen;
    time_t ctime;           /* Client creation time */
    time_t lastinteraction; /* time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */

    // 复制功能相关
    int slaveseldb;         /* slave selected db, if this client is a slave */
    int authenticated;      /* when requirepass is non-NULL */
    // 客户端当前的同步状态
    int replstate;          /* replication state if this is a slave */
    // 同步数据库的文件描述符
    int repldbfd;           /* replication DB file descriptor */
    // 同步数据库文件的偏移量
    long repldboff;         /* replication DB file offset */
    // 同步数据库文件的大小
    off_t repldbsize;       /* replication DB file size */
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */

    // 事务实现
    multiState mstate;      /* MULTI/EXEC state */

    // 阻塞状态
    blockingState bpop;   /* blocking state */
    list *io_keys;          /* Keys this client is waiting to be loaded from the
                             * swap file in order to continue. */

    // 被监视的 KEY
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */

    // 订阅与发布
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */

    /* Response buffer */
    // 回复缓存的当前缓存
    int bufpos;
    // 回复缓存,可以保存多个回复
    char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;

复制偏移量

执行复制的双方---主从服务器都会维护一个复制偏移量。
主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
从服务器每次接收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量加上N。
通过对比主从服务器的复制偏移量,程序很容易知道主从服务器是否处于一致状态。

  • 主从状态一致: 


  • 主从状态不一致:



    假如从服务器A在断线后就立即重新连接主服务器,并且成功,那么接下来,从服务器将向主服务器发送PSYNC命令,报告从服务器A当前的复制偏移量为10086,那么这时,主服务器应该对从服务器执行完全重同步还是部分重同步?如果执行部分重同步的话,主服务器又如何补偿从服务器A在断线期间丢失的那部分数据呢?

复制积压区

复制积压区是由主服务器维护的一个固定长度的队列,默认大小为1M。
当主服务器进行命令传播时,它不仅将写命令发送给所有从服务器,还会将写命令入列到复制积压区缓冲区里面。
因此,主服务器的复制积压区里面会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量。


当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对主服务器进行何种复制操作:
1.如果offset偏移量之后的数据,仍然存在于复制积压区里面,那么主服务器将对从服务器执行部分重同步操作。
2.如果offset偏移量之后的数据,不在复制积压区里面,那么主服务器将会对从服务器进行完全重同步操作。

  • 服务器允许ID
    每个Redis服务器,不论是主服务器还是从服务器都会有自己的运行ID。这个ID在服务器启动时自动生成,由40个随机十六进制字符组成。
    当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器会将这个运行ID保存起来。
    当从服务器断线并重连上一个主服务器时,从服务器将向当前连接的主服务器发送自己的之前保存的运行ID:
    1.如果ID一致,说明短线后重连的就是之前连接的服务器;
    2.如果ID不一致,说明短信后重连的不是之前链接的服务器,那么主服务器将对从服务器进行完整重同步操作。

源码解析

/* Slave replication state - slave side */
#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 */
#define REDIS_REPL_TRANSFER 3 /* Receiving .rdb from master */
#define REDIS_REPL_CONNECTED 4 /* Connected to master */
初始化时设置
server.replstate = REDIS_REPL_CONNECT
即slave需要连接master
slave周期性调用replicationCron,查看slave状态:

void replicationCron(void) {
    /*判断是否IO超时*/
    if (server.masterhost && server.replstate == REDIS_REPL_TRANSFER &&
        (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
    {
        redisLog(REDIS_WARNING,"Timeout receiving bulk data from MASTER...");
        replicationAbortSyncTransfer(); //终止连接,并设置server.replstate = REDIS_REPL_CONNECT;
    }

    /* Timed out master when we are an already connected slave? */
    if (server.masterhost && server.replstate == REDIS_REPL_CONNECTED &&
        (time(NULL)-server.master->lastinteraction) > server.repl_timeout)
    {
        redisLog(REDIS_WARNING,"MASTER time out: no data nor PING received...");
        freeClient(server.master);
    }

    /* Check if we should connect to a MASTER */
    if (server.replstate == REDIS_REPL_CONNECT) {
        redisLog(REDIS_NOTICE,"Connecting to MASTER...");
        if (connectWithMaster() == REDIS_OK) { //连接master
            redisLog(REDIS_NOTICE,"MASTER <-> SLAVE sync started");
        }
    }
    
    /* If we have attached slaves, PING them from time to time.
     * So slaves can implement an explicit timeout to masters, and will
     * be able to detect a link disconnection even if the TCP connection
     * will not actually go down. */
    if (!(server.cronloops % (server.repl_ping_slave_period*10))) {
        listIter li;
        listNode *ln;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = ln->value;

            /* Don't ping slaves that are in the middle of a bulk transfer
             * with the master for first synchronization. */
            if (slave->replstate == REDIS_REPL_SEND_BULK) continue;
            if (slave->replstate == REDIS_REPL_ONLINE) {
                /* If the slave is online send a normal ping */
                addReplySds(slave,sdsnew("PING\r\n"));
            } else {
                /* Otherwise we are in the pre-synchronization stage.
                 * Just a newline will do the work of refreshing the
                 * connection last interaction time, and at the same time
                 * we'll be sure that being a single char there are no
                 * short-write problems. */
                if (write(slave->fd, "\n", 1) == -1) {
                    /* Don't worry, it's just a ping. */
                }
            }
        }
    }
}

当server.replstate == REDIS_REPL_CONNECT时,slave连接master,连接成功后,slave执行syncWithMaster函数,syncWithMaster将向master发送SYNC命令

int connectWithMaster(void) {
    int 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;
    }

    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_s = fd;
    server.replstate = REDIS_REPL_CONNECTING;
    return REDIS_OK;
}

master端:
master对于slave的连接和client的连接统一处理,在接收到slave发出的SYNC命令后,执行syncCommand,syncCommand 将查看当前状态,如果正在做快照,则等待,否则启动后台进程做快照。

void syncCommand(redisClient *c) {
    /* ignore SYNC if aleady slave or in monitor mode */
    if (c->flags & REDIS_SLAVE) return;

    /* Refuse SYNC requests if we are a slave but the link with our master
     * is not ok... */
    if (server.masterhost && server.replstate != REDIS_REPL_CONNECTED) {
        addReplyError(c,"Can't SYNC while not connected with my master");
        return;
    }

    /* SYNC can't be issued when the server has pending data to send to
     * the client about already issued commands. We need a fresh reply
     * buffer registering the differences between the BGSAVE and the current
     * dataset, so that we can copy to other slaves if needed. */
    if (listLength(c->reply) != 0) {
        addReplyError(c,"SYNC is invalid with pending input");
        return;
    }

    redisLog(REDIS_NOTICE,"Slave ask for synchronization");
    /* Here we need to check if there is a background saving operation
     * in progress, or if it is required to start one */
    if (server.bgsavechildpid != -1) {
       .....
    } else {
        /* Ok we don't have a BGSAVE in progress, let's start one */
        redisLog(REDIS_NOTICE,"Starting BGSAVE for SYNC");
        if (rdbSaveBackground(server.dbfilename) != REDIS_OK) {
            redisLog(REDIS_NOTICE,"Replication failed, can't BGSAVE");
            addReplyError(c,"Unable to perform background save");
            return;
        }
        c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
    }
    c->repldbfd = -1;
    c->flags |= REDIS_SLAVE;
    c->slaveseldb = 0;
    listAddNodeTail(server.slaves,c);
    return;
}

在完成快照后,执行updateSlavesWaitingBgsave函数,updateSlavesWaitingBgsave将查看当前master的各个slave的状态,如果发现有在等待bgsave完成的,则注册事件sendBulkToSlave,sendBulkToSlave将快照文件发送给slave

void updateSlavesWaitingBgsave(int bgsaveerr) {
    listNode *ln;
    int startbgsave = 0;
    listIter li;

    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
        redisClient *slave = ln->value;

        if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) {
            startbgsave = 1;
            slave->replstate = REDIS_REPL_WAIT_BGSAVE_END;
        } else if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) {
            struct redis_stat buf;

            if (bgsaveerr != REDIS_OK) {
                freeClient(slave);
                redisLog(REDIS_WARNING,"SYNC failed. BGSAVE child returned an error");
                continue;
            }
            if ((slave->repldbfd = open(server.dbfilename,O_RDONLY)) == -1 ||
                redis_fstat(slave->repldbfd,&buf) == -1) {
                freeClient(slave);
                redisLog(REDIS_WARNING,"SYNC failed. Can't open/stat DB after BGSAVE: %s", strerror(errno));
                continue;
            }
            slave->repldboff = 0;
            slave->repldbsize = buf.st_size;
            slave->replstate = REDIS_REPL_SEND_BULK;
            aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE);
            if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave) == AE_ERR) {
                freeClient(slave);
                continue;
            }
        }
    }
    if (startbgsave) {
        if (rdbSaveBackground(server.dbfilename) != REDIS_OK) {
            listIter li;

            listRewind(server.slaves,&li);
            redisLog(REDIS_WARNING,"SYNC failed. BGSAVE failed");
            while((ln = listNext(&li))) {
                redisClient *slave = ln->value;

                if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START)
                    freeClient(slave);
            }
        }
    }
}

在slave完成第一次的同步后,后续如果master接收到改变db状态的命令,则调用replicationFeedSlaves将相应变更发送slave

/* Call() is the core of Redis execution of a command */
void call(redisClient *c) {
    long long dirty, start = ustime(), duration;

    dirty = server.dirty;
    c->cmd->proc(c);
    dirty = server.dirty-dirty;
    duration = ustime()-start;
    slowlogPushEntryIfNeeded(c->argv,c->argc,duration);

    if (server.appendonly && dirty > 0)
        feedAppendOnlyFile(c->cmd,c->db->id,c->argv,c->argc);
    if ((dirty > 0 || c->cmd->flags & REDIS_CMD_FORCE_REPLICATION) &&
        listLength(server.slaves))
        replicationFeedSlaves(server.slaves,c->db->id,c->argv,c->argc);
    if (listLength(server.monitors))
        replicationFeedMonitors(server.monitors,c->db->id,c->argv,c->argc);
    server.stat_numcommands++;
}

总结:

  1. redis主从复制,并没有增加太多额外代码,但是功能强大,支持多个slave,并且支持slave作为master。
  2. redis虽然宣称主从复制无阻塞,但是,由于redis使用单线程服务,而和slave的交互由处理线程统一处理,因此,对性能有影响。在slave第一次和master做同步时,如果master快照文件较大,则快照文件的传输将耗费较长时间,文件传输过程中master无法提供服务。

你可能感兴趣的:(「Redis源码解读」—多机数据库(一)主从)