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

1
業務背景

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%

2
小世界 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 有時候寫不進去,需要手動處理。

3
數據庫存儲選型

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

高併發讀寫
方便快捷的 DDL 操作
分佈式、支持實時快捷擴縮容
讀寫分離支持
海量表數據,新增字段業務無感知

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

圖片

包括大 Key 的支持,高併發讀的性能,單熱 Key 寫入性能,局部讀能力等等。發現在大 Key 支持方面,Tendis 不能滿足我們業務需求,,主要是大 Value 和 Redis 的 Key 是不降冷的,永久佔用內存。

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

4
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 主要涉及個人主頁 Feed 拉取、關注頁個人 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以內,整體性能良好。

圖片

5
關於作者

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

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