1. 程式人生 > >MongoDB使用小結:一些不常見的經驗分享

MongoDB使用小結:一些不常見的經驗分享

     最近一年忙碌於資料處理相關的工作,跟MongoDB打交道極多,以下為實踐過程中的Q&A,後續會不定期更新補充。

     另有《MongoDB使用小結:一些常用操作分享》,注:本文完成時MongoDB的最新版本為MongoDB 2.6。

1、count統計結果錯誤

這是由於分散式叢集正在遷移資料,它導致count結果值錯誤,需要使用aggregate pipeline來得到正確統計結果,例如:

db.collection.aggregate([{$group: {_id: null, count: {$sum: 1}}}])

引用:“On a sharded cluster, count can result in an inaccurate count if orphaned documents exist or if a chunk migration is in progress.”

參考:http://docs.mongodb.org/manual/reference/command/count/ 

2、從shell中更新/寫入到文件的數字,會變為float型別

引用:“shell中的數字都被MongoDB當作是雙精度數。這意味著如果你從資料庫中獲得的是一個32位整數,修改文件後,將文件存回資料庫的時候,這個整數也就被換成了浮點數,即便保持這個整數原封不動也會這樣的。”

參考:《MongoDB權威指南》第一版

3、restore資料到新DB時,不要去先建索引

把bson資料檔案restore到另一個DB時,需要注意:不能先建立索引再restore資料,否則效能極差,mongorestore工具預設會在restore完資料時,根據dump出來的index資訊建立索引,無須自己建立,如果是要更換索引,也應該在資料入庫完之後再建立。

4、DB中的namespace數量太多導致無法建立新的collection

錯誤提示:error: hashtable namespace index max chain reached:1335,如何解決呢?
這是DB中的collection個數太多導致,在實踐中以每個collection 8KB計算(跟官方文件裡說的不同,可能跟index有關係),256MB可以支援36000個collection。db.system.namespaces.count() 命令可以統計當前DB內的collection數目,DB可支援collection數量是由於nssize引數指定的,它指定了dbname.ns磁碟檔案的大小,也就指定了DB可支援的最大collection數目,ns為namespace縮寫。預設nssize為16MB。
如果重啟MongoD並修改了nssize引數,這新nssize只會對新加入的DB生效,對以前已經存在的DB不生效,如果你想對已經存在的DB採用新的nssize,必須在加大nssize重啟之後新建DB,然後把舊DB的collection 複製到新DB中。
namespace限制相關文件:http://docs.mongodb.org/manual/reference/limits/#Number-of-Namespaces

5、moveChunk因舊資料未刪除而失敗

錯誤日誌:”moveChunk failed to engage TO-shard in the data transfer: can't accept new chunks because there are still 1 deletes from previous migration“。
意思是說,當前正要去接受新chunk 的shard正在刪除上一次資料遷移出的資料,不能接受新Chunk,於是本次遷移失敗。這種log裡顯示的是warning,但有時候會發現shard的刪除持續了十幾天都沒完成,檢視日誌,可以發現同一個chunk的刪除在不斷重複執行,重啟所有無法接受新chunk的shard可以解決這個問題。
參考:
http://stackoverflow.com/questions/26640861/movechunk-failed-to-engage-to-shard-in-the-data-transfer-cant-accept-new-chunk
如果採用了balancer自動均衡,那麼可以加上_waitForDelete引數,如:
{ "_id" : "balancer", "activeWindow" : { "start" : "12:00", "stop" : "19:30" }, "stopped" : false, "_waitForDelete" : true }
,這樣就不會因delete堆積而導致後續migrate失敗,當然,需要考慮到這裡的阻塞是否會影響到程式正常運轉,在實踐中慎重採用使用waitForDelete,因為發現加上它之後遷移效能非常差,可能出現卡住十幾個小時的情況,外界拿住了被遷移chunk的遊標控制代碼,這時候刪除不能執行,阻塞了後續其它遷移操作。
遊標被開啟而導致被遷移資料無法及時刪除時的日誌:
2015-03-07T10:21:20.118+0800 [RangeDeleter] rangeDeleter waiting for open cursors in: cswuyg_test.cswuyg_test, min: { _id: -6665031702664277348 }, max: { _id: -6651575076051867067 }, elapsedSecs: 6131244, cursors: [ 220477635588 ]
這可能會卡住幾十小時,甚至一直卡住,影響後續的moveChunk操作,導致資料不均衡。
解決方法還是:重啟。

