MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析

MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析

導語:計算機硬件在飛速發展,數據規模在急速膨脹,可是數據庫仍然使用是十年之前的架構體系,WiredTiger 嘗試打破這一切,充分利用多核與大內存時代來從新設計數據庫引擎,達到 7 - 10 倍寫入性能提高。但一些先行使用的同行發現新版有間歇性掛起的問題,本文由袁榮喜向「高可用架構」投稿,經過分析 WiredTiger 源代碼,剖析深坑本後的緣由。算法

MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析袁榮喜,學霸君工程師,2015 年加入學霸君,負責網絡實時傳輸和分佈式系統的架構設計和實現,專一於基礎技術領域,在網絡傳輸、數據庫內核、分佈式系統和併發編程方面有必定了解。數據庫

從 MongoDB 3.0 版本引入 WiredTiger 存儲引擎(如下稱爲 WT)以來,一直有同窗反應在高速寫入數據時 WT 引擎會間歇性寫掛起,有時候寫延遲達到了幾十秒,這確實是個嚴重的問題。編程

引發這類問題的關鍵在於 WT 的 LRU Cache 的設計模型,WT 在設計 LRU Cache 時採用分段掃描標記和 hazard pointer 的淘汰機制,在 WT 內部稱這種機制叫 eviction Cache 或者 WT Cache,其設計目標是充分利用現代計算機超大內存容量來提升事務讀寫併發。在高速不間斷寫時內存操做是很是快的,可是內存中的數據最終必須寫入到磁盤上,將頁數據(page)由內存中寫入磁盤上是須要寫入時間,一定會和應用程序的高速不間斷寫產生競爭,這在任何數據庫存儲引擎都是沒法避免的,只是因爲 WT 利用大內存和寫無鎖的特性,讓這種不平衡現象更加顯著。數組

下圖是一位網名叫 chszs 同窗對 mongoDB 3.0 和 3.2 版本測試高速寫遇到的 hang 現象。
MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析
緩存

從上圖能夠看出,數據週期性出現了 hang 現象,筆者在單獨對 WT 進行高併發順序寫時遇到的狀況和上圖基本一致,有時候掛起長達 20 秒。針對此問題我結合 WT 源碼和調試測試進行了分析,基本得出的結論以下:服務器

1.WT 引擎的 eviction Cache 實現時沒有考慮 LRU Cache 的分級淘汰,只是經過掃描 btree 來標記,這使得它和一些獨佔式 btree 操做(例如: checkpoint)容易發生競爭。
2.WT btree 的 checkpoint 機制設計存在 bug,在大量併發寫事務發生時,checkpoint 須要很長時間才能完成,形成刷入磁盤的數據很大,寫盤時間很長。容易引發 Cache 滿而掛起全部的讀寫操做。
3.WT 引擎的 redo log 文件超過 1GB 大小後就會另外新建一個新的 redo log 文件來繼續存儲新的日誌,在操做系統層面上新建一個文件的是須要屢次 I/O 操做,一旦和 checkpoint 數據刷盤操做同時發生,全部的寫也就掛起了。

微信

要完全弄清楚這幾個問題,就須要對從 WT 引擎的 eviction Cache 原理來剖析,經過分析原理找到解決此類問題的辦法。先來看 eviction Cache 是怎麼實現的,爲何要這麼實現。網絡

eviction cahce 原理

eviction Cache 是一個 LRU Cache,即頁面置換算法緩衝區,LRU Cache 最先出現的地方是操做系統中關於虛擬內存和物理內存數據頁的置換實現,後被數據庫存儲引擎引入解決內存和磁盤不對等的問題。因此 LRU Cache 主要是解決內存與數據大小不對稱的問題,讓最近將要使用的數據緩存在 Cache 中,把最遲使用的數據淘汰出內存,這是 LRU 置換算法的基本原則。但程序代碼是沒法預測將來的行爲,只能根據過去數據頁的狀況來肯定,通常咱們認爲過去常用的數據比不經常使用的數據將來被訪問的機率更高,不少 LRU Cache 大部分是基於這個規則來設計。數據結構

