Redis 5-0 部分源碼剖析

從前有句古話說得好,天將降大任於斯人也,必要先看 Redis。

以前古人還說過,窗前明月光,低頭 Redis。

古人還說過,所有的答案都在源碼裏。

昨天還有人跟我說,用 Redis 比 Tair 申請要方便。

不識廬山真面目,只緣身在此山中

我們先給出一副大圖,來看看 Redis AOF Rewrite 的總體流程是怎麼樣的。

先看看大圖裏的幾大組成部分

  1. 主進程與子進程,大家都知道,Redis AOF Rewrite 是通過創建一個子進程來完成的。父子進程有一個重要特性,那就是 "讀時共享,寫時複製"。後面我們詳細聊

  2. 父子進程間通信使用的三個通道。

  3. 客戶端寫入 Redis 主進程時,涉及到的兩個數據結構,aof_buf,aof_rewrite_buf_blocks;子進程涉及到的 aof_child_diff 數據結構。

  4. 一份當前使用的 AOF 文件,這份文件是準備退休的 "現役" 文件,另一份是子進程正在重寫的 "預備役" 文件。

大致涉及的內容就是如此了,接下來按照一個 AOF Rewrite 執行的時間順序來看看到底發生了什麼事

萬般皆由長風起

先來看看,Redis AOF Rewrite 機制是怎麼觸發的呢?

有兩種方式大家應該都很清楚了,一種是配置文件中配置的若干時間內,發生了若干鍵值對的變化,達到閾值就需要觸發一次重寫。

(這裏補充一句,這個檢查是在 Redis 後臺主線程中調度時檢查的,這個時間並不會是很確定的)

所謂的 serverCron 就是這個方法,何時觸發,如何觸發,我們回頭細說

另一種是客戶端,要求執行 bgrewriteaof。

這裏寫的第三種,是在開啓 AOF 時,纔會進行一次。一般是在啓動時就完成了 AOF 的啓動。

但這裏有一種特殊情況,就是在 Redis 有主從時。從服務在跟主服務同步數據時,主服務會生成一個 RDB 文件給從,從使用假客戶端讀取數據恢復至內存。這個階段,從服務是需要停止 AOF(如果原來開啓的話)的。等到完成了主從同步的數據恢復後,自動開啓 AOF,這裏就會執行一次 AOF 的重寫。

山一程,水一程,fork 子進程

不管是什麼原因,當確定要進行 AOF Rewrite 之後,首先做的就是進行一系列檢查,然後 fork 一個子進程。

有紅線的地方就是 fork 子進程的地方。

if 語句中的是子進程需要執行的代碼,else 中是主進程的。

先不着急研究主子進程分別做了什麼事,看下前面的校驗。

首先,如果有 AOF 重寫子進程或 RDB 重寫進程存在的話,就不進行本次的重寫。

其次,如果創建主子進程的管道失敗的話,也不進行重寫。

創建子進程成功之後,子進程就會立即開始 AOF 文件的重寫,而主進程則是繼續提供對外服務,只是爲了確保重寫期間 AOF 不丟失,會多做幾步操作。

這裏創建的管道十分重要,一共有三個:

提示:由於fork子進程會讓主子進程共享內存,那麼子進程一定是要知道主進程原有的數據存儲在哪裏的。

這裏就涉及到將原有主進程的頁表複製一份過來的操作。這個操作是阻塞的,會導致 fork 操作卡住。

因此在流量大的時候,要注意 AOF Rewrite 將 Redis 卡住,而使 RT 增大。

話分兩頭

子進程

我們看看子進程都做了啥:

  1. 創建一個臨時文件,名字是 temp-rewriteaof-{pid} 的文件,然後初始化一下文件句柄之類的引用。

  2. 判斷是否是 RDB 混合模式,還是純 AOF 模式,進行重寫。這兩者的區別,這裏就不贅述了。

真正的重寫其實很簡單,就是挨個的讀取 db 的內容,然後以對應的格式,寫入文件中。

由於主子進程之間有 "讀時共享,寫時複製" 的限制,也就是如果是讀取時兩者公用一份內容,當有人要寫的數據時,會將原有數據 copy 出來一份,在新的 copy 的數據上修改,舊保持不變。

Redis 就利用了這一功能,保證了讀到的數據是 fork 之前的最後版本的數據。

到這裏爲止,其實是 AOF Rewrite 的核心邏輯,其餘的邏輯都是圍繞在 AOF Rewrite 期間有數據發生變化來做的。

整個重寫是非常耗費 CPU 的,趁着子進程加班加點幹活時,我們來看一眼主進程在做什麼。

主進程



主進程在 fork 完子進程,把 ORK 交代給子進程之後,就對子進程不管不問了。偶爾檢查一下子進程有沒有把工作完成(通信管道有沒有新數據 / 子進程有沒有消失)。

假設在 AOF 重寫過程中,有客戶端發來了一個 set a 1 的請求,會將原來的 a 的值由 0 改爲 1。

由於有主子進程 "讀時共享,寫時複製" 的存在,不用擔心子進程,它會讀到老的數據。

在完成內存數據變化後,會走到下面這個方法中。我們仔細來看看。

這個方法在 aof.c 文件中,所有寫 aof 的操作都走這個方法。

它做了一下幾件事:

  1. Redis 是有多個 db 的,如果命令操作的 db 不是當前的 db,那麼就會插入一條 select db 的命令。根據 ditcid 參數來確定

  2. 將帶有過期時間的操作,轉換爲 PEXPIREAT(EXPIRE/PEXPIRE/EXPIREAT/SETEX/PSETEX/SET [EX seconds][PX milliseconds])

  3. 將操作的命令,轉換爲 RESP 格式

  4. 寫入 AOF 相關緩存

