首页 > Redis > Redis源码学习-AOF数据持久化原理分析(1)

Redis源码学习-AOF数据持久化原理分析(1)

2013年6月6日 发表评论 阅读评论 10055次阅读    

继上面一篇文章“Redis源码学习-AOF数据持久化原理分析(0)”,介绍了Redis是如何将不断从客户端发送过来的数据持久化到AOF文件中的。这里介绍一下AOF rewrite是如何工作的,也就是redis是如何将AOF文件初始化,并后台自动AOF rewrite的。

Redis的AOF会导致AOF文件不断的增大,因为他是基于命令重放而存储的,也就是将客户端发送的数据全部存储下来,没有坐合并,所以会哟u这个情况出现:客户端发送100条set kulv  studyhard指令,那么aof文件中将会存储100条这样的命令,而不是1条。

因此问题很明显了,aof文件会不断疯长,尽管存储了很多重复数据。那怎么解决呢?AOF rewrite。也就是定时的或者满足一定条件的时候,重新根据当前的内存快照生成一份AOF文件,覆盖掉原来的文件。相当于用这种方式来不断的整理aof文件似的,一个效果。

Redis里面触发AOF rewrite 的时机有2个:

  1. 用户设置“config set appendonly yes”开启AOF的时候调用一次;
  2. 用户设置“bgrewriteaof”命令的时候,如果当前没有aof/rdb进程在持久化数据,则调用一次;
  3. 如果用户设置了auto-aof-rewrite-percentage和auto-aof-rewrite-min-size指令,且aof文件增长到min-size以上,并且增长率大于percentage的时候,自动触发AOF rewrite.

如果上述指令发送的时候,当前已经有进程在处理这个动作了,那么redis会设置server.aof_rewrite_scheduled标志。然后在serverCron定时任务里面就会判断这种情况,从而再调用rewriteAppendOnlyFileBackground()。

下面从第一种情况开始介绍Redis AOF rewrite机制。

