本文主要讨論這幾個問題:
Mongo shell中使用大整數字面量
片鍵使用自增長字段
程式裡遊标循環疊代過程中進行長時間的操作
濫用數組類型
濫用upsert更新參數
錯誤的設計索引
錯誤的認為複制等于備份
(本文讨論在社群交流群以及工作開發過程中常見的一些錯誤。)
Mongo shell中使用大整數字面量,但預設整數字面量類型卻是雙精度浮點數,導緻丢失精度
問題描述:
通過mongo shell插入或更新一個大整數(長度約大于等于16位數字)時,例如:
但實際上查詢發現,插入的123456789111111111變為另外一個值123456789111111100,如下:
分析:
由于mongo shell實際上是一個js引擎,而在javascript中,基本類型中并沒有int或long,所有整數字面量實際上都以雙精度浮點數表示(IEEE754格式)。64位的雙精度浮點數中,實際是由1bit符号位,11bit的階碼位,52bit的尾數位構成。
11bit的餘-1023階碼使得雙精度浮點數提供大約-1.7E308~+1.7E308的範圍,52bit的尾數位大概能表示15~16位數字(部分16位長的整數已經超出52bit能表示的範圍)。
是以當我們在mongo shell中直接使用整數字面量時,實際上它是以double表示的,而當這個整數字面量大約超過16位數字時,就可能發生有些整數無法精确表示的情況,隻能使用一個接近能表示的整數來替代。如上面例子中,存入18位的數字123456789111111111,實際上能有效表示的數字隻有16位,另外兩位發生精度丢失的情況。
解決方法:
使用NumberLong()函數構造長整型的包裝類型,記住傳入的參數一定要加雙引号,否則使用整數字面量的話又會被當做double而可能丢失精度。
注意,除了在mongo shell(javascript語言環境中),在其他不支援長整型而預設使用浮點數代替表示的程式設計語言中也會存在類似問題,操作時一定要留意。
關于雙精度浮點格式詳情,可以參考:
a)《雙精度浮點數格式》:
https://en.wikipedia.org/wiki/Double-precision_floating-point_format
b)《程式設計卓越之道-第一卷:深入了解計算機》- 第4章 浮點表示法
片鍵使用自增長字段,導緻寫熱點
- 問題描述:
使用ObjectId或時間戳等具有自增長性質(并不一定是嚴格自增長,大緻趨勢符合也行)的值類型作為分片集合片鍵時,新寫入資料的請求始終都路由到同一個分片節點。
- 分析:
如下圖,MongoDB使用片鍵的範圍來對資料分區,每個範圍(連續且不重疊),對應一個資料塊(Chunk)。是以當片鍵是自增長類型時,插入的資料實際上都是落在一個Chunk存儲的範圍内,導緻所有寫入請求都路由到這個Chunk所在的分片,進而導緻這個節點成為寫熱點,寫負載不能均衡的分擔到叢集中的多個分片節點,進而喪失了通過分片叢集橫向擴充寫性能的意義。

