redis源码阅读-持久化之aof与aof重写详解

Posted 5ycode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis源码阅读-持久化之aof与aof重写详解相关的知识,希望对你有一定的参考价值。

aof相关配置


aof-rewrite-incremental-fsync yes
# aof 开关,默认是关闭的,改为yes表示开启
appendonly no
# aof的文件名,默认
appendfilename "appendonly.aof"
# aof刷数据的策略,有no/everysec/aways
appendfsync everysec
no-appendfsync-on-rewrite no
# aof超出配置大小的比例,模式是100%,可以理解为阈值 
auto-aof-rewrite-percentage 100
# aof 配置的文件的大小,默认64mb
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
# rewrite 进行时候,rewrite 文件分两种格式,1. 先 用 rdb 序列化,序列化结果写入aof文件,然后期间积累的差异用追加aof命令格式 ,2 整个文件都是aof的命令追加格式
aof-use-rdb-preamble yes 

appendfsync 一共有3种策略

  • alays 主要有数据改动就把数据刷入磁盘,性能相对最差,但最安全
  • everysec 每隔1秒刷一次数据,redis默认的,也是redis推荐的
  • no 不主动刷,什么时候刷数据,取决于操作系统,大多数linux 30秒提交一次

aof写入:


在之前分析redis的流程的时候《redis源码阅读三-终于把主线任务执行搞明白了》里读取客户端信息的时候

在processCommand 函数里,解析出来执行命令,放入了client中

int processCommand(client *c) 
    //解析出来命令
	c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    //CMD_CALL_FULL 包含着CMD_CALL_SLOWLOG | CMD_CALL_STATS | CMD_CALL_PROPAGATE
    //CMD_CALL_PROPAGATE 包含着CMD_CALL_PROPAGATE_AOF|CMD_CALL_PROPAGATE_REPL
    call(c,CMD_CALL_FULL);

//命令执行
void call(client *c, int flags) 
    /**
     * dirty 记录修改次数
     * start记录命令开始执行时间us
     * duration记录命令执行花费时间
     */
    long long dirty;
    ustime_t start, duration;
    int client_old_flags = c->flags;
    struct redisCommand *real_cmd = c->cmd;
    //从server.dirty中获取
    dirty = server.dirty;
    //命令执行,会执行对应的redisCommand
    c->cmd->proc(c);
    //计算执行时间
    duration = ustime()-start;
    //在对应的命令里执行一次,dirty+1
    //这里表示本次命令有多少次修改
    dirty = server.dirty-dirty;
    if (flags & CMD_CALL_PROPAGATE && (c->flags & CLIENT_PREVENT_PROP) != CLIENT_PREVENT_PROP)
        if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))
            propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
        

/**
 * 传播执行的命令到aof或从节点
 * @param cmd
 * @param dbid
 * @param argv
 * @param argc
 * @param flags
 */
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags)

    if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
        //aof追加(主要是生成aof内容,并追加到.aof_buf)
        feedAppendOnlyFile(cmd,dbid,argv,argc);
    if (flags & PROPAGATE_REPL)
        //主从复制处理
        replicationFeedSlaves(server.slaves,dbid,argv,argc);

在feedAppendOnlyFile方法里主要是aof内容生成,方法就不具体列了,主要做了三件事

  • 组装刚执行命令的aof内容buf,将过期时间由相对转成绝对(重点)
  • 如果AOF开启的情况,将刚组装的buf放入到server.aof_buf 后
  • 如果正在重写aof,将buf写到 server.aof_rewrite_buf_blocks中(在aofRewriteBufferAppend里)
/**
 * aof重写的情况下调用,将buf写到一个链表里
 * @param s  aof信息
 * @param len
 */
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) 
    listNode *ln = listLast(server.aof_rewrite_buf_blocks);
    aofrwblock *block = ln ? ln->value : NULL;
    while(len) 
        //将传递过来的s写入到aof_rewrite_buf_blocks
    
    //现在是主进程,如果有管道,通过管道server.aof_pipe_write_data_to_child 给子进程
    if (aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0) 
        aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,AE_WRITABLE, aofChildWriteDiffData, NULL);

