MongoDB的WiredTigerLAS.wt大小異常分析
背景
最近在運維MongoDB時遇到一個磁盤空間增長異常的問題,主要是WiredTigerLAS.wt這個文件佔用了70GB以上的空間。經排查,有不少用戶都遇到過這個問題,其背後的根本原因和MongoDB的一個bug有關。本篇文章會詳細分析這個問題背後的原因以及涉及到的相關技術原理,並給出解決方法。
WiredTigerLAS.wt的來龍去脈
首先我們看下這個WiredTigerLAS.wt文件是幹什麼用的。從後綴名上可以知道這是WiredTiger的一個表文件,並且是一個系統表文件。通過搜索相關JIRA可以得知這個文件和WiredTiger的Cache evict相關,因此這裡先簡單介紹一下WiredTiger的Cache evict相關原理。
WiredTiger的Cache evict原理
WiredTiger的B-tree、Page、Extent基本介紹
我們知道,WiredTiger使用B-Tree來組織數據,並且在內存和磁盤上使用了不同的page格式,引用友東大神之前畫的兩張圖做個介紹,讓大家先有個大致的概念(可以先不關注圖中細節):
圖1 WiredTiger Btree、Page、Extent
圖2. WiredTiger Page Internals
其中,在磁盤上的page被稱為extent。當內存壓力小時,extent會在內存中也緩存一份,以減小用戶的訪問延遲。它由page header、block header和kv列表三部分組成,其中page header存儲了extent經過解壓和解密後在內存中的大小。block header存儲了extent在磁盤中的大小和checksum值。kv列表中的key和value都是由cell和data兩部分組成,cell存儲了data的數據格式和長度,data就是具體的k/v值。
WiredTiger內存中的一個leaf page能保存的數據大小是有上限的,這由一個leaf_value_max的參數決定,長度超過leaf_value_max的數據被稱為overflow data。WiredTiger會將該overflow data保存為一個單獨的overflow extent存在表文件中,並用overflow extent address(包含overflow extent的offset和size)替換該value值。這裡可以看到,訪問overflow extent時需要一次額外的磁盤IO,性能肯定不如普通的extent,因此,在MongoDB中,配置的leaf_value_max的值是64MB,由於mongodb的文檔大小不會超過16MB,所以不會出現產生overflow extent的情況。
WiredTiger的事務可見性基本原理
接下來我們還需要簡單瞭解一下WiredTiger中事務可見性的一些基本原理。WiredTiger的事務隔離是基於snapshot技術來實現,支持read uncommited、read commited和snapshot三種隔離級別。
在WiredTiger的內存page中,對這個page的修改會通過Insert和Update列表組織在一起,如上圖2所示(不少WiredTiger相關的原理文章中都有介紹,此處不再贅述)。總的來說,一個內存page由『已持久化』(緩存在內存中的extent)和『未持久化』這兩種文檔組成,而未持久化的文檔又分為『未提交』 和『已提交』的文檔。 未提交的文檔都是『部分事務可見』的文檔(修改文檔的事務可見,其他read commited或snapshot事務不可見),而已提交的文檔分為『所有事務可見』、『 部分事務可見』和『所有事務不可見』三類。對於同個key,按照更新順序從新到老,已提交文檔的順序是:部分事務可見 < 所有事務可見 < 所有事務不可見。若一個key在某個時刻的值是所有事務可見的,則早於該時刻的舊值肯定就是所有事務都不可見的,所以對於同一個key,只會有一個文檔是所有事務可見的。
那怎麼判斷哪些已提交的文檔是所有事務可見的,哪些是部分事務可見的,哪些又是所有事務都不可見的呢?這裡是通過事務id來進行判斷的(MongoDB 4.0開始增加了時間戳的判斷,由於和本文關係不大,此處進行簡化說明),基本規則就是遍歷上面的key的Update列表,根據Update產生的事務id和當前事務的snapshot列表進行比較來判斷。
參照下圖,我們舉例說明,假設所有事務都是使用snapshot隔離級別,現在事務t1、t2、t3、t5已經提交,只有事務t10還在運行中,而在事務t10開始時,事務t1和t2已經提交,但事務t3和t5還未提交。那麼事務t10無法看見事務t3和事務t5所更新的文檔,即使他們在t10運行的過程中變成提交狀態。對應到圖中也就是{Key1, Value1-3}和{Key3,Value3-1}對於事務t10是不可見的,只有早於事務t3提交的文檔{Key1, Value1-2}、{Key3, Value3}才是所有事務可見的文檔,而更早的{Key1, Value1-1}則是所有事務不可見的文檔。
下圖是上面這個例子對應到內存中leaf page的結構,它包含了已持久化的文檔(Leaf Extent中的文檔)和未持久化的文檔(Modify中的文檔),其中的紫色文檔是部分事務可見的未提交的文檔,紅色文檔是部分事務可見的已提交的文檔,綠色文檔是所有事務可見的已提交的文檔,灰色文檔是所有事務不可見的已提交的文檔。
WiredTiger page逐出的方式
WiredTiger在以下4種情況下會進行page逐出:
1)cursor在訪問btree的page時,當發現page的內存佔用量超過了memory_page_max(MongoDB的配置值是10MB),就會對它做逐出操作,以減小page的內存佔用量。
2)後臺eviction線程根據lru queue排序逐出page
3)內存壓力大時,用戶線程會根據lru queue排序逐出page
4)checkpoint會清理它讀進cache的page
Page逐出在WiredTiger代碼中是在__wt_evict中實現的。__wt_evict會依據當前cache使用率情況,分為內存使用低逐出和內存使用高逐出兩種。若內存使用率bytes_inmem/cache_size < (eviction_target+eviction_trigger)/200 且 髒頁使用率bytes_dirty_leaf/cache_size < (eviction_dirty_target+eviction_dirty_trigger)/200,則被認為是內存使用低,反之是內存使用高。
在WiredTiger中,eviction_target默認值是80,eviction_trigger默認值是95,eviction_dirty_target默認值是5,eviction_dirty_trigger默認值是20,所以內存使用低逐出的判斷規則也就是:內存使用率<87.5%且髒頁使用率<17.5%。
內存使用低逐出
它主要有3個目標:
1) 從page中未持久化的文檔(Modify中的文檔)裡,刪除『所有事務不可見的已提交文檔』,減少內存的佔用。
2) 將page中最新的『已提交文檔持久化到磁盤,以減少下一次checkpoint所需的時間。
3) 在內存中依然保留該page,避免下次操作讀到該page時需要訪問磁盤,增大訪問延遲。
內存使用低逐出會先將最新的『已提交文檔』以extent格式逐出到表文件tablename.wt中,同個key的文檔只會持久化一個value值。如果page的modify中有新寫的key或者對已有key的更新,那麼取出最新的已提交value值,若沒有,則從leaf extent中取出其原始的value值。當內存使用率低時,最新的已提交文檔在內存中會組成新的extent並替代老的extent關聯到page上,並釋放內存中老的extent。最後從modify中刪除『所有事務不可見的已提交文檔』。
如下圖所示,最新的已提交文檔是『部分事務可見的已提交的文檔』(紅色文檔),也就是說紅色文檔{Key1,Value1-3}和{Key3,Value3-1}會被更新到新的leaf extent中,刪除所有事務不可見的文檔Value1-1,而其他的文檔仍然要保留在modify中。
那最新的已提交的文檔Value1-3和Value3-1都已持久化了,為什麼還要在modify中保留它們呢?因為Value1-3和Value3-1是部分事務可見的,對於其它不可見的事務來說,它們還需要看到其之前的文檔Value1-2和Value3,所以這些文檔依然都要保留。
內存使用高逐出
它主要有2個目標:
1) 儘可能刪除內存中page的modify和extent,大幅減少內存的佔用。如果因含有未提交的文檔,而無法刪除modify和extent。那麼也要降級為內存使用低逐出,適當減少內存的佔用。
2) 將page中最新的『已提交文檔』持久化到磁盤,並減少下一次checkpoint的時間。
內存使用高逐出根據page的modify包含的文檔種類不同,對應有不同的處理方式。具體有以下三種情況:
1)page的modify不包含『部分事務可見的文檔』
2)page的modify不包含『部分事務可見的未提交的文檔』
3)page的modify包含『部分事務可見的未提交的文檔』
我們先介紹page的modify不包含『部分事務可見的文檔』的情況,也就是說page的modify中只包含所有事務可見的已提交文檔(綠色文檔),和所有事務不可見的已提交文檔(灰色文檔),如下圖所示。由於所有事務不可見的已提交文檔(灰色文檔)是需要被刪除的,這樣modify中所剩下的『所有事務可見的已提交文檔』(綠色文檔)就是最新的『已提交的文檔』,而內存使用高逐出會將這些綠色文檔組成新的extent逐出到表文件tablename.wt中,所以modify中的數據都已被逐出,不再需要保留。最後清理內存中page的extent和modify,並將新的extent在文件中的offset和size關聯到page上,便於下次訪問時從磁盤中讀出extent。
對於page的modify不包含『部分事務可見的未提交的文檔』,也就是說page中只包含部分事務可見的已提交的文檔(紅色文檔),所有事務可見的已提交文檔(綠色文檔),和所有事務不可見的已提交文檔(灰色文檔),如下圖所示。內存使用高逐出不僅會將最新的『已提交文檔』逐出到表文件tablename.wt中,而且還會判斷是否滿足使用las逐出的條件。我們將modify中除『所有事務不可見的已提交文檔』(灰色文檔)之外的其它文檔逐出到表WiredTigerLAS中的過程叫做LAS逐出。文檔被LAS逐出到表WiredTigerLAS之後,很快會被持久化到表文件WiredTigerLAS.wt中,以減少內存的使用。
在下圖中,最新的『已提交文檔』是部分事務可見的已提交的文檔(紅色文檔),所以紅色文檔{Key1,Value1-3}和{Key3,Value3-1}會被更新到新的leaf extent中。除『所有事務不可見的已提交文檔』(灰色文檔)之外的其它文檔就是部分事務可見的已提交的文檔(紅色文檔)、所有事務可見的已提交文檔(綠色文檔),包括{Key1,Value1-3}、{Key1,Value1-2}、{Key3,Value3-1}、{Key3,Value3},它們會被寫到表WiredTigerLAS中。
如果page的modify包含『部分事務可見的未提交的文檔』,或者page的modify不包含『部分事務可見的未提交的文檔』但不滿足las逐出的條件,那麼modify中的數據就不能被逐出,這就導致內存使用高逐出會降級為內存使用率低逐出。而內存使用率低在刪除『所有事務不可見的已提交文檔』後,還需要在內存中保留modify和extent,使得該page就只能釋放少量的內存,但一個表中有很多page,可能某些page滿足第一種情況page的modify不包含『部分事務可見的文檔』,釋放這種page後再次從磁盤中讀取的代價較低,優先被逐出。
讀取逐出的page
根據內存使用率低逐出和內存使用率高逐出的結果,逐出後的page有以下三種形式:
1) page的modify和extent仍然在內存中
2) page的extent在磁盤文件tablename.wt中
3) page的extent在磁盤文件tablename.wt中,modify在表WiredTigerLAS中
第1種情況最簡單,操作直接訪問內存中page的文檔即可。第2種情況需要先從磁盤上的tablename.wt文件中讀出extent並關聯到page上,然後才能訪問。第3種情況在第2種情況的基礎上,還要從表WiredTigerLAS中讀取出該page的所有相關文檔,並重建出page的modify。
LAS逐出
LAS逐出既然可以確保清理內存中的page,為什麼內存高逐出方法不都採用LAS逐出呢?一方面是因為WiredTiger只有redo日誌,要求文檔只有提交後才能被持久化,而las逐出的文檔在寫入表WiredTigerLAS後就可以被持久化,所以包含『部分事務可見的未提交的文檔』的page不可以執行las逐出。另一方面LAS逐出的代價較高,需要將『包含部分事務可見的已提交的文檔』和『所有事務可見的已提交文檔』一個一個寫入表WiredTigerLAS中,後續若有操作訪問該page時,還需要一個一個文檔從表WiredTigerLAS中讀取出來,所以基於讀寫性能考慮,進行las逐出的條件很苛刻。
LAS逐出不僅要page符合LAS逐出的條件,而且要整個cache的使用也符合LAS逐出的條件。先看下整個cache使用所需符合的LAS逐出條件:內存卡主超過2s(內存卡主是指 內存使用率bytes_inmem/cache_size > eviction_trigger/100 或 髒頁使用率bytes_dirty_leaf/cache_size > eviction_dirty_trigger/100) 或 近期逐出的page更適合做LAS逐出(page中『部分事務可見的文檔』/『未持久化的文檔』>80%)。而page需符合LAS逐出的條件是page逐出不需要分頁,且page的modify中所有文檔都是『已提交的文檔』。
LAS逐出過程通過cursor,在一個事務中將一個page的modify中『包含部分事務可見的已提交的文檔』和『所有事務可見的已提交文檔』寫入表WiredTigerLAS。為了便於之後讀取或者清理表WiredTigerLAS中的數據,寫入表WiredTigerLAS的文檔格式除了包含原始的key和value外,還需要保存更多的數據,如下圖所示。page在LAS逐出時會有一個唯一的LAS逐出自增id,便於讀取page時查找。page在LAS逐出時是先按照key從小到大遍歷,對於每個key又按照update從新到舊遍歷,且每個key最新的update類型會特殊標記為BIRTHMARK(例如文檔{Key1,Value1-3}和{Key3,Value3-1}的類型),為了讓LAS清理便於識別不同key的分界點。
表WiredTigerLAS文檔的key:LAS逐出自增id,btree的id,本次LAS逐出的序號,原始key
表WiredTigerLAS文檔的value:update的事務id,update的事務開始時間,update的事務持久化時間,update的事務狀態,update類型,原始value
LAS逐出一個page的所有文檔時,是放在一個事務中的,為了保證原子性。為了性能考慮,使用了read uncommited隔離級別(由於read committed需要訪問全局事務表,來分析哪些事務可見)。
LAS清理
逐出到LAS表裡的key如何清理呢?它的清理是由一個後臺LAS清理線程來完成的。該線程每隔2s會通過cursor遍歷一遍表WiredTigerLAS,當它發現標記為BIRTHMARK的update時,它會檢查該update對應的文檔當前是不是所有事務可見的,如果是的話,那麼就刪除該key對應的update列表。所以LAS清理線程的目的是保證WiredTigerLAS.wt文件大小不會持續增加。
WiredTigerLAS.wt大小異常原因
至此,我們已經摸清了WiredTigerLAS.wt的來龍去脈。並且,我們還知道,有一個LAS清理機制可以保證WiredTigerLAS.wt文件的大小得到控制。那為什麼用戶還是遇到了WiredTigerLAS.wt文件大小異常的情況呢?原因是LAS清理邏輯有個bug,只有在有表被刪除時,才會執行LAS清理邏輯。這個bug最近才被官方修復,並且修復之後,又暴露了一個之前被掩蓋的數據不一致的bug,這和LAS清理使用的read uncommited隔離級別有關。
上面說了LAS逐出一個page的所有文檔時,是放在一個事務中的。同樣,LAS清理也是在一個事務中進行的。由於LAS逐出和LAS清理是併發執行,使用read uncommited隔離級別的LAS清理可能只清理了某個key的update列表中部分update的情況(例如清理了{Key1,Value1-3},保留了{Key1,Value1-2})。這是因為當LAS逐出在一個事務中先寫完{Key1,Value1-3}時,LAS清理就能看到剛寫的{Key1,Value1-3},這時如果發現{Key1,Value1-3}已經全局可見了,LAS清理就會清理update列表,而這時只清理{Key1,Value1-3},等LAS清理完並遍歷到下一個key後,LAS逐出才繼續寫了{Key1,Value1-2},這樣就導致在表WiredTigerLAS中殘留value1-1的情況。而之後用戶訪問該page時就會出現讀不到{Key1,Value1-3},只能讀到{Key1,Value1-2}的情況,造成數據不一致(雖然在extent中有{Key1,Value1-3},但查找時會優先訪問modify中的文檔)。
這個bug的修復也比較簡單,將LAS清理改為使用read commited隔離級別就可以了,這樣LAS清理就不可能看到某個key的不完整的update列表的情況,也就不可能出現只清理了某個key的update列表中部分update的情況。
總結
本文詳細分析了WiredTigerLAS.wt大小增長異常的原因,並介紹了相關技術原理,下面做個簡要總結:
1) 表WiredTigerLAS的作用是當內存使用率高時用於臨時存放不能被逐出到用戶表文件中的數據,表WiredTigerLAS中的數據會被高優先級逐出到磁盤文件WiredTigerLAS.wt中,所以能達到減少內存使用的目的。
2)當WiredTiger內存使用率高且有大量併發寫入時,它會使用內存使用率高逐出+LAS逐出方式來逐出page,而大量併發寫入會使得page的modify中會包含很多『部分事務可見的已提交的文檔』,LAS逐出最後會將這些文檔逐出到表WiredTigerLAS中,造成文件WiredTigerLAS.wt持續有數據寫入。
3)當MongoDB負載穩定的時候,LAS清理機制本來可以保證文件WiredTigerLAS.wt空間達到一定大小後就不再增加,但由於LAS清理執行時機的bug,造成寫入的數據無法被刪除,而又有新數據寫入,造成文件WiredTigerLAS.wt大小持續增大。
4)LAS清理執行時機的bug被修復後,暴露了原來被掩蓋的LAS清理和LAS逐出事務併發執行時可能導致數據出現不一致的問題,將LAS清理換為使用read commited隔離級別後得到解決。
最後,以上問題在最新的MongoDB版本(包括阿里雲最新的MongoDB版本)中都已經得到了修復中,請大家放心使用。