將存儲在MongoDB數據庫中的Collection進行分片需要選定分片Key(Shard key),對於分片Key的選定直接決定了集羣中數據分佈是否均衡、集羣性能是否合理。那麼我們究竟該選擇什麼樣的字段來作爲分片Key呢?有如下幾個需要考慮點。
以下述記錄日誌的Document爲例:
{
server : "ny153.example.com" ,
application : "apache" ,
time : "2011-01-02T21:21:56.249Z" ,
level : "ERROR" ,
msg : "something is broken"
}
基數
Mongodb中一個被分片的Collection的所有數據都存放在衆多的Chunk中。一個Chunk存放分片字段的一個區間範圍的數據。選擇一個好的分片字段非常重要,否則就會遭遇到不能被拆分的大Chunk。
用上述的日誌爲例,如果選擇{server:1}來作爲一個分片Key的話,一個server上的所有數據都是在同一個Chunk中,很容易想到一個Server上的日誌數據會超過200MB(默認Chunk大小)。如果分片Key是{server:1,time:1},那麼能夠將一個Server上的日誌信息進行分片,直至毫秒級別,絕對不會存在不可被拆分的Chunk。
將Chunk的規模維持在一個合理的大小是非常重要的,只有這樣數據才能均勻分佈,並且移動Chunk的代價也不會過大。
寫操作可擴展
使用分片的一個主要原因之一是分散寫操作。爲了實現這個目標,儘可能的將寫操作分散到多個Chunk就尤爲重要了。
用上述的日誌實例,選擇{time:1}來作爲分片key將導致所有的寫操作都會落在最新的一個Chunk上去,這樣就形成了一個熱點區域。如果選擇{server:1,application:1,time:1}來作爲分片Key的話,那麼每一個Server上的應用的日誌信息將會寫在不同的地方,如果有100個Server和應用對,有10臺Server,那麼每一臺Server將會分擔1/10的寫操作。
查詢隔離
另外一個需要考慮的是任何一個查詢操作將會由多少個分片來來提供服務。最理想的情況是,一個查詢操作直接從Mongos進程路由到一個Mongodb上去,並且這個Mongodb擁有該次查詢的全部數據。因此,如果你知道最爲通用的查詢操作的都以server作爲一個查詢條件的話,以Server作爲一個起始的分片Key會使整個集羣更加高效。
任何一個查詢都能執行,不管使用什麼來作爲分片Key,但是,如果Mongos進程不知道是哪一個Mongodb的分片擁有要查詢的數據的話,Mongos將會讓所有的Mongod分片去執行查詢操作,再將結果信息彙總起來返回。顯而易見,這回增加服務器的響應時間,會增加網絡成本,也會無謂的增加了Load。
排序
在需要調用sort()來查詢排序後的結果的時候,以分片Key的最左邊的字段爲依據,Mongos可以按照預先排序的結果來查詢最少的分片,並且將結果信息返回給調用者。這樣會花最少的時間和資源代價。
相反,如果在利用sort()來排序的時候,排序所依據的字段不是最左側(起始)的分片Key,那麼Mongos將不得不併行的將查詢請求傳遞給每一個分片,然後將各個分片返回的結果合併之後再返回請求方。這個會增加Mongos的額外的負擔。
可靠性
選擇分片Key的一個非常重要因素是萬一某一個分片徹底不可訪問了,受到影響的Chunk有多大(即使是用貌似可以信賴的Replica Set)。
假定,有一個類似於Twiter的系統,Comment記錄類似如下形式:
{
_id: ObjectId("4d084f78a4c8707815a601d7"),
user_id : 42 ,
time : "2011-01-02T21:21:56.249Z" ,
comment : "I am happily using MongoDB",
}
由於這個系統對寫操作非常敏感,所以需要將寫操作扁平化的分佈到所有的Server上去,這個時候就需要用id或者user_id來作爲分片Key了。使用Id作爲分片Key有最大粒度的扁平化,但是在一個分片宕機的情況下,會影響幾乎所有的用戶(一些數據丟失了)。如果使用User_id作爲分片Key,只有極少比率的用戶會收到影響(在存在5個分片的時候,20%的用戶受影響),但是這些用戶會再也不會看到他們的數據了。
索引優化
正如在別的章節中提到索引的一樣,如果只有一部分的索引被讀或者更新的話,通常會有更好的性能,因爲“活躍”的部分在大多數時間內能駐留在內存中。本文上述的所描述的選擇分片Key的方法都是爲了最終數據能夠均勻的分佈,與此同時,每一個Mongod的索引信息也被均勻分佈了。相反,使用時間戳作爲分片key的起始字段會有利於將常用索引限定在較小的一部分。
假定有一個圖片存儲系統,圖片記錄類似於如下形式:
{
_id: ObjectId("4d084f78a4c8707815a601d7"),
user_id : 42 ,
title: "sunset at the beach",
upload_time : "2011-01-02T21:21:56.249Z" ,
data: ...,
}
你也能構造一個客戶id,讓它包括圖片上傳時間對應的月度信息和一個唯一標誌符(ObjectID,數據的MD5等)。記錄看起來就像下面這個樣子的:
{
_id: "2011-01-02_4d084f78a4c8707815a601d7",
user_id : 42 ,
title: "sunset at the beach",
upload_time : "2011-01-02T21:21:56.249Z" ,
data: ...,
}
客戶id作爲分片key,並且這個id也被用於去訪問這個Document。即能將數據均衡的分佈在各個節點上,也減少了大多數查詢所使用的索引比例。
更進一步來講:
在每一個月份的開始,在開最初的一段時間內只有一個Server來存取數據,隨着數據量的增長,負載均衡器(balancer)就開始進行分裂和遷移數據塊了。爲了避免潛在的低效率和遷移數據,預先創建分片範圍區間是明智之舉。(如果有5個Sever則分5個區間)
可以繼續改進,可以把User_ID包含到圖片的id中來。這樣的話會讓一個用戶的所有Document都在一個分片上。比如用“2011-01-02_42_4d084f78a4c8707815a601d7”作爲圖片的id。
GridFS
根據需求的不同,GridFS有幾種不同的分片方法。基於預先存在的索引是慣用的分片辦法:
1)“files”集合(Collection)不會分片,所有的文件記錄都會位於一個分片上,高度推薦使該分片保持高度靈活(至少使用由3個節點構成的replica set)。
2)“chunks”集合(Collection)應該被分片,並且用索引”files_id:1”。已經存在的由MongoDB的驅動來創建的“files_id,n”索引不能用作分片Key(這個是一個分片約束,後續會被修復),所以不得不創建一個獨立的”files_id”索引。使用“files_id”作爲分片Key的原因是一個特定的文件的所有Chunks都是在相同的分片上,非常安全並且允許運行“filemd5”命令(要求特定的驅動)。
運行如下命令:
> db.fs.chunks.ensureIndex({files_id: 1});
> db.runCommand({ shardcollection : "test.fs.chunks", key : { files_id : 1 }})
{ "collectionsharded" : "test.fs.chunks", "ok" : 1 }
由於默認的files_id是一個ObjectId,files_id將會升序增長,因此,GridFS的全部Chunks都會被從一個單點分片上存取。如果寫的負載比較高,就需要使用其他的分片Key了,或者使用其它的值(_id)來作爲分片Key了。
選擇分片Key的需要考慮的因素具有一定的對立性,不可能樣樣的具備,在實際使用過程中還是需要根據需求的不同來進行權衡,適當放棄一些。沒有萬能的普適分片辦法,需求才是王道。