6、bson size不能超過16MB的限制

單個文件的BSON size不能超過16MB。find查詢有時會遇到16MB的限制,譬如使用$in 查詢的時候,in中的陣列元素不能太多。對一些特殊的資料來源做MapReduce,MapReduce中間會將資料組合為“KEY:[VALUE1、VALUE2]”這樣的格式,當value特別多的時候,也可能會遇上16MB的限制。 限制無處不在,需要注意,”The issue is that the 16MB document limit applies to everything - documents you store, documents MapReduce tries to generate, documents aggregation tries to return, etc.

7、批量插入

批量插入可以減少資料往伺服器的提交次數,提高效能,一般批量提交的BSON size不超過48MB,如果超過了,驅動程式自動修改為往mongos的多次提交。

8、安全寫入介紹及其沿革

關鍵字:acknowledge、write concern。

在2012年11月之前,MongoDB驅動、shell客戶端預設是不安全寫入,也就是fire-and-forget,動作發出之後,不關心是否真的寫入成功,如果這時候出現了_id重複、非UTF8字元等異常,客戶端不會知道。在2012年11月之後,預設為安全寫入,安全級別相當於引數w=1,客戶端可以知道寫入操作是否成功。如果程式碼使用Mongo或者Collection來連線資料庫,則說明它是預設不安全寫入的legacy程式碼,安全寫入已經把連線資料庫修改為MongoClient介面。
安全寫入可以分為三個級別,
第一級是預設的安全寫入,確認資料寫入到記憶體中就返回(w=N屬於這一級);
第二級是Journal save,資料在寫入到DB磁碟檔案之前,MongoDB會先把操作寫入到Journal檔案,這一級指的是確認寫入了Journal檔案就返回;
第三級是fysnc,所有資料刷寫到到DB磁碟檔案才返回。
一般第一級就足夠了,第二級是為了保證在機器異常斷電的情況下也不會丟失資料。安全寫入要付出效能的程式碼:不安全寫入的效能大概是預設安全寫入的3倍。使用fync引數則效能更差,一般不使用。
如果是副本集(replica set),其w=N引數,N表示安全寫入到多少個副本集才返回。
參考:
http://docs.mongodb.org/manual/release-notes/drivers-write-concern/
http://docs.mongodb.org/manual/core/write-concern/
http://blog.mongodirector.com/understanding-durability-write-safety-in-mongodb/
http://whyjava.wordpress.com/2011/12/08/how-mongodb-different-write-concern-values-affect-performance-on-a-single-node/

9、善用索引——可能跟你以為的不一樣

使用組合索引的時候,如果有兩組索引,在限量查詢的情況下,可能跟常規的認識不同:
利用組合索引做的查詢,在不同數量級下會有不同效能:
組合索引A: {"age": 1, "username": 1}
組合索引B: {"username": 1, "age": 1}
全量查詢: db.user.find({"age": {"$gte": 21, "$lte": 30}}).sort({"username" :1}),使用索引A的效能優於索引B。
限量查詢: db.user.find({"age": {"$gte": 21, "$lte": 30}}).sort({"username": 1}).limit(1000),使用索引B的效能優於索引A。
這兩個查詢在使用索引A的時候,是先根據age索引找到符合age的資料,然後再對這些結果做排序。使用索引B的時候,是遍歷name,對應的資料判斷age,然後得到的結果是name有序的。
優先使用sort key索引,在大多數應用上執行得很好。
參考:《MongoDB——The Definitive Guide 2nd Edition》page89

10、查詢時索引位置的無順序性

做find的時候,並不要求索引一定要在前面,
譬如:
db.test集合中對R有索引
db.test.find({R:"AA", "H": "BB"}).limit(100).explain()
db.test.find({"H":"BB", "R" : "AA"}).limit(100).explain()
這兩個查詢效能一樣,它都會使用R索引。

