最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

VLDB頂會論文Async-fork解讀與Redis在得物的實踐

2023-07-25 16:58 作者:阿里云  | 我要投稿

1. 背景

在Redis中,在AOF文件重寫、生成RDB備份文件以及主從全量同步過程中,都需要使用系統(tǒng)調(diào)用fork創(chuàng)建一個子進程來獲取內(nèi)存數(shù)據(jù)快照,在fork()函數(shù)創(chuàng)建子進程的時候,內(nèi)核會把父進程的「頁表」復(fù)制一份給子進程,如果頁表很大,復(fù)制頁表的過程耗時會非常長,那么在此期間,業(yè)務(wù)訪問Redis讀寫延遲會大幅增加。

最近,阿里云聯(lián)合上海交大,在數(shù)據(jù)庫頂級會議VLDB上發(fā)表了一篇文章《Async-fork: Mitigating Query Latency Spikes Incurred by the Fork-based Snapshot Mechanism from the OS Level》,文章介紹到,他們設(shè)計了一個新的fork(稱為Async-fork),將fork調(diào)用過程中最耗時的頁表拷貝部分從父進程移動到子進程,父進程因而可以快速返回用戶態(tài)處理用戶查詢,子進程則在此期間完成頁表拷貝,從而減少fork期間到達(dá)請求的尾延遲。所以該特性在類似Redis類型的內(nèi)存數(shù)據(jù)庫上均能取得不錯的效果。

2. 基本概念

2.1 物理內(nèi)存地址

也即實際的物理內(nèi)存地址空間。

2.2?虛擬地址空間

虛擬地址空間(Virtual Address Space)是每一個程序被加載運行起來后,操作系統(tǒng)為進程分配的虛擬內(nèi)存,它為每個進程提供了一個假象,即每個進程都在獨占地使用主存。

每個進程所能訪問的最大的虛擬地址空間由計算機的硬件平臺決定,具體地說是由CPU的位數(shù)決定的。比如32位的CPU就是我們常說的4GB虛擬內(nèi)存空間。

程序訪問內(nèi)存地址使用虛擬地址空間,然后由操作系統(tǒng)將這個虛擬地址映射到適當(dāng)?shù)奈锢韮?nèi)存地址上。這樣,只要操作系統(tǒng)處理好虛擬地址到物理內(nèi)存地址的映射,就可以保證不同的程序最終訪問的內(nèi)存地址位于不同的區(qū)域,彼此沒有重疊,就可以達(dá)到內(nèi)存地址空間隔離的效果。

當(dāng)進程創(chuàng)建時,每個進程都會有一個自己的4GB虛擬地址空間。要注意的是這個4GB的地址空間是“虛擬”的,并不是真實存在的,而且每個進程只能訪問自己虛擬地址空間中的數(shù)據(jù),無法訪問別的進程中的數(shù)據(jù),通過這種方法實現(xiàn)了進程間的地址隔離。

對于Linux,4GB的虛擬地址空間包含用戶態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間兩部分,默認(rèn)分配狀態(tài)如下:

2.3?內(nèi)存頁表

「頁表」保存的是虛擬內(nèi)存地址與物理內(nèi)存地址的映射關(guān)系。

CPU訪問數(shù)據(jù)的時候,CPU發(fā)出的地址是虛擬地址,CPU中內(nèi)存管理單元(MMU)通過查詢頁表,把虛擬地址轉(zhuǎn)換為物理地址,再去訪問物理內(nèi)存條。

2.3.1 內(nèi)存分頁

分頁是把整個虛擬和物理內(nèi)存空間切成一段段固定尺寸的大小,這樣一個連續(xù)并且尺寸固定的內(nèi)存空間,我們叫頁(Page)。在Linux下,每一頁的大小為4KB。

在32位的環(huán)境下,虛擬地址空間共有4GB,假設(shè)一個頁的大小是4KB(2^12),那么就需要大約100萬(2^20)個頁,每個「頁表項」需要4個字節(jié)大小來存儲,那么整個4GB空間的映射就需要有4MB的內(nèi)存來存儲頁表。

