行業案例 | MongoDB 在 QQ 小世界 Feed 雲系統中的應用及業務架構優化實踐

MongoDB中文社群 發表於 2022-06-21
MongoDB


業務背景



QQ 小世界最主要的四個 Feed 場景有:基於推薦流的廣場頁、個人主頁,被動訊息列表以及基於關注流的關注頁。


最新 Feed 雲架構由騰訊老 Feeds 雲重構而來,老 Feeds 雲存在如下問題:


  • 效能問題

老系統讀寫效能差,通過調研測試確認 MongoDB 讀寫效能好,同時支援更多查詢功能。老系統無法像 MongoDB 一樣支援欄位過濾( Feed 許可權過濾等),欄位排序(個人主頁贊排序等),事務等。


  • 資料一致性問題

老系統採用了 ckv+tssd 為 tlist 做一層快取,系統依賴多款儲存服務,容易形成資料不一致的問題。


  • 同步元件維護性問題

老系統採用同步中心元件作為服務間的連線橋樑,同步中心元件缺失運維維護,因此採用kafka作為中介軟體作為非同步處理。


  • 儲存元件維護成本高

老系統 Feeds 底層 tlist 、 tssd 擴容、監控資訊等服務能力相對不足。


  • 服務冗餘問題

老系統設計不合理,評論、回覆、贊、轉等互動服務冗雜在 Feeds 服務中,缺乏功能拆分,存在服務過濾邏輯冗雜,協議設計不規範等問題。

 

MongoDB 的優勢


除了讀寫效能,通過調研及測試確認 MongoDB 擁有高效能、低時延、分散式、高壓縮比、天然高可用、多種讀寫分離訪問策略、快速 DDL 操作等優勢,可以方便 QQ 系統業務快速迭代開發。

    

新的 Feed 雲架構,也就是 UFO(UGC Feed all in One)系統,通過一些列的業務側架構優化,儲存服務遷移 MongoDB 後,最終獲得了極大收益,主要收益如下:


  • 維護成本降低

  • 業務效能提升

  • 使用者體驗更好

  • 儲存成本更少

  • 業務迭代開發效率提升

  • Feed 命中率顯著提升,幾乎100%



小世界 Feed 雲系統面臨的問題



通過 Feed 雲系統改造,研發全新的 UFO 系統替換掉之前老的 Feed 雲系統,實現了小世界的效能提升、三地多活容災;同時針對小世界特性,對新 Feed 雲系統做了削峰策略優化,極大的提升了使用者體驗。


2.1. 老 Feed 系統主要問題


行業案例 | MongoDB 在 QQ 小世界 Feed 雲系統中的應用及業務架構優化實踐


改造優化前面臨的問題主要有三個方面:


  • 寫效能差

QQ 小世界為開放關係鏈的社交,時有出現熱 Key 寫入 效能不足的問題。比如被動落地慢,Feed 發表、寫評論吞吐量低等。


  • 機房不穩定

之前小世界所有服務都是單地域部署,機房出現問題就會引起整個服務不可用,單點問題比較突出。


  • 業務增長快,系統負載高

小世界業務目前 DAU 漲的很快,有時候做會出現新使用者蜂擁進入小世界的情況,對後臺的負載造成壓力。

 

2.2. 新場景下 Feed 雲問題


行業案例 | MongoDB 在 QQ 小世界 Feed 雲系統中的應用及業務架構優化實踐


Feed 雲是從 QQ 空間系統裡抽出來的一套通用 Feed 系統,支援 Feed 發表,評論,回覆,點贊等基礎的 UGC 操作。同時支援關係鏈、時間序拉取 Feed ,按 ID 拉取 Feed 等,小世界就是基於這套 Feed 雲系統搭起來的。


但在小世界場景下,Feed 雲還是有很多問題。我們分析 Feed 雲主要存在三個問題。首先是之前提到的慢的問題,主要體現在熱 Key 寫入效能差,SSP 同步框架效能差。其次一個問題是維護成本高,因為他採用了多套儲存,同時程式碼比較老舊,很難融入新的中臺。另外還有使用不方便問題,主要體現在一個是 Feed 非同步落地,也就是我發表一個 Feed,跟上層返回已經發表成功,但實際上還可能沒有在 Feed 系統最終落地。在一個是大 Key 有時候寫不進去,需要手動處理。



資料庫儲存選型



下面就是對儲存進行選型,首先我們要細化對儲存的要求,按照我們的目標 DAU,候選儲存需要滿足以下要求:


  • 高併發讀寫

  • 方便快捷的 DDL 操作

  • 分散式、支援實時快捷擴縮容

  • 讀寫分離支援

  • 海量表資料,新增欄位業務無感知


目前騰訊內部大致符合我們需求的儲存主要是 MongoDB 和 Redis,因此那我就對兩者做了對比,下表裡面列了一些詳細的情況。4C8G低規格 MongoDB 例項效能資料對比結果如下:


行業案例 | MongoDB 在 QQ 小世界 Feed 雲系統中的應用及業務架構優化實踐


