MongoDB報表實例方案選型python
背景介紹git
在咱們的生產環境使用的是複製集,爲了將數據庫服務器的業務壓力分攤,咱們將數據庫拆分到了不一樣的複製集上運行。github
咱們在MongoDB複製集上運行應用程序,有時候有報表需求,常規用途是得到用戶行爲的分析,還有其餘商業定製指標數據;有搜索引擎的查詢需求,使用Solr從oplog.rs獲取增量數據更新產品信息的索引。sql
這些報表查詢和搜索引擎的查詢需求,儘可能不能影響到線上的業務正常運行,所以不能直接在生產數據庫上運行報表。通過開發和運維討論以後,在項目成立之初,計劃隔斷報表任務以至不會影響到生產任務。mongodb
來談談混淆生產和報表的問題shell
工做集(workingset),是MongoDB在任何的時間間隔讀取和寫入的整個數據庫的一個子集。生產環境中的活躍用戶操做文檔數據,操做系統將它們保持在物理內存中。數據庫
注意:不要讓你的工做集增加大過內存!可使用MongoDB的監控服務Cloud Manager監控你的實例。若是確實出現這個問題,你須要分片,所以容量規劃是很重要的,能夠做爲獨立的專題來說。即便數據庫大小是可用內存的數百或數千倍,若是你在前期合理規劃了架構並優化了索引,MongoDB一樣也能高效運行。在工做集外的數據會保持和磁盤上一致,當用戶空閒時,他們操做的文檔將會再也不使用,所佔用的內存用於新的活躍用戶的內存請求。json
報表應用會查詢大量的數據,通常不會重複訪問相同的數據,每一個報表應用可能徹底訪問不一樣的數據集合。這意味着須要持續提供內存給新的文檔讀取請求。若是你將報表應用和生產應用放在相同的實例上運行,報表應用將會與你的生產應用爭奪內存,持續不斷地請求活躍用戶的數據,而你的生產應用持續不斷的加載它(getmore)。那麼數據庫服務器性能將發生波動。ruby
報表應用,將會有大量count、aggregate、mapReduce等聚合操做,這些操做對於MongoDB來講效率不高,所以將它與生產任務分開是一個好的作法。服務器
使用專屬報表實例的複製集
MongoDB複製集具備在線持久性,經過複製數據到一個集合中的全部節點,並對客戶端提供無縫的故障轉移。包含一個主節點提供寫,而剩下的是隻讀副本。當條件須要的時候選舉決定哪一個節點是主。複製集應該包含一個奇數成員幫助快速選舉。
判斷不可達的機器是否宕機基本上沒法判斷,有可能被網絡被分區了。所以若是複製集中的大多數節點下線了(也就是說,3個成員中的2個下線),即便一個健康的主節點保留,它會降級爲一個只讀的副本。不這麼作可能致使多個機器在一個網絡分區的狀況下定義它們本身爲主節點,出現多個主節點,致使可怕的數據不一致。
所以一個複製集包含至少3個成員,提供一個機器失敗的錯誤容忍。
在MongoDB官方的文檔中,推薦限制報表查詢到專屬節點。報表基本不須要寫操做,而是統計最終一致性數據。若是提取的數據有秒級或分級延時,每日的報表是不容許的。若是你的計數統計丟失了一些操做,這將致使報表數據不許確。
你能夠在MongoDB複製集環境構建專屬的報表節點,方案有隱藏的複製集成員hidden member或者讀偏好read preference設置相關的標籤集合tag sets。第一種方法更簡單,第二種方法更靈活。
下圖是使用專屬節點提供報表需求的架構圖:
隱藏成員方案
參考:https://docs.mongodb.com/manual/tutorial/configure-a-hidden-replica-set-member/
隱藏成員是複製集的一部分,可是不能成爲主,而且對客戶端應用程序不可見。隱藏成員能夠在選舉中投票。
一個複製集的隱藏成員被配置爲priority: 0,是爲了阻止它們被選舉爲主。設置hidden: true,即便他們指定了一個讀偏好爲secondary,也會阻止客戶端鏈接到複製集路由讀操做到它。
從一個隱藏成員讀數據,你只能經過直連該隱藏成員訪問,並指定slave_ok,而不能經過MongoReplicaSetClient類。
隱藏成員設置
你可使用mongo shell來隱藏一個存在複製集的成員:
$ mongo admin -uxucy -p PRIMARY> conf = rs.config() { "_id" : "test", "version" : 21, "members" : [ { "_id" : 0, "host" : "xucy.local:27017", }, { "_id" : 1, "host" : "xucy.local:28017", }, { "_id" : 2, "host" : "xucy.local:29017", } ] } PRIMARY> conf.members[1].priority = 0 PRIMARY> conf.members[1].hidden = true PRIMARY> conf.version += 1 PRIMARY> rs.reconfig(conf)
xucy.local:28017如今隱藏了,它將繼續複製操做和像往常同樣在選舉中投票,可是鏈接到複製集的客戶端將不會從它讀取,即便xucy.local:29017下線。
Ruby版的報表應用鏈接代碼示例:
require 'mongo' reporting = Mongo::MongoClient.new("xucy.local", "28017", slave_ok: true) reporting['my_application']['users'].aggregate(...)
限制說明
使用隱藏的成員是一個最簡單的方式,配置實例用於專屬的工做負載,像報表和搜索引擎訪問,然而使用上有一些限制須要說明的。
隱藏成員不能在緊急狀況下讀取
帶有2個普通和1個隱藏成員在一個複製集中,對於寫的錯誤容忍等價於一個常規的3個成員的集合。然而,你失去兩個節點,你的生產應用將不能優雅的降級到只讀模式,由於你的隱藏成員將不容許複製集客戶端讀取。若是你只是喜歡隱藏成員訪問簡單,土豪方案是使用一個5成員(帶有一個隱藏成員)的複製集。
對於複製集的包裝代碼不能被使用
不少團隊建立應用定製的包裝代碼時,使用MongoDB驅動提供的複製集鏈接訪問方法,添加複製集鏈接的基本信息給客戶端。由於你須要使用獨立鏈接到你的報表實例,你不能重用它。
標籤成員方案
參考:https://docs.mongodb.com/manual/tutorial/configure-replica-set-tag-sets/
標籤成員,更加複雜,可是,是更靈活的方法,用於路由報表查詢到一個專屬節點去使用標籤和讀偏好。
設置一個成員爲priority: 0,阻止它被選舉爲主,可是不設置它爲隱藏,分配一個標籤use: reporting:
PRIMARY> conf = rs.config() { "_id" : "test", "version" : 21, "members" : [ { "_id" : 0, "host" : "xucy.local:27017", }, { "_id" : 1, "host" : "xucy.local:28017", }, { "_id" : 2, "host" : "xucy.local:29017", } ] } PRIMARY> conf.members[1].priority = 0 PRIMARY> conf.members[1].tags = { "use": "reporting" } PRIMARY> conf.version += 1 PRIMARY> rs.reconfig(conf)
在這種狀況下,xucy.local:28017毫不會成爲主。然而,當其餘兩個機器變得不可達,你的應用還能處理讀請求到報表服務器。它會繼續運行,不會致使你的報表應用在這樣一個事件期間暫停。
Python版報表應用鏈接代碼示例:
from pymongo import MongoReplicaSetClient from pymongo.read_preferences import ReadPreference rep_set = MongoReplicaSetClient( 'xucy.local:27017,xucy.local:28017,xucy.local:29017', replicaSet = 'test', read_preference = ReadPreference.SECONDARY, tag_sets = [{'use':'reporting'}] ) rep_set.my_application.users.aggregate(...)
對於報表應用來講,在主可用的狀況下,確保儘可能不要在剩下的惟一的輔助成員上運行報表應用,由於這樣將報表和生產混合在一塊兒了。
以上只發送報表查詢到標記有use: reporting的輔助成員,而且若是沒有可用的主,咱們應該從根本上阻止繼續運行。在實踐中,若是你發現沒有主,你應該拋出異常並在你的擴展代碼中處理它們。還要作好狀態的監控,如:reporting_system.ok()。當發現異常時進行分支處理。
益處和考慮
使用標籤和讀偏好相對隱藏成員來講,帶來必定的靈活性。
容易添加報表實例
由於你的鏈接代碼是可定義的,而不是指定到一個專門的主機。你能夠添加更多節點爲報表實例,只須要添加並標記他們,像這樣:
PRIMARY> rs.add({_id:3, host:"xucy.local:30017", priority:0, tags:{'use':'reporting'}})
你原來的代碼將會利用到新的報表實例,而且複製集將繼續運行,不用觸發選舉和從客戶端斷開鏈接。
報表實例能夠被跳過或刪除
當你想將目前使用的報表實例提供給其餘應用時,報表標記在必要時能夠被移動,或者移除。像這樣的一個從新配置將會觸發選舉,並重連全部客戶端,這是能夠接受的。注意:這是一個逆向的方法,經過增長經常使用生產可用實例,分發生產讀到副本成員。
一些驅動須要手工同步
檢查你的驅動文檔,例如,Ruby驅動(像1.9.2),不會刷新副本集的視圖,除非客戶端像這樣使用refresh_mode: :sync顯式初始化。
Solr生成全文索引
MongoDB複製集配置簡單、容易上手是我喜歡MongoDB的緣由之一。對於MongoDB報表實例,不管你使用隱藏成員仍是標籤成員,開發和部署都很是簡單。咱們在生產環境也將報表實例用於Solr生成全文索引。
Solr是一個獨立的企業級搜索應用服務器,它對外提供相似於Web-service的API接口。用戶能夠經過http請求,向搜索引擎服務器提交必定格式的XML文件,生成索引;也能夠經過Http Get操做提出查找請求,並獲得XML格式的返回結果。
讀取報表實例的方案選型
期初的方案是使用mongo-connector集成MongoDB到Solr實現增量索引。(http://ultrasql.blog.51cto.com/9591438/1696083/)
mongo-connctor是一款用於同步MongoDB數據到其餘系統組件,好比它能同步數據到Solr、ElasticSearch或者其餘MongoDB集羣中去。它的實現原理是依據MongoDB的Replica Set複製模式,經過分析oplog日誌文件達到最終的同步目的。安裝配置啓動過程可參考 官方文檔 。
因爲是單進程版本的,效率對咱們當時來講不高,若是能改爲利用多核性能的話可能好些。
MongoDB Tailable Cursors
MongoDB 有一個叫 Tailable Cursors的特性,它相似於tail -f 命令,你在一個Capped Collection上面執行查詢操做,當操做完成後,你能夠不關閉返回的數據Cursor,並持續地從中讀出新加入的數據。
在高寫入的Capped Collection上,索引不可用時,可以使用Tailable Cursors。例如,MongoDB複製使用了Tailable Cursors來獲取Primary的尾oplog日誌。
考慮如下與Tailable Cursors相關的行爲:
Tailable Cursors不使用索引,並以天然排序返回文檔。
由於Tailable Cursors不使用索引,查詢的初始掃描很是耗性能;可是,遊標初始化完後,隨後獲取到的新增長的文檔是很快速的。
Tailable Cursors若是遇到如下狀況之一將會僵死或無效:
查詢無匹配結果。
遊標在集合尾部返回文檔,隨後應用程序刪除了該文檔。
僵死的遊標id爲0。
DBQuery.Option.awaitData
在使用TailableCursor時,此參數會在數據讀盡時先阻塞一小段時間後再讀取一次並進行返回。
跟蹤oplog的示例:
use local var cursor = db.oplog.rs.find({"op" : "u", "ns" : "MyDB.Product"},{"ts": 1, "o2._id": 1}).addOption(DBQuery.Option.tailable).addOption(DBQuery.Option.awaitData); while(cursor.hasNext()){ var doc = cursor.next(); printjson(doc); };
2.6版的遊標方法:
cursor.addOption()
https://docs.mongodb.com/v2.6/reference/method/cursor.addOption/
3.2版的遊標方法:
cursor.tailable()
https://docs.mongodb.com/manual/reference/method/cursor.tailable/
咱們根據該特性,開發了Java應用程序,先初始化全量同步數據到Solr生成索引、記錄同步時間。而後經過Tailable Cursors讀取oplog.rs對比上次記錄的同步時間,若是是新的變動,經過新的進程異步獲取日誌裏記錄的最新數據更新到Solr的文檔裏。