分片邏輯圖
上節搭建的分片集群從邏輯上看如下圖所示:
片:可以普通的mongod進程,也可以是副本集。但是即使一片內有多台服務器,也只能有一個主服務器,其他的服務器保存相同的數據。
mongos路由進程:它路由所有請求,然后將結果聚合。它不保存存儲數據或配置信息。
配置服務器:存儲集群的配置信息。
整個分布式的集群通過mongos對客戶端提供了一個透明統一的接口,客戶端不需要關系具體的分片細節,所有分片的動作都是自動執行的,那是如何做到透明和自動的。
切分數據
上節中建立好集群后,默認的是不會將存儲的每條數據進行分片處理,需要在數據庫和集合的粒度上都開啟分片功能。開啟test庫的分片功能:
1. ./bin/mongo –port 20000 2. mongos> use admin 3. switched to db admin 4. mongos> db.runCommand({"enablesharding":"test"}) 5. { "ok" : 1 }
開啟user集合分片功能:
1. mongos> db.runCommand({"shardcollection":"test.user","key":{"_id":1}}) 2. { "collectionsharded" : "test.user", "ok" : 1 }
注意:需要切換到admin庫執行命令。
片鍵:上面的key就是所謂的片鍵(shard key)。MongoDB不允許插入沒有片鍵的文檔。但是允許不同文檔的片鍵類型不一樣,MongoDB內部對不同類型有一個排序:
片鍵的選擇至關重要,后面會進行詳細的說明。
這時再切換到config庫如下查看:
1. mongos> use config 2. mongos> db.databases.find() 3. { "_id" : "admin", "partitioned" : false, "primary" : "config" } 4. { "_id" : "OSSP10", "partitioned" : false, "primary" : "shard0000" } 5. { "_id" : "test", "partitioned" : true, "primary" : "shard0000" } 6. { "_id" : "test2", "partitioned" : false, "primary" : "shard0000" } 7. { "_id" : "test3", "partitioned" : false, "primary" : "shard0001" } 8. mongos> db.chunks.find() 9. { "_id" : "test.user-_id_MinKey", "lastmod" : { "t" : 1, "i" : 0 }, "lastmodEpoch" : ObjectId("515a3797d249863e35f0e3fe"), "ns" : "test.user", "min" : { "_id" : { "$minKey" : 1 } }, "max" : { "_id" : { "$maxKey" : 1 } }, "shard" : "shard0000" }
Chunks:理解MongoDB分片機制的關鍵是理解Chunks。mongodb不是一個分片上存儲一個區間,而是每個分片包含多個區間,這每個區間就是一個塊。
1. mongos> use config 2. mongos> db.settings.find() 3. { "_id" : "chunksize", "value" : 64 } 4. ……
平衡:如果存在多個可用的分片,只要塊的數量足夠多,MongoDB就會把數據遷移到其他分片上,這個遷移的過程叫做平衡。Chunks默認的大小是64M200M,查看config.settings可以看到這個值:
只有當一個塊的大小超過了64M200M,MongoDB才會對塊進行分割(但根據我的實踐2.4版本塊分割的算法好像並不是等到超過chunkSize大小就分割,也不是一分為二,有待繼續學習),並當最大分片上的塊數量超過另一個最少分片上塊數量達到一定閾值會發生所謂的chunk migration來實現各個分片的平衡 (如果啟動了balancer進程的話)。這個閾值隨着塊的數量不同而不同:
這帶來一個問題是我們測開發測試的時候,往往希望通過觀察數據在遷移來證明分片是否成功,但64M200M顯然過大,解決方法是我們可以在啟動mongos的時候用—chunkSize來制定塊的大小,單位是MB。
1. ./bin/mongos --port 20000 --configdb 192.168.32.13:10000 --logpath log/mongos.log --fork --chunkSize 1
我指定1MB的塊大小重新啟動了一下mongos進程。
或者向下面這要修改chunkSize大小:
1. mongos> use config 2. mongos> db.settings.save( { _id:"chunksize", value: 1 } )
2.4版本之前MongoDB基於范圍進行分片的(2.2.4會介紹基於哈希的分片),對一個集合分片時,一開始只會創建一個塊,這個塊的區間是(-∞,+∞),-∞表示MongoDB中的最小值,也就是上面db.chunks.find()我們看到的$minKey,+∞表示最大值即$maxKey。Chunck的分割是自動執行的,類似於細胞分裂,從區間的中間分割成兩個。
分片測試
現在對上面我們搭建的MongoDB分片集群做一個簡單的測試。目前我們的集群情況如下(為了方面展示我采用了可視化的工具MongoVUE):
像上面提到那樣為了盡快能觀察到測試的效果,我么啟動mongos時指定的chunkSize為1MB。
我們對OSSP10庫和bizuser集合以Uid字段進行分片:
1. mongos> use admin 2. switched to db admin 3. mongos> db.runCommand({"enablesharding":"OSSP10"}) 4. { "ok" : 1 } 5. mongos> db.runCommand({"shardcollection":"OSSP10.bizuser","key":{"Uid":1}}) 6. { "collectionsharded" : "OSSP10.bizuser", "ok" : 1 }
在插入數據之前查看一下config.chunks,通過查看這個集合我們可以了解數據是怎么切分到集群的:
1. mongos> db.chunks.find() 2. { "_id" : "OSSP10.bizuser-Uid_MinKey", "lastmod" : { "t" : 1, "i" : 0 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : { "$minKey" : 1 } }, "max" : { "Uid" : { "$maxKey" : 1 } }, "shard" : "shard0000" }
開始只有一個塊,區間(-∞,+∞)在shard0000(192.168.32.13:27019)上,下面我們循環的插入100000條數據:
1. mongos> use OSSP10 2. switched to db OSSP10 3. mongos> for(i=0;i<100000;i++){ db.bizuser.insert({"Uid":i,"Name":"zhanjindong","Age":13,"Date":new Date()}); }
完成后我們在觀察一下config.chunks的情況:
1. mongos> db.chunks.find() 2. { "_id" : "OSSP10.bizuser-Uid_MinKey", "lastmod" : { "t" : 3, "i" : 0 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : { "$minKey" : 1 } }, "max" : { "Uid" : 0 }, "shard" : "shard0002" } 3. { "_id" : "OSSP10.bizuser-Uid_0.0", "lastmod" : { "t" : 3, "i" : 1 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 0 }, "max" : { "Uid" : 6747 }, "shard" : "shard0000" } 4. { "_id" : "OSSP10.bizuser-Uid_6747.0", "lastmod" : { "t" : 2, "i" : 0 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 6747 }, "max" : { "Uid" : { "$maxKey" : 1 } }, "shard" : "shard0001" }
我們可以看見剛開始的塊分裂成了三塊分別是(-∞,0)在shard0002上,[0,6747)在shard0000上和[6747,+ ∞)在shard0001上。
說明:這里需要說明的是MongoDB中的區間是左閉右開的。這樣說上面第一個塊不包含任何數據,至於為什么還不清楚,有待繼續調研。
我們持續上面的插入操作(uid從0到100000)我們發現塊也在不停的分裂:
1. mongos> db.chunks.find() 2. { "_id" : "OSSP10.bizuser-Uid_MinKey", "lastmod" : { "t" : 3, "i" : 0 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : { "$minKey" : 1 } }, "max" : { "Uid" : 0 }, "shard" : "shard0002" } 3. { "_id" : "OSSP10.bizuser-Uid_0.0", "lastmod" : { "t" : 3, "i" : 1 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 0 }, "max" : { "Uid" : 6747 }, "shard" : "shard0000" } 4. { "_id" : "OSSP10.bizuser-Uid_6747.0", "lastmod" : { "t" : 3, "i" : 4 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 6747 }, "max" : { "Uid" : 45762 }, "shard" : "shard0001" } 5. { "_id" : "OSSP10.bizuser-Uid_99999.0", "lastmod" : { "t" : 3, "i" : 3 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 99999 }, "max" : { "Uid" : { "$maxKey" : 1 } }, "shard" : "shard0001" } 6. { "_id" : "OSSP10.bizuser-Uid_45762.0", "lastmod" : { "t" : 3, "i" : 5 }, "lastmodEpoch" : ObjectId("515a8f1dc1de43249d8ce83e"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 45762 }, "max" : { "Uid" : 99999 }, "shard" : "shard0001" }
分片的規則正是上面提到的塊的自動分裂和平衡,可以發現不同的塊是分布在不同的分片上。
注意:這里使用Uid作為片鍵通常是有問題的,2.3對遞增片鍵有詳細說明。
Note:通過sh.status()可以很直觀的查看當前整個集群的分片情況,類似如下:
對已有數據進行分片
面的演示都是從零開始構建分片集群,但是實際中架構是一個演進的過程,一開始都不會進行分片,只有當數據量增長到一定程序才會考慮分片,那么對已有的海量數據如何處理。我們在上節的基礎上繼續探索。《深入學習MongoDB》中有如下描述:
我們現在來嘗試下(也許我使用的最新版本會有所有變化),先在192.168.71.43:27179上再啟一個mongod進程:
1. numactl --interleave=all ./bin/mongod --dbpath data/shard3/ --logpath log/shard3.log --fork --port 27019
現在我們像這個mongod進程中的OSSP10庫的bizuser集合中插入一些數據:
1. for(i=0;i<10;i++){ db.bizuser.insert({"Uid":i,"Name":"zhanjindong2","Age":13,"Date":new Date()}); }
我們這個時候嘗試將這個mongod加入到之前的集群中去:
1. mongos> db.runCommand({addshard:"192.168.71.43:27019" }) 2. { 3. "ok" : 0, 4. "errmsg" : "can't add shard 192.168.71.43:27019 because a local database 'OSSP10' exists in another shard0000:192.168.32.13:27019" 5. }
果然最新版本依舊不行。我們刪除OSSP10庫,新建個OSSP20庫再添加分片則可以成功。
1. { "shardAdded" : "shard0003", "ok" : 1 }
那么看來我們只能在之前已有的數據的mongod進程基礎上搭建分片集群,那么這個時候添加切片,MongoDB對之前的數據又是如何處理的呢?我們現在集群中移除上面添加的切片192.168.71.43:27019,然后在刪除集群中的OSSP10庫。
這次我們盡量模擬生產環境進行測試,重新啟動mongos並不指定chunksize,使用默認的最優的大小(64M200M)。然后在192.168.71.43:27019中新建OSSP10庫並向集合bizuser中插入100W條數據:
1. for(i=0;i<1000000;i++){ db.bizuser.insert({"Uid":i,"Name":"zhanjindong2","Age":13,"Date":new Date()}); }
然后將此節點添加到集群中,並仍然使用遞增的Uid作為片鍵對OSSP10.bizuser進行分片:
1. mongos> use admin 2. switched to db admin 3. mongos> db.runCommand({"enablesharding":"OSSP10"}) 4. { "ok" : 1 } 5. mongos> db.runCommand({"shardcollection":"OSSP10.bizuser","key":{"Uid":1}}) 6. { "collectionsharded" : "OSSP10.bizuser", "ok" : 1 }
觀察一下config.chunks可以看見對新添加的切片數據進行切分並進行了平衡(每個分片上一個塊),基本是對Uid(0~1000000)進行了四等分,當然沒那精確:
1. mongos> use config 2. switched to db config 3. mongos> db.chunks.find() 4. { "_id" : "OSSP10.bizuser-Uid_MinKey", "lastmod" : { "t" : 2, "i" : 0 }, "lastmodEpoch" : ObjectId("515b8f3754fde3fbab130f92"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : { "$minKey" : 1 } }, "max" : { "Uid" : 0 }, "shard" : "shard0000" } 5. { "_id" : "OSSP10.bizuser-Uid_381300.0", "lastmod" : { "t" : 4, "i" : 1 }, "lastmodEpoch" : ObjectId("515b8f3754fde3fbab130f92"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 381300 }, "max" : { "Uid" : 762601 }, "shard" : "shard0003" } 6. { "_id" : "OSSP10.bizuser-Uid_762601.0", "lastmod" : { "t" : 1, "i" : 2 }, "lastmodEpoch" : ObjectId("515b8f3754fde3fbab130f92"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 762601 }, "max" : { "Uid" : { "$maxKey" : 1 } }, "shard" : "shard0003" } 7. { "_id" : "OSSP10.bizuser-Uid_0.0", "lastmod" : { "t" : 3, "i" : 0 }, "lastmodEpoch" : ObjectId("515b8f3754fde3fbab130f92"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 0 }, "max" : { "Uid" : 250000 }, "shard" : "shard0001" } 8. { "_id" : "OSSP10.bizuser-Uid_250000.0", "lastmod" : { "t" : 4, "i" : 0 }, "lastmodEpoch" : ObjectId("515b8f3754fde3fbab130f92"), "ns" : "OSSP10.bizuser", "min" : { "Uid" : 250000 }, "max" : { "Uid" : 381300 }, "shard" : "shard0002" }
MongoDB這種自動分片和平衡的能力使得在遷移老數據的時候變得非常簡單,但是如果數據特別大話則可能會非常的慢。
Hashed Sharding
MongoDB2.4以上的版本支持基於哈希的分片,我們在上面的基礎上繼續探索。
交代一下因為環境變動這部分示例操作不是在之前的那個集群上,但是系統環境都是一樣,邏輯架構也一樣,只是ip地址不一樣(只搭建在一台虛擬機上),后面環境有改動不再說明:
配置服務器:192.168.129.132:10000
路由服務器:192.168.129.132:20000
分片1:192.168.129.132:27017
分片2:192.168.129.132:27018
……其他分片端口依次遞增。
我們重建之前的OSSP10庫,我們仍然使用OSSP10.bizuser不過這次啟動哈希分片,選擇_id作為片鍵:
1. mongos> use admin 2. switched to db admin 3. mongos> db.runCommand({"enablesharding":"OSSP10"}) 4. { "ok" : 1 } 5. mongos> db.runCommand({"shardcollection":"OSSP10.bizuser","key":{"_id":"hashed"}}) 6. { "collectionsharded" : "OSSP10.bizuser", "ok" : 1 }
我們現在查看一下config.chunks
1. mongos> use config 2. switched to db config 3. mongos> db.chunks.find() 4. { "_id" : "OSSP10.bizuser-_id_MinKey", "lastmod" : { "t" : 2, "i" : 2 }, "lastmodEpoch" : ObjectId("515e47ab56e0b3341b76f145"), "ns" : "OSSP10.bizuser", "min" : { "_id" : { "$minKey" : 1 } }, "max" : { "_id" : NumberLong("-4611686018427387902") }, "shard" : "shard0000" } 5. { "_id" : "OSSP10.bizuser-_id_-4611686018427387902", "lastmod" : { "t" : 2, "i" : 3 }, "lastmodEpoch" : ObjectId("515e47ab56e0b3341b76f145"), "ns" : "OSSP10.bizuser", "min" : { "_id" : NumberLong("-4611686018427387902") }, "max" : { "_id" : NumberLong(0) }, "shard" : "shard0000" } 6. { "_id" : "OSSP10.bizuser-_id_0", "lastmod" : { "t" : 2, "i" : 4 }, "lastmodEpoch" : ObjectId("515e47ab56e0b3341b76f145"), "ns" : "OSSP10.bizuser", "min" : { "_id" : NumberLong(0) }, "max" : { "_id" : NumberLong("4611686018427387902") }, "shard" : "shard0001" } 7. { "_id" : "OSSP10.bizuser-_id_4611686018427387902", "lastmod" : { "t" : 2, "i" : 5 }, "lastmodEpoch" : ObjectId("515e47ab56e0b3341b76f145"), "ns" : "OSSP10.bizuser", "min" : { "_id" : NumberLong("4611686018427387902") }, "max" : { "_id" : { "$maxKey" : 1 } }, "shard" : "shard0001" }
MongoDB的哈希分片使用了哈希索引:
哈希分片仍然是基於范圍的,只是將提供的片鍵散列成一個非常大的長整型作為最終的片鍵。官方文中描述如下:
不像普通的基於范圍的分片,哈希分片的片鍵只能使用一個字段。
選擇哈希片鍵最大的好處就是保證數據在各個節點分布基本均勻,下面使用_id作為哈希片鍵做個簡單的測試:
1. mongos> db.runCommand({"enablesharding":"mydb"}) 2. db.runCommand({"shardcollection":"mydb.mycollection","key":{"_id":"hashed"}}) 3. mongos> use mydb 4. mongos> for(i=0;i<333333;i++){ db.mycollection.insert({"Uid":i,"Name":"zhanjindong2","Age":13,"Date":new Date()}); }
通過MongoVUE觀察三個切片上的數據量非常均勻:
上面是使用官方文檔中推薦的Objectid作為片鍵,情況很理想。如果使用一個自增長的Uid作為片鍵呢:
1. db.runCommand({"shardcollection":"mydb.mycollection","key":{"Uid":"hashed"}}) 2. for(i=0;i<333333;i++){ db.mycollection.insert({"Uid":i,"Name":"zhanjindong2","Age":13,"Date":new Date()}); }
故障恢復
先不考慮集群中每個分片是副本集復雜的情況,只考慮了每個分片只有一個mongod進程,這種配是只是不夠健壯還是非常脆弱。我們在testdb.testcollection上啟動分片,然后向其中插入一定量的數據(視你的chunkSize而定),通過觀察config.chunks確保testdb.testcollection上的數據被分布在了不同的分片上。
1. mongos> db.runCommand({"enablesharding":"testdb"}) 2. mongos> db.runCommand({"shardcollection":"testdb.testcollection","key":{"Uid":1}}) 3. use testdb 4. for(i=0;i<1000000;i++){ db.testcollection.insert({"Uid":i,"Name":"zhanjindong","Age":13,"Date":new Date()}); } 5. mongos> use config 6. switched to db config 7. mongos> db.chunks.find() 8. …… 9. { "_id" : "testdb.testcollection-Uid_747137.0", "lastmod" : { "t" : 4, "i" : 1 }, "lastmodEpoch" : ObjectId("515fc5f0365c860f0bf8e0cb"), "ns" : "testdb.testcollection", "min" : { "Uid" : 747137 }, "max" : { "Uid" : 962850 }, "shard" : "shard0000" } 10. …… 11. { "_id" : "testdb.testcollection-Uid_0.0", "lastmod" : { "t" : 1, "i" : 3 }, "lastmodEpoch" : ObjectId("515fc5f0365c860f0bf8e0cb"), "ns" : "testdb.testcollection", "min" : { "Uid" : 0 }, "max" : { "Uid" : 6757 }, "shard" : "shard0001" } 12. …… 13. { "_id" : "testdb.testcollection-Uid_531424.0", "lastmod" : { "t" : 2, "i" : 4 }, "lastmodEpoch" : ObjectId("515fc5f0365c860f0bf8e0cb"), "ns" : "testdb.testcollection", "min" : { "Uid" : 531424 }, "max" : { "Uid" : 747137 }, "shard" : "shard0002" }
這時我們強制殺掉shard2進程:
1. [root@localhost mongodb-2.4.1]# ps -ef |grep mongo 2. root 6329 1 0 22:52 ? 00:00:08 ./bin/mongod --dbpath data/shard3/ --logpath log/shard3.log --fork --port 27019 3. kill -9 6329
我們嘗試用屬於不同范圍的Uid對testdb.testcollection進行寫入操作(這里插入一條記錄應該不會導致新的塊遷移):
1. [root@localhost mongodb-2.4.1]# ps -ef |grep mongo 2. use testdb 3. switched to db testdb 4. ####寫 5. mongos> db.testcollection.insert({"Uid":747138,"Name":"zhanjindong","Age":13,"Date":new Date()}) 6. ##向shard0000插入沒有問題 7. mongos> db.testcollection.insert({"Uid":6756,"Name":"zhanjindong","Age":13,"Date":new Date()}) 8. ##向shard0001插入沒有問題 9. mongos> db.testcollection.insert({"Uid":531425,"Name":"zhanjindong","Age":13,"Date":new Date()}) 10. socket exception [CONNECT_ERROR] for 192.168.129.132:27019 11. ##向shard0002插入出問題 12. ####讀 13. mongos> db.testcollection.find({Uid:747139}) 14. ##從shard0000讀取沒有問題 15. mongos> db.testcollection.find({Uid: 2}) 16. ##從shard0001讀取沒有問題 17. mongos> db.testcollection.find({Uid: 531426}) 18. error: { 19. "$err" : "socket exception [SEND_ERROR] for 192.168.129.132:27019", 20. "code" : 9001, 21. "shard" : "shard0002" 22. } 23. ##從shard0002讀取有問題 24. b.testcollection.count() 25. Sat Apr 6 00:23:19.246 JavaScript execution failed: count failed: { 26. "code" : 11002, 27. "ok" : 0, 28. "errmsg" : "exception: socket exception [CONNECT_ERROR] for 192.168.129.132:27019" 29. } at src/mongo/shell/query.js:L180
可以看到插入操作涉及到分片shard0002的操作都無法完成。這是順理成章的。
下面我們重新啟動shard3看集群是否能自動恢復正常操作:
1. ./bin/mongod --dbpath data/shard3/ --logpath log/shard3.log --fork --port 27019 2. mongos> use config 3. switched to db config 4. mongos> db.shards.find() 5. { "_id" : "shard0000", "host" : "192.168.129.132:27017" } 6. { "_id" : "shard0001", "host" : "192.168.129.132:27018" } 7. { "_id" : "shard0002", "host" : "192.168.129.132:27019" }
重復上面的插入和讀取shard0002的操作:
1. mongos> db.testcollection.insert({"Uid":531425,"Name":"zhanjindong","Age":13,"Date":new Date()}) 2. ##沒有問題 3. db.testcollection.find({Uid: 531426}) 4. { "_id" : ObjectId("515fc791b86c543aa1d7613e"), "Uid" : 531426, "Name" : "zhanjindong", "Age" : 13, "Date" : ISODate("2013-04-06T06:58:25.516Z") } 5. ##沒有問題
總結:當集群中某個分片宕掉以后,只要不涉及到該節點的操縱仍然能進行。當宕掉的節點重啟后,集群能自動從故障中恢復過來。
這一節在前一節搭建的集群基礎上做了一些簡單的測試,下一節的重點是性能和優化相關。