前言
在工作生活中,我們時常會遇到一些性能問題:比如手機用久了,在滑動窗口或點擊 APP 時會出現(xiàn)頁面反應(yīng)慢、卡頓等情況;比如運行在某臺服務(wù)器上進(jìn)程的某些性能指標(biāo)(影響用戶體驗的 PCT99 指標(biāo)等)不達(dá)預(yù)期,產(chǎn)生告警等;造成性能問題的原因多種多樣,可能是網(wǎng)絡(luò)延遲高、磁盤 IO 慢、調(diào)度延遲高、內(nèi)存回收等,這些最終都可能影響到用戶態(tài)進(jìn)程,進(jìn)而被用戶感知。
在 Linux 服務(wù)器場景中,內(nèi)存是影響性能的主要因素之一,本文從內(nèi)存管理的角度,總結(jié)歸納了一些常見的影響因素(比如內(nèi)存回收、Page Fault 增多、跨 NUMA 內(nèi)存訪問等),并介紹其對應(yīng)的調(diào)優(yōu)方法。
內(nèi)存回收
操作系統(tǒng)總是會盡可能利用速度更快的存儲介質(zhì)來緩存速度更慢的存儲介質(zhì)中的內(nèi)容,這樣就可以顯著的提高用戶訪問速度。比如,我們的文件一般都存儲在磁盤上,磁盤對于程序運行的內(nèi)存來說速度很慢,因此操作系統(tǒng)在讀寫文件時,都會將磁盤中的文件內(nèi)容緩存到內(nèi)存上(也叫做 page cache),這樣下次再讀取到相同內(nèi)容時就可以直接從內(nèi)存中讀取,不需要再去訪問速度更慢的磁盤,從而大大提高文件的讀寫效率。
上述情況需要在內(nèi)存資源充足的前提條件下,然而在內(nèi)存資源緊缺時,操作系統(tǒng)自身難保,會選擇盡可能回收這些緩存的內(nèi)存,將其用到更重要的任務(wù)中去。這時候,如果用戶再去訪問這些文件,就需要訪問磁盤,如果此時磁盤也很繁忙,那么用戶就會感受到明顯的卡頓,也就是性能變差了。
在 Linux 系統(tǒng)中,內(nèi)存回收分為兩個層面:整機和 memory cgroup。
在整機層面
設(shè)置了三條水線:min、low、high;當(dāng)系統(tǒng) free 內(nèi)存降到 low 水線以下時,系統(tǒng)會喚醒kswapd 線程進(jìn)行異步內(nèi)存回收,一直回收到 high 水線為止,這種情況不會阻塞正在進(jìn)行內(nèi)存分配的進(jìn)程;但如果 free 內(nèi)存降到了 min 水線以下,就需要阻塞內(nèi)存分配進(jìn)程進(jìn)行回收,不然就有 OOM(out of memory)的風(fēng)險,這種情況下被阻塞進(jìn)程的內(nèi)存分配延遲就會提高,從而感受到卡頓。
圖 1. per-zone watermark
這些水線可以通過內(nèi)核提供的 /proc/sys/vm/watermark_scale_factor 接口來進(jìn)行調(diào)整,該接口合法取值的范圍為 [0, 1000],默認(rèn)為 10,當(dāng)該值設(shè)置為 1000 時,意味著 low 與 min 水線,以及 high 與 low 水線間的差值都為總內(nèi)存的 10% (1000/10000)。
針對 page cache 型的業(yè)務(wù)場景,我們可以通過該接口抬高 low 水線,從而更早的喚醒 kswapd 來進(jìn)行異步的內(nèi)存回收,減少 free 內(nèi)存降到 min 水線以下的概率,從而避免阻塞到業(yè)務(wù)進(jìn)程,以保證影響業(yè)務(wù)的性能指標(biāo)。
在 memory cgroup 層面
目前內(nèi)核沒有設(shè)置水線的概念,當(dāng)內(nèi)存使用達(dá)到 memory cgroup 的內(nèi)存限制后,會阻塞當(dāng)前進(jìn)程進(jìn)行內(nèi)存回收。不過內(nèi)核在 v5.19內(nèi)核 中為 memory cgroup提供了 memory.reclaim 接口,用戶可以向該接口寫入想要回收的內(nèi)存大小,來提早觸發(fā) memory cgroup 進(jìn)行內(nèi)存回收,以避免阻塞 memory cgroup 中的進(jìn)程。
Huge Page
內(nèi)存作為寶貴的系統(tǒng)資源,一般都采用延遲分配的方式,應(yīng)用程序第一次向分配的內(nèi)存寫入數(shù)據(jù)的時候會觸發(fā) Page Fault,此時才會真正的分配物理頁,并將物理頁幀填入頁表,從而與虛擬地址建立映射。
圖 2. Page Table
此后,每次 CPU 訪問內(nèi)存,都需要通過 MMU 遍歷頁表將虛擬地址轉(zhuǎn)換成物理地址。為了加速這一過程,一般都會使用 TLB(Translation-Lookaside Buffer)來緩存虛擬地址到物理地址的映射關(guān)系,只有 TLB cache miss 的時候,才會遍歷頁表進(jìn)行查找。
頁的默認(rèn)大小一般為 4K,隨著應(yīng)用程序越來越龐大,使用的內(nèi)存越來越多,內(nèi)存的分配與地址翻譯對性能的影響越加明顯。試想,每次訪問新的 4K 頁面都會觸發(fā) Page Fault,2M 的頁面就需要觸發(fā) 512 次才能完成分配。
另外 TLB cache 的大小有限,過多的映射關(guān)系勢必會產(chǎn)生 cacheline 的沖刷,被沖刷的虛擬地址下次訪問時又會產(chǎn)生 TLB miss,又需要遍歷頁表才能獲取物理地址。
對此,Linux 內(nèi)核提供了大頁機制。上圖的 4 級頁表中,每個 PTE entry 映射的物理頁就是 4K,如果采用 PMD entry 直接映射物理頁,則一次 Page Fault 可以直接分配并映射 2M 的大頁,并且只需要一個 TLB entry 即可存儲這 2M 內(nèi)存的映射關(guān)系,這樣可以大幅提升內(nèi)存分配與地址翻譯的速度。
因此,一般推薦占用大內(nèi)存應(yīng)用程序使用大頁機制分配內(nèi)存。當(dāng)然大頁也會有弊端:比如初始化耗時高,進(jìn)程內(nèi)存占用可能變高等。
可以使用 perf 工具對比進(jìn)程使用大頁前后的 PageFault 次數(shù)的變化:
perf stat -e page-faults -p-- sleep 5
目前內(nèi)核提供了兩種大頁機制,一種是需要提前預(yù)留的靜態(tài)大頁形式,另一種是透明大頁(THP, Transparent Huge Page) 形式。
1. 靜態(tài)大頁
首先來看靜態(tài)大頁,也叫做 HugeTLB。靜態(tài)大頁可以設(shè)置 cmdline 參數(shù)在系統(tǒng)啟動階段預(yù)留,比如指定大頁 size 為 2M,一共預(yù)留 512 個這樣的大頁:
hugepagesz=2M hugepages=512
還可以在系統(tǒng)運行時動態(tài)預(yù)留,但該方式可能因為系統(tǒng)中沒有足夠的連續(xù)內(nèi)存而預(yù)留失敗。
預(yù)留默認(rèn) size(可以通過 cmdline 參數(shù) default_hugepagesz=指定size)的大頁:
echo 20 > /proc/sys/vm/nr_hugepages
預(yù)留特定 size 的大頁:
echo 5 > /sys/kernel/mm/hugepages/hugepages-*/nr_hugepages
預(yù)留特定 node 上的大頁:
echo 5 > /sys/devices/system/node/node*/hugepages/hugepages-*/nr_hugepages
當(dāng)預(yù)留的大頁個數(shù)小于已存在的個數(shù),則會釋放多余大頁(前提是未被使用)。
編程中可以使用 mmap(MAP_HUGETLB) 申請內(nèi)存。詳細(xì)使用可以參考內(nèi)核文檔 :https://www.kernel.org/doc/Documentation/admin-guide/mm/hugetlbpage.rst
這種大頁的優(yōu)點是一旦預(yù)留成功,就可以滿足進(jìn)程的分配請求,還避免該部分內(nèi)存被回收;缺點是: (1) 需要用戶顯式地指定預(yù)留的大小和數(shù)量。 (2) 需要應(yīng)用程序適配,比如: - mmap、shmget 時指定 MAP_HUGETLB; - 掛載 hugetlbfs,然后 open 并 mmap
當(dāng)然也可以使用開源 libhugetlbfs.so,這樣無需修改應(yīng)用程序
預(yù)留太多大頁內(nèi)存后,free 內(nèi)存大幅減少,容易觸發(fā)系統(tǒng)內(nèi)存回收甚至 OOM
緊急情況下可以手動減少 nr_hugepages,將未使用的大頁釋放回系統(tǒng);也可以使用 v5.7 引入的HugeTLB + CMA 方式,細(xì)節(jié)讀者可以自行查閱。
2. 透明大頁
再來看透明大頁,在 THP always 模式下,會在 Page Fault 過程中,為符合要求的 vma 盡量分配大頁進(jìn)行映射;如果此時分配大頁失敗,比如整機物理內(nèi)存碎片化嚴(yán)重,無法分配出連續(xù)的大頁內(nèi)存,那么就會 fallback 到普通的 4K 進(jìn)行映射,但會記錄下該進(jìn)程的地址空間 mm_struct;然后 THP 會在后臺啟動khugepaged 線程,定期掃描這些記錄的 mm_struct,并進(jìn)行合頁操作。因為此時可能已經(jīng)能分配出大頁內(nèi)存了,那么就可以將此前 fallback 的 4K 小頁映射轉(zhuǎn)換為大頁映射,以提高程序性能。整個過程完全不需要用戶進(jìn)程參與,對用戶進(jìn)程是透明的,因此稱為透明大頁。
雖然透明大頁使用起來非常方便、智能,但也有一定的代價: (1)進(jìn)程內(nèi)存占用可能遠(yuǎn)大所需:因為每次Page Fault 都盡量分配大頁,即使此時應(yīng)用程序只讀寫幾KB (2)可能造成性能抖動:
在第 1 種進(jìn)程內(nèi)存占用可能遠(yuǎn)大所需的情況下,可能造成系統(tǒng) free 內(nèi)存更少,更容易觸發(fā)內(nèi)存回收;系統(tǒng)內(nèi)存也更容易碎片化。
khugepaged 線程合頁時,容易觸發(fā)頁面規(guī)整甚至內(nèi)存回收,該過程費時費力,容易造成 sys cpu 上升。
mmap lock 本身是目前內(nèi)核的一個性能瓶頸,應(yīng)當(dāng)盡量避免 write lock 的持有,但 THP 合頁等操作都會持有寫鎖,且耗時較長(數(shù)據(jù)拷貝等),容易激化 mmap lock 鎖競爭,影響性能。
因此 THP 還支持 madvise 模式,該模式需要應(yīng)用程序指定使用大頁的地址范圍,內(nèi)核只對指定的地址范圍做 THP 相關(guān)的操作。這樣可以更加針對性、更加細(xì)致地優(yōu)化特定應(yīng)用程序的性能,又不至于造成反向的負(fù)面影響。
可以通過 cmdline 參數(shù)和 sysfs 接口設(shè)置 THP 的模式:
cmdline 參數(shù):
transparent_hugepage=madvise
sysfs 接口:
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
詳細(xì)使用可以參考內(nèi)核文檔 :
https://www.kernel.org/doc/Documentation/admin-guide/mm/transhuge.rst
mmap_lock 鎖
上一小節(jié)有提到 mmap_lock 鎖,該鎖是內(nèi)存管理中的一把知名的大鎖,保護了諸如mm_struct 結(jié)構(gòu)體成員、 vm_area_struct 結(jié)構(gòu)體成員、頁表釋放等很多變量與操作。
mmap_lock 的實現(xiàn)是讀寫信號量,當(dāng)寫鎖被持有時,所有的其他讀鎖與寫鎖路徑都會被阻塞。Linux 內(nèi)核已經(jīng)盡可能減少了寫鎖的持有場景以及時間,但不少場景還是不可避免的需要持有寫鎖,比如 mmap 以及 munmap 路徑、mremap 路徑和 THP 轉(zhuǎn)換大頁映射路徑等場景。
應(yīng)用程序應(yīng)該避免頻繁的調(diào)用會持有 mmap_lock 寫鎖的系統(tǒng)調(diào)用 (syscall),比如有時可以使用 madvise(MADV_DONTNEED)釋放物理內(nèi)存,該參數(shù)下,madvise 相比 munmap 只持有 mmap_lock 的讀鎖,并且只釋放物理內(nèi)存,不會釋放 VMA 區(qū)域,因此可以再次訪問對應(yīng)的虛擬地址范圍,而不需要重新調(diào)用 mmap 函數(shù)。
另外對于 MADV_DONTNEED,再次訪問還是會觸發(fā) Page Fault 分配物理內(nèi)存并填充頁表,該操作也有一定的性能損耗。如果想進(jìn)一步減少這部分損耗,可以改為 MADV_FREE 參數(shù),該參數(shù)也只會持有 mmap_lock 的讀鎖,區(qū)別在于不會立刻釋放物理內(nèi)存,會等到內(nèi)存緊張時才進(jìn)行釋放,如果在釋放之前再次被訪問則無需再次分配內(nèi)存,進(jìn)而提高內(nèi)存訪問速度。
一般 mmap_lock 鎖競爭激烈會導(dǎo)致很多 D 狀態(tài)進(jìn)程(TASK_UNINTERRUPTIBLE),這些 D 進(jìn)程都是進(jìn)程組的其他線程在等待寫鎖釋放。因此可以打印出所有 D 進(jìn)程的調(diào)用棧,看是否有大量 mmap_lock 的等待。
for i in `ps -aux | grep " D" | awk '{ print $2}'`; do echo $i; cat /proc/$i/stack; done
內(nèi)核社區(qū)專門封裝了 mmap_lock 相關(guān)函數(shù),并在其中增加了 tracepoint,這樣可以使用 bpftrace 等工具統(tǒng)計持有寫鎖的進(jìn)程、調(diào)用棧等,方便排查問題,確定優(yōu)化方向。
bpftrace -e 'tracepointmmap_lock_start_locking /args->write == true/{ @[comm, kstack] = count();}'
跨 numa 內(nèi)存訪問
在 NUMA 架構(gòu)下,CPU 訪問本地 node 內(nèi)存的速度要大于遠(yuǎn)端 node,因此應(yīng)用程序應(yīng)盡可能訪問本地 node 上的內(nèi)存。可以通過 numastat 工具查看 node 間的內(nèi)存分配情況:
觀察整機是否有很多 other_node 指標(biāo)(遠(yuǎn)端內(nèi)存訪問)上漲:
watch -n 1 numastat -s
查看單個進(jìn)程在各個node上的內(nèi)存分配情況:
numastat -p
1. 綁 node
可以通過 numactl 等工具把進(jìn)程綁定在某個 node 以及對應(yīng)的 CPU 上,這樣該進(jìn)程只會從該本地 node 上分配內(nèi)存。
但這樣做也有相應(yīng)的弊端,比如:該 node 剩余內(nèi)存不夠時,進(jìn)程也無法從其他 node 上分配內(nèi)存,只能期待內(nèi)存回收后釋放足夠的內(nèi)存,而如果進(jìn)入直接內(nèi)存回收會阻塞內(nèi)存分配,就會有一定的性能損耗。
此外,進(jìn)程組的線程數(shù)較多時,如果都綁定在一個 node 的 CPU 上,可能會造成 CPU 瓶頸,該損耗可能比遠(yuǎn)端 node 內(nèi)存訪問還大,比如 ngnix 進(jìn)程與網(wǎng)卡就推薦綁定在不同的 node 上,這樣雖然網(wǎng)卡收包時分配的內(nèi)存在遠(yuǎn)端 node 上,但減少了本地 node 的 CPU 上的網(wǎng)卡中斷,反而可以獲得更好的性能提升。
2. numa balancing
內(nèi)核還提供了 numa balancing 機制,可以通過 /proc/sys/kernel/numa_balancing 文件或者 cmdline 參數(shù) numa_balancing=進(jìn)行開啟。
該機制可以動態(tài)的將進(jìn)程訪問的 page 從遠(yuǎn)端 node 遷移到本地 node 上,從而使進(jìn)程可以盡可能的訪問本地內(nèi)存。
但該機制實現(xiàn)也有相應(yīng)的代價,在 page 的遷移是通過 Page Fault 機制實現(xiàn)的,會有相應(yīng)的性能損耗;另外如果遷移時找不到合適的目標(biāo) node,可能還會把進(jìn)程遷移到正在訪問的 page 的 node 的 CPU 上,這可能還會導(dǎo)致 cpu cache miss,從而對性能造成更大的影響。
因此需要根據(jù)業(yè)務(wù)進(jìn)程的具體行為,來決定是否開啟 numa balancing 功能。
總結(jié)
性能優(yōu)化一直是大家關(guān)注的話題,其優(yōu)化方向涉及到 CPU 調(diào)度、內(nèi)存、IO等,本文重點針對內(nèi)存優(yōu)化提出了幾點思路。但是魚與熊掌不可兼得,文章提到的調(diào)優(yōu)操作都有各自的優(yōu)點和缺點,不存在一個適用于所有情況的優(yōu)化方法。針對于不同的 workload,需要分析出具體的性能瓶頸,從而采取對應(yīng)的調(diào)優(yōu)方法,不能一刀切的進(jìn)行設(shè)置。在沒有發(fā)現(xiàn)明顯性能抖動的情況下,往往可以繼續(xù)保持當(dāng)前配置。
審核編輯:劉清
-
Linux系統(tǒng)
+關(guān)注
關(guān)注
4文章
595瀏覽量
27465 -
TLB電路
+關(guān)注
關(guān)注
0文章
9瀏覽量
5268 -
LINUX內(nèi)核
+關(guān)注
關(guān)注
1文章
316瀏覽量
21698 -
MMU
+關(guān)注
關(guān)注
0文章
91瀏覽量
18331 -
numa結(jié)構(gòu)
+關(guān)注
關(guān)注
0文章
4瀏覽量
1152
原文標(biāo)題:Linux 內(nèi)核內(nèi)存性能調(diào)優(yōu)的一些筆記
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論