关于redis:故障分析-Redis-AOF-重写源码分析

44次阅读

共计 13665 个字符,预计需要花费 35 分钟才能阅读完成。

作者:朱鹏举

新人 DBA,会点 MySQL,Redis,Oracle,在常识的陆地中挣扎,活下来就算胜利 …

本文起源:原创投稿

* 爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。


AOF 作为 Redis 的数据长久化形式之一,通过追加写的形式将 Redis 服务器所执行的写命令写入到 AOF 日志中来记录数据库的状态。但当一个键值对被多条写命令重复批改时,AOF 日志会记录相应的所有命令,这也就意味着 AOF 日志中存在反复的 ” 有效命令 ”,造成的后果就是 AOF 日志文件越来越大,应用 AOF 日志来进行数据恢复所需的工夫越来越长。为了解决这个问题,Redis 推出了 AOF 重写性能

什么是 AOF 重写

简略来说,AOF 重写就是依据过后键值对的最新状态,为它生成对应的写入命令,而后写入到长期 AOF 日志中。在重写期间 Redis 会将产生更改的数据写入到重写缓冲区 aof_rewrite_buf_blocks 中,于重写完结后合并到长期 AOF 日志中,最初应用长期 AOF 日志替换原来的 AOF 日志。当然,为了防止阻塞主线程,Redis 会 fork 一个过程来执行 AOF 重写操作。

如何定义 AOF 重写缓冲区

我晓得你很急,然而你先别急,在理解 AOF 重写流程之前你会先遇到第一个问题,那就是如何定义 AOF 重写缓冲区。

一般来说咱们会想到用 malloc 函数来初始化一块内存用于保留 AOF 重写期间主过程收到的命令,当残余空间有余时再用 realloc 函数对其进行扩容。然而 Redis 并没有这么做,Redis 定义了一个 aofrwblock 构造体,其中蕴含了一个 10MB 大小的字符数组,当做一个数据块,负责记录 AOF 重写期间主过程收到的命令,而后应用 aof_rewrite_buf_blocks 列表将这些数据块连接起来,每次调配一个 aofrwblock 数据块。

//AOF 重写缓冲区大小为 10MB,每一次调配一个 aofrwblock
typedef struct aofrwblock {
unsigned long used, free;
char buf[AOF_RW_BUF_BLOCK_SIZE]; //10MB
} aofrwblock;

那么问题来了,为什么 Redis 的开发者要抉择本人保护一个字符数组呢,答案是在应用 realloc 函数进行扩容的时候,如果此时客户端的写申请波及到正在长久化的数据,那么就会触发 Linux 内核的大页机制,造成不必要的内存空间节约,并且申请内存的工夫变长。

Linux 内核从 2.6.38 开始反对大页机制, 该机制反对 2MB 大小的內存页调配, 而惯例的内存页调配是按 4KB 的粒度来执行的。这也就意味着在 AOF 重写期间,客户端的写申请可能会批改正在进行长久化的数据,在这一过程中, Redis 就会采纳写时复制机制,一旦有数据要被批改, Redis 并不会间接批改內存中的数据,而是将这些数据拷贝一份,而后再进行批改。即便客户端申请只批改 100B 的数据, Redis 也须要拷贝 2MB 的大页。

AOF 重写流程

不晓得说什么,贴个代码先。

int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
if (aofCreatePipes() != C_OK) return C_ERR;
openChildInfoPipe();
start = ustime();
if ((childpid = fork()) == 0) {char tmpfile[256];
/* Child */
closeListeningSockets(0);
redisSetProcTitle("redis-aof-rewrite");
snprintf(tmpfile,256,
"temp-rewriteaof-bg-%d.aof"
, (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write"
,
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_AOF);
exitFromChild(0);
} else {exitFromChild(1);
}
} else {
/* Parent */
server.stat_fork_time = ustime()-start;
/* GB per second. */
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fo
rk_time / (1024*1024*1024);
latencyAddSampleIfNeeded("fork"
,server.stat_fork_time/1000);
if (childpid == -1) {closeChildInfoPipe();
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s"
,
strerror(errno));
aofClosePipes();
return C_ERR;
}
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %d"
,childpid);
server.aof_rewrite_scheduled = 0;
server.aof_rewrite_time_start = time(NULL);
server.aof_child_pid = childpid;
updateDictResizePolicy();
server.aof_selected_db = -1;
replicationScriptCacheFlush();
return C_OK;
}
return C_OK; /* unreached */
}

