REDIS的AOF实现

Redis AOF
上文我们介绍了Redis的主框架,以及两种持久化大概原理。本文我们将从源码角度分析Redis AOF的相关实现。(本文基于的版本为2.4.2)
1. 相关配置项
首先我们看一下redis.conf里的关于AOF的配置选项:
Appendonly(yes,no):是否开启AOF持久化
Appendfilename(log/appendonly.aof):AOF日志文件
Appendfsync(always,everysec,no):AOF日志文件同步的频率,always代表每次写都进行fsync,everysec每秒钟一次,no不主动fsync,由OS自己来完成。
no-appendfsync-on-rewrite(yes,no):进行rewrite时,是否需要fsync
auto-aof-rewrite-percentage(100):当AOF文件增长了这个比例(这里是增加了一倍),则后台rewrite自动运行
auto-aof-rewrite-min-size(64mb):进行后面rewrite要求的最小AOF文件大小。这两个选项共同决定了后面rewrite进程是否到达运行的时机
注:rewrite是指当AOF很大的时候,通过重写内存的数据来删除原来的AOF文件,生成最新的内存数据的AOF日志,即把当前的结果逆转化为相应的操作命令写到AOF文件中。
通过上面的选项我们可以知道redis的三个AOF处理流程:
 每次更新操作进行的AOF写操作(涉及同步频率)
 Rewrite,当满足auto-aof-rewrite-percentage,auto-aof-rewrite-min-size时后面自动运行rewrite操作。
 Rewrite,当收到bgrewriteaof客户端命令时,马上运行后面rewrite操作
注:当某个key过期的时候也会写AOF,其实它跟第一种很类似,这里就不再介绍。下面我们将分别介绍这三个流程。
在redis的较新版本中(不知道从哪个版本开始)增加了两个新的子进程:
 REDIS_BIO_CLOSE_FILE,负责所有的close file操作
 REDIS_BIO_AOF_FSYNC,负责fsync操作
因为这两个操作都可能会引起阻塞,如果在主线程中完成的话,会影响系统对事件的响应,所以这里统一由相应的线程来完成,每个线程都有一个自己的bio_jobs list,用来保存需要的处理的job任务。其相应的代码在bio.c(线程处理函数为bioProcessBackgroundJobs)里,这两个线程在initServer时创建bioInit()。
注:标准命令格式:如set aaa xiang
*3\r\n$3\r\nset\r\n$3\r\n\aaa\r\n$5\r\n\xiang\r\n
其中*3表示该命令的参数个数,后面的数字表示每个参数的长度。

2. AOF的处理流程
2.1 每次更新操作的AOF写

主要涉及的配置是:Appendfsync,no-appendfsync-on-rewrite。该操作的入口在(redis.c):

[cpp]  view plain copy
  1. void call(redisClient *c) {  
  2.     dirty = server.dirty;  //上次的脏数据个数  
  3.     c->cmd->proc(c);    //执行命令操作,如果该操作是一个更新操作,则server.dirty会增加  
  4.     dirty = server.dirty-dirty; //此次执行导致的脏数据个数  
  5.     …  
  6.     if (server.appendonly && dirty > 0) //有脏数据并且开启了AOF功能  
  7.         feedAppendOnlyFile(c->cmd,c->db->id,c->argv,c->argc); //将数据保存到server.aofbuf  
  8. …  
  9. }  

我们再来看一下feedAppendOnlyFile的实现

[cpp]  view plain copy
  1. void feedAppendOnlyFile(struct redisCommand…{  
  2. if (dictid != server.appendseldb){ //当月操作的db与上一次不一样,所以要重新写一个新的select db命令,当rewrite的时候也会把appendseldb置为-1  
  3.         buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",  
  4.             (unsigned long)strlen(seldb),seldb);  
  5.         server.appendseldb = dictid;  
  6. }  
  7. …  
  8. buf = catAppendOnlyGenericCommand(buf,argc,argv); //转换为标准命令格式  
  9. server.aofbuf = sdscatlen(server.aofbuf,buf,sdslen(buf)); //将命令写到aofbuf,这个buf会在serverCron当Appendfsync到满足时fsync到文件  
  10. if (server.bgrewritechildpid != -1) //如果有bgrewrite子进程的话,则也必须把该命令保存到bgrewritebuf,以便在子进程结束时,把新的变更追加到rewrite后的文件  
  11.     server.bgrewritebuf = sdscatlen(server.bgrewritebuf,buf,sdslen(buf));  
  12. …  
  13. }  
