在介绍 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);
}
}
总结
- AOF 是 redis 提供的另一种数据持久化方案,其将 redis 服务处理的写请求编码后写入到 AOF 文件,启动时候加载 AOF 文件 replay 即可重建数据库
- AOF 相比 RDB 的粒度更细,通过配置不同的 sync 策略,redis 提供不同的数据持久化保证。但是需要注意的是,即使选用性能最差的 always,redis 还是不具有 100% 的数据持久性,因为其 AOF 写入发生在命令处理之后
- AOF 和 rdb 一样,为了防止阻塞 redis,将 close 操作放到了后台线程
- AOF rewrite 可以对 AOF 文件进行瘦身,防止 AOF 文件不断膨胀