WT 的 eviction Cache 也不例外的遵循了這個 LRU 原理,不過 WT 的 eviction Cache 對數據頁採用的是分段局部掃描和淘汰,而不是對內存中全部的數據頁作全局管理。基本思路是一個線程階段性的去掃描各個 btree,並把 btree 能夠進行淘汰的數據頁添加到一個 LRU queue 中,當 queue 填滿了後記錄下這個過程當前的 btree 對象和 btree 的位置(這個位置是爲了做爲下次階段性掃描位置),而後對 queue 中的數據頁按照訪問熱度排序,最後各個淘汰線程按照淘汰優先級淘汰 queue 中的數據頁,整個過程是週期性重複。 WT 的這個 evict 過程涉及到多個 eviction thread 和 hazard pointer 技術。多線程

WT 的 evict 過程都是以 page 爲單位作淘汰,而不是以 K/V。這一點和 Memcache、 Redis 等經常使用的緩存 LRU 不太同樣,由於在磁盤上數據的最小描述單位是 page block,而不是記錄。

eviction 線程模型

從上面的介紹能夠知道 WT 引擎的對 page 的 evict 過程是個多線程協同操做過程,WT 在設計的時候採用一種叫作 leader-follower 的線程模型,模型示意圖以下:
MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析
Leader thread 負責週期性掃描全部內存中的 btree 索引樹,將符合 evict 條件的 page 索引信息填充到 eviction queue,當填充 queue 滿時,暫停掃描並記錄下最後掃描的 btree 對象和 btree 上的位置,而後對 queue 中的 page 按照事務的操做次數和訪問次作一次淘汰評分,再按照評分從小到大作排序。也就是說最評分越小的 page 越容易淘汰。下個掃描階段的起始位置就是上個掃描階段的結束位置,這樣能保證在若干個階段後全部內存中的 page 都被掃描過一次,這是爲了公平性。

這裏必需要說明的是一次掃描極可能只是掃描內存一部分 btree 對象,而不是所有,因此我對這個過程稱爲階段性掃描(evict pass),它不是對整個內存中的 page 作評分排序。這個階段性掃描的間隔時間是 100 毫秒,而觸發這個 evict pass 的條件就是 WT Cache 管理的內存超出了設置的閾值,這個在後面的 eviction Cache 管理的內存小節中詳細介紹。

在 evict pass 後,若是 evction queue 中有等待淘汰的 page 存在就會觸發一個操做系統信號來激活 follower thread 來進行 evict page 工做。雖然 evict pass 的間隔時間一般是 100 毫秒,這裏有個問題就是當 WT Cache 的內存觸及上限而且有大量寫事務發生時,讀寫事務線程在事務開始時會喚醒 leader thread 和 follower thread,這就會產生大量的操做系統上下文切換,系統性能急劇降低。好在 WT-2.8 版本修復了這個問題,leader follower 經過搶鎖來成爲 leader,經過多線程信號合併和週期性喚醒來 follower,並且 leader thread 也承擔 evict page 的工做,能夠避免大部分的線程喚醒和上下文切換。是否是有點像 Nginx 的網絡模型?

hazard pointer

hazard pointer 是一個無鎖併發技術,其應用場景是單個線程寫和多個線程讀的場景,大體的原理是這樣的,每一個讀的線程設計一個與之對應的無鎖數組用於標記這個線程引用的 hazard pointer 對象。讀線程的步驟以下:

1.讀線程在訪問某個 hazard pointer 對象時,先將在本身的標記數組中標記訪問的對象。
2.讀線程在訪問完畢某個 hazard pointer 對象時,將其對應的標記從標記數組中刪除。

寫線程的步驟大體是這樣的,寫線程若是須要對某個 hazard pointer 對象寫時,先判斷全部讀線程是否標記了這個對象,若是標記了,放棄寫。若是未標記,進行寫。

關於 hazard pointer 理論能夠訪問 https://www.research.ibm.com/people/m/michael/ieeetpds-2004.pdf

