MongoDB設計命名規範


1.      

1.       庫名全部小寫,禁止使用任何`_`以外的特殊字符,禁止使用數字打頭的庫名,如:`123_abc`

2.       庫以文件夾的形式存在,使用特殊字符或其它不規範的命名方式會導致命名混亂;

3.       數據庫名最多爲64字符;

4.       在創建新的庫前應儘量評估該庫的體積、QPS等,提前與DBA討論是應該新建一個庫還是專門爲該庫創建一個新的集羣;

某開發在拿到DBA提供的MongoDB後由於MongoDB的權限控制比較寬鬆,導致該業務的開發在創建集合的時候懶得與DBA討論,而是隨意的將所有集合都創建在一個庫中,最初並沒有什麼問題,因爲業務的請求量並不大。半年後,該業務增長到了一個比較大的量級,而此時開發人員上線了一個新的項目,該項目的寫入量很大,大部分都爲批量更新,由於所有集合都存放在一個庫中,這個新項目的批量更新帶來了頻繁的鎖、I/O平均等。最後開發配合DBA一起將該庫拆散到了多個新的庫中,將一庫N集合轉換爲單庫單集合,性能問題迎刃而解。

2.       集合

1.       集合名全部小寫,禁止使用任何`_`以外的特殊字符,禁止使用數字打頭的集合名,如:`123_abc`,禁止system打頭; system是系統集合前綴;

2.       集合名稱最多爲64字符;

3.       一個庫中寫入較大的集合會影響其它集合的讀寫性能,如果業務比較繁華的集合在一個DB中,建議最多80個集合,同時也要考慮磁盤I/O的性能;

4.       如果評估單集合數據量較大,可以將一個大表拆分爲多個小表,然後將每一個小表存放在獨立的庫中或者sharding分表;

5.       MongoDB的集合擁有“自動清理過期數據”的功能,只需在該集合中文檔的時間字段增加一個TTL索引即可實現該功能,但需要注意的是該字段的類型則必須是mongoDate(),一定要結合實際業務設計是否需要;

6.       設計輪詢集合---集合是否設計爲Capped限制集,一定要結合實際業務設計是否需要;

7.       創建集合規則

不同的業務場景是可以配置進行不同的配置;

