redis 源码系列(15):快照 + 增量记录 --- AOF

在介绍 rdb 的文章中,我们提到 rdb 并不能完美的解决 redis 持久化的问题。因为其只是 redis 数据库的某一时刻的快照,而且因为 rdb 的 dump 过程往往会消耗大量的服务器资源,所以一般都是在业务低峰期进行,而且不会很频繁,一般都是以天为单位来进行。redis 在 dump 两次 rdb 之间,可能会丢失大量的数据。

所以 redis 提供了另一种持久化手段,即 append only file(AOF),也是今天的主角。不同于 rdb 是数据库某一刻的状态快照,AOF 以日志的形式,将数据库的数据将命令的格式保存到文件,在 reload 的时候只需要 replay AOF 中的命令即可。

AOF

传播命令

在 redis.c 中的 有一个传播函数,负责将 redis 处理的命令传播到 aof 和 slave 中:

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);
}

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    robj *tmpargv[3];
		// 如果当前命令需要更换 redis database,先写入一个 select command
    if (dictid != server.aof_selected_db) {
        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;
    }
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
  			// 将 expire、pexpire、expireat 命令都转为等价的 PEXPIREAT 命令
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
    } else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
        // 将 setex 和 psetex 命令转换为 SET + PEXPIREAT 命令
        tmpargv[0] = createStringObject("SET",3);
        tmpargv[1] = argv[1];
        tmpargv[2] = argv[3];
        buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
        // PEXPIREAT
        decrRefCount(tmpargv[0]);
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
    } else {
        // 其他命令都不需要转换
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }

    // 将重新构建的命令写入到 AOF 缓存内
    if (server.aof_state == REDIS_AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));

    // 如果后台的 aof rewrite 正在进行,我们还需要将数据写入到 aof rewrite buffer list 中
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

    // 释放
    sdsfree(buf);
}

sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;
    // 重建命令的个数,格式为 *<count>\r\n
    buf[0] = '*';
    len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
    buf[len++] = '\r';
    buf[len++] = '\n';
    dst = sdscatlen(dst,buf,len);
    // 将所有命令转化为字符串表示,并且拼接起来
    // 单条命令的格式:$<length>\r\n<content>\r\n
    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);
        // 组合 $<length>\r\n
        buf[0] = '$';
        len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
        buf[len++] = '\r';
        buf[len++] = '\n';
        dst = sdscatlen(dst,buf,len);
        // 组合 <content>\r\n
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"\r\n",2);
        decrRefCount(o);
    }
    // 返回重建后的协议内容
    return dst;
}

写入 AOF 缓存到文件

每次在 redis 进入 eventloop 等待处理客户端请求前,都会检查是否需要 flush aof 缓存:

void beforeSleep(struct aeEventLoop *eventLoop) {
    REDIS_NOTUSED(eventLoop);
    // 略过不想干代码
    // 将 AOF 缓冲区的内容写入到 AOF 文件
    flushAppendOnlyFile(0);
    /* Call the Redis Cluster before sleep function. */
    // 在进入下个事件循环前,执行一些集群收尾工作
    if (server.cluster_enabled) clusterBeforeSleep();
}
// flush aof buf 到 aof 文件
void flushAppendOnlyFile(int force) {
    ssize_t nwritten;
    int sync_in_progress = 0;
    // 缓冲区中没有任何内容,直接返回
    if (sdslen(server.aof_buf) == 0) return;
    // 策略为每秒 FSYNC 
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        // 是否有 SYNC 正在后台进行?
        sync_in_progress = bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC) != 0;

    // 每秒 fsync ,并且强制写入为假
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
        // 如果后台有尚未进行的 AOF 任务,而且这次不是强制写入,我们可能 delay flush
        if (sync_in_progress) {
            if (server.aof_flush_postponed_start == 0) {
                // 如果没有 delay 的 flush 任务,我们设置 aof_flush_postponed_start 为当前时间
                // 本次 flush 操作被 delay
                server.aof_flush_postponed_start = server.unixtime;
                return;

            } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
                // 如果之前的 delay 操作距离现在不足 2s,我们也不处理本次 flush
                return;

            }
					  // 否则即使后台有待完成的任务,我们也要强制 flush,记录 delayed fsync
            server.aof_delayed_fsync++;
            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.");
        }
    }
    // 清除 delay flush 的标示
    server.aof_flush_postponed_start = 0;
		// 将 aof buf 写入到文件中,在一个正常的文件系统中,这个写入应该是原子的,而且不应该出现 short write
    // 注意 write 和 sync 是不同的,write 是无法保证写入数据已经落盘的
    nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
    if (nwritten != (signed)sdslen(server.aof_buf)) {
        // short write 或者出错,需要处理
        static time_t last_write_error_log = 0;
        int can_log = 0;
        // 将日志的记录频率限制在每行 AOF_WRITE_LOG_ERROR_RATE 秒
        if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
            can_log = 1;
            last_write_error_log = server.unixtime;
        }
        if (nwritten == -1) {
            // 如果写入出错,那么尝试将该情况写入到日志里面
            if (can_log) {
                redisLog(REDIS_WARNING,"Error writing to the AOF file: %s",
                    strerror(errno));
                server.aof_last_write_errno = errno;
            }
        } else {
            // short write,如果需要记录日志的话,输出日志
            if (can_log) {
                redisLog(REDIS_WARNING,"Short write while writing to "
                                       "the AOF file: (nwritten=%lld, "
                                       "expected=%lld)",
                                       (long long)nwritten,
                                       (long long)sdslen(server.aof_buf));
            }
            // 尝试移除新追加的不完整内容,保持 AOF 的有效性
            if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
                // 移除失败,记录日志
                if (can_log) {
                    redisLog(REDIS_WARNING, "Could not remove short write "
                             "from the append-only file.  Redis may refuse "
                             "to load the AOF the next time it starts.  "
                             "ftruncate: %s", strerror(errno));
                }
            } else {
                nwritten = -1;
            }
            server.aof_last_write_errno = ENOSPC;
        }
        if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
            // 如果 aof sync 策略是 always 的话,这个错误是无法恢复的
            // 因为这个配置代表了我们希望每一次写入 aof 文件,都必须进行成功的 sync,直接退出程序
            redisLog(REDIS_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
            exit(1);
        } else {
            // 其他 sync 策略则可以恢复,我们设置 aof_last_write_status
            // redis 在下次成功写入之前不再接受 write 类型的命令
            server.aof_last_write_status = REDIS_ERR;
            // 如果发生了short write,而且 truncate 也失败了,那么更新 aof_buf 和 aof_current_size
            if (nwritten > 0) {
                server.aof_current_size += nwritten;
                sdsrange(server.aof_buf,nwritten,-1);
            }
            return; /* We'll try again on the next call... */
        }
    } else {
        // 写入成功,更新最后写入状态
        if (server.aof_last_write_status == REDIS_ERR) {
            redisLog(REDIS_WARNING,
                "AOF write error looks solved, Redis can write again.");
            server.aof_last_write_status = REDIS_OK;
        }
    }

    // 更新写入后的 AOF 文件大小
    server.aof_current_size += nwritten;
		// 我们不希望 aof_buf 占用太多内存,如果占用内存不超过 4000 bytes,就复用当前 aof_buf
    // 否则释放当前 aof_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_no_fsync_on_rewrite 而且后台有正在进行的 aof rewrite 或者 bgsave,不要进行 fsync
    if (server.aof_no_fsync_on_rewrite &&
        (server.aof_child_pid != -1 || server.rdb_child_pid != -1))
            return;

    // 如果满足条件,执行 fsync 
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        // 每次写入 aof 都执行 sync,那么我们不能将 fsync 放到后台线程
        // 因为 always 的语意是说每次 flush 调用后,数据必然已经完成落盘
        aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */
        // 更新最后一次执行 fsnyc 的时间
        server.aof_last_fsync = server.unixtime;
    } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
                server.unixtime > server.aof_last_fsync)) {
        // 策略为每秒 fsnyc ,并且距离上次 fsync 已经超过 1 秒,如果后台没有 sync 任务,创建后台任务
        if (!sync_in_progress) aof_background_fsync(server.aof_fd);
        // 更新最后一次执行 fsync 的时间
        server.aof_last_fsync = server.unixtime;
    }
}

void aof_background_fsync(int fd) {
    bioCreateBackgroundJob(REDIS_BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL);
}

要注意,向 AOF 缓存添加数据是发生在我们时间循环处理中处理客户端命令时,但是每次 flushAppendOnlyFile 则是发生在进入 eventloop(beforeSleep)前。所以我们在某个命令中对 AOF 的修改,要在命令全部处理完后,下一次时间循环前,才可以进行写入。这也是 redis 不具有(100%)持久性的原因,因为它的写入和处理命令是异步的,即使启用 always sync 的策略,仍然不能保证没有数据丢失。

