快問快答!
- 圖解 AOF 日志
- 圖解 RDB 快照
AOF 日志篇的問題
問題一
這位讀者的意思是,他認(rèn)為 Redis 是單線程的,但是他在文章里看到 Redis 在 AOF 重寫日志的時候,會創(chuàng)建子進(jìn)程來重寫日志,他就覺得不對勁。Redis 確實(shí)是以單線程架構(gòu)被大家所知,但是這個單線程指的是「從網(wǎng)絡(luò) IO 處理到實(shí)際的讀寫命令處理」都是由單個線程完成的,并不是說整個 Redis 里只有一個主線程。有些命令操作可以用后臺子進(jìn)程執(zhí)行(比如快照生成、AOF 重寫)。嚴(yán)格意義上說的話,Redis 4.0 之后并不是單線程架構(gòu)了,除了主線程外,它也有后臺線程在處理一些耗時比較長的操作,例如清理臟數(shù)據(jù)、無用連接的釋放、大 Key 的刪除等等。你可能聽到 Redis 6.0 版本支持了多線程技術(shù),不過這個并不是指多個線程同時在處理讀寫命令,而是使用多線程來處理 Socket 的讀寫,最終執(zhí)行讀寫命令的過程還是只在主線程里。之所以采用多線程 IO 是因?yàn)镽edis 處理請求時,網(wǎng)絡(luò)處理經(jīng)常是瓶頸,通過多個 IO 線程并行處理網(wǎng)絡(luò)操作,可以提升整體處理性能。那為什么處理操作命令的過程只在單線程里呢?因?yàn)?Redis 不存在 CPU 成為瓶頸的情況,主要受限于內(nèi)存和網(wǎng)絡(luò)。而且使用單線程的好處在于,可維護(hù)性高、實(shí)現(xiàn)簡單。如果采用多線程模型來處理讀寫命令,雖然能提升并發(fā)性能,但是它卻引入了程序執(zhí)行順序的不確定性,帶來了并發(fā)讀寫的一系列問題,增加了系統(tǒng)復(fù)雜度、同時可能存在線程切換、甚至加鎖解鎖、死鎖造成的性能損耗。關(guān)于 Redis 單線程的問題就介紹這么多,后續(xù)在寫一篇詳細(xì)點(diǎn)的文章。問題二
這個讀者的意思是,AOF 重寫緩沖區(qū)占滿了會發(fā)生什么?其實(shí)重寫緩沖區(qū)并不是一個很大塊的內(nèi)存空間,而是一些內(nèi)存塊的鏈表,每個內(nèi)存塊的大小為 10MB,這樣就組成了一個重寫緩沖區(qū)。AOF 重寫緩沖區(qū)塊的數(shù)據(jù)結(jié)構(gòu)如下:細(xì)心的同學(xué)可能發(fā)現(xiàn),aofrwblock 結(jié)構(gòu)里沒有 prev 和 next 指針呀,那怎么組成鏈表的呢?Redis 是這樣做的,用 listNode 結(jié)構(gòu)包裹著 aofrwblock 結(jié)構(gòu),會將 listNode 結(jié)構(gòu)里的 value 指針指向 aofrwblock。接下來,我們看看 Redis 是如何使用申請和使用 aofrwblock 結(jié)構(gòu)的。下面這個函數(shù),就是將操作命令追加到 AOF 重寫緩沖區(qū)的實(shí)現(xiàn):可以看到,當(dāng)一個內(nèi)存塊 10MB 大小用完后,就會通過zmalloc() 在申請一個內(nèi)存塊,并將其追加到鏈表的末尾。如果遇到系統(tǒng)內(nèi)存緊張,導(dǎo)致申請內(nèi)存失敗時會發(fā)生什么呢?我們直接看下 zmalloc() 的實(shí)現(xiàn):可以看到,當(dāng) zmalloc() 申請內(nèi)存失敗的時候,就會打印一條日志,并調(diào)用 abort() 終止 Redis 進(jìn)程。現(xiàn)在就可以回答讀者的問題了,重寫緩沖區(qū)占滿了會發(fā)生什么?重寫緩沖區(qū)是邊用邊申請的,也就是說是動態(tài)申請的,并不是一次性就分配好的。如果一直分配內(nèi)存,當(dāng)耗盡系統(tǒng)的內(nèi)存資源的時候,zmalloc() 就無法申請成功,就會打印一條日志,隨后就 Redis 進(jìn)程就退出了。RDB 日志篇的問題
問題一
這位讀者的意思是,為什么執(zhí)行 bgsave ?命令來生成快照文件的時候,是創(chuàng)建子進(jìn)程而不是線程。AOF 重寫日志和 bgsave 快照生成都是通過創(chuàng)建子進(jìn)程來負(fù)責(zé)的,這里使用子進(jìn)程而不是線程,是因?yàn)槿绻鞘褂镁€程,多線程之間會共享內(nèi)存,那么在修改共享內(nèi)存數(shù)據(jù)的時候,需要通過加鎖來保證數(shù)據(jù)的安全,而這樣就會降低性能。而使用子進(jìn)程,創(chuàng)建子進(jìn)程時,父子進(jìn)程是共享內(nèi)存數(shù)據(jù)的,不過這個共享的內(nèi)存只能以只讀的方式,而當(dāng)父子進(jìn)程任意一方修改了該共享內(nèi)存,就會發(fā)生「寫時復(fù)制」,于是父子進(jìn)程就有了各自獨(dú)立的數(shù)據(jù)副本,就不用加鎖來保證數(shù)據(jù)安全,減少了鎖的開銷和避免死鎖的發(fā)生。
問二
bgsave 和 save 的區(qū)別就在于:- bgsave 會使用 ?fork() 系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程,創(chuàng)建快照的工作在子進(jìn)程里;
- save 不會創(chuàng)建子進(jìn)程,創(chuàng)建快照的工作在主線程里。
- 創(chuàng)建子進(jìn)程的途中,由于要復(fù)制父進(jìn)程的頁表等數(shù)據(jù)結(jié)構(gòu),阻塞的時間跟頁表的大小有關(guān),頁表越大,阻塞的時間也越長;
- 創(chuàng)建完子進(jìn)程后,如果子進(jìn)程或者父進(jìn)程修改了共享數(shù)據(jù),就會發(fā)生寫時復(fù)制,這期間會拷貝物理內(nèi)存,如果內(nèi)存越大,自然阻塞的時間也越長;
fork() 函數(shù),主線程無法繼續(xù)執(zhí)行,相當(dāng)于停頓了。所以針對這種情況建議用 sava。雖然 save 會一直阻塞 Redis 直到快照生成完畢,但是它這個阻塞并不是意味著停頓了,而是在執(zhí)行生成快照的程序,只是期間主線程無法處理接下來的讀寫命令。并且因?yàn)椴恍枰獎?chuàng)建子進(jìn)程,所以不會像 bgsave ?一樣因?yàn)閯?chuàng)建子進(jìn)程而導(dǎo)致 Redis 停頓,并且因?yàn)闆]有子進(jìn)程在爭搶資源,所以 sava 創(chuàng)建快照的速度比 bgsave 創(chuàng)建快照的速度要快一些。問題三
這兩個看一下源碼就知道了呀。先看回答第一個問題,我們直接看 Redis 加載 AOF 文件函數(shù)實(shí)現(xiàn):打開 AOF 文件之后,首先讀取 5 個字符如果是「REDIS」,那么就說明這是一個混合持久化的 AOF 文件,因?yàn)?RDB ?格式一定是以「REDIS」開頭,而純 AOF 格式則一定以「*」開頭。所以如果開頭的 5 個字符是 「REDIS」 會先進(jìn)入rdbLoadRio() 函數(shù)來加載 RDB 內(nèi)容。rdbLoadRio() 函數(shù)就不詳細(xì)展開了,就是按約定好的格式解析文件內(nèi)容直到遇到 RDB_OPCODE_EOF 結(jié)束標(biāo)記后返回。接著 loadAppendOnlyFile() 函數(shù)繼續(xù)以 AOF 格式解析文件直到結(jié)束整個加載過程完成。再來看第二個問題,是通過什么方法將內(nèi)存寫入文件的?很簡單的,就是通過大家都知道的 write() 系統(tǒng)調(diào)用將內(nèi)存數(shù)據(jù)寫入到文件呀。好了,這次就暫時回答這么多問題了。你們覺得小林答的夠詳細(xì)嗎?覺得不錯的,給小林個三連呀!我們下次見啦~





