天天看點

MongoDB報表執行個體方案選型

背景介紹

在我們的生産環境使用的是複制集,為了将資料庫伺服器的業務壓力分攤,我們将資料庫拆分到了不同的複制集上運作。

我們在MongoDB複制集上運作應用程式,有時候有報表需求,正常用途是獲得使用者行為的分析,還有其他商業定制名額資料;有搜尋引擎的查詢需求,使用Solr從oplog.rs擷取增量資料更新産品資訊的索引。

這些報表查詢和搜尋引擎的查詢需求,盡量不能影響到線上的業務正常運作,是以不能直接在生産資料庫上運作報表。經過開發和運維讨論之後,在項目成立之初,計劃隔斷報表任務以緻不會影響到生産任務。

來談談混淆生産和報表的問題

工作集(workingset),是MongoDB在任何的時間間隔讀取和寫入的整個資料庫的一個子集。生産環境中的活躍使用者操作文檔資料,作業系統将它們保持在實體記憶體中。

注意:不要讓你的工作集增長大過記憶體!可以使用MongoDB的監控服務Cloud Manager監控你的執行個體。如果确實出現這個問題,你需要分片,是以容量規劃是很重要的,可以作為獨立的專題來講。即使資料庫大小是可用記憶體的數百或數千倍,如果你在前期合理規劃了架構并優化了索引,MongoDB同樣也能高效運作。在工作集外的資料會保持和磁盤上一緻,當使用者空閑時,他們操作的文檔将會不再使用,所占用的記憶體用于新的活躍使用者的記憶體請求。

報表應用會查詢大量的資料,一般不會重複通路相同的資料,每個報表應用可能完全通路不同的資料集合。這意味着需要持續提供記憶體給新的文檔讀取請求。如果你将報表應用和生産應用放在相同的執行個體上運作,報表應用将會與你的生産應用争奪記憶體,持續不斷地請求活躍使用者的資料,而你的生産應用持續不斷的加載它(getmore)。那麼資料庫伺服器性能将發生波動。

報表應用,将會有大量count、aggregate、mapReduce等聚合操作,這些操作對于MongoDB來說效率不高,是以将它與生産任務分開是一個好的做法。

使用專屬報表執行個體的複制集

MongoDB複制集具有線上持久性,通過複制資料到一個集合中的所有節點,并對用戶端提供無縫的故障轉移。包含一個主節點提供寫,而剩下的是隻讀副本。當條件需要的時候選舉決定哪個節點是主。複制集應該包含一個奇數成員幫助快速選舉。

判斷不可達的機器是否當機基本上無法判斷,有可能被網絡被分區了。是以如果複制集中的大多數節點下線了(也就是說,3個成員中的2個下線),即使一個健康的主節點保留,它會降級為一個隻讀的副本。不這麼做可能導緻多個機器在一個網絡分區的情況下定義它們自己為主節點,出現多個主節點,導緻可怕的資料不一緻。

是以一個複制集包含至少3個成員,提供一個機器失敗的錯誤容忍。

在MongoDB官方的文檔中,推薦限制報表查詢到專屬節點。報表基本不需要寫操作,而是統計最終一緻性資料。如果提取的資料有秒級或分級延時,每日的報表是不允許的。如果你的計數統計丢失了一些操作,這将導緻報表資料不準确。

你可以在MongoDB複制集環境建構專屬的報表節點,方案有隐藏的複制內建員hidden member或者讀偏好read preference設定相關的标簽集合tag sets。第一種方法更簡單,第二種方法更靈活。

下圖是使用專屬節點提供報表需求的架構圖:

<a href="http://s3.51cto.com/wyfs02/M00/89/F4/wKiom1gikuzy4G0qAACAwu5AyUo337.jpg" target="_blank"></a>

隐藏成員方案

隐藏成員是複制集的一部分,但是不能成為主,并且對用戶端應用程式不可見。隐藏成員可以在選舉中投票。

一個複制集的隐藏成員被配置為priority: 0,是為了阻止它們被選舉為主。設定hidden: true,即使他們指定了一個讀偏好為secondary,也會阻止用戶端連接配接到複制集路由讀操作到它。

從一個隐藏成員讀資料,你隻能通過直連該隐藏成員通路,并指定slave_ok,而不能通過MongoReplicaSetClient類。

