MongoDB如何使用記憶體?為什麼記憶體滿了?

張友東發表於2019-01-08

最近接到多個MongoDB記憶體方面的線上case及社群問題諮詢,主要集中在:

  • 為什麼我的 MongoDB 使用了 XX GB 記憶體?
  • 一個機器上部署多個 Mongod 例項/程式,WiredTiger cache 應該如何配置?
  • MongoDB 是否應該使用 SWAP 空間來降低記憶體壓力?

MongoDB 記憶體用在哪?

Mongod 程式啟動後,除了跟普通程式一樣,載入 binary、依賴的各種library 到記憶體,其作為一個DBMS,還需要負責客戶端連線管理,請求處理,資料庫後設資料、儲存引擎等很多工作,這些工作都涉及記憶體的分配與釋放,預設情況下,MongoDB 使用 Google tcmalloc 作為記憶體分配器,記憶體佔用的大頭主要是「儲存引擎」與 「客戶端連線及請求的處理」。

儲存引擎 Cache

MongoDB 3.2 及以後,預設使用 WiredTiger 儲存引擎,可通過 cacheSizeGB 選項配置 WiredTiger 引擎使用記憶體的上限,一般建議配置在系統可用記憶體的60%左右(預設配置)。

舉個例子,如果 cacheSizeGB 配置為 10GB,可以認為 WiredTiger 引擎通過tcmalloc分配的記憶體總量不會超過10GB。為了控制記憶體的使用,WiredTiger 在記憶體使用接近一定閾值就會開始做淘汰,避免記憶體使用滿了阻塞使用者請求。

目前有4個可配置的引數來支援 wiredtiger 儲存引擎的 eviction 策略(一般不需要修改),其含義是:

引數 預設值 含義
eviction_target 80 當 cache used 超過 eviction_target,後臺evict執行緒開始淘汰 CLEAN PAGE
eviction_trigger 95 當 cache used 超過 eviction_trigger,使用者執行緒也開始淘汰 CLEAN PAGE
eviction_dirty_target 5 當 cache dirty 超過 eviction_dirty_target,後臺evict執行緒開始淘汰 DIRTY PAGE
eviction_dirty_trigger 20 當 cache dirty 超過 eviction_dirty_trigger, 使用者執行緒也開始淘汰 DIRTY PAGE

在這個規則下,一個正常執行的 MongoDB 例項,cache used 一般會在 0.8 * cacheSizeGB 及以下,偶爾超出問題不大;如果出現 used>=95% 或者 dirty>=20%,並一直持續,說明記憶體淘汰壓力很大,使用者的請求執行緒會阻塞參與page淘汰,請求延時就會增加,這時可以考慮「擴大記憶體」或者 「換更快的磁碟提升IO能力」。

TCP 連線及請求處理

MongoDB Driver 會跟 mongod 程式建立 tcp 連線,並在連線上傳送資料庫請求,接受應答,tcp 協議棧除了為連線維護socket後設資料為,每個連線會有一個read buffer及write buffer,使用者收發網路包,buffer的大小通過如下sysctl系統引數配置,分別是buffer的最小值、預設值以及最大值,詳細解讀可以google。

net.ipv4.tcp_wmem = 8192  65536  16777216
net.ipv4.tcp_rmem = 8192  87380  16777216

redhat7(redhat6上並沒有匯出這麼詳細的資訊) 上通過 ss -m 可以檢視每個連線的buffer的資訊,如下是一個示例,讀寫 buffer 分別佔了 2357478bytes、2626560bytes,即均在2MB左右;500個類似的連線就會佔用掉 1GB 的記憶體;buffer 佔到多大,取決於連線上傳送/應答的資料包的大小、網路質量等,如果請求應答包都很小,這個buffer也不會漲到很大;如果包比較大,這個buffer就更容易漲的很大。

tcp    ESTAB      0      0                       127.0.0.1:51601                                 127.0.0.1:personal-agent
   skmem:(r0,rb2357478,t0,tb2626560,f0,w0,o0,bl0)
   

除了協議棧上的記憶體開銷,針對每個連線,Mongod 會起一個單獨的執行緒,專門負責處理這條連線上的請求,mongod 為處理連線請求的執行緒配置了最大1MB的執行緒棧,通常實際使用在幾十KB左右,通過 proc 檔案系統看到這些執行緒棧的實際開銷。 除了處理請求的執行緒,mongod 還有一系列的後臺執行緒,比如主備同步、定期重新整理 Journal、TTL、evict 等執行緒,預設每個執行緒最大ulimit -s(一般10MB)的執行緒棧,由於這批執行緒數量比較固定,佔的記憶體也比較可控。

# cat /proc/$pid/smaps

7f563a6b2000-7f563b0b2000 rw-p 00000000 00:00 0
Size:              10240 kB
Rss:                  12 kB
Pss:                  12 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:        12 kB
Referenced:           12 kB
Anonymous:            12 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB

執行緒在處理請求時,需要分配臨時buffer儲存接受到的資料包,為請求建立上下文(OperationContext),儲存中間的處理結果(如排序、aggration等)以及最終的應答結果等。

當有大量請求併發時,可能會觀察到 mongod 使用記憶體上漲,等請求降下來後又慢慢釋放的行為,這個主要是 tcmalloc 記憶體管理策略導致的,tcmalloc 為效能考慮,每個執行緒會有自己的 local free page cache,還有 central free page cache;記憶體申請時,按 local thread free page cache ==> central free page cache 查詢可用記憶體,找不到可用記憶體時才會從堆上申請;當釋放記憶體時,也會歸還到 cache 裡,tcmalloc 後臺慢慢再歸還給 OS, 預設情況下,tcmalloc 最多會 cache min(1GB,1/8 * system_memory) 的記憶體, 通過 setParameter.tcmallocMaxTotalThreadCacheBytesParameter 引數可以配置這個值,不過一般不建議修改,儘量在訪問層面做調優)

tcmalloc cache的管理策略,MongoDB 層暴露了幾個引數來調整,一般不需要調整,如果能清楚的理解tcmalloc原理及引數含義,可做針對性的調優;MongoDB tcmalloc 的記憶體狀態可以通過 db.serverStatus().tcmalloc 檢視,具體含義可以看 tcmalloc 的文件。重點可以關注下 total_free_bytes ,這個值告訴你有多少記憶體是 tcmalloc 自己快取著,沒有歸還給 OS 的。

mymongo:PRIMARY> db.serverStatus().tcmalloc
{
    "generic" : {
        "current_allocated_bytes" : NumberLong("2545084352"),
        "heap_size" : NumberLong("2687029248")
    },
    "tcmalloc" : {
        "pageheap_free_bytes" : 34529280,
        "pageheap_unmapped_bytes" : 21135360,
        "max_total_thread_cache_bytes" : NumberLong(1073741824),
        "current_total_thread_cache_bytes" : 1057800,
        "total_free_bytes" : 86280256,
        "central_cache_free_bytes" : 84363448,
        "transfer_cache_free_bytes" : 859008,
        "thread_cache_free_bytes" : 1057800,
        "aggressive_memory_decommit" : 0,
        ...
    }
}

如何控制記憶體使用?

合理配置 WiredTiger cacheSizeGB

  • 如果一個機器上只部署 Mongod,mongod 可以使用所有可用記憶體,則是用預設配置即可。
  • 如果機器上多個mongod混部,或者mongod跟其他的一些程式一起部署,則需要根據分給mongod的記憶體配額來配置 cacheSizeGB,按配額的60%左右配置即可。

控制併發連線數

TCP連線對 mongod 的記憶體開銷上面已經詳細分析了,很多同學對併發有一定誤解,認為「併發連線數越高,資料庫的QPS就越高」,實際上在大部分資料庫的網路模型裡,連線數過高都會使得後端記憶體壓力變大、上下文切換開銷變大,從而導致效能下降。

MongoDB driver 在連線 mongod 時,會維護一個連線池(通常預設100),當有大量的客戶端同時訪問同一個mongod時,就需要考慮減小每個客戶端連線池的大小。mongod 可以通過配置 net.maxIncomingConnections 配置項來限制最大的併發連線數量,防止資料庫壓力過載。

是否應該配置 SWAP

官方文件上的建議如下,意思是配置一下swap,避免mongod因為記憶體使用太多而OOM。

For the WiredTiger storage engine, given sufficient memory pressure, WiredTiger may store data in swap space.

Assign swap space for your systems. Allocating swap space can avoid issues with memory contention and can prevent the OOM Killer on Linux systems from killing mongod. 

開啟 SWAP 與否各有優劣,SWAP開啟,在記憶體壓力大的時候,會利用SWAP磁碟空間來緩解記憶體壓力,此時整個資料庫服務會變慢,但具體變慢到什麼程度是不可控的。不開啟SWAP,當整體記憶體超過機器記憶體上線時就會觸發OOM killer把程式幹掉,實際上是在告訴你,可能需要擴充套件一下記憶體資源或是優化對資料庫的訪問了。

是否開啟SWAP,實際上是在「好死」與「賴活著」的選擇,個人覺得,對於一些重要的業務場景來說,首先應該為資料庫規劃足夠的記憶體,當記憶體不足時,「及時調整擴容」比「不可控的慢」更好。

其他

  • 儘量減少記憶體排序的場景,記憶體排序一般需要更多的臨時記憶體
  • 主備節點配置差距不要過大,備節點會維護一個buffer(預設最大256MB)用於儲存拉取到oplog,後臺從buffer裡取oplog不斷重放,當備同步慢的時候,這個buffer會持續使用最大記憶體。
  • 控制集合及索引的數量,減少databse管理後設資料的記憶體開銷;集合、索引太多,後設資料記憶體開銷是一方面的影響,更多的會影響啟動載入的效率、以及執行時的效能。


相關文章