/**
 * 将server.aof_rewrite_buf_blocks 中的数据写入server.aof_pipe_write_data_to_child子进程的管道里
 * 此处增量更新的数据,会由子进程在将虚拟空间里的数据落地后再次读取
 * @param el
 * @param fd
 * @param privdata
 * @param mask
 */
void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) 
     while(1) 
          ln = listFirst(server.aof_rewrite_buf_blocks);
          block = ln ? ln->value : NULL;
          if (block->used > 0) 
              //通过管道把block->buf里的数据写给子进程
              nwritten = write(server.aof_pipe_write_data_to_child, block->buf,block->used);
              if (nwritten <= 0) return;
              memmove(block->buf,block->buf+nwritten,block->used-nwritten);
              block->used -= nwritten;
              block->free += nwritten;
          
          if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);
     
    

这里有几个变量

  • server.aof_buf aof缓冲区,用于存放每次执行命令后的aof信息
  • server.aof_rewrite_buf_blocks 只要是有aof的子进程,就把新产生的命令添加上去
  • server.aof_pipe_write_data_to_child aof 子进程的管道,用于将server.aof_rewrite_buf_blocks信息给子进程

在主进程执行命令后,并没有写aof文件,只是将命令拼装成了字符串,放入到了aof缓冲区server.aof_buf 中,如果有aof的子进程,将aof信息放入到server.aof_rewrite_buf_blocks 然后通过管道将该信息给到子进程。

那aof是什么时候写入到文件里呢?,别急,看下图

aof有几个场景的写入:

  • 在主流程的循环体里
    • 在循环执行前的beforesleep里
    • 在serverCron里
  • 准备停止之前调用一次
  • 通过configSetCommand设置
  • 主从复制

最终都是调用的flushAppendOnlyFile

//在beforeSleep里,没有任何的逻辑判断,直接调用
void beforeSleep(struct aeEventLoop *eventLoop) 
    //将aof缓冲区写入磁盘
    flushAppendOnlyFile(0);    

//在serverCron中
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) 
    //设置了刷入延期开始时间,直接调用
    if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);
    //每秒再判断一次,如果上一次执行异常,再刷入一次
    run_with_period(1000) 
        if (server.aof_last_write_status == C_ERR)
            flushAppendOnlyFile(0);
    


/**
  * 刷入文件
  * @param force 1 强制,0不强制
  */
void flushAppendOnlyFile(int force) 
    //每秒刷入的时候,判断一下aof的fsync是否在bio线程里执行了,是sync_in_progress 为true
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        sync_in_progress = aofFsyncInProgress();
    //每秒并且非强制刷入
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) 
        if (sync_in_progress) 
            //刷入延期开始时间为0,表示没有任何的执行
            if (server.aof_flush_postponed_start == 0) 
                //先将刷入延期开始时间置为当前时间
                server.aof_flush_postponed_start = server.unixtime;
                return;
                //以后再来,只要当前时间-开始时间<2直接返回(所以对redis的aof来说,并不是每秒执行,而是至少2秒以上,如果阻塞了,时间更久)
             else if (server.unixtime - server.aof_flush_postponed_start < 2) 
                return;
            
            server.aof_delayed_fsync++;
        
    
    //这里用了卫语句的方式,将不能触发的在前面拦截
    //写入到aof文件里
    nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
    //写完aof后,将刷入延期开始时间置为0
    server.aof_flush_postponed_start = 0;
    //省略异常的处理
    //记录aof当前的大小
    server.aof_current_size += nwritten;
     /**
     * aof的可用buf比较小了,就清空buf
     */
    if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) 
        sdsclear(server.aof_buf);
     else 
        sdsfree(server.aof_buf);
        server.aof_buf = sdsempty();
    

    
    
//aof写入方式的枚举
configEnum aof_fsync_enum[] = 
    "everysec", AOF_FSYNC_EVERYSEC,
    "always", AOF_FSYNC_ALWAYS,
    "no", AOF_FSYNC_NO,
    NULL, 0
