1. 程式人生 > >記一次MongoDB效能問題+Linux記憶體管理學習筆記--實體記憶體分配

記一次MongoDB效能問題+Linux記憶體管理學習筆記--實體記憶體分配

最近忙著把一個專案從MySQL遷移到MongoDB,在匯入舊資料的過程中,遇到了些許波折,犯了不少錯誤,但同時也學到了不少知識,遂記錄下來。

公司為這個專案專門配備了幾臺高效能務器,清一色的雙路四核超執行緒CPU,外加32G記憶體,運維人員安裝好MongoDB後,就交我手裡了,我習慣於在使用新伺服器前先看看相關日誌,瞭解一下基本情況,當我瀏覽MongoDB日誌時,發現一些警告資訊:

WARNING: You are running on a NUMA machine. We suggest launching mongod like this to avoid performance problems: numactl –interleave=all mongod [other options]

當時我並不太清楚NUMA是什麼東西,所以沒有處理,只是把問題反饋給了運維人員,後來知道運維人員也沒有理會這茬兒,所以問題的序幕就這樣拉開了。

遷移工作需要匯入舊資料。MongoDB本身有一個mongoimport工具可供使用,不過它只接受json、csv等格式的原始檔,不適合我的需求,所以我沒用,而是用PHP寫了一個指令碼,平穩運行了一段時間後,我發現數據匯入的速度下降了,同時PHP丟擲異常:

cursor timed out (timeout: 30000, time left: 0:0, status: 0)

我一時判斷不出問題所在,想想先在PHP腳本里加大Timeout的值應付一下:

<?php

MongoCursor::$timeout = -1;

?>

可惜這樣並沒有解決問題,錯誤反倒變著花樣的出現了:

max number of retries exhausted, couldn’t send query, couldn’t send query: Broken pipe

接著使用strace跟蹤了一下PHP指令碼,發現程序卡在了recvfrom操作上:

shell> strace -f -r -p <PID>
recvfrom(<FD>,

通過如下命令查詢recvfrom操作的含義:

shell> apropos recvfrom
receive a message from a socket

或者按照下面的方式確認一下:

shell> lsof -p <PID>
shell> ls -l /proc/<PID>/fd/<FD>

此時如果查詢MongoDB的當前操作,會發現幾乎每個操作會消耗大量的時間:

mongo> db.currentOp()

與此同時,執行mongostat的話,結果會顯示很高的locked值。

我在網路上找到一篇:MongoDB Pre-Splitting for Faster Data Loading and Importing,看上去和我的問題很類似,不過他的問題實質是由於自動分片導致資料遷移所致,解決方法是使用手動分片,而我並沒有使用自動分片,自然不是這個原因。

詢問了幾個朋友,有人反映曾遇到過類似的問題,在他的場景裡,問題的主要原因是系統IO操作繁忙時,資料檔案預分配堵塞了其它操作,從而導致雪崩效應。

為了驗證這種可能,我搜索了一下MongoDB日誌:

shell> grep FileAllocator /path/to/log
[FileAllocator] allocating new datafile ... filling with zeroes...
[FileAllocator] done allocating datafile ... took ... secs

我使用的檔案系統是ext4(xfs也不錯 ),建立資料檔案非常快,所以不是這個原因,但如果有人使用ext3,可能會遇到這類問題,所以還是大概介紹一下如何解決:

MongoDB按需自動生成資料檔案:先是<DB>.0,大小是64M,然後是<DB>.1,大小翻番到128M,到了<DB>.5,大小翻番到2G,其後的資料檔案就保持在2G大小。為了避免可能出現的問題,可以採用事先手動建立資料檔案的策略:

#!/bin/sh

DB_NAME=$1

cd /path/to/$DB_NAME

for INDEX_NUMBER in {5..50}; do
    FILE_NAME=$DB_NAME.$INDEX_NUMBER

    if [ ! -e $FILE_NAME ]; then
        head -c 2146435072 /dev/zero > $FILE_NAME
    fi
done

注:數值2146435072並不是標準的2G,這是INT整數範圍決定的。

最後一個求助方式就是官方論壇了,那裡的國際友人建議我檢查一下是不是索引不佳所致,死馬當活馬醫,我激活了Profiler記錄慢操作:

mongo> use <DB>
mongo> db.setProfilingLevel(1);

不過結果顯示基本都是insert操作(因為我是匯入資料為主),本身就不需要索引:

mongo> use <DB>
mongo> db.system.profile.find().sort({$natural:-1})

問題始終沒有得到解決,求人不如求己,我又重複了幾次遷移舊資料的過程,結果自然還是老樣子,但我發現每當出問題的時候,總有一個名叫irqbalance的程序CPU佔用率居高不下,搜尋了一下,發現很多介紹irqbalance的文章中都提及了NUMA,讓我一下子想起之前在日誌中看到的警告資訊,我勒個去,竟然繞了這麼大一個圈圈!安下心來仔細翻閱文件,發現官方其實已經有了相關介紹,按如下設定搞定:

shell> echo 0 > /proc/sys/vm/zone_reclaim_mode
shell> numactl --interleave=all mongod [options]

關於zone_reclaim_mode核心引數的說明,可以參考官方文件

注:從MongoDB1.9.2開始:MongoDB會在啟動時自動設定zone_reclaim_mode。

至於NUMA的含義,簡單點說,在有多個物理CPU的架構下,NUMA把記憶體分為本地和遠端,每個物理CPU都有屬於自己的本地記憶體,訪問本地記憶體速度快於訪問遠端記憶體,預設情況下,每個物理CPU只能訪問屬於自己的本地記憶體。對於MongoDB這種需要大記憶體的服務來說就可能造成記憶體不足,NUMA的詳細介紹,可以參考老外的文章

理論上,MySQL、Redis、Memcached等等都可能會受到NUMA的影響,需要留意。

Linux記憶體管理學習筆記--實體記憶體分配

每次深入瞭解一個技術問題,隨著挖據的深入,都發現其背後總非常深的背景知識,甚至需要深入到很多底層系統,這個過程有時會讓自己迷失,會讓自己忘了當初的目的。

前篇中介紹系統啟動時記憶體的使用情況,本篇將介紹簡要Linux如何接管主機的實體記憶體、組織記憶體,最後會較為詳細的介紹Linux分配記憶體的一段程式碼。

前面說了,Linux MM系統細節非常多,自己在探究的時候,也是嘗試儘量抓住主線,這裡也只能抽取了一些“主線劇情”介紹,其中還可以擴展出很多細節,看客感興趣可以自己深究,後續如果興趣還在,我也還會繼續寫出來。核心版本如果沒有特別說明,就是使用2.6.33版本。

1. 實體記憶體組織

先宣告一下,這裡說的Linux都是執行Intel X86架構的。從80386開始,為了更好支援記憶體管理、虛擬記憶體技術,x86架構開始支援處理器的分頁模式(分頁是基於分段)。系統將記憶體分為一個個固定大小的塊,稱作“page frames”,x86架構每一個“page frames”大小為4096位元組。Linux中使用struct page結構來描述一個“page frames”【連結中給出了2.6.18核心下的Page結構】,一個Page結構對應了一個實體記憶體頁。

在Linux中,所有的struct page物件都放在一個數組mem_map,mem_map每一個元素對應一個Page。

2. NUMA下的記憶體結構

在NUMA架構下,系統根據CPU的物理顆數,將記憶體分成對應的Node。例如,兩顆物理CPU,16GB記憶體的硬體:系統則將記憶體分成兩個8GB,分別分配給兩顆CPU:

my111.cm3:/root>#numactl --hardware
available: 2 nodes (0-1)
node 0 size: 8065 MB
node 1 size: 8080 MB

每一個Node,系統又將其分為多個Zone,64位x86架構下(參考:8.1.5),分為兩個ZONE_DMA(低16MB,)、ZONE_NORMAL(其餘記憶體)。所以NUMA架構下的記憶體分配,也就是在各個zone分配記憶體。

3. 記憶體分配函式棧

從底層系統的角度,記憶體分配有如下函式(這裡介紹的底層函式,和上層函式的關係,以後再介紹):

這裡來調查一下函式alloc_pages都做了些什麼,都呼叫了哪些函式:

free_area是一個底層儲存空閒記憶體頁的陣列,有著特殊的結構,它也是記憶體分配Buddy system的核心變數。

4. get_page_from_freelist和zone_reclaim_mode

上面函式get_page_from_freelist【mm/page_alloc.c】通過遍歷系統中各個zone,來尋找可用記憶體,根據Linux系統中zone_reclaim_mode的設定不同,遍歷時的行為略有不同。zone_reclaim_mode是Linux中的一個可配置引數,為了解該引數如何影響記憶體分配,那就開啟get_page_from_freelist的程式碼,仔細看看遍歷各個zone的流程:

上面看到,zone_reclaim_mode非零時,如果某個zone記憶體不夠,則會嘗試出發一次記憶體回收工作(zone_reclaim),等於零時,則直接嘗試寫一個zone。

上面是2.6.33核心的程式碼流程圖,2.6.18(RHEL5.4的核心)中則因為沒有zcl相對簡單一些:

流程圖中可以看到,zone_reclaim_mode非零時,get_page_from_freelist【mm/page_alloc.c】函式中會呼叫zone_watermark_ok掃描free_area,如果當面有沒有足夠的可用記憶體,就會呼叫zone_reclaim【mm/vmscan.c】函式回收記憶體,zone_reclaim實際呼叫zone_reclaim【mm/vmscan.】收回記憶體。

最後

每次深入瞭解一個技術問題,隨著挖據的深入,都發現其背後總非常深的背景知識,甚至需要深入到很多底層系統,這個過程有時會讓自己迷失,會讓自己忘了當初的目的。如果是Linux方面的技術問題,一般最後會收縮到“體系結構”、“Linux原理”和“演算法”,這恰恰對應了計算機系考研時候的三門課程:體系結構、作業系統、和資料結構

參考:

轉自:http://www.orczhou.com/index.php/2011/02/linux-memory-management-3/