暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

来聊聊Redis的AOF重写机制

43

写在文章开头

AOF
会将用户的指令按照RESP协议将数据持久化的物理磁盘中,由于AOF
是每条指令都会进行这周操作,所以随着时间的推移appendonly.aof
的体积会逐渐增大,于是redis就提出了aof重写这一机制来重写appendonly.aof
。 笔者看过市面上的很多文章,它们都一致认为AOF重写是解析appendonly.aof
文件,基于del等指令将抹去一些无用的键值对,但是笔者查看源码后发现,此类说法有着严重的错误,所以笔者就基于此文来详细讨论一些Redis
AOF
重写机制。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注  “加群”  即可和笔者和笔者的朋友们进行深入交流。

详解Redis可靠性AOF重写机制

AOF重写时机

我们先来聊聊AOF
的几个重写时机,首先是用户手动执行config set appendonly yes
,服务端就会基于此指令得到对应的指令函数configSetCommand
,该函数会解析用户传参得知用户要开启appendonly ,此时就会触发一次AOF
重写然后将文件落盘:

对应的我们给出configSetCommand
的入口,可以看到其内部判断逻辑会解析出用户的参数,在得知是AOF
开启之后就会调用stopAppendOnly
进行AOF
重写并持久化到磁盘中:

void configSetCommand(redisClient *c) {
   //......

       //......
  else if (!strcasecmp(c->argv[2]->ptr,"appendonly")) {//如果config set 是开启appendonly则调用stopAppendOnly开启AOF并进行一次AOF重写完成文件持久化
        int enable = yesnotoi(o->ptr);

        if (enable == -1goto badfmt;
        if (enable == 0 && server.aof_state != REDIS_AOF_OFF) {//非关闭AOF则调用stopAppendOnly触发重写持久化文件
            stopAppendOnly();
        } else if (enable && server.aof_state == REDIS_AOF_OFF) {
            //......
        }
    }  //......

        
    addReply(c,shared.ok);
    return;

 //......
}

我们查看startAppendOnly
的逻辑可以看到其内部会调用rewriteAppendOnlyFileBackground
,该函数就会fork
出一个子进程进行异步的AOF
重写然后进行文件落盘:

int startAppendOnly(void) {
    //......
    //调用rewriteAppendOnlyFileBackground执行fork子进程完成aof重写落盘
    if (rewriteAppendOnlyFileBackground() == REDIS_ERR) {
        close(server.aof_fd);
       //......
        return REDIS_ERR;
    }
 //......
    return REDIS_OK;
}


另外一个常见的AOF
重写时机则是执行bgrewriteaof
指令,该指令同样会执行AOF
重写落盘,对应的源码如下,可以看到其核心本质也是检查是否有其他RDB
或者AOF
子进程存在持久化操作,如果没有则调用rewriteAppendOnlyFileBackground
进行异步AOF
重写落盘,如果发现有rdb
等持久化存在则将aof_rewrite_scheduled
等待下一次redis
的事件循环得知该参数为1之后再次尝试AOF
重写:

对应的我们给出bgrewriteaofCommand
源码,其逻辑和笔者分析基本一致,读者可自行查阅:

//用户手动调用bgrewriteaof进行aof重写
void bgrewriteaofCommand(redisClient *c) {
    if (server.aof_child_pid != -1) {//如果存在aof子进程则不进行aof重写
        addReplyError(c,"Background append only file rewriting already in progress");
    } else if (server.rdb_child_pid != -1) {//如果进有rdb持久化存在,则设置aof_rewrite_scheduled后续时间时间检查允许的情况下直接进行重写
        server.aof_rewrite_scheduled = 1;
        addReplyStatus(c,"Background append only file rewriting scheduled");
    } else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {//执行aof重写
        addReplyStatus(c,"Background append only file rewriting started");
    } else {
        addReply(c,shared.err);
    }
}

最后一种则是redis
自带的事件轮询必须执行的函数serverCron
该方法就会检查上一步设置的aof_rewrite_scheduled
是否为1,若为1则进行AOF
重写。亦或者发现当前AOF
文件大小超过配置的最大值以及没有rdb
aof
子进程也会触发AOF
异步重写落盘:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    //......
    //aof_rewrite_scheduled设置为1,且没有其他持久化子进程则进行aof重写
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
        server.aof_rewrite_scheduled)
    {
        rewriteAppendOnlyFileBackground();
    }

   //......

         /* Trigger an AOF rewrite if needed */
         //没有其他持久化子进程,且当前大小超出aof_rewrite_perc阈值,则进行aof重写 
         if (server.rdb_child_pid == -1 &&
             server.aof_child_pid == -1 &&
             server.aof_rewrite_perc  && //auto-aof-rewrite-percentage aof大小超出基础大小比例,默认为1
             server.aof_current_size > server.aof_rewrite_min_size)//当前大小aof_current_size大于auto-aof-rewrite-min-size为64M
         {
            long long base = server.aof_rewrite_base_size ?
                            server.aof_rewrite_base_size : 1;
            long long growth = (server.aof_current_size*100/base) - 100;
            if (growth >= server.aof_rewrite_perc) {
                redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                //执行AOF异步重写落盘
                rewriteAppendOnlyFileBackground();
            }
         }
    }


   //......
}

AOF重写核心函数

我们了解的redis
的触发AOF
的几个时间点之后,再来聊聊AOF
重写的流程,如下图所示,在进行AOF
重写时,redis
会遍历所有数据库的键值对,然后将其生成redis
RESP
协议规范的字符串,然后再将字符串写入写入aof物理文件中。