可以看到到上面AOF操作也只是写到buf中,并没有进行写操作,下面我们将查看该过程。通过查看代码我们可以知道flushAppendOnlyFile()函数是进行真正的写操作。另外我们可以知道该函数会在beforeSleep及serverCron中调用。其中beforeSleep是aeMain循环,每次进行事件处理前必须调用一次:
[cpp]  view plain copy
  1. void aeMain(aeEventLoop *eventLoop) {  
  2.     eventLoop->stop = 0;  
  3.     while (!eventLoop->stop) {  
  4.         if (eventLoop->beforesleep != NULL)  
  5.             eventLoop->beforesleep(eventLoop);  
  6.         aeProcessEvents(eventLoop, AE_ALL_EVENTS);  
  7.     }  
  8. }  
而serverCron则先判断是否有延迟的flush操作:
if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);
下面我们来看一下该函数flushAppendOnlyFile:
[cpp]  view plain copy
  1. flushAppendOnlyFile(int force){  
  2. …  
  3.     if (server.appendfsync == APPENDFSYNC_EVERYSEC) //如果我们设置的fsync频率为everysec  
  4.         sync_in_progress = bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC) != 0;//判断是否已经有fsync job在等待fsync线程的处理  
  5.     if (server.appendfsync == APPENDFSYNC_EVERYSEC && !force) {  
  6.         if (sync_in_progress) {  //已经有fsync job在等待处理了,先不write也不把该job放到fsync线程处理队列里,如果之前并没有延迟fsync的job,则标志现在已经有这样的情况并且设置这个时间为server.unixtime.如果之前已经有延迟的fsync job,则如果这个延迟小于2s,则直接返回再等待,否则就需要flush了。  
  7.             if (server.aof_flush_postponed_start == 0) {  
  8.                 server.aof_flush_postponed_start = server.unixtime;  
  9.                 return;  
  10.             } else if (server.unixtime - server.aof_flush_postponed_start < 2) {  
  11.                 return;  
  12.             }  
  13.             redisLog(REDIS_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");  
  14.         }  
  15. server.aof_flush_postponed_start = 0;  
  16. nwritten = write(server.appendfd,server.aofbuf,sdslen(server.aofbuf)); //write buf这里是nonblock的,此时并没有被fsync到磁盘,会直接返回  
  17.     if (server.no_appendfsync_on_rewrite &&  
  18.         (server.bgrewritechildpid != -1 || server.bgsavechildpid != -1))  
  19.             return//判断no_appendfsync_on_rewrite条件  
  20.     if (server.appendfsync == APPENDFSYNC_ALWAYS) { //如果是APPENDFSYNC_ALWAYS,则必须马上调用fsync,此时主线程就会被阻塞  
  21.         aof_fsync(server.appendfd); /* Let's try to get this data on the disk */  
  22.         server.lastfsync = server.unixtime;  
  23.     } else if ((server.appendfsync == APPENDFSYNC_EVERYSEC &&  
  24.                 server.unixtime > server.lastfsync)) { //如果没有等待的job则把该job加到fsync线程的job队列里  
  25.         if (!sync_in_progress) aof_background_fsync(server.appendfd);  
  26.         server.lastfsync = server.unixtime;  
  27.     }  
  28. }  
通过上面的介绍我们可以知道即使Appendfsync设置为alway,并不是每次执行完一条更新命令就直接写(write+fsync)aof file,这个过程(write+fsync)会被推迟到事件处理流程结束后beforeSleep后进行(一个疑问先写到server.aofbuf,然后再写到数据文件,过程中如果crash会不会丢数据呢? 答案是:不会,因为在一次事件处理结束之后会调用beforeSleep进行flash,而它也是在下一次事件处理之前完成的,即只有在flash之后才会给客户端回复成功与否.注这个解释来自@hoterran.这样是否有重复写的可能?);如果在beforeSleep时已经有fsync job在等待fsync线程处理(只有一个aof fd,之前还在想为什么它不能再被放到list里),if (server.appendfsync == APPENDFSYNC_EVERYSEC && !force) && if (sync_in_progress),则该次的请求会被标志为server.aof_flush_postponed_start,那么在调用serverCron时会再次调用flushAppendOnlyFile,看是否现在能够进行write并且把该job提交给fsync线程,或者如果已经等待超过2s,则给出一个系统提示。[同样的貌似everysec,也并不是真正的每1s fsync一次]

2.2 后面自动运行rewrite
该操作涉及的配置:auto-aof-rewrite-percentage,auto-aof-rewrite-min-size。
该过程是在serverCron里判断,是满足到达运行bgrewrite的时机:
[cpp]  view plain copy
  1. serverCron(){  
  2. if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) {  
  3. else {  
  4. //判断是否需要rdbSaveBackground,后面运行save rdb  
  5.          if (server.bgsavechildpid == -1 &&  
  6.              server.bgrewritechildpid == -1 &&  
  7.              server.auto_aofrewrite_perc &&  
  8.              server.appendonly_current_size > server.auto_aofrewrite_min_size)  
  9.          { //当前没有后面rewrite子进程,并且满足了auto_aofrewrite_min_size  
  10.             long long base = server.auto_aofrewrite_base_size ?  
  11.                             server.auto_aofrewrite_base_size : 1;  
  12.             long long growth = (server.appendonly_current_size*100/base) - 100;  
  13.             if (growth >= server.auto_aofrewrite_perc) { //判断增长比例  
  14.                 redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);  
  15.                 rewriteAppendOnlyFileBackground();  
  16.             }  
  17.         }  
  18. }}  