一步到 ” 胃 ” 间接看源码置信不少同学都感觉很胃疼,然而整顿过后了解起来就会轻松不少

  • 父过程
  1. 若以后有正在进行的 AOF 重写子过程或者 RDB 长久化子过程,则退出 AOF 重写流程
  2. 创立 3 个管道
  3. parent -> children data
  4. children -> parent ack
  5. parent -> children ack
  6. 将 parent -> children data 设置为非阻塞
  7. 在 children -> parent ack 上注册读事件的监听
  8. 将数组 fds 中的六个⽂件描述符别离复制给 server 变量的成员变量
  9. 关上 children->parent ack 通道,用于将 RDB/AOF 保留过程的信息发送给父过程
  10. 用 start 变量记录以后工夫
  11. fork 出一个子过程,通过写时复制的模式共享主线程的所有内存数据
  • 子过程
  • 敞开监听 socket,防止接管客户端连贯
  • 设置过程名
  • 生成 AOF 长期文件名
  • 遍历每个数据库的每个键值对,以插入(命令 + 键值对)的形式写到长期 AOF ⽂件中
  • 父过程
  • 计算上一次 fork 曾经破费的工夫
  • 计算每秒写了多少 GB 内容
  • 判断上一次 fork 是否完结,没完结则此次 AOF 重写流程就此停止
  • 将 aof_rewrite_scheduled 设置为 0(示意当初没有待调度执⾏的 AOF 重写操作)
  • 敞开 rehash 性能(Rehash 会带来较多的数据挪动操作,这就意味着⽗过程中的内存批改会⽐较多,对于 AOF 重写⼦过程来说,就须要更多的工夫来执行写时复制,进⽽实现 AOF ⽂件的写⼊,这就会给 Redis 零碎的性能造成负⾯影响)
  • 将 aof_selected_db 设置为 -1(以强制在下一次调用 feedAppendOnlyFile 函数(写 AOF 日志)的时候将 AOF 重写期间累计的内容合并到 AOF 日志中)
  • 当发现正在进行 AOF 重写工作的时候

    (1)将收到的新的写命令缓存在 aofrwblock 中

    (2)查看 parent -> children data 下面有没有写监听,没有的话注册一个

    (3)触发写监听时从 aof_rewrite_buf_blocks 列表中一一取出 aofrwblock 数据块,通过 parent -> children data 发送到 AOF 重写子过程

  • 子过程重写完结后,将重写期间 aof_rewrite_buf_blocks 列表中没有生产实现的数据追加写入到长期 AOF 文件中

管道机制

Redis 创立了 3 个管道用于 AOF 重写时父子过程之间的数据传输,那么管道之间的通信机制就成为了咱们须要理解的内容。

1. 子过程从 parent -> children data 读取数据 (触发机会)

  • rewriteAppendOnlyFileRio

    由重写⼦过程执⾏,负责遍历 Redis 每个数据库,⽣成 AOF 重写⽇志,在这个过程中,会不断地调⽤ aofReadDiffFromParent

  • rewriteAppendOnlyFile

    重写⽇志的主体函数,也是由重写⼦过程执⾏的,本⾝会调⽤ rewriteAppendOnlyFileRio,调⽤完后会调⽤ aofReadDiffFromParent 屡次,尽可能多地读取主过程在重写⽇志期间收到的操作命令

  • rdbSaveRio

    创立 RDB ⽂件的主体函数,使⽤ AOF 和 RDB 混合长久化机制时,这个函数会调⽤ aofReadDiffFromParent

// 将从父级累积的差别读取到缓冲区中,该缓冲区在重写完结时连贯
ssize_t aofReadDiffFromParent(void) {char buf[65536]; // 大多数 Linux 零碎上的默认管道缓冲区大小
ssize_t nread, total = 0;
while ((nread =
read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);
total += nread;
}
return total;
}

2. 子过程向 children -> parent ack 发送 ACK 信号

  • 在实现⽇志重写,以及屡次向⽗过程读取操作命令后,向 children -> parent ack 发送 ”!”,也就是向主过程发送 ACK 信号,让主过程停⽌发送收到的新写操作