关于 flush 函数还有一个很重要的优化点,就是将 fsync 放到后台线程(是的,虽然我一直说 redis 是单线程,但是我撒谎了)去做,这么做是为了避免 fsync 阻塞主线程太多时间。

bio

redis 将一些与本地文件相关的阻塞操作(close,fsync),放到了后台线程中去处理,来避免对主线程长时间的阻塞,相关代码在 src/bio.c 中。

bio 相关接口的实现是非常简单以及直观的:

static pthread_t bio_threads[REDIS_BIO_NUM_OPS];
static pthread_mutex_t bio_mutex[REDIS_BIO_NUM_OPS];
static pthread_cond_t bio_condvar[REDIS_BIO_NUM_OPS];
// 待处理任务
static list *bio_jobs[REDIS_BIO_NUM_OPS];
// 记录每种类型 job 队列里有多少 job 等待执行
static unsigned long long bio_pending[REDIS_BIO_NUM_OPS];
// 待处理任务结构体
struct bio_job {
    // 任务创建时的时间
    time_t time; /* Time at which the job was created. */
    // 任务参数
    void *arg1, *arg2, *arg3;
};

void *bioProcessBackgroundJobs(void *arg);
// 子线程栈大小
#define REDIS_THREAD_STACK_SIZE (1024*1024*4)
// 初始化 bio 模块
void bioInit(void) {
    pthread_attr_t attr;
    pthread_t thread;
    size_t stacksize;
    int j;
    // 每一种 bio 任务需要创建一个后台线程,初始化其对应的资源 mutex、cond
    for (j = 0; j < REDIS_BIO_NUM_OPS; j++) {
        pthread_mutex_init(&bio_mutex[j],NULL);
        pthread_cond_init(&bio_condvar[j],NULL);
        bio_jobs[j] = listCreate();
        bio_pending[j] = 0;
    }
		// 设置栈大小
    pthread_attr_init(&attr);
    pthread_attr_getstacksize(&attr,&stacksize);
    if (!stacksize) stacksize = 1; /* The world is full of Solaris Fixes */
    while (stacksize < REDIS_THREAD_STACK_SIZE) stacksize *= 2;
    pthread_attr_setstacksize(&attr, stacksize);

    // 为每一个任务创建后台线程
    for (j = 0; j < REDIS_BIO_NUM_OPS; j++) {
        void *arg = (void*)(unsigned long) j;
        if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
            redisLog(REDIS_WARNING,"Fatal: Can't initialize Background Jobs.");
            exit(1);
        }
        bio_threads[j] = thread;
    }
}
// 创建后台任务
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
    struct bio_job *job = zmalloc(sizeof(*job));
    job->time = time(NULL);
    job->arg1 = arg1;
    job->arg2 = arg2;
    job->arg3 = arg3;
    // 加锁,将任务推入队列,等待对应后台线程处理
    pthread_mutex_lock(&bio_mutex[type]);
    listAddNodeTail(bio_jobs[type],job);
    bio_pending[type]++;
    pthread_cond_signal(&bio_condvar[type]);
    pthread_mutex_unlock(&bio_mutex[type]);
}
// 后台处理线程
void *bioProcessBackgroundJobs(void *arg) {
    struct bio_job *job;
    unsigned long type = (unsigned long) arg;
    sigset_t sigset;
    // 主动 kill 一个 thread 是很少见的,一般并不推荐使用这种方式
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    pthread_mutex_lock(&bio_mutex[type]);
    // 屏蔽 SIGALRM,确保主线程处理 SIGALRM(多线程和信号真的是很难办)
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGALRM);
    if (pthread_sigmask(SIG_BLOCK, &sigset, NULL))
        redisLog(REDIS_WARNING,
            "Warning: can't mask SIGALRM in bio.c thread: %s", strerror(errno));
		// 死循环,开始处理任务
    while(1) {
        listNode *ln;
        // 等待新任务
        if (listLength(bio_jobs[type]) == 0) {
            pthread_cond_wait(&bio_condvar[type],&bio_mutex[type]);
            continue;
        }
				// 从任务队列取出任务
        ln = listFirst(bio_jobs[type]);
        job = ln->value;
        pthread_mutex_unlock(&bio_mutex[type]);
				// 执行对应任务
        if (type == REDIS_BIO_CLOSE_FILE) {
            close((long)job->arg1);
        } else if (type == REDIS_BIO_AOF_FSYNC) {
            aof_fsync((long)job->arg1);
        } else {
            redisPanic("Wrong job type in bioProcessBackgroundJobs().");
        }

        zfree(job);
        /* Lock again before reiterating the loop, if there are no longer
         * jobs to process we'll block again in pthread_cond_wait(). */
        pthread_mutex_lock(&bio_mutex[type]);
        // 将执行完成的任务从队列中删除,并减少任务计数器
        listDelNode(bio_jobs[type],ln);
        bio_pending[type]--;
    }
}
// 查看 type 对应的 pending 后台任务
unsigned long long bioPendingJobsOfType(int type) {
    unsigned long long val;

    pthread_mutex_lock(&bio_mutex[type]);
    val = bio_pending[type];
    pthread_mutex_unlock(&bio_mutex[type]);
    return val;
}