這4MB大小的頁表,看起來也不是很大。但是每個進程都是有自己的虛擬地址空間,也就說都有自己的頁表。每個機器上同時運行多個進程,頁表將占用大量內(nèi)存。

2.3.2?多級頁表

要解決上面提到的存儲進程頁表項占用大量內(nèi)存空間的問題,就需要采用一種叫作多級頁表(Multi-Level Page Table)的解決方案。

我們把這個100 多萬個「頁表項」的單級頁表再分頁,將頁表(一級頁表)分為1024個頁表(二級頁表),每個二級頁表中包含1024個「頁表項」,形成二級分頁。這樣,一級頁表就可以覆蓋整個4GB 虛擬地址空間,但如果某個一級頁表的頁表項沒有被用到,也就不需要創(chuàng)建這個頁表項對應(yīng)的二級頁表了,即可以在需要時才創(chuàng)建二級頁表。也就是,內(nèi)存中只需要保存一級頁表以及使用到的二級頁表,大量的未被使用的二級頁表則不需要分配內(nèi)存并加載在內(nèi)存中,因此,達(dá)到節(jié)省頁表占用內(nèi)存空間的目的。

對于64位的系統(tǒng),使用四級分頁目錄,分別是:

?頁全局目錄項PGD(Page Global Directory);

?頁上級目錄項PUD(Page Upper Directory);

?頁中間目錄項PMD(Page Middle Directory);

?頁表項PTE(Page Table Entry);

2.4?虛擬內(nèi)存區(qū)域(VMA)

進程的虛擬內(nèi)存空間包含一段一段的虛擬內(nèi)存區(qū)域(Virtual memory area, 簡稱 VMA),每個VMA描述虛擬內(nèi)存空間中一段連續(xù)的區(qū)域,每個VMA由許多虛擬頁組成,即每個VMA包含許多頁表項PTE。

3. Fork原理

在默認(rèn)fork的調(diào)用過程中,父進程需要將許多進程元數(shù)據(jù)(例如文件描述符、信號量、頁表等)復(fù)制到子進程,而頁表的復(fù)制是其中最耗時的部分(占據(jù)fork調(diào)用耗時的97%以上)。

Linux的fork()使用寫時拷貝 (copy-on-write) 頁的方式實現(xiàn)。寫時拷貝是一種可以推遲甚至避免拷貝數(shù)據(jù)的技術(shù)。在創(chuàng)建子進程的過程中,操作系統(tǒng)會把父進程的「頁表」復(fù)制一份給子進程,這個頁表記錄著虛擬地址和物理地址映射關(guān)系,此時,操作系統(tǒng)并不復(fù)制整個進程的物理內(nèi)存,而是讓父子進程共享同一個物理內(nèi)存。同時,操作系統(tǒng)內(nèi)核會把共享的所有的內(nèi)存頁的權(quán)限都設(shè)為read-only。

那什么時候會發(fā)生物理內(nèi)存的復(fù)制呢?

當(dāng)父進程或者子進程在向共享內(nèi)存發(fā)起寫操作時,內(nèi)存管理單元MMU檢測到內(nèi)存頁是read-only的,于是觸發(fā)缺頁中斷異常(page-fault),處理器會從中斷描述符表(IDT)中獲取到對應(yīng)的處理程序。在中斷程序中,內(nèi)核就會把觸發(fā)異常的物理內(nèi)存頁復(fù)制一份,并重新設(shè)置其內(nèi)存映射關(guān)系,將父子進程的內(nèi)存讀寫權(quán)限設(shè)置為可讀寫,于是父子進程各自持有獨立的一份,之后進程才會對內(nèi)存進行寫操作,這個過程也被稱為寫時復(fù)制(Copy On Write)。

4.?Fork的痛點

在原生fork下,在父進程調(diào)用fork()創(chuàng)建子進程的過程中,雖然使用了寫時復(fù)制頁表的方式進行優(yōu)化,但由于要復(fù)制父進程的頁表,還是會造成父進程出現(xiàn)短時間阻塞,阻塞的時間跟頁表的大小有關(guān),頁表越大,阻塞的時間也越長。