這裏與 AOF Rewrite 相關的,是步驟 4,我們重點來看下這個步驟做了什麼:

首先,判斷是否開啓了 AOF,那顯然我們是開啓了的啊,就需要將這條語句,寫入舊的 AOF 文件中。這個很合理啊,萬一重寫失敗了呢,數據不可以丟啊。

其次,如果有 aof 子進程 pid 存在,那麼還要多做一步 aofRewriteBufferAppend(),這個是做什麼的呢?

它是將剛剛生成的語句,再次保存在一個 aof_rewrite_buf_blocks 的結構當中。

這個 aof_rewrite_buf_blocks 是一個 list 結構,它保存的都是 10M 大小一個的 block。block 中存儲的就是剛剛生成的語句。

然後方法返回。本次 aof 寫入操作結束。

aof_rewrite_buf_blocks 的數據,會等待創建的管道 1 是否允許寫入 (寫入時機由別的機制保證,這裏略過),如果允許寫入,就將數據寫入這個管道中,然後將內存中寫入部分的數據釋放。

提示:這裏需要注意,aof_buf與aof_rewrite_buf_blocks是兩個數據結構,裏面的數據也是兩份,不是公用一份。

因此重寫階段,數據變更會讓主進程將這些數據在內存中存儲兩份,這對主進程是額外的壓力。

子進程拿到第一個 KR 了

我們把目光再次回到子進程身上。

此時它已經完成對原有數據的重寫,拿到第一個 KR,我們恭喜一下它~

現在我們知道了 Rewrite 期間變化的數據,是會通過管道通知的。那麼子進程是如何處理的麼?

點點滴滴,聚水成河

其實在 “子進程” 章節中重寫舊數據時,就開始處理了。子進程在重寫舊數據時,會時不時的讀取一下管道。

rdbSaveRio 方法中

rewriteAppendOnlyFileRio 方法中

這些讀取出來的數據都會保存在 aof_child_diff 的數據結構中。

這樣舊數據會直接寫入到 aof 重寫文件中,期間變化的數據會保存在內存 aof_child_diff 中,數據順序就不會混亂了。最後把這部分變化的數據統一寫入待 aof 重寫文件中即可。

最重要的是向上管理

子進程在完成第一個 KR,重寫了舊數據之後,就馬不停蹄的開展了下一項工作。從管道 1 中讀取變化的數據。

這裏有兩個點需要注意,一個是主進程可能一直在接受新的數據,這就導致通道 1 中永遠有數據,子進程就無限制的讀取數據,這肯定是不能接受到,因此,限制了最多隻會讀取 1s。

另一個,如果一直沒有數據,也沒必要一直等啊,大家時間都很寶貴的,如果 20 毫秒沒有數據,那麼子進程也就不會讀取了。

下面就到了關鍵一步,要通知主進程自己的工作已經完成了 80%,進行向上管理了。

子進程向通道 2 寫入一個!號,通知主進程,請停止向管道 1 當中寫入數據

主進程接受會邀

主進程在接受到通道 2 的通知之後,將 aof_stop_sending_diff 設置爲 1,變化的數據仍舊進入 aof_rewrite_buf_blocks 中,但是不會在寫入通道 1 中了,全部停留在自己的內存中。

主進程接着向通道 3 中寫入一個!,表明自己停止寫入數據。

臨門一腳

子進程收到通知後,最後將通道 1 中的數據全部讀取出來,然後刷入磁盤,並將 aof 重寫文件重寫命名爲 temp-rewriteaof-bg-{pid}.aof,然後自己退出進程。

這裏的寫入,就是指將內存中的 aof_child_buf 中的數據一股腦的寫入文件當中,然後隨着進程退出,內存一併釋放。

主進程:我來兜底!

此時子進程已經完成了它的工作,退出了進程,主進程在 serverCron 中發現子進程已經不存在了之後,就調用 backgroundRewriteDoneHandler 方法,處理善後的工作。

  1. 將之前 aof_rewrite_buf_blocks 還存在的數據,寫入 aof 重寫文件之中。(temp-rewriteaof-bg-{pid}.aof)

  2. 將 aof 文件設置爲重寫文件,重寫 aof 正式轉正,舊文件退休。

道由白雲盡,春與青溪長

到此爲止,一次完整的 AOF Rewrite 重寫就結束了。

縱觀整個過程,我們可以看到,核心的重點是如何處理 AOF Rewrite 期間變化的數據。

爲了保證這部分的數據正確,Redis 5.0 版本總共使用了兩個內存結構存儲 (主進程的 aof_rewrite_buf_blocks,子進程的 aof_child_diff),兩個磁盤 IO(主進程寫入舊 AOF 文件,子進程寫入新 AOF 重寫文件),三個通信管道,雙倍的 CPU 開銷來完成。

有興趣的同學可以瞭解下還未正式發佈的 Redis 7.0,Multi Part AOF 的實現。這個版本的實現完美解決了上述的冗餘問題。

好的,今天的分享就到此爲止啦,感謝大家。

團隊介紹

我們是大聚划算技術團隊。

使命:讓貨品和心智運營變得高效且有確定性!

願景:與運營、產品合力,打造最具價格優惠心智的購物入口,最具爆發性的營銷矩陣。

職責:負責支持聚划算、百億補貼、天天特賣等業務。我們聚焦優惠和選購體驗,通過數智化驅動形成更有效率和確定性的貨品運營方法論,爲消費者提供精選和極致性價比的商品,爲商家提供更具爆發確定性的營銷方案。

這是一支極具業務 sense,又具備複雜業務系統架構和設計經驗的團隊。

作者 | 時晝

編輯 | 橙子君

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/z07Y7TePpDsBPqO1N4tjSg