本文來自部落格園,作者:T-BARBARIANS,轉載請註明原文連結:https://www.cnblogs.com/t-bar/p/17359545.html 謝謝!
一、前言
近幾年開發了一些大型的應用程式,在程式效能調優或者解決一些疑難雜症問題的過程中,遇到最多的還是與記憶體相關的一些問題。例如glibc記憶體分配器ptmalloc,google的記憶體分配器tcmalloc都存在「記憶體漏失」,即記憶體不歸還作業系統的問題;ptmalloc記憶體分配效能低下的問題;隨著系統長時間執行,buffer/cache被某些應用大量使用,幾乎完整佔用系統記憶體,導致其他應用程式記憶體申請失敗等等問題。
之所以記憶體相關的問題層出不窮,關鍵還是它的地位太重要了。這次還是與記憶體相關,分享的是追蹤buffer/cache佔用的記憶體到底被誰(哪些應用程式)偷吃了!
有關buffer/cache的文章,大多提及的是如何釋放並歸還到系統的方法,但是分析buffer/cache記憶體消耗背後原因的相關文章卻鳳毛麟角。buffer/cache為什麼會增長,它到底被哪些程式使用了,我相信這也是很多同行的疑惑,因此想通過本篇文章分享一些buffer/cache記憶體消耗問題的跟蹤方法,為類似問題的優化和解決提供一些參考。
二、問題描述
如下圖1和圖2所示,buffer/cache已經佔用了46GB的記憶體,達到了整個系統記憶體的37%,這個佔比已經非常高了。buffer/cache長期佔用不釋放,同時供系統上其它程序使用的可用記憶體幾乎快沒了。
圖1
圖2
長此以往,會出現什麼問題呢?最直接的問題就是其他程序沒法玩了,比如大一點的記憶體塊就無法申請。之前分享過一次相關問題的定位,參見連結:https://www.cnblogs.com/t-bar/p/16626951.html,我在這篇文章裡也詳細介紹了buffer/cache的釋放方法,解決了當時的燃眉之急。
為啥最近又開始與buffer/cache糾纏上了呢?
「echo 1 > /proc/sys/vm/drop_caches」釋放的是所有cache,這些cache是當前系統上所有程式在執行過程中載入到記憶體的一些檔案資訊,這些資訊被當做快取用,好處是CPU下次讀取某個檔案時就會比第一次從磁碟讀取快多了。drop_caches執行時會清空所有cache,這樣會帶來一個問題:當某些程式需要讀取之前載入到cache的資訊時就需要重新從磁碟讀取,這就會產生IO等待,或者IO競爭,從而拖累程式效能。在某些平臺上,我們已經發現有高效能程式因為cache的粗暴清空產生了效能抖動。因此,我們就沒法像以前一樣迴避buffer/cache到底被誰使用的問題,並且直接粗暴釋放的策略在某些平臺上也就失效了。
根據上面的描述,我們當前面臨的問題就是:究竟是誰佔用了buffer/cache,以及弄清是誰佔用後,是否可以規避它對buffer/cache的大量使用。面對這個問題,老闆最近又上火了。
三、buffer/cache使用跟蹤
開始介紹一下調查buffer/cache佔用的跟蹤思路吧。
1、hcache
網上有一些貼文分享了hcache可以檢視哪些檔案使用了cache,那hache真的可以幫助我們對buffer/cache進行全面調查嗎?我們一起來看看。
根據前面的問題描述,當前buffer/cache已經佔用了46GB的記憶體。先使用hcache檢視一下top100的cache佔用,如下圖所示(擷取了Cached靠前的一部分)。
圖3
top100,即使top200的Size統計之和,也只有幾個GB,離46GB相差甚遠,結果說明hcache遺漏了很多cache的使用統計。
hcache還有一個能力,檢視某個程序當前使用的cache。我們看看clickhouse的cache使用,結果如下圖所示。
圖4
正在執行的clickhouse,居然只能看到程式可執行檔案本身當前的cache佔用,程式執行過程中已開啟的cache檔案卻沒統計。不過這裡有個小收穫:程式載入進記憶體後,程式的可執行檔案,依賴的庫檔案使用的記憶體都是在buffer/cache裡。
圖5
從上面的結果發現hcahce有很多缺點,只能粗略的看到一些可執行程式檔案,或者一些庫檔案使用的cache大小,沒有統計各程式執行態的cache使用,因此對cache佔用問題的排查作用非常有限。
2、top + lsof + fincore
找了很多資料,除了hcache確實沒有其他方法可以統計當前執行程式消耗的cache大小了,但是hcache本身不可靠。沒有直接的辦法,那就只有圍魏救趙了,這也是buffer/cache分佈情況不便跟蹤調查的原因。
該從哪裡入手呢?當然是top命令給方向,哪些程式cpu使用率高,且使用了一定的記憶體,那就查它。因為只有它們才有可能在不斷的使用cache,調查大方向有了。
圖6
下一步呢?buffer/cache的使用肯定跟檔案相關啊,還是那句話:linux一切皆檔案。那有沒有可以實時檢視某個程序當前已開啟的檔案方法?lsof命令可以!我們用lsof查一下clickhouse,某時刻,clickhouse開啟的檔案如下圖7圖8所示,篇幅太長,圖7只擷取了前面部分。
圖7
圖8只擷取了型別TYPE=REG(REG表示檔案型別為普通,還有DIR為目錄等等等),即擷取了clickhouse當前開啟,且正在使用的一部分型別為普通的檔案。
圖8
不斷的執行:lsof -p $(pidof clickhouse-server),發現每次檢視到的檔名都不一樣。好了,這說明clickhouse會在執行過程中不斷的大量開啟,讀寫和關閉檔案。嫌疑很重了。
下一步呢?有沒有辦法可以實時檢視當前這些檔案是不是使用了cache,以及各自使用cache的大小?還真有,fincore可以檢視某個檔案使用的cache大小,連結:https://github.com/david415/linux-ftools。輪子就是齊全啊,要啥有啥。
命令列:lsof -p $(pidof clickhouse-server) | grep 'REG' | awk '{print $9}' | xargs ./fincore --pages=false --summarize --only-cached *
截圖較大,點開看會清晰一點。
圖9
fincore統計了命令列執行時,clickhouse當前開啟的檔案使用的cache之和為1.2GB左右。到這裡,當前的探索結果與前文提到的問題:究竟是誰佔用了buffer/cache,越來越接近了。
通過top + lsof,發現了一個非常重要的線索,就是clickhouse在目錄/opt/runtime/esch/ch/store下頻繁的開啟了很多檔案,那這個目錄下面到底都是一些什麼檔案?有沒有都使用了cache呢?clickhouse是不是cache的消耗大戶呢?解決這些疑惑就產生了另外一個需求,需要一種可以統計指定目錄的cache大小的工具。這次fincore也不行了,fincore有一個致命弱點,即只能獲得某個指定檔案的cache佔用大小,不能獲取指定目錄使用的cache大小,更別指望統計巢狀目錄的cache大小。因此,是時候該請vmtouch出場了,連結:https://github.com/hoytech/vmtouch,還是這句話:輪子就是齊全啊,要啥有啥。
3、vmtouch
vmtouch可以統計指定目錄的cache佔用大小,即使是巢狀目錄。
迫不及待的直接奔主題,看看clickhouse目錄/opt/runtime/esch/ch/store下是什麼,以及使用了多少cache。擷取了該目錄下的部分檔案,內容如下圖所示。
圖10
直接統計一下/opt/runtime/esch/ch/store目錄佔用的cache規模吧,結果如下圖所示。
圖11
shit,居然吃了我42GB的記憶體啊!地主家的餘糧也不多啊---老闆哭著說。
激動之餘,我還要確認一下42GB cache的使用者是不是它!如何證明呢,還是使用「echo 1 > /proc/sys/vm/drop_caches」,看看釋放完畢之後,free可用記憶體的大小是否會增長42GB左右。
執行前的記憶體分佈情況:
圖12
圖13
執行後的記憶體分佈情況:
圖14
圖15
執行cache釋放後,free從2GB變為了45GB,擴大了43GB;buffer/cache從46GB變為了3GB,減小了43GB。從cache釋放了clickhouse的42GB+1GB其它程式佔用的cache,說明我們環境上,clickhouse就是cache的消耗大戶!老闆沸不沸騰我不知道,反正我是沸騰了。
四、clickhouse cache耗費
為什麼clickhouse對buffer/cache的消耗如此巨大?在好奇心的驅使下,又開始了新的調查。此時此景想到了一句歌詞:一波還未平息,一波又來侵襲,茫茫人海,狂風暴雨。。。
1、clickhouse cache耗費原因
從哪開始調查呢?想起了lsof命令的執行結果,如前文圖9所示的重要線索,clickhouse有大量時間都在開啟目錄:/opt/runtime/esch/ch/store/032/03216cf6-357f-477f-bc9b-5eedb07a5d07,判斷該目錄下面肯定有大量消耗cache的檔案。直接進入該目錄,繼續使用vmtouch統計,不出所料,結果如下圖所示,032目錄就吃了24GB記憶體,心好痛啊。
圖16
clickhouse的什麼機制會如此瘋狂的消耗cache呢?我們再看看目錄下有些什麼型別的檔案,擷取了部分檔案。
圖17
發現目錄下主要是很多以日期編號開頭的目錄檔案,有純數位組成的,也有帶有merge字元的目錄。隨便開啟一個5月17日當天的目錄檔案:20230517_563264_565136_5
圖18
20230517_563264_565136_5 目錄就佔用了2GB cache,驚不驚喜意不意外?而且上面的所有檔案,都完全載入到了cache中,比如在磁碟中佔用743MB的檔案cuid.bin,同樣在cache中佔用了742MB。
圖19
查閱clickhouse資料後才發現,數位編號的目錄都是clickhouse的很多分割區part,clickhouse服務會根據相關策略自動的在後臺合併這些分割區。想想,如果在每一次合併分割區時,才將上一次的某兩個分割區從磁碟進行IO讀取,那將帶來多大的效能開銷。因此,clickhouse的開發者會將上一次的分割區合併結果儲存在cache裡,下一次該分割區與其它分割區再次進行合併時,直接從記憶體裡讀取資料就好了。這就是為什麼clickhouse消耗如此巨大cache的原因。當然,clickhouse對cache的消耗與您當前環境的資料儲存規模呈正相關。
再來看一個問題,那昨天的所有分割區,載入的資料還會保留在cache裡嗎?
我找了一個昨天的分割區,可以發現昨天的分割區目錄裡的檔案是不再佔用cache的。上一天的分割區,clickhouse認為是合併完成的分割區,已經不需要再進行合併了,自然就clear了cache的佔用,開發者也是想到了的。
圖20
2、clickhouse cache耗費調整
可是當天分割區消耗的cache,以及merge過程中使用的cache就讓我沒法玩了,尤其是在clickhouse服務並未獨立部署的場景。那clickhouse自身可以支援改變這一機制嗎?帶著疑問又開始了一探究竟,完全沒法停下來。
後來在clickhouse社群找到了一個可以節省cache使用的相關問題。連結:https://github.com/ClickHouse/ClickHouse/issues/1566,有設定min_part_size_for_direct_merge,意思是超過min_part_size時,啟用direct_io。也就是此時clickhouse會通過direct_io的方式讀寫merge的原始檔和目的檔案,而不是使用cache快取,通過這種方式減少cache的使用。
圖21
我們的clickhouse版本比較高,看社群記錄,clickhouse官方將之前的min_part_size_for_direct_merge改成為min_merge_bytes_to_use_direct_io,Minimal amount of bytes to enable O_DIRECT in merge (0 - disabled)。預設超過10GB時會使用direct_io的方式進行merge。
圖22
那我將min_merge_bytes_to_use_direct_io設定足夠小,甚至是1byte,是不是就可以完全避免對cache的使用了?答案是否定的,原因是:min_merge_bytes_to_use_direct_io只是讀寫表資料時使用了direct_io,替換了常用的buffer_io。也就是說只是在資料傳輸過程不使用cache,節省的是這個環節的cache記憶體消耗。merge完成後,先通過direct_io將資料寫入到磁碟,同時會繼續使用cache快取merge完成後的資料,方便為下一次與其它分割區進行快速merge做準備。因為每次merge都是merge舊資料與新資料,因此新合成的分割區所使用的cache只會比merge前的更大。direct_io與buffer_io的區別如下圖所示。
圖23
需要注意的是,設定min_merge_bytes_to_use_direct_io還有一個副作用,當發生merger行為時會導致磁碟IO急劇拉高。因為direct_io是對磁碟進行直接操作,這種IO方式與buffer_io(使用了page cache做緩衝層)相比,帶給磁碟的衝擊更大。但是如果鈔票比較多,可以做磁碟raid,或者增加了SSD,磁碟能夠扛住direct_io衝擊的同時,還能支援前端的絲滑查詢,那就另當別論啦!
圖24
另外還設定過相關引數:max_bytes_to_merge_at_max_space_in_pool,用處不大,就不繼續介紹了,讀者可以自行驗證。只能說clickhouse當前沒有提供待merge分割區檔案所佔用cache的清理機制。
五、cache清理
一頓操作猛如虎,定睛一看原地杵。clickhouse自身無法限制cahce消耗;「echo 1 > /proc/sys/vm/drop_caches」又太粗暴,會清理掉其它程序載入到cache中的內容。只想搞掉clickhouse佔用的大量cache,該怎麼辦?
有時候你不得不相信車到山前必有路,船到橋頭自然直。再次請出vmtouch!
vmtouch的help中有一個「-e」的選項,即「evict pages from memory」,顧名思義將page cache從記憶體中驅逐出去。既然vmtouch可以統計指定檔案或目錄佔用的cache,那自然就可以實現對指定檔案或目錄的cahce清理!
圖25
先來看看執行效果。執行前有記憶體分佈,以及某個目錄下cache的使用情況:
圖26
圖27
執行:vmtouch -e ./* 後,記憶體分佈如下圖28所示。驚喜嗎?剛好減少了某個目錄下佔用的30GB cache,同時free記憶體增加了30GB,實現了對指定目錄cache消耗的定點清理!
圖28
很好奇vmtouch實現指定目錄記憶體清理的方法,去看了看原始碼,簡單貼兩張圖就大致明白了。
1、傳遞指定path;
2、如果該path下無目錄,則通過vmtouch_file函數執行cache的釋放操作;如果該目錄下存在其他檔案(包括子目錄),則遍歷該目錄下的所有檔案,並通過vmtouch_crawl實現遞迴呼叫,回到第1步。
圖29
那vmtouch_file函數如何實現cache的釋放操作呢?答案就是通過系統呼叫:posix_fadvise,並使用POSIX_FADV_DONTNEED宏。定義如下:
1 int posix_fadvise(int fd, off_t offset, off_t len, int advice); 2 3 advice: 4 POSIX_FADV_DONTNEED,指定的資料在未來的一段時間內不會被存取,丟棄page cache中的資料
當advice為POSIX_FADV_DONTNEED時,核心會先將髒頁刷盤,再清除相關page cache,從而達到cache釋放的目的!
圖30
但是查閱資料,發現 posix_fadvise 實現髒頁刷盤使用的引數是:WB_SYNC_NONE,即posix_fadvise 不會同步等待其它程序發起的髒頁刷盤行為,這可能會帶來一個問題:不能完全釋放指定路徑下的cache,因此可以在執行cache釋放前先使用 fsync,將所有的髒頁進行回寫,完成後再呼叫 posix_fadvise,實現指定path下所有cache的釋放。如果不關心臟頁使用的cache是否可以被全被釋放,那直接使用posix_fadvise 就好了。
寫到這裡,可能有些讀者會產生一個問題,清理cache就真的很好嗎?答案是肯定的。為什麼這麼說呢,我想可以概括為以下兩點:
一是有些服務使用完cache後,這部分cache再也不會被存取,這一類cache消耗當然是需要幹掉的;
二是類似我們環境clickhouse這一類應用,它們會再次使用之前使用過的cache。這種情形確實不好辦,但是如果不辦它,最後就是大家都別玩了,魚和熊掌不可兼得。因此,對這一類應用,定期清理cache是必要的,通過一個折中的辦法讓大家共存下來。對依賴cache這一類應用,cache清理後就只有辛苦下磁碟了(人在家中坐,鍋從天上來)。
六、結語
摸索cache的過程曲折且漫長,還好最後找到了一個大家都可以接受的辦法。文章篇幅比較長,簡單總結一下全文吧,概括起來可歸納為以下四點:
1、驗證了 hcache 無法統計cache的全面消耗情況,推薦了一種通過 lsof+fincore 探測程序中活躍目錄以及檔案的方法,並以此作為cache消耗的關鍵調查線索。這種方法適用於多種場景下的cache消耗調查,不僅僅是clickhosue。
2、通過vmtouch可以統計出某個檔案、目錄,甚至是巢狀目錄下各檔案真正使用cache的大小,從而明確cache消耗的分佈情況。
3、分析了clickhouse大量消耗cache的原因,探索了clickhouse自身是否具備減少cache使用的能力和機制。
4、提供了清理各檔案,或者各指定目錄所佔用cache的通用方法,屬於定點清除的騷操作。
技術是不斷實踐積累的,在此分享出來與大家一起共勉!
如果文章對你有些許幫助,還請各位技術愛好者登入點贊呀,非常感謝!
本文來自部落格園,作者:T-BARBARIANS,轉載請註明原文連結:https://www.cnblogs.com/t-bar/p/17359545.html 謝謝!