VLDB 頂會(huì)論文 Async-fork 解讀與 Redis 實(shí)踐
1、背景
在 Redis 中,在 AOF 文件重寫(xiě)、生成 RDB 備份文件以及主從全量同步過(guò)程中,都需要使用系統(tǒng)調(diào)用 fork 創(chuàng)建一個(gè)子進(jìn)程來(lái)獲取內(nèi)存數(shù)據(jù)快照,在 fork() 函數(shù)創(chuàng)建子進(jìn)程的時(shí)候,內(nèi)核會(huì)把父進(jìn)程的「頁(yè)表」復(fù)制一份給子進(jìn)程,如果頁(yè)表很大,復(fù)制頁(yè)表的過(guò)程耗時(shí)會(huì)非常長(zhǎng),那么在此期間,業(yè)務(wù)訪問(wèn) Redis 讀寫(xiě)延遲會(huì)大幅增加。
最近,阿里云聯(lián)合上海交大,在數(shù)據(jù)庫(kù)頂級(jí)會(huì)議 VLDB 上發(fā)表了一篇文章《Async-fork: Mitigating Query Latency Spikes Incurred by the Fork-based Snapshot Mechanism from the OS Level》,文章介紹到,他們?cè)O(shè)計(jì)了一個(gè)新的 fork(稱為 Async-fork),將 fork 調(diào)用過(guò)程中最耗時(shí)的頁(yè)表拷貝部分從父進(jìn)程移動(dòng)到子進(jìn)程,父進(jìn)程因而可以快速返回用戶態(tài)處理用戶查詢,子進(jìn)程則在此期間完成頁(yè)表拷貝,從而減少 fork 期間到達(dá)請(qǐng)求的尾延遲。所以該特性在類似 Redis 類型的內(nèi)存數(shù)據(jù)庫(kù)上均能取得不錯(cuò)的效果。
2、基本概念
2.1 物理內(nèi)存地址
也即實(shí)際的物理內(nèi)存地址空間。
2.2 虛擬地址空間
虛擬地址空間(Virtual Address Space)是每一個(gè)程序被加載運(yùn)行起來(lái)后,操作系統(tǒng)為進(jìn)程分配的虛擬內(nèi)存,它為每個(gè)進(jìn)程提供了一個(gè)假象,即每個(gè)進(jìn)程都在獨(dú)占地使用主存。
每個(gè)進(jìn)程所能訪問(wèn)的最大的虛擬地址空間由計(jì)算機(jī)的硬件平臺(tái)決定,具體地說(shuō)是由 CPU 的位數(shù)決定的。比如 32 位的 CPU 就是我們常說(shuō)的 4GB 虛擬內(nèi)存空間。
程序訪問(wèn)內(nèi)存地址使用虛擬地址空間,然后由操作系統(tǒng)將這個(gè)虛擬地址映射到適當(dāng)?shù)奈锢韮?nèi)存地址上。這樣,只要操作系統(tǒng)處理好虛擬地址到物理內(nèi)存地址的映射,就可以保證不同的程序最終訪問(wèn)的內(nèi)存地址位于不同的區(qū)域,彼此沒(méi)有重疊,就可以達(dá)到內(nèi)存地址空間隔離的效果。
當(dāng)進(jìn)程創(chuàng)建時(shí),每個(gè)進(jìn)程都會(huì)有一個(gè)自己的 4GB 虛擬地址空間。要注意的是這個(gè) 4GB 的地址空間是“虛擬”的,并不是真實(shí)存在的,而且每個(gè)進(jìn)程只能訪問(wèn)自己虛擬地址空間中的數(shù)據(jù),無(wú)法訪問(wèn)別的進(jìn)程中的數(shù)據(jù),通過(guò)這種方法實(shí)現(xiàn)了進(jìn)程間的地址隔離。
對(duì)于 Linux,4GB 的虛擬地址空間包含用戶態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間兩部分,默認(rèn)分配狀態(tài)如下:
2.3 內(nèi)存頁(yè)表
「頁(yè)表」保存的是虛擬內(nèi)存地址與物理內(nèi)存地址的映射關(guān)系。
CPU 訪問(wèn)數(shù)據(jù)的時(shí)候,CPU 發(fā)出的地址是虛擬地址,CPU 中內(nèi)存管理單元(MMU)通過(guò)查詢頁(yè)表,把虛擬地址轉(zhuǎn)換為物理地址,再去訪問(wèn)物理內(nèi)存條。
2.3.1 內(nèi)存分頁(yè)
分頁(yè)是把整個(gè)虛擬和物理內(nèi)存空間切成一段段固定尺寸的大小,這樣一個(gè)連續(xù)并且尺寸固定的內(nèi)存空間,我們叫頁(yè)(Page)。在 Linux 下,每一頁(yè)的大小為 4KB。
在 32 位的環(huán)境下,虛擬地址空間共有 4GB,假設(shè)一個(gè)頁(yè)的大小是 4KB(2^12),那么就需要大約 100 萬(wàn)(2^20)個(gè)頁(yè),每個(gè)「頁(yè)表項(xiàng)」需要 4 個(gè)字節(jié)大小來(lái)存儲(chǔ),那么整個(gè) 4GB 空間的映射就需要有 4MB 的內(nèi)存來(lái)存儲(chǔ)頁(yè)表。
這 4MB 大小的頁(yè)表,看起來(lái)也不是很大。但是每個(gè)進(jìn)程都是有自己的虛擬地址空間,也就說(shuō)都有自己的頁(yè)表。每個(gè)機(jī)器上同時(shí)運(yùn)行多個(gè)進(jìn)程,頁(yè)表將占用大量?jī)?nèi)存。
2.3.2 多級(jí)頁(yè)表
要解決上面提到的存儲(chǔ)進(jìn)程頁(yè)表項(xiàng)占用大量?jī)?nèi)存空間的問(wèn)題,就需要采用一種叫作多級(jí)頁(yè)表(Multi-Level Page Table)的解決方案。
我們把這個(gè) 100 多萬(wàn)個(gè)「頁(yè)表項(xiàng)」的單級(jí)頁(yè)表再分頁(yè),將頁(yè)表(一級(jí)頁(yè)表)分為 1024 個(gè)頁(yè)表(二級(jí)頁(yè)表),每個(gè)二級(jí)頁(yè)表中包含 1024 個(gè)「頁(yè)表項(xiàng)」,形成二級(jí)分頁(yè)。這樣,一級(jí)頁(yè)表就可以覆蓋整個(gè) 4GB 虛擬地址空間,但如果某個(gè)一級(jí)頁(yè)表的頁(yè)表項(xiàng)沒(méi)有被用到,也就不需要?jiǎng)?chuàng)建這個(gè)頁(yè)表項(xiàng)對(duì)應(yīng)的二級(jí)頁(yè)表了,即可以在需要時(shí)才創(chuàng)建二級(jí)頁(yè)表。也就是,內(nèi)存中只需要保存一級(jí)頁(yè)表以及使用到的二級(jí)頁(yè)表,大量的未被使用的二級(jí)頁(yè)表則不需要分配內(nèi)存并加載在內(nèi)存中,因此,達(dá)到節(jié)省頁(yè)表占用內(nèi)存空間的目的。
對(duì)于 64 位的系統(tǒng),使用四級(jí)分頁(yè)目錄,分別是:
- 頁(yè)全局目錄項(xiàng) PGD(Page Global Directory);
- 頁(yè)上級(jí)目錄項(xiàng) PUD(Page Upper Directory);
- 頁(yè)中間目錄項(xiàng) PMD(Page Middle Directory);
- 頁(yè)表項(xiàng) PTE(Page Table Entry);
2.4 虛擬內(nèi)存區(qū)域(VMA)
進(jìn)程的虛擬內(nèi)存空間包含一段一段的虛擬內(nèi)存區(qū)域(Virtual memory area, 簡(jiǎn)稱 VMA),每個(gè) VMA 描述虛擬內(nèi)存空間中一段連續(xù)的區(qū)域,每個(gè) VMA 由許多虛擬頁(yè)組成,即每個(gè) VMA 包含許多頁(yè)表項(xiàng) PTE。
3、Fork 原理
在默認(rèn) fork 的調(diào)用過(guò)程中,父進(jìn)程需要將許多進(jìn)程元數(shù)據(jù)(例如文件描述符、信號(hào)量、頁(yè)表等)復(fù)制到子進(jìn)程,而頁(yè)表的復(fù)制是其中最耗時(shí)的部分(占據(jù) fork 調(diào)用耗時(shí)的 97% 以上)。
Linux 的 fork() 使用寫(xiě)時(shí)拷貝 (copy-on-write) 頁(yè)的方式實(shí)現(xiàn)。寫(xiě)時(shí)拷貝是一種可以推遲甚至避免拷貝數(shù)據(jù)的技術(shù)。在創(chuàng)建子進(jìn)程的過(guò)程中,操作系統(tǒng)會(huì)把父進(jìn)程的「頁(yè)表」復(fù)制一份給子進(jìn)程,這個(gè)頁(yè)表記錄著虛擬地址和物理地址映射關(guān)系,此時(shí),操作系統(tǒng)并不復(fù)制整個(gè)進(jìn)程的物理內(nèi)存,而是讓父子進(jìn)程共享同一個(gè)物理內(nèi)存。同時(shí),操作系統(tǒng)內(nèi)核會(huì)把共享的所有的內(nèi)存頁(yè)的權(quán)限都設(shè)為 read-only。
那什么時(shí)候會(huì)發(fā)生物理內(nèi)存的復(fù)制呢?
當(dāng)父進(jìn)程或者子進(jìn)程在向共享內(nèi)存發(fā)起寫(xiě)操作時(shí),內(nèi)存管理單元 MMU 檢測(cè)到內(nèi)存頁(yè)是 read-only 的,于是觸發(fā)缺頁(yè)中斷異常(page-fault),處理器會(huì)從中斷描述符表(IDT)中獲取到對(duì)應(yīng)的處理程序。在中斷程序中,內(nèi)核就會(huì)把觸發(fā)異常的物理內(nèi)存頁(yè)復(fù)制一份,并重新設(shè)置其內(nèi)存映射關(guān)系,將父子進(jìn)程的內(nèi)存讀寫(xiě)權(quán)限設(shè)置為可讀寫(xiě),于是父子進(jìn)程各自持有獨(dú)立的一份,之后進(jìn)程才會(huì)對(duì)內(nèi)存進(jìn)行寫(xiě)操作,這個(gè)過(guò)程也被稱為寫(xiě)時(shí)復(fù)制(Copy On Write)。
4、Fork 的痛點(diǎn)
在原生 fork 下,在父進(jìn)程調(diào)用 fork() 創(chuàng)建子進(jìn)程的過(guò)程中,雖然使用了寫(xiě)時(shí)復(fù)制頁(yè)表的方式進(jìn)行優(yōu)化,但由于要復(fù)制父進(jìn)程的頁(yè)表,還是會(huì)造成父進(jìn)程出現(xiàn)短時(shí)間阻塞,阻塞的時(shí)間跟頁(yè)表的大小有關(guān),頁(yè)表越大,阻塞的時(shí)間也越長(zhǎng)。
我們?cè)跍y(cè)試中很容易觀察到 fork 產(chǎn)生的阻塞現(xiàn)象,以及 fork 造成的 Redis 訪問(wèn)抖動(dòng)現(xiàn)象。
4.1 測(cè)試環(huán)境
Redis 版本:優(yōu)化前 Redis-server
機(jī)器操作系統(tǒng):無(wú) Async-fork 特性的系統(tǒng)
測(cè)試數(shù)據(jù)量:21.63G
4.2 阻塞現(xiàn)象復(fù)現(xiàn)
在使用 Redis-benchmark 壓測(cè)的過(guò)程中,手動(dòng)執(zhí)行 bgsave 命令,觀察 fork 耗時(shí)和壓測(cè)指標(biāo) TP100。
使用 info stats 返回上次 fork 耗時(shí):latest_fork_usec:183632,可以看到 fork 耗時(shí) 183 毫秒。
在壓測(cè)過(guò)程中分別不執(zhí)行 bgsave 和執(zhí)行 bgsave,結(jié)果如下:
從壓測(cè)數(shù)據(jù)可以看到,單機(jī)環(huán)境下壓測(cè),壓測(cè)時(shí)未執(zhí)行 bgsave,TP100 約 1 毫秒;如果壓測(cè)過(guò)程中,手動(dòng)執(zhí)行 bgsave 命令,觸發(fā) fork 操作,TP100 達(dá)到 187 毫秒。
4.3 Strace 跟蹤 fork 過(guò)程耗時(shí)
strace 常用來(lái)跟蹤進(jìn)程執(zhí)行時(shí)的系統(tǒng)調(diào)用和所接收的信號(hào)。
由于 Linux 中通過(guò) clone() 系統(tǒng)調(diào)用實(shí)現(xiàn) fork();我們可以看到追蹤到 clone 系統(tǒng)調(diào)用,并且耗時(shí) 183 毫秒,與 info stats統(tǒng)計(jì)的 fork 耗時(shí)一致。
5、Async-fork
鑒于以上 linux 原生 fork 系統(tǒng)調(diào)用的痛點(diǎn),對(duì)于像 Redis 這樣的高性能內(nèi)存數(shù)據(jù)庫(kù),將會(huì)增加 fork 期間的用戶訪問(wèn)延遲,論文中設(shè)計(jì)了一個(gè)新的 fork(稱為 Async-fork)來(lái)解決上述問(wèn)題。
Async-fork 設(shè)計(jì)的核心思想是將 fork 調(diào)用過(guò)程中最耗時(shí)的頁(yè)表拷貝工作從父進(jìn)程移動(dòng)到子進(jìn)程,縮短父進(jìn)程調(diào)用 fork 時(shí)陷入內(nèi)核態(tài)的時(shí)間,父進(jìn)程因而可以快速返回用戶態(tài)處理用戶查詢,子進(jìn)程則在此期間完成頁(yè)表拷貝。與 Linux 中的默認(rèn)原生 fork 相比,Async-fork 顯著減少了 Redis 快照期間到達(dá)請(qǐng)求的尾延遲。
5.1 Async-fork 的挑戰(zhàn)
然而,Async-fork 的實(shí)現(xiàn)過(guò)程中,實(shí)際工作并非描述的這么簡(jiǎn)單。頁(yè)表的異步復(fù)制操作可能導(dǎo)致快照不一致。以下圖為例,Redis 在 T0 時(shí)刻保存內(nèi)存快照,而某個(gè)用戶請(qǐng)求在 T2 時(shí)刻向 Redis 插入了新的鍵值對(duì)(k2, v2),這將導(dǎo)致父進(jìn)程修改它的頁(yè)表項(xiàng)(PTE2)。假如 T2 時(shí)刻這個(gè)被修改的頁(yè)表項(xiàng)(PTE2)還沒(méi)有被子進(jìn)程復(fù)制完成, 這個(gè)修改后的內(nèi)存頁(yè)表項(xiàng)及對(duì)應(yīng)內(nèi)存頁(yè)后續(xù)將被復(fù)制到子進(jìn)程,這個(gè)新插入的鍵值對(duì)將被子進(jìn)程最終寫(xiě)入硬盤(pán),破壞了快照一致性。(快照文件應(yīng)該記錄的是保存拍攝內(nèi)存快照那一刻的內(nèi)存數(shù)據(jù))
圖片來(lái)源于:參考資料[1] 第 8 頁(yè)
5.2 Async-fork 詳解
前面提到,每個(gè)進(jìn)程都有自己的虛擬內(nèi)存空間,Linux 使用一組虛擬內(nèi)存區(qū)域 VMA 來(lái)描述進(jìn)程的虛擬內(nèi)存空間,每個(gè) VMA 包含許多頁(yè)表項(xiàng)。
在默認(rèn) fork 中,父進(jìn)程遍歷每個(gè) VMA,將每個(gè) VMA 復(fù)制到子進(jìn)程,并自上而下地復(fù)制該 VMA 對(duì)應(yīng)的頁(yè)表項(xiàng)到子進(jìn)程,對(duì)于 64 位的系統(tǒng),使用四級(jí)分頁(yè)目錄,每個(gè) VMA 包括 PGD、PUD、PMD、PTE,都將由父進(jìn)程逐級(jí)復(fù)制完成。在 Async-fork 中,父進(jìn)程同樣遍歷每個(gè) VMA,但只負(fù)責(zé)將 PGD、PUD 這兩級(jí)頁(yè)表項(xiàng)復(fù)制到子進(jìn)程。
隨后,父進(jìn)程將子進(jìn)程放置到某個(gè) CPU 上使子進(jìn)程開(kāi)始運(yùn)行,父進(jìn)程返回到用戶態(tài),繼續(xù)響應(yīng)用戶請(qǐng)求。由子進(jìn)程負(fù)責(zé)每個(gè) VMA 剩下的 PMD 和 PTE 兩級(jí)頁(yè)表的復(fù)制工作。
如果在父進(jìn)程返回用戶態(tài)后,子進(jìn)程復(fù)制內(nèi)存頁(yè)表期間,父進(jìn)程需要修改還未完成復(fù)制的頁(yè)表項(xiàng),怎樣避免上述提到的破壞快照一致性問(wèn)題呢?
圖片來(lái)源于:參考資料[1] 第 7 頁(yè)
5.2.1 主動(dòng)同步機(jī)制
父進(jìn)程返回用戶態(tài)后,父進(jìn)程的 PTE 可能被修改。如果在子進(jìn)程復(fù)制內(nèi)存頁(yè)表期間,父進(jìn)程檢測(cè)到了 PTE 修改,則會(huì)觸發(fā)主動(dòng)同步機(jī)制,也就是父進(jìn)程也加入頁(yè)表復(fù)制工作,來(lái)主動(dòng)完成被修改的相關(guān)頁(yè)表復(fù)制,該機(jī)制用來(lái)確保 PTE 在修改前被復(fù)制到子進(jìn)程。
當(dāng)一個(gè) PTE 將被修改時(shí),父進(jìn)程不僅復(fù)制這一個(gè) PTE,還同時(shí)將位于同一個(gè)頁(yè)表上的所有 PTE(一共 512 個(gè) PTE),連同它的父級(jí) PMD 項(xiàng)復(fù)制到子進(jìn)程。
父進(jìn)程中的 PTE 發(fā)生修改時(shí),如果子進(jìn)程已經(jīng)復(fù)制過(guò)了這個(gè) PTE,父進(jìn)程就不需要復(fù)制了,否則會(huì)發(fā)生重復(fù)復(fù)制。怎么區(qū)分 PTE 是否已經(jīng)復(fù)制過(guò)?
Async-fork 使用 PMD 項(xiàng)上的 RW 位來(lái)標(biāo)記是否被復(fù)制。具體而言,當(dāng)父進(jìn)程第一次返回用戶態(tài)時(shí),它所有 PMD 項(xiàng)被設(shè)置為寫(xiě)保護(hù)(RW=0),代表這個(gè) PMD 項(xiàng)以及它指向的 512 個(gè) PTE 還沒(méi)有被復(fù)制到子進(jìn)程。當(dāng)子進(jìn)程復(fù)制一個(gè) PMD 項(xiàng)時(shí),通過(guò)檢查這個(gè) PMD 是否為寫(xiě)保護(hù),即可判斷該 PMD 是否已經(jīng)被復(fù)制到子進(jìn)程。如果還沒(méi)有被復(fù)制,子進(jìn)程將復(fù)制這個(gè) PMD,以及它指向的 512 個(gè) PTE。
在完成 PMD 及其指向的 512 個(gè) PTE 復(fù)制后,子進(jìn)程將父進(jìn)程中的該 PMD 設(shè)置為可寫(xiě)(RW=1),代表這個(gè) PMD 項(xiàng)以及它指向的 512 個(gè) PTE 已經(jīng)被復(fù)制到子進(jìn)程。當(dāng)父進(jìn)程觸發(fā)主動(dòng)同步時(shí),也通過(guò)檢查 PMD 項(xiàng)是否為寫(xiě)保護(hù)判斷是否被復(fù)制,并在完成復(fù)制后將 PMD 項(xiàng)設(shè)置為可寫(xiě)。同時(shí),在復(fù)制 PMD 項(xiàng)和 PTE 時(shí),父進(jìn)程和子進(jìn)程都鎖定 PTE 表,因此它們不會(huì)出現(xiàn)同時(shí)復(fù)制同一 PMD 項(xiàng)指向的 PTE。
在操作系統(tǒng)中,PTE 的修改分為兩類:
1)VMA 級(jí)的修改。例如,創(chuàng)建、合并、刪除 VMA 等操作作用于特定 VMA 上,VMA 級(jí)的修改通常會(huì)導(dǎo)致大量的 PTE 修改,因此涉及大量的 PMD。
2)PMD 級(jí)的修改。PMD 級(jí)的修改僅涉及一個(gè) PMD。
5.2.2 錯(cuò)誤處理
Async-fork 在復(fù)制頁(yè)表時(shí)涉及到內(nèi)存分配,難免會(huì)發(fā)生錯(cuò)誤。例如,由于內(nèi)存不足,進(jìn)程可能無(wú)法申請(qǐng)到新的 PTE 表。當(dāng)錯(cuò)誤發(fā)生時(shí),應(yīng)該將父進(jìn)程恢復(fù)到它調(diào)用 Async-fork 之前的狀態(tài)。
在 Async-fork 中,父進(jìn)程 PMD 項(xiàng)目的 RW 位可能會(huì)被修改。因此,當(dāng)發(fā)生錯(cuò)誤時(shí),需要將 PMD 項(xiàng)全部回滾為可寫(xiě)。
6、Redis 優(yōu)化實(shí)踐
6.1 Async-fork 阻塞現(xiàn)象
在支持 Async-fork 的操作系統(tǒng)(即 Tair 專屬操作系統(tǒng)鏡像)機(jī)器上測(cè)試,理論上來(lái)說(shuō),按照文章的預(yù)期,用戶不需要作任何修改(Async-fork 使用了原生 fork 相同的接口,沒(méi)有另外新增接口),就可以享受 Async-fork 優(yōu)化帶來(lái)的優(yōu)勢(shì),但是,使用 Redis 實(shí)際測(cè)試過(guò)程中,結(jié)果不符合預(yù)期,在 Redis 壓測(cè)過(guò)程中手動(dòng)執(zhí)行 bgsave 命令觸發(fā) fork 操作,還是觀察到了 TP100 抖動(dòng)現(xiàn)象。
測(cè)試環(huán)境
Redis 版本:優(yōu)化前 Redis-Server
機(jī)器操作系統(tǒng):Tair 專屬操作系統(tǒng)鏡像
測(cè)試數(shù)據(jù)量:54.38G
問(wèn)題現(xiàn)象
現(xiàn)象:fork 耗時(shí)正常,但是壓測(cè)過(guò)程中執(zhí)行 bgsave,TP100 不正常
在壓測(cè)過(guò)程中執(zhí)行 bgsave,使用 info stats 返回上次 fork 耗時(shí):latest_fork_usec:426
TP100 結(jié)果如下:
也就是說(shuō),觀察到的 fork 耗時(shí)正常,但是壓測(cè)過(guò)程中 Redis 依然出現(xiàn)了尾延遲,這顯然不符合預(yù)期。
追蹤過(guò)程
使用 strace 命令進(jìn)行分析,結(jié)果如下:
可以觀察到,clone 耗時(shí) 380 微秒,已經(jīng)大幅降低,也就 fork 快速返回了用戶態(tài)響應(yīng)用戶請(qǐng)求。然而,注意到,緊接著出現(xiàn)了一個(gè) mmap 耗時(shí) 358 毫秒,與 TP100 數(shù)據(jù)接近。
由于 mmap 系統(tǒng)調(diào)用會(huì)在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段滿足大小要求的虛擬地址,并且為此虛擬地址分配一個(gè)虛擬內(nèi)存區(qū)域( vm_area_struct 結(jié)構(gòu)),也就是會(huì)觸發(fā) VMA 級(jí)虛擬頁(yè)表變化,也就觸發(fā)父進(jìn)程主動(dòng)同步機(jī)制,父進(jìn)程主動(dòng)幫助完成相應(yīng)頁(yè)表復(fù)制動(dòng)作。VMA 級(jí)虛擬頁(yè)表變化,需要將對(duì)應(yīng)的三級(jí)和四級(jí)所有頁(yè)目錄都復(fù)制到子進(jìn)程,因此,耗時(shí)比較高。
那么,這個(gè) mmap 調(diào)用又是哪里來(lái)的呢?
定位問(wèn)題
perf 是 Linux下的一款性能分析工具,能夠進(jìn)行函數(shù)級(jí)與指令級(jí)的熱點(diǎn)查找。
通過(guò) perf trace 可以看到響應(yīng)調(diào)用堆棧及耗時(shí),分析結(jié)果如下:
也就可以看到,在 bgsave 執(zhí)行邏輯中,有一處打印日志中的 fprintf 調(diào)用了 mmap,很顯然這應(yīng)該是 fork 返回父進(jìn)程后,父進(jìn)程中某處調(diào)用。
6.2 Async-fork 適配優(yōu)化
針對(duì)找出來(lái)的代碼位置,可以進(jìn)行相應(yīng)優(yōu)化,針對(duì)此處的日志影響,我們可以屏蔽日志或者將日志移動(dòng)到子進(jìn)程進(jìn)行打印,通過(guò)同樣的分析手段,如果存在其他影響,均可進(jìn)行對(duì)應(yīng)優(yōu)化。進(jìn)行相應(yīng)適配優(yōu)化修改后,我們?cè)俅芜M(jìn)行測(cè)試。
測(cè)試環(huán)境
Redis 版本:優(yōu)化后 Redis-Server
機(jī)器操作系統(tǒng):Tair 專屬操作系統(tǒng)鏡像
測(cè)試數(shù)據(jù)量:54.38G
現(xiàn)象
在壓測(cè)過(guò)程中執(zhí)行 bgsave,fork 耗時(shí)和 TP100 均正常。
使用 info stats 返回上次 fork 耗時(shí):latest_fork_usec:414
TP100 結(jié)果如下:
跟蹤驗(yàn)證
再次使用 strace 和 perf 工具跟蹤驗(yàn)證
strace 跟蹤父進(jìn)程只看到 clone,并且耗時(shí)只有 378 微秒,
Perf trace 跟蹤父進(jìn)程也只看到 clone 調(diào)用
由于我們的優(yōu)化是將觸發(fā) mmap 的相關(guān)日志修改到子進(jìn)程中,使用 Perf trace 跟蹤 fork 產(chǎn)生的子進(jìn)程,命令為:
strace -p 14697 -T -tt -f -ff -o strace05.out
通過(guò) Redis 日志文件找到子進(jìn)程 pid 為 15931;打開(kāi)對(duì)應(yīng)生成的保存子進(jìn)程 strace 信息的文件strace05.out.15931(父進(jìn)程 strace 信息保存在文件strace05.out.14697)
在子進(jìn)程中看到了 mmap 調(diào)用,子進(jìn)程中調(diào)用不會(huì)影響父進(jìn)程對(duì)業(yè)務(wù)訪問(wèn)的響應(yīng)。
7性能測(cè)試
修改 Redis 代碼,針對(duì) Async-fork 適配優(yōu)化后,我們針對(duì) fork 與 Async-fork 進(jìn)行了性能對(duì)比測(cè)試;測(cè)試包含不同數(shù)據(jù)量下 fork() 命令耗時(shí)與 fork() 操作對(duì)壓測(cè)過(guò)程中 TP100 的影響。
7.1 fork() 命令耗時(shí)
fork() 命令耗時(shí),即針對(duì) Redis 執(zhí)行 bgsave 命令后,通過(guò) Redis 提供的 info stats命令觀察到的latest_fork_usec用時(shí)。
注:由于 fork 與 Async-fork 系統(tǒng)下,fork() 操作產(chǎn)生的latest_fork_usec數(shù)據(jù)差距懸殊非常大,使用單縱軸會(huì)導(dǎo)致 Async-fork 的數(shù)據(jù)在圖表中顯示不明顯,不方便查看,因此,該圖表使用了雙縱軸;雖然 Async-fork 的圖表看起來(lái)比較高,但是實(shí)際右縱軸范圍小,所以數(shù)據(jù)小
從圖表可以看出,使用支持 Async-fork 的操作系統(tǒng),fork() 操作產(chǎn)生的耗時(shí)非常小,不管數(shù)據(jù)量多大,耗時(shí)都非常穩(wěn)定,基本在 200 微秒左右;而原生 fork 產(chǎn)生的耗時(shí)會(huì)隨著數(shù)據(jù)量增長(zhǎng)而增長(zhǎng),而且是從幾十毫秒增長(zhǎng)到幾百毫秒。
7.2 TP100 抖動(dòng)
在使用 Redis-benchmark 壓測(cè)過(guò)程中,手動(dòng)執(zhí)行 bgsave 命令,觸發(fā)操作系統(tǒng) fork() 操作,觀察不同數(shù)據(jù)量下,fork 與 Async-fork 對(duì) Redis 壓測(cè)時(shí) TP100 的影響。
從圖上可以看出,使用支持 Async-fork 的操作系統(tǒng),fork() 操作對(duì) Redis 壓測(cè)產(chǎn)生的性能影響非常小,性能提升非常明顯,不管數(shù)據(jù)量多大,耗時(shí)都非常穩(wěn)定,基本在 1-2 毫秒左右;而原生 fork 產(chǎn)生的抖動(dòng)影響時(shí)間會(huì)隨著數(shù)據(jù)量增長(zhǎng)而增長(zhǎng), TP100 從幾十毫秒增長(zhǎng)到幾百毫秒。
8、總結(jié)
通過(guò)不同數(shù)據(jù)量下對(duì)比測(cè)試,我們可以看到,Async-fork 相比原生 fork,阻塞時(shí)間大大減少,性能提升非常明顯。而且阻塞時(shí)間非常穩(wěn)定,不會(huì)因?yàn)閿?shù)據(jù)量的增長(zhǎng)出現(xiàn)倍數(shù)級(jí)增長(zhǎng)。
在單機(jī)測(cè)試場(chǎng)景下,8G 數(shù)據(jù)量大小下,TP100 和latest_fork_usec 耗時(shí)均減少 98% 以上。
基于論文中 Async-fork 的設(shè)計(jì)思想,Tair 專屬操作系統(tǒng)鏡像已支持該特性,并且將該特性集成在原生 fork 中,沒(méi)有新增系統(tǒng)調(diào)用接口,理論上用戶只需要使用支持 Async-fork 的操作系統(tǒng),程序無(wú)需做任何修改,就可以享受到 Async-fork 特性帶來(lái)的性能提升。對(duì)于 Redis 而言,我們也只需要對(duì) Redis 稍加適配就可以獲得該技術(shù)帶來(lái)的紅利。
在 Redis 應(yīng)用場(chǎng)景中,在添加從節(jié)點(diǎn)、RDB 文件備份、AOF 持久化文件重寫(xiě)等場(chǎng)景下,應(yīng)用支持 Async-fork 的操作系統(tǒng),都將極大的減少對(duì)業(yè)務(wù)的影響。