int rewriteAppendOnlyFile(char *filename) {
rio aof;
FILE *fp;
char tmpfile[256];
char byte;
// 留神,与 rewriteAppendOnlyFileBackground()函数应用的长期名称相比,咱们必须在此处应用不同的临
时名称
snprintf(tmpfile,256,
"temp-rewriteaof-%d.aof"
, (int) getpid());
fp = fopen(tmpfile,
"w");
if (!fp) {
serverLog(LL_WARNING,
"Opening the temp file for AOF rewrite in rewriteAppendOnly
File(): %s"
, strerror(errno));
return C_ERR;
}
server.aof_child_diff = sdsempty();
rioInitWithFile(&aof,fp);
if (server.aof_rewrite_incremental_fsync)
rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);
if (server.aof_use_rdb_preamble) {
int error;
if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) {
errno = error;
goto werr;
}
} else {if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
}
// 当父过程仍在发送数据时,在此处执行初始的慢速 fsync,以便使下一个最终的 fsync 更快
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
// 再读几次,从父级获取更多数据。咱们不能永远读取(服务器从客户端接收数据的速度可能快于它向子过程发送数
据的速度),所以咱们尝试在循环中读取更多的数据,只有有更多的数据呈现。如果看起来咱们在浪费时间,咱们会停止(在没有新数据的状况下,这会在 20ms 后产生)。int nodata = 0;
mstime_t start = mstime();
while(mstime()-start < 1000 && nodata < 20) {if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();}
// 发送 ACK 信息让父过程进行发送音讯
if (write(server.aof_pipe_write_ack_to_parent,
"!"
,1) != 1) goto werr;
if (anetNonBlock(NULL,server.aof_pipe_read_ack_from_parent) != ANET_OK)
goto werr;
// 期待父过程返回的 ACK 信息,超时工夫为 10 秒。通常父过程应该尽快回复,但万一失去回复,则确信子过程最终会
被终止。if (syncRead(server.aof_pipe_read_ack_from_parent,&byte,1,5000) != 1 ||
byte != '!') goto werr;
serverLog(LL_NOTICE,
"Parent agreed to stop sending diffs. Finalizing AOF...");
// 如果存在最终差别数据,那么将读取
aofReadDiffFromParent();
// 将收到的差别数据写入文件
serverLog(LL_NOTICE,
"Concatenating %.2f MB of AOF diff received from parent."
,
(double) sdslen(server.aof_child_diff) / (1024*1024));
if (rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)) == 0)
goto werr;
// 确保数据不会保留在操作系统的输入缓冲区中
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
// 应用 RENAME 确保仅当生成 DB 文件失常时,才主动更改 DB 文件
if (rename(tmpfile,filename) == -1) {
serverLog(LL_WARNING,
"Error moving temp append only file on the final destinatio
n: %s"
, strerror(errno));
unlink(tmpfile);
return C_ERR;
}
serverLog(LL_NOTICE,
"SYNC append only file rewrite performed");
return C_OK;
werr:
serverLog(LL_WARNING,
"Write error writing append only file on disk: %s"
, strerror(err
no));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}

3. 父过程从 children -> parent ack 读取 ACK

  • 当 children -> parent ack 上有了数据,就会触发之前注册的读监听
  • 判断这个数据是不是 ”!”
  • 是就向 parent -> children ack 写入 ”!”,表⽰主过程曾经收到重写⼦过程发送的 ACK 信息,同时给重写⼦过程回复⼀个 ACK 信息
void aofChildPipeReadable(aeEventLoop *el, int fd, void *privdata, int mask) {
char byte;
UNUSED(el);
UNUSED(privdata);
UNUSED(mask);
if (read(fd,&byte,1) == 1 && byte == '!') {
serverLog(LL_NOTICE,
"AOF rewrite child asks to stop sending diffs.");
server.aof_stop_sending_diff = 1;
if (write(server.aof_pipe_write_ack_to_child,
"!"
,1) != 1) {
// 如果咱们无奈发送 ack,请告诉用户,但不要重试,因为在另一侧,如果内核无奈缓冲咱们的写入,或
者子级已终止,则子级将应用超时
serverLog(LL_WARNING,
"Can't send ACK to AOF child: %s"
,
strerror(errno));
}
}
// 删除处理程序,因为在重写期间只能调用一次
aeDeleteFileEvent(server.el,server.aof_pipe_read_ack_from_child,AE_READABLE);
}

什么时候触发 AOF 重写

开启 AOF 重写性能当前 Redis 会主动触发重写,破费精力去理解触发机制感觉意义不大。想法很不错,下次别想了。不然当你手动
执行 Bgrewriteaof 命令却发现总是报错时,疼的不只有你的头,还有你的胃。

1. 手动触发
  • 以后没有正在执⾏ AOF 重写的⼦过程
  • 以后没有正在执⾏创立 RDB 的⼦过程,有会将 aof_rewrite_scheduled 设置为 1(AOF 重写操作被设置为了待调度执⾏)
