上一篇对RDB的源码分析是比较多的,但是AOF持久化执行进行了一些理论上的分析和概念的说明。本来想自己偷一些懒,将上篇文章中最后所给链接的AOF实现代码随便过一过算了,后来也就是在过的过程中发现自己这也看不懂那也看不懂才知道AOF的重要性和难度。
后来又花了不少时间查阅资料、结合源代码分析,对AOF的大概执行过程有了更深一些的了解,现在就将自己的理解和大家进行分享。其中肯定有理解不正确的地方,还望大神们能给予指正。
AOF相关配置项
首先我们看一下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进程是否到达运行的时机
通过上面的选项我们可以知道redis有三个AOF处理流程:
- 每次更新操作进行的AOF写操作(涉及同步频率);
- Rewrite,当满足auto-aof-rewrite-percentage,auto-aof-rewrite-min-size时后面自动运行rewrite操作;
- Rewrite,当收到bgrewriteaof客户端命令时,马上运行后面rewrite操作。
注:当某个key过期的时候也会写AOF,其实它跟第一种很类似,也就是DEL操作。
在redis的较新版本中(不知道从哪个版本开始)增加了两个新的子进程:
- REDIS_BIO_CLOSE_FILE,负责所有的close file操作
- REDIS_BIO_AOF_FSYNC,负责fsync操作
因为这两个操作都可能会引起阻塞,如果在主线程中完成的话,会影响系统对事件的响应,所以这里统一由相应的子线程来完成,每个子线程都有一个自己的bio_jobs list,用来保存需要的处理的job任务。其相应的代码在bio.c(线程处理函数为bioProcessBackgroundJobs)里,这两个线程在initServer时创建bioInit()。
void initServer() { //... // 初始化 BIO 系统 bioInit(); }
AOF的处理流程
1.每次更新操作进行的AOF写操作(涉及同步频率)
主要涉及的配置是:Appendfsync(AOF日志文件同步的频率),no-appendfsync-on-rewrite(进行rewrite时,是否需要fsync),该操作的入口在redis.c。
void call(redisClient *c, int flags) { ... // 保留旧 dirty 计数器值 dirty = server.dirty; // 计算命令开始执行的时间 start = ustime(); // 执行实现函数 c->cmd->proc(c); // 计算命令执行耗费的时间 duration = ustime()-start; // 计算命令执行之后的 dirty 值 dirty = server.dirty-dirty; .... /* Propagate the command into the AOF and replication link */ // 将命令复制到 AOF 和 slave 节点 if (flags & REDIS_CALL_PROPAGATE) { int flags = REDIS_PROPAGATE_NONE; // 强制 REPL 传播 if (c->flags & REDIS_FORCE_REPL) flags |= REDIS_PROPAGATE_REPL; // 强制 AOF 传播 if (c->flags & REDIS_FORCE_AOF) flags |= REDIS_PROPAGATE_AOF; // 如果数据库有被修改,那么启用 REPL 和 AOF 传播 if (dirty) flags |= (REDIS_PROPAGATE_REPL | REDIS_PROPAGATE_AOF); if (flags != REDIS_PROPAGATE_NONE) propagate(c->cmd,c->db->id,c->argv,c->argc,flags); } ... }
我们再来看一下propagate的实现:
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc, int flags) { // 传播到 AOF if (server.aof_state != REDIS_AOF_OFF && flags & REDIS_PROPAGATE_AOF) feedAppendOnlyFile(cmd,dbid,argv,argc); // 传播到 slave if (flags & REDIS_PROPAGATE_REPL) replicationFeedSlaves(server.slaves,dbid,argv,argc); }
我们再来看一下feedAppendOnlyFile的实现:
void feedAppendOnlyFile(struct redisCommand…{ if (dictid != server.aof_selected_db) {//当前操作的db与上一次不一样,所以要重新写一个新的select db命令,当rewrite的时候也会把appendseldb置为-1 char seldb[64]; snprintf(seldb,sizeof(seldb),"%d",dictid); buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n", (unsigned long)strlen(seldb),seldb); server.aof_selected_db = dictid; } …
buf = catAppendOnlyGenericCommand(buf,argc,argv); //转换为标准命令格式 server.aofbuf = sdscatlen(server.aofbuf,buf,sdslen(buf)); //将命令写到aofbuf,这个buf会在serverCron当Appendfsync到满足时fsync到文件 if (server.bgrewritechildpid != -1) //如果有bgrewrite子进程的话,则也必须把该命令保存到bgrewritebuf,以便在子进程结束时,把新的变更追加到rewrite后的文件 server.bgrewritebuf = sdscatlen(server.bgrewritebuf,buf,sdslen(buf)); … }
可以看到到上面AOF操作也只是写到buf中,并没有将其写到文件中,下面我们将查看写到文件中的过程。通过查看代码我们可以知道flushAppendOnlyFile()函数是进行真正的写入文件操作。另外我们可以知道该函数会在beforeSleep及serverCron中调用。其中beforeSleep是aeMain循环,每次进行事件处理前必须调用一次:
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
/* This function gets called every time Redis is entering the * main loop of the event driven library, that is, before to sleep * for ready file descriptors. */ // 每次处理事件之前执行 void beforeSleep(struct aeEventLoop *eventLoop) { ... /* Write the AOF buffer on disk */ // 将 AOF 缓冲区的内容写入到 AOF 文件 flushAppendOnlyFile(0); ... }
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... // 根据 AOF 政策, // 考虑是否需要将 AOF 缓冲区中的内容写入到 AOF 文件中 /* AOF postponed flush: Try at every cron cycle if the slow fsync * completed. */ if (server.aof_flush_postponed_start) flushAppendOnlyFile(0); ... }
下面我们来看一下该函数flushAppendOnlyFile的实现
通过上面的介绍我们可以知道即使Appendfsync设置为alway,并不是每次执行完一条更新命令就直接写(write+fsync)aof file,这个过程(write+fsync)会被推迟到事件处理流程结束后beforeSleep后进行(一个疑问先写到server.aofbuf,然后再写到数据文件,过程中如果crash会不会丢数据呢? 答案是:不会,因为在一次事件处理结束之后会调用beforeSleep进行flash,而它也是在下一次事件处理之前完成的,即只有在同步到文件之后才会给客户端回复成功与否);如果在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.后面自动运行rewrite
该操作涉及的配置:auto-aof-rewrite-percentage,auto-aof-rewrite-min-size。
该过程是在serverCron里判断,是满足到达运行bgrewrite的时机:
3. 客户端发送bgrewriteaof命令
通过查找readonlyCommandTable表,我们可以看到当客户端发送bgrewriteaof命令过来的时候,服务器调用bgrewriteaofCommand函数来进行处理。该函数会判断当前是否已经有bgrewritechildpid存在,或者bgsavechildpid存在则标志server.aofrewrite_scheduled = 1,需要进行bgrewrite,但不是现在,而是在serverCron处理的时候。否则则直接调用rewriteAppendOnlyFileBackground,创建bgrewrite进程,进行rewrite操作。
rewriteAppendOnlyFileBackground实现如下:
接下来我们看一下子进程是如何完成该工作的:
至此子进程完成rewrite操作。那么父进程也就是主线程是在什么时候获得子进程退出状态,并且做了些什么操作?
在上面的serverCron中可以看到:
// 接收子进程发来的信号,非阻塞 if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { int exitcode = WEXITSTATUS(statloc); int bysignal = 0; if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc); // BGSAVE 执行完毕 if (pid == server.rdb_child_pid) { backgroundSaveDoneHandler(exitcode,bysignal); // BGREWRITEAOF 执行完毕 } else if (pid == server.aof_child_pid) { backgroundRewriteDoneHandler(exitcode,bysignal); } else { redisLog(REDIS_WARNING, "Warning, detected child with unmatched pid: %ld", (long)pid); } updateDictResizePolicy();
即父进程在serverCron里通过server.bgrewritechildpid来判断是否需要等待子进程退出的信号。
进一步我们来看一下backgroundRewriteDoneHandler作了哪些操作:(注意这里是AOF的难点,使用了很强的技巧,反正我是看了好半天,才略懂)
关于backgroundRewriteDoneHandler其中为什么这么做,可以参考文章:http://www.hoterran.info/redis-aof-backgroud-thread。
http://www.cnblogs.com/lukexwang/p/4705393.html