这里我们简单说明一下RESP
协议,因为客户端传入的指令都是基于RESP
协议的字符串,所以AOF使用这种格式的字符串就可以保证调用和redis
客户端一样的方法完成指令写入数据库,由于实现逻辑复用。以本文为例,假设我们aof
重写时遍历得到数据库0有一个键值对key
为k,value
为v,我可知这条数据是用户通过set k v
写入数据库的数据。对应AOF遍历到这个键值对之后就会基于RESP协议
得到下面这样一段字符串(附含义和注释):

# 写入数据库0的信息,后续aof恢复时就可以通过select 0定位到数据库中
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n

# 字符串长度为3的set指令
*3\r\n$3\r\nset\r\n 
# 1个字符串长度的key为k
$1\r\nk\r\n 
# 1个字符串长度value为v
$1\r\nv\r\n


对应的我们给出AOF
重写的核心代码入口rewriteAppendOnlyFileBackground
,可以看到它本质就是fork
出一个子进程,然后子进程创建一个临时文件将解析到键值对字符串写入,最后通过原子重命名的方式将aof
文件重命名为appendonly.aof

int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;

    if (server.aof_child_pid != -1return REDIS_ERR;
    if (aofCreatePipes() != REDIS_OK) return REDIS_ERR;
    start = ustime();
    if ((childpid = fork()) == 0) {//fork子进程进行aof重写
        char tmpfile[256];

      
      //......
        //生成一个tmp文件将内存数据库的键值对写入文件
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {//重写aof
            size_t private_dirty = zmalloc_get_private_dirty();
   //......
   //结束子进程
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
       //......
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

最后我们步入最核心的逻辑rewriteAppendOnlyFile
,可以看到其内部就是生成一个临时文件,然后遍历数据库中的键值对,根据键值对的类型生成相应的RESP字符串
(例如遍历到的值类型为字符串则转为set
指令的字符串,若是集合类型则声称hset
的字符串),最后写入临时文件,后续redis
的定时轮询时间时间会遍历检查先前自行任务的子进程的pid是否是aof
子进程的pid
,如果是则说明aof
重写完成直接将文件重命名为appendonly.aof

对应我们给出上述逻辑的核心代码入口rewriteAppendOnlyFile
,可以看到大致步骤就是生成临时文件,遍历键值对工具数据结构类型生成对应的RESP
字符串,完成后写入aof
临时文件:

int rewriteAppendOnlyFile(char *filename) {
   //......

    //打开临时文件
    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;
    }

    //......
    for (j = 0; j < server.dbnum; j++) {
        //根据遍历结果获得当前库生成select指令字符串
        char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";
        redisDb *db = server.db+j;
        dict *d = db->dict;
        if (dictSize(d) == 0continue;
        //获取库的字典迭代器
        di = dictGetSafeIterator(d);
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }

        
        //写入切库select指令指令
        if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0goto werr;
        if (rioWriteBulkLongLong(&aof,j) == 0goto werr;

        
        //遍历当前内存库
        while((de = dictNext(di)) != NULL) {
            sds keystr;
            robj key, *o;
            long long expiretime;
            //获取键值对
            keystr = dictGetKey(de);
            o = dictGetVal(de);
            initStaticStringObject(key,keystr);

            expiretime = getExpire(db,&key);

            //......
            if (o->type == REDIS_STRING) {//如果value是字符串则记录set指令
                /* Emit a SET command */
                char cmd[]="*3\r\n$3\r\nSET\r\n";
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0goto werr;
                /* Key and value */
                if (rioWriteBulkObject(&aof,&key) == 0goto werr;
                if (rioWriteBulkObject(&aof,o) == 0goto werr;
            } else if (o->type == REDIS_LIST) {//如果是list则用RPUSH插入到尾部
                if (rewriteListObject(&aof,&key,o) == 0goto werr;
            } else if (o->type == REDIS_SET) {//调用SADD遍历并存储
                if (rewriteSetObject(&aof,&key,o) == 0goto werr;
            } else if (o->type == REDIS_ZSET) {//调用ZADD进行遍历重写
                if (rewriteSortedSetObject(&aof,&key,o) == 0goto werr;
            } else if (o->type == REDIS_HASH) {//调用HMSET进行重写
                if (rewriteHashObject(&aof,&key,o) == 0goto werr;
            } else {
                redisPanic("Unknown object type");
            }
           //......
    }

    //刷盘结束,将数据写入到磁盘中
    //......
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1goto werr;

    //......
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1goto werr;
    if (fclose(fp) == EOF) goto werr;

    //......
    return REDIS_ERR;
}

后续redis的定时任务就会检查最近执行任务的子进程是否为aof
子进程,如果是则说明aof重写
完成调用backgroundRewriteDoneHandler
将文件重命名为appendonly.aof

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
   
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
        int statloc;
        pid_t pid;

        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
          //......

            if (pid == server.rdb_child_pid) {
               //......
            } else if (pid == server.aof_child_pid) {//如果子进程为aof的,则说明重写完成,文件重命名为appendonly.aof
                backgroundRewriteDoneHandler(exitcode,bysignal);
            } else {
               //......
            }
          //......
        }
    } else {
        //......
    }

//......
}

我们步入backgroundRewriteDoneHandler
即可看到文件重命名为appendonly.aof
的原子重命名的逻辑:

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
    if (!bysignal && exitcode == 0) {
        //......
        //将上一步aof重写生成的tmpfile重命名为appendonly.aof
        if (rename(tmpfile,server.aof_filename) == -1) {
           //......
            close(newfd);
            if (oldfd != -1) close(oldfd);
            goto cleanup;
        }
         //......
}

小结

以上便是笔者对于Redis的AOF重写机制源码分析的全部内容,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注  “加群”  即可和笔者和笔者的朋友们进行深入交流。

参考

Redis Config Set 命令:https://www.runoob.com/redis/server-config-set.html


文章转载自写代码的SharkChili,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论