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

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


業務背景



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 系統主要問題



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


  • 寫效能差

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


  • 機房不穩定

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


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

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

 

2.2. 新場景下 Feed 雲問題



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


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



資料庫儲存選型



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


  • 高併發讀寫

  • 方便快捷的 DDL 操作

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

  • 讀寫分離支援

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


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



包括大 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  個數量級:



                          4.5.  MongoDB 叢集監控資訊統計


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




                          關於作者



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

                           


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

                          相關文章