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 。遷移數據以後的刪除也會有以下的日誌:app

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 分鐘左右。less

統計最近2個週期,開啓 balancer 之後 moveChunk 的狀況:運維

從上表可知此場景下, {_id : hashed} 分片類型集合數據基本已經均勻了,沒必要重啓開啓 balancer 。由於 每一個chunk 文檔數較多,刪除會比較耗資源。異步

關閉表的 balancer 能夠解決升級以後負載升高的問題,可是居然是爲什麼升級到 4.0 以後 CPU 負載較高, 而 3.2 版本穩定在低位呢?這隻有多是一個緣由:4.0 版本更頻繁的發生 moveChunk, 持續的刪除數據致使 CPU 負載一直較高;3.2 版本較少的發生 moveChunk,不用刪除數據因此負載很低。ide

因此本次問題的根本是: 4.0 版本和 3.2 版本的 balancer 與 moveChunk 的邏輯是否有差異?一樣的操做,爲何 4.0版本的集羣會有較多的 moveChunk ?idea

擼代碼: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對活動的大力支持!

相關文章
相關標籤/搜索