近年來,互聯網數據出現了爆炸式增長,單機數據庫在容量和性能上往往難以滿足各個互聯網服務的需求。在此背景下,很多數據庫通過支持橫向擴展能力來滿足業務需求,通過分片的方式將數據打散到多臺服務器上,使得整體性能和容量得到成倍提升。
MongoDB 從(cong)最(zui)初設計(ji)上考慮到了海(hai)量(liang)數據的需求(qiu),因此原(yuan)生就支持分片(pian)(pian)集群。本文(wen)將(jiang)對 MongoDB 的分片(pian)(pian)原(yuan)理進行分析,闡述分片(pian)(pian)架構和實(shi)現(xian)原(yuan)理,并(bing)說明使(shi)用分片(pian)(pian)的注意事項。
MongoDB 分片(pian)架構
MongoDB 的分(fen)片架構如下,包含(han) 3 種角色:
- mongos: 作為分片集群(qun)的(de)接(jie)入(ru)層,接(jie)受用(yong)戶的(de)讀寫請(qing)求,并根據路由轉發到底層 shard. 1 個分片集群(qun)可(ke)以有 1 個或者(zhe)多(duo)個 mongos 節(jie)點。
- shard:1 個 shard 存儲了一部分分片表的(de)數據,1 個分片集(ji)群(qun)可以有 1 個或者多(duo)個 shard. 每(mei)個 shard 有多(duo)個 mongod 節(jie)點組成的(de)副本集(ji)構(gou)成。
- config servers: 存儲(chu)配置元(yuan)數(shu)據以(yi)及分片(pian)表的(de)(de)路由(you)等(deng)信(xin)息(xi)。 1 個(ge)分片(pian)集(ji)群有(you) 1 個(ge) config servers, config servers 是由(you)多個(ge) mongod 節點組(zu)成的(de)(de)副本集(ji)。