Hazard pointer 是怎樣應用在 WT 中呢?咱們這樣來看待這個事情,把內存 page 的讀寫看作 hazard pointer 的讀操做,把 page 從內存淘汰到磁盤上的過程看作 hazard pointer 的寫操做,這樣瞬間就能明白爲何 WT 在頁的操做上能夠不遵照 The FIX Rules 規則,而是採用無鎖併發的頁操做。要達到這種訪問方式有個條件就是內存中 page 自己的結構要支持 lock free 訪問,這個在剖析 WiredTiger 數據頁無鎖及壓縮一文中介紹過了。從上面的描述能夠看出 evict page 的過程當中首先要作一次 hazard pointer 寫操做檢查,然後才能進行 page 的 reconcile 和數據落盤。

hazard pointer 併發技術的應用是整個 WT 存儲引擎的關鍵,它關係到 btree 結構、 internal page 的構造、事務線程模型、事務併發等實現。 Hazard pointer 使得 WT 不依賴 The Fix Rules 規則,也讓 WT 的 btree 結構更加靈活多變。

Hazard pointer 是比較新的無鎖編程模式,能夠應用在不少地方,筆者曾在一個高併發媒體服務器上用到這個技術,之後有機會把裏面的技術細節分享出來。

eviction Cache 管理的內存

eviction Cache 其實就是內存管理和 page 淘汰系統,目標就是爲了使得管轄的內存不超過物理內存的上限,而觸發淘汰 evict page 動做的基礎依據就是內存上限。 eviction Cache 管理的內存就是內存中 page 的內存空間,page 的內存分爲幾部分:

1.從磁盤上讀取到已經刷盤的數據,在 page 中稱做 disk buffer。若是 WT 沒有開啓壓縮且使用的 MMAP 方式讀寫磁盤,這個 disk 2.buffer 的數據大小是不計在 WT eviction Cache 管理範圍以內的。若是是開啓壓縮,會將從 MMAP 讀取到的 page 數據解壓到一個 WT 分配的內存中,這個新分配的內存是計在 WT eviction Cache 中的。
3.Page 在內存中新增的修改事務數據內存空間,計入在 eviction Cache 中。
Page 基本的數據結構全部的內存空間,計入在 eviction Cache 中。

PS: 關於 page 結構和內存相關的細節請查看 剖析 WiredTiger 數據頁無鎖及壓縮。

WT 在統計 page 的內存總量是經過一個 footprint 機制來統計兩項數據,一項是總的內存使用量 mem_size,一項是增刪改形成的髒頁數據總量 dirty_mem_size。統計方式很簡單,就是每次對頁進行載入、增刪改、分裂和銷燬時對上面兩項數據作原子增長或者減小計數,這樣能夠精確計算到當前系統中 WT 引擎內存佔用量。假設引擎外部配置最大內存空間爲 cache_size,內存上限觸發 evict 的比例爲 80%,內存髒頁上限觸發 evict 的比例爲 75%. 那麼系統觸發 evict pass 操做的條件爲:

mem_size > cache_size * 80%

或者

dirty_mem_size > cache_size * 75%

知足這個條件 leader 線程就會進行 evict pass 階段性掃描並填充 eivction queue,最後驅使 follower 線程進行 evict page 操做。

evict pass 策略

前面介紹過 evict pass 是一個階段性掃描的過程,整個過程分爲掃描階段、評分排序階段和 evict 調度階段。掃描階段是經過掃描內存中 btree,檢查 btree 在內存中的 page 對象是否能夠進行淘汰。掃描步驟以下:

一、根據上次 evict pass 最後掃描的 btree 和它對應掃描的位置最爲本次 evict pass 的起始位置,若是當前掃描的 btree 被其餘事務線程設成獨佔訪問方式,跳過當前 btree 掃描下個 btree 對象。

二、進行 btree 遍歷掃描,若是 page 知足淘汰條件,將 page 的索引對象添加到 evict queue 中,淘汰條件爲:

  • 若是 page 是數據頁,必須 page 當前最新的修改事務必須早以 evict pass 事務。
  • 若是 page 是 btree 內部索引頁,必須 page 當前最新的修改事務必須早以 evict pass 事務且當前處於 evict queue 中的索引頁對象很少於 10 個。
  • 當前 btree 不處於正創建 checkpoint 狀態

