天天看點

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

用過mongodb 3.0之後版本的同學應該都比較熟悉wiredtiger的cache evict問題。

<code>連續好幾個版本在cache 淘汰算法上設計都有些小問題,現象總結起來就是寫入hang住</code>。本文使用的是mongodb v3.2.9下wiredtiger-2.8.1(現在wt官方主推v2.9.0版本, mongodb v3.4之後會使用這個版本,雲mongodb現在可以使用v3.2.9,後續我們會很快支援)。wiredtiger逐漸并入mongodb的分之管理,github可以看到如下:

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

我們在服務内部使用者的時候,發現在write heavy場景下,通過mongostat發現used%居高不下,寫入也會抖動的比較厲害,磁盤io偶爾出現uitl 100%或者非常空閑的奇怪現象,抓取了mongo的stack發現異常的線程和cache evict相關,緊急情況下收集了部分資料,将mongodb更新到v3.2.10,問題解決。于是,我們線上下環境進行了複現,收集了profiling資料。

參數選項

含義

cache_size

cache的總大小

eviction_trigger

百分比, 如果cache記憶體使用量超過此百分比,則觸發cache evict的淘汰

eviction_target

百分比,如果觸發了evict淘汰,則需要将cache記憶體使用量降至此百分比之下,才停止evict

eviction_dirty_trigger

百分比, 如果cache中dirty page記憶體使用量超過此百分比,則觸發cache evict的淘汰

eviction_dirty_target

百分比,如果觸發了evict淘汰,則需要将cache中dirty page記憶體使用量降至此百分比之下,才停止evict

evict.threads_max

最大并發的evict線程數

evict.threads_min

最少并發的evict線程數

mongodb預設設定:

<code>threads_max = 4, eviction_target == eviction_dirty_target == 80%</code>

<code>eviction_trigger == eviction_dirty_trigger == 95%,cache_size使用者設定</code>

線上下環境我們讓cache_size盡可能的小,分别設定了<code>2g</code>和<code>10mb</code>,通過sysbench和ycsb在insert heavy情況下複現并遇到了如下兩個問題。

<code>壓測程式qps陡降到100以下,mongostat發現寫入幾乎完全hang住,cache used 100%左右徘徊</code>

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

堆棧采樣顯示<code>__tree_walk_internal</code>占據絕大部分cpu,這個函數是btree 周遊page所用。cache子產品的evict_server在尋找可以淘汰的dirty/clean page時會用這個函數依次周遊dirty/clean page,檢視是否可以将page進行淘汰,evict_server每檢查一次page就會觸發__tree_walk_internal,<code>到這裡我們猜想cache evict是在大量的周遊page</code>,導緻cpu飙高。

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

同樣一個測試資料集,我們使用cachesize=10mb(最初的目的是讓問題盡快複現),偶發出現寫入hang,但cpu都是idle。并且mongodb基本是不可用的狀态,·cache used甚至超過100%·

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

此時獲得的堆棧采樣在·__wt_sleep()·函數采樣比例較高。但由于wiredtiger自選地方比較多,__wt_sleep()經常出現在spin超過一定次數後發生yield,還好有strace,發現sleep()的調用的入參十分有規律,是n + 1000的sleep方式,通過追代碼發現如下

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

這是當evict_server找到一些page後,<code>但是evict_worker不能進行evict操作</code>,也就是之前統計的<code>pages_evicted</code>等于現在的<code>cache-&gt;pages_evict</code>(說明evict的page數量是0),會主動的休眠一下。

<code>這裡官方的想法應該是現在evict_worker已經很忙了,不能再evict(比如磁盤io很大),這時候應該通過sleep放慢evict_server查找髒頁的速度(這個假設不一定是成立的,後面會講到</code>)。

至此,基本的trace工具能搜集到的都差不多收集完了,其實還有systemtap這個神器還沒用,打點的功能真是沒的說。細心的同學可能發現上圖中紅色方形中的wt_stat相關的内部采集點,沒錯wt内部提供了這樣的機制,通過點計數方profiling内部的邏輯。

mongodb本身支援了wt的profiling參數,通過--<code>wiredtigerstatisticslogdelaysecs</code>可定期将統計資訊寫入統計日志(預設0不開啟)。我們設定5s一次的采集,在dbpath下會生成很多wiredtigerstat.xx.yy的檔案。然後我們通過内置的<code>wtstats.py</code>腳本來生成可讀的html