從 0 開(kai)始探究分(fen)片原(yuan)理
分(fen)片無非是(shi)選(xuan)好 shardKey 之后(hou),按照(zhao)某種(zhong)規則將數據均(jun)(jun)勻分(fen)布到多個(ge)shard 中,保(bao)證每個(ge) shard 的數據量(liang)和請求量(liang)基本(ben)均(jun)(jun)衡。因此,本(ben)質上來說要解決 2 個(ge)核心問題:
- 如(ru)何(he)選擇 shardKey?
- 如(ru)何(he)選取(qu)合適(shi)的分片算法(fa)?
ShardKey 選取
分片鍵(jian)的(de)選取需要(yao)保障數(shu)據分布足夠離散,每個(ge)分片的(de)存儲(chu)容量(liang)均衡(heng),并(bing)能夠將(jiang)數(shu)據操作均勻分發到集(ji)群中的(de)所有分片中。
如果(guo)分片(pian)鍵選取不佳(jia),可能會導(dao)致(zhi)各個分片(pian)負(fu)載(zai)不均,出現 jumbo chunk 導(dao)致(zhi)無法分裂等問題。而且分片(pian)鍵一旦(dan)確定之后(hou),不能在(zai)運行過程中進(jin)行變(bian)更,需要按(an)新分片(pian)鍵創(chuang)建(jian)新表后(hou)重新導(dao)入數據(4.4及(ji)以上版本支(zhi)持 shardKey 變(bian)更,但(dan)是并不建(jian)議做這(zhe)種在(zai)線操作)。
一般選取分片(pian)鍵時,會考慮以(yi)下因素:
1. 分片鍵的區分度
分(fen)片鍵的取(qu)值(zhi)基數(shu)決定(ding)了最(zui)多能(neng)包(bao)含多少個 chunk,如果取(qu)值(zhi)基數(shu)太(tai)小(xiao),則會導致 chunk 數(shu)量很(hen)低,可能(neng)會有負載不均的問題。
比如按照“性別”來設置(zhi)分片鍵就不是合理的選擇,因為(wei) “性別” 只有 “男”、“女” 2 種取(qu)值,這樣最(zui)多(duo) 2 個 chunk.
2. 分片鍵(jian)的(de)取值分布是否均勻(yun)
如果分(fen)片(pian)鍵的(de)取(qu)值存(cun)在熱點,也可能導致分(fen)片(pian)負載(zai)不均(jun)。比(bi)如以 “國家” 作為片(pian)建,會由(you)于(yu)各個(ge)國家之間人(ren)(ren)口(kou)的(de)差異(yi)出現負載(zai)不均(jun),人(ren)(ren)口(kou)多(duo)的(de)國家存(cun)儲(chu)量大請(qing)求多(duo),而(er)人(ren)(ren)口(kou)少的(de)國家存(cun)儲(chu)量小請(qing)求少。
對于這種場景,可以考慮使用復合鍵(jian)來作為分片鍵(jian),降低(di)出現熱點的概率。
3. 是否會(hui)按照分片鍵單調寫(xie)入
如果(guo)業務按照分片(pian)鍵遞(di)增或者遞(di)減(jian)的趨勢進行讀(du)寫,則可能在(zai)某(mou)(mou)一(yi)時(shi)刻請求集中在(zai)某(mou)(mou)個(ge) chunk 上,無法發揮(hui)集群多(duo)個(ge)分片(pian)的優勢。
比如對于存儲(chu)日志的場(chang)景,如果按照(zhao)日志創建時間進行范圍分(fen)(fen)片(pian),則在某(mou)一時間內一直對同(tong)一個(ge) chunk 進行寫入。對于這(zhe)種場(chang)景,可以考慮復合分(fen)(fen)片(pian)鍵或(huo)者哈(ha)希(xi)分(fen)(fen)片(pian)來(lai)避(bi)免這(zhe)種情況(kuang)。
4. 查詢模型是否(fou)包含分片鍵(jian)
在確定分片(pian)(pian)鍵(jian)后,需要考慮業(ye)務的(de)查(cha)詢(xun)請(qing)(qing)求(qiu)中是否包含有(you)分片(pian)(pian)鍵(jian)。mongos 節點會根據查(cha)詢(xun)請(qing)(qing)求(qiu)中的(de)分片(pian)(pian)鍵(jian)將(jiang)請(qing)(qing)求(qiu)轉發到(dao)對應的(de) shard server 中。如果(guo)查(cha)詢(xun)請(qing)(qing)求(qiu)中不包含分片(pian)(pian)鍵(jian),則(ze) mongos 節點會將(jiang)請(qing)(qing)求(qiu)廣播到(dao)后端的(de)所有(you) shard server,進行(xing) scatter/gather 模式的(de)查(cha)詢(xun),此時查(cha)詢(xun)性能(neng)會有(you)所下(xia)降。
分片算法
傳統的(de) Hash
提到將數據(ju)打散,往(wang)往(wang)第(di)一個想到的(de)就是(shi) hash. 比如有 N 個 shard,可以通過 Hash(shardKey) mod N 的(de)方式(shi),將每(mei)條數據(ju)按照 shardKey 映射到 編(bian)號(hao)為 [0, N-1] 的(de) shard 中(zhong)。
這種簡單 hash 方式雖然(ran)將(jiang)數據打散的(de)非常均勻,但是無(wu)法處理增加和刪除 shard 的(de)問題,一旦(dan)出現 shard 變(bian)更(geng),所有(you)數據都要重新分布。
在實(shi)際的業務場景中,增加和(he)刪除 shard 是非常常見的操作(zuo)。因此,一般很(hen)少(shao)直(zhi)接使用(yong)簡單(dan) hash 的方式(shi)。
傳統(tong)的一致(zhi)性 Hash
一致性 hash 算法能夠解決簡單 hash 在 shard 數(shu)量變動時,所有數(shu)據需要重新分布(bu)的問題。
其基本思路(lu)時(shi)將hash 后(hou)的(de)線性空間(jian)當作一個環,然后(hou)根據 shard 個數在環上分(fen)配相(xiang)應的(de) node,每條數據根據 shardKey 計算(suan)出的(de) hash 值找到距離(li)最近的(de)弄得,而且 node 值不(bu)大于 hash 值。
如果新增(zeng)或者刪除 shard,會(hui)導致(zhi) node 發(fa)生變化,但是(shi)只有部(bu)分(fen)涉及到的數據需要重新分(fen)布。相比簡單 hash 方式,對(dui)系統的影響相對(dui)較小。
另外,通過(guo)引入虛擬 node,可以將重新分(fen)(fen)布時的數據(ju)遷移操(cao)作分(fen)(fen)散到多個節點,并且盡量保證均勻。
不過盡管如此,在增(zeng)加和刪除 node 時,仍然會出(chu)現數據不均衡的情況。
MongoDB 的 Range 分片
MongoDB 引入了(le) chunk 的(de)概念,作(zuo)(zuo)為分片、路(lu)由以及(ji)數(shu)(shu)據(ju)遷移的(de)單元。 1 個(ge)(ge)(ge) chunk 可以看(kan)作(zuo)(zuo)是(shi)(shi)分片表中(zhong) shardKey 連續排列的(de)數(shu)(shu)據(ju)集合(he),是(shi)(shi)一個(ge)(ge)(ge)邏輯概念,各個(ge)(ge)(ge) chunk 在底層存儲引擎(qing)中(zhong)并不(bu)是(shi)(shi)分開存儲。 1 個(ge)(ge)(ge) chunk 默認是(shi)(shi) 64MB,用(yong)戶也可以根據(ju)實際需要自(zi)(zi)行調整。1個(ge)(ge)(ge) chunk 中(zhong)的(de)數(shu)(shu)據(ju)數(shu)(shu)據(ju)總大小超過 64MB 會(hui)自(zi)(zi)動進行分裂,如果各個(ge)(ge)(ge) shard 之間 chunk 的(de)個(ge)(ge)(ge)數(shu)(shu)不(bu)均勻(yun),會(hui)自(zi)(zi)動觸發負載(zai)均衡(heng)操(cao)作(zuo)(zuo)。
以 shardKey 是 “x” 為例(li),其取值(zhi)空間由 1 個或(huo)者多個 chunk 組成(cheng):