三、若是本次 evict pass 當前的 btree 有超過 100 個 page 在 evict queue 中或者 btree 處於正在創建 checkpoint 時,結束這個 btree 的掃描,切換到下一個 btree 繼續掃描。

四、若是 evict queue 填充滿時或者本次掃描遍歷了全部 btree,結束本次 evict pass。

PS: 在開始 evict pass 時,evict queue 可能存在有上次掃描且未淘汰出內存的 page,那麼此次 evict pass 必定會讓 queue 填滿(大概 400 個 page)。

評分排序階段是在 evict pass 後進行的,當 queue 中有 page 時,會根據每一個 page 當前的訪問次數、 page 類型和淘汰失敗次數等計算一個淘汰評分,而後按照評分從小打到進行快排,排序完成後,會根據 queue 中最大分數和最小分數計算一個淘汰邊界 evict_throld,queue 中全部大於 evict_throld 的 page 不列爲淘汰對象。

WT 爲了讓 btree 索引頁儘可能保存在內存中,在評分的時候索引頁的分值會加上 1000000 分,讓 btree 索引頁免受淘汰。

evict pass 最後會作個判斷,若是有 follower 線程存在,用系統信號喚醒 follower 進行 evict page。若是系統中沒有 follower,leader 線程進行 eivct page 操做。這個模型在 WT-2.81 版本已經修改爲搶佔模式。

evict page 過程

evict page 其實就是將 evict queue 中的 page 數據先寫入到磁盤文件中,而後將內存中的 page 對象銷燬回收。整個 evict page 也分爲三個階段:從 evict queue 中獲取 page 對象、 hazard pointer 判斷和 page 的 reconcile 過程,整個過程的步驟以下:

  1. 從 evict queue 頭開始獲取 page,若是發現 page 的索引對象不爲空,對 page 進行 LOCKED 原子性標記防止其餘讀事務線程引用並將 page 的索引從 queue 中刪除。
  2. 對淘汰的 page 進行 hazard pointer,若是有其餘線程對 page 標記 hazard pointer,page 不能被 evict 出內存,將 page 的評分加 100.
  3. 若是沒有其餘線程對 page 標記 hazard pointer,對 page 進行 reconcile 並銷燬 page 內存中的對象。

evict page 的過程大部分是由 follower thread 來執行,這個在上面的線程模型一節中已經描述過。但在一個讀寫事務開始以前,會先檢查 WT Cache 是否有足夠的內存空間進行事務執行,若是 WT Cache 的內存容量觸及上限閾值,事務執行線程會嘗試去執行 evict page 工做,若是 evict page 失敗,會進行線程堵塞等待直到 WT Cache 有執行讀寫事務的內存空間(是否是讀寫掛起了?)。這種情況通常出如今正在創建 checkpoint 的時候,那麼 checkpoint 是怎麼引發這個現象的呢?下面來分析原因。

eviction Cache 與 checkpoint 之間的事

衆所周知,創建 checkpoint 的過程是將內存中全部的髒頁(dirty page)同步刷入磁盤上並將 redo log 的重演位置設置到最後修改提交事務的 redo log 位置,相對於 WT 引擎來講,就是將 eviction Cache 中的全部髒頁數據刷入磁盤但並不將內存中的 page 淘汰出內存。這個過程其實和正常的 evict 過程是衝突的,並且 checkpoint 過程當中須要更多的內存完成這項工做,這使得在一個高併發寫的數據庫中有可能出現掛起的情況發生。爲了更好的理解整個問題的細節,咱們先來看看 WT checkpoint 的原理和過程。

btree 的 checkpoint

WT 引擎中的 btree 創建 checkpoint 過程仍是比較複雜的,過程的步驟也比較多,並且不少步驟會涉及到索引、日誌、事務和磁盤文件等。我以 WT-2.7(mongoDB 3.2) 版本爲例子,checkpoint 大體的步驟以下圖:
MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析

