您好,登錄后才能下訂單哦!
MongoDB的最佳實踐是怎樣的,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
作為MongoDB的一名方案架構師,我的大部分時間都是在和MongoDB的客戶和用戶交互。在這里,我希望通過一個不斷更新的活文章的方式來為大家收集整理一下MongoDB開發及維護時候值得了解或者遵從的一些最佳實踐。我非常真切地希望您也可以參與進來,共同維護這個文檔,讓更多的用戶受惠。
MongoDB服務器在默認安裝下不啟用鑒權。這意味著每個人都可以直接連接到mongod實例并執行任意數據庫操作。建議按照文檔啟用鑒權 http://docs.mongoing.com/manual-zh/tutorial/enable-authentication.html
MongoDB支持按角色定義的權限系統。你應該基于“最少權限”準則,顯式的為用戶分配僅需要的相應權限。
盡可能使用LDAP、Kerbero之類的中央鑒權服務器,并使用強口令策略。
如果你的服務器有多個網卡,建議只在內網的IP上監聽服務。
MongoDB企業版支持存儲加密,對涉及到客戶的敏感數據應該使用加密引擎來保護數據。
MongoDB的建議最小部署是3個數據節點構成的復制集。復制集可以提供以下優點:
系統99.999% 高可用
自動故障切換
數據冗余
容災部署
讀寫分離
分片可以用來擴展你系統的讀寫能力,但是分片也會帶來不少新的挑戰比如說管理上的復雜度,成本的增加,選擇合適片鍵的挑戰性等等。一般來說,你應該先窮盡了其他的性能調優的選項以后才開始考慮分片,比如說,索引優化,模式優化,代碼優化,硬件資源優化,IO優化等。
分片的一些觸發條件為:
數據總量太大,無法在一臺服務器上管理
并發量太高,一臺服務器無法及時處理
磁盤IO壓力太大
單機系統內存不夠大,無法裝下熱數據
服務器網卡處理能力達到瓶頸
多地部署情況下希望支持本地化讀寫
取決于你分片的觸發條件,你可以按照總的需求 然后除以每一臺服務器的能力來確定所需的分片數。
分片之間的數據互相不復制。每個分片的數據必須在分片內保證高可用。因此,對每一個分片MongoDB要求至少部署3個數據節點來保證該分片在絕大部分時間都不會因為主節點宕機而造成數據不可用。
在分片場景下, 最重要的一個考量是選擇合適的片鍵。選擇片鍵需要考慮到應用的讀寫模式。通常來說一個片鍵要么是對寫操作優化,要么是對讀操作優化。要根據哪種操作更加頻繁而進行相應的權衡。
片鍵值應該具有很高的基數,或者說,這個片鍵在集合內有很多不同的值,例如_id就是一個基數很高的片鍵因為_id值不會重復
片鍵一般不應該是持續增長的,比如說timestamp就是個持續增長的片鍵。此類片鍵容易造成熱分片現象,即新的寫入集中到某一個分片上
好的片鍵應該會讓查詢定向到某一個(或幾個)分片上從而提高查詢效率。一般來說這個意味著片鍵應該包括最常用查詢用到的字段
好的片鍵應該足夠分散,讓新的插入可以分布到多個分片上從而提高并發寫入率。
可以使用幾個字段的組合來組成片鍵,以達到幾個不同的目的(基數,分散性,及查詢定向等)
MongoDB是一個高性能高并發的數據庫,其大部分的IO操作為隨機更新。一般來說本機自帶的SSD是最佳的存儲方案。如果使用普通的硬盤,建議使用RAID10條帶化來提高IO通道的并發能力。
MongoDB很多的性能瓶頸和IO相關。建議為日志盤(Journal和系統日志)單獨設定一個物理卷,減少對數據盤IO的資源占用。
系統日志可以直接在命令行或者配置文件參數內指定。Journal日志不支持直接指定到另外的目錄,可以通過對Journal目錄創建symbol link的方式來解決。
MongoDB在WiredTiger存儲引擎下建議使用XFS文件系統。Ext4最為常見,但是由于ext文件系統的內部journal和WiredTiger有所沖突,所以在IO壓力較大情況下表現不佳。
WiredTiger 對寫操作的落盤是異步發生的。默認是60秒做一次checkpoint。做checkpoint需要對內存內所有臟數據遍歷以便整理然后把這些數據寫入硬盤。如果緩存超大(如大于128G),那么這個checkpoint時間就需要較長時間。在checkpoint期間數據寫入性能會受到影響。目前建議實際緩存設置在64GB或以下。
Transparent Huge Pages (THP) 是Linux的一種內存管理優化手段,通過使用更大的內存頁來減少Translation Lookaside Buffer(TLB)的額外開銷。 MongoDB數據庫大部分是比較分散的小量數據讀寫,THP對MongoDB這種工況會有負面的影響所以建議關閉。
http://docs.mongoing.com/manual-zh/tutorial/transparent-huge-pages.html
防止MongoDB 的log文件無限增大,占用太多磁盤空間。好的實踐是啟用log rotation并及時清理歷史日志文件。
http://docs.mongoing.com/manual-zh/tutorial/rotate-log-files.html
足夠的Oplog空間可以保證有足夠的時間讓你從頭恢復一個從節點,或者對從節點執行一些比較耗時的維護操作。假設你最長的下線維護操作需要H小時,那么你的Oplog 一般至少要保證可以保存 H 2 或者 H3 小時的oplog。
如果你的MongoDB部署的時候未設置正確的Oplog 大小,可以參照下述鏈接來調整:
http://docs.mongoing.com/manual-zh/tutorial/change-oplog-size.html
禁止系統對文件的訪問時間更新會有效提高文件讀取的性能。這個可以通過在 /etc/fstab 文件中增加 noatime 參數來實現。例如:
/dev/xvdb /data ext4 noatime 0 0
修改完文件后重新 mount就可以:
# mount -o remount /data
Linux默認的文件描述符數和最大進程數對于MongoDB來說一般會太低。建議把這個數值設為64000。因為MongoDB服務器對每一個數據庫文件以及每一個客戶端連接都需要用到一個文件描述符。如果這個數字太小的話在大規模并發操作情況下可能會出錯或無法響應。 你可以通過以下命令來修改這些值:
ulimit -n 64000 ulimit -u 64000
在一個使用NUMA技術的多處理器Linux 系統上,你應該禁止NUMA的使用。MongoDB在NUMA環境下運行性能有時候會可能變慢,特別是在進程負載很高的情況下。
預讀值是文件操作系統的一個優化手段,大致就是在程序請求讀取一個頁面的時候,文件系統會同時讀取下面的幾個頁面并返回。這原因是因為很多時候IO最費時的磁盤尋道。通過預讀,系統可以提前把緊接著的數據同時返回。假設程序是在做一個連續讀的操作,那么這樣可以節省很多磁盤尋道時間。
MongoDB很多時候會做隨機訪問。對于隨機訪問,這個預讀值應該設置的較小為好.一般來說32是一個不錯的選擇。
你可以使用下述命令來顯示當前系統的預讀值:
sudo blockdev --report
要更改預讀值,可以用以下命令:
sudo blockdev --setra 32
把 換成合適的存儲設備。
在使用MongoDB復制集或者分片集群的時候,注意一定要使用NTP時間服務器。這樣可以保證MongoDB集群成原則之間正確同步。
關鍵的指標包括:
Disk Space 磁盤空間
CPU
RAM 使用率
Ops Counter 增刪改查
Replication Lag 復制延遲
Connections 連接數
Oplog Window
默認情況下MongoDB會在日志文件中(mongod.log)記錄超過100ms的數據庫操作。
這個是針對于數據量較大比如說超過幾十上百萬(文檔數目)數量級的集合。如果沒有索引MongoDB需要把所有的Document從盤上讀到內存,這會對MongoDB服務器造成較大的壓力并影響到其他請求的執行。
如果你的查詢會使用到多個字段,MongoDB有兩個索引技術可以使用:交叉索引和組合索引。交叉索引就是針對每個字段單獨建立一個單字段索引,然后在查詢執行時候使用相應的單字段索引進行索引交叉而得到查詢結果。交叉索引目前觸發率較低,所以如果你有一個多字段查詢的時候,建議使用組合索引能夠保證索引正常的使用。
例如,如果應用需要查找所有年齡小于30歲的深圳市馬拉松運動員:
db.athelets.find({sport: "marathon", location: "sz", age: {$lt: 30}}})
那么你可能需要這樣的一個索引:
db.athelets.ensureIndex({sport:1, location:1, age:1});
以上文為例子,在創建組合索引時如果條件有匹配和范圍之分,那么匹配條件(sport: “marathon”) 應該在組合索引的前面。范圍條件(age: <30)字段應該放在組合索引的后面。
有些時候你的查詢只需要返回很少甚至只是一個字段,例如,希望查找所有虹橋機場出發的所有航班的目的地。已有的索引是:
{origin: 1, dest: 1}
如果正常的查詢會是這樣(只需要返回目的地機場):
db.flights.find({origin:"hongqiao"}, {dest:1});
這樣的查詢默認會包含_id 字段,所以需要掃描匹配的文檔并取回結果。相反,如果使用這個查詢語句:
db.flights.find({origin:"hongqiao"}, {_id:0, dest:1});
MongoDB則可以直接從索引中取得所有需要返回的值,而無需掃描實際文檔(文檔可能需要從硬盤里調入到內存)
在對一個集合創建索引時,該集合所在的數據庫將不接受其他讀寫操作。對數據量的集合建索引,建議使用后臺運行選項 {background: true}
MongoDB可以讓你像關系型數據庫一樣設計表結構,但是它不支持外鍵,也不支持復雜的Join!如果你的程序發現有大量實用JOIN的地方,那你的設計可能需要重新來過。參照以下相關模式設計建議。
MongoDB的模式設計基于靈活豐富的JSON文檔模式。在很多情況下,一個MongoDB應用的數據庫內的集合(表)的數量應該遠遠小于使用關系數據庫的同類型應用。MongoDB表設計不遵從第三范式。MongoDB的數據模型非常接近于對象模型,所以基本上就是按照主要的Domain object的數量來建相應的集合。根據經驗,一般小型應用的集合數量通常在幾個之內,中大型的應用會在10多個或者最多幾十個。
MongoDB模式設計不能按照第三范式,很多時候允許數據在多個文檔中重復,比如說,在每一個員工的文檔中重復他的部門名字,就是一個可以接受的做法。如果部門名字改了,可以執行一個update({},{}, {multi:true}) 的多文檔更新來一次性把部門名字更新掉。
一般來說,如果某個字段的數據值經常會變,則不太適合被大量冗余到別的文檔或者別的集合里面去。舉例來說,如果我們是在做一些股票類型資產管理, 可能有很多人都購買了Apple的股票,但是如果把經常變動的股價冗余到客戶的文檔里,由于股票價格變動頻繁,會導致有大量的更新操作。從另外一個角度來說,如果是一些不經常變的字段,如客戶的姓名,地址,部門等,則可以盡管進行冗余shi’yang
對于一對多的關系,如一個人有幾個聯系方式,一本書有10幾個章節,等等,建議使用內嵌方式,把N的數據以數組形式來描述,如:
> db.person.findOne() { user_id: 'tjworks', name: 'TJ Tang', contact : [ { type: 'mobile', number: '1856783691' }, { type: 'wechat', number: 'tjtang826'} ] }
有些時候這個一對多的多端數量較大, 比如說,一個部門內有多少員工。在華為一個三級部門可能有數千員工,這個時候如果把所有員工信息直接內嵌到部門內肯定不是個好的選擇,有可能會超出16MB的文檔限制。這個時候可以采用引用ID的方式:
> db.departments.findOne() { name : 'Enterprise BG', president: 'Zhang San', employees : [ // array of references to Employee colletion ObjectID('AAAA'), ObjectID('F17C'), ObjectID('D2AA'), // etc ] }
如果需要查詢部門下員工相關信息,你可以使用$lookup聚合操作符來把員工信息進行關聯并返回。
如果一對多情況下,這個多端數量無限大并會頻繁增長,比如說,一個測量儀的每分鐘讀數,一年下來有幾十萬條,這個時候即使是把ID放到數組里都會管理不便,這個時候就應該把多端的數據創建一個集合,并在那個集合的文檔里加入對主文檔的連接引用,如:
> db.sensors.findOne() { _id : ObjectID('AAAB'), name : 'engine temperature', vin : '4GD93039GI239', engine_id: '20394802', manuafacture: 'First Motor', production_date: '2014-02-01' ... } >db.readings.findOne() { time : ISODate("2014-03-28T09:42:41.382Z"), sensor: ObjectID('AAAB'), reading: 67.4 }
如果你有需要把PDF文件,圖片,甚至小視頻等二進制文件需要管理,建議使用MongoDB 的GridFS API 或者自己手動分集合來分開管理二進制數據和元數據。
數組是用來表達 1對多關系的利器,但是MongoDB對嵌套的數組內元素缺乏直接更新能力。比如說:
{ name: "Annice", courses: [ { name: "English", score: 97 }, { name: "Math", score: 89 }, { name: "Physics", score: 95 } ] }
這樣設計沒有嵌套數組,我們可以直接對 Math的score 修改為99:
db.students.update({name: "Annice", "courses.name":"Math"}, {$set:{"courses.$.score": 99 }})
注意數組定位符 $的用法, 的用法,$ 表示當前匹配的第一個數組元素的在數組內的索引。
但是下面這種情況就涉及到了數組嵌套:
{ name: "Annice", courses: [ { name: "Math", scores: [ {term: 1, score: 80} , {term: 2, score: 90} ] }, { name: "Physics", score: 95 } ] }
這個時候如果你想對Math course的term 1的Score進行修改,你就需要把 scores 這個數組整個調到內存然后在代碼里對這個嵌套數組的元素進行修改。這是因為MongoDB的數組定位符 $ 只對第一層數組有效。
當然,如果你的模型不需要修改嵌套的數組內元素,那么這條就不適用。
Java驅動的默認連接池大小是100。建議按照應用的實際情況做調整。對壓力較小的應用可以適當調小減少對應用服務器的資源占用。
MongoDB的建議最小部署是一個復制集,包含3個數據節點。默認情況下應用的寫操作(更新,插入或者刪除)在主節點上完成后就會立即返回。寫操作則通過OPLOG方式在后臺異步方式復制到其他節點。在極端情況下,這些寫操作可能還未在復制到從節點的時候主節點就出現宕機。這個時候發生主備節點切換,原主節點的寫操作會被回滾到文件而對應用不可見。為防止這種情況出現,MongoDB建議對重要的數據使用 {w: “marjority”} 的選項。{w: “majority”} 可以保證數據在復制到多數節點后才返回成功結果。使用該機制可以有效防止數據回滾的發生。
另外你可以使用 {j:1} (可以和 w:”majrotiy” 結合使用) 來指定數據必須在寫入WAL日志之后才向應用返回成功確認。這個會導致寫入性能有所下降,但是對于重要的數據可以考慮使用。
MongoDB由于是一個分布式系統,一份數據會在多個節點上進行復制。從哪個節點上讀數據,要根據應用讀數據的需求而定。以下是集中可以配置的讀選項:
primary: 默認,在主節點上讀數據
priaryPreferred: 先從主節點上讀,如果為成功再到任意一臺從節點上讀
secondary: 在從節點上讀數據(當有多臺節點的時候,隨機的使用某一臺從節點)
secondaryPreferred: 首先從從節點上讀,如果從節點由于某種原因不能提供服務,則從主節點上進行讀
nearest: 從距離最近的節點來讀。距離由ping操作的時間來決定。
除第一個選項之外,其他讀選項都存在讀到的數據不是最新的可能。原因是數據的復制是后臺異步完成的。
MongoClient是個線程安全的類,自帶線程池。通常在一個JVM內不要實例化多個MongoClient實例,避免連接數過多和資源的不必要浪費。
MongoDB使用復制集技術可以實現99.999%的高可用。當一臺主節點不能寫入時,系統會自動故障轉移到另一臺節點。轉移可能會耗時幾秒鐘,在這期間應用應該捕獲相應的Exception并執行重試操作。重試應該有backoff機制,例如,分別在1s,2s,4s,8s等時候進行重試。
MongoDB 沒有表結構定義。每個文檔的結構由每個文檔內部的字段決定。所有字段名會在每個文檔內重復。使用太長的字段名字會導致對內存、網絡帶寬更多的需求。(由于壓縮技術,長字段名對硬盤上的存儲不會有太多占用)
如: School, Course, StudentRecord
或者:school, course, stuent_record
不要把MongoDB和普通的鍵值型數據庫(KV)視為等同。MongoDB支持和關系型數據庫update語句類似的in place update。你只需要在update語句中指定需要更新的字段,而不是整個文檔對象。
舉例來說,加入我想把用戶的名字從TJ改為Tang Jianfa.
不建議的做法:
user = db.users.findOne({_id: 101}); user.name="Tang Jianfa" db.users.save(user);
建議的做法:
user = db.users.findOne({_id: 101}); // do certain things db.users.update({_id:101}, {$set: {name: "Tang Jianfa"}});
MongoDB 支持類似于SQL語句里面的select,可以對返回的字段進行過濾。使用Projection可以減少返回的內容,降低網絡傳輸的量和代碼中轉化成對象所需的時間。
很多時候我們用MongoDB來存儲一些時效性的數據,如7天的監控數據。與其自己寫個后臺腳本定期清理過期數據,你可以使用TTL索引來讓MongoDB自動刪除過期數據:
db.data.ensureIndex({create_time:1}, {expireAfterSeconds: 7*24*3600})
有些時候你不知道一條文檔數據是否已經在庫里存在。這個時候你要么先查詢一下,要么就是使用upsert語句。在SpringData下面upsert語句需要你把每個字段的值都在upsert語句中格式化出來。字段多的時候未免有些繁瑣。SpringData MongoDB里面的MongoTemplate有個execute方法可以用來實現一個DB調用,也不用繁瑣的把所有字段羅列出來的例子。
public boolean persistEmployee(Employee employee) throws Exception { BasicDBObject dbObject = new BasicDBObject(); mongoTemplate.getConverter().write(employee, dbObject); mongoTemplate.execute(Employee.class, new CollectionCallback<Object>() { public Object doInCollection(DBCollection collection) throws MongoException, DataAccessException { collection.update(new Query(Criteria.where("name").is(employee.getName())).getQueryObject(), dbObject, true, // means upsert - true false // multi update – false ); return null; } }); return true; }
SpringData MongoDB默認會在MongoDB文檔中添加一個_class字段,里面保存的是fully qualified class name, 如”com.mongodb.examples.Customer”。對于有些小文檔來說,這個字段可能會占據不小一部分的存儲空間。如果你不希望SpringData 自動加入這個字段,你可以:
1) 自定義MongoTypeMapper
@Bean public MongoTemplate mongoTemplate() throws UnknownHostException { MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(new DefaultDbRefResolver (mongoDbFactory()), new MongoMappingContext()); mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null)); return new MongoTemplate(mongoDbFactory(), mappingMongoConverter ); }
2) 在使用find語句時,顯式地指定類的名字/類型:
MongoTemplate.find(new Query(), Inventory.class))
看完上述內容,你們掌握MongoDB的最佳實踐是怎樣的的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。