1. 集羣概述
在大數據領域常常提到的4V特徵中,Volume(數據量大)是首當其衝被提及的。
由於單機垂直擴展能力的侷限,水平擴展的方式則顯得更加的靠譜。 MongoDB 自帶了這種能力,可以將數據存儲到多個機器上以提供更大的容量和負載能力。
此外,同時爲了保證數據的高可用,MongoDB 採用副本集的方式來實現數據複製。
一個典型的MongoDB集羣架構會同時採用分片+副本集的方式,如下圖:
1.1 架構說明
數據分片(Shards)
分片用於存儲真正的集羣數據,可以是一個單獨的 Mongod實例,也可以是一個副本集。 生產環境下Shard一般是一個 Replica Set,以防止該數據片的單點故障。
對於分片集合(sharded collection)來說,每個分片上都存儲了集合的一部分數據(按照分片鍵切分),如果集合沒有分片,那麼該集合的數據都存儲在數據庫的 Primary Shard中。
配置服務器(Config Servers)
保存集羣的元數據(metadata),包含各個Shard的路由規則,配置服務器由一個副本集(ReplicaSet)組成。
查詢路由(Query Routers)
Mongos是 Sharded Cluster 的訪問入口,其本身並不持久化數據 。Mongos啓動後,會從 Config Server 加載元數據,開始提供服務,並將用戶的請求正確路由到對應的Shard。
Sharding 集羣可以部署多個 Mongos 以分擔客戶端請求的壓力。
2. 分片機制
下面的幾個細節,對於理解和應用 MongoDB 的分片機制比較重要,所以有必要提及一下:
2.1 數據如何切分
首先,基於分片切分後的數據塊稱爲 chunk,一個分片後的集合會包含多個 chunk,每個 chunk 位於哪個分片(Shard) 則記錄在 Config Server(配置服務器)上。
Mongos 在操作分片集合時,會自動根據分片鍵找到對應的 chunk,並向該 chunk 所在的分片發起操作請求。
數據是根據分片策略來進行切分的,而分片策略則由 分片鍵(ShardKey)+分片算法(ShardStrategy)組成。
MongoDB 支持兩種分片算法:
2.1.1 範圍分片
如上圖所示,假設集合根據x字段來分片,x的取值範圍爲[minKey, maxKey](x爲整型,這裏的minKey、maxKey爲整型的最小值和最大值),將整個取值範圍劃分爲多個chunk,每個chunk(默認配置爲64MB)包含其中一小段的數據:
如Chunk1包含x的取值在[minKey, -75)的所有文檔,而Chunk2包含x取值在[-75, 25)之間的所有文檔...
範圍分片能很好的滿足範圍查詢的需求,比如想查詢x的值在[-30, 10]之間的所有文檔,這時 Mongos 直接能將請求路由到 Chunk2,就能查詢出所有符合條件的文檔。 範圍分片的缺點在於,如果 ShardKey 有明顯遞增(或者遞減)趨勢,則新插入的文檔多會分佈到同一個chunk,無法擴展寫的能力,比如使用_id作爲 ShardKey,而MongoDB自動生成的id高位是時間戳,是持續遞增的。
2.1.2 哈希分片
Hash分片是根據用戶的 ShardKey 先計算出hash值(64bit整型),再根據hash值按照範圍分片的策略將文檔分佈到不同的 chunk。
由於 hash值的計算是隨機的,因此 Hash 分片具有很好的離散性,可以將數據隨機分發到不同的 chunk 上。 Hash 分片可以充分的擴展寫能力,彌補了範圍分片的不足,但不能高效的服務範圍查詢,所有的範圍查詢要查詢多個 chunk 才能找出滿足條件的文檔。
2.2 如何保證均衡
如前面的說明中,數據是分佈在不同的 chunk上的,而 chunk 則會分配到不同的分片上,那麼如何保證分片上的 數據(chunk) 是均衡的呢?
在真實的場景中,會存在下面兩種情況:
- A. 全預分配,chunk 的數量和 shard 都是預先定義好的,比如 10個shard,存儲1000個chunk,那麼每個shard 分別擁有100個chunk。此時集羣已經是均衡的狀態(這裏假定)
- B. 非預分配,這種情況則比較複雜,一般當一個 chunk 太大時會產生分裂(split),不斷分裂的結果會導致不均衡;或者動態擴容增加分片時,也會出現不均衡的狀態。 這種不均衡的狀態由集羣均衡器進行檢測,一旦發現了不均衡則執行 chunk數據的搬遷達到均衡。
MongoDB 的數據均衡器運行於 Primary Config Server(配置服務器的主節點)上,而該節點也同時會控制 Chunk 數據的搬遷流程。
對於數據的不均衡是根據兩個分片上的 Chunk 個數差異來判定的,閾值對應表如下:
Number of Chunks | Migration Threshold |
---|---|
Fewer than 20 | 2 |
20-79 | 4 |
80 and greater | 8 |
MongoDB 的數據遷移對集羣性能存在一定影響,這點無法避免,目前的規避手段只能是將均衡窗口對齊到業務閒時段。
2.3 應用高可用
應用節點可以通過同時連接多個 Mongos 來實現高可用,如下:
當然,連接高可用的功能是由 Driver 實現的。
3. 副本集
副本集又是另一個話題,實質上除了前面架構圖所體現的,副本集可以作爲 Shard Cluster 中的一個Shard(片)之外,對於規模較小的業務來說,也可以使用一個單副本集的方式進行部署。
MongoDB 的副本集採取了一主多從的結構,即一個Primary Node + N* Secondary Node的方式,數據從主節點寫入,並複製到多個備節點。
典型的架構如下:
利用副本集,我們可以實現:
- 數據庫高可用,主節點宕機後,由備節點自動選舉成爲新的主節點;
- 讀寫分離,讀請求可以分流到備節點,減輕主節點的單點壓力。
請注意,讀寫分離只能增加集羣"讀"的能力,對於寫負載非常高的情況卻無能爲力。
對此需求,使用分片集羣並增加分片,或者提升數據庫節點的磁盤IO、CPU能力可以取得一定效果。
2.3.1 選舉
MongoDB 副本集通過 Raft 算法來完成主節點的選舉,這個環節在初始化的時候會自動完成,如下面的命令:
config = {
_id : "my_replica_set",
members : [
{_id : 0, host : "rs1.example.net:27017"},
{_id : 1, host : "rs2.example.net:27017"},
{_id : 2, host : "rs3.example.net:27017"},
]
}
rs.initiate(config)
initiate 命令用於實現副本集的初始化,在選舉完成後,通過 isMaster()命令就可以看到選舉的結果:
> db.isMaster()
{
"hosts" : [
"192.168.100.1:27030",
"192.168.100.2:27030",
"192.168.100.3:27030"
],
"setName" : "myReplSet",
"setVersion" : 1,
"ismaster" : true,
"secondary" : false,
"primary" : "192.168.100.1:27030",
"me" : "192.168.100.1:27030",
"electionId" : ObjectId("7fffffff0000000000000001"),
"ok" : 1
}
受 Raft算法的影響,主節點的選舉需要滿足"大多數"原則,可以參考下表:
投票成員數 | 大多數 |
---|---|
1 | 1 |
2 | 2 |
3 | 2 |
4 | 3 |
5 | 3 |
因此,爲了避免出現平票的情況,副本集的部署一般採用是基數個節點,比如3個,正所謂三人行必有我師..
2.3.2 心跳
在高可用的實現機制中,心跳(heartbeat)是非常關鍵的,判斷一個節點是否宕機就取決於這個節點的心跳是否還是正常的。
副本集中的每個節點上都會定時向其他節點發送心跳,以此來感知其他節點的變化,比如是否失效、或者角色發生了變化。
利用心跳,MongoDB 副本集實現了自動故障轉移的功能,如下圖:
默認情況下,節點會每2秒向其他節點發出心跳,這其中包括了主節點。 如果備節點在10秒內沒有收到主節點的響應就會主動發起選舉。
此時新一輪選舉開始,新的主節點會產生並接管原來主節點的業務。 整個過程對於上層是透明的,應用並不需要感知,因爲 Mongos 會自動發現這些變化。
如果應用僅僅使用了單個副本集,那麼就會由 Driver 層來自動完成處理。
2.3.3 複製
主節點和備節點的數據是通過日誌(oplog)複製來實現的,這很類似於 mysql 的 binlog。
在每一個副本集的節點中,都會存在一個名爲local.oplog.rs的特殊集合。 當 Primary 上的寫操作完成後,會向該集合中寫入一條oplog,而 Secondary 則持續從 Primary 拉取新的 oplog 並在本地進行回放以達到同步的目的。
下面,看看一條 oplog 的具體形式:
{
"ts" : Timestamp(1446011584, 2),
"h" : NumberLong("1687359108795812092"),
"v" : 2,
"op" : "i",
"ns" : "test.nosql",
"o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" }
}
其中的一些關鍵字段有:
- ts 操作的 optime,該字段不僅僅包含了操作的時間戳(timestamp),還包含一個自增的計數器值。
- h 操作的全局唯一表示
- v oplog 的版本信息
- op 操作類型,比如 i=insert,u=update..
- ns 操作集合,形式爲 database.collection
- o 指具體的操作內容,對於一個 insert 操作,則包含了整個文檔的內容
MongoDB 對於 oplog 的設計是比較仔細的,比如:
- oplog 必須保證有序,通過 optime 來保證。
- oplog 必須包含能夠進行數據回放的完整信息。
- oplog 必須是冪等的,即多次回放同一條日誌產生的結果相同。
- oplog 集合是固定大小的,爲了避免對空間佔用太大,舊的 oplog 記錄會被滾動式的清理。