在上圖中,其中綠色的部分是在開始 checkpoint 事務以前會將全部的 btree 的髒頁寫入文件 OS Cache 中,若是在高速寫的狀況下,寫的速度接近也 reconcile 的速度,那麼這個過程將會持續很長時間,也就是說 OS Cache 中會存在大量未落盤的數據。並且在 WT 中 btree 採用的 copy on write(寫時複製)和 extent 技術,這意味 OS Cache 中的文件數據大部分是在磁盤上是連續存儲的,那麼在綠色框最後一個步驟會進行同步刷盤,這個時候若是 OS Cache 的數據量很大就會形成這個操做長時間佔用磁盤 I/O。這個過程是會把全部提交的事務修改都進行 reconcile 落盤操做。

在上圖的紫色是真正開始 checkpoint 事務的步驟,這裏須要解釋的是因爲前面綠色步驟刷盤時間會比較長,在這個時間範圍裏會有新的寫事務發生,也就意味着會新的髒頁,checkpint 必須把最新提交的事務修改落盤並且還要防止 btree 的分裂,這個時候就會得到 btree 的獨佔排他式訪問,這時 eviction Cache 不能對這個 btree 上的頁進行 evict 操做(在這種狀況下是否是容易形成 WT Cache 滿而掛起讀寫事務?)。

PS:WT-2.8 版本以後對 checkpoint 改動很是大,主要是針對上面兩點作了拆分,防止讀寫事務掛起發生,但大致過程是差很少的。

寫掛起

經過前面的分析大??知道寫掛起的緣由了,主要引發掛起的現象主要是由於寫內存的速度遠遠高於寫磁盤的速度。先來看一分內存和磁盤讀寫的速度的數據吧。順序讀寫的對比:
MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析

從上圖能夠看出,SATA 磁盤的順序讀寫 1MB 數據大概須要 8ms,SSD 相對快一點,大概只需 2ms. 但內存的讀寫遠遠大於磁盤的速度。 SATA 的隨機讀取算一次 I/O 時間,大概在 8ms 到 10ms,SSD 的隨機讀寫時間比較快,大概 0.1ms。

咱們來分析 checkpoint 時掛起讀寫事務的幾種狀況,假設系統在高速寫某一張表(每秒以 100MB / S 的速度寫入),每 1 分鐘作一次 checkpoint。那麼 1 分鐘後開始進行圖 3 中綠色的步驟,這個時候可能會這一分鐘寫入的髒數據壓縮先寫入到 OS Cache 中,這個時候可能 OS Cache 可能存有近 2GB 的數據。這 2GB 的 sync 刷到磁盤上的時間至少須要 10 ~ 20 秒,並且磁盤 I/O 是被這個同步刷盤的任務佔用了。這個時候有可能發生幾件事情:

  1. 外部的寫事務還在繼續,事務提交時須要寫 redo log 文件,這個時候磁盤 I/O 被佔用了,寫事務掛起等待。
  2. 外部的讀寫事務還在繼續,redo log 文件滿了,須要新建一個新的 redo log 文件,可是新建文件須要屢次隨機 I/O 操做,磁盤 I/O 暫時沒法調度來建立文件,全部寫事務掛起。
  3. 外部讀寫事務線程還在繼續,由於 WT Cache 觸發上限閾值須要 evict page。 Evict page 時也會調用 reconcile 將 page 寫入 OS Cache,但這個文件的 OS Cache 正在進行 sync,evict page 只能等 sync 完成才能寫入 OS Cache,evict page 線程掛起,其餘讀寫事務在開始時會判斷是否有足夠的內存進行事務執行,若是沒有足夠內存,全部讀寫事務掛起。

這三種狀況是由於階段性 I/O 被耗光而形成讀寫事務掛起的。

在圖 3 紫色步驟中, checkpoint 事務開始後會先得到 btree 的獨佔排他訪問方式,這意味這個 btree 對象上的 page 不能進行 evict, 若是這個 btree 索引正在進行高速寫入,有可能讓 checkpoint 過程當中數據頁的 reconcile 時間很長,從而耗光 WT Cache 內存形成讀寫事務掛起現象,這個現象極爲在測試中極爲少見(遇見過兩次)。 要解決這幾個問題只要解決內存和磁盤 I/O 不對等的問題就能夠了。

內存和磁盤 I/O 的權衡