解決方法:
a). 使用随機值類型的字段作為片鍵,例如version 4 UUID (Random UUID)
b) .對自增長型字段建立哈希索引,建立片鍵時通過hashed選項,指定使用該哈希索引值作為片鍵,例如: 關于如何設計片鍵,可以參考:
a)《深入學習MongoDB》- 3.1節 選擇片鍵
b)《片鍵 – 搭建MongoDB分片叢集之關鍵》:
http://www.mongoing.com/blog/post/on-selecting-a-shard-key-for-mongodb
程式裡遊标循環疊代過程中進行長時間的操作
- 問題描述:
大概類似如下代碼描述的操作方式,程式中可能經常會遇到這樣的需求,取出一個文檔後,需要對這個文檔做一些比較耗時的複雜處理。
分析:
在MongoDB伺服器端,也會為相應查詢維護一個遊标對象,遊标會消耗記憶體和其他資源(比如鎖,CPU等)。
遊标隻有在周遊完了所有查詢的結果以後,或者用戶端主動發來消息要求終止(比如到達遊标使用逾時時間,預設是10分鐘,或者是用戶端檢測到用戶端遊标已經不再使用時),MongoDB才會銷毀遊标,釋放其占用的資源,是以我們應該盡快釋放遊标,特别是當我們的系統面對的是網際網路應用這樣高并發的業務場景時,我們應該盡可能的不要浪費資料庫端資源,基本原則應該做到減少占用時間,不用時要盡快關閉遊标。
- 解決方法:
按需而取,通過查詢過濾條件,limit方法,盡量限制遊标疊代文檔數量。
大部分業務場景,通常我們并不需要在疊代遊标過程中完成這些處理操作,如果是這樣,我們可以類似如下處理,盡快的疊代遊标,将資料送出給隊列讓另外的線程異步處理,以便能盡快釋放遊标連接配接:
參考:
-
遊标介紹:
https://docs.mongodb.com/manual/reference/method/db.collection.find/index.html
-
疊代遊标:
https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#read-operations-curso
濫用數組類型
- 問題描述:
在社群的讨論群中,經常會有同學讨論使用MongoDB實作類似微網誌的關注和粉絲功能,考慮用數組來儲存關注好友或者粉絲。
- 分析:
将某個使用者的粉絲或者關注好友,儲存在該使用者文檔的數組字段中,雖然這樣設計結構看似很直覺,在讀取時也很高效,一次檢索就可以将該使用者的基本資訊及其粉絲和關注好友都取出來。
但問題是,首先,在MongoDB中文檔有大小限制,目前版本中每個文檔最大不能超過16M,是以使用内嵌文檔存儲無法滿足粉絲或關注好友增長的需求,大使用者節點可能将會有大量粉絲或關注使用者,超過16M,屆時程式将很難擴充。其次,面對排重,排序,過濾篩選等一些複雜需求,使用數組存儲将導緻操作複雜,性能低下。
- 解決方法:
在使用數組前,我們應該充分評估,結合數組的特性,從業務的讀寫場景、将來的擴充、查詢寫入性能、操作維護是否簡單等各方面考慮數組是否真的滿足我們的需求,不要盲目的進行資料結構設計和開發。
當然,如果存儲的元素數量有限,且不會對其進行一些複雜的操作,使用内嵌數組将是很好的方式,它可以減少檢索次數,提升讀操作性能。
另外,就是在查詢時使用project操作,隻傳回需要的元素和字段,而不是整個内嵌數組,以免浪費帶寬。
- 參考:
-
内嵌文檔/數組的資料模型:
https://docs.mongodb.com/manual/core/data-model-design/#data-modeling-embedding
-
查詢内嵌數組:
https://docs.mongodb.com/manual/tutorial/query-arrays/
https://docs.mongodb.com/manual/tutorial/query-array-of-documents/
-
對查詢的數組使用投影(project)操作:
https://docs.mongodb.com/manual/tutorial/project-fields-from-query-result
濫用upsert更新參數
- 問題描述:
在我們的業務場景中,通常都同時有插入(insert)資料和更新(update)資料的需求,很多時候,我們無法判斷正要寫入的資料是否已經存在于資料庫中,對于這種情況,MongoDB為update操作提供了upsert選項,使得我們在一個操作中能自動處理上述情況,即當資料庫不存在寫入資料時,執行insert操作,當資料庫已經存在寫入資料,則執行update操作。(不過,這裡要注意,由于并發操作,我們可能會同時對相同資料執行upsert操作,此時可能會造成寫入資料重複。為了避免這種情況,應該對upsert操作的query字段建立唯一索引進行限制)。
但很多時候,即使我們能夠在寫入之前分辨資料是插入還是更新,但由于程式員“懶”這個特性,都會仍然對所有寫操作使用update(upsert=true),而不是區分的使用insert和update。
- 分析:
不加區分的使用upsert,雖然簡化了我們程式的書寫邏輯,但是是以也帶來了寫入性能的損失。upsert操作在寫入前都會先根據查詢條件檢索一次,判斷後再進行操作,同時為了避免并發寫入導緻重複資料,還需要對query的字段建立唯一索引進行限制,寫入時維護索引的開銷,進一步降低了寫入性能。作者在之前的開發中測試過,不加區分的使用upsert和加以區分的使用insert、update兩種情況,性能相差差不多1倍。
- 解決方法:
慎用upsert參數,當我們在寫入前可以區分資料是否已經存在資料庫中時,在程式中進行判斷,區分的使用insert和update操作。
- 參考:
a). upsert參數:
https://docs.mongodb.com/manual/reference/method/db.collection.update/#upsert-parameter
錯誤的設計索引
- 問題描述:
通常,我們開發中遇到的大部分讀性能問題,可能都是因為沒有為查詢、排序操作建立索引,或者建立了錯誤的索引導緻的。特别是在資料量比較大的情況,由于沒有利用上索引,導緻全表掃描,資料庫需要從磁盤讀取大量資料到緩存,占用大量的記憶體,磁盤IO,CPU等系統資源,由于對這些資源的争用,同時也可能會影響到期間進行的寫入操作。
- 解決方法:
- 首先,我們要充分了解資料庫索引設計的一些原則和技巧。
- 其次,結合業務中對資料的檢索需求,設計合适的索引:
a). 有哪些字段的檢索需求,是否有範圍查詢需求,是否有排序需求,需要檢索字段的選擇性如何。将這些需求和資料情況一一列出,為我們後續建立索引提供依據。
b). 是否可以建立複合索引,複合索引字段如何組織順序,才能使得複合索引能夠覆寫更多的查詢需求,滿足範圍查詢的需求,滿足排序的需求(通常複合索引中,按照等值查詢、排序、範圍查詢的順序來組織索引字段,同時結合考慮索引選擇性,是否其他查詢能複用複合索引的左字首)。索引是否能覆寫查詢,使得檢索性能最優。
c). 通過explain檢視執行計劃,判斷我們的查詢和排序是否能夠用上索引,是否用上我們預期那個最合理的索引。
d). 檢查我們設計的索引是否有重複索引、無用索引,是否缺失索引。比如複合索引已經能覆寫某些單字段索引。業務查詢調整等原因,有些索引已經不再使用。通過慢查詢日志,發現有些查詢沒有索引,嚴重影響系統性能。及時删除重複的、不再使用的索引,為嚴重影響性能的查詢補上合适的索引。
- 參考:
a)MongoDB索引介紹:
https://docs.mongodb.com/manual/indexes/index.html
b)《資料庫索引設計和優化》,這本書雖然比較老,還是非常值得參考。
錯誤的認為複制等于備份
- 問題描述:
MongoDB提供了副本集的部署模式,通過主從的複制架構設計,從節點通過複制主節點的資料,為資料提供了多個副本,并且通過選舉機制,在主節點挂掉後,自動選舉一個從節點成為新的主節點,實作自動故障轉移,保證系統的高可用性。
但是很多同學誤解了高可用和複制,将其作用等同于備份,進而忽視了備份的重要性,甚至導緻資料丢失無法恢複的後果,跑路事件時有發生。
- 分析:
通過複制實作的高可用架構,并不能代替備份操作。當我們誤操作,或者誤操作後沒有及時處理時(即使在副本集中通過延遲節點留給我們一些緩沖時間),副本也會同步這些誤操作,導緻資料受到破壞,如果此時我們沒有備份資料,資料将無法恢複,進而可能帶來無法避免的後果。
另外,即使是高可用架構,99.999%的高可用性,但你也可能命中注定是那0.001%的倒黴蛋。
是以,一定要備份,一定要備份,一定要備份。
- 解決方法:
當然,最好和最安全的解決方案,是通過MongoDB企業版提供的背景管理工具,比如ops manager進行全量備份,實時增量備份。
或者有技術能力的,可以通過結合參考Mongo Shake,MongoSync等開源同步複制工具 (注意不是備份),實作自己的實時增量備份工具。
- 參考:
-
MongoDB備份工具介紹:
https://docs.mongodb.com/manual/core/backups/index.html
-
MongoDB Ops Manager:
https://www.mongodb.com/products/ops-manager
-
Mongo Shake:
https://github.com/aliyun/mongo-shake
-
MongoSync:
https://github.com/Qihoo360/mongosync
/
作者簡介
/
鐘秋
- BBD技術經理,資深架構師。MongoDB中文社群聯席主席。
- 有豐富項目中應用MongoDB經驗,熟悉MongoDB互相模式設計及性能優化,熟悉大資料相關技術和網際網路及大資料應用架構設計。
原文釋出時間為:2018-09-13
本文作者: Mongoing中文社群
本文來自雲栖社群合作夥伴“ Mongoing中文社群”,了解相關資訊可以關注“ Mongoing中文社群”。