我們在測試中很容易觀察到fork產(chǎn)生的阻塞現(xiàn)象,以及fork造成的Redis訪問抖動現(xiàn)象。

4.1?測試環(huán)境

Redis版本:優(yōu)化前Redis-server

機器操作系統(tǒng):無Async-fork特性的系統(tǒng)

測試數(shù)據(jù)量:21.63G

4.2?阻塞現(xiàn)象復(fù)現(xiàn)

在使用Redis-benchmark壓測的過程中,手動執(zhí)行bgsave命令,觀察fork耗時和壓測指標(biāo)TP100。

使用?info stats?返回上次fork耗時:latest_fork_usec:183632,可以看到fork耗時183毫秒。

在壓測過程中分別不執(zhí)行bgsave和執(zhí)行bgsave,結(jié)果如下:

從壓測數(shù)據(jù)可以看到,單機環(huán)境下壓測,壓測時未執(zhí)行bgsave,TP100約1毫秒;如果壓測過程中,手動執(zhí)行bgsave命令,觸發(fā)fork操作,TP100達(dá)到187毫秒。

4.3?Strace跟蹤fork過程耗時

strace 常用來跟蹤進程執(zhí)行時的系統(tǒng)調(diào)用和所接收的信號。

由于Linux中通過clone()系統(tǒng)調(diào)用實現(xiàn)fork();我們可以看到追蹤到clone系統(tǒng)調(diào)用,并且耗時183毫秒,與?info stats?統(tǒng)計的fork耗時一致。

5.?Async-fork

鑒于以上linux原生fork系統(tǒng)調(diào)用的痛點,對于像Redis這樣的高性能內(nèi)存數(shù)據(jù)庫,將會增加fork期間的用戶訪問延遲,論文中設(shè)計了一個新的fork(稱為Async-fork)來解決上述問題。

Async-fork設(shè)計的核心思想是將fork調(diào)用過程中最耗時的頁表拷貝工作從父進程移動到子進程,縮短父進程調(diào)用fork時陷入內(nèi)核態(tài)的時間,父進程因而可以快速返回用戶態(tài)處理用戶查詢,子進程則在此期間完成頁表拷貝。與Linux中的默認(rèn)原生fork相比,Async-fork顯著減少了Redis快照期間到達(dá)請求的尾延遲。

5.1 Async-fork 的挑戰(zhàn)

然而,Async-fork的實現(xiàn)過程中,實際工作并非描述的這么簡單。頁表的異步復(fù)制操作可能導(dǎo)致快照不一致。以下圖為例,Redis在T0時刻保存內(nèi)存快照,而某個用戶請求在T2時刻向Redis插入了新的鍵值對(k2, v2),這將導(dǎo)致父進程修改它的頁表項(PTE2)。假如T2時刻這個被修改的頁表項(PTE2)還沒有被子進程復(fù)制完成, 這個修改后的內(nèi)存頁表項及對應(yīng)內(nèi)存頁后續(xù)將被復(fù)制到子進程,這個新插入的鍵值對將被子進程最終寫入硬盤,破壞了快照一致性。(快照文件應(yīng)該記錄的是保存拍攝內(nèi)存快照那一刻的內(nèi)存數(shù)據(jù))

圖片來源于:參考資料[1] 第8頁

5.2?Async-fork詳解

前面提到,每個進程都有自己的虛擬內(nèi)存空間,Linux使用一組虛擬內(nèi)存區(qū)域VMA來描述進程的虛擬內(nèi)存空間,每個VMA包含許多頁表項。

在默認(rèn)fork中,父進程遍歷每個VMA,將每個VMA復(fù)制到子進程,并自上而下地復(fù)制該VMA對應(yīng)的頁表項到子進程,對于64位的系統(tǒng),使用四級分頁目錄,每個VMA包括PGD、PUD、PMD、PTE,都將由父進程逐級復(fù)制完成。在Async-fork中,父進程同樣遍歷每個VMA,但只負(fù)責(zé)將PGD、PUD這兩級頁表項復(fù)制到子進程。