隐藏成員設定

你可以使用mongo shell來隐藏一個存在複制集的成員:

1

2

3

4

5

6

7

<code>$ mongo admin -uxucy -p</code>

<code>PRIMARY</code><code>&gt; conf = rs.config()</code>

<code>{ </code><code>"_id"</code> <code>: </code><code>"test"</code><code>, </code><code>"version"</code> <code>: 21, </code><code>"members"</code> <code>: [ { </code><code>"_id"</code> <code>: 0, </code><code>"host"</code> <code>: </code><code>"xucy.local:27017"</code><code>, }, { </code><code>"_id"</code> <code>: 1, </code><code>"host"</code> <code>: </code><code>"xucy.local:28017"</code><code>, }, { </code><code>"_id"</code> <code>: 2, </code><code>"host"</code> <code>: </code><code>"xucy.local:29017"</code><code>, } ] }</code>

<code>PRIMARY</code><code>&gt; conf.members[1].priority = 0</code>

<code>PRIMARY</code><code>&gt; conf.members[1].hidden = </code><code>true</code>

<code>PRIMARY</code><code>&gt; conf.version += 1</code>

<code>PRIMARY</code><code>&gt; rs.reconfig(conf)</code>

xucy.local:28017現在隐藏了,它将繼續複制操作和像往常一樣在選舉中投票,但是連接配接到複制集的用戶端将不會從它讀取,即使xucy.local:29017下線。

Ruby版的報表應用連接配接代碼示例:

<code>require </code><code>'mongo'</code>

<code>reporting = Mongo::MongoClient.</code><code>new</code><code>(</code><code>"xucy.local"</code><code>, </code><code>"28017"</code><code>, slave_ok: </code><code>true</code><code>)</code>

<code>reporting[</code><code>'my_application'</code><code>][</code><code>'users'</code><code>].aggregate(...)</code>

限制說明

使用隐藏的成員是一個最簡單的方式,配置執行個體用于專屬的工作負載,像報表和搜尋引擎通路,然而使用上有一些限制需要說明的。

隐藏成員不能在緊急情況下讀取

帶有2個普通和1個隐藏成員在一個複制集中,對于寫的錯誤容忍等價于一個正常的3個成員的集合。然而,你失去兩個節點,你的生産應用将不能優雅的降級到隻讀模式,因為你的隐藏成員将不允許複制集用戶端讀取。如果你隻是喜歡隐藏成員通路簡單,土豪方案是使用一個5成員(帶有一個隐藏成員)的複制集。

對于複制集的包裝代碼不能被使用

很多團隊建立應用定制的包裝代碼時,使用MongoDB驅動提供的複制集連接配接通路方法,添加複制集連接配接的基本資訊給用戶端。因為你需要使用獨立連接配接到你的報表執行個體,你不能重用它。

标簽成員方案

<a href="https://docs.mongodb.com/manual/tutorial/configure-replica-set-tag-sets/" target="_blank"></a>

标簽成員,更加複雜,但是,是更靈活的方法,用于路由報表查詢到一個專屬節點去使用标簽和讀偏好。

設定一個成員為priority: 0,阻止它被選舉為主,但是不設定它為隐藏,配置設定一個标簽use: reporting:

<code>PRIMARY</code><code>&gt; conf.members[1].tags = { </code><code>"use"</code><code>: </code><code>"reporting"</code> <code>}</code>

在這種情況下,xucy.local:28017絕不會成為主。然而,當其他兩個機器變得不可達,你的應用還能處理讀請求到報表伺服器。它會繼續運作,不會導緻你的報表應用在這樣一個事件期間暫停。

Python版報表應用連接配接代碼示例:

<code>from</code> <code>pymongo </code><code>import</code> <code>MongoReplicaSetClient</code>

<code>from</code> <code>pymongo.read_preferences </code><code>import</code> <code>ReadPreference</code>

<code>rep_set </code><code>=</code> <code>MongoReplicaSetClient( </code><code>'xucy.local:27017,xucy.local:28017,xucy.local:29017'</code><code>, replicaSet </code><code>=</code> <code>'test'</code><code>, read_preference </code><code>=</code> <code>ReadPreference.SECONDARY, tag_sets </code><code>=</code> <code>[{</code><code>'use'</code><code>:</code><code>'reporting'</code><code>}] )</code>

