MongoDB分片实战(三):性能和优化
插入、查詢和更新
插入:MongoDB會根據片鍵和conifg中的信息寫入到指定的分片上。
讀取:以下內容摘抄自《深入學習MongoDB》
關于讀取:上一節故障恢復中已經有所驗證。
更新:如果要更新單個文檔一定要在片鍵中使用片鍵(update的第一個參數)。我們現在OSSP10.bizuser(已經在_id上進行哈希分片)中插入一條記錄:
1. mongos> use OSSP10 2. switched to db OSSP10 3. mongos> db.bizuser.insert({"Uid":10001,"Name":"zhanjindong","Age":23}) 4. db.bizuser.find({"Name":"zhanjindong"}) 5. { "_id" : ObjectId("5160dd378bc15cdb61a131fc"), "Uid" : 10001, "Name" : "zhanjindong", "Age" : 23 }?嘗試根據Name來更新這個文檔會得到一個錯誤:
1. mongos> db.bizuser.update({"Name":"zhanjindong"},{"$set":{"Sex":0}}) 2. For non-multi updates, must have _id or full shard key ({ _id: "hashed" }) in query根據片鍵來更新則不會有任何問題:
1. mongos> db.bizuser.update({"_id":ObjectId("5160dd378bc15cdb61a131fc")},{"$set":{"Sex":0}}) 2. mongos> db.bizuser.find({"Name":"zhanjindong"}) 3. { "Age" : 23, "Name" : "zhanjindong", "Sex" : 0, "Uid" : 10001, "_id" : ObjectId("5160dd378bc15cdb61a131fc") }但是批量更新中可以用任何條件:
1. mongos> db.bizuser.insert({"Uid":10002,"Name":"dsfan","Age":23}) 2. mongos> db.bizuser.insert({"Uid":10003,"Name":"junling","Age":25}) 3. mongos> db.bizuser.update({"Age":23},{"$set":{"Sex":1}},false,true) 4. mongos> db.bizuser.find({"Age":23}) 5. { "Age" : 23, "Name" : "zhanjindong", "Sex" : 1, "Uid" : 10001, "_id" : ObjectId("5160dd378bc15cdb61a131fc") } 6. { "Age" : 23, "Name" : "dsfan", "Sex" : 1, "Uid" : 10002, "_id" : ObjectId("5160e2af8bc15cdb61a131fd") }之所以更新單個文檔會有這個強制限制是因為如果不指定片鍵,MongoDB會將更新操作路由到所有分片上,則無法保證更新操作在整個集群中只操作了一次(不同的分片上可能存在相同Uid的數據),指定片鍵后,更新操作只會路由到某一個分片上,MongoDB能保證只會更新在這個分片找到的第一個符合條件的文檔。下面提到的唯一索引問題和這個問題本質是一樣的。批量更新沒有這個限制很好理解。
排序:在需要調用sort()來查詢排序后的結果的時候,以分片Key的最左邊的字段為依據,Mongos可以按照預先排序的結果來查詢最少的分片,并且將結果信息返回給調用者。這樣會花最少的時間和資源代價。 相反,如果在利用sort()來排序的時候,排序所依據的字段不是最左側(起始)的分片Key,那么Mongos將不得不并行的將查詢請求傳遞給每一個分片,然后將各個分片返回的結果合并之后再返回請求方。這個會增加Mongos的額外的負擔。
片鍵和索引
?
所有分片的集合在片鍵上都必須建索引,這是MongoDB自動執行的,所以如果選擇某個字段作為片鍵但是基本不在這個字段做查詢那么等于浪費了一個索引,而增加一個索引總是會使得插入操作變慢。
唯一索引問題 如果集群在_id上進行了分片,則無法再在其他字段上建立唯一索引:
1. mongos> db.bizuser.ensureIndex( { "Uid": 1 }, { unique: true } ) 2. { 3. "err" : "can't use unique indexes with sharding ns:OSSP10.bizuser key: { Uid: 1.0 }", 4. "code" : 10205, 5. "n" : 0, 6. "ok" : 1 7. }之所以出現這個錯誤是因為MongoDB無法保證集群中除了片鍵以外其他字段的唯一性(驗證了CAP理論),能保證片鍵的唯一性是因為文檔根據片鍵進行切分,一個特定的文檔只屬于一個分片,MongoDB只要保證它在那個分片上唯一就在整個集群中唯一。
如果實現分片集群上的文檔唯一性一種方法是在創建片鍵的時候指定它的唯一性:
1. mongos> use admin 2. switched to db admin 3. mongos> db.runCommand({"enablesharding":"test"}) 4. mongos> db.runCommand({"shardcollection":"test.users","key":{"Uid":1},unique:true}) 5. mongos> use test 6. switched to db test 7. mongos> db.users.insert({"Uid":1001}) 8. mongos> db.users.insert({"Uid":1001}) 9. E11000 duplicate key error index: test.users.$Uid_1 dup key: { : 1001.0 }事實上就是建立了一個唯一索引:
Note:更多關于分片集群上唯一索引問題參見《MongoDB Manual》page468。
哈希索引:
?
哈希索引支持使用任何單個字段包括內嵌文檔,但是不能使用復合的字段,因此創建哈希索引的時候也只能指定一個字段:
1. mongos> db.runCommand({"shardcollection":"mydb.mycollection","key":{"Uid":"hashed","Name":"hashed"}}) 2. { 3. "ok" : 0, 4. "errmsg" : "hashed shard keys currently only support single field keys" 5. }片鍵的選擇
片鍵的選擇對于整個分片集群的性能至關重要,上一節對分片集群中的讀、寫和更新操作已經做了說明,選擇片鍵的時候要考慮到讀自身應用的讀寫模式和新增分片的情況。
小基數片鍵:如果某個片鍵一共只有N個值,那最多只能有N個數據塊,也最多只有個N個分片。則隨著數據量的增大會出現非常大的但不可分割的chunk。如果打算使用小基數片鍵的原因是需要在那個字段上進行大量的查詢,請使用組合片鍵,并確保第二個字段有非常多的不同值。
1. mongos> db.runCommand({"enablesharding":"mydb"}) 2. db.runCommand({"shardcollection":"mydb.mycollection","key":{"x":1}}) 3. mongos> use mydb 4. switched to db mydb 5. mongos> var arrayObj = new Array("A","B","C") 6. mongos> for(i=0;i<333333;i++){ db.mycollection.insert({"x":arrayObj[i%3],"y":"zhanjindong2","Age":13,"Date":new Date()}); }無論再插入多少條數據,我們查看一下config.chunks會發現只會有三個塊,三個塊最多只能使用三個分片:
1. { "_id" : "mydb.mycollection-x_MinKey", "lastmod" : { "t" : 2, "i" : 0 }, "lastmodEpoch" : ObjectId("51613a843999888c2cd63f41"), "ns" : "mydb.mycollection", "min" : { "x" : { "$minKey" : 1 } }, "max" : { "x" : "A" }, "shard" : "shard0001" } 2. { "_id" : "mydb.mycollection-x_\"A\"", "lastmod" : { "t" : 3, "i" : 0 }, "lastmodEpoch" : ObjectId("51613a843999888c2cd63f41"), "ns" : "mydb.mycollection", "min" : { "x" : "A" }, "max" : { "x" : "C" }, "shard" : "shard0002" } 3. { "_id" : "mydb.mycollection-x_\"C\"", "lastmod" : { "t" : 3, "i" : 1 }, "lastmodEpoch" : ObjectId("51613a843999888c2cd63f41"), "ns" : "mydb.mycollection", "min" : { "x" : "C" }, "max" : { "x" : { "$maxKey" : 1 } }, "shard" : "shard0000" }遞增的片鍵:使用遞增的分片的好處是數據的“局部性”,使得將最新產生的數據放在一起,對于大部分應用來說訪問新的數據比訪問老的數據更頻繁,這就使得被訪問的數據盡快能的都放在內存中,提升讀的性能。這類的片鍵比如時間戳、日期、ObjectId、自增的主鍵(比如從sqlserver中導入的數據)。但是這樣會導致新的文檔總是被插入到“最后”一個分片(塊)上去,這種片鍵創造了一個單一且不可分散的熱點,不具有寫分散性。
隨機片鍵:隨機片鍵(比如MD5)最終會使數據塊均勻分布在各個分片上,一般觀點會以為這是一個很好的選擇,解決了遞增片鍵不具有寫分散的問題,但是正因為是隨機性會導致每次讀取都可能訪問不同的塊,導致不斷將數據從硬盤讀到內存中,磁盤IO通常會很慢。
舉個例子:比如mydb.mycollection集合記錄下面這樣的用戶信息,Uid是一個比較隨機的值:
{Uid:12313477994,Name:zhanjindong,Age:23,CreatedTime:2013-04-08 15:23:24.122 }如果我們對Uid進行分片,那么同一分鐘創建的用戶信息可能被寫入到了不同的塊上(通常在不同的分片上),這有很好的分散性。但如果我們想根據時間來查找這一分鐘產生的所有新用戶,則mongos必須將查詢操作路由給所有的分片的多個塊上。但如果我們根據時間進行分片,那么這一分鐘內新增用戶可能都寫入到一個塊中,那么上面的查詢操作只需要路由給一個分片上的一個塊就完成了。
組合片鍵:一個理想的片鍵是同時擁有遞增片鍵和隨即片鍵的優點,這點很難做到關鍵是要理解自己的數據然后做出平衡。通常需要組合片鍵達到這種效果:
準升序鍵加搜索鍵 {coarselyAscending:1,search:1}
其中coarselyAscending每個值最好能對應幾十到幾百個數據塊(比如以月為單位或天為單位),serach鍵應當是應用程序中通常都會依據其進行查詢的字段,比如GUID。
注意:serach字段不能是升序字段,不然整個復合片鍵就下降為升序片鍵。這個字段應該具備非升序、分布隨機基數適當的特點。
事實上,上面這種復合片鍵中的第一個字段保證了擁有數據局部性,第二字段則保證了查詢的隔離性。同時因為第二個字段具備分布隨機的特性因此又一定程度的擁有隨機片鍵的特點。
哈希片鍵:對于哈希片鍵的選擇官方文檔中有很明確的說明:
選擇哈希分片最大好處就是使得數據在各個節點分布比較均勻。2.2.5 Hased Shaeding一節對哈希片鍵的使用有簡單的測試。
注意:建立哈希片鍵的時候不能指定唯一:
1. mongos> db.runCommand({"shardcollection":"OSSP10.Devices","key":{"DeviceId":"hashed"},unique:true}) 2. { "ok" : 0, "errmsg" : "hashed shard keys cannot be declared unique." }基于范圍vs基于哈希
什么時候選擇基于范圍的分片,什么時候選擇基于哈希的分片呢?官方文檔的說明很少:
基于哈希的分片通常可以使得集群中數據分布的更加均勻。但是考慮具體應用情況可能有所不同,下面是引用10gen的產品市場總監Kelly Stirman的一段話:
當使用基于范圍的分片,如果你的應用程序基于一個分片鍵范圍請求數據,那么這些查詢會被路由到合適的分片,通常只有一個分片,特殊情況下可能有一些分片。在一個使用了基于哈希分片的系統中,同樣的查詢會將請求路由到更多的分片,可能是所有的分片。理想情況下,我們希望查詢會被路由到一個單獨的分片或者盡可能少的分片,因為這樣的擴展能力要比將所有的查詢路由到所有的分片好。因此,如果你非常理解自己的數據和查詢,那么基于范圍的分片可能是最好的選擇。
出處:http://www.infoq.com/news/2013/03/mongodb-2-4
總結:對MongoDB 單條記錄的訪問比較隨機時,可以考慮采用哈希分片,否則范圍分片可能會更好。
Balancer
小的chunkSize能保證各個分片數據分布更均勻,但導致遷移更頻繁。MongoDB為了盡量減少對性能的影響對塊遷移的算法有很多的優化措施:2.2.2節對Migration Threshold有簡單的說明,另外balancer進程能聰明的避開整個集群高峰時期。
可以定時的執行數據遷移:
1. use config 2. db.settings.update({ _id : "balancer" }, { $set : { activeWindow : { start : "23:00", stop: "6:00"刪除定時數據遷移設置:
1. use config 2. db.settings.update({ _id : "balancer" }, { $unset : { activeWindow : true } })開啟和關閉balancer:
1. sh.startBalancer() 2. sh.stopBalancer()如果正在有數據進行遷移的話,stopBalancer會等待遷移結束,可以通過下面的方式查看當前是否有遷移在進行:
1. use config 2. while( db.locks.findOne({_id: "balancer"}).state ) { 3. print("waiting..."); sleep(1000);Note:更多關于balancer信息參見《MongoDB-Manual》page 455
手動分片
MongoDB自動分片都是先從一個分片上的一個塊開始的,然后通過不斷的分裂和遷移來達到數據的切分和平衡,依賴機器自動執行的好處是簡單,但是代價就是性能(雖然balancer已經做了很多優化)。因此MongoDB允許進行手動切分,手動切分有下面兩個步驟(官方示例):
1、用split命令對空集合進行手動的切分。
1. mongos> use admin 2. switched to db admin 3. mongos> db.runCommand({"enablesharding":"myapp"}) 4. mongos> db.runCommand({"shardcollection":"myapp.users","key":{"email":1}}) 5. for ( var x=97; x<97+26; x++ ){ 6. for( var y=97; y<97+26; y+=6 ) { 7. var prefix = String.fromCharCode(x) + String.fromCharCode(y); 8. db.runCommand( { split : "myapp.users" , middle : { email : prefix } } ); 9. } 10. }注意:
最好只對一個空的集合進行預分割,如果對存在數據的集合進行預分割,MongoDB會先進行自動分割,然后在嘗試進行手動的分割。這可能導致大規模的分割和低效的平橫。
2、利用moveChunk命令手動的移動分割的塊:
1. var shServer = [ "sh0.example.net", "sh1.example.net", "sh2.example.net", "sh3.example.net", "sh4.example.net" ]; 2. for ( var x=97; x<97+26; x++ ){ 3. for( var y=97; y<97+26; y+=6 ) { 4. var prefix = String.fromCharCode(x) + String.fromCharCode(y); 5. db.adminCommand({moveChunk : "myapp.users", find : {email : prefix}, to : shServer[(y-97)/6]}) 6. } 7. }或者利用balancer自動平衡。
要很好進行手動的切分必須了解片鍵的范圍,如果片鍵是一個隨機值比如哈希分片,則很難進行手動的預分割,其次及時進行了預分割隨后插入數據塊分裂和遷移(沒關閉balancer)依然會存在。
結論:預分割和手動分片適合于將片鍵范圍確定的數據初始化到分片集群中。
其他
journal
如果機器是32位的話在配置分片集群啟動shard的時候跟上—journal參數。因為64位默認是開啟journal的,32位沒有。
NUMA CPU架構問題
NUMA是多核心CPU架構中的一種,其全稱為Non-Uniform MemoryAccess,簡單來說就是在多核心CPU中,機器的物理內存是分配給各個核的。2.1.1的表中可以看到192.168.71.43這臺機器的CPU架構正是NUMA。
在NUMA架構的機器上啟動mongodb進程需要特別注意。我們先以正常的方式啟動mongodb,然后登錄,如下:
3. ./bin/mongod --dbpath data/ --logpath log/mongodb.log –fork 4. ./bin/mongo你會看到類似下面的警告信息:
1. . Server has startup warnings: 2. Mon Apr 1 20:49:25.900 [initandlisten] 3. Mon Apr 1 20:49:25.900 [initandlisten] ** WARNING: You are running on a NUMA machine. 4. Mon Apr 1 20:49:25.900 [initandlisten] ** We suggest launching mongod like this to avoid performance problems: 5. Mon Apr 1 20:49:25.900 [initandlisten] ** numactl --interleave=all mongod [other options] 6. Mon Apr 1 20:49:25.900 [initandlisten]按照提示我們應該向下面這樣啟動mongodb進程,在啟動命令前加上numactl --interleave選項:
1. numactl --interleave=all ./bin/mongod --dbpath data/ --logpath log/mongodb.log --fork這時再登錄mongodb就不會再有警告信息了。
以上只是就問題解決問題,至于在NUMA架構的CPU上非正常啟動mongodb會帶來什么樣的性能影響還沒做驗證,網上可以搜到一些別人使用的經驗。官方的文檔(參看MongoDB Documentation, Release 2.4.1 12.8.1 MongoDB on NUMA Hardware)中有如下說明:
簡單做下解釋,NUMA架構中每個核訪問分配給自己的內存會比分配給其他核內存要快,有下面幾種訪問控制策略:
- 缺省(default):總是在本地節點分配(分配在當前進程運行的節點上);
- 綁定(bind):強制分配到指定節點上;
- 交叉(interleave):在所有節點或者指定的節點上交織分配;
- 優先(preferred):在指定節點上分配,失敗則在其他節點上分配。
但是目前mongodb在這種架構下工作的不是很好,--interleave=all就是禁用NUMA為每個核單獨分配內存的機制。
轉載于:https://www.cnblogs.com/zhanjindong/archive/2013/04/12/3017387.html
總結
以上是生活随笔為你收集整理的MongoDB分片实战(三):性能和优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Web服务器、Servlet和Servl
- 下一篇: Exchange 2010迁移Excha