1. 程式人生 > >MongoDB疑難解析:為什麼升級之後負載升高了?

MongoDB疑難解析:為什麼升級之後負載升高了?

本文是“我和MongoDB的故事”徵文比賽的二等獎得主李鵬衝的文章。下面我們一起來欣賞下。

問題

近期線上一個三分片叢集從 3.2 版本升級到 4.0 版本以後,叢集節點的 CPU 的負載升高了很多(10% -> 40%), 除了版本的升級,專案邏輯和操作量均無變化。關閉 Balancer 以後 CPU 負載迴歸正常,穩定在 10% 以下。為此,只能經常關閉當前正在寫入表的 balancer , 每週二開啟 balancer 開啟均衡,在此期間節點的 CPU 負載持續穩定在 40% 。叢集有 3 個分片,除了 MongoDB 版本的變化,專案本身的邏輯無任何變化。那麼升級以後 CPU 負載較大變化的背後是什麼原因呢?

監控與日誌

首先可以明確,升級以後 CPU 負載升高和 balancer 遷移資料有關。觀察升級以後 4.0 版本,週二開啟 balancer 期間的負載情況和 mongostat 結果:

可以發現,CPU 負載升高和 delete 資料的情況很吻合。而遷移資料資料之後源節點需要刪除遷移走的資料,所以肯定有大量的 delete 。遷移資料之後的刪除也會有如下的日誌:

53094:2019-10-08T10:09:24.035199+08:00 I SHARDING [Collection Range Deleter] No documents remain to delete in dt2log.tbl_log_item_20191001 range [{ _id: -3074457345618258602 }, { _ id: -3033667061349287050 })
53095:2019-10-08T10:09:24.035222+08:00 I SHARDING [Collection Range Deleter] Waiting for m ajority replication of local deletions in dt2log.tbl_log_item_20191001 range [{ _id: -3074 457345618258602 }, { _id: -3033667061349287050 })
53096:2019-10-08T10:09:24.035274+08:00 I SHARDING [Collection Range Deleter] Finished dele ting documents in dt2log.tbl_log_item_20191001 range [{ _id: -3074457345618258602 }, { _id
-3033667061349287050 })

所以從監控和日誌判斷, CPU 負載較高主要是因為遷移資料之後的刪除導致。而且叢集的表都是 {_id : hashed} 分片型別的表,資料量較大,但是每條資料較小,平均每個 chunk 10w+ 的文件數,刪除資料速度約 200-300/s ,所以移動一個 chunk 導致的刪除就會持續 10 分鐘左右。

統計最近2個週期,開啟 balancer 以後 moveChunk 的情況:

從上表可知此場景下, {_id : hashed} 分片型別集合資料基本已經均勻了,不必重啟開啟 balancer 。因為 每個chunk 文件數較多,刪除會比較耗資源。

關閉表的 balancer 可以解決升級之後負載升高的問題,但是竟然是為何升級到 4.0 之後 CPU 負載較高, 而 3.2 版本穩定在低位呢?這隻有可能是一個原因:4.0 版本更頻繁的發生 moveChunk, 持續的刪除資料導致 CPU 負載一直較高;3.2 版本較少的發生 moveChunk,不用刪除資料所以負載很低。

所以本次問題的根本是: 4.0 版本和 3.2 版本的 balancer 與 moveChunk 的邏輯是否有差別?同樣的操作,為什麼 4.0版本的叢集會有較多的 moveChunk ?

擼程式碼:splitChunk、balancer與moveChunk

當通過 mongos 發生插入和更新刪除操作時,mongos 會估算對應 chunks 的資料量的大小,滿足條件會觸發splitChunk 的操作,splitChunk 之後可能會導致叢集的 chunk 分佈不均勻。balancer 檢測資料的分佈情況,當資料分配不均勻時,發起 moveChunk 任務,將資料從 chunks 較多的分片遷移到 chunks 較少的分片,遷移之後源節點會非同步刪除遷移走的 chunk 資料。

3.2 版本和 4.0 版本,此部分邏輯最大的區別就是, 3.2 版本 balancer 在 mongos,4.0 版本在 config(3.4版本開始),moveChunk 過程和刪除資料的邏輯基本沒有差異。

splitChunk