包括大 Key 的支援,高併發讀的效能,單熱 Key 寫入效能,區域性讀能力等等。發現在大 Key 支援方面,Tendis 不能滿足我們業務需求,,主要是大 Value 和 Redis 的 Key 是不降冷的,永久佔用記憶體。


所以最終我們選擇了 MongoDB 作為最終儲存。



MongoDB 業務用法及核心 效能優化


 

4.1. MongoDB 表設計


4.1.1. Feed 表及索引設計


  •  InnerFeed 表

InnerFeed 為整個主動被動Feed結構,主要設計Feed核心資訊,設計 Feed 主人、唯一ID、Feed 許可權:

     message InnerFeed   {       string        feedID = 1; //id,儲存層使用,唯一標識一條feed       string        feedOwner = 2; //Feeds主人       trpc.feedcloud.ufobase.SingleFeed    feedData = 3;  //feed詳情資料       uint32        feedMask = 4;      //資訊中心內部使用的     //feed 許可權flag標誌,參考 ENUM_UGCFLAG       trpc.feedcloud.ufougcright.ENUM_UGCFLAG   feedRightFlag = 5;       };


    •  SingleFeed 表

    SingleFeed 為 Feed 基本資訊,Feed 型別,主動、評論被動、回覆被動、Feed 生成時間以及 Feed 詳情:

       message SingleFeed {       int32         feedType = 4;   //Feed型別,主動、評論被動、回覆被動。。。     uint32        feedTime = 5;       FeedsSummary       summary = 7;   //FeedsSummary       map<string, string> ext = 14;      //擴充資訊       ...   };


      •  FeedsSummary 表

      FeedsSummary 為 Feed 詳情,其中 UgcData 為原貼主貼資料,UgcData.content 負責儲存業務自定義的二進位制資料,OpratorInfo 為 Feed 操作詳情,攜帶對應操作的操作人、時間、修改資料等資訊:

         //FeedsSummary   message FeedsSummary   {       UgcData            ugcData = 1;         //內容詳情       OpratorInfo        opInfo  = 2;         //操作資訊   };      // UgcData 詳情   message UgcData   {       string              userID = 1 [(validate.rules).string.tsecstr = true]     uint32              cTime = 2;       bytes               content = 5;   //透傳資料,二進位制buffer   ...   };      message OpratorInfo   {       uint32        action = 1;  //操作型別,如評論、回覆等,見FC_API_ACTION        //操作人uin       string        userID = 2 [(validate.rules).string.tsecstr = true];           uint32                  cTime = 3;           //操作時間      //如果是評論或者回復,當前評論或者回復詳情放這裡,其它回覆內容是全部。         T2Body                  t2body = 4;             uint32                  modifyFlag = 11;      //ENUM_FEEDS_MODIFY_DEFINE      ...   };

         

        • Feed索引設計

        Feed  主要涉及個人主頁 F eed  拉取、關注頁個人 Feed 聚合:

          "key" : {"feedOwner" : -1,"feedData.feedKey" : -1}


          根據 FeedID 拉取指定的 Feed 詳情:

            "key" : {"feedOwner" : -1,"feedData.feedTime" : -1}


            4.1.2. 評論回覆表及所有設計


            •  InnerT2Body 表  

            InnerT2Body 為整個評論結構,回覆作為內嵌陣列內嵌評論中,結構如下:

               message InnerT2Body   {       string   feedID = 1;       //如果是評論或者回復,當前評論或者回復詳情放這裡,其它回覆內容是全部。     trpc.feedcloud.ufobase.T2Body t2body = 2;         };


              • T2Body 表

              T2Body 為評論資訊,涉及評論  ID、時間、內容等基本資訊:

                 message T2Body                   //comment(評論)   {       string              userID = 1;      //評論uin       uint32              cTime = 2;       //評論時間       string              ID = 3;          //ugc中的seq       //評論內容,二進位制結構,可包含文字、圖片等,業務自定義       string              content = 5;         uint32              respNum = 6;     //回覆數       repeated T3Body     vt3Body = 7;        //回覆列表       ...   };


                • T3Body 表

                T3Body 為回覆資訊,涉及回覆 ID、時間、內容、被回覆人的 ID 等基本資訊:

                   message T3Body                     //reply(回覆)   {       string              userID = 1;              //回覆人       uint32              cTime = 2;               //回覆時間       int32               modifyFlag = 3;      //見COMM_REPLY_MODIFYFLAG       string              ID = 4;                  //ugc中的seq       string              targetUID = 5;           //被回覆人       //回覆內容,二進位制結構,可包含文字、圖片等,業務自定義      string              content = 6;             };


                  • 評論索引設計

                  (1)評論主要涉及評論時間序排序:"key" : {"feedID" : -1,"t2body.cTime" : -1}

                  (2)根據評論 ID 拉取指定的評論詳情:"key" : {"feedID" : -1,"t2body.ID" : -1}


                  4.2. 片建選擇及分片方式


                  以 Feed 表為例,QQ 小世界主要查詢都帶有 feedowner ,並且該欄位唯一,因此選擇碼 ID 作為片建,這樣可以最大化提升查詢效能,索引查詢都可以通過同一個分片獲取資料。此外,為了避免分片間資料不均衡引起的 moveChunk 操作,因此選擇 hashed 分片方式,同時提前進行預分片,MongoDB 預設支援 hashed 預分片,預分片方式如下:

                     use feed   sh.enableSharding("feed")   //n為實際分片數   sh.shardCollection("feed.feed", {"feedowner": "hashed"}, false,{numInitialChunks:8192*n})


                    4.3. 低峰期滑動視窗設定


                    當分片間 chunks 資料不均衡的情況下,會觸發自動 balance 均衡,對於低規格例項,balance 過程存在如下問題:


                    • CPU 消耗過高,遷移過程甚至消耗90%左右 CPU

                    • 業務訪問抖動,耗時增加

                    • 慢日誌增加

                    • 異常告警增多


                    以上問題都是由於 balance 過程進行 moveChunk 資料搬遷過程引起,為了快速實現資料從一個分片遷移到另一個分片,MongoDB 內部會不停的把資料從一個分片挪動到另一個分片,這時候就會消耗大量 CPU,從而引起業務抖動。


                    MongoDB 核心也考慮到了 balance 過程對業務有一定影響,因此預設支援了 balance 視窗設定,這樣就可以把 balance 過程和業務高峰期進行錯峰,這樣來最大化規避資料遷移引起的業務抖動。例如設定凌晨0-6點低峰期進行balance視窗設定,對應命令如下:

                       use config   db.settings.update({"_id":"balancer"},{"$set":{"activeWindow":{"start":"00:00","stop":"06:00"}}},true)


                      4.4.  MongoDB 核心優化


                      4.4.1核心認證隨機數生成優化


                      MongoDB 在認證過程中會讀取  /dev/urandom 用來生成隨機字串來返回給客戶端,目的是為了保證每次認證都有個不同的 Auth 變數,以防止被重放攻擊。當同時有大量連線進來時,會導致多個執行緒同時讀取該檔案,而出於安全性考慮,避免多併發讀返回相同的字串(雖然概率極小),在該檔案上加一把 spinlock 鎖(很早期的時候並沒有這把鎖,所以也沒有效能問題),導致 CPU 大部分消耗在 spinlock ,這導致在多併發情況下隨機數的讀取效能較差,而設計者的初衷也不是為了速度。


                      騰訊 MongoDB 核心隨機數優化方法:新版本核心已做相關優化:mongos 啟動的時候讀 /dev/urandom 獲取隨機字串作為種子,傳給偽隨機數演算法,後續的隨機字串由演算法實現,不去核心態獲取。


                      優化前後測試對比驗證方法:通過 Python 指令碼模擬不斷建鏈斷鏈場景,1000個子程式併發寫入,連線池引數設定 socketTimeoutMS=100,maxPoolSize=100 ,其中 socketTimeoutMS 超時時間設定較短,模擬超時後不斷重試直到成功寫入資料的場景(最多100次)。測試主要程式碼如下:

                         def insert(num,retry):       print("insert:",num)       if retry <= 0:           print("unable to write to database")           return       db_client = pymongo.MongoClient(MONGO_URI,maxPoolSize=100,socketTimeoutMS=100)       db = db_client['test']       posts = db['tb3']       try:           saveData = []           for i in range(0, num):               saveData.append({               'task_id':i,               })               posts.insert({'task_id':i})       except Exception as e:           retry -= 1           insert(num,retry)           print("Exception:",e)      def main(process_num,num,retry):       pool = multiprocessing.Pool(processes=process_num)       for i in xrange(num):           pool.apply_async(insert, (100,retry, ))       pool.close()       pool.join()       print "Sub-processes done."      if __name__ == "__main__":       main(1000,1000,100)


                        優化結果如下:


                        優化前: CPU 峰值消耗60核左右,重試次數 1710,而且整體測試耗時要更長,差不多增加2 倍。 優化後: CPU 峰值: 7核 左右,重試次數 1272,整體效能更好。


                        mongos 連線池優化:


                        通過調整 MinSize 和 MaxSize ,將連線數固定,避免非必要的連線過期斷開重建,防止請求波動期間造成大量連線的新建和斷開,能夠很好的緩解毛刺。優化方法如下:

                           ShardingTaskExecutorPoolMaxSize: 70   ShardingTaskExecutorPoolMinSize: 35


                          如下圖所示,17:30調整的,慢查詢少了 2  個數量級:


                          行業案例 | MongoDB 在 QQ 小世界 Feed 雲系統中的應用及業務架構優化實踐


                          4.5.  MongoDB 叢集監控資訊統計


                          如下圖所示,整個 QQ 小世界資料庫儲存遷移 MongoDB 後,平均響應時延控制在5ms以內,整體效能良好。


                          行業案例 | MongoDB 在 QQ 小世界 Feed 雲系統中的應用及業務架構優化實踐



                          關於作者



                          騰訊 PCG 功能開發一組團隊, 騰訊 MongoDB 團隊。   

                           


                          來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69961190/viewspace-2901819/,如需轉載,請註明出處,否則將追究法律責任。