MongoDB在vivo評論中臺的應用案例

本文來自獲得《2021MongoDB技術實踐與應用案例徵集活動》入圍案例獎作品
作者:vivo互聯網技術

1.業務背景

隨着公司業務發展和用戶規模的增多,很多項目都在打造自己的評論功能,而評論的業務形態基本類似。當時各項目都是各自設計實現,存在較多重複的工作量;並且不同業務之間數據存在孤島,很難產生聯繫。因此我們決定打造一款公司級的評論業務中臺,爲各業務方提供評論業務的快速接入能力。在經過對各大主流app評論業務的競品分析,我們發現大部分評論的業務形態都具備評論、回覆、二次回覆、點贊等功能。具體如下圖所示:

涉及到的核心業務概念有:
【主題 topic】 評論的主題,商城的商品、應用商店的app、社區的帖子。
【評論 comment】用戶針對於主題發表的內容。
【回覆 reply】用戶針對於某條評論發表的內容,包括一級回覆和二級回覆。

2.爲什麼選擇MongoDB

團隊在數據庫選型設計時,對比了多種主流的數據庫,最終在MySQL和MongoDB兩種存儲之進行抉擇。

由於評論業務的特殊性,它需要如下能力:
【字段擴展】業務方不同評論模型存儲的字段有一定差異,需要支持動態的自動擴展。
【海量數據】作爲公司中臺服務,數據量隨着業務方的增多成倍增長,需要具備快速便捷的水平擴展和遷移能力。
【高可用】作爲中颱產品,需要提供快速和穩定的讀寫能力,能夠讀寫分離和自動恢復
而評論業務不涉及用戶資產,對事務的要求性不高。因此我們選用了MongoDB集羣作爲了最底層的數據存儲方式。

3.MongoDB在評論中臺的應用

3.1 MongoDB集羣知識

集羣架構
由於單臺機器存在磁盤/IO/CPU等各方面的瓶頸,所以MongoDB提供集羣方式的部署架構,如圖所示:

主要由以下三個部分組成:
mongos:路由服務器,負責管理應用端的具體鏈接。應用端請求到mongos服務後,mongos把具體的讀寫請求轉發應的shard節點上執行。一個集羣可以有1~N個mongos節點。
config:配置服務器,用於分存儲分片集合的元數據和配置信息,必須爲複製集(關於複製集概念戳我) 方式部署。mongos通過config配置服務器合的元數據信息。
shard:用於存儲集合的分片數據的MongoDB服務,同樣必須以複製集方式部署。

片鍵
MongoDB數據是存在collection(對應MySQL表)中。集羣模式下,collection按照片鍵(shard key)拆分成多個區間,每個區間組成一個chunk,按照規則分佈在不同的shard中。並形成元數據註冊到config服務中管理。

分片鍵只能在分片集合創建時指定,指定後不能修改。

分片鍵主要有兩大類型:
hash分片:通過hash算法進行散列,數據分佈的更加平均和分散。支持單列和多列hash。
範圍分片:按照指定片鍵的值分佈,連續的key往往分佈在連續的區間,更加適用範圍查詢場景。單數據散列性由分片鍵本身保證。

3.2 評論中臺的實踐

片鍵的選擇
MongoDB集羣中,一個集合的數據部署是分散在多個shard分片和chunk中的,而我們希望一個評論列表的查詢最好只訪問到一個shard分片,因此確定了範圍分片的方式。
起初設置只使用單個key作爲分片鍵,以comment評論表舉例,主要字段有{"_id":唯一id,"topicId":主題id,"text":文本內容,"createDate":時間} ,考慮到一個主題id的評論儘可能連續分佈,我們設置的分片鍵爲topicId。隨着性能測試的介入,我們發現了有兩個非常致命的問題:
jumbo chunk問題
唯一鍵問題