// 杀死线程
void bioKillThreads(void) {
    int err, j;

    for (j = 0; j < REDIS_BIO_NUM_OPS; j++) {
        if (pthread_cancel(bio_threads[j]) == 0) {
            if ((err = pthread_join(bio_threads[j],NULL)) != 0) {
                redisLog(REDIS_WARNING,
                    "Bio thread for job type #%d can be joined: %s",
                        j, strerror(err));
            } else {
                redisLog(REDIS_WARNING,
                    "Bio thread for job type #%d terminated",j);
            }
        }
    }
}

bio 为每一个后台 blocking io 操作都创建了一个后台线程,并且使用一个简单的任务队列与主线程通信。可以说是一个非常简单的类似线程池的实现,对于任何有多线程编程经验的人,上述的代码都不会有陌生的地方。

AOF load

如果开启了 AOF , redis 启动的时候就会从 AOF 文件恢复数据:

void loadDataFromDisk(void) {
    // 记录开始时间
    long long start = ustime();
    if (server.aof_state == REDIS_AOF_ON) {
        // AOF 持久化已打开?尝试载入 AOF 文件
        if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)
            // 打印载入信息,并计算载入耗时长度
            redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        // AOF 持久化未打开 尝试载入 RDB 文件
        if (rdbLoad(server.rdb_filename) == REDIS_OK) {
            // 打印载入信息,并计算载入耗时长度
            redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        } else if (errno != ENOENT) {
            redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}

int loadAppendOnlyFile(char *filename) {
    // 执行命令需要客户端,构造一个伪客户端
    struct redisClient *fakeClient;
    // 打开 AOF 文件
    FILE *fp = fopen(filename,"r");
    struct redis_stat sb;
    int old_aof_state = server.aof_state;
    long loops = 0;
    // 检查文件的正确性
    if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {
        server.aof_current_size = 0;
        fclose(fp);
        return REDIS_ERR;
    }
    // 检查文件是否正常打开
    if (fp == NULL) {
        redisLog(REDIS_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
        exit(1);
    }
		// 关闭 aof 标志,防止执行过程中,又有命令传播到 aof
    server.aof_state = REDIS_AOF_OFF;
    fakeClient = createFakeClient();
    // 设置服务器的状态为:正在载入
    startLoading(fp);
    while(1) {
        int argc, j;
        unsigned long len;
        robj **argv;
        char buf[128];
        sds argsds;
        struct redisCommand *cmd;
        // 间隔性的处理客户端请求,但是处于 loading 态的 redis 只能执行部分命令
        if (!(loops++ % 1000)) {
            loadingProgress(ftello(fp));
            processEventsWhileBlocked();
        }
        // 读入文件内容到缓存
        if (fgets(buf,sizeof(buf),fp) == NULL) {
            if (feof(fp))
                // 文件已经读完,跳出
                break;
            else
                goto readerr;
        }
        // 确认协议格式,比如 *3\r\n
        if (buf[0] != '*') goto fmterr;
        // 取出命令参数,比如 *3\r\n 中的 3
        argc = atoi(buf+1);
        // 至少要有一个参数(被调用的命令)
        if (argc < 1) goto fmterr;
        // 从文本中创建字符串对象:包括命令,以及命令参数
        // 例如 $3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
        // 将创建三个包含以下内容的字符串对象:
        // SET 、 KEY 、 VALUE
        argv = zmalloc(sizeof(robj*)*argc);
        for (j = 0; j < argc; j++) {
            if (fgets(buf,sizeof(buf),fp) == NULL) goto readerr;

            if (buf[0] != '$') goto fmterr;

            // 读取参数值的长度
            len = strtol(buf+1,NULL,10);
            // 读取参数值
            argsds = sdsnewlen(NULL,len);
            if (len && fread(argsds,len,1,fp) == 0) goto fmterr;
            // 为参数创建对象
            argv[j] = createObject(REDIS_STRING,argsds);

            if (fread(buf,2,1,fp) == 0) goto fmterr; /* discard CRLF */
        }
				// 查找命令对应的 command 结构体
        cmd = lookupCommand(argv[0]->ptr);
        if (!cmd) {
            redisLog(REDIS_WARNING,"Unknown command '%s' reading the append only file", (char*)argv[0]->ptr);
            exit(1);
        }
				// 通过伪客户端执行命令,redis 命令的执行必须通过客户端
        fakeClient->argc = argc;
        fakeClient->argv = argv;
        cmd->proc(fakeClient);
        // 伪客户端不应该有返回数据
        redisAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
        // 不应该处于阻塞
      	redisAssert((fakeClient->flags & REDIS_BLOCKED) == 0);
				// 清理数据
        for (j = 0; j < fakeClient->argc; j++)
            decrRefCount(fakeClient->argv[j]);
        zfree(fakeClient->argv);
    }
    // 执行完 AOF 命令,应该没有未完结的 MULTI 事务
    if (fakeClient->flags & REDIS_MULTI) goto readerr;
    // 关闭 AOF 文件
    fclose(fp);
    // 释放伪客户端
    freeFakeClient(fakeClient);
    // 复原 AOF 状态
    server.aof_state = old_aof_state;
    // 停止载入
    stopLoading();
    // 更新服务器状态中, AOF 文件的当前大小
    aofUpdateCurrentSize();
    // 记录前一次重写时的大小
    server.aof_rewrite_base_size = server.aof_current_size;
    return REDIS_OK;
// 读入错误
readerr:
    // 非预期的末尾,可能是 AOF 文件在写入的中途遭遇了停机
    if (feof(fp)) {
        redisLog(REDIS_WARNING,"Unexpected end of file reading the append only file");
    
    // 文件内容出错
    } else {
        redisLog(REDIS_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
    }
    exit(1);
// 内容格式错误
fmterr:
    redisLog(REDIS_WARNING,"Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>");
    exit(1);
}

在传播命令到 AOF 文件中我们就看到了,AOF 数据就是编码以后的命令文本。加载 AOF 文件的过程就是通过 replay 这些命令从新构建数据库的过程。

AOF rewrite

AOF 通过不断的传播命令到文件中,必然会导致文件大小的不断膨胀,加载时间也会不断延长。而且 AOF 中的数据会存在很多不必要的冗余,比如对同一个 key 的重复赋值,并不需要保留对其的所有赋值命令,而只需要保留最后一个赋值命令就可以了。所以 redis 实现了 AOF rewrite 机制,来定期重写 AOF 文件.

AOF rewrite buffer

当 AOF rewrite 正在进行时,redis 会将 AOF 开始后产生的 AOF 数据写入到 AOF rewrite buffer 中:

#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10)    /* 10 MB per block */

typedef struct aofrwblock {
    // 缓存块已使用字节数和可用字节数
    unsigned long used, free;
    // 缓存块
    char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;
// 重置 aof buffer list
void aofRewriteBufferReset(void) {
    // 释放旧有的缓存(链表)
    if (server.aof_rewrite_buf_blocks)
        listRelease(server.aof_rewrite_buf_blocks);
    // 初始化新的缓存(链表)
    server.aof_rewrite_buf_blocks = listCreate();
    listSetFreeMethod(server.aof_rewrite_buf_blocks,zfree);
}
// 返回当前 aof buffer 大小
unsigned long aofRewriteBufferSize(void) {
    // 计算公式 (len(bufferlist)-1) * AOF_RW_BUF_BLOCK_SIZE + used of last buffer
    // 取出链表中最后的缓存块
    listNode *ln = listLast(server.aof_rewrite_buf_blocks);
    aofrwblock *block = ln ? ln->value : NULL;
    // 没有缓存被使用
    if (block == NULL) return 0;
    // 总缓存大小 = (缓存块数量-1) * AOF_RW_BUF_BLOCK_SIZE + 最后一个缓存块的大小
    unsigned long size =
        (listLength(server.aof_rewrite_buf_blocks)-1) * AOF_RW_BUF_BLOCK_SIZE;
    size += block->used;
    return size;
}
// 向 buffer list 末尾添加缓存
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
    // 指向最后一个缓存块
    listNode *ln = listLast(server.aof_rewrite_buf_blocks);
    aofrwblock *block = ln ? ln->value : NULL;
    while(len) {
				// 如果 buffer list 末尾有未用完的 block,先写入到这个 block
        if (block) {
            unsigned long thislen = (block->free < len) ? block->free : len;
            if (thislen) {  /* The current block is not already full. */
                memcpy(block->buf+block->used, s, thislen);
                block->used += thislen;
                block->free -= thislen;
                s += thislen;
                len -= thislen;
            }
        }
				// 到这里,已经将 buffer list 原先可能未使用的 block 写满,
        // 如果 len != 0 那么需要申请新的 block
        if (len) { /* First block to allocate, or need another block. */
            int numblocks;
            // 分配缓存块
            block = zmalloc(sizeof(*block));
            block->free = AOF_RW_BUF_BLOCK_SIZE;
            block->used = 0;
            // 链接到链表末尾
            listAddNodeTail(server.aof_rewrite_buf_blocks,block);
						// 每创建 10 个 buffer block 就记录一次日志
            numblocks = listLength(server.aof_rewrite_buf_blocks);
            if (((numblocks+1) % 10) == 0) {
                int level = ((numblocks+1) % 100) == 0 ? REDIS_WARNING :
                                                         REDIS_NOTICE;
                redisLog(level,"Background AOF buffer size: %lu MB",
                    aofRewriteBufferSize()/(1024*1024));
            }
        }
    }
}
// 将 aof 数据写入到 fd
ssize_t aofRewriteBufferWrite(int fd) {
    listNode *ln;
    listIter li;
    ssize_t count = 0;
    // 遍历所有缓存块
    listRewind(server.aof_rewrite_buf_blocks,&li);
    while((ln = listNext(&li))) {
        aofrwblock *block = listNodeValue(ln);
        ssize_t nwritten;
        if (block->used) {
            // 写入缓存块内容到 fd
            nwritten = write(fd,block->buf,block->used);
            if (nwritten != block->used) {
                if (nwritten == 0) errno = EIO;
                return -1;
            }
            // 积累写入字节
            count += nwritten;
        }
    }
    return count;
}

下面来看如何执行 AOF rewrite

// AOF rewrite 的实现其实很简单,其与 rdb 有点相似,主要区别只是 dump 文件的编码方式
int rewriteAppendOnlyFile(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    rio aof;
    FILE *fp;
    char tmpfile[256];
    int j;
    long long now = mstime();
    // 与 rdb 文件一个套路,先创建 tmp 文件,rewrite 结束后再原子地改名 
    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;
    }
    // 初始化文件 io
    rioInitWithFile(&aof,fp);
    // 设置自动 sync 的字节上限
    if (server.aof_rewrite_incremental_fsync)
        rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);
    // 遍历所有数据库
    for (j = 0; j < server.dbnum; j++) {
        char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";
        redisDb *db = server.db+j;
        // 指向键空间
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;
        // 创建键空间迭代器
        di = dictGetSafeIterator(d);
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }
        // 先写入 select 命令
        if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
        if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;
        // 遍历 database 写入其内所有 key 
        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 (expiretime != -1 && expiretime < now) continue;
						// 根据值类型,构造不同的命令保存
            if (o->type == REDIS_STRING) {
                /* Emit a SET command */
                char cmd[]="*3\r\n$3\r\nSET\r\n";
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkObject(&aof,o) == 0) goto werr;
            } else if (o->type == REDIS_LIST) {
                if (rewriteListObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_SET) {
                if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_ZSET) {
                if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_HASH) {
                if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
            } else {
                redisPanic("Unknown object type");
            }
						// 有过期时间的话用 PEZPIREAT 保存过期时间
            if (expiretime != -1) {
                char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
                // 写入 PEXPIREAT expiretime 命令
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
            }
        }
        // 释放迭代器
        dictReleaseIterator(di);
    }
    // 确保写入内容落盘
    if (fflush(fp) == EOF) goto werr;
    if (aof_fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;
    // 原子改名
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    redisLog(REDIS_NOTICE,"SYNC append only file rewrite performed");
    return REDIS_OK;
werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
    if (di) dictReleaseIterator(di);
    return REDIS_ERR;
}
/*
	AOF rewrite 在后台启动的流程如下:
	1.用户调用 BGREWRITEAOF
	2.redis 调用 rewriteAppendOnlyFileBackground,fork rewrite aof 的子进程
	3.子进程完成后,通过信号通知 redis 进程
	4.如果子进程正常退出,将 rewrite buffer 里面的新数据写入到临时文件,完成后替换 AOF 文件
*/
int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;
    // 已经有进程在进行 AOF 重写了
    if (server.aof_child_pid != -1) return REDIS_ERR;
    // 记录 fork 开始前的时间,计算 fork 耗时用
    start = ustime();
    if ((childpid = fork()) == 0) {
        char tmpfile[256];
        // 关闭网络连接 fd
        closeListeningSockets(0);
        // 为进程设置名字,方便记认
        redisSetProcTitle("redis-aof-rewrite");
        // 创建临时文件,并进行 AOF 重写
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {
            size_t private_dirty = zmalloc_get_private_dirty();

            if (private_dirty) {
                redisLog(REDIS_NOTICE,
                    "AOF rewrite: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
            // 发送重写成功信号
            exitFromChild(0);
        } else {
            // 发送重写失败信号
            exitFromChild(1);
        }
    } else {
        // 记录执行 fork 所消耗的时间
        server.stat_fork_time = ustime()-start;

        if (childpid == -1) {
            redisLog(REDIS_WARNING,
                "Can't rewrite append only file in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        redisLog(REDIS_NOTICE,
            "Background append only file rewriting started by pid %d",childpid);
        // 记录 AOF 重写的信息
        server.aof_rewrite_scheduled = 0;
        server.aof_rewrite_time_start = time(NULL);
        server.aof_child_pid = childpid;
        // 关闭字典自动 rehash
        updateDictResizePolicy();
				// aof_selected_db 设置为 -1 会让后续传播命令到 rewrite buffer 的时候以一个 select 命令开始
        server.aof_selected_db = -1;
        replicationScriptCacheFlush();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}
// AOF rewrite 子进程退出以后父进程调用
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
    if (!bysignal && exitcode == 0) {
        // AOF rewrite 子进程正常退出
        int newfd, oldfd;
        char tmpfile[256];
        long long now = ustime();
        redisLog(REDIS_NOTICE,
            "Background AOF rewrite terminated with success");
        // 打开保存新 AOF 文件内容的临时文件
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof",
            (int)server.aof_child_pid);
        newfd = open(tmpfile,O_WRONLY|O_APPEND);
        if (newfd == -1) {
            redisLog(REDIS_WARNING,
                "Unable to open the temporary AOF produced by the child: %s", strerror(errno));
            goto cleanup;
        }
        // 当启动 AOF rewrite 后台进程之后,AOF 的数据也会写入到 AOF rewrite buffer 中
        // 将这些 AOF 新数据也写入到新的 AOF 文件中,注意在这个时候 redis 不会处理任何新任务
        // 所以 AOF rewrite 时是 redis 的快照数据,后续新加入的增量写数据,合并起来就可以重新构建数据库
        if (aofRewriteBufferWrite(newfd) == -1) {
            redisLog(REDIS_WARNING,
                "Error trying to flush the parent diff to the rewritten AOF: %s", strerror(errno));
            close(newfd);
            goto cleanup;
        }

        redisLog(REDIS_NOTICE,
            "Parent diff successfully flushed to the rewritten AOF (%lu bytes)", aofRewriteBufferSize());
				// 后续我们只需要将这个 tmp 文件 rename 即可完成 AOF rewrite,但是在 rename 时
        // 我们要避免因为 rename 导致删除原来的 AOF 文件,这个删除可能会阻塞我们的进程很长时间
        // 存在下述两种情况
        // 1. AOF 没有开启,那么当 rename 时,因为没有其他进程持有对 AOF 文件的引用,会删除原文件,导致阻塞
        // 2. AOF 已经开启,则 redis 服务持有对原 AOF 文件的引用,所以 rename 并不会导致删除原文件,只有当 close  oldfd 时候才会导致删除
        // 如果 redis 并未持有对原 AOF 文件的引用,打开文件增加文件引用
        if (server.aof_fd == -1) {
             // 不需要检查是否打开成功
             oldfd = open(server.aof_filename,O_RDONLY|O_NONBLOCK);
        } else {
            oldfd = -1; /* We'll set this to the current AOF filedes later. */
        }
				// rename 不会导致原 AOF 文件的删除
        if (rename(tmpfile,server.aof_filename) == -1) {
            redisLog(REDIS_WARNING,
                "Error trying to rename the temporary AOF file: %s", strerror(errno));
            close(newfd);
            if (oldfd != -1) close(oldfd);
            goto cleanup;
        }
				
        if (server.aof_fd == -1) {
            // 没有开启 AOF 的话,我们需要 close newfd,不需要担心这个会导致删除
            close(newfd);
        } else {
            // 设定新的 aof_fd
            oldfd = server.aof_fd;
            server.aof_fd = newfd;
            // 因为前面进行了 AOF 重写缓存追加,所以这里立即 fsync 一次
            if (server.aof_fsync == AOF_FSYNC_ALWAYS)
                aof_fsync(newfd);
            else if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
                aof_background_fsync(newfd);
            // 强制引发 SELECT
            server.aof_selected_db = -1; /* Make sure SELECT is re-issued */
            // 更新 AOF 文件的大小
            aofUpdateCurrentSize();
            // 记录前一次重写时的大小
            server.aof_rewrite_base_size = server.aof_current_size;
						// aof_buf 内的数据已经没有作用了,释放 aof_buf
            sdsfree(server.aof_buf);
            server.aof_buf = sdsempty();
        }

        server.aof_lastbgrewrite_status = REDIS_OK;
        redisLog(REDIS_NOTICE, "Background AOF rewrite finished successfully");
				// 更新 aof_state 为 REDIS_AOF_ON (仅发生在第一次)
        if (server.aof_state == REDIS_AOF_WAIT_REWRITE)
            server.aof_state = REDIS_AOF_ON;
				// 在后台线程 close 原 AOF 文件
        if (oldfd != -1) bioCreateBackgroundJob(REDIS_BIO_CLOSE_FILE,(void*)(long)oldfd,NULL,NULL);
        redisLog(REDIS_VERBOSE,
            "Background AOF rewrite signal handler took %lldus", ustime()-now);

    } else if (!bysignal && exitcode != 0) {
        // BGREWRITEAOF 重写出错
        server.aof_lastbgrewrite_status = REDIS_ERR;
        redisLog(REDIS_WARNING,
            "Background AOF rewrite terminated with error");
    } else {
        // 子进程因为接受到信号而退出
        server.aof_lastbgrewrite_status = REDIS_ERR;
        redisLog(REDIS_WARNING,
            "Background AOF rewrite terminated by signal %d", bysignal);
    }

cleanup:
    // 清空 AOF 缓冲区
    aofRewriteBufferReset();
    // 移除临时文件,进行到这里可能临时文件已经不存在了,所以后面的 remove 函数中不需要检查错误
    aofRemoveTempFile(server.aof_child_pid);
    // 重置默认属性
    server.aof_child_pid = -1;
    server.aof_rewrite_time_last = time(NULL)-server.aof_rewrite_time_start;
    server.aof_rewrite_time_start = -1;
    if (server.aof_state == REDIS_AOF_WAIT_REWRITE)
        server.aof_rewrite_scheduled = 1;
}

void aofRemoveTempFile(pid_t childpid) {
    char tmpfile[256];
    snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) childpid);
    unlink(tmpfile);
}