<code>rep_set.my_application.users.aggregate(...)</code>

對于報表應用來說,在主可用的情況下,確定盡量不要在剩下的唯一的輔助成員上運作報表應用,因為這樣将報表和生産混合在一起了。

以上隻發送報表查詢到标記有use: reporting的輔助成員,并且如果沒有可用的主,我們應該從根本上阻止繼續運作。在實踐中,如果你發現沒有主,你應該抛出異常并在你的擴充代碼中處理它們。還要做好狀态的監控,如:reporting_system.ok()。當發現異常時進行分支處理。

益處和考慮

使用标簽和讀偏好相對隐藏成員來說,帶來一定的靈活性。

容易添加報表執行個體

因為你的連接配接代碼是可定義的,而不是指定到一個專門的主機。你可以添加更多節點為報表執行個體,隻需要添加并标記他們,像這樣:

<code>PRIMARY</code><code>&gt; rs.</code><code>add</code><code>({_id:3, host:</code><code>"xucy.local:30017"</code><code>, priority:0, tags:{</code><code>'use'</code><code>:</code><code>'reporting'</code><code>}})</code>

你原來的代碼将會利用到新的報表執行個體,并且複制集将繼續運作,不用觸發選舉和從用戶端斷開連接配接。

報表執行個體可以被跳過或删除

當你想将目前使用的報表執行個體提供給其他應用時,報表标記在必要時可以被移動,或者移除。像這樣的一個重新配置将會觸發選舉,并重連所有用戶端,這是可以接受的。注意:這是一個逆向的方法,通過增加常用生産可用執行個體,分發生産讀到副本成員。

一些驅動需要手工同步

檢查你的驅動文檔,例如,Ruby驅動(像1.9.2),不會重新整理副本集的視圖,除非用戶端像這樣使用refresh_mode: :sync顯式初始化。

Solr生成全文索引

MongoDB複制集配置簡單、容易上手是我喜歡MongoDB的原因之一。對于MongoDB報表執行個體,無論你使用隐藏成員還是标簽成員,開發和部署都非常簡單。我們在生産環境也将報表執行個體用于Solr生成全文索引。

Solr是一個獨立的企業級搜尋應用伺服器,它對外提供類似于Web-service的API接口。使用者可以通過http請求,向搜尋引擎伺服器送出一定格式的XML檔案,生成索引;也可以通過Http Get操作提出查找請求,并得到XML格式的傳回結果。

讀取報表執行個體的方案選型

由于是單程序版本的,效率對我們當時來說不高,如果能改成利用多核性能的話可能好些。

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的示例:

<code>use </code><code>local</code>

<code>var </code><code>cursor</code> <code>= db.oplog.rs.find({</code><code>"op"</code> <code>: </code><code>"u"</code><code>, </code><code>"ns"</code> <code>: </code><code>"MyDB.Product"</code><code>},{</code><code>"ts"</code><code>: 1, </code><code>"o2._id"</code><code>: 1}).addOption(DBQuery.</code><code>Option</code><code>.tailable).addOption(DBQuery.</code><code>Option</code><code>.awaitData);</code>

<code>while(</code><code>cursor</code><code>.hasNext()){</code>

<code>var doc = </code><code>cursor</code><code>.</code><code>next</code><code>();</code>

<code>printjson(doc);</code>

<code>};</code>

2.6版的遊标方法:

cursor.addOption()

<a href="https://docs.mongodb.com/v2.6/reference/method/cursor.addOption/" target="_blank">https://docs.mongodb.com/v2.6/reference/method/cursor.addOption/</a>

3.2版的遊标方法:

cursor.tailable()

<a href="https://docs.mongodb.com/manual/reference/method/cursor.tailable/" target="_blank">https://docs.mongodb.com/manual/reference/method/cursor.tailable/</a>

我們根據該特性,開發了Java應用程式,先初始化全量同步資料到Solr生成索引、記錄同步時間。然後通過Tailable Cursors讀取oplog.rs對比上次記錄的同步時間,如果是新的變更,通過新的程序異步擷取日志裡記錄的最新資料更新到Solr的文檔裡。

<a href="http://s3.51cto.com/wyfs02/M01/89/F4/wKiom1gikuzjSq3nAACeDiivZJw972.jpg" target="_blank"></a>