11、使用組合索引做shard key可以大幅度提高叢集效能

“固定值+增量值” 兩欄位做組合索引可以有效的實現分散式叢集中的分散多熱點寫入、讀取。以下為讀書筆記:
在單個MongoDB例項上,最高效的寫入是順序寫入,而MongoDB叢集則要求寫入能隨機,以便平均分散到多個MongoDB例項。所以最高效的寫入是有多個區域性熱點:在多個MongoDB例項之間是分散寫入,在例項內部是順序寫入。 要實現這一點,我們採用組合索引。
例如:shardkey的第一部分是很粗糙的,可選集很少的欄位,索引的第二部分是遞增欄位,當資料增加到一定程度時,會出現很多第一部分相同第二部分不同的chunk,資料只會在最後一個chunk裡寫入資料,當第一部分不同的chunk分散在多個shard上,就實現了多熱點的寫入。如果在一個shard上,不止一個chunk可以寫入資料,那也就是說不止一個熱點,當熱點非常多的時候,也就等同於無熱點的隨機寫入。當一個chunk分裂之後,只能有一個成為熱點,另一個不能再被寫入,否則就會產生兩個熱點,不再寫入的chunk也就是死掉了,後續只會對它有讀操作。

最典型的應用是具有日期屬性的日誌處理,shard key選擇“日期+使用者ID”組合,保證了資料寫入時的區域性熱點(一個shard上只有少數幾個chunk被寫入,避免隨機IO)和全域性分散(所有的shard上都有寫入資料,充分利用磁碟IO)。
我在實踐中除了書中講到的組合鍵方式外,還加上了預分片策略,避免了早期資料增長過程中的分片和資料遷移。另外還儘可能的製造能利用區域性性原理的資料寫入,例如在資料寫入之前先對資料排序,有大約30%左右的update效能提升。

預分片是這樣子做的:根據組合shardkey資訊先分裂好chunk,把這些空chunk移動到各個shard上,避免了後續自動分裂引起的資料遷移。

good case:

環境:一臺機器、7分片、MongoDB2.6版本、shard key選擇“日期+使用者ID組合”,

資料:寫入使用批量插入,對10億條日誌級分片叢集的寫入,寫入1000W條日誌只需要35分鐘,每條日誌約0.11K。

bad case:

環境:3臺機器、18分片、MongoDB2.6版本、shard key選擇 _id的hashid

資料:寫入採用批量插入,對3億條日誌級分片叢集的寫入,寫入300W條日誌耗時35分鐘,每條日誌約0.11K。

從對比可以看到,在資料量比較大的情況下選擇組合索引做shard key效能明顯優於選擇hashid。

我在實際應用中還遇到選擇hashid的更極端情況:對3條機器&18分片&3億條日誌叢集每天寫入300W條日誌,耗時170分鐘,每條日誌約4K。每次寫入資料時,所有分片磁碟IO使用率都達到100%。

附建立組合索引和使用組合索引做shard key 的 mongo shell程式碼:

xx> db.cswuyg.ensureIndex({'code':1, 'insert_time':1})

xx> use admin

admin> db.runCommand({"enablesharding":"xx"})

admin> db.runCommand({"shardcollection":"xx.cswuyg","key":{'code':1, 'insert_time':1}})

參考:《MongoDB——The Definitive Guide 2nd Edition》 page268

12、怎麼建索引更能提高查詢效能?

在查詢時,索引是否高效,要注意它的cardinality(cardinality越高表示該鍵可選擇的值越多),在組合索引中,讓cardinality高的放在前面。注意這裡跟分散式環境選擇shard key的不同。以下為讀書筆記:
index cardinality(索引雜湊程度),表示的是一個索引所對應到的值的多少,雜湊程度越低,則一個索引對應的值越多,索引效果越差:在使用索引時,高雜湊程度的索引可以更多的排除不符合條件的文件,讓後續的比較在一個更小的集合中執行,這更高效。所以一般選擇高雜湊程度的鍵做索引,或者在組合索引中,把高雜湊程度的鍵放在前面。
參考:《MongoDB——The Definitive Guide 2nd Edition》 page98