a.       如果是讀多寫少的表在創建時我們可以儘量將 page size 設置的比較小,比如 16KB,如果表數據量不太大(

"internal_page_max=16KBleaf_page_max=16KBleaf_value_max=8KBos_cache_max=1GB"

b.       如果這個讀多寫少的表數據量比較大,可以爲其設置一個壓縮算法,例如:

"block_compressor=zlibinternal_page_max=16KBleaf_page_max=16KBleaf_value_max=8KB"

c.       注意:該zlib壓縮算法不要使用,對cpu消耗特別大,如果使用snapp消耗20% cpu,而且使用zlib能消耗90%cpu,甚至100%

d.       如果是寫多讀少的表,可以將 leaf_page_max 設置到 1MB,並開啓壓縮算法,也可以爲其制定操作系統層面 page cache 大小的os_cache_max 值,讓它不會佔用太多的 page cache 內存,防止影響讀操作;

e.       案例

db.createCollection(

   "logs",

   { storageEngine: { wiredTiger: {configString: "internal_page_max=16KB,leaf_page_max=16KB,leaf_value_max=8KB,os_cache_max=1GB" } } }

)

f.       說明

讀多寫少的表

internal_page_max=16KB   默認爲4KB

leaf_page_max=16KB       默認爲32KB

leaf_value_max=8KB       默認爲64MB

os_cache_max=1GB         默認爲0

 

讀多寫少的表  而且數據量比較大

block_compressor=zlib    默認爲snappy

internal_page_max=16KB   默認爲4KB

leaf_page_max=16KB       默認爲32KB

leaf_value_max=8KB       默認爲64MB

3.       文檔

1.       文檔中的key禁止使用任何`_`以外的特殊字符;

2.       儘量將同樣類型的文檔存放在一個集合中,將不同類型的文檔分散在不同的集合中;相同類型的文檔能夠大幅度提高索引利用率,如果文檔混雜存放則可能會出現查詢經常需要全表掃描的情況;

3.       禁止使用_id,如:向_id中寫入自定義內容;

某業務的MongoDB在放量後出現嚴重的寫入性能問題,大致爲:寫入達到300/s的時候IO跑滿,排查中發現,該業務在設計的時候爲了方便, 而將_id中寫入了無序的類似md5的數據。MongoDB的表與InnoDB相似,都是索引組織表,數據內容跟在主鍵後,_idMongoDB中的默認主鍵,一旦_id的值爲非自增,當數據量達到一定程度之後,每一次寫入都可能導致主鍵的二叉樹大幅度調整,這將是一個代價極大的寫入, 所以寫入就會隨着數據量的增大而下降,所以一定不要在_id中寫入自定義的內容。

4.       儘量不要讓數組字段成爲查詢條件

某業務在一個表的數組字段上創建了一個索引,創建完畢之後發現表體積增大了很多很多,排查發現是由於索引體積的大幅度增大導致在MongoDB,如果爲一個數組字段添加索引,那麼MongoDB會主動爲這個數組中的所有元素依次添加獨立索引,例如: 爲數組字段{a:[x,y,z]}添加索引{a:1},實際上添加的索引爲:

{a:[x:1]}

{a:[y:1]}

{a:[z:1]}

該業務的數組字段中有11個元素,那麼等於一次創建了11條索引,這是索引體積大幅度增大的根本原因。另外,如果組合索引中存在數組字段,那麼MongoDB會爲每一個元素與其它字段的組合創建一個獨立的索引,例如: 爲數組字段{a:[x,y,z]}{b:qqq}添加索引{a:1,b:1},實際上添加的索引爲:

{a:[x:1],b:1}

{a:[y:1],b:1}

{a:[z:1],b:1}

如果一個組合索引中存在兩個數組字段,那麼索引的數量將是兩個數組字段中元素的笛卡兒積,所以MongoDB不允許索引中存在一個以上的數組字段。

5.       如果字段較大,應儘量壓縮存放

某業務上線後一直很正常,但在放量3倍之後發現MongoDB服務器的網卡流量報警,IO壓力報警,排查中發現,該業務講一個超長的文本字段存放在MongoDB,而這個字段的平均體積達到了7K。在併發爲2000QPS的場景下,每次取出1~20條數據,導致這個MongoDB每秒鐘要發送將100MB的數據,而對於數據庫而言,讀寫均爲隨機IO,所以在如此大的數據吞吐場景中,IO達到了報警閾值。

由於文本是一個容易壓縮的樣本, 所以我們對該字段進行了壓縮存放,使其平均體積降低到了2K,而解壓在業務端進行處理,最終將吞吐降低到了20MB/S左右。

如果字段較大且會成爲查詢條件,例如一長串的url,儘量轉成md5後存放

某業務上線前進行壓力測試,測試中發現某個場景下的查詢性能不夠理想,排查中發現該場景的查詢條件類似:{url:xxxx},url字段中的值大部分都很長很長,該字段的平均體積達到了0.5K,在這種情況下索引的體積會變得很大從而導致雖然請求雖然能夠走索引但效率並不夠理想,於是dba配合業務開發一起對該場景進行優化:

1.將該字段的存放的內容由真實的url改爲url內容md5後的值,字段體積得到了大幅度縮小,固定在了32

2.查詢時,用戶請求通過url查詢,而此時程序會將該url進行md5,然後用得到的值進行查詢,由於所以體積大幅度縮小,所以查詢速度有了極大的提高,優化完畢後再次進行壓力測試,性能達標,爲之前的6倍。

6.       由於MongoDB是大小寫敏感的,如果字段無需大小寫敏感,爲了提高查詢效率應儘量存放統一了大小寫後的數據,如:全部小寫或爲該字段增加一個統一了大小寫的輔助字段;

某業務需要根據字段{a:XxX}來進行查詢,MongoDBa的值是大小寫敏感的,並且無法配置爲忽略大小寫,但該業務場景爲了滿足查詢需求而需要忽略大小寫,這個大小寫敏感與否的矛盾導致業務需要使用正則來進行匹配:{a:/xxx/i},i參數在正則中表示忽略大小寫,上線後發現, 查詢性能非常低下,在一個擁有200萬文檔的集合中一次查詢需要消耗2.8~7,並發達到50QPS的時候MongoDB實例所在服務器的CPU就跑到了973%

MongoDB在查詢條件中使用正則的時候,能夠像普通精確匹配一樣使用索引達到高效率的查詢,但一旦使用了參數i來忽略大小寫查詢優化器就需要對每一個數據的大小寫進行調整然後再進行匹配,此時這個請求就變成了全表掃描,這就是效率低下的根本原因。

對於這種場景可以採用新建一個統一了大小的字段,例如全部小寫:假設原字段爲:{a:aAbB},那麼爲其添加一個全部爲小寫的對應字段:{a_low:aabb}然後通過字段a_low進行查詢就能達到精確匹配,按照該方案改進後,該場景的查詢耗時降低到了2毫秒雖然新增字段導致實例會變大一些,但對於換來性能的大幅度提升還是非常值得的。

7.       不要存放太長的字符串,如果這個字段爲查詢條件,那麼確保該字段的值不超過1KB

8.       MongoDB的索引僅支持1K以內的字段,如果你存入的數據長度超過1K,那麼它將無法被索引;

4.       索引

1.       MongoDB 的組合索引使用策略與 MySQL 一致,遵循“最左原則”;

2.       索引名稱長度不要超過128字符;

3.       應儘量綜合評估查詢場景,通過評估儘可能的將單列索引併入組合索引以降低所以數量,結合12;

MongoDB的組合索引規則和MySQL一樣,都遵循最左原理,假設一個組合索引爲:{a:1,b:1,c:1},那麼以下條件的查詢是可以被用到的:

{a:1}

{a:1,b:2}

{a:1,b:2,c:3}

{}

以下條件的查詢是不能用到索引的:

{b:1}

{b:1:c:2}

{c:2}

另外在設計索引的時候可以通過該原理減少索引的數目,如果需要通過{a:xxx}{a:xxx,b:xxx}來進行查詢,那麼創建索引:

{a:1,b:1}

即可同時滿足這兩個查詢場景而無需再單獨創建{a:1}索引。

4.       在創建組合索引的時候,應評估索引中包含的字段,儘量將數據基數大(唯一值多的數據)的字段放在組合索引的前面;

某業務某場景下的查詢十分緩慢,大概需要1.7秒左右,需要進行調優,該場景的查詢和對應索引如下:

查詢:{name:baidu,status:0}

索引:{status:1,name:1}

乍一看沒什麼問題,因爲查詢和索引十分匹配,但對該集合分析後發現該集合一共有150萬文檔,status=0的有1499930由於這基本上佔了99%的文檔數目(數據基數很小),所以導致雖然使用了索引,但依然需要從149萬行數據中找到name=baidu的數據但name字段則有大量不同的數據(數據基數很大),所以如果將該組合索引調整爲name在前,該查詢即可先通過name字段抽出較少的數據,再通過status進行過濾,就快了:

{name:1.status:1}調整後查詢耗時降低到3~5毫秒。

5.       在數據量較大的時候,MongoDB 索引的創建是一個緩慢的過程,所以應當在上前線或數據量變得很大前儘量評估,按需創建會用到的索引;

6.       MongoDB 支持 TTL 索引,該索引能夠按你的需要自動刪除XXX秒之前的數據並會盡量選擇在業務低峯期執行刪除操作;看業務是否需要這一類型索引;

7.       如果你存放的數據是地理位置信息,比如:經緯度數據。那麼可以在該字段上添加 MongoDB 支持的地理索引:2d 2dsphere,但他們是不同的,混用會導致結果不準確;

2d:只能用於點對點索引,適用於平面地圖和時間連續的數據,比如非常適用於遊戲地圖【2dsphere:允許指定點、線和多邊形。適用於地球表面類型的地圖(球體) 】如果在球體表面創建2d索引,則會導致極點附近出現大量扭曲變形,最終導致結果不準確;

8.       MongoDB 的全文索引目前仍然處於“實驗”階段,性能並不夠理想,當前不建議使用;

9.       MongoDB2.4開始,支持索引的 ICP 功能,可以通過其合理減少索引數量;

MongoDB2.4開始,組合索引能夠被更有效的利用,如:

索引{x:1,y:1,z:1}可以被查詢{x:1,z:1}所利用如果x字段的數據基數很大,而該條件匹配到的數據有很少,在這種情況下無需專門添加{x:1,z:1}索引,索引{x:1,y:1,z:1}即可帶來理想的性能但需要注意的是,ICP 性能並沒有原生的連續的組合索引效率好,如果發現效率不佳那麼還是需要添加單獨的{x:1,z:1}索引;

10.   創建索引要在後臺創建,避免阻塞業務正常DML和查詢

db.works.createIndex({plan:1,trainingpoints:1,cmsOrder:1,stateValue:1},{background:true})

a.       添加唯一索引

db.bodys.createIndex({user:1,date:-1},{unique:true,background:true}) 唯一索引  3.2以下必須這樣加唯一索引;

b.       添加帶有數組好其他列的索引

db.antis.createIndex({"action.last":1,refe_type:1,_id:1},{background:true})

其中action.last是數組

spacer.gif

 

c.       TTL索引,字段create_date180天后自動清理數據

db.orders.createIndex({"create_date":1},{"expireAfterSeconds":15552000})

d.       案例說明

創建位置和狀態索引,爲了能快速處理“某地未處理訂單”查詢,這是一個多條件的查詢,所以是一個複合索引,

status字段放在前面,因爲多數的查詢都會依賴狀態字段

db.order.createIndex({"status":1, "delivery.city":1,"delivery.address":1})

在這個Demo裏,還有一種加快查詢速度的方法就是,創建一個只包含指定狀態的一個Partial Indexes索引。

比如status必須爲delivering 才加入到索引中,有效控制索引的大小,加快查詢速度。

db.order.createIndex({"delivery.city":1,"delivery.address":1},{partialFilterExpression:{'status':{$eq:"delivering"}}})

e.        

11.   創建索引建議:先做等值查詢,在做排序,在做範圍查詢。

5.       實踐操作性能

1.       索引中的-11是不一樣的,一個是逆序,一個是正序,應當根據自己的業務場景建立適合的索引排序,需要注意的是{a:1,b:-1} {a:-1,b:1}是一樣的;

2.       在開發業務的時候儘量檢查自己的程序性能,可以使用explain() 函數檢查你的查詢執行詳情,另外 hint() 函數相當於 MySQL 中的force index()

3.       查詢中的某些 $ 操作符可能會導致性能低下,如 $ne$not$exists$nin$or,儘量在業務中不要使用

a.       $exist:因爲鬆散的文檔結構導致查詢必須遍歷每一個文檔

b.       $ne:如果當取反的值爲大多數,則會掃描整個索引

c.       $not:可能會導致查詢優化器不知道應當使用哪個索引,所以會經常退化爲全表掃描

d.       $nin:全表掃描

e.       $or:有多少個條件就會查詢多少次,最後合併結果集,所以儘可能的使用 $in

4.       如果你結合體積大小/文檔數固定,那麼建議創建 capped(封頂)集合,這種集合的寫入性能非常高並無需專門清理老舊數據,需要注意的是 capped 表不支持r emove() update()

5.       在寫入數據的時候,如果你需要實現類似 MySQL INSERT INTO ON DUPLICATE KEY UPDATE 的功能,那麼可以選擇 upsert() 函數;

db.analytice.update(

{"url":"/blog"},

{"$inc":{"visits":1}},

true

)

3個參數表示,這是upsert

6.       不要一次取出太多的數據進行排序,MongoDB 目前支持對32MB以內的結果集進行排序,如果需要排序,那麼請儘量限制結果集中的數據量;

7.       MongoDB 的聚合框架非常好用,能夠通過簡單的語法實現複雜的統計查詢,並且性能也不錯;

8.       如果需要清理掉一個集合中的所有數據,那麼 remove() 的性能是非常低下的,該場景下應當使用 drop()

remove() 是逐行操作,所以在刪除大量數據的時候性能很差;

9.       寫入大量數據的時候可以選擇使用 batchInsert,但目前MongoDB 每一次能夠接受的最大消息長度爲48MB,如果超出48MB,將會被自動拆分爲多個48MB的消息;

10.   在使用數組字段做爲查詢條件的時候,將於覆蓋索引無緣;

這是因爲數組是保存在索引中的,即便將數組字段從需要返回的字段中剔除,這樣的索引仍然無法覆蓋查詢;

11.   在查詢中如果有範圍條件,那麼儘量和定值條件放在一起進行過濾,並在創建索引的時候將定值查詢字段放在範圍查詢字段前;

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章