隨后,父進程將子進程放置到某個CPU上使子進程開始運行,父進程返回到用戶態(tài),繼續(xù)響應(yīng)用戶請求。由子進程負(fù)責(zé)每個VMA剩下的PMD和PTE兩級頁表的復(fù)制工作。

如果在父進程返回用戶態(tài)后,子進程復(fù)制內(nèi)存頁表期間,父進程需要修改還未完成復(fù)制的頁表項,怎樣避免上述提到的破壞快照一致性問題呢?

圖片來源于:參考資料[1] 第7頁

5.2.1?主動同步機制

父進程返回用戶態(tài)后,父進程的PTE可能被修改。如果在子進程復(fù)制內(nèi)存頁表期間,父進程檢測到了PTE修改,則會觸發(fā)主動同步機制,也就是父進程也加入頁表復(fù)制工作,來主動完成被修改的相關(guān)頁表復(fù)制,該機制用來確保PTE在修改前被復(fù)制到子進程。

當(dāng)一個PTE將被修改時,父進程不僅復(fù)制這一個PTE,還同時將位于同一個頁表上的所有PTE(一共512個PTE),連同它的父級PMD項復(fù)制到子進程。

父進程中的PTE發(fā)生修改時,如果子進程已經(jīng)復(fù)制過了這個PTE,父進程就不需要復(fù)制了,否則會發(fā)生重復(fù)復(fù)制。怎么區(qū)分PTE是否已經(jīng)復(fù)制過?

Async-fork使用PMD項上的RW位來標(biāo)記是否被復(fù)制。具體而言,當(dāng)父進程第一次返回用戶態(tài)時,它所有PMD項被設(shè)置為寫保護(RW=0),代表這個PMD項以及它指向的512個PTE還沒有被復(fù)制到子進程。當(dāng)子進程復(fù)制一個PMD項時,通過檢查這個PMD是否為寫保護,即可判斷該PMD是否已經(jīng)被復(fù)制到子進程。如果還沒有被復(fù)制,子進程將復(fù)制這個PMD,以及它指向的512個PTE。

在完成PMD及其指向的512個PTE復(fù)制后,子進程將父進程中的該PMD設(shè)置為可寫(RW=1),代表這個PMD項以及它指向的512個PTE已經(jīng)被復(fù)制到子進程。當(dāng)父進程觸發(fā)主動同步時,也通過檢查PMD項是否為寫保護判斷是否被復(fù)制,并在完成復(fù)制后將PMD項設(shè)置為可寫。同時,在復(fù)制PMD項和PTE時,父進程和子進程都鎖定PTE表,因此它們不會出現(xiàn)同時復(fù)制同一PMD項指向的PTE。

在操作系統(tǒng)中,PTE的修改分為兩類:

?VMA級的修改。例如,創(chuàng)建、合并、刪除VMA等操作作用于特定VMA上,VMA級的修改通常會導(dǎo)致大量的PTE修改,因此涉及大量的PMD。

?PMD級的修改。PMD級的修改僅涉及一個PMD。

5.2.2?錯誤處理

Async-fork在復(fù)制頁表時涉及到內(nèi)存分配,難免會發(fā)生錯誤。例如,由于內(nèi)存不足,進程可能無法申請到新的PTE表。當(dāng)錯誤發(fā)生時,應(yīng)該將父進程恢復(fù)到它調(diào)用Async-fork之前的狀態(tài)。

在Async-fork中,父進程PMD項目的RW位可能會被修改。因此,當(dāng)發(fā)生錯誤時,需要將PMD項全部回滾為可寫。

6.?Redis優(yōu)化實踐

6.1 Async-fork 阻塞現(xiàn)象

在支持Async-fork的操作系統(tǒng)(即Tair專屬操作系統(tǒng)鏡像)機器上測試,理論上來說,按照文章的預(yù)期,用戶不需要作任何修改(Async-fork使用了原生fork相同的接口,沒有另外新增接口),就可以享受Async-fork優(yōu)化帶來的優(yōu)勢,但是,使用Redis實際測試過程中,結(jié)果不符合預(yù)期,在Redis壓測過程中手動執(zhí)行bgsave命令觸發(fā)fork操作,還是觀察到了TP100抖動現(xiàn)象。