打開之後可以看到cache、cursor、blockmanager、log、transaction等統計資訊

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

此圖可以清晰的看到cache evict的内部采集名額,發現在大量出現下降的400這個地方,很多統計都是陡降(pages_queued要淘汰的page入隊數量、pages_evict淘汰數量、pages_reconcile葉子寫盤數量),增長的三個名額分别是:

<code>pages walked for eviction</code>,evict時周遊的page數量

<code>pages seen by eviction</code>,evict時檢查的page數量

<code>pages currently held in cache</code>,cache中page的數量

那麼,問題就顯而易見了,pages在不斷的被evict_server看到,周遊的速度還是很快的,但是有如下問題:

通路的page不能被evict,是以不能如隊列,是以cpu會飙升,但找不到合适的page,<code>這間接導緻了上面說的在不能evict時候會sleep的問題</code>。

不能被evict_worker寫入磁盤,導緻evict失敗。

問題和現象已經很明朗了,下面就是根據代碼找到問題所在,這裡需要gdb和perf進行調試和采樣,需要帶上wiredtiger的debug symbol。為了防止編譯mongodb代碼(編譯時間太痛苦,20分鐘以上)。

這裡使用<code>--use-system-wiredtiger</code>來指定可以使用自己編譯的wiredtiger,這個技巧也可用來測試wt的相容性和開發的debug用。

經過debug發現3處存在問題的地方:

evict_server通過__evict_walk_file周遊btree檔案的page時,會給cache-&gt;evict_walk_lock加鎖。同樣在checkpoint_server中,對evict_walk_lock也會加鎖<code>導緻evict和checkpoint操作是互斥的</code>

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

這帶來的效果就是checkpoint如果慢,則evict也會滞後,導緻cache增長。這樣的問題在測試時候出現過,表現為寫入hang滞後,cache used一直漲,但過一段時間後,寫入可以恢複的場景。

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

page_seen是周遊btree的page數量,從第二節中給出的曲線圖可以看出這個值一直在快速增長

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

可以evict的page陡降說明上述代碼中<code>__wt_page_can_evict()傳回false</code>,或者之前的continue,說明page不能淘汰。這裡分析了下原因可能有3個:

看到的page很多都是internal_page也就是索引頁

或者cache狀态是wt_evict_pass_would_block幾近滿了并且memory_footprint很大

發現btree在做checkpoint,并且陷入了之前說的sleep問題(此時并不是evict太慢,而是沒有page可以evict,但是evict_server還是會sleep)

MongoDB WiredTiger 存儲引擎cache_pool設計 (下) -- 實踐篇

可以看到__wt_evict(這個函數是evict_worker淘汰一個page時調用),傳回的ret值是16也就是ebusy,在深入往下追隻有兩個地方會傳回ebusy:

在看設定wt_evict_update_restore的地方

由于未設定wt_evict_update_restore而導緻__wt_evict不能刷page

分析到這裡,總結一下原因

checkpoint影響cache evict,導緻evict延時,并且可能導緻page不能當做evict_page進行淘汰

evict_server由于__wt_evict失敗(由于wt_readgen_oldest标記為mark),evict_worker不能刷page,這時由于evict_server的退讓機制(上面的sleep機制) 導緻evict問題被放大,越來越慢。

mongodb官方針對cache問題也提了幾個issue,在v3.2.10和v3.4.0中都進不了不少。涉及的issue如下:

wt-2924 ensure we are doing eviction when threads are waiting for it

wt-2545 investigate eviction tree walk

wt-2639 add tree walk optimization to skip subtrees not in memory

wt-2702 under high thread load, wiredtiger exceeds cache size

wt-2664 change eviction so any eviction thread can find candidates

可以增大cachesize,讓evict更少,以此來減少上面沖突的機率,也可以拉平evict的差距。

減小evict_triger、evict_target的設定,更早的觸發evict行為,這樣每次evict執行時間會縮短

修改源碼,臨界淘汰時候之淘汰clean page(這樣減少磁盤io),或者禁用sleep退讓模式

用更高iops的硬體,交給硬體來解決軟體的問題(沒辦法的辦法)