Permalink
Browse files

aof modify

  • Loading branch information...
hoterran committed Aug 4, 2012
1 parent b1905f7 commit 830fdad94bacaa8ade64071a4fb35b248243d655
Showing with 241 additions and 0 deletions.
  1. +241 −0 redis-aof.md
View
@@ -0,0 +1,241 @@
+#AOF
+
+
+aof 原理有点类似 redo log。每次执行命令后如果数据发生了变化(server.dirty发生了变化),会接着调用 feedAppendOnlyFile。
+
+ void call(RedisClient *c, struct RedisCommand *cmd) {
+ long long dirty;
+ dirty = server.dirty;
+ cmd->proc(c); //执行命令
+ dirty = server.dirty-dirty;
+ if (server.appendonly && dirty)
+ feedAppendOnlyFile(cmd,c->db->id,c->argv,c->argc);
+
+feedAppendOnlyFile并非把直接存入 aof 文件,而是先把命令存储到 server.aofbuf 里。
+
+ void feedAppendOnlyFile(struct RedisCommand *cmd, int dictid, robj **argv, int argc) {
+
+ buf = catAppendOnlyGenericCommand(buf,argc,argv);
+ server.aofbuf = sdscatlen(server.aofbuf,buf,sdslen(buf));
+ ....
+ if (server.bgrewritechildpid != -1)
+ server.bgrewritebuf = sdscatlen(server.bgrewritebuf,buf,sdslen(buf));
+ }
+
+在这个过程中,如果存在 bgrewritechild 进程,变化数据还会写到 server.bgrewritebuf 里。
+
+待到接下来的循环的 before_sleep 函数会通过 flushAppendOnlyFile 函数把 server.aofbuf 里的数据 write 到 append file 里。
+
+ void flushAppendOnlyFile(void) {
+ ....
+ nwritten = write(server.appendfd,server.aofbuf,sdslen(server.aofbuf));
+
+redis.conf里配置每次 write 到 append file 后,fsync的规则,fsync的作用大家都知道,把从page cache刷新到disk。
+
+ #appendfsync always
+ appendfsync everysec
+ #appendfsync no
+
+该参数的原理 MySQL 的 innodb_flush_log_at_trx_commit 一样,是个较影响io的一个参数,需要在高性能和不丢数据之间做 trade-off。软件的优化就是 trade-off的过程,没有银弹,默认选用的是 everysec,每次 fsync 会记录时间,距离上次 fsync 超过1s,则会再次触发fsync。
+
+ /* Fsync if needed */
+ now = time(NULL);
+ if (server.appendfsync == APPENDFSYNC_ALWAYS ||
+ (server.appendfsync == APPENDFSYNC_EVERYSEC &&
+ now-server.lastfsync > 1))
+ {
+ /* aof_fsync is defined as fdatasync() for Linux in order to avoid
+ * flushing metadata. */
+ aof_fsync(server.appendfd); /* Let's try to get this data on the disk */
+ server.lastfsync = now;
+ }
+
+Redis.conf里的no-appendfsync-on-rewrite参数的意义是,如果在做rdb或者bgrewrite过程中,不会对aof文件进行fsync,这样对磁盘的写入操作不会因为要写 rdb 和 aof 两个文件而快速的摆动磁头,减少了寻道时间,让rdb、bgrewrite可以快速的完成,但这样同时增加了风险,因为生成rdb的时间还是比较长的。如果在这过程中os crash,部分aof数据还在page cache里,但还未写入到disk上。
+
+ if (server.no_appendfsync_on_rewrite &&
+ (server.bgrewritechildpid != -1 || server.bgsavechildpid != -1))
+ return; //跳出这个函数,不再进行fsync
+
+另外为什么Redis采用这种模式,每次写完内存,再写到server.aofbuf,而不是直接写到aof文件内,这是一个很多很大的性能优化,因为一次循环可能接收多次网络请求,所以的变化都合并到aofbuf里,然后再写入文件里,把多次的小io,转化成一次连续的大io,这也是常规的数据库优化方法。
+
+那么既然先写到server.aofbuf,写入aof文件之前,Redis crash会不会丢数据呢?答案是不会,为何?我先看看网络事件库如何处理读写事件
+
+ if (fe->mask & mask & AE_READABLE) {
+ rfired = 1;
+ fe->rfileProc(eventLoop,fd,fe->clientData,mask);
+ }
+ if (fe->mask & mask & AE_WRITABLE) {
+ if(!rfired||fe->wfileProc!=fe->rfileProc)
+ fe->wfileProc(eventLoop,fd,fe->clientData,mask);
+ }
+
+rfired变量决定了在同一次文件事件循环内,如果对于某个fd触发了可读事件的函数,不会再次触发写事件。我们来看函数执行的简化步骤:
+* readQueryFromClient()
+* call()
+* feedAppendOnlyFile()
+* 因为rfired原因退出本次循环 下一次循环
+* beforeSleep()-->flushAppendOnlyFile()
+* aeMain()--->sendReplyToClient()
+
+只有执行完了flush之后才会通知客户端数据写成功了,所以如果在feed和flush之间crash,客户接会因为进程退出接受到一个fin包,也就是一个错误的返回,所以数据并没有丢,只是执行出了错。
+
+Redis crash后,重启除了利用 rdb 重新载入数据外,还会读取append file(Redis.c 1561)加载镜像之后的数据。
+
+
+##如何激活aof
+
+
+激活aof,可以在Redis.conf配置文件里设置
+
+ appendonly yes
+
+也可以通过config命令在运行态启动aof
+
+ cofig set appendonly yes
+
+每次激活 aof ,调用函数 startAppendOnly(aof.c)必然的做执行一次 bgrewriteaof ,生成一个 aof 文件,并强制刷 fsync。这样做保证了aof文件在任何时候数据都是完整的。
+
+ int startAppendOnly(void) {
+ ....
+ if (rewriteAppendOnlyFileBackground() == REDIS_ERR) {
+ ....
+
+一旦开启 aof,则 Redis 重启后只会读取 aof 文件(Redis.c),而无视rdb文件的存在。
+
+Redis关闭(Redis.c)之时也会强制的刷一次fsync。
+
+ int prepareForShutdown() {
+ ....
+ if (server.appendonly) {
+ /* Append only file: fsync() the AOF and exit */
+ aof_fsync(server.appendfd);
+ ....
+
+##bgrewriteaof
+
+
+aof 的一个问题就是随着时间 append file 会变的很大,比如一个做 incr 的 key,aof 文件里记录的都是从1到N的自增的过程,其实我们只要保存最后的值即可。
+
+所以我们需要 bgrewriteaof 命令重新整理文件,只保留最新的key-value数据,会调用 rewriteAppendOnlyFile 这个函数,该函数与 rdbSave 工作原理类似。保存全库的kv数据,但aof数据未压缩,而且是明文存储。
+
+ int rewriteAppendOnlyFile(char *filename) {
+ snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
+ fp = fopen(tmpfile,"w");
+ for (j = 0; j < server.dbnum; j++) {
+ if (o->type == REDIS_STRING) {
+ //set
+ } else if (o->type == REDIS_LIST) {
+ //rpush
+ } else if (o->type == REDIS_SET) {
+ //sadd
+ } else if (o->type == REDIS_ZSET) {
+ //zadd
+ } else if (o->type == REDIS_HASH) {
+ //hset
+ }
+
+等 bgrewritecihld 进程完成快照退出之时(Redis.c),再调用 backgroundRewriteDoneHandler 函数
+
+ if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
+ if (pid == server.bgsavechildpid) {
+ backgroundSaveDoneHandler(statloc);
+ } else {
+ backgroundRewriteDoneHandler(statloc);
+ }
+
+backgroundRewriteDoneHandler 处理 bgrewriteaof 生成的临时文件,合并 bgrewritebuf 和临时文件两部分数据后,就生成了新的 aof 文件,并做一次强制的 fsync。
+
+ void backgroundRewriteDoneHandler(int statloc) {
+ ....
+ fd = open(tmpfile,O_WRONLY|O_APPEND);
+ ....
+ write(fd,server.bgrewritebuf,sdslen(server.bgrewritebuf)
+ ....
+ rename(tmpfile,server.appendfilename)
+ ....
+ if (server.appendfsync != APPENDFSYNC_NO) aof_fsync(fd)
+ ...
+
+合并 bgrewritebuf 很重要,否则最终的 aof 文件里的数据和内存里的数据就不一致了。
+
+##aof文件格式
+
+
+我们来看看aof文件的格式,知道aof的格式可以很方便解析他,可以自己实现更加异步的(Redis的复制已经是异步模式了)的复制技术。aof文件是ascii格式的,意味着可以明文的读取,而rdb文件可能是经过压缩的,所以即便aof文件做过 bgrewriteaof,aof 文件也是远大于rdb 文件。
+
+ *参数的个数\r\n
+ $参数1的长度\r\n
+ 参数1\r\n
+
+ $参数N的长度\r\n
+ 参数N\r\n
+
+例如一个 "set a 1" 的命令放到aof文件里的格式就是这样的。
+
+ *3^M
+ $3^M
+ set^M
+ $1^M
+ a^M
+ $1^M
+ 1^M
+
+执行命令前后,server.dirty 发生变化的命令,才会存储到 aof 文件里。
+
+如果出现事务,多个命令会在exec后出现在aof文件里,例如一个mulit,set a 1, set b 2,exec命令之后的文件格式如下。
+
+ *1^M
+ $5^M
+ MULTI^M
+ *3^M
+ $3^M
+ set^M
+ $1^M
+ a^M
+ $1^M
+ 1^M
+ *3^M
+ $3^M
+ set^M
+ $1^M
+ b^M
+ $1^M
+ 2^M
+ *1^M
+ $4^M
+ exec^M
+
+Redis-check-aof 这个 binary 可以用来检测 aof 文件的合法性。原理简单,先读取×后的数字,确定参数格式,再读取$后参数的长度,再读取参数。对于事务需要额外的处理,出现multi的地方必须要出现exec。
+
+当Redis出现crash,Redis重启的时候(Redis.c),如果激活了aof,则会查找aof文件,并载入这个aof文件。
+
+ if (server.appendonly) {
+ if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)
+ RedisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);
+ } else {
+ if (rdbLoad(server.dbfilename) == REDIS_OK)
+ RedisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",time(NULL)-start);
+ }
+
+载入 aof 文件的方法很有意思,先是创建一个 fake client ,这个 client 并非通过网络连接上来的客户端,而是伪造的一个对象,把 aof 文件里的命令一一保存在 client 的 arc,argv 里,使得这 client 像是某个网络连接连接上来,发送消息给服务端一样,这样处理 aof 里命令的方法就可以重用,而不需要额外的编写程序了。
+
+ int loadAppendOnlyFile(char *filename) {
+ ....
+ fakeClient = createFakeClient();
+ while(1) {
+ fgets(buf,sizeof(buf),fp)
+ argc = atoi(buf+1); //命令参数格式
+ argv = zmalloc(sizeof(robj*)*argc);
+ for (j = 0; j < argc; j++) {
+ ....
+ }
+ cmd = lookupCommand(argv[0]->ptr);
+ fakeClient->argc = argc;
+ fakeClient->argv = argv;
+ cmd->proc(fakeClient); //模拟客户端执行命令
+ ....
+ }
+ freeFakeClient(fakeClient);
+ ...
+ }
+

0 comments on commit 830fdad

Please sign in to comment.