?測試環(huán)境

Redis版本:優(yōu)化前Redis-Server

機器操作系統(tǒng):Tair專屬操作系統(tǒng)鏡像

測試數(shù)據(jù)量:54.38G

?問題現(xiàn)象

現(xiàn)象:fork耗時正常,但是壓測過程中執(zhí)行bgsave,TP100不正常

在壓測過程中執(zhí)行bgsave,使用?info stats?返回上次fork耗時:latest_fork_usec:426

TP100結(jié)果如下:

也就是說,觀察到的fork耗時正常,但是壓測過程中Redis依然出現(xiàn)了尾延遲,這顯然不符合預(yù)期。

?追蹤過程

使用 strace 命令進行分析,結(jié)果如下:

可以觀察到,clone耗時380微秒,已經(jīng)大幅降低,也就fork快速返回了用戶態(tài)響應(yīng)用戶請求。然而,注意到,緊接著出現(xiàn)了一個mmap耗時358毫秒,與TP100數(shù)據(jù)接近。

由于mmap系統(tǒng)調(diào)用會在當(dāng)前進程的虛擬地址空間中,尋找一段滿足大小要求的虛擬地址,并且為此虛擬地址分配一個虛擬內(nèi)存區(qū)域( vm_area_struct 結(jié)構(gòu)),也就是會觸發(fā)VMA級虛擬頁表變化,也就觸發(fā)父進程主動同步機制,父進程主動幫助完成相應(yīng)頁表復(fù)制動作。VMA級虛擬頁表變化,需要將對應(yīng)的三級和四級所有頁目錄都復(fù)制到子進程,因此,耗時比較高。

那么,這個mmap調(diào)用又是哪里來的呢?

?定位問題

perf是Linux下的一款性能分析工具,能夠進行函數(shù)級與指令級的熱點查找。

通過perf trace可以看到響應(yīng)調(diào)用堆棧及耗時,分析結(jié)果如下:

也就可以看到,在bgsave執(zhí)行邏輯中,有一處打印日志中的fprintf調(diào)用了mmap,很顯然這應(yīng)該是fork返回父進程后,父進程中某處調(diào)用。

6.2?Async-fork 適配優(yōu)化

針對找出來的代碼位置,可以進行相應(yīng)優(yōu)化,針對此處的日志影響,我們可以屏蔽日志或者將日志移動到子進程進行打印,通過同樣的分析手段,如果存在其他影響,均可進行對應(yīng)優(yōu)化。進行相應(yīng)適配優(yōu)化修改后,我們再次進行測試。

?測試環(huán)境

Redis版本:優(yōu)化后Redis-Server

機器操作系統(tǒng):Tair專屬操作系統(tǒng)鏡像

測試數(shù)據(jù)量:54.38G

?現(xiàn)象

在壓測過程中執(zhí)行bgsave,fork耗時和TP100均正常。

使用?info stats?返回上次fork耗時:latest_fork_usec:414

TP100結(jié)果如下:

?跟蹤驗證

再次使用strace和perf工具跟蹤驗證

strace跟蹤父進程只看到clone,并且耗時只有378微秒,

Perf trace跟蹤父進程也只看到clone調(diào)用

由于我們的優(yōu)化是將觸發(fā)mmap的相關(guān)日志修改到子進程中,使用Perf trace跟蹤fork產(chǎn)生的子進程,命令為:

strace -p 14697 -T -tt -f -ff -o strace05.out

通過Redis日志文件找到子進程pid為15931;打開對應(yīng)生成的保存子進程strace信息的文件strace05.out.15931(父進程strace信息保存在文件?strace05.out.14697)

在子進程中看到了mmap調(diào)用,子進程中調(diào)用不會影響父進程對業(yè)務(wù)訪問的響應(yīng)。

7. 性能測試