13、非原地update,效能會很差

update文件時,如果新文件的空間佔用大於舊文件加上它周圍padding的空間,那麼就會放棄原來的位置,把資料拷貝到新空間。
參考:《MongoDB——The Definitive Guide 2nd Edition》 page43

14、無法在索引建立之後再去增加索引的過期時間

如果索引建立指定了過期時間,後續要update過期時間可以這樣子:db.runCommand({"collMod":"a", index:{keyPattern:{"_":-1}, expireAfterSeconds: 60}})。

注意,通過collMod能修改過期時間的前提是:這個索引有過期時間,如果這個索引之前沒有設定過期時間,那麼無法update,只能刪了索引,重建索引並指定過期時間。
參考:http://docs.mongodb.org/manual/tutorial/expire-data/

15、_id索引無法刪除

參考:《MongoDB——The Definitive Guide 2nd Edition》 page114

16、paddingFactor是什麼?

它是儲存空間冗餘係數,1.0表示沒有冗餘,1.5表示50%的冗餘空間,有了冗餘空間,可以讓後續引發size增加的操作更快(不會導致重新分配磁碟空間和文件遷移),一般是在1到4之間。可以通過db.collection.stats()看到collection的該值“paddingFactor”。
該值是MongoDB自己處理的,使用者無法設定paddingFactor。我們可以在compact的時候對已經有的文件指定該值,但這個paddingFactor值不影響後續新插入的文件。
repairDatabase跟compact類似,也能移除冗餘減少儲存空間,但冗餘空間少了會導致後續增加文件size的update操作變慢。
雖然我們無法設定paddingFactor,但是可以使用usePowerOf2Sizes保證分配的空間是2的倍數,這樣也可以起到作用(MongoDB2.6版本起預設啟用usePowerOf2Size)。
或者手動實現padding:在插入文件的時候先用預設字元佔用一塊空間,等到真實資料寫入時,再unset掉它。

參考:
http://docs.mongodb.org/v2.4/core/record-padding/
http://docs.mongodb.org/v2.4/faq/developers/#faq-developers-manual-padding

17、usePowerOf2Size是什麼

這是為更有效的複用磁碟空間而設定的引數:分配的磁碟空間是2的倍數,如果超過了4MB,則是距離計算值最近的且大於它的完整MB數。
可以通過db.collections.stats()看到該值“userFlags”。
MongoDB2.6之後預設開啟usePowerOf2Size引數
使用後的效果可以看這裡的PPT:http://www.slideshare.net/mongodb/use-powerof2sizes-27300759

18、aggregate pipeline 指定運算完成輸出文件跟MapReduce相比有不足

(基於MongoDB2.6版本)MapReduce可以指定輸出到特定的db.collection中,例如:out_put = bson.SON([("replace", "collection_name" ), ("db", "xx_db")])
aggregate pipeline只能指定collection名字,也就意味著資料只能寫入到本db,同時結果不能寫入到capped collection、shard collection中。
相比之下,aggregate pipeline限制是比較多的,如果我們需要把結果放到某個DB下,則需要再做一次遷移:
db.runCommand({renameCollection:"sourcedb.mycol",to:"targetdb.mycol"})
但是!!上面的這條命令要求在admin下執行,且只能遷移往同shard下的DB,且被遷移的collection不能是shard的。
附錯誤碼資訊:
https://github.com/mongodb/mongo/blob/master/src/mongo/s/commands_public.cpp#L778
uassert(13140, "Don't recognize source or target DB", confFrom && confTo);
uassert(13138, "You can't rename a sharded collection", !confFrom->isSharded(fullnsFrom));
uassert(13139, "You can't rename to a sharded collection", !confTo->isSharded(fullnsTo));
uassert(13137, "Source and destination collections must be on same shard", shardFrom == shardTo);
參考:http://docs.mongodb.org/manual/reference/method/db.collection.mapReduce/#mapreduce-out-mtd

19、殺掉MongoD程序的幾種方式