void bgrewriteaofCommand(client *c) {if (server.aof_child_pid != -1) {
addReplyError(c,
"Background append only file rewriting already in progress");
} else if (server.rdb_child_pid != -1) {
server.aof_rewrite_scheduled = 1;
addReplyStatus(c,
"Background append only file rewriting scheduled");
} else if (rewriteAppendOnlyFileBackground() == C_OK) {
addReplyStatus(c,
"Background append only file rewriting started");
} else {addReply(c,shared.err);

   }
}
2. 开启 AOF 与主从复制
  • 开启 AOF 性能当前,执行一次 AOF 重写
  • 主从节点在进⾏复制时,如果从节点的 AOF 选项被关上,那么在加载解析 RDB ⽂件时,AOF 选项会被敞开,⽆论从节点是否胜利加载 RDB ⽂件,restartAOFAfterSYNC 函数都会被调⽤,⽤来复原被敞开的 AOF 性能,在这个过程中会执行一次 AOF 重写
int startAppendOnly(void) {char cwd[MAXPATHLEN]; // 谬误音讯的当前工作目录门路
    int newfd;
    newfd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
    serverAssert(server.aof_state == AOF_OFF);
    if (newfd == -1) {char *cwdp = getcwd(cwd,MAXPATHLEN);
        serverLog(LL_WARNING,
            "Redis needs to enable the AOF but can't open the ""append only file %s (in server root dir %s): %s",
            server.aof_filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        return C_ERR;
    }
    if (server.rdb_child_pid != -1) {
        server.aof_rewrite_scheduled = 1;
        serverLog(LL_WARNING,"AOF was enabled but there is already a child process saving an RDB file on disk. An AOF background was scheduled to start when possible.");
    } else {
        // 敞开正在进行的 AOF 重写过程,并启动一个新的 AOF:旧的 AOF 无奈重用,因为它没有累积 AOF 缓冲区。if (server.aof_child_pid != -1) {serverLog(LL_WARNING,"AOF was enabled but there is already an AOF rewriting in background. Stopping background AOF and starting a rewrite now.");
            killAppendOnlyChild();}
        if (rewriteAppendOnlyFileBackground() == C_ERR) {close(newfd);
            serverLog(LL_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
            return C_ERR;
        }
    }
    // 咱们正确地关上了 AOF,当初期待重写实现,以便将数据附加到磁盘上
    server.aof_state = AOF_WAIT_REWRITE;
    server.aof_last_fsync = server.unixtime;
    server.aof_fd = newfd;
    return C_OK;
}
3. 定时工作
  1. 每 100 毫秒触发一次,由 server.hz 管制,默认 10
  2. 以后没有在执⾏的 RDB ⼦过程 && AOF 重写⼦过程 && aof_rewrite_scheduled=1
  3. 以后没有在执⾏的 RDB ⼦过程 && AOF 重写⼦过程 && aof_rewrite_scheduled=0

    AOF 性能已启⽤ && AOF ⽂件⼤⼩⽐例超出 auto-aof-rewrite-percentage && AOF ⽂件⼤⼩绝对值超出 auto-aofrewrite-min-size

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
......
// 判断以后没有在执⾏的 RDB ⼦过程 && AOF 重写⼦过程 && aof_rewrite_scheduled=1
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{rewriteAppendOnlyFileBackground();
}
// 查看正在进行的后盾保留或 AOF 重写是否终止
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
int statloc;
pid_t pid;
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
if (pid == -1) {
serverLog(LL_WARNING,
"wait3() returned an error: %s."
"rdb_child_pid = %d, aof_child_pid = %d"
,
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
} else if (pid == server.rdb_child_pid) {backgroundSaveDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();} else if (pid == server.aof_child_pid) {backgroundRewriteDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();} else {if (!ldbRemoveChild(pid)) {
serverLog(LL_WARNING,
"Warning, detected child with unmatched pid: %ld"
,
(long)pid);
}
}
updateDictResizePolicy();
closeChildInfoPipe();}
} else {
// 如果没有正在进行的后盾 save/rewrite,请查看是否必须立刻 save/rewrite
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
// 如果咱们达到了给定的更改量、给定的秒数,并且最新的 bgsave 胜利,或者如果产生谬误,至多曾经
过了 CONFIG_bgsave_RETRY_DELAY 秒,则保留。if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
      {
serverLog(LL_NOTICE,
"%d changes in %d seconds. Saving..."
,
sp->changes, (int)sp->seconds);
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
rdbSaveBackground(server.rdb_filename,rsiptr);
break;
}
}
// 判断 AOF 性能已启⽤ && AOF ⽂件⼤⼩⽐例超出 auto-aof-rewrite-percentage && AOF ⽂件⼤⼩相对
值超出 auto-aof-rewrite-min-size
if (server.aof_state == AOF_ON &&
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
long long growth = (server.aof_current_size*100/base) - 100;
if (growth >= server.aof_rewrite_perc) {
serverLog(LL_NOTICE,
"Starting automatic rewriting of AOF on %lld%% growt
h"
,growth);
rewriteAppendOnlyFileBackground();}
}
}
......
return 1000/server.hz;
}