split chunks 一般是在插入、更新、刪除資料時,由 mongos 發出到分片的 splitVector 命令,此時分片才會判斷是否需要 split 。但是 mongos 並不知道每個 chunk 真正的資料量,是利用一個簡單的估算演算法判斷的。

  • 啟動時,mongos 預設每個 chunk 的原始大小為 0-1/5 maxChunkSize 範圍取個隨機值 ;

  • 之後 chunk 內資料,每次 update/insert 操作時,chunkSize = chunkSize + docSize;

  • 當 chunkSize > maxChunkSize/5 時,觸發一次可能 split chunk 的操作; 到 分片mongod 執行 splitVector命令 ,splitVector 命令返回 chunk 的分割點,如果返回為空那麼不需要 split ,否則 繼續 splitChunk。

也就是說,splitChunk 操作有滯後性,即使資料分佈均衡,也有可能 splitChunk 執行時間的差異導致 chunks 分佈存在中間的不均勻狀態,導致大量的 moveChunk 。

balancer

無論 3.2 還是 4.0 的 balancer ,預設的檢測週期為 10s , 如果發生了 moveChunk ,檢測週期為 1s 。balancer 基本過程也大致相同:

  • config.shards 讀取分片資訊 ;

  • config.collections 讀取所有集合資訊,並且隨機排序儲存到一個數組中;

  • 對每個集合從 config.chunks 讀取 chunks 的資訊;

含有最多 chunks 數量 (maxChunksNum)的分片為源分片,含有最少 chunks 數量(minChunksNum)的分片為目的分片; 如果 maxChunksNum – minChunksNum 大於遷移的閾值 (threshold), 那麼就是不均衡狀態,需要遷移,源分片的 chunks 第一個 chunk 為待遷移的 chunk ,構造一個遷移任務(源分片,目的分片,chunk)。

每次 balancer 會檢測所有集合的情況,每個集合最多一個遷移任務 ; 而且構造遷移任務時,如果某個集合含有最多數量的分片或者最少數量 chunks 的分片,已經屬於某一個遷移任務,那麼此集合本輪 balancer 不會發生遷移。最後,本次檢測出的遷移任務完成以後才開始下次 balancer 過程。

balancer 過程中,會對集合做一次隨機排序,當有多個集合的資料需要均衡時,遷移時也是隨機的,並不是遷移完一個集合開始下一個集合。

重點關注上述的遷移閾值,就是這個遷移的閾值 threshold 在 3.2 和 4.0 版本有所不同。

3.2 版本, chunks 數量小於 20 的時候為 2, 小於 80 的時候為 4, 大於 80 的時候為 8 。也就是說假設兩分片叢集,某個表有 100 個chunk , 每個分片分別有 47 和 53 個chunk 。那麼此時 balance 認為是均衡的,不會發生遷移。

int threshold = 8;
if (balancedLastTime || distribution.totalChunks() < 20) threshold = 2;
else if (distribution.totalChunks() < 80) threshold = 4;

4.0 版本,chunks 數量差距大於 2 的時候就會發生遷移。同樣的上述例子中,每個分片分別有 47 和 53 個 chunk時, balance 認為是不均衡的,會發生遷移。

const size_t kDefaultImbalanceThreshold = 2; const size_t kAggressiveImbalanceThreshold = 1;
const size_t imbalanceThreshold = (shouldAggressivelyBalance || distribution.totalChunks()
< 20)
? kAggressiveImbalanceThreshold: kDefaultImbalanceThreshold;
// 這裡雖然有個 1 ,但是實際差距為 1 的時候不會發生遷移,因為判斷遷移時,還有一個指標:平均每個分片的最大 ch
unks 數量,只有當 chunks 數量大於這個值的時候才會發生遷移。
const size_t idealNumberOfChunksPerShardForTag = (totalNumberOfChunksWithTag / totalNumberOfShardsWithTag) + (totalNumberOfChunksWithTag % totalNumberOfShardsWithTag ? 1 : 0);

關於此閾值,官方文件也有介紹:

To minimize the impact of balancing on the cluster, the balancer only begins balancing after the distribution of chunks for a sharded collection has reached certain thresholds. The thresholds apply to the difference in number of chunks between the shard with the most chunks for the collection and the shard with the fewest chunks for that collection. The balancer has the following thresholds:

The balancer stops running on the target collection when the difference between the number of chunks on any two shards for that collection is less than two, or a chunk migration fails.

但是從程式碼上,從3.4 版本開始,此閾值的邏輯就已經變化了,但是文件並沒有更新。