;    
  • aof的写入是在beforeSleep 里,在serverCron 主要是处理延期写入或者处理写入异常
  • aof通过server.aof_flush_postponed_start来延期写入,第一次将此值赋值为当前时间,写完以后置为0
  • aof通过write写入文件(获取的是文件fd对应的偏移指针,顺序写)

aof重写


在上一篇《redis源码阅读-持久化之RDB》中

serverCron在执行的时候,有一个backgroundRewriteDoneHandler方法,当时就有疑问,这个重写是干啥的?

然后搜索了下,发现了这个函数rewriteAppendOnlyFileBackground

我们看下这个函数的触发的时机

先说下触发时机:

  • 在周期性循环里进行
  • 通过命令调用执行aof重写
    • 一个是bgrewriteaofCommand
    • 一个是configSetCommand
  • 主从复制时触发

在serverCron里有两次调用

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) 
    /**调用①,这次调用是之前有rdb进程的时候,设置为了aof重写等待任务(本该执行的逻辑之前因为rdb没有执行,所以直接触发)*/
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && server.aof_rewrite_scheduled)
        rewriteAppendOnlyFileBackground();
    
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||ldbPendingChildren())
        
    else 
       
        /** 调用②
         *  在没有rdb和aof子进程的时候,
         *  先判断rdb是否达到了执行条件
         *     save 900 1  配置的判断
         *  再判断aof是否达到了执行条件
         *    1,先判断aof是否打开
         *    2,如果有rdb或aof子进程了也不再执行(互斥)
         *    3,有触发阈值,
         *    4,aof当前的文件的大小大于设置的最小大小
         */
        if (server.aof_state == AOF_ON && server.rdb_child_pid == -1 && server.aof_child_pid == -1 && server.aof_rewrite_perc &&
            server.aof_current_size > server.aof_rewrite_min_size)
        
            /**
             * aof_rewrite_base_size 启动默认为0,第一次达到min_size以后设置为1,这时候触发
             * 等待aof重写完以后,会把aof_rewrite_base_size设置为重写后的大小,这个值一致随着aof文件大小自增
             */
            long long base = server.aof_rewrite_base_size ?server.aof_rewrite_base_size : 1;
            /**
              * 这个公式也很有意思,aof的重写会随着文件的变大而逐步变大,而不是64mb
              */
            long long growth = (server.aof_current_size*100/base) - 100;
            if (growth >= server.aof_rewrite_perc) 
                //触发aof重写
                rewriteAppendOnlyFileBackground();
            
        
    

  • ① 位置标注的调用是为了延迟补偿
  • ②位置标注的每次在没有rdb和aof子进程的情况下都会进来
  • redis通过server.aof_rewrite_scheduled来控制aof的延迟执行
/**
 * 后台的方式重写aof,通过fork进程处理
 *
 * @return
 */
int rewriteAppendOnlyFileBackground(void) 
    pid_t childpid;
    long long start;
    //双重校验,只要有aof或rdb的子进程就不再执行,
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    //创建6个管道,分别用来数据传输,父子进程之间的ack响应
    if (aofCreatePipes() != C_OK) return C_ERR;
    //创建执行aof完以后回写的管道
    openChildInfoPipe();
    start = ustime();
    //fork子进程进行重写
    if ((childpid = fork()) == 0) 
        char tmpfile[256];

        /* Child */
        //关闭自己不需要关注的
        closeClildUnusedResourceAfterFork();
        //设置进程名称
        redisSetProcTitle("redis-aof-rewrite");
        //格式化重写aof的文件名
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        //重写aof文件
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) 
            size_t private_dirty = zmalloc_get_private_dirty(-1);
            server.child_info_data.cow_size = private_dirty;
            sendChildInfo(CHILD_INFO_TYPE_AOF);
            exitFromChild(0);
         else 
            exitFromChild(1);
        
     else 
        /* Parent */
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) 
            closeChildInfoPipe();
            aofClosePipes();
            return C_ERR;
        
        //已经开始处理了,就把等待任务表示写回0
        server.aof_rewrite_scheduled = 0;
        //标记aof重写开始的时间
        server.aof_rewrite_time_start = time(NULL);
        //标记aof的的子进程
        server.aof_child_pid = childpid;
        //禁用hash resize
        updateDictResizePolicy();
        server.aof_selected_db = -1;
        replicationScriptCacheFlush();
        return C_OK;
    
    return C_OK; /* unreached */