需(xu)要注意(yi)的(de)是,1個(ge)(ge) chunk 只能分(fen)布(bu)在某(mou)1個(ge)(ge) shard中。如(ru)果讀請求是一個(ge)(ge)小范圍的(de)查詢,則 mongos 只需(xu)要將請求轉(zhuan)發(fa)到對(dui)應的(de) shard 上,這樣能充分(fen)利用 range 分(fen)片的(de)優勢(shi)。如(ru)果涉及(ji)多個(ge)(ge) shard, 則 mongos 需(xu)要使用 scatter/gather 方式(shi)和多個(ge)(ge) shard 交互。
MongoDB 的(de) Hash 分片
如果用戶的(de)(de)寫入(ru)(ru)操(cao)作(zuo)存在(zai)按 shardKey 的(de)(de)單調(diao)性,比如以時(shi)間(jian)戳(chuo)為 shardKey并按照時(shi)間(jian)順序寫入(ru)(ru)。此時(shi)會導致寫入(ru)(ru)操(cao)作(zuo)始終集中在(zai) shardKey 最大的(de)(de) chunk 上,壓力始終在(zai)某一個(ge) shard 中,而且會觸發大量的(de)(de)分裂和遷移操(cao)作(zuo)。對于 range 分片來說簡直就是災(zai)難。
MongoDB 的(de)(de) hash 分(fen)片方式能夠(gou)很好的(de)(de)解決這(zhe)個(ge)問題。和(he) range 分(fen)片不同,shardKey 首先會經過 hash函(han)數(md5)計算出一(yi)個(ge)整數類型的(de)(de) hash 值,然(ran)后根據這(zhe)個(ge) hash 值分(fen)散到(dao)對應的(de)(de) chunk 和(he) shard 上(shang)。如下圖所示。

Hash 函數(shu)對用戶(hu)來說是不感知的,而(er)且代碼中(zhong)固定為 md5 算法。
MongoDB 的 hash 分片算(suan)法和一致(zhi)性 hash 算(suan)法有點類(lei)似,但是不(bu)同(tong)之處在(zai)于(yu) chunk 分裂(lie)機制(zhi)和自動(dong)負載均衡(heng)機制(zhi)使(shi)得(de)數據(ju)分布理(li)論上(shang)更(geng)加均勻(yun)。
MongoDB 的 Zone 分區
MongoDB 可(ke)以(yi)根據 shardKey 對 shard 設(she)置(zhi) zone 屬(shu)性(xing),比如對于(yu)多地域部署場景,可(ke)以(yi)將(jiang)(jiang)華南機(ji)房的(de) shard 設(she)置(zhi)為 “South” zone,將(jiang)(jiang)華北機(ji)房的(de) shard 設(she)置(zhi)為 “North” zone,通過(guo)這(zhe)種(zhong)方式可(ke)以(yi)將(jiang)(jiang)對應的(de)數(shu)據限定在特定的(de) shard 中存儲。