1)進入到MongoD的命令列模式執行shutdown,
eg: 
$ mongo --port 10001
> use admin
> db.shutdownServer()
2)1方式的簡化:
eg:mongo admin --port 10001 --eval "db.shutdownServer()"
3)使用MongoD命令列關閉,需要指定db路徑:
mongod --dbpath ./data/db --shutdown

20、叢集的shard key慎重採用hash

如果你的日誌是有日期屬性的,那麼shard key不要使用hash,否則刪除過期日誌時無法成塊刪除;在更新日誌的時候,也不能利用區域性性原理,查詢、更新、插入資料都會因此而變慢。一般來說,hash id應付小資料量時壓力不大,但在資料量較大(熱資料大於可用記憶體容量)時,CRUD效能極差,且會放大碎片對效能的影響:資料非常分散,當有過期日誌被刪除後,這些刪除後的空間成為碎片,可能會因為磁碟預讀策略被載入到記憶體中。另外,採用hash shard key還會浪費掉一個索引,浪費不少空間。

21、副本數也不用太多

如果你的副本數量超過了12個(MongoDB3.0.0超過了50個),那麼就要選擇使用 master-slave ,但這樣會失去故障自恢復功能,主節點故障時,需要手動去切換到無故障節點。

22、mongos的config server配置資訊中不要使用localhost、127.0.0.1

啟動mongos時,config server的配置資訊不得使用localhost、127.0.0.1,否則新增其它機器的shard時,會出現錯誤提示:
"can’t use localhost as a shard since all shards need to communicate. either use all shards and configdbs in localhost or all in actual IPs host: xxxxx isLocalHost"

以新的config server啟動mongos,也需要重啟config server,否則會有錯誤提示:
“could not verify config servers were active and reachable before write”

如果改完後面又出現 “mongos specified a different config database string”  錯誤,那麼還需要重啟mongod,

修改了config server 幾乎是要全部例項重啟。另外,在配置replica set時也不得使用localhost、127.0.0.1。
參考:http://stackoverflow.com/questions/21226255/where-is-the-mongos-config-database-string-being-stored

23、shard key的選擇跟update效能緊密關聯

分散式MongoDB,shard key的選擇跟update效能,甚至是update可用性有很大關係,需要注意。
1、在對文件個別欄位update時,如果query部分沒有帶上shard key,效能會很差,因為mongos需要把這條update語句派發給所有的shard 例項。
2、當update 的upsert引數為true時,query部分必須帶上 shard key,否則語句執行出錯,例子:
mongos> db.test.update({"_id":".7269993106A92327A89ABCD70D46AD5"}, {"$set":{"P": "aaa"}, "$setOnInsert":{"TEST":"a"}}, true)
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 61,
"errmsg" : "upsert { q: { _id: \".7269993106A92327A89ABCD70D46AD5\" }, u: { $set: { P: "aaa" }, $setOnInsert: { TEST: \"a\" } }, multi: false, upsert: true } does not contain shard key for pattern { _: 1.0, B: 1.0 }"
}
})
這是因為如果沒有shard key,mongos既不能在所有shard例項上執行這條語句(可能會導致每個shard都插入資料),也無法選擇在某個shard上執行這條語句,於是出錯了。
另外,需要特別注意,如果使用pymongo引擎,它不會告訴你出錯了,只是函式呼叫陷入不返回,在shell下執行才能看到錯誤資訊。

附:
以下英文部分來自:https://jira.mongodb.org/browse/SERVER-13010
It's actually not clear to me that this is something we can support - problem is this:
> db.coll.update({ _id : 1 }, { }, true);
> db.coll.find()
{ "_id" : ObjectId("53176700a2bc4d46c176f14a") }
Upserts generate new _ids in response to this operation, and therefore we can't actually target this correctly in a sharded environment. The shard on which we need to perform the query may not be the shard on which the new _id is placed.
意思是說,upsert產生了新的_id,_id就是shard key,但是如果query裡沒有shard key,它們不知道要到哪個shard上執行這個命令,upsert產生的shard key可能並不是執行這條命令的shard的。
另外,如果_id不是shard key我們的例子也是不能成功的,因為沒有shard key,這條upsert要在哪個shard上執行呢?不能像普通update那樣給所有的shard去做,否則可能導致插入多條。
參考:
https://jira.mongodb.org/browse/SERVER-13010
http://docs.mongodb.org/manual/core/sharding-shard-key/
http://stackoverflow.com/questions/17190246/which-of-the-following-statements-are-true-about-choosing-and-using-a-shard-key