AOF 重写性能的毛病

哪怕是你心中的她,也并非是白璧无瑕的存在,更别说 Redis 这个人工产物了。但不去发现也就自然而然不存在毛病,对吧~

1. 内存开销
  • 在 AOF 重写期间,主过程会将 fork 之后的数据变动写进 aof_rewrite_buf 与 aof_buf 中,其内容绝大部分是反复的,在高流量写入的场景下两者耗费的空间简直一样大。
  • AOF 重写带来的内存开销有可能导致 Redis 内存忽然达到 maxmemory 限度,甚至会触发操作系统限度被 OOM Killer 杀死,导致 Redis 不可服务。
2.CPU 开销
  • 在 AOF 重写期间主过程须要破费 CPU 工夫向 aof_rewrite_buf 写数据,并应用 eventloop 事件循环向子过程发送 aof_rewrite_buf 中的数据。
// 将数据附加到 AOF 重写缓冲区,如果须要,调配新的块
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
......
// 创立事件以便向子过程发送数据
if (!server.aof_stop_sending_diff &&
aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0)
{
aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,
AE_WRITABLE, aofChildWriteDiffData, NULL);
 }
  ......
}
  • 在子过程执行 AOF 重写操作的前期,会循环读取 pipe 中主过程发送来的增量数据,而后追加写入到长期 AOF 文件。
int rewriteAppendOnlyFile(char *filename) {
......
// 再次读取几次以从父过程获取更多数据。咱们不能永远读取(服务器从客户端接收数据的速度可能快于它向子级发
送数据的速度),因而咱们尝试在循环中读取更多数据,只有有很好的机会会有更多数据。如果看起来咱们在浪费时间,咱们会停止(在没有新数据的状况下,这会在 20ms 后产生)int nodata = 0;
mstime_t start = mstime();
while(mstime()-start < 1000 && nodata < 20) {if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();}
 ......
}

在子过程实现 AOF 重写操作后,主过程会在 backgroundRewriteDoneHandler 中进行收尾工作,其中一个工作就是将在重
写期间 aof_rewrite_buf 中没有生产实现的数据写入长期 AOF 文件,耗费的 CPU 工夫与 aof_rewrite_buf 中遗留的数据量成正
比。

3. 磁盘 IO 开销

在 AOF 重写期间,主过程会将 fork 之后的数据变动写进 aof_rewrite_buf 与 aof_buf 中,在业务顶峰期间其内容绝大部分是反复的,一次操作产生了两次 IO 开销。

4.Fork

虽说 AOF 重写期间不会阻塞主过程,然而 fork 这个霎时肯定是会阻塞主过程的。因而 fork 操作破费的工夫越长,Redis 操作提早的工夫就越长。即便在一台一般的机器上,Redis 也能够解决每秒 50K 到 100K 的操作,那么几秒钟的提早可能意味着数十万次操作的速度减慢,这可能会给应用程序带来重大的稳定性问题。

为了防止一次性拷贝大量内存数据给子过程造成的长时间阻塞问题,fork 采纳操作系统提供的写时复制(Copy-On-Write)机制,但 fork 子过程须要拷贝过程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表)。这个拷贝过程会耗费大量 CPU 资源,拷贝实现之前整个过程是会阻塞的,阻塞工夫取决于整个实例的内存大小,实例越大,内存页表越大,fork 阻塞工夫越久。拷贝内存页表实现后,子过程与父过程指向雷同的内存地址空间,也就是说此时尽管产生了子过程,然而并没有申请与父过程雷同的内存大小。

参考资料:

1. 极客工夫专栏《Redis 源码分析与实战》. 蒋德钧.2021

2. 极客工夫专栏《Redis 核心技术与实战》. 蒋德钧.2020

3.Redis 7.0 Multi Part AOF 的设计和实现. 驱动 qd.2022:
https://developer.aliyun.com/…

4.Redis 5.0.8 源码:https://github.com/redis/redi…

正文完
 0