jumbo chunk:
官方文檔中,MongoDB中的chunk大小被限制在了1M-1024M。分片鍵的值是chunk劃分的唯一依據,在數據量持續寫入超過chunk size設定值時, MongoDB集羣就會自動的進行分裂或遷移。而對於同一個片鍵的寫入是屬於一個chunk,無法被分裂,就會造成 jumbo chunk問題。
舉例:若我們設置1024M爲一個chunk的大小,單個document 5KB計算,那麼單個chunk能夠存儲21W左右document。考慮熱點的主題評論( 如微信評論),評論數可能達到40W+,因此單個chunk很容易超過1024M。超過最大size的chunk依然能夠提供讀寫服務,只是不會再進行分裂和遷移,長久以往會造成集羣之間數據的不平衡。

唯一鍵問題:
MongoDB集羣的唯一鍵設置增加了限制,必須是包含分片鍵的;如果_id不是分片鍵,_id索引只能保證單個shard上的唯一性。
You cannot specify a unique constraint on a hashed index
For a to-be-sharded collection, you cannot shard the collection if the collection has other unique indexes
For an already-sharded collection, you cannot create unique indexes on other fields

因此我們刪除了數據和集合,調整topicId 和 _id 爲聯合分片鍵 重新創建了集合。這樣即打破了chunk size的限制,也解決了唯一性問題。

遷移和擴容
隨着數據的寫入,當單個chunk中數據大小超過指定大小時(或chunk中的文件數量超過指定值)。MongoDB集羣會在插入或更新時,自動觸發chunk的拆分。

拆分會導致集合中的數據塊分佈不均勻,在這種情況下,MongoDB balancer組件會觸發集羣之間的數據塊遷移。balancer組件是一個管理數據遷移的後臺進程,如果各個shard分片之間的chunk數差異超過閾值,balancer會進行自動的數據遷移。

balancer是可以在線對數據遷移的,但是遷移的過程中對於集羣的負載會有較大影響。一般建議可以通過如下設置,在業務低峯時進行。
db.settings.update(
{ _id: "balancer" },
{ $set: { activeWindow : { start : "", stop : "" } } },
{ upsert: true }
)
MongoDB的擴容也非常簡單,只需要準備好新的shard複製集後,在mongos節點中執行:
sh.addShard("<replica_set>/<:port>")
擴容期間因爲chunk的遷移,同樣會導致集羣可用性降低,因此只能在業務低峯進行。

集羣的擴展
作爲中颱服務,對於不同的接入業務方,通過表隔離來區分數據。以comment評論表舉例,每個接入業務方都單獨創建一張表,業務方A表爲 comment_clientA ,業務方B表爲 comment_clientB,均在接入時創建表和相應索引信息。但只是這樣設計存在幾個問題:
單個集羣,不能滿足部分業務數據物理隔離的需要
集羣調優(如split遷移時間)很難業務特性差異化設置
水平擴容帶來的單個業務方數據過於分散問題

因此我們擴展了MongoDB的集羣架構:

擴展後的評論MongoDB集羣 增加了 【邏輯集羣】和【物理集羣】的概念。一個業務方屬於一個邏輯集羣,一個物理集羣包含多個邏輯集羣。
增加了路由層設計,由應用負責擴展spring的MongoTemplate和連接池管理,實現了業務到MongoDB集羣之間的切換選擇服務。
不同的MongoDB分片集羣,實現了物理隔離和差異調優的可能。

3.3 自研MongoDB事件採集處理平臺及應用

評論中臺中有很多統計數據的查詢:評論數、回覆數、點贊數、未讀回覆數等等,由於評論數據量較大,單個業務的數據量都在億級別以上,瀏覽器、短視頻等內容型的業務評論數據量都是在百億級別,前臺業務查詢時對數據庫做實時count性能肯定是無法達到要求的,因此我們選擇使用空間換時間的方式,在數據表中增加相關的統計數據字段,每次有新的評論發表或者狀態變更時對該字段的值做$inc原子操作。這種方式確實能夠提升數據查詢的效率,在評論這種讀多寫少的場景下十分合適。