/**
 * 创建aof管道
 * @return
 */
int aofCreatePipes(void) 
    int fds[6] = -1, -1, -1, -1, -1, -1;
    int j;

    if (pipe(fds) == -1) goto error; /* parent -> children data. */
    if (pipe(fds+2) == -1) goto error; /* children -> parent ack. */
    if (pipe(fds+4) == -1) goto error; /* parent -> children ack. */
    /* 设置数据管道为非阻塞 */
    if (anetNonBlock(NULL,fds[0]) != ANET_OK) goto error;
    if (anetNonBlock(NULL,fds[1]) != ANET_OK) goto error;
    /**
     * 注册时间处理,监听 server.aof_pipe_read_ack_from_child 传递过来的ack信息,如果有就 server.aof_stop_sending_diff = 1;
     * 同时删除server.aof_pipe_read_ack_from_child的事件处理器
     */
    if (aeCreateFileEvent(server.el, fds[2], AE_READABLE, aofChildPipeReadable, NULL) == AE_ERR) goto error;
    //父进程往子进程写数据的管道
    server.aof_pipe_write_data_to_child = fds[1];
    //子进程从父进程读数据的管道
    server.aof_pipe_read_data_from_parent = fds[0];
    //子进程发送ack给父进程的管道
    server.aof_pipe_write_ack_to_parent = fds[3];
    //父进程读子进程发送的ack信息的管道
    server.aof_pipe_read_ack_from_child = fds[2];
    //父进程发送ack信息给子进程的管道
    server.aof_pipe_write_ack_to_child = fds[5];
    //子进程读取父进程发送的ack信息的管道
    server.aof_pipe_read_ack_from_parent = fds[4];
    server.aof_stop_sending_diff = 0;
    return C_OK;

error:
    for (j = 0; j < 6; j++) if(fds[j] != -1) close(fds[j]);
    return C_ERR;

rof重写

/**
 * 同步重写aof
 * @param filename
 * @return
 */
int rewriteAppendOnlyFile(char *filename) 
    rio aof;
    FILE *fp;
    char tmpfile[256];
    char byte;

    //又创建了一个临时文件
    snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) 
        return C_ERR;
    

    server.aof_child_diff = sdsempty();
    //初始化一个文件io对象
    rioInitWithFile(&aof,fp);

    if (server.aof_rewrite_incremental_fsync)
        riosetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);
    /**
     * aof_use_rdb_preamble = yes 表示开启了混合模式,先将rdb信息刷入aof文件
     * rdb是比较节省空间
     */
    if (server.aof_use_rdb_preamble) 
        int error;
        if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) 
            errno = error;
            goto werr;
        
     else 
        //直接以redis的协议从所有db的全局hash表中读取数据写入aof文件
        if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
    
    //清空缓冲区
    if (fflush(fp) == EOF) goto werr;
    //刷入磁盘,正常都是在操作系统的页缓存里,fsync可以刷入磁盘
    if (fsync(fileno(fp)) == -1) goto werr;

    int nodata = 0;
    mstime_t start = mstime();
    /**
     * 在1秒内,每次等待1毫秒,从管道里获取数据
     */
    while(mstime()-start < 1000 && nodata < 20)以上是关于redis源码阅读-持久化之aof与aof重写详解的主要内容,如果未能解决你的问题,请参考以下文章

《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读

Redis的持久化机制详解—RDB与AOF持久化

Redis 详解 AOF 持久化

Redis之AOF重写及其实现原理

redis aof持久化源码分析

Redis持久化详解