rewriteAppendOnlyFileBackground()函数也在下面的情况中出现,所以我们在下面一起分析。

2.3 客户端发送bgrewriteaof命令
通过查找readonlyCommandTable表,我们可以看到当客户端发送bgrewriteaof命令过来的时候,服务器调用bgrewriteaofCommand函数来进行处理。该函数会判断当前是否已经有bgrewritechildpid存在,或者bgsavechildpid存在则标志server.aofrewrite_scheduled = 1,需要进行bgrewrite,但不是现在,而是在serverCron处理的时候。否则则直接调用rewriteAppendOnlyFileBackground,创建bgrewrite进程,进行rewrite操作。
[cpp]  view plain copy
  1. rewriteAppendOnlyFileBackground(){  
  2. if ((childpid = fork()) == 0) { //后台子进程  
  3.         if (server.ipfd > 0) close(server.ipfd); //关闭listen套接字  
  4.         if (server.sofd > 0) close(server.sofd);  
  5.         snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid()); //新的aof临时文件名,这个在rewriteAppendOnlyFile里又使用了一个新的tempfile name  
  6.         if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) … //rewrite并写到新的tempfile  
  7. }else{  
  8.         server.aofrewrite_scheduled = 0; //子进程已经被调度  
  9.         server.bgrewritechildpid = childpid; //作为判断是否有rewrite子进程的标志  
  10.         updateDictResizePolicy(); //此时应disable resize dict  
  11.         server.appendseldb = -1; //以使得下一个更新操作先写select db命令  
  12.         return REDIS_OK;  
  13. }  
  14. }  
接下来我们看一下子进程是如何完成该工作的:
[cpp]  view plain copy
  1. rewriteAppendOnlyFile(char *filename){  
  2.     snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid()); //打开一个新的tempfile  
  3. fp = fopen(tmpfile,"w");  
  4. for (j = 0; j < server.dbnum; j++) { //对所有的db库遍历  
  5.         char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n"//对每个db先写select db命令  
  6.         redisDb *db = server.db+j;  
  7.         if (fwrite(selectcmd,sizeof(selectcmd)-1,1,fp) == 0) goto werr;  
  8.         if (fwriteBulkLongLong(fp,j) == 0) goto werr; //db id  
  9.         while((de = dictNext(di)) != NULL) { //获得该db内的每个dictEntry  
  10.             keystr = dictGetEntryKey(de); //获得key值  
  11.             o = dictGetEntryVal(de); //获得value值  
  12.             initStaticStringObject(key,keystr); //将keystr转换为robj的类型,  
  13.             if (o->type == REDIS_STRING) { //下面就是一个一个的判断value的类型,以选择它对应的命令,及encoding方式,我们这里就举REDIS_STRING类型为例  
  14.                 char cmd[]="*3\r\n$3\r\nSET\r\n"//先写命令  
  15.                 if (fwrite(cmd,sizeof(cmd)-1,1,fp) == 0) goto werr;  
  16.                 if (fwriteBulkObject(fp,&key) == 0) goto werr;  //写key  
  17.                 if (fwriteBulkObject(fp,o) == 0) goto werr;  //写value  
  18.             }else if(…)  
  19.             Else if…  
  20.         }  
  21. }  
  22.     fflush(fp); //fsync文件并close  
  23.     aof_fsync(fileno(fp));  
  24. fclose(fp);  
  25. rename(tmpfile,filename); //将它rename为temp-rewriteaof-bg-%d.aof名字,不明白这里为什么要使用一个新的tmpfile:temp-rewriteaof-%d.aof  
  26. }  
至此子进程完成rewrite操作。那么父进程也就是主线程是在什么时候获得子进程退出状态,并且做了些什么操作?
[cpp]  view plain copy
  1. if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) {  
  2.     if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {  
  3.         if (pid == server.bgsavechildpid) {  
  4.             backgroundSaveDoneHandler(statloc); //后台save rdb进程  
  5.         } else {  
  6.             backgroundRewriteDoneHandler(statloc);  //有后台rewrite子进程退出,调用该函数进行处理  
  7.         }  
  8.         updateDictResizePolicy();  
  9.     }  
  10. }  
