偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

VLDB 頂會(huì)論文 Async-fork 解讀與 Redis 實(shí)踐

數(shù)據(jù)庫(kù) 其他數(shù)據(jù)庫(kù)
通過(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)。

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

127.0.0.1:6380> info memory
# Memory
used_memory:23220597688
used_memory_human: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è)過(guò)程中未執(zhí)行 bgsave
[root@xxx bin]# Redis-benchmark -d 256 -t set -n 1000000 -a xxxxxx -p 6380
====== SET ======
1000000 requests completed in 8.15 seconds
50 parallel clients
256 bytes payload
keep alive: 1


99.90% <= 1 milliseconds
100.00% <= 1 milliseconds
122669.27 requests per second


# 壓測(cè)過(guò)程中執(zhí)行 bgsave
[root@xxx bin]# Redis-benchmark -d 256 -t set -n 1000000 -a xxxxxx -p 6380
====== SET ======
1000000 requests completed in 13.97 seconds
50 parallel clients
256 bytes payload
keep alive: 1


86.41% <= 1 milliseconds
86.42% <= 2 milliseconds
99.95% <= 3 milliseconds
99.99% <= 4 milliseconds
99.99% <= 10 milliseconds
99.99% <= 11 milliseconds
99.99% <= 12 milliseconds
100.00% <= 187 milliseconds
100.00% <= 187 milliseconds
71561.47 requests per second

從壓測(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)。

$ strace -p 32088 -T -tt -o strace00.out
14:01:33.623495 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fbe5242fa50) = 37513 <0.183533>
14:01:33.807142 open("/data1/6380/6380.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 60 <0.000018>
14:01:33.807644 lseek(60, 0, SEEK_END) = 8512 <0.000017>
14:01:33.807690 stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=528, ...}) = 0 <0.000010>
14:01:33.807732 fstat(60, {st_mode=S_IFREG|0644, st_size=8512, ...}) = 0 <0.000007>
14:01:33.807756 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbe52437000 <0.000009>
14:01:33.807787 write(60, "35994:M 21 Mar 14:01:33.807 * Ba"..., 69) = 69 <0.000015>
14:01:33.807819 close(60) = 0 <0.000008>
14:01:33.807845 munmap(0x7fbe52437000, 4096) = 0 <0.000013>

由于 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

127.0.0.1:6679> info memory
# Memory
used_memory:58385641120
used_memory_human: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é)果如下:

# 壓測(cè)過(guò)程中執(zhí)行 bgsave
[root@xxx ~]# /usr/bin/Redis-benchmark -d 256 -t set -n 1000000 -a xxxxxx -p 6679
====== SET ======
1000000 requests completed in 7.88 seconds
50 parallel clients
256 bytes payload
keep alive: 1


100.00% <= 411 milliseconds
100.00% <= 412 milliseconds
100.00% <= 412 milliseconds
126871.35 requests per second

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

追蹤過(guò)程

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

$ strace -p 32088 -T -tt -o strace00.out
14:18:12.933441 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f461c0daa50) = 13772 <0.000380>
14:18:12.933884 open("/data1/6679/6679.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 60 <0.000019>
14:18:12.933948 lseek(60, 0, SEEK_END) = 11484 <0.000013>
14:18:12.933983 stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=556, ...}) = 0 <0.000016>
14:18:12.934032 fstat(60, {st_mode=S_IFREG|0644, st_size=11484, ...}) = 0 <0.000014>
14:18:12.934062 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f461c0e4000 <0.358768>
14:18:13.292883 write(60, "32088:M 21 Mar 14:18:12.933 * Ba"..., 69) = 69 <0.000032>
14:18:13.292951 close(60) = 0 <0.000014>
14:18:13.292980 munmap(0x7f461c0e4000, 4096) = 0 <0.000019>
$ strace -p 11559 -T -tt -e trace=memory -o trace00.out
14:18:12.934062 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f461c0e4000 <0.358768>
14:18:13.292980 munmap(0x7f461c0e4000, 4096) = 0 <0.000019>

可以觀察到,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é)果如下:

$ perf trace -p 11559 -o trace01.out --max-stack 15 -T
616821913.647 (358.740 ms): Redis-server_4/32088 mmap(len: 4096, prot: READ|WRITE, flags: PRIVATE|ANONYMOUS ) = 0x7f461c0e4000
__mmap64 (/usr/lib64/libc-2.17.so)
__GI__IO_file_doallocate (inlined)
__GI__IO_doallocbuf (inlined)
__GI__IO_file_overflow (inlined)
_IO_new_file_xsputn (inlined)
_IO_vfprintf_internal (inlined)
__GI_fprintf (inlined)
serverLogRaw (/usr/local/Redis/Redis-server)
serverLog (/usr/local/Redis/Redis-server)
rdbSaveBackground (/usr/local/Redis/Redis-server)
bgsaveCommand (/usr/local/Redis/Redis-server)
call (/usr/local/Redis/Redis-server)
processCommand (/usr/local/Redis/Redis-server)
processInputBuffer (/usr/local/Redis/Redis-server)
aeProcessEvents (/usr/local/Redis/Redis-server)