24、通過repairDatabase提高效能

從db.stats()中可以看到幾個跟碎片相關的關鍵欄位,dataSize,表示資料的大小,它包含了padding的空間;storageSize,表示這些資料儲存佔用的空間,包含了dataSize和被刪除資料所佔空間,可以認為storageSize/dataSize就是磁碟碎片比例,當刪除、update文件比較多後,它會變大,考慮做repairDatabase,以減少碎片讓資料更緊湊,在實踐中,這對提高CURD效能極其有用。repairDatabase時需要注意:它是把資料拷貝到新的地方,然後再做處理,所以repair之前在DB目錄所在磁碟需要預留一倍的空閒磁碟空間,如果你發現磁碟空間不足,可以停止服務,然後增加一塊新磁碟,再執行例項級別的repair,並指定--repairpath為新磁碟路徑,eg:mongod --dbpath /path/to/corrupt/data --repair --repairpath /media/external-hd/data/db,例項的資料會拷貝到/media/external-hd/data/db上做處理。

參考:《MongoDB——The Definitive Guide 2nd Edition》page325

25、索引欄位的長度不能大於1024位元組

索引欄位的長度不能大於1024位元組,否則shell下會有插入錯誤提示:"errmsg" : "insertDocument :: caused by :: 17280 Btree::insert: key too large to index”。 使用pymongo的“continue_on_error”引數,不會發出錯誤提示,要注意。

26、修改索引的expireAfterSeconds之後,負載均衡失敗

修改索引的expireAfterSeconds之後,負載均衡失敗,出現錯誤提示“2015-06-05T09:59:49.056+0800 [migrateThread] warning: failed to create index before migrating data.  idx: { v: 1, key: { _: -1 }, name: "__-1", ns: "cswuyg_test.cswuyg_test", expireAfterSeconds: 5227200 } error: IndexOptionsConflict Index with name: __-1 already exists with different options 檢查發生moveChunk的兩個shard,並沒有發現不一致,懷疑存在快取,重啟所有shard解決。

27、config DB無法寫入

因config DB無法修改,只可讀,導致drop、enablesharding失敗: config server 相關日誌:2015-06-11T16:51:19.078+0800 [replmaster] local.oplog.$main Assertion failure isOk() src/mongo/db/storage/extent.h 80 mongos 相關日誌: [LockPinger] warning: pinging failed for distributed lock pinger 'xxx:1234/xxx:1235:1433993544:1804289383'. : : caused by :: isOk() 這是同事遇到的問題,不確定是什麼操作引起的。重啟、configdb做repair均無法解決。 最後通過dump、restore解決:(1)把舊configdb dump出來;(2)restore到新的configure server;(3)mongos採用新的configure server;(4)重啟全部mongod。

28、sort()方法的size限制

當我對一個沒有建索引的欄位做find,然後做sort的時候,可能觸發sort的size的32MB限制,例如:

1 2 3 4 5 cswuyg> db.stotal.find({}).sort({'type':-1}) Error: error: { "$err" "Executor error: Overflow sort stage buffered data usage of 33554493 bytes exceeds internal limit of 33554432 bytes", "code" : 17144 }
有兩種解決方法: 解決方法一:對需要排序的欄位建索引 db.stotal.ensureIndex({'type': -1}) 解決方法二:修改預設配置,把sort時可以用的記憶體設定大點:cswuyg> db.adminCommand({setParameter:1, internalQueryExecMaxBlockingSortBytes:335544320}) 參考:http://askubuntu.com/questions/501937/how-to-increase-buffered-data-limit 這兩種解決方法各有利弊:(1)增加了索引會導致資料寫入變慢,儲存佔用變多;(2)不建索引修改預設配置,會導致sort的時候佔用更多的記憶體。