0、指令打开AOF Rewrite(appendonly yes

用户发送“config set appendonly yes”命令的时候,redis的处理函数为startAppendOnly()。初始化的时候如果配置文件里面指定了这个选项为打开状态,当然就会自动从一开始就是有AOF机制的,这种情况下不能发送这个命令,否则redis会直接死掉····太霸道了···这么果断····

来看看startAppendOnly这个处理函数:

首先APPEND打开server.aof_fd = open(server.aof_filename···)aof文件,前提是调用者知道aof是关闭的。

然后就调用rewriteAppendOnlyFileBackground()进行内存数据快照的处理。

然后设置server.aof_state = REDIS_AOF_WAIT_REWRITE;这样在AOF快照保存完后,主进程会把server.aof_state切换为REDIS_AOF_ON打开状态。

/* Called when the user switches from "appendonly no" to "appendonly yes"
 * at runtime using the CONFIG command. */
int startAppendOnly(void) {
	//当用户敲入这个命令时会调用这里: config set appendonly yes
	//调用顺序为configCommand->configSetCommand->startAppendOnly
	//如果之前已经打开过了,这里又再一次打开···不好的,
	//因为configSetCommand里面会判断之前是什么状态的
    server.aof_last_fsync = server.unixtime;
    server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
    redisAssert(server.aof_state == REDIS_AOF_OFF);
    if (server.aof_fd == -1) {
        redisLog(REDIS_WARNING,"Redis needs to enable the AOF but can't open the append only file: %s",strerror(errno));
        return REDIS_ERR;
    }
	//fork创建进程,也就是快照,将数据写入磁盘,同时主进程会将这期间写入的数据
	//放到临时的aof_buf里面,然后等自进程写完快照后,追加到后面
    if (rewriteAppendOnlyFileBackground() == REDIS_ERR) {
        close(server.aof_fd);
        redisLog(REDIS_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
        return REDIS_ERR;
    }
    /* We correctly switched on AOF, now wait for the rerwite to be complete
     * in order to append data on disk. */

    //下面这条语句,会让Redis写完AOF快照后,继续追加数据到AOF文件。
    server.aof_state = REDIS_AOF_WAIT_REWRITE;
    return REDIS_OK;
}

真正的快照保存在rewriteAppendOnlyFileBackground函数中,我们可以先想一下,对于一个高速运转的Redis服务来说,怎么才能把其某个时刻的数据全部原原本本的,一致的保存起来?想到几种方法:

  1. 应用程序自己做快照,比如copy一份数据出来,这个的缺点是需要加锁,copy的过程中无法支持写入操作,会导致逻辑“卡住”;
  2. 为了避免第一种情况的卡,应用代码中实现COW(Copy-on-write)的机制,这样效率会更高,没有修改的数据不会导致卡顿,但实现难度也相对大很多。
  3. 写时重定向(Redirect-on-write),将新写入的数据写到其他地方,后续再同步回来,这样也可以支持,实际上redis的AOF某些方面也借签了这个。
  4. Split-Mirror技术,这个比较麻烦,需要硬件和软件支持,一般在存储系统中应用。

关于数据快照,有几篇论文不错,这里不多介绍了:

  1. IBM: Understanding and exploiting snapshot technology for data protection, Part 1: Snapshot technology overview
  2. MS: How Database Snapshots Work
  3. MS:Split-Mirror Backup and Restore

Redis采用的方式是比较巧妙的COW写时复制技术,其利用fork()进程的原理,对当前整个程序做个快照,新建一个一模一样的进程,其底层其实就是COW。回到rewriteAppendOnlyFileBackground函数,函数主要就2个分支,从fork()函数分开。

0、子进程/快照进程分支:

当前的主进程运行fork()语句,创建一个子进程,子进程一开始立马就将监听端口关闭,这样就不会接收新连接了。然后调用rewriteAppendOnlyFile函数,将数据写到临时文件"temp-rewriteaof-bg-%d.aof"中去。

/* This is how rewriting of the append only file in background works:
 *
 * 1) The user calls BGREWRITEAOF
 * 2) Redis calls this function, that forks():
 *    2a) the child rewrite the append only file in a temp file.
 *    2b) the parent accumulates differences in server.aof_rewrite_buf.
 * 3) When the child finished '2a' exists.
 * 4) The parent will trap the exit code, if it's OK, will append the
 *    data accumulated into server.aof_rewrite_buf into the temp file, and
 *    finally will rename(2) the temp file in the actual file name.
 *    The the new file is reopened as the new append only file. Profit!
 */
int rewriteAppendOnlyFileBackground(void) {
//serverCron或者startAppendOnly调用这里,准备将数据写到AOF文件里面去。
    pid_t childpid;
    long long start;

    if (server.aof_child_pid != -1) return REDIS_ERR;
    start = ustime();
    if ((childpid = fork()) == 0) {
        char tmpfile[256];

        /* Child */
		//子进程在此,这里相当于给进程的内存建立了一个快照,关闭监听客户端连接的fd
        if (server.ipfd > 0) close(server.ipfd);
        if (server.sofd > 0) close(server.sofd);
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {
			//得到本进程的脏虚拟页面大小。
            size_t private_dirty = zmalloc_get_private_dirty();

            if (private_dirty) {
                redisLog(REDIS_NOTICE,
                    "AOF rewrite: %lu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
			//调用_exit()关闭子进程,注意必须调用_exit,而不是exit。后者会刷文件,删除临时文件等等的。
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }

1、父进程/工作进程分支:

主进程的工作在这里比较简单,只需要清空server.aof_rewrite_scheduled标志,避免下次serverCron函数又进行AOF rewrite,然后记录子进程的pid为server.aof_child_pid ,然后调用updateDictResizePolicy,这个updateDictResizePolicy函数里面会考虑如果当前正在后台有快照进程在写数据,那他不会对字典进行resize,这样能够避免COW机制被打乱,导致大量的COW触发,分配很多内存。

    } else {//父进程设置一些标志,记录子进程pid,然后返回。
        /* Parent */
        server.stat_fork_time = ustime()-start;
        if (childpid == -1) {
            redisLog(REDIS_WARNING,
                "Can't rewrite append only file in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        redisLog(REDIS_NOTICE,
            "Background append only file rewriting started by pid %d",childpid);
        server.aof_rewrite_scheduled = 0;
        server.aof_rewrite_time_start = time(NULL);
        server.aof_child_pid = childpid;
        updateDictResizePolicy();
        /* We set appendseldb to -1 in order to force the next call to the
         * feedAppendOnlyFile() to issue a SELECT command, so the differences
         * accumulated by the parent into server.aof_rewrite_buf will start
         * with a SELECT statement and it will be safe to merge. */
        server.aof_selected_db = -1;
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

下面继续介绍rewriteAppendOnlyFile函数,这个函数会遍历所有的字典K-V,将其写入磁盘,然后fsync刷新操作系统缓存。

函数首先打开一个临时文件"temp-rewriteaof-%d.aof",然后循环每一个db,也就是server.dbnum,一个个将DB的数据,具体的数据格式就是将当前内存的数据还原成跟客户端的协议格式,文本形式然后写入到文件中,具体的代码不是这里的重点,下面简单看一下代码:

/* Write a sequence of commands able to fully rebuild the dataset into
 * "filename". Used both by REWRITEAOF and BGREWRITEAOF.
 *
 * In order to minimize the number of commands needed in the rewritten
 * log Redis uses variadic commands when possible, such as RPUSH, SADD
 * and ZADD. However at max REDIS_AOF_REWRITE_ITEMS_PER_CMD items per time
 * are inserted using a single command. */
int rewriteAppendOnlyFile(char *filename) {
//rewriteAppendOnlyFileBackground调用这里,将文件写入aof文件里面去。
    dictIterator *di = NULL;
    dictEntry *de;
    rio aof;
    FILE *fp;
    char tmpfile[256];
    int j;
    long long now = mstime();

    /* Note that we have to use a different temp name here compared to the
     * one used by rewriteAppendOnlyFileBackground() function. */
    snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
        return REDIS_ERR;
    }
	//设置rioFileIO等信息
    rioInitWithFile(&aof,fp);
    if (server.aof_rewrite_incremental_fsync)//设置r->io.file.autosync = bytes;每32M刷新一次。
        rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);
    for (j = 0; j < server.dbnum; j++) {//遍历每一个db.将其内容写入磁盘。
        char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";
        redisDb *db = server.db+j;
        dict *d = db->dict;//找到这个db的key字典
        if (dictSize(d) == 0) continue;
        di = dictGetSafeIterator(d);
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }

        /* SELECT the new DB */
		//写入select,后面写入当前所指的db序号。这样就写入: SELECT db_id
        if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
        if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;

        /* Iterate this DB writing every entry */
        while((de = dictNext(di)) != NULL) {//一个个遍历这个字典的所有key,将其写到AOF文件里面去。
            sds keystr;
            robj key, *o;
            long long expiretime;

            keystr = dictGetKey(de);
            o = dictGetVal(de);
            initStaticStringObject(key,keystr);//初始化一个字符串对象。

            expiretime = getExpire(db,&key);//获取超时时间。

            /* Save the key and associated value */
            if (o->type == REDIS_STRING) {
				//插入KV赋值语句: set keystr valuestr
                /* Emit a SET command */
                char cmd[]="*3\r\n$3\r\nSET\r\n";
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                /* Key and value */
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkObject(&aof,o) == 0) goto werr;
            } else if (o->type == REDIS_LIST) {
                if (rewriteListObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_SET) {
                if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_ZSET) {
                if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_HASH) {
                if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
            } else {
                redisPanic("Unknown object type");
            }
            /* Save the expire time */
            if (expiretime != -1) {
                char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
                /* If this key is already expired skip it */
                if (expiretime < now) continue;
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
            }
        }
        dictReleaseIterator(di);
    }

值得注意的是,这里的写文件是用标准库写入的,为什么呢?缓存,能够充分利用标准库的缓存机制,这样不用每次调用都调用系统调用。比如rioWrite()->rioFileWrite()->fwrite()。

如果用户配置了“aof-rewrite-incremental-fsync on”,则表示要fwrite写一部分数据后就调用fsync刷一下数据到磁盘,这里不用调用fflush,为什么呢?因为其的缓存不会特别大?也许吧,其实调用也可以的。 这里会每fwrite 32M(REDIS_AOF_AUTOSYNC_BYTES宏)数据后,就显示调用一次fsync,保证数据写入正确。

/* Returns 1 or 0 for success/failure. */
static size_t rioFileWrite(rio *r, const void *buf, size_t len) {
    size_t retval;
//调用fwrite带缓存的写入数据
    retval = fwrite(buf,len,1,r->io.file.fp);
    r->io.file.buffered += len;

    if (r->io.file.autosync &&
        r->io.file.buffered >= r->io.file.autosync)
    {//如果写入的数据大于32M 了,那就fsync从内核刷到磁盘上。
   //注意这里是fsync,不是fflush刷新,那么一个问题来了:
   //那如果还有数据在标准库的缓存里面,那么调用fsync就会漏掉这部分,
   //而直接将操作系统缓存的数据刷到磁盘上。
        aof_fsync(fileno(r->io.file.fp));
        r->io.file.buffered = 0;
    }
    return retval;
}

rewriteAppendOnlyFile的for循环处理完每一个DB后,也就是数据写入完成了,为了保证安全,需要调用fflush将数据从标准库缓存刷到文件系统缓存中,然后调用fsync刷到磁盘上面,之后才close。文件关闭后,做个rename改为调用者设置的文件名,也就是rewriteAppendOnlyFileBackground设置的文件名:"temp-rewriteaof-bg-%d.aof"。

    /* Make sure data will not remain on the OS's output buffers */
    fflush(fp);//将缓存从标准库刷新到操作系统缓存
    aof_fsync(fileno(fp));//从操作系统缓存刷到磁盘去。
    fclose(fp);

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    if (rename(tmpfile,filename) == -1) {
		//重命名为上层设置的文件名。形如"temp-rewriteaof-bg-%d.aof"
        redisLog(REDIS_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    redisLog(REDIS_NOTICE,"SYNC append only file rewrite performed");
    return REDIS_OK;
}

我们知道,rewriteAppendOnlyFile将快照进程的数据写到磁盘里面去之后,关闭文件,然后退出。另外我们从前一篇文章“Redis源码学习-AOF数据持久化原理分析(0)”中可以看出,AOF文件建立后,如果打开了aof开关,那么后续的请求命令都会追加到aof文件中的。那问题来了,快照文件生成过程中收到的命令是怎么保证写入到AOF文件的呢?答案是:保存这段时间的Diff数据,等待快照进程写完快照后,主进程将其写入到AOF文件末尾。

回到第一篇文章后面的feedAppendOnlyFile函数,其末尾还有几行语句我们没有介绍,这个函数调用的时机是call()-》propagate()-》feedAppendOnlyFile(),也就是redis每处理一条条指令,都会调用这个函数。函数末尾判断如果server.aof_child_pid不为1,那就说明有快照进程正在写数据到临时文件,那么这就说明,我们必须将这段时间接收到的指令先暂时存储起来,等到快照进程完成任务后,将这部分数据写入到AOF文件末尾,保证数据不丢。

//如果有子进程在倒数据到AOF文件里面,那么其写入的肯定是之前的快照,fork的。所以这里需要记住
//这些改动的指令,以备子进程写完后,主进程能将这期间的数据追加到AOF文件后面。
//将s指令放到server.aof_rewrite_buf_blocks的后面,等子进程写完他的快照后,
    /* If a background append only file rewriting is in progress we want to
     * accumulate the differences between the child DB and the current one
     * in a buffer, so that when the child process will do its work we
     * can append the differences to the new append only file. */
//这里,因为AOF都已经关闭了,那么这里其实没有必要再存储DIFF数据了。
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
/*这里说一下server.aof_buf和server.aof_rewrite_buf_blocks的区别
aof_buf是正常情况下aof文件打开的时候,会不断将这份数据写入到AOF文件中。
aof_rewrite_buf_blocks 是如果用户主动触发了写AOF文件的命令时,比如 config set appendonly yes命令
	那么redis会fork创建一个后台进程,也就是当时的数据快照,然后将数据写入到一个临时文件中去。
	在此期间发送的命令,我们需要把它们记录起来,等后台进程完成AOF临时文件写后,serverCron定时任务
	感知到这个退出动作,然后就会调用backgroundRewriteDoneHandler进而调用aofRewriteBufferWrite函数,
	将aof_rewrite_buf_blocks上面的数据,也就是diff数据写入到临时AOF文件中,然后再unlink替换正常的AOF文件。

因此可以知道,aof_buf一般情况下比aof_rewrite_buf_blocks要少,
但开始的时候可能aof_buf包含一些后者不包含的前面部分数据。

*/
    sdsfree(buf);
}

上面是保存快照期间的DIFF数据,那么写入呢?是在定时任务serverCron中完成的。这个函数顶是每隔一毫秒调用。这是initServer函数调用如下命令设置的每毫秒定时器:aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL)。
serverCron函数比较长,跟我们相关的就一个if-else分支,条件是是否有快照进程在做AOF rewrite操作。
如果有快照进程或rdb进程在刷快照到磁盘,那么wait3()看一下是否结束,如果结束就做响应的扫尾工作;
如果没有,那就判断一下是否达到save指令的阈值,或者AOF文件增长超过指定的大小和百分比,如果超过,则触发RDB或者触发后台AOF rewrite,以备保存数据或者做AOF文件整理,缩减。
第二部分比较简单,AOF文件增长的最大百分比是auto-aof-rewrite-percentage指令设置的。看下面的代码。

  } else {
        /* If there is not a background saving/rewrite in progress check if
         * we have to save/rewrite now */
         for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;

            /* Save if we reached the given amount of changes,
             * the given amount of seconds, and if the latest bgsave was
             * successful or if, in case of an error, at least
             * REDIS_BGSAVE_RETRY_DELAY seconds already elapsed. */
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                (server.unixtime-server.lastbgsave_try >
                 REDIS_BGSAVE_RETRY_DELAY ||
                 server.lastbgsave_status == REDIS_OK))
            {
                redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
                    sp->changes, sp->seconds);
                rdbSaveBackground(server.rdb_filename);
                break;
            }
         }

         /* Trigger an AOF rewrite if needed */
         if (server.rdb_child_pid == -1 &&
             server.aof_child_pid == -1 &&
             server.aof_rewrite_perc &&
             server.aof_current_size > server.aof_rewrite_min_size)
         {
            long long base = server.aof_rewrite_base_size ? server.aof_rewrite_base_size : 1;
            long long growth = (server.aof_current_size*100/base) - 100;
			//如果AOF文件增长超过了指定百分比,那么需要自动rewrite aof文件了
			//AOF文件增长的最大百分比是auto-aof-rewrite-percentage指令设置的。
            if (growth >= server.aof_rewrite_perc) {
                redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                rewriteAppendOnlyFileBackground();
            }
         }
    }