616822272.562 ( 0.010 ms): Redis-server_4/32088 munmap(addr: 0x7f461c0e4000, len: 4096 ) = 0
__munmap (inlined)
__GI__IO_setb (inlined)
_IO_new_file_close_it (inlined)
_IO_new_fclose (inlined)
serverLogRaw (/usr/local/Redis/Redis-server)
serverLog (/usr/local/Redis/Redis-server)
rdbSaveBackground (/usr/local/Redis/Redis-server)
bgsaveCommand (/usr/local/Redis/Redis-server)
call (/usr/local/Redis/Redis-server)
processCommand (/usr/local/Redis/Redis-server)
processInputBuffer (/usr/local/Redis/Redis-server)
aeProcessEvents (/usr/local/Redis/Redis-server)
aeMain (/usr/local/Redis/Redis-server)
main (/usr/local/Redis/Redis-server)

也就可以看到,在 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

127.0.0.1:6680> info memory
# Memory
used_memory:58385641144
used_memory_human:54.38G

現(xiàn)象

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

使用 info stats 返回上次 fork 耗時(shí):latest_fork_usec:414

TP100 結(jié)果如下:

# 壓測(cè)過(guò)程中執(zhí)行 bgsave
[root@xxx Redis]# /usr/bin/Redis-benchmark -d 256 -t set -n 1000000 -a dRedis123456 -p 6680
====== SET ======
1000000 requests completed in 7.50 seconds
50 parallel clients
256 bytes payload
keep alive: 1


99.99% <= 1 milliseconds
99.99% <= 2 milliseconds
100.00% <= 2 milliseconds
133386.69 requests per second

跟蹤驗(yàn)證

再次使用 strace 和 perf 工具跟蹤驗(yàn)證

strace 跟蹤父進(jìn)程只看到 clone,并且耗時(shí)只有 378 微秒,

# strace -p 14697 -T -tt -o strace04.out
14:42:00.723224 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fa5340d0a50) = 15470 <0.000378>

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

# perf trace -p 14697 -o trace04.out --max-stack 15 -T
618249694.830 ( 0.423 ms): Redis-server/14697 ... [continued]: clone()) = 15470 (Redis-server)
__GI___fork (inlined)
rdbSaveBackground (/usr/local/Redis/Redis-server)
bgsaveCommand (/usr/local/Redis/Redis-server)
call (/usr/local/Redis/Redis-server)
processCommand (/usr/local/Redis/Redis-server)
processInputBuffer (/usr/local/Redis/Redis-server)
aeProcessEvents (/usr/local/Redis/Redis-server)
aeMain (/usr/local/Redis/Redis-server)
main (/usr/local/Redis/Redis-server)

由于我們的優(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)程 strace 信息
14:47:40.878387 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa5340da000 <0.000008>
14:47:40.878415 write(6, "15931:C 21 Mar 14:47:40.878 * Ba"..., 69) = 69 <0.000015>
14:47:40.878447 close(6) = 0 <0.000006>
14:47:40.878467 munmap(0x7fa5340da000, 4096) = 0 <0.000010>
14:47:40.878494 open("temp-15931.rdb", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6 <0.000020>
14:47:40.878563 fstat(6, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0 <0.000006>
14:47:40.878584 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa5340da000 <0.000006>

在子進(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ù)的影響。

責(zé)任編輯:武曉燕 來(lái)源: 得物技術(shù)
相關(guān)推薦

2021-09-02 10:15:50

計(jì)算平臺(tái)MaxCompute 阿里云

2024-09-19 19:08:46

2020-06-28 10:16:53

PyTorchTensorFlow機(jī)器學(xué)習(xí)

2022-05-17 10:37:36

阿里云數(shù)據(jù)庫(kù)PolarDB-X

2022-07-12 10:37:08

阿里巴巴達(dá)摩院機(jī)器學(xué)習(xí)

2021-10-13 17:53:14

AI 數(shù)據(jù)人工智能

2020-05-06 09:11:50

DevOps

2019-07-03 09:01:29

博士生頂會(huì)計(jì)算機(jī)

2024-07-18 21:21:29

2025-06-04 09:03:00

2025-07-01 13:52:19

2022-12-19 14:39:29

機(jī)器人論文

2023-10-09 18:35:37

得物Redis架構(gòu)

2024-05-10 11:35:22

Redis延時(shí)隊(duì)列數(shù)據(jù)庫(kù)

2015-03-27 22:23:28

FreeSyncLiquidVR

2017-05-25 11:03:56

互聯(lián)網(wǎng)

2024-05-16 16:17:00

騰訊云數(shù)據(jù)庫(kù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)