启动 BGREWRITEAOF 以后,redis 还会持续的接受客户端请求。当处理后续的写请求时,需要将这些命令的 AOF 编码数据写入到 AOF rewrite buffer 中,即 AOF rewrite buffer 中存储的是自上次 AOF rewrite 开始以后的所有 增量写操作。

关于 close AOF 文件时候的优化也值得学习,尽量避免在工作线程中执行阻塞操作的操作。

command

void bgrewriteaofCommand(redisClient *c) {
    if (server.aof_child_pid != -1) {
         // 不能重复运行 BGREWRITEAOF
        addReplyError(c,"Background append only file rewriting already in progress");
    } else if (server.rdb_child_pid != -1) {
      	// 如果正在执行 BGSAVE ,那么预定 BGREWRITEAOF
    		// 等 BGSAVE 完成之后, BGREWRITEAOF 就会开始执行
        server.aof_rewrite_scheduled = 1;
        addReplyStatus(c,"Background append only file rewriting scheduled");
    } else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {
      	// 执行 BGREWRITEAOF
        addReplyStatus(c,"Background append only file rewriting started");
    } else {
        addReply(c,shared.err);
    }
}

总结

  1. AOF 是 redis 提供的另一种数据持久化方案,其将 redis 服务处理的写请求编码后写入到 AOF 文件,启动时候加载 AOF 文件 replay 即可重建数据库
  2. AOF 相比 RDB 的粒度更细,通过配置不同的 sync 策略,redis 提供不同的数据持久化保证。但是需要注意的是,即使选用性能最差的 always,redis 还是不具有 100% 的数据持久性,因为其 AOF 写入发生在命令处理之后
  3. AOF 和 rdb 一样,为了防止阻塞 redis,将 close 操作放到了后台线程
  4. AOF rewrite 可以对 AOF 文件进行瘦身,防止 AOF 文件不断膨胀
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值