理解分片的最佳途徑就是瞭解它實際是怎麼工作的。幸運的是可以在一台機器上配置分片集群,接下來我們就會這麼做,1 還會模擬上一節裡提到的基於雲的電子錶格應用程序的行為。在此過程中,我們會仔細查看全局分片配置,通過第一手資料來瞭解數據是如何基於分片鍵進行分區的。
1. 為了進行測試,你可以在單台機器上運行各個mongod
和mongos
進程。在本章後續的內容裡,我們會看到生產環境下的分片配置,以及一套切實可行的分片部署所需的最小服務器數量。
9.2.1 配置
配置分片集群有兩個步驟。第一步,啟動所有需要的mongod
和mongos
進程。第二步,也是比較簡單的一步,發出一系列命令來初始化集群。你將構建的分片集群由兩個分片和三個配置服務器組成,另外還要啟動一個mongos
與集群通信。你要啟動的全部進程如圖9-2所示,括號裡是它們的端口號。
圖9-2 由示例分片集群構成的進程全貌
你要運行一堆命令來啟動集群,因此如果覺得自己一葉障目,不見泰山,不妨回頭看看這張圖。
1. 啟動分片組件
讓我們開始為兩個副本集創建數據目錄吧,它們將成為分片的一部分。
$ mkdir /data/rs-a-1 $ mkdir /data/rs-a-2 $ mkdir /data/rs-a-3 $ mkdir /data/rs-b-1 $ mkdir /data/rs-b-2 $ mkdir /data/rs-b-3
接下來,啟動每個mongod
進程。因為要運行很多進程,所以可以使用--fork
選項,讓它們運行在後台。2以下是啟動第一個副本集的命令。
2. 注意,如果運行在Windows上,fork
是沒用的。因為必須打開新終端窗口來運行每個進程,最好把logpath
選項也忽略了。
$ mongod --shardsvr --replSet shard-a --dbpath /data/rs-a-1 --port 30000 --logpath /data/rs-a-1.log --fork --nojournal $ mongod --shardsvr --replSet shard-a --dbpath /data/rs-a-2 --port 30001 --logpath /data/rs-a-2.log --fork --nojournal $ mongod --shardsvr --replSet shard-a --dbpath /data/rs-a-3 --port 30002 --logpath /data/rs-a-3.log --fork --nojournal
以下是啟動第二個副本集的命令:
$ mongod --shardsvr --replSet shard-b --dbpath /data/rs-b-1 --port 30100 --logpath /data/rs-b-1.log --fork --nojournal $ mongod --shardsvr --replSet shard-b --dbpath /data/rs-b-2 --port 30101 --logpath /data/rs-b-2.log --fork --nojournal $ mongod --shardsvr --replSet shard-b --dbpath /data/rs-b-3 --port 30102 --logpath /data/rs-b-3.log --fork --nojournal
如往常一樣,現在要初始化這些副本集了。單獨連上每個副本集,運行rs.initiate
,隨後添加剩餘的節點。第一個副本集上的命令是這樣的:3
3. arete
是本地主機的名字。
$ mongo arete:30000 > rs.initiate
大概一分鐘之後,初始節點就變成主節點了,隨後就能添加剩餘的節點了:
> rs.add(\"arete:30000\") > rs.add(\"arete:30001\", {arbiterOnly: true})
初始化第二個副本集的方法與之類似。在運行rs.initiate
後等待一分鐘:
$ mongo arete:30100 > rs.initiate > rs.add(\"arete:30100\") > rs.add(\"arete:30101\", {arbiterOnly: true})
最後,在每個副本集上通過Shell運行rs.status
命令,驗證一下兩個副本集是否正常運行。如果一切順利,就可以準備啟動配置服務器了。4現在,創建每個配置服務器的數據目錄,通過configsvr
選項啟動各個配置服務器的mongod
進程。
4. 同樣的,如果是運行在Windows上,忽略--fork
和-logpath
選項,在新窗口裡啟動各個mongod
。
$ mkdir /data/config-1 $ mongod --configsvr --dbpath /data/config-1 --port 27019 --logpath /data/config-1.log --fork --nojournal $ mkdir /data/config-2 $ mongod --configsvr --dbpath /data/config-2 --port 27020 --logpath /data/config-2.log --fork --nojournal $ mkdir /data/config-3 $ mongod --configsvr --dbpath /data/config-3 --port 27021 --logpath /data/config-3.log --fork --nojournal
用Shell連接或者查看日誌文件,確保每台配置服務器都已啟動並已正常運行,並驗證每個進程都在監聽配置的端口。查看每台配置服務器的日誌,應該能看到這樣的內容:
Wed Mar 2 15:43:28 [initandlisten] waiting for connections on port 27020 Wed Mar 2 15:43:28 [websvr] web admin interface listening on port 28020
如果每個配置服務器都在運行了,那麼就能繼續下一步,啟動mongos
。必須用configdb
選項來啟動 mongos
,它接受一個用逗號分隔的配置服務器地址列表:5
5. 在配置列表時要小心,不要在配置服務器地址間加入空格。
$ mongos --configdb arete:27019,arete:27020,arete:27021 --logpath /data/mongos.log --fork --port 40000
2. 配置集群
現在已經準備好了所有的組件,是時候來配置集群了。先從連接mongos
開始。為了簡化任務,可以使用分片輔助方法,它們是全局sh
對像上的方法。要查看可用輔助方法的列表,請運行sh.help
。
你將鍵入一系列配置命令,先從addshard
命令。該命令的輔助方法是sh.addShard
,它接受一個字符串,其中包含副本集名稱,隨後是兩個或多個要連接的種子節點地址。這裡你指定了兩個先前創建的副本集,用的是每個副本集中非仲裁節點的地址:
$ mongo arete:40000 > sh.addShard(\"shard-a/arete:30000,arete:30001\") { \"shardAdded\" : \"shard-a\", \"ok \":1} > sh.addShard(\"shard-b/arete:30100,arete:30101\") { \"shardAdded\" : \"shard-b\", \"ok \":1}
如果命令執行成功,命令的響應中會包含剛添加的分片的名稱。可以檢查config
數據庫的shards
集合,看看命令的執行效果。你使用了getSiblingDB
方法來切換數據庫,而非use
命令:
> db.getSiblingDB(\"config\").shards.find { \"_id\" : \"shard-a\", \"host\" : \"shard-a/arete:30000,arete:30001\" } { \"_id\" : \"shard-b\", \"host\" : \"shard-b/arete:30100,arete:30101\" }
listshards
命令會返回相同的信息,這是一個快捷方式:
> use admin > db.runCommand({listshards: 1})
在報告分片配置時,Shell的sh.status
方法能很好地總結集群的情況。現在就來試試。
下一步配置是開啟一個數據庫上的分片,這是對任何集合進行分片的先決條件。應用程序的數據庫名為cloud-docs
,可以像下面這樣開啟分片:
> sh.enableSharding(\"cloud-docs\")
和以前一樣,可以檢查config
裡的數據查看剛才所做的變更。config
數據庫裡有一個名為databases
的集合,其中包含了一個數據庫的列表。每個文檔都標明了數據庫主分片的位置,以及它是否分區(是否開啟了分片):
>db.getSiblingDB(\"config\").databases.find { \"_id\" : \"admin\", \"partitioned\" : false, \"primary\" : \"config\" } { \"_id\" : \"cloud-docs\", \"partitioned\" : true, \"primary\" : \"shard-a\" }
現在你要做的就是分片spreadsheets
集合。在對集合進行分片時,要定義一個分片鍵。這裡將使用組合分片鍵{username: 1, _id: 1}
,因為它能很好地分佈數據,還能方便查看和理解塊的範圍:
sh.shardCollection(\"cloud-docs.spreadsheets\", {username: 1, _id: 1})>
同樣,可以通過檢查config
數據庫來驗證分片集合的配置:
> db.getSiblingDB(\"config\").collections.findOne { \"_id\" : \"cloud-docs.spreadsheets\", \"lastmod\" : ISODate(\"1970-01-16T00:50:07.268Z\"), \"dropped\" : false, \"key\" : { \"username\" : 1, \"_id\" : 1 }, \"unique\" : false }
分片集合的定義可能會提醒你幾點;它看起來和索引定義有幾分相似之處,尤其是有那個unique
鍵。在對一個空集合進行分片時,MongoDB會在每個分片上創建一個與分片鍵對應的索引。6可以直接連上分片,運行getIndexes
方法進行驗證。此處,你可以連接到第一個分片,方法的輸出包含分片鍵索引,正如預料的那樣:
6. 如果是在對現有集合進行分片,必須在運行shardcollection
命令前創建一個與分片鍵對應的索引。
$ mongo arete:30000 > use cloud-docs > db.spreadsheets.getIndexes [ { \"name\" : \"_id_\", \"ns\" : \"cloud-docs.spreadsheets\", \"key\" : { \"_id\" : 1 }, \"v\" : 0 }, { \"ns\" : \"cloud-docs.spreadsheets\", \"key\" : { \"username\" : 1, \"_id\" : 1 }, \"name\" : \"username_1__id_1\", \"v\" : 0 } ]
一旦完成了集合的分片,分片集群就準備就緒了。現在可以向集群寫入數據,數據將分佈到各分片上。下一節裡我們會瞭解到它是如何工作的。
9.2.2 寫入分片集群
我們將向分片集合寫入數據,這樣你才能觀察塊的排列與移動。塊是MongoDB分片的要素。每個示例文檔都表示了一個電子錶格,看起來是這樣的:
{ _id: ObjectId(\"4d6f29c0e4ef0123afdacaeb\"), filename: \"sheet-1\", updated_at: new Date, username: \"banks\", data: \"RAW DATA\" }
請注意,data
字段會包含一個5 KB的字符串以模擬原始數據。
本書的源代碼中包含一個Ruby腳本,你可以用它向集群寫入文檔數據。該腳本接受一個循環次數作為參數,每個循環裡都會為200個用戶各插入5 KB的文檔。腳本的源碼如下:
require \'rubygems\' require \'mongo\' require \'names\' @con = Mongo::Connection.new(\"localhost\", 40000) @col = @con[\'cloud\'][\'spreadsheets\'] @data = \"abcde\" * 1000 def write_user_docs(iterations=0, name_count=200) iterations.times do |n| name_count.times do |n| doc = { :filename => \"sheet-#{n}\", :updated_at => Time.now.utc, :username => Names::LIST[n], :data => @data } @col.insert(doc) end end end if ARGV.empty? || !(ARGV[0] =~ /^d+$/) puts \"Usage: load.rb [iterations] [name_count]\" else iterations = ARGV[0].to_i if ARGV[1] && ARGV[1] =~ /^d+$/ name_count = ARGV[1].to_i else name_count = 200 end write_user_docs(iterations, name_count) end
如果手頭有腳本,可以在命令行裡不帶參數運行腳本,它會循環一次,插入200個值:
$ ruby load.rb
現在,通過Shell連接mongos
。如果查詢spreadsheets
集合,你會發現其中包含200個文檔,總大小在1 MB左右。還可以查詢一個文檔,但要排除data
字段(你不想在屏幕上輸出5 KB文本吧)。
$ mongo arete:40000 > use cloud-docs > db.spreadsheets.count 200 > db.spreadsheets.stats.size 1019496 > db.spreadsheets.findOne({}, {data: 0}) { \"_id\" : ObjectId(\"4d6d6b191d41c8547d0024c2\"), \"username\" : \"Cerny\", \"updated_at\" : ISODate(\"2011-03-01T21:54:33.813Z\"), \"filename\" : \"sheet-0\" }
現在,可以檢查一下整個分片範圍裡發生了什麼,切換到config
數據庫,看看塊的個數:
> use config > db.chunks.count 1
目前只有一個塊,讓我們看看它什麼樣:
> db.chunks.findOne { \"_id\" : \"cloud-docs.spreadsheets-username_MinKey_id_MinKey\", \"lastmod\" : { \"t\" : 1000, \"i\" : 0 }, \"ns\" : \"cloud-docs.spreadsheets\", \"min\" : { \"username\" : { $minKey:1}, \"_id\" : { $minKey:1} }, \"max\" : { \"username\" : { $maxKey:1}, \"_id\" : { $maxKey:1} }, \"shard\" : \"shard-a\" }
你能說出這個塊所表示的範圍嗎?如果只有一個塊,那麼它的範圍是這個分片集合。這是由min
和max
字段標識的,這些字段通過$minKey
和$maxKey
限定了塊的範圍。
MINKEY與MAXKEY
作為BSON類型的邊界,
$minKey
與$maxKey
常用於比較操作之中。$minKey
總是小於所有BSON類型,而$maxKey
總是大於所有BSON類型。因為給定的字段值能包含各種BSON類型,所以在分片集合的兩端,MongoDB使用這兩個類型來標記塊的端點。
通過向spreadsheets
集合添加更多數據,你能看到更有趣的塊範圍。還是使用之前的Ruby腳本,但這次要循環100次,向集合中插入20 000個文檔,總計100 MB:
$ ruby load.rb 100
可以像下面這樣驗證插入是否成功:
> db.spreadsheets.count 20200 > db.spreadsheets.stats.size 103171828
樣本插入速度
注意,向分片集群插入數據需要花好幾分鐘時間。速度如此之慢有三個原因。首先,每次插入都要與服務器交互一次,而在生產環境中可以使用批量插入。其次,你是在使用Ruby進行插入,Ruby的BSON序列器要比其他某些驅動的慢。最後,也是最重要的,你是在一台機器上運行所有分片節點的,這為磁盤帶來了巨大的負擔,因為四個節點正在同時向磁盤寫入數據(兩個副本集的主節點,以及兩個副本集的從節點)。有理由相信,在適當的生產環境部署中,插入的速度會快許多。
插入了這麼多數據之後,現在肯定有不止一個塊了。可以統計chunks集合的文檔數快速檢查塊的狀態:
> use config > db.chunks.count 10
運行sh.status
能看到更詳細的信息,該方法會輸出所有的塊以及它們的範圍。簡單起見,我只列出頭兩個塊的信息:
> sh.status sharding version: { \"_id\" : 1, \"version\":3} shards: { \"_id\": \"shard-a\", \"host\": \"shard-a/arete:30000,arete:30001\" } { \"_id\": \"shard-b\", \"host\": \"shard-b/arete:30100,arete:30101\" } databases: { \"_id\": \"admin\", \"partitioned\": false, \"primary\": \"config\" } { \"_id\": \"test\", \"partitioned\": false, \"primary\": \"shard-a\" } { \"_id\": \"cloud-docs\", \"partitioned\": true, \"primary\": \"shard-b\" } shard-a 5 shard-b 5 { \"username\": { $minKey:1}, \"_id\" : { $minKey:1}}-- >> { \"username\": \"Abdul\", \"_id\": ObjectId(\"4e89ffe7238d3be9f0000012\") } on: shard-a { \"t\" : 2000, \"i\" : 0 } { \"username\" : \"Abdul\", \"_id\" : ObjectId(\"4e89ffe7238d3be9f0000012\") } -->> { \"username\" : \"Buettner\", \"_id\" : ObjectId(\"4e8a00a0238d3be9f0002e98\") } on : shard-a { \"t\" : 3000, \"i\" : 0 }
情況明顯不同了,現在有10個塊了。當然,每個塊所表示的是一段連續範圍的數據。可以看到第一個塊由$minKey
到Abdul
的文檔構成,第二個塊由Abdul
到Buettner
的文檔構成。7不僅塊變多了,塊還遷移到了第二個分片上。通過sh.status
的輸出能看到這個變化,但還有更簡單的方法:
7. 如果你是跟著示例一路做下來的,會發現自己的塊分佈與示例稍有不同。
> db.chunks.count({\"shard\": \"shard-a\"}) 5 > db.chunks.count({\"shard\": \"shard-b\"}) 5
集群的數據量還不大。拆分算法表明會經常發生數據拆分,正如你所見,這能在早期就實現數據和塊的均勻分佈。從現在開始,只要寫操作在現有塊範圍內保持均勻分佈,就不太會發生遷移。
早期塊拆分
分片集群會在早期積極進行塊拆分,以便加快數據在分片中的遷移。具體說來,當塊的數量小於10時,會按最大塊尺寸(16 MB)的四分之一進行拆分;當塊的數量在10到20之間時,會按最大塊尺寸的一半(32 MB)進行拆分。
這種做法有兩個好處。首先,這會預先創建很多塊,觸發一次遷移。其次,這次遷移幾乎是無痛的,因為塊的尺寸越小,其遷移的數據就越少。
現在,拆分的閾值會增大。通過大量插入數據,你會看到拆分是怎麼緩緩減慢的,以及塊是怎麼增長到最大尺寸的。試著再向集群裡插入800 MB數據:
$ ruby load.rb 800
這條命令會執行很長時間,因此你可能會想在啟動加載進程後暫時離開吃些點心。執行完畢之後,總數據量比以前增加了8倍。但是如果查看分塊狀態,你會發現塊的數量差不多只是原來的兩倍:
> use config > db.chunks.count 21
由於塊的數量變多了,塊的平均範圍就變小了,但是每個塊都會包含更多數據。舉例來看,集合裡的第一個塊的範圍只是Abbott
到Bender
,但它的大小已經接近60 MB了。因為目前塊的最大尺寸是64 MB,所以如果繼續插入數據,很快就能看到塊的拆分了。
另一件值得注意的事情是塊的分佈還是很均勻的,就和之前一樣:
> db.chunks.count({\"shard\": \"shard-a\"}) 11 > db.chunks.count({\"shard\": \"shard-b\"}) 10
儘管剛才在插入800 MB數據時塊的數量增加了,但你還是可以猜到沒有發生遷移;一個可能的情況是每個原始塊被一拆為二,期間還有一次額外的拆分。可以查詢config
數據庫的changelog
集合加以驗證:
> db.changelog.count({what: \"split\"}) 20 > db.changelog.find({what: \"moveChunk.commit\"}).count 6
這符合我們的猜測。一共發生了20次拆分,產生了20個塊,但只發生了6次遷移。要再深入瞭解一下究竟發生了什麼,可以查看變更記錄的具體條目。舉例來說,以下條目記錄了第一次的塊移動:
> db.changelog.findOne({what: \"moveChunk.commit\"}) { \"_id\" : \"arete-2011-09-01T20:40:59-2\", \"server\" : \"arete\", \"clientAddr\" : \"127.0.0.1:55749\", \"time\" : ISODate(\"2011-03-01T20:40:59.035Z\"), \"what\" : \"moveChunk.commit\", \"ns\" : \"cloud-docs.spreadsheets\", \"details\" : { \"min\" : { \"username\" : { $minKey : 1 }, \"_id\" : { $minKe y:1} }, \"max\" : { \"username\" : \"Abbott\", \"_id\" : ObjectId(\"4d6d57f61d41c851ee000092\") }, \"from\" : \"shard-a\", \"to\" : \"shard-b\" } }
這裡可以看到塊從shard-a
移到了shard-b
。總的來說,在變更記錄裡找到的文檔可讀性都比較好。在深入瞭解分片並打算打造自己的分片集群之時,配置變更記錄是瞭解拆分和遷移行為的優秀材料,應該經常看看它。