讀古今文學網 > MongoDB實戰 > 9.2 示例分片集群 >

9.2 示例分片集群

理解分片的最佳途徑就是瞭解它實際是怎麼工作的。幸運的是可以在一台機器上配置分片集群,接下來我們就會這麼做,1 還會模擬上一節裡提到的基於雲的電子錶格應用程序的行為。在此過程中,我們會仔細查看全局分片配置,通過第一手資料來瞭解數據是如何基於分片鍵進行分區的。

1. 為了進行測試,你可以在單台機器上運行各個mongodmongos進程。在本章後續的內容裡,我們會看到生產環境下的分片配置,以及一套切實可行的分片部署所需的最小服務器數量。

9.2.1 配置

配置分片集群有兩個步驟。第一步,啟動所有需要的mongodmongos進程。第二步,也是比較簡單的一步,發出一系列命令來初始化集群。你將構建的分片集群由兩個分片和三個配置服務器組成,另外還要啟動一個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\"
}
  

你能說出這個塊所表示的範圍嗎?如果只有一個塊,那麼它的範圍是這個分片集合。這是由minmax字段標識的,這些字段通過$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個塊了。當然,每個塊所表示的是一段連續範圍的數據。可以看到第一個塊由$minKeyAbdul的文檔構成,第二個塊由AbdulBuettner的文檔構成。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
  

由於塊的數量變多了,塊的平均範圍就變小了,但是每個塊都會包含更多數據。舉例來看,集合裡的第一個塊的範圍只是AbbottBender,但它的大小已經接近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。總的來說,在變更記錄裡找到的文檔可讀性都比較好。在深入瞭解分片並打算打造自己的分片集群之時,配置變更記錄是瞭解拆分和遷移行為的優秀材料,應該經常看看它。