Linux內存管理優化:面向低延遲/高吞吐量數據庫GraphDB
譯文【2013年10月11日 51CTO外電頭條】本文將 LinkedIn 工程師 Apurva Mehta 在 Blog 上分享的《面向低延遲/高吞吐量數據庫(GraphDB)的 Linux 內存管理優化》做了簡單的翻譯整理,希望對大家有所幫助。
簡介
GraphDB在LinkedIn的實時分布式社交圖譜服務當中充當著存儲層的角色。我們的服務旨在處理簡單查詢(例如來自LinkedIn成員的一級與二級網絡請求)與復雜查詢(例如成員之間的距離以及成員之間的關聯路徑等圖譜結果)。我們支持多種節點及邊界類型,而且能夠直接處理所有正處于執行當中的查詢。感興趣的朋友不妨點擊此處的博文,對應用程序使用我們社交圖譜的方式進行初步了解。
LinkedIn上的每一個頁面視圖都會產生多條指向GraphDB的查詢請求。這意味著GraphDB每秒鐘都要處理成千上萬條查詢請求,而且99%的查詢都能在微秒級別的延遲之內得到響應(通常延遲為十幾微秒)。有鑒于此,即使GraphDB的響應延遲提高到僅僅5毫秒,LinkedIn的全局訪問效果也將受到嚴重影響。
在2013年的大部分時段,我們已經發現GraphDB會在使用高峰當中偶爾出現間歇性響應延遲。我們深入調查了這些高峰時段,并努力了解Linux內核如何管理NUMA(即非統一內存訪問)系統上的虛擬內存。概括來說,針對NUMA的一部分Linux優化存在嚴重的負面作用,會因此對延遲產生直接性不利影響。我們認為此次研究的成果足以幫助任何一套運行在Linux系統環境下、對延遲要求較高的在線數據庫系統獲得性能改進。經過我們的優化調整,問題出現幾率(例如響應緩慢或者查詢超時的出現比例)已經下降到原先的四分之一。
在文章的***部分,我們將共同了解相關背景資料,包括:GraphDB在數據管理方面的流程大綱、性能問題的具體表現以及Linux虛擬內存管理(簡稱VMM)子系統的運作方式。在文章的第二部分,我們將詳細探討解決辦法、指導意見以及結論匯總,旨在通過實驗找到問題出現的根源。***,我們將歸納通過此次實例所獲得的經驗。
本文適合對操作系統運行機制具備一定了解的朋友。
背景資料
1) GraphDB如何管理數據
GraphDB在本質上是一種內存內數據庫。在讀取方面,我們將所有數據文件映射到虛擬內存頁面當中,并始終將其保留在內存中的活動集之下。我們的讀取活動具備很強的隨機性,指向目標遍布整個數據集,且99%的請求都要求將延遲控制在微秒級別。一臺典型的GraphDB主機能夠擁有48GB物理內存,常用內存量為20GB:其中15GB用于處理堆外虛擬內存頁面映射數據,5GB用于JVM堆。
而在寫入方面,我們擁有一套日志-結構化存儲系統。我們將全部數據劃分為以10MB為單位的純追加部分。目前,每一臺GraphDB主機大約擁有1500個活動部分,其中只有25個能夠隨時接受寫入操作,其它1475個則處于只讀狀態。
由于數據采用日志結構化存儲方式,我們需要定期對其進行壓縮。此外,由于我們所采取的壓縮計劃比較積極,因此每天每臺主機上約有九百個數據片段會被遺棄。換言之,每天每臺主機上約有容量達9GB的數據文件徹底消失,但這還僅僅是LinkedIn全局數據增量中的一小部分。概括來講,每臺48GB主機在運行五天之后頁面緩存就會被垃圾堆滿。
2) 問題癥狀
我們所遇到的性能問題主要表現為在使用高峰期GraphDB出現響應延遲。在高峰期出現的同時,我們往往會面臨數量龐大的直接頁面掃描以及內存執行效率低下等困擾,具體情況如sar所示。在個別情況下,sar -B 下"每秒pqsand(pqscand/s)"列的輸出效率將拖慢至每秒100萬到500萬頁面掃描,虛擬內存效率也會降至0%--這些癥狀往往會持續數小時。
在性能表現急劇下滑的過程中,系統的內存壓力卻并不明顯:這是因為我們可以通過/proc/meminfo的記錄看到大量無效緩存頁面。此外,并不是pqscand/s中的所有高峰時段都會引發GraphDB的延遲問題。
最讓我們感到困惑的兩個問題是:
1、如果系統中不存在明顯的內存壓力,為什么系統內核會對頁面進行掃描?
2、即使內核開始對頁面進行掃描,為什么我們的響應延遲會急劇升高?只有寫入線程需要占用新的內存分配,而且寫入與讀取線程池是彼此獨立的。因此,為什么實際結果是雙方會相互產生影響?
正是這些問題的答案促使我們對NUMA系統的Linux優化方案做出調整。需要強調的是,我們最終將注意力集中在了Linux的"區回收(zone reclaim)"功能方面。如果大家對于NUMA、Linux以及區回收不太了解,別擔心,我們會在下一部分內容中做出詳細講解。在這些資料的輔助下,大家應該可以順利理解文章其它部分的論證過程。
3) 關于Linux、NUMA以及區回收的那些事兒
要想深入理解問題產生的根源,我們首先需要明確Linux系統如何處理NUMA架構。我為大家甄選了一部分優秀的講解資源,希望能幫助各位快速掌握相關的背景知識:
- Jeff Frost:《Linux中的PostgreSQL、NUMA以及區回收模式》 。 如果大家時間有限,優先推薦各位閱讀這篇文章。
- Christoph Lameter:《非統一內存訪問概述》。大家需要著重閱讀其中關于Linux如何從NUMA區中回收內存的部分。
- Jeremy Cole:《MySQL“交換錯亂”問題以及NUMA架構的影響》。
同樣重要的是,大家需要理解Linux從頁面緩存中使用回收頁面的具體機制。
簡而言之,Linux為每個NUMA區保留著一組三個頁面列表:活動列表、非活動列表以及空閑列表。新頁面進行分配時會被從空閑列表轉移到活動列表。而LRU算法則負責將頁面從活動列表轉移到非活動列表,而后再由非活動列表轉移至空閑列表。下面我為大家推薦一份學習Linux頁面緩存管理知識的***資料:
- Mel Gorman: 《了解Linux虛擬內存管理器》,第十三章:頁面回收。
我們首先認真閱讀了上述資料,而后嘗試關閉掉生產主機上的區回收模式。關閉之后,我們的性能表現立刻獲得顯著提升。有鑒于此,我們決定在本文中詳細描述區回收的運作機制以及對性能造成影響的原因。
本文的其它內容深入探討了Linux區回收的相關內容,如果大家對區回收還不太熟悉,請首先閱讀前文推薦的Jeff Frost的相關論述。
重現并理解Linux的區回收活動
1) 設置實驗環境
為了理解區回收的觸發原理以及區回收如何影響性能表現,我們編寫了一款程序,用于模擬GraphDB的讀取與寫入活動。我們以二十四小時為期限運行該程序,在前面十七個小時內、我們開啟了區回收模式。而在***七個小時中,我們關閉了區回收模式。該程序在整個二十四小時當中不間斷地運行,環境中的惟一變化就是在第十七小時通過向/proc/sys/vm/zone_reclaim_mode中寫入"0"來禁用區回收。
下面我們來解讀該程序的運行內容:
1、它將2500個10MB數據文件映射至頁面緩存當中,全部讀取一遍,而后取消映射,這樣Linux頁面緩存當中就充斥著垃圾數據。如此一來,系統的運行狀態類似于GraphDB主機在正常運行數天后的情況。
2、一組讀取線程會將另一組2500個10MB文件映射至頁面緩存當中,再隨機讀取其中的一部分內容。這2500個文件構成了活動集合,用于模擬GraphDB在日常使用中的讀取狀態。
3、一組寫入進程不斷創建10MB文件。一旦某個文件創建完成,寫入線程就會從活動集合中隨機挑選一個文件、取消其映射并用剛剛創建的新文件加以取代。這一過程旨在模擬GraphDB在日常使用中的寫入活動。
4、***,如果讀取線程完成讀取所消耗的時間超過100毫秒,則自動輸出該次訪問流程的usr、sys以及elapsed time。這使我們得以成功追蹤到讀取性能的變化軌跡。
我們用于運行該程序的主機擁有48GB物理內存。我們的工作組大約占用了其中的25GB,除此之外系統中沒有運行其它任何任務。通過這種方式,我們保證主機不會遇到任何形式的內存壓力。
大家可以點擊此處在Github上查看我們的模擬流程。
2) 了解區回收機制如何被觸發
當某個進程針對頁面發出請求時,系統內核會首先檢查***NUMA區是否擁有足夠的空余內存以及是否存在1%以上可以回收的頁面。這一百分比可以調節,并由vm.min_unmapped_ratio sysctl來決定。可回收頁面屬于由文件支持的頁面(即與頁面緩存存在映射關系的文件所產生的頁面),但其目前并未被映射到任何進程當中。在/proc/meminfo中,我們可以很清楚地看到,所謂"可回收頁面(reclaimable pages)"就是那些"活動(文件)-非活動(文件)-被映射"(Active(file)+Inactive(file)-Mapped)的內容。
那么系統內核如何判斷多少空閑內存才夠用呢?內存會使用區"水平標記(watermarks)",通過/proc/sys/vm/min_free_kbytes 中的值來進行判斷。它們同時也會檢查/proc/sys/vm/lowmem_reserve_ratio 中的值。特定主機內經過計算的值會被保存在/proc/zoneinfo 當中,并搭配如下所示的"低/中/高(low/min/high)"標簽:
- Node 1, zone Normal
- pages free 17353
- min 11284
- low 14105
- high 16926
- scanned 0
- spanned 6291456
- present 6205440
內存會在區內空閑頁面數量低于水平標記時執行頁面回收。而當空閑頁面的數量高于"低"水平標記后,頁面回收操作就會中止。此外,這一計算過程針對的是各獨立區:即使其它區仍擁有足夠的空閑內存,只要當前區被觸發,回收機制就會付諸實施。
下面我們通過圖表來展示實驗過程中的活動表現。其中值得注意的包括以下幾點:
- 黑色線條代表區中的頁面掃描,并以右側的y軸為基準生成圖形。
- 紅色線條代表區中的空閑頁面數量。
- 綠色線條代表的是"低"水平標記。
通過實驗,我們觀察到了與生產主機類似的表現。在所有情況下,頁面掃描情況都與空閑頁面情況保持吻合--二者的數值恰好相反。換句話來說,Linux會在空閑頁面數量低于"低"水平標記時觸發區回收機制。
3) 系統區回收機制的特性
理解了區回收模式的觸發原理,下面我們將關注重點放在其它方面--區回收模式如何影響性能表現。為了實現這一目標,我們在實驗過程中每秒一次收集來自下列源的信息:
- /proc/zoneinfo
- /proc/vmstat
- /proc/meminfo
- numactl -H
在根據這些文件中的數據繪制圖表并總結活動模式后,我們發現了一些非常有趣的特性--這些特性正是解答區回收機制對讀取性能產生負面影響的關鍵所在。
在前期觀察中,區回收機制處于啟用狀態,這時Linux執行的大多是直接回收(即回收任務在應用程序線程內直接執行,并被計作直接頁面掃描)。一旦區回收模式被關閉,直接回收活動立刻停止,但由kswapd執行的回收數量卻開始增加。這就解釋了我們為何會在sar中觀察到每秒pqscand如此之高:
其次,即使我們的讀取與寫入操作并未發生變化,Linux活動與非活動列表內的頁面數量也在區回收模式關閉后發生了顯著變化。需要強調的是,在區回收機制的開啟時,Linux會在活動列表中保留總大小約20GB的頁面信息。而在區回收機制關閉后,Linux在活動列表內的信息保留量增加到約25GB,這正是我們整個工作集的大小:
***,我們觀察到在區回收模式被關閉后,頁面內活動出現了顯著差別。值得關注的一點在于,雖然從頁面緩存到磁盤駐留的速率保持不變,但區回收被關閉后從磁盤駐留到頁面內的速率顯著降低。主要故障率的變動情況與頁面內傳輸速率完全一致。下面的圖表證明了這一結論:
***,我們發現程序中的高占用內存訪問活動的數量在區回收模式被關閉后大幅減少。以下圖表顯示了內存訪問延遲(單位為毫秒)以及響應時間在系統及用戶CPU層面的具體消耗。可以看到,該程序的大部分運行時間被消耗在了I/O等待方面,而且偶爾還會在系統CPU當中遭遇阻塞。
4) 區回收模式如何影響讀取性能
基于以上統計結果,看起來由區回收所觸發的直接回收途徑似乎在將頁面從活動列表中移除并轉移到非活動列表方面表現得太過激進。特別是在區回收機制啟用時,活動頁面似乎會被直接清盤并被塞進非活動列表,而后再被移出到空閑列表當中。有鑒于此,讀取活動會遭遇極高的主要故障機率,性能表現也變得一落千丈。
導致問題進一步加劇的則是shrink_inactive_list函數,作為直接回收路徑的組成部分、它似乎在區中采用了一種全局自旋鎖,從而阻止其它線程在回收過程中對區產生修改。正因為如此,我們才會在高峰時段發現系統CPU在讀取線程中出現鎖定,這很可能是因為多個線程之間存在沖突。
NUMA內存平衡機制同樣會觸發直接頁面掃描
我們剛剛了解了區回收模式如何觸發直接頁面掃描,也證實了這類將頁面數據擠出活動列表的粗暴掃描正是讀取性能下滑的罪魁禍首。除了區回收之外,我們還發現一項名為Transparent HugePages (簡稱THP)的紅帽Linux功能在對NUMA區進行內存"平衡調整"時同樣會觸發直接頁面掃描。
在THP功能的推動下,系統會以透明化方式為匿名(即非文件支持)內存分配2MB"大型頁面"。這種做法能夠提高TLB中的命中率并降低系統中頁面列表的大小。紅帽方面表示,THP在特定工作負載中能夠帶來高達10%的性能提升。
另外,由于這項功能以透明化方式運作,因此它還利用一部分代碼將大型頁面分割成多個常規頁面(在/proc/vmsat中被統計為thp_split),或者將多個常規頁面匯聚成巨大頁面(被統計為thp_collapse)。
我們已經看到,即使區中不存在內存壓力,thp_split仍然會導致很高的直接頁面掃描比率。我們還發現Linux系統會將我們的5GB Java堆分割成巨大頁面,從而將其在不同NUMA區之間進行移動。大家請看如下圖表:總大小約5GB、來自兩個NUMA區的數據被分割成巨大頁面,其中一部分被從Node 1移動到了Node 0。這類活動必然會帶來很高的直接頁面掃描比率。
我們無法在自己的實驗環境下重現這一狀況,而且這似乎也不能算是導致直接頁面掃描的常見原因。不過我們手中有大量數據可以證明,Transparent HugePages功能與NUMA系統的協作效果并不理想,因此我們決定在自己的RHEL設備上運行下列命令來禁用該功能。
- echo never > /sys/kernel/mm/transparent_hugepage/enabled
經驗教訓
1) Linux的NUMA優化對于典型數據庫負載并無意義
數據庫的主要性能提升源自在內存中對大量數據進行緩存處理,而NUMA優化并不能做到這一點。需要強調的是,利用內存緩存規避磁盤讀寫所節約的時間要遠遠超過將內存接入多插槽系統中特定插槽所換來的延遲改進。
Linux的NUMA優化機制可以通過以下幾種方式禁用,從而提高性能表現:
- 關閉區回收模式:向 /etc/sysctl.conf 中添加vm.zone_reclaim_mode = 0并運行sysctl -p以載入新設定。
- 為自己的應用程序啟用NUMA交叉存取功能: 在運行應用時加入numactl --interleave=all 命令。
以上兩種設置已經成為我們全部生產系統中的默認狀態。
2) 不要對Linux設置掉以輕心:親手管理頁面緩存中的垃圾內容
由于GraphDB的日志結構化存儲系統無法重新使用其數據片段,因此承受著時間的推移我們在Linux頁面緩存中產生了大量垃圾內容。事實證明,Linux在正確清理這類垃圾內容時表現得相當糟糕:它通常會不由分說地把一切數據扔進廢紙簍,這種過分激進的處理方式令我們的讀取性能遭遇極高的主要故障比率。直接回收與kswapd都起到了助紂為虐的作用,但前者的負面作用更為明顯。
我們已經為自己的存儲系統添加了片段池,這樣我們就能夠重復使用這些片段。通過這種方式,我們降低了需要創建的文件數量,同時減小了對Linux頁面緩存造成的處理壓力。通過初步測試,我們發現片段池機制帶來了令人鼓舞的出色效果。
寫在***的話
自從對區回收機制產生懷疑,我們就立即著手在生產系統中關閉這一模式。事實證明,我們的調整帶來了顯著成效。在過去的四個月中,我們的生產主機一直處于中位延遲狀態下。不必掌聲鼓勵、也無需鮮花簇擁,關閉區回收機制這樣一個小小的決定讓LinkedIn迎來了顯而易見的性能提升--這正是我們技術人員的***樂趣!






