重点看第一部分,也就是判断后台快照进程是否完成工作,如果完成了,就进行扫尾工作,比如将DIFF数据写入文件。

    /* Check if a background saving or AOF rewrite in progress terminated. */
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
        int statloc;
        pid_t pid;

        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
            int exitcode = WEXITSTATUS(statloc);
            int bysignal = 0;

            if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);

            if (pid == server.rdb_child_pid) {
				//把数据保存到磁盘上去,跟AOF的区别是AOF会不断的追加改动到文件。
				//RDB只会将快照保存,并且通知其他slave
                backgroundSaveDoneHandler(exitcode,bysignal);
            } else if (pid == server.aof_child_pid) {
            //退出的进程的pid为aof日志的进程,也就是在rewriteAppendOnlyFileBackground这里fork创建的进程
            //用户敲入这样的命令可以出发AOF文件重写 config set appendonly yes
            //从而在定时任务中检测到AOF进程已经写完快照并退出,从而下面必须写在此期间写入的数据到文件。
                backgroundRewriteDoneHandler(exitcode,bysignal);
            } else {
                redisLog(REDIS_WARNING,
                    "Warning, detected child with unmatched pid: %ld",
                    (long)pid);
            }
            updateDictResizePolicy();
        }
    } else {

从上面的注释可以看出,如果aof后台快照进程已经完成工作并退出,那么就会调用backgroundRewriteDoneHandler函数。
这个函数完成DIFF数据的追加,并且重命名临时文件为正常的aof文件。并且保证不会阻塞主进程。其大体执行流程为:

  1. 打开后台AOF快照进程写入数据的临时文件"temp-rewriteaof-bg-%d.aof"。
  2. 调用aofRewriteBufferWrite将快照保存期间的DIFF数据追加到临时文件中去。
  3. 如果之前AOF是关闭的,那么open打开正常的aof文件server.aof_filename,放到oldfd上;
  4. 将临时文件重命名为正常文件,由于老的目的文件名文件已经open了,不管是否打开了AOF,所以这个rename操作不好导致内核unlink 老文件,也就不会阻塞主进程;
  5. 如果之前AOF是关闭的,那么close(newfd);这里也不会阻塞,因为之前是关闭的,那么aof_rewrite_buf_blocks上面肯定没有数据,所以肯定不会写入文件,那么close是不会阻塞的。
  6. 如果之前AOF是打开的,那么老文件不能主进程close,否则会阻塞,因此需要考虑把这个关闭动作放后台线程做,如果是AOF_FSYNC_EVERYSEC模式的话。
  7. 统计aof文件大小,更新server.aof_rewrite_base_size ,这样下次serverCron可用来判断文件增长率。
  8. 如果之前是REDIS_AOF_WAIT_REWRITE状态,那么切换server.aof_state为REDIS_AOF_ON状态,因为只有“config set appendonly yes”指令才会设置这个状态,也就是需要写完快照后,立即打开AOF。
  9. 如果oldfd也就是老文件是打开的,那么需要放到后台去close,注意这个close会导致文件被unlink,这是因为我们上面做了个rename将这个文件干掉了,只是当时由于refcount 不为0所以没有unlink。

下面一步步看看代码。

上面第1,2步比较简单,具体看代码中的注释:

/* A background append only file rewriting (BGREWRITEAOF) terminated its work.
 * Handle this. */
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
//serverCron定时任务在判断如果aof_child_pid不为-1的时候,会调用wait3(&statloc,WNOHANG,NULL)
//看看退出的进程是谁,如果是AOF快照进程,那么就意味着刚才开始的AOF文件刷新的进程已经完成快照的数据
//写入AOF文件,下面我们需要将在此期间修改的数据,也就是命令,写到AOF后面。
//rewriteAppendOnlyFileBackground创建的进程快照。
    if (!bysignal && exitcode == 0) {
	//AOF进程正常退出。如果非正常就至少打条日志
        int newfd, oldfd;
        char tmpfile[256];
        long long now = ustime();

        redisLog(REDIS_NOTICE,
            "Background AOF rewrite terminated with success");

        /* Flush the differences accumulated by the parent to the
         * rewritten AOF. */
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof",(int)server.aof_child_pid);
        newfd = open(tmpfile,O_WRONLY|O_APPEND);
        if (newfd == -1) {
            redisLog(REDIS_WARNING, "Unable to open the temporary AOF produced by the child: %s", strerror(errno));
            goto cleanup;
        }
		//将aof_rewrite_buf_blocks上面的一块块数据写入到fd代表的文件中。
		//关于怎么把DIFF数据放到上面的列表块中的,参考feedAppendOnlyFile。
		//如果AOF是关闭的,那么这里不会写入任何数据,因为stopAppendOnly会清空这个列表。
		//并且清除REDIS_AOF_WAIT_REWRITE标志为OFF,这样feedAppendOnlyFile函数也进不去,也就不会写数据到这个列表中。
        if (aofRewriteBufferWrite(newfd) == -1) {
            redisLog(REDIS_WARNING, "Error trying to flush the parent diff to the rewritten AOF: %s", strerror(errno));
            close(newfd);
            goto cleanup;
        }

关于Redis下面的动作,也就是rename,为了避免主进程被block,代码中做了一些判断,下面看看作者细心的注释吧先:

/* The only remaining thing to do is to rename the temporary file to
* the configured file and switch the file descriptor used to do AOF
* writes. We don't want close(2) or rename(2) calls to block the
* server on old file deletion.
*
* There are two possible scenarios:
*
* 1) AOF is DISABLED and this was a one time rewrite. The temporary
* file will be renamed to the configured file. When this file already
* exists, it will be unlinked, which may block the server.
*
* 2) AOF is ENABLED and the rewritten AOF will immediately start
* receiving writes. After the temporary file is renamed to the
* configured file, the original AOF file descriptor will be closed.
* Since this will be the last reference to that file, closing it
* causes the underlying file to be unlinked, which may block the
* server.
*
* To mitigate the blocking effect of the unlink operation (either
* caused by rename(2) in scenario 1, or by close(2) in scenario 2), we
* use a background thread to take care of this. First, we
* make scenario 1 identical to scenario 2 by opening the target file
* when it exists. The unlink operation after the rename(2) will then
* be executed upon calling close(2) for its descriptor. Everything to
* guarantee atomicity for this switch has already happened by then, so
* we don't care what the outcome or duration of that close operation
* is, as long as the file descriptor is released again. */

看看代码实现,注意下面的rename不会造成老文件的unlink,所以不会阻塞主进程的。

        if (server.aof_fd == -1) {
            /* AOF disabled */

             /* Don't care if this fails: oldfd will be -1 and we handle that.
              * One notable case of -1 return is if the old file does
              * not exist. */
             oldfd = open(server.aof_filename,O_RDONLY|O_NONBLOCK);
        } else {
            /* AOF enabled */
            oldfd = -1; /* We'll set this to the current AOF filedes later. */
        }

        /* Rename the temporary file. This will not unlink the target file if
         * it exists, because we reference it with "oldfd". */
        if (rename(tmpfile,server.aof_filename) == -1) {
			//int rename ( const char * oldname, const char * newname );
			//把临时文件改名为正常的AOF文件名。由于当前oldfd已经指向这个之前的正常文件名的文件,
			//所以当前不会造成unlink操作,得等那个oldfd被close的时候,内核判断该文件没有指向了,就删除之。
            redisLog(REDIS_WARNING,
                "Error trying to rename the temporary AOF file: %s", strerror(errno));
            close(newfd);
            if (oldfd != -1) close(oldfd);
            goto cleanup;
        }

接下来的事情就是关闭临时文件,或者切换aof文件句柄,如果之前的AOF是关闭的,那么server.aof_fd == -1,这个时候,第2步中aofRewriteBufferWrite函数肯定不会写入任何数据到临时文件里面去,那么直接close(newfd);是安全的。
如果之前AOF是打开的,那么我们就不能简单close了,因为已经写入了部分数据到临时文件中了。既然AOF打开,那我们其实压根不用close临时文件,直接用就行了。最多跟进fsync的模式不同,判断一下是否要显示fsync或者放后台进程去fsync操作。

        if (server.aof_fd == -1) {
            /* AOF disabled, we don't need to set the AOF file descriptor
             * to this new file, so we can close it. */
   //如果AOF关闭了,那只要处理这个文件了,那么,直接关闭这个新的文件,啥事都么有了。
   //但是这里会不会导致服务器卡呢?这个newfd应该是临时文件的最后一个fd了。
   //不会的,因为这个文件在本函数不会写入数据,因为stopAppendOnly函数会清空aof_rewrite_buf_blocks列表。
            close(newfd);
        } else {
            /* AOF enabled, replace the old fd with the new one. */
            oldfd = server.aof_fd;
			//指向新的fd,此时这个fd由于上面的rename语句存在,已经为正常aof文件名了。
			//切换newfd为server.aof_fd,这样后面的写入操作都会写到新的这个AOF文件中了。
            server.aof_fd = newfd;
            if (server.aof_fsync == AOF_FSYNC_ALWAYS)//配置了总是要FSYNC
                aof_fsync(newfd);
            else if (server.aof_fsync == AOF_FSYNC_EVERYSEC)//放后台线程去FSYNC
                aof_background_fsync(newfd);
            server.aof_selected_db = -1; /* Make sure SELECT is re-issued */
			//更新文件大小信息: server.aof_current_size = sb.st_size;
            aofUpdateCurrentSize();

			//设置文件大小,这样在serverCron里面就可以判断AOF文件的增长率了,如果增长率大于
			//config get auto-aof-rewrite-percentage 指令设置的值,那么就会进行后台自动AOF 重写。
            server.aof_rewrite_base_size = server.aof_current_size;

            /* Clear regular AOF buffer since its contents was just written to
             * the new AOF from the background rewrite buffer. */
            //由于当前这个aof_buf肯定是server.aof_rewrite_buf_blocks的后缀,所以不用写入文件了。
            sdsfree(server.aof_buf);
            server.aof_buf = sdsempty();
        }

完成这个动作后,剩下的代码就是打开aof,后台关闭老文件。

        server.aof_lastbgrewrite_status = REDIS_OK;

        redisLog(REDIS_NOTICE, "Background AOF rewrite finished successfully");
		//下面判断是否需要打开AOF,比如bgrewriteaofCommand就不需要打开AOF。
        /* Change state from WAIT_REWRITE to ON if needed */
        if (server.aof_state == REDIS_AOF_WAIT_REWRITE)
            server.aof_state = REDIS_AOF_ON;

        /* Asynchronously close the overwritten AOF. */
		//让后台线程去关闭这个旧的AOF文件FD,只要CLOSE就行,会自动unlink的,因为上面已经有rename了。
        if (oldfd != -1)
			bioCreateBackgroundJob(REDIS_BIO_CLOSE_FILE,(void*)(long)oldfd,NULL,NULL);
//```````
}

到这里,redis利用fork做数据快照已经刷到磁盘了,写盘期间的DIFF数据也刷到磁盘了。剩下的就是“实时”AOF了,也就是不断将客户端发送的命令追加到AOF文件中,也就是server.aof_fd句柄对应的文件中。当然,这样重复的命令,过期的命令等都会存在于AOF文件,也就是大部分来的指令都会存储在AOF中,不会做任何优化,除了快照时写入的数据。
等到文件大小超过配置的百分比后,Redis又会在serverCron函数中调用rewriteAppendOnlyFileBackground()开启AOF文件的Rewrite操作,这样又可以缩减文件大小,去掉重复的多余的指令。如此循环。

redis的数据持久化到这里写的差不多了,还剩下RDB这个没有讲,后续再分享吧。

如果想从概要上面而不是代码层面了解Redis的持久化,推荐看作者的这篇文章:“Redis Persistence” ,另外作者为了科普或者介绍redis的持久化,特意写了这篇解密文章"Redis persistence demystified"。

Share
分类: Redis 标签: , ,

注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。