首页 » PHP教程 » phpredisset无效技巧_故障分析 Redis AOF 重写源码分析

phpredisset无效技巧_故障分析 Redis AOF 重写源码分析

访客 2024-11-19 0

扫一扫用手机浏览

文章目录 [+]

新人 DBA ,会点 MySQL ,Redis ,Oracle ,在知识的海洋中挣扎,活下来就算成功...

本文来源:原创投稿

phpredisset无效技巧_故障分析  Redis AOF 重写源码分析

爱可生开源社区出品,原创内容未经授权不得随意利用,转载请联系

AOF 作为 Redis 的数据持久化办法之一,通过追加写的办法将 Redis 做事器所实行的写命令写入到 AOF 日志中来记录数据库的状态。
但当一个键值对被多条写命令反复修正时,AOF 日志会记录相应的所有命令,这也就意味着 AOF 日志中存在重复的"无效命令",造成的结果便是 AOF 日志文件越来越大,利用 AOF 日志来进行数据规复所需的韶光越来越长。
为理解决这个问题,Redis 推出了 AOF 重写功能

phpredisset无效技巧_故障分析  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,每一次分配一个aofrwblocktypedef 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/(10241024));}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_fork_time / (102410241024);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 /}

一步到"胃"直接看源码相信不少同学都以为很胃疼,但是整理过后理解起来就会轻松不少

父进程若当前有正在进行的AOF重写子进程或者RDB持久化子进程,则退出AOF重写流程创建3个管道parent -> children datachildren -> parent ackparent -> children ack将parent -> children data设置为非壅塞在children -> parent ack上注册读事宜的监听将数组fds中的六个⽂件描述符分别复制给server变量的成员变量打开children->parent ack通道,用于将RDB/AOF保存过程的信息发送给父进程用start变量记录当前韶光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重写⽇志,在这个过程中,会时时地调⽤ aofReadDiffFromParentrewriteAppendOnlyFile 重写⽇志的主体函数,也是由重写⼦进程执⾏的,本⾝会调⽤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 rewriteAppendOnlyFile(): %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 contiguoustimeouts. /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) / (10241024));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 destination: %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(errno));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.定时任务每100毫秒触发一次,由server.hz掌握,默认10当前没有在执⾏的RDB⼦进程 && AOF重写⼦进程 && aof_rewrite_scheduled=1当前没有在执⾏的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=1if (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/rewritefor (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-sizeif (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_size100/base) - 100;if (growth >= server.aof_rewrite_perc) {serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",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 contiguoustimeouts. /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/article/866957

4.Redis 5.0.8源码:https://github.com/redis/redis/tree/5.0

标签:

相关文章