Mongos與集羣均衡

版權聲明:本文由孔德雨原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/190mongodb

來源:騰雲閣 https://www.qcloud.com/community架構

 

mongodb 能夠以單複製集的方式運行,client 直連mongod讀取數據。
單複製集的方式下,數據的水平擴展的責任推給了業務層解決(分實例,分庫分表),mongodb原生提供集羣方案,該方案的簡要架構以下:

mongodb集羣是一個典型的去中心化分佈式集羣。mongodb集羣主要爲用戶解決了以下問題:併發

  • 元數據的一致性與高可用(Consistency + Partition Torrence)
  • 業務數據的多備份容災(由複製集技術保證)
  • 動態自動分片
  • 動態自動數據均衡
    下文經過介紹mongodb集羣中各個組成部分,逐步深刻剖析mongodb集羣原理。

ConfigServer

mongodb元數據所有存放在configServer中,configServer 是由一組(至少三個)mongod實例組成的集羣。
configServer 的惟一功能是提供元數據的增刪改查。和大多數元數據管理系統(etcd,zookeeper)相似,也是保證一致性與分區容錯性。自己不具有中心化的調度功能。分佈式

ConfigServer與複製集

ConfigServer的分區容錯性(P)和數據一致性(C)是複製集自己的性質。
MongoDb的讀寫一致性由WriteConcern和ReadConcern兩個參數保證。
writeConcern https://docs.mongodb.com/v3.2/reference/write-concern/
readConcern https://docs.mongodb.com/v3.2/reference/read-concern/
二者組合能夠獲得不一樣的一致性等級。
指定 writeConcern:majority 能夠保證寫入數據不丟失,不會因選舉新主節點而被回滾掉。
readConcern:majority + writeConcern:majority 能夠保證強一致性的讀
readConcern:local + writeConcern:majority 能夠保證最終一致性的讀
mongodb 對configServer所有指定writeConcern:majority 的寫入方式,所以元數據能夠保證不丟失。
對configServer的讀指定了ReadPreference:PrimaryOnly的方式,在CAP中捨棄了A與P獲得了元數據的強一致性讀。高併發

Mongos

數據自動分片

對於一個讀寫操做,mongos須要知道應該將其路由到哪一個複製集上,mongos經過將片鍵空間劃分爲若干個區間,計算出一個操做的片鍵的所屬區間對應的複製集來實現路由。

Collection1 被劃分爲4個chunk,其中
chunk1 包含(-INF,1) , chunk3 包含[20, 99) 的數據,放在shard1上。
chunk2 包含 [1,20), chunk4 包含[99, INF) 的數據,放在shard2上。
chunk 的信息存放在configServer 的mongod實例的 config.chunks 表中,格式以下:測試

{   
    "_id" : "mydb.foo-a_\"cat\"",   
    "lastmod" : Timestamp(1000, 3),  
    "lastmodEpoch" : ObjectId("5078407bd58b175c5c225fdc"),   
    "ns" : "mydb.foo",   
    "min" : {         "animal" : "cat"   },   
    "max" : {         "animal" : "dog"   },   
    "shard" : "shard0004"
}

值得注意的是:chunk是一個邏輯上的組織結構,並不涉及到底層的文件組織方式。spa

啓發式觸發chunk分裂

mongodb 默認配置下,每一個chunk大小爲16MB。超過該大小就須要執行chunk分裂。chunk分裂是由mongos發起的,而數據放在mongod處,所以mongos沒法準確判斷每一個增刪改操做後某個chunk的數據實際大小。所以mongos採用了一種啓發式的觸發分裂方式:
mongos在內存中記錄一份 chunk_id -> incr_delta 的哈希表。
對於insert和update操做,估算出incr_delta的上界(WriteOp::targetWrites), 當incr_delta超過閾值時,執行chunk分裂。
值得注意的是:線程

1) chunk_id->incr_delta 是維護在mongos內存裏的一份數據,重啓後丟失
2) 不一樣mongos之間的這份數據相互獨立
3) 不帶shardkey的update 沒法對 chunk_id->incr_delta 做用日誌

所以這個啓發式的分裂方式很不精確,而除了手工以命令的方式分裂以外,這是mongos自帶的惟一的chunk分裂方式。code

chunk分裂的執行過程

1) 向對應的mongod 發起splitVector 命令,得到一個chunk的可分裂點
2) mongos 拿到這些分裂點後,向mongod發起splitChunk 命令

splitVector執行過程:

1) 計算出collection的文檔的 avgRecSize= coll.size/ coll.count
2) 計算出分裂後的chunk中,每一個chunk應該有的count數, split_count = maxChunkSize / (2 * avgRecSize)
3) 線性遍歷collection 的shardkey 對應的index的 [chunk_min_index, chunk_max_index] 範圍,在遍歷過程當中利用split_count 分割出若干spli

splitChunk執行過程:

1) 得到待執行collection的分佈式鎖(向configSvr 的mongod中寫入一條記錄實現)
2) 刷新(向configSvr讀取)本shard的版本號,檢查是否和命令發起者攜帶的版本號一致
3) 向configSvr中寫入分裂後的chunk信息,成功後修改本地的chunk信息與shard的版本號
4) 向configSvr中寫入變動日誌
5) 通知mongos操做完成,mongos修改自身元數據

chunk分裂的執行流程圖:

問題與思考

問題一:爲什麼mongos在接收到splitVector的返回後,執行splitChunk 要放在mongod執行而不是mongos中呢,爲什麼不是mongos本身執行完了splitChunk再通知mongod 修改元數據?
咱們知道chunk元數據在三個地方持有,分別是configServer,mongos,mongod。若是chunk元信息由mongos更改,則其餘mongos與mongod都沒法第一時間得到最新元數據。可能會發生這樣的問題,以下圖描述:

Mongos對元數據的修改尚未被mongod與其餘mongos感知,其餘mongos與mongod的版本號保持一致,致使其餘mongos寫入錯誤的chunk。

若是chunk元信息由mongod更改,mongod 先於全部的mongos感知到本shard的元數據被更改,因爲mongos對mongod的寫入請求都會帶有版本號(以發起者mongos的POV 持有的版本號),mongod發現一個讀寫帶有的版本號低於自身版本號時就會返回 StaleShardingError,從而避免對錯誤的chunk進行讀寫。

Mongos對讀寫的路由

讀請求:
mongos將讀請求路由到對應的shard上,若是獲得StaleShardingError,則刷新本地的元數據(從configServer讀取最新元數據)並重試。
寫請求:
mongos將寫請求路由到對應的shard上,若是獲得StaleShardingError,並不會像讀請求同樣重試,這樣作並不合理,截至當前版本,mongos也只是列出了一個TODO(batch_write_exec.cpp:185)

185          // TODO: It may be necessary to refresh the cache if stale, or maybe just
186          // cancel and retarget the batch

chunk遷移

chunk遷移由balancer模塊執行,balancer模塊並非一個獨立的service,而是mongos的一個線程模塊。同一時間只有一個balancer模塊在執行,這一點是mongos在configServer中註冊分佈式鎖來保證的。

balancer 對於每個collection的chunk 分佈,計算出這個collection須要進行遷移的chunk,以及每一個chunk須要遷移到哪一個shard上。計算的過程在BalancerPolicy 類中,比較瑣碎。

chunk遷移.Step1

MigrationManager::scheduleMigrations balancer對於每個collection,嘗試得到該collection的分佈式鎖(向configSvr申請),若是得到失敗,代表該collection已有正在執行的搬遷任務。這一點說明對於同一張表統一時刻只能有一個搬遷任務。若是這張表分佈在不一樣的shard上,徹底隔離的IO條件能夠提升併發,不過mongos並無利用起來這一點。
若是得到鎖成功,則向源shard發起moveChunk 命令

chunk遷移.Step2

mongod 執行moveChunk命令
cloneStage
1) 源mongod 根據須要遷移的chunk 的上下限構造好查詢計劃,基於分片索引的掃描查詢。並向目標mongod發起recvChunkStart 指令,讓目標chunk 開始進入數據拉取階段。
2) 源mongod對此階段的修改, 將id字段buffer在內存裏(MigrationChunkClonerSourceLegacy類),爲了防止搬遷時速度過慢buffer無限制增加,buffer大小設置爲500MB,在搬遷過程當中key的更改量超過buffer大小會致使搬遷失敗。
3) 目標mongod 在接收到recvChunkStart命令後

a. 基於chunk的range,將本mongod上的可能髒數據清理掉

b. 向源發起_migrateClone指定,經過1)中構造好的基於分配索引的掃描查詢獲得該chunk 數據的snapshot

c. 拷貝完snapshot後,向源發起_transferMods命令,將2)中維護在內存buffer中的修改

d. 源在收到_transferMods後,經過記錄的objid查詢對應的collection,將真實數據返回給目標。

e. 目標在收完_transferMods 階段的數據後,進入steady狀態,等待源接下來的命令。這裏有必要說明的是:用戶數據源源不斷的寫入,理論上_transferMods 階段會一直有新數據,可是必需要找到一個點截斷數據流,將源的數據(搬遷對應的chunk的數據)設置爲不可寫,才能發起路由更改。所以這裏所說的「_transferMods階段的全部數據」只是針對於某個時間點,這個時間點事後依然會有新數據進來。

f. 源心跳檢查目標是否已經處於steady狀態,若是是,則封禁chunk的寫入,向目標發起_recvChunkCommit命令,以後源的chunk上就無修改了。

g. 目標收到_recvChunkCommit命令後,拉取源chunk上的修改並執行,執行成功後源解禁路由並清理源chunk的數據

流程圖以下:

總結

通過分析,咱們發現Mongos在遷移方面有很大的待提高空間
1) 一張表同一時間只能有一個chunk在搬遷,沒有充分利用不一樣機器之間的IO隔離來作併發提速。
2) 搬遷時須要掃描源的數據集,一方面會與業務爭QPS,一方面會破壞(若是是Mmap引擎)熱點讀寫的working-set
3) Mongos啓發式分裂chunk的方式極不靠譜,mongos重啓後,啓發信息就丟失了,並且部分常見的寫入模式也不會記錄啓發信息

通過CMongo團隊的測試,mongos自帶的搬遷方案處理100GB的數據須要33小時。CMongo團隊分析了mongos自帶的搬遷方案的缺陷,自研了一套基於備份的搬遷方案,速度有30倍以上的提高,敬請期待!

相關文章
相關標籤/搜索