帶來的問題:
作爲中颱類項目,需要適配vivo不同的評論業務場景,隨着業務發展,這部分統計口徑和類型也會隨之變動,各種統計類型越來越多(目前已有20多種統計),在代碼主流程中耦合的統計邏輯越來越越重,並且各種統計邏輯的代碼散落在系統代碼的各個角落,系統也越來越臃腫,並且代碼變更時很容易帶來統計數據不準的問題,這類問題會影響評論及回覆的下拉展示的效果,舉一個較爲典型的例子,如果當前評論下有1條回覆,但是統計錯誤,誤認爲沒有回覆,則當前人回覆信息則無法顯示,如果需要修復此類問題,我們發現涉及面比較廣,無法收斂。因此我們對統計相關的邏輯進行了一次大的重構。

事件數據服務
我們選擇事件驅動的方式將統計邏輯從主流程中解構出來,每一個統計都有其對應的變更事件,例如:
評論發表事件 > 評論數統計點贊、
取消點贊事件 > 點贊數統計
僅自身可見事件 > 用戶僅自身可見數統計

但是這個事件如何發出來呢?一開始我們想的是在主流程中根據場景發送不同類型的事件,但是這種方式不夠靈活,每次增加一種統計類型就需要增加一種事件類型。轉換下思路,其實每發出一種事件其本質上是數據庫某些數據發生了變化,那我們爲何不監聽數據庫的操作日誌,採集數據的變更,通過對比變更前後的數據來自定義事件,進而進行數據的統計,整體流程如下:

其中監聽模塊負責監聽MongoDB數據變更,並對變更事件進行處理和分發,當被監聽的表發生數據變更時會將變更前後的數據通過mq的方式分發給數據服務。數據服務專門負責評論中臺數據統計、數據刷新等功能,這些功能對系統負載影響比較大,需要和主系統解耦。

從上圖可以看到,整個監聽模塊包括了”bees採集“和”業務事件平臺“兩個部分,一個負責採集,一個負責對事件進行處理和分發,這裏爲什麼還需要一個獨立的平臺進行事件處理和分發,有2個原因:
直接採集的事件,其事件內容依然無法滿足業務需求,無法滿足對比變更前後的數據來自定義事件,因此需要對相應的事件數據進行合併,具體的實現在後文進行介紹。
變更事件需要按照業務方自定義的條件進行過濾,只需要將滿足條件的事件分發給下游數據服務。

因此這裏複用了vivo自研的業務事件平臺,通過low Code方式對事件數據進行處理和過濾後,發給下游的數據服務,整個流程爲bees採集—>事件平臺—>評論數據服務,接下來我們先介紹下MongoDB的採集系統部分。

MongoDB採集架構和流程
對於MongoDB的採集,我們複用了vivo自研的在Bees大數據採集平臺能力,基於這個平臺的DB數據採集的子組件(bees-dbsync),我們開發了用於MongoDB的數據採集的鏈路,適用於實時獲取MongoDB的變更信息,實現了對於MongoDB的全量和增量的採集能力。

功能介紹
現階段MongoDB採集模塊可以支持的功能如下:
支持在任務接入後進行一次全量採集,全量採集結束後會根據用戶配置的延遲時間開啓增量採集。
支持針對單一任務的啓動、停止、修改等操作。操作完成後會實時進行相應的變更。
增量採集斷點續傳功能。在任意時刻停止任務後進行恢復採集,任務將由上一次完成的採集點位後一個點位開始繼續進行採集。在oplog大小保證數據完整的條件下,不會有數據漏採的情況發生。
全量、增量採集均支持按照用戶自定義的kafka分區發送規則發送至相應的分區。
全量、增量採集均支持任務狀態和數據量監控。
全量、增量採集均支持根據副本集、sharding變化自動開啓採集。

方案架構
在採集系統中,關於MongoDB的採集模塊架構圖如下:

採集系統中,MongoDB採集模塊內部結構如上圖橙色方框內所示。主要包括:
MongoDBParser首先將對數據進行解析和過濾。對於MongoDB全量數據採集,MongoDBParser將直接對全表執行find()操作,對於MongoDB 增量數據採集,MongoDBParser將讀取local庫下的oplog.rs表,根據遞增的ts值依次獲取新增的oplog事件,由於ts值的唯一性,已經發送完成的 ts值將作爲歷史點位信息保存在Bees-DBSync本地並上傳到Bees-Manager的數據庫中,成爲斷點續傳功能的必須條件。
StreamData,具體由MongoDBStreamData實現,負責存放Bees-DBSync從業務MongDB解析後的數據。
PackageThread,負責對MongoDBStreamData格式的數據進行任務信息的填充,並將數據打包成統一的PkgData數據格式,用以保證MongoDB、MySQL等發送格式的一致性。
PkgData,是Bees-DBSync內部處理後統一的數據格式。
KafkaSink模塊,負責對目的端信息進行處理,根據用戶配置的不同分區發送規則,將PkgData格式的數據發送到不同的Kafka分區。

數據完整性保障
借鑑了RingBuffer設計思路:

定義了三個點位:
Pull:最後一次拉取oplog的點位
Sink:最後一次發送Kafka的點位
Ack:最後一次成功Sink到Kafka的點位
三個點位的關係是Ack <= Sink <= Pull,三個點位記錄了日誌採集情況,出現異常情況時可以從記錄的點位恢復採集,最終保障數據完整性。

採集流程
MongoDB採集任務執行流程圖如下:

以上部分是採集系統的整理架構和流程,接下來介紹下,對變更事件進行合併和處理的架構和流程。

MongoDB變更事件處理平臺
在這裏我們複用了vivo自研的事件處理平臺的能力,新增了對MongoDB變更事件的處理,這個平臺的建設目的是面向我們的服務端開發同學,通過畫布拖拽方式幫助業務對採集到的存儲變更事件進行處理和分發。平臺提供了一個低延遲的流式處理解決方案,支持畫布的方式對採集到的MongoDB變更事件進行過濾(filter)、轉化處理(map、udf)和輸出(sink),支持UDF插件的方式自定義處理採集到的數據,並將處理的數據分發到不同的輸出端。目前支持將MongoDB採集的事件通過filter、map後sink到es,hbase,kafka,rabbitmq以及通過dubbo方式通知到不同下游。

業務事件平臺的整體架構圖

在評論中臺的業務場景中,評論中臺需要依據MongoDB字段發生變更的條件來進行過濾和統計,例如,需要更加字段“commentNum”進行判斷,當前字段的值是否是從少變多,是否數值有增加,這就需要根據MongoDB的變更前的值以及變更後的值進行比較,對符合評論業務場景條件的文檔內容進行向下傳遞,因此我們需要捕獲到MongoDB變更前的值,以及未變更字段的值。

如下圖所示,需要集合對Oplog的變更事件的採集,組合成完整的文檔內容,當前文檔內容放在data屬性下,old屬性下爲涉及到修改的字段在變更前的值。

但是我們發現,採集MongoDB的Oplog或者基於change stream都無法完全滿足我們的需求。

MongoDB原生的Oplog不同於binlog,Oplog只包含當前變更的字段值,Oplog缺少變更前的值,以及未變更字段的值,change stream也無法拿到變更前的值,基於這樣的問題,我們引入Hbase字段多版本的特性,先預熱數據到Hbase中,然後通過查詢到的字段舊版本值,進行數據合併,構成完整的變更事件。

MongoDB事件數據合併流程

4.寫在最後

MongoDB集羣在評論中臺項目中已上線運行了兩年,過程中完成了約20+個業務方接入,承載了百億級評論回覆數據的存儲,表現較爲穩定。BSON非結構化的數據,也支撐了我們多個版本業務的快速升級。而熱門數據內存化存儲引擎,較大的提高了數據讀取的效率。另外我們針對MongoDB實現了異步事件採集能力,進一步擴展了MongoDB的應用場景。

但對於MongoDB來說,集羣化部署是一個不可逆的過程,集羣化後也帶來了索引,分片策略等較多的限制。因此一般業務在使用MongoDB時,副本集方式就能支撐TB級別的存儲和查詢,並非一定需要使用集羣化方式。

以上內容基於MongoDB 4.0.9版本特性,和最新版本的MongoDB細節上略有差異。

關於作者:

vivo互聯網技術:百靈評論項目開發團隊,魯班事件平臺開發團隊,bees數據採集團隊

參考資料:
https://docs.mongodb.com/manual/introduction/

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