moveChunk

moveChunk 是一個比較複雜的動作, 大致過程如下:

目的分片,首先要刪除要移動的 chunk 的資料。所以會有一個刪除任務。

可以在 config.settings 設定 _secondaryThrottle 和 waitForDelete 設定 moveChunk 過程中 插入資料和刪除資料的 write concern

  • _secondaryThrottle: true 表示 balancer 插入資料時,至少等待一個 secondary 節點回復;false 表示不等待寫到 secondary 節點; 也可以直接設定為 write concern ,則遷移時使用這個 write concern . 3.2 版本預設 true, 3.4 開始版本預設 false;

  • waitForDelete: 遷移一個 chunk 資料以後,是否同步等待資料刪除完畢;預設為 false , 由一個單獨的執行緒非同步刪除孤兒資料。

設定方式如下:

use config db.settings.update(
{ "_id" : "balancer" },
{ $set : { "_secondaryThrottle" : { "w": "majority" } ,"_waitForDelete" : true } },
{ upsert : true }
)

3.2 版本 _secondaryThrottle 預設 true, 3.4 開始版本預設 false,所以 3 .2 版本和4.0 版本 moveChunk 遷移資料時,4.0版本會更快完成,遷移中 目的分片的每秒 insert 量級也會更多,對 CPU 負載也會有些許的影響。

另外,3.4.18/3.6.10/4.0.5 及之後版本,還有以下引數 (Parameter) 調整插入資料的速度:

  • migrateCloneInsertionBatchDelayMS: 遷移資料時,每次插入的間隔,預設 0 不等待。

  • migrateCloneInsertionBatchSize: 遷移資料時,每次插入的數量,預設為 0 無限制。

設定方式如下:

db.adminCommand({setParameter:1,migrateCloneInsertionBatchDelayMS:0})
db.adminCommand({setParameter:1,migrateCloneInsertionBatchSize:0})

非同步刪除資料執行緒

3.2 和 4.0 版本的非同步刪除執行緒具體實現略有不同,但是,根本過程還是一致的,用一個佇列儲存需要刪除的 range, 迴圈的取佇列的資料刪除資料。所以非同步刪除資料執行緒是按照 chunk 進入佇列的順序,逐個刪除。總入口:

3.2 版本 db/range_deleter.cpp 執行緒入口 RangeDeleter::doWork()
4.0 版本 db/s/metadata_manager.cpp scheduleCleanup 時會有一個唯一的執行緒執行清理任務

4.0 版本在刪除資料時,按批刪除資料,每次刪除數量計算方式如下:

maxToDelete = rangeDeleterBatchSize.load();
if (maxToDelete <= 0) {
maxToDelete = std::max(int(internalQueryExecYieldIterations.load()), 1); // 128
}

有較多的引數可以靈活的控制刪除速度,預設情況下,900s 以後開始清理 chunks 的資料,每次清理 128 個文件,每隔 20ms 刪除一次。具體通過以下引數設定:

  • rangeDeleterBatchDelayMS: 刪除每個 chunk 資料的時候分批次刪除,每批之間間隔的時間,單位 ms,預設 20ms;

  • internalQueryExecYieldIterations: 預設為 128;

  • rangeDeleterBatchSize:每次刪除資料的數量,預設即為0;為0時 ,則每次刪除的數量為max(internalQueryExecYieldIterations,1),

  • orphanCleanupDelaySecs: moveChunk 以後延遲刪除資料的時間,單位 s ,預設 900 s

總結

  • moveChunk 可能對系統的負載產生影響,主要是刪除資料階段的影響,一般遷移中的插入資料影響較小;

  • 3.4 及之後的版本存在 balancer 遷移閾值較低的問題,可能會更頻繁的產生 moveChunk;

  • 文件資料多而小的表,而且是 hashed 分片,本應預分配一定的 chunk 以後永久關閉表的 balancer。開啟balancer 時,3.2 版本因為均衡閾值較大,較少發生 moveChunk 遷移資料,所以負載較低; 4.0 版本均衡閾值很小,更容易發生遷移,頻繁的遷移之後刪除資料導致負載較高。

作者:李鵬衝

網易遊戲高階運維工程師,MongoDB和MySQL資料庫愛好者,目前專注於SAAS平臺的開發與運維工作。

感謝MongoDB官方,錦木資訊和Tapdata對活動的大力支援