Zone 必須按照 shardKey 或者 shardKey 的(de)前綴進行(xing)設置。屬于(yu)某個(ge)(ge) zone 的(de)數據,在數據遷移時(shi)只(zhi)會在同一個(ge)(ge) zone 的(de)多個(ge)(ge)shard 之間移動(dong)。
路由管理(li)
無論是 range 分(fen)片還是 hash 分(fen)片,都至少(shao)需(xu)要維護以下 2 種信(xin)息:
- 表(biao)屬性元(yuan)數據。包括 shardKey、分片(pian)算法、創(chuang)建時間以及是(shi)否刪(shan)除等。
- 路由信息。描述(shu)了每(mei)個 chunk 的取值范(fan)圍、存儲在哪個 shard、版本(ben)號(hao)信息等。
MongoDB 分片集群(qun)在 config server 節(jie)點上使用 config 庫存(cun)(cun)儲(chu)這(zhe)些信(xin)息(xi),其中 config.collections 表(biao)存(cun)(cun)儲(chu)了各個分片表(biao)的屬性(xing)元數據,config.chunks 表(biao)存(cun)(cun)儲(chu)了各個分片表(biao)的路由信(xin)息(xi)。
比(bi)如在一個 4.0 版本分(fen)片集群(qun)中(zhong),采用(yong) hash分(fen)片方式創建了一個分(fen)片表 db2.shardcoll1, 則 config.collections 存儲(chu)的屬性元數(shu)據(ju)為:
mongos> use config
switched to db config
mongos> db.collections.find({ "_id" : "db2.shardcoll1"}).pretty()
{
	"_id" : "db2.shardcoll1",  // 表名
	"lastmodEpoch" : ObjectId("64c725bcdae50f066c478c7f"),  // 創建時間
	"lastmod" : ISODate("1970-02-19T17:02:47.297Z"),
	"dropped" : false,   // 是否被刪除
	"key" : {   // shardKey 和分片算法
		"a" : "hashed"
	},
	"unique" : false,   // shardKey是否具有唯一屬性
	"uuid" : UUID("0eafa40d-2b84-4326-9033-2cf8923a8c18")   // 唯一 ID
}采用 hash 方式創建(jian)分片表(biao)后(hou),默認(ren)會生成 2 個 chunk,路由表(biao)為(wei):
mongos> use config
switched to db config
mongos> db.chunks.find({"ns" : "db2.shardcoll1"}).pretty()
{  // 第 1 個 chunk
	"_id" : "db2.shardcoll1-a_MinKey",  // 以表名和 chunk 的最小值作為 id
	"ns" : "db2.shardcoll1",   // 表名
	"min" : {  // 本 chunk 的最小值為 $minKey (包含)
		"a" : { "$minKey" : 1 }
	},
	"max" : {  // 本 chunk 的最大值為 0 (不包含)
		"a" : NumberLong(0)
	},
	"shard" : "sharding_shard1",  // 本 chunk 存儲在 sharding_shard1 上
	"lastmod" : Timestamp(1, 0),  // 版本號
	"lastmodEpoch" : ObjectId("64c725bcdae50f066c478c7f"),  // 最近修改時間
	"history" : [  // 歷史記錄
		{
			"validAfter" : Timestamp(1690772924, 6),
			"shard" : "sharding_shard1"
		}
	]
}
{
	"_id" : "db2.shardcoll1-a_0",  // 以表名和 chunk 的最小值作為 id
	"ns" : "db2.shardcoll1",  // 表名
	"min" : {  // 本 chunk 的最小值為 0 (包含)
		"a" : NumberLong(0)
	},
	"max" : {  // 本 chunk 的最大值為 $maxKey (不包含)
		"a" : { "$maxKey" : 1 }
	},
	"shard" : "sharding_shard1",  // 本 chunk 存儲在 sharding_shard1 上
	"lastmod" : Timestamp(1, 1),   // 版本號
	"lastmodEpoch" : ObjectId("64c725bcdae50f066c478c7f"),   // epoch 信息,最近修改時間
	"history" : [  // 歷史記錄
		{
			"validAfter" : Timestamp(1690772924, 6),
			"shard" : "sharding_shard1"
		}
	]
}對于數據量很大的分片表(biao)(biao)來(lai)說,其(qi)路由表(biao)(biao)也是非常大的。按照 1 個(ge)(ge) chunk 默認 64 MB 來(lai)計算(suan),1個(ge)(ge) 6TB 的分片表(biao)(biao)對應(ying)的路由表(biao)(biao)會有 10 萬多(duo)條記錄。而且隨著(zhu)數據不斷(duan)寫入,chunk 不斷(duan)分(fen)裂使得(de)路由(you)表不斷(duan)更新,如何感知路由(you)變化以及快(kuai)速進行路由(you)同(tong)步就成了(le)非常關鍵的問題。
路由表(biao)版本(ben)
MongoDB 采(cai)用版本控(kong)制方式來追蹤路由變(bian)化,每個 chunk 的路由版本信息由 3 部分組(zu)成:
- Major Version, 主(zhu)版(ban)本號,如果(guo)出現了遷移(yi)則 version 增加。
- Minor Version, 小(xiao)版(ban)本,如(ru)果出現了 split 分裂則(ze) version 增加。
- Epoch,ObjectId 類(lei)型的唯(wei)一 ID
根據上(shang)述 chunk version 派生(sheng)出 2 個概念:
- Shard Version, 某個 shard 上維護的最高的 chunk version.
- Collection Version, 分(fen)片表全局最高的 chunk version.
路由信息同(tong)步
Config server 上存儲的一定是最(zui)新(xin)而且(qie)最(zui)權威的路由(you)信息,mongos 和 mongod 的路由(you)信息都從 config server 增量獲取。
對于 mongos 節(jie)點會(hui)(hui)在(zai)內(nei)存(cun)中(zhong)維(wei)護路(lu)由(you)(you)信息(xi)(xi),啟動時沒有路(lu)由(you)(you)信息(xi)(xi),會(hui)(hui)從 config server 上做(zuo)一次全量拉取(qu)。對于 mongod 節(jie)點,會(hui)(hui)在(zai)內(nei)存(cun)中(zhong)維(wei)護路(lu)由(you)(you)信息(xi)(xi),而且會(hui)(hui)在(zai) config.cache.chunks.{ns} 表(biao)中(zhong)緩存(cun)路(lu)由(you)(you)信息(xi)(xi),而且這(zhe)個緩存(cun)路(lu)由(you)(you)信息(xi)(xi)的表(biao)會(hui)(hui)進行主(zhu)從同步。
路(lu)由(you)(you)信(xin)息的(de)更新整體(ti)上(shang)采用懶加載的(de)方式。具體(ti)來說,mongos 轉發給 mongod 的(de)請(qing)求會額外攜帶mongos 認為的(de)路(lu)由(you)(you)版本信(xin)息,mongod 在收到(dao)請(qing)求之后(hou)會先進行路(lu)由(you)(you)版本比較。如果發現有某一(yi)方的(de)版本比較舊,則先從(cong) config server 上(shang)拉(la)取到(dao)最新路(lu)由(you)(you)之后(hou)再(zai)進行請(qing)求重(zhong)試。如果 2 邊的(de)路(lu)由(you)(you)版本一(yi)樣,則正(zheng)常執(zhi)行請(qing)求。
Chunk 分裂
對于 4.0 及以下版本(ben),會在(zai) mongos 節點(dian)上記錄(lu)每個 chunk 的寫入(ru)情況(kuang)。對于 4.2 及以上版本(ben),會在(zai) mongod 節點(dian)上記錄(lu) chunk 的大小。
如果 chunk 的(de)大小(xiao)達到了設定的(de)閾值(默認 64MB),則會觸發 chunk 分裂。分為 2 個步驟:
- SplitVector,確定分裂點。大(da)致步驟為根(gen)據 shardKey 索(suo)引掃描(miao)出(chu) chunk 的文檔條(tiao)數以及平均文檔長度信息,然后(hou)將 chunk 劃(hua)分成(cheng)滿足(zu)閾值大(da)小的多個子 chunk。
- SplitChunk, 執行 chunk 分(fen)裂。由于(yu) chunk 只(zhi)是一個邏輯上(shang)的概念,因此不(bu)會涉(she)及(ji)存儲(chu)引(yin)擎(qing)側(ce)的表(biao)變(bian)更,只(zhi)會涉(she)及(ji)到路由信息變(bian)更。大致流程為(wei)將分(fen)裂出的chunk 信息提(ti)交到 config server 并(bing)刪除原先(xian)chunk 的路由信息,然后刷(shua)新 mongod 本地的路由表(biao)。
Balancer 數據均衡
Config server 主節點(dian)上會啟動一個后臺(tai)線程定期檢查(默認 2 次(ci)操作之間間隔 10 s)分片(pian)表(biao)的 chunks 數(shu)量在各個分片(pian)之間是(shi)否均衡。是(shi)否均衡的評判標準為chunk 數(shu)量最多的 shard 和 chunk 數(shu)量最少的 shard 之間的差值(zhi)是(shi)否超過了如下(xia)閾值(zhi):