引發寫掛起問題的緣由多種多樣,但歸根結底是由於內存和磁盤速度不對稱的問題。由於 WT 的設計原則就是讓數據儘可能利用現代計算機的超大內存,但是內存中的髒數據在 checkpoint 時須要同步寫入磁盤形成瞬間 I/O 很高,這是矛盾的。要解決這些問題我的認爲有如下幾個途徑:

  1. 將 MongoDB 的 WT 版本升級到 2.8,2.8 版本對 evict queue 模型作了分級,儘可能避免 evict page 過程當中堵塞問題 ,2.8 的 checkpoint 機制不在是分爲預前刷盤和 checkpoint 刷盤,而是採用逐個對 btree 直接作 checkpoint 刷盤,緩解了 OS Cache 緩衝太多的文件髒數據問題。
  2. 試試 direct I/O 或許會有不一樣的效果,WT 是支持 direct I/O 模式。筆者試過 direct I / O 模式,讓 WT Cache 完全接管全部的物理內存管理,寫事務的併發會比 MMAP 模式少 10%,但沒有出現過超過 1 秒的寫延遲問題。
  3. 嘗試將 WT Cache 設小點,大概設置成整個內存的 1 / 4 左右。這種作法是能夠緩解 OS Cache 中瞬間緩存太多文件髒數據的問題,但會引發 WT Cache 頻繁 evict page 和頻繁的 leader-follower 線程上下文切換。並且這種機制也依賴於 OS page Cache 的刷盤週期,週期太長效果不明顯。
  4. 用多個磁盤來存儲,redo log 文件放在一個單獨的機械磁盤上,數據放在單獨一個磁盤上,避免 redo log 與 checkpoint 刷盤發生競爭。
  5. 有條件的話,換成將磁盤換成 SSD 吧。這一點比較難,mongoDB 如今也大量使用在 OLAP 和大數據存儲,而高速寫的場景都發生這些場景,成本是個問題。若是是 OLTP 建議用 SSD。

這些方法只能緩解讀寫事務掛起的問題,不能說完全解決這個問題,WT 引擎發展很快,開發團隊正對 WT eviction Cache 和 checkpoint 正在作優化,這個問題慢慢變得再也不是問題,尤爲是 WT-2.8 版本,大量的模型和代碼優化都是集中在這個問題上。

後記

WT 的 eviction Cache 可能有不少不完善的地方,也確實給咱們在使用的過程形成了一些困撓,應該用中立的角度去看待它。能夠說它的讀寫併發速度是其餘數據庫引擎不能比的,正是因爲它很快,纔會有寫掛起的問題,由於磁盤的速度就那麼快。以上的分析和建議或許對碰到相似問題的同窗有用。

WT 團隊的研發速度也很快,每一年會發布 2 到 3 個版本,這類問題是他們正在重點解決的問題。在國內也有不少 mongoDB 這方面相關的專家,他們在解決此類問題有很是豐富的經驗,也能夠請求他們來幫忙解決這類問題。

在本文問題分析過程當中獲得了阿里雲張友東的幫助,在此表示感謝。

參考閱讀

  • 有關並行化——你知道的多是錯的
  • 7-10倍寫入性能提高:剖析WiredTiger數據頁無鎖及壓縮黑科技
  • MongoDB 新存儲引擎 WiredTiger 實現(事務篇)
  • MongoDB 2015 回顧:全新里程碑式的 WiredTiger 存儲引擎

對 WiredTiger 及 MongoDB 引擎設計及使用感興趣的同窗,歡迎在本文留言,介紹對 WiredTiger/MongoDB 的使用及瞭解,咱們將邀請評論中有意向的同窗與本文做者及業內相關專家在 『高可用架構—WiredTiger/MongoDB』 微信羣進行交流。

技術原創及架構實踐文章,歡迎經過公衆號菜單「聯繫咱們」進行投稿。轉載請註明來自高可用架構「ArchNotes」微信公衆號及包含如下二維碼。

高可用架構

改變互聯網的構建方式

MongoDB 3.0掛起緣由?WiredTiger實現:一個LRU cache深坑引起的分析

相關文章
相關標籤/搜索