即父进程在serverCron里通过server.bgrewritechildpid来判断是否需要等待子进程退出的信号。
进一步我们来看一下backgroundRewriteDoneHandler作了哪些操作:(这里使用了一些技巧解决了一些之前aof存在的缺陷及问题,值得细看)
[cpp]  view plain copy
  1. backgroundRewriteDoneHandler(int statloc){  
  2. if (!bysignal && exitcode == 0) { //判断退出状态  
  3.         snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof",  
  4.             (int)server.bgrewritechildpid);  
  5.         newfd = open(tmpfile,O_WRONLY|O_APPEND); //打开子进程rewrite的临时文件  
  6.         …  
  7.         nwritten = write(newfd,server.bgrewritebuf,sdslen(server.bgrewritebuf)); //将bgrewritebuf写的tempfile里  
  8. /*当rename的是oldfile是真正的存在,并且该文件没有被open,也即没有其它的进程引用它,那么此时rename它的话会导致该文件的unlink操作,这就会导致主线程阻塞;这里解决的办法是先open(O_NONBLOCK),来增加引用计数,这里不用管是否open成功,因为如果这个file本身就不存在则也就不会有unlink的问题.如果该文件已经被打开过了,则先把它的oldfd=-1,然后rename,这时不会有unlink操作,然后由后台线程进行close操作,因为此时close就会导致unlink阻塞*/  
  9.         if (server.appendfd == -1) //如果oldfile文件没有被open,客户端可以通过发送命令来disable aof  
  10.            oldfd = open(server.appendfilename,O_RDONLY|O_NONBLOCK); //增加oldfile的引用计数,防止rename导致的unlink阻塞  
  11.         else  
  12.            oldfd=-1; //这里置为-1是为了rename失败时close使用,否则在下面该值又会被置为旧的aof fd,然后在后台进行close  
  13.         rename(tmpfile,server.appendfilename); //这里的rename已经不会导致unlink  
  14.         if (server.appendfd == -1) {  
  15.             close(newfd); //如果现在aof disable,则close新的aof file  
  16.         } else {  
  17.             oldfd = server.appendfd;  //还原oldfd  
  18.             server.appendfd = newfd; //设置newfd为新的aof fd  
  19.             if (server.appendfsync == APPENDFSYNC_ALWAYS)  
  20.                 aof_fsync(newfd); //直接fsync阻塞  
  21.             else if (server.appendfsync == APPENDFSYNC_EVERYSEC)  
  22.                 aof_background_fsync(newfd); //将该fsync放到fsync线程队列里  
  23.             server.appendseldb = -1; /* Make sure SELECT is re-issued */  
  24.             aofUpdateCurrentSize();  
  25.             server.auto_aofrewrite_base_size = server.appendonly_current_size;  
  26.             sdsfree(server.aofbuf); //清除aofbuf,因为这些已经存在bgrewritebuf里,被写入现在的aof file了  
  27.             server.aofbuf = sdsempty();  
  28.         }  
  29.         if (oldfd != -1) bioCreateBackgroundJob  
  30. (REDIS_BIO_CLOSE_FILE,(void*)(long)oldfd,NULL,NULL); //后台close线程  
  31.         …  
  32. }  
  33. }  
下面的这篇文章也解释了新版本解决了一些旧版本aof存在的问题:http://www.hoterran.info/redis-aof-backgroud-thread

3. 总结

通过这篇文章我们学习了AOF涉及的大多数内容,其实质可以分为两种内容:server接受到一条更新操作时向aofbuf里写这条命令,然后在结束一次事件循环后(beforeSleep),进行fsync操作,此时会根据配置的sync频率来选择是直接(alawy)由主线程fsync还是由fsync线程来sync(everysec);其二就是rewrite操作,该操作是由后台子进程来实现,子进程利用copy-on-write获得与父进程一样的地址空间,它把现在的所有db的所有dict表的内容重新还原为命令的形式写到一个临时的文件里,同时父进程现在必须把新的更新操作缓存到bgrewrietbuf里,当子进程结束的时候(已经把之前的数据写到临时文件里),父进程在serverCron的时候把刚才bgrewritebuf里的内容追加到子进程的临时文件里,然后rename这个临时文件为配置文件指定的文件名。这样就完成了一次rewrite及互换的操作。另外作者为了主线程的最佳性能可谓“煞费苦心”,当然这是也我们想看到的,因为我们能够从这些变化里学到很多知识及技艺。


参考文献:

http://www.hoterran.info/redis_persistence
http://www.hoterran.info/redis-aof-backgroud-thread

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值