修改Redis代碼,針對Async-fork適配優(yōu)化后,我們針對fork與Async-fork進行了性能對比測試;測試包含不同數(shù)據(jù)量下fork()命令耗時與fork()操作對壓測過程中TP100的影響。

7.1 fork()命令耗時

fork()命令耗時,即針對Redis執(zhí)行?bgsave?命令后,通過Redis提供的?info stats?命令觀察到的?latest_fork_usec?用時。

注:由于fork與Async-fork系統(tǒng)下,fork()操作產(chǎn)生的?latest_fork_usec?數(shù)據(jù)差距懸殊非常大,使用單縱軸會導(dǎo)致Async-fork的數(shù)據(jù)在圖表中顯示不明顯,不方便查看,因此,該圖表使用了雙縱軸;雖然Async-fork的圖表看起來比較高,但是實際右縱軸范圍小,所以數(shù)據(jù)小。

從圖表可以看出,使用支持Async-fork的操作系統(tǒng),fork()操作產(chǎn)生的耗時非常小,不管數(shù)據(jù)量多大,耗時都非常穩(wěn)定,基本在200微秒左右;而原生fork產(chǎn)生的耗時會隨著數(shù)據(jù)量增長而增長,而且是從幾十毫秒增長到幾百毫秒。

7.2 TP100抖動

在使用Redis-benchmark壓測過程中,手動執(zhí)行bgsave命令,觸發(fā)操作系統(tǒng)fork()操作,觀察不同數(shù)據(jù)量下,fork與Async-fork對Redis壓測時TP100的影響。

從圖上可以看出,使用支持Async-fork的操作系統(tǒng),fork()操作對Redis壓測產(chǎn)生的性能影響非常小,性能提升非常明顯,不管數(shù)據(jù)量多大,耗時都非常穩(wěn)定,基本在1-2毫秒左右;而原生fork產(chǎn)生的抖動影響時間會隨著數(shù)據(jù)量增長而增長,TP100從幾十毫秒增長到幾百毫秒。

8.?總結(jié)

通過不同數(shù)據(jù)量下對比測試,我們可以看到,Async-fork相比原生fork,阻塞時間大大減少,性能提升非常明顯。而且阻塞時間非常穩(wěn)定,不會因為數(shù)據(jù)量的增長出現(xiàn)倍數(shù)級增長。

在單機測試場景下,8G數(shù)據(jù)量大小下,TP100和?latest_fork_usec?耗時均減少98% 以上。

基于論文中Async-fork的設(shè)計思想,Tair專屬操作系統(tǒng)鏡像已支持該特性,并且將該特性集成在原生fork 中,沒有新增系統(tǒng)調(diào)用接口,理論上用戶只需要使用支持Async-fork的操作系統(tǒng),程序無需做任何修改,就可以享受到Async-fork特性帶來的性能提升。對于Redis而言,我們也只需要對Redis稍加適配就可以獲得該技術(shù)帶來的紅利。

在Redis應(yīng)用場景中,在添加從節(jié)點、RDB文件備份、AOF持久化文件重寫等場景下,應(yīng)用支持Async-fork的操作系統(tǒng),都將極大的減少對業(yè)務(wù)的影響。

參考資料:

[1]?《Async-fork: Mitigating Query Latency Spikes Incurred by the Fork-based Snapshot Mechanism from the OS Level》


VLDB頂會論文Async-fork解讀與Redis在得物的實踐的評論 (共 條)

分享到微博請遵守國家法律
田东县| 延安市| 永清县| 昌宁县| 黄龙县| 绥滨县| 勐海县| 丰宁| 泌阳县| 繁峙县| 武定县| 玉林市| 临武县| 河津市| 桦南县| 河间市| 庐江县| 霸州市| 益阳市| 兴和县| 桐城市| 衡山县| 那曲县| 江口县| 彭州市| 温宿县| 昆明市| 武夷山市| 七台河市| 田东县| 察隅县| 宝丰县| 淮滨县| 栾川县| 定襄县| 徐汇区| 大方县| 信丰县| 休宁县| 定日县| 大埔县|