如何選擇MongoDB的分片欄位(Shard Key)
將儲存在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中一個被分片的
用上述的日誌為例,如果選擇{server:1}來作為一個分片Key的話,一個server上的所有資料都是在同一個Chunk中,很容易想到一個Server上的日誌資料會超過200MB(預設Chunk大小)。如果分片Key是{server:1,time:1},那麼能夠將一個Server上的日誌資訊進行分片,直至毫秒級別,絕對不會存在不可被拆分的Chunk。
將Chunk的規模維持在一個合理的大小是非常重要的,只有這樣資料才能均勻分佈,並且移動
寫操作可擴充套件
使用分片的一個主要原因之一是分散寫操作。為了實現這個目標,儘可能的將寫操作分散到多個Chunk就尤為重要了。
用上述的日誌例項,選擇{time:1}來作為分片key將導致所有的寫操作都會落在最新的一個Chunk上去,這樣就形成了一個熱點區域。如果選擇{server:1,application:1,time:1}來作為分片Key的話,那麼每一個Server上的應用的日誌資訊將會寫在不同的地方,如果有100個Server和應用對,有10臺Server,那麼每一臺Server將會分擔1/10的寫操作。
查詢隔離
另外一個需要考慮的是任何一個查詢操作將會由多少個分片來來提供服務。最理想的情況是,一個查詢操作直接從
任何一個查詢都能執行,不管使用什麼來作為分片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的需要考慮的因素具有一定的對立性,不可能樣樣的具備,在實際使用過程中還是需要根據需求的不同來進行權衡,適當放棄一些。沒有萬能的普適分片辦法,需求才是王道。