如果滿足(zu)遷(qian)移條件,則(ze) config server 會(hui)通(tong)過(guo) moveChunk 命令,通(tong)知 shard 之間(jian)進行(xing) chunk 遷(qian)移。值得注意的是,chunk 數據移動不會(hui)經過(guo) config server,只(zhi)會(hui)在 shard 之間(jian)進行(xing),config server 只(zhi)會(hui)充(chong)當控制者(zhe)角色。
數據遷移(yi)是非常消耗資源的(de),從運營(ying)經驗來看,很多業(ye)務(wu)的(de)請(qing)求(qiu)毛刺(ci)都(dou)和(he)數據均衡操作有(you)關。因此,對于請(qing)求(qiu)延遲敏感或(huo)資源使用(yong)率(lv)較高的(de)用(yong)戶,盡量(liang)使用(yong) hash 分(fen)片保(bao)證(zheng)數據分(fen)布(bu)均勻,并將數據均衡窗口配(pei)置在業(ye)務(wu)低峰期。
使用 MongoDB 分片集群(qun)的注意(yi)事(shi)項
- 合理選(xuan)擇 shardKey,避免(mian)出現(xian)數據和請求傾斜(xie)的(de)情況,避免(mian)出現(xian) jumbo chunk 導致負載不均(jun)。
- 分片(pian)集(ji)群(qun)盡量(liang)(liang)創建(jian)分片(pian)表(biao)。如果確實需要創建(jian)非分片(pian)表(biao),也盡量(liang)(liang)不要放在(zai)(zai)同一個 db 中,避免出現該 db 所(suo)在(zai)(zai)的 primary shard 數據量(liang)(liang)過大或者負載過高。
- 使用(yong) hash 分(fen)(fen)片時(shi),盡量使用(yong)預分(fen)(fen)片時(shi)創建 initial chunks 的方式,這樣可以避(bi)免(mian)后續的 chunk 分(fen)(fen)裂和(he)遷移。
- 根據業(ye)務需(xu)求,配置合理的 balancer 運行窗口(kou),盡量避(bi)開業(ye)務高峰期。
總結(jie)
MongoDB 原生支持了非常(chang)完善的數(shu)據(ju)分(fen)片能(neng)力,通過(guo)數(shu)據(ju)分(fen)片也(ye)使(shi)得 MongoDB 在(zai)應對海(hai)量數(shu)據(ju)時游刃有余。MongoDB 支持 range 和 hash 分(fen)片方式,并通過(guo)配置 zone 分(fen)區來進(jin)行(xing)更(geng)加靈活(huo)的控(kong)制。
MongoDB 通過(guo)路由版本控制(zhi)來保(bao)證(zheng)路由信息(xi)在多個節點(dian)上的(de)一致(zhi)性,并(bing)通過(guo) chunk 自(zi)動分裂和遷(qian)移(yi)機制(zhi)保(bao)證(zheng)了數據均衡。