手機QQ公衆號億級消息實時羣發架構

編者按:高可用架構分享及傳播在架構領域具有典型意義的文章,本文由孫子荀分享。轉載請註明來自高可用架構公衆號 ArchNotes。

孫子荀,2009 年在華爲從事內核和分佈式系統的開發工作;2011 年在百度從事過高性能計算方面的工作;2012 年加入騰訊進行 QQ 羣廣告系統的開發,隨後負責騰訊雲加速的帶寬調度系統的設計研發;2014 年開始手 Q 公衆號後臺設計開發。 騰訊優秀講師,包括 Linux 內核的講授,並行計算等課程。在內核、數據挖掘、計算機廣告等方面有很深的造詣。

手 Q 公衆號是從去年底的開始開發,期間封閉開發半年時間,基礎能力已經對齊微信,而且在其他領域有新的拓展。目前已經對騰訊系的業務開放,對外小範圍開放。等待政府批文之後,將全面開放註冊。

現在業務規模支撐百萬公衆號,關係鏈存儲上T 。機器規模春節期間在數百臺,有深圳天津兩個中心,分散在十幾個 IDC 機房。消息規模每天數十億,支撐了手 Q 一半以上的日活,包括騰訊新聞、QQ 音樂、附近、天氣、購物號、訂閱號、興趣號等,只要是手 Q 非個人好友,基本都是公衆號。

我去微信進行授課的時候,和微信的同學做過交流,發現大家的技術挑戰和問題的場景以及背景有着很大的不同,無法簡單的在技術上無法複用。

這裏舉個例子:QQ 公衆號的關係鏈設計之初是爲了承載億級的,這點和微博的收聽關係很像。而微信現在在用 MySQL 集羣的方式,主要都是萬級別的關係鏈。億級在支持高併發訪問和 IDC 一致性的時候就更加有挑戰。

QQ 公衆號從開始就支撐公司內部的億級 DAU 的業務,比如音樂、騰訊新聞、春節紅包等這樣的業務。 消息峯值在每秒數十萬 。 而微信主要是對外,外部用戶加起來很難達到這個量級。

今天受高可用架構邀請,主要介紹的是 QQ 公衆號的羣發子系統,也是公衆號業務用使用頻率最高的一個功能。

羣發需求、場景分析

羣發,無論是微信還是 QQ 公衆號都是使用功能最多的業務。

在微信,羣發只是公衆號運營者對其關注者進行消息發送,然而在 QQ 公衆號裏面,羣發需要支持非常多的複雜組合模式。 比如自定義號碼包、關係鏈&分組、標籤、人羣定向等,這是千人一面。 現在我們也支持了千人千面的羣發方式。

羣發我們一共開發了 4 期,從基礎設施到調度能力。現在是 5 期的發送效果改造(通過 pCTR 開始從營銷往生態用戶考慮)。這個過程我們在不斷的抽象各種層次關係。

我們先說最簡單的場景:關係鏈發送。我們通過關係鏈接口,拉取一批用戶,發送一批。當然這個過程可以並行。到了後來有了業務方提供自己挖掘的號碼包(騰訊基本各個大型業務都會有自己的挖掘團隊)。然後我們發現可以複用這種方式,無非是針對不同底層存儲( NoSQL、文件或者 MySQL)實現一個 JDBC 那樣的存儲適配驅動,最終都是 getBatchFromxxx 、endBatchToxxx 。但是這樣的系統,任務不能自己恢復,沒有狀態維護,發送和拉取嚴重耦合在一起,誰按照需求發展都不獨立。

於是我們決定將所有的發送都收斂到號碼包。無論來自哪個存儲,都收斂到一個發送的號碼包文件。通過文件解耦, 有了這個文件,就天然有了作業任務的一個輸入條件,形成一個作業任務,交給作業系統。作業系統負責任務的調度分發、排隊、self-heal 。

這個系統裏面有像騰訊新聞一樣,一次發送上億用戶的,而且要求峯值每秒數十萬的速度,更多的有像訂閱號那樣一次發送幾萬,每秒併發幾百個任務一起發送的。

在包處理層,我們處理各個需求,對於業務自己挖掘的號碼包,我們做關係鏈過濾、CRC 過濾(不要重發)、頻次配額過濾、黑名單過濾、定向條件過濾等等。

這裏使用了一個 GoF 的責任鏈模式, 根據業務號的屬性和當前的模式來決定到底需要開啓哪些過濾器插件。

在這層根據包來源的介質,我們做了加速處理。

對於十幾億的粉絲,拉取時間受限於關係鏈的存儲網絡開銷,業務機的帶寬維持在 20Mbyte/s,就算並行也有時間浪費。我們希望永遠不要因爲預處理耽誤時間。每次羣發都有這麼大的網絡開銷是十分可惜的。 

所以我們把超過千萬的粉絲全量拉取到本地。同時受益於關係鏈變更的時候,我們會有專門的消息隊列用於生產事件,各個需求模塊監聽按需處理,使得增量同步保證短時間的一致性也變得非常高效。於是我們設計了一個模塊用於把大於某個粉絲級別的用戶保存到本地,這裏其實只需要保存二進制文件。 算一下 1億 * 8byte 大約 760M 的樣子。 這樣對於 1T 的磁盤,單機就可以滿足公衆號 所有千萬級以上號碼的粉絲。

同時產生一個固定頻率的增量文件。每次使用的時候進行全量的掃描,再做一遍增量文件的邏輯(如:{"uin":"A"; "op" :"關注";"time":""};{"uin":"A"; "op" :"取消關注";"time":""})。

PS:在騰訊一直有一個思想叫一切皆可控,就是系統服務能力不是壓測出來的,而是設計的時候,就清楚的知道單機的服務能力,根據網絡模型、級聯帶寬、包大小處理能力等進行預估。這就需要設計的人有內核、應用層編程、技術選件的把握,要求挺高的,騰訊的後臺 T4 纔能有這個 level 。:)

對於加速文件,當集羣機器過多的時候,如何保證的關係鏈文件已經存在集羣中所有機器?

  • 一種方式就是放在分佈式文件系統上,然後本機定時從中拷貝出來,但是會導致 CFS 出口帶寬壓力。
  • 一種就是通過 BT 來分發,我們團隊之前是從事下載的,這個在旋風時代做的已經非常多了。通過 btsync 來做到內網搭建一個 DHT 網絡進行 P2P 下載。速度非常快。

但是優化都可能導致引入新的問題。如果有粉絲劇烈的變化的情況,剛剛的優化其實是不可靠的。

於是這裏我們堅持一定要進行實時的合理性校驗。但是如果每個都去校驗效率非常低,不實際甚至不如不優化。 我們通過抽樣校驗,隨機抽取10份樣本, 每次採樣的數目範圍在 [100,10000], 具體是粉絲總數 / 1萬,錯誤率控制在 10次錯誤率的算術平均值必須小於 2%,且單次不能超過 10% 。否則我們會放棄優化回源從原始的存儲業務服務拉取。

說這個是希望大家知道任何優化可能都是有損的,還是要考慮可能的風險。

羣發架構

層級看起來如下:

框架從設計的開始就有以下幾個目標:

  1. 分層,任意層都可以水平擴展。
  2. 不依賴本地文件。
  3. Self-Heal(機房、IDC)。
  4. 平滑升級。
  5. 動態調度。

任意層水平擴展

就是對於任何運行的狀態都能恢復,比如 Spark 的 check point 機制。我們所有的任務,在生命週期都通過騰訊的容災 CDB 進行狀態落地,任務文件都有流水保存。 任務都存放在 CDB 中作爲一條作業,接口層和任務調度層之間通過我們設計的無主並行任務分配算法(Acentric Parallel Task Allocation)進行無損的任務分發(參考了思科的 OSPF )。任務調度層的機器任意添加故障都不會導致任務丟失,而且並行進行任務的批量調度。在調度層和任務分發層以及執行層,通過 ZMQ 這樣的消息組件進行消息傳遞。多生產和多消費者模式。

APTA 介紹

APTA 其實就是一致性HASH + OSPF 路由算法。 簡單介紹如下:

首先作業集羣中的機器有幾種狀態

集羣狀態

ONLINE : 表示機器在線 ,已經被集羣所有的機器知道。

JOINING: 表示機器剛剛進入集羣,還沒有被接受。

WORKING: 表示機器已經加入整個作業的分配中。

OFFLINE : 表示機器已經失去聯繫。且與任何機器都沒有連接。

SILENCE: 靜默狀態,表示當前不進行任務的處理工作。

下面是集羣的狀態扭轉圖:

ONLINE-> SILENCE :在ONLINE狀態下收到作業進程表裏面所有作業進程回覆的LSR包。

SILENCE -> WORKING :SILENCE狀態維持12s 自動變成WORKING 。

WORKING->SILENCE :一旦收到任意一個服務過來的LSR包。

WORKING->OFFLINE: 在WORKING狀態下 沒有收到任何Hello包,自己發出的Hello包也沒有任何回覆。

我們不需要一臺作業進程所在的機器被集羣中一半以上的機器認可,才能正常工作。 因爲主要這臺機器不脫離集羣 ,和任意一個作業機器有網絡連接。都可以存活在 作業集羣中。 擁有處理作業任務的能力。

上面這種場景,藍色機器是完全被接受的。因爲他可以進行作業任務。

動態調度

首先所有調用接口的任務都附屬狀態機,跟着任務的作業狀態進行扭轉。

由於執行作業的機器槽位永遠小於需要被執行的任務,所以我們需要在調度層決定哪些業務可以被調度。在這裏我們參考了 Linux 操作系統的優先級調度,對於每一個作業任務區分是實時作業還是非實時。

實時:緊急任務,無法被搶佔。

非實時:支持搶佔。

根據公式得出一個任務的調度分,然後根據這個調度分來排入優先級隊列。

任務離開調度層都開始真正的作業。第一步就是接受預處理,就是剛剛說號碼包處理的邏輯。處理完畢之後就需要真正的發送了,我們在分發的接口層會評價任務需要的資源。然後根據下游執行機器上的 manger 程序上報的負載情況來決定如何進行任務拆分和資源的分配。

內存和任務的大小有關,CPU 網卡帶寬的需求和發送速度有關。 速度如果業務沒有權利設置,那就是我們根據全局的負載計算出來的。 受限於網卡的處理能力和下游服務器的處理能力,其實就是一個受限的最優化問題 。我們會優先選擇單機的資源,如果單機得不到滿足就拆分任務單元,放到多個機器進行發送。 這裏看到和 yarn 不同,我們把業務調度和資源分配分在了不同層和階段來解決,其實簡化了問題。

我們沒有用 Docker 這樣的技術,感覺還沒必要。但是在作業執行層,我們對於不同的作業有發送的網卡需求,有需要寫 CFS 的網卡需求,爲了保證有限網卡的獨立性,我們通過 cgroup 的 net_cls 來分配業務進程的帶寬。 使得同一機器上的不同進程資源不受影響。

補充一點,剛剛的狀態機,在各個狀態轉換的時候,用了MySQL 的 trigger ,由專門的程序來處理,然後生產到消息隊列給外圍的系統,解耦開來。

爲了保證一定的到達率,我們發送都是在線發送,只對在線的用戶進行發送, 發送完畢在線的,這個作業就算結束了,對於用戶沒有在線那麼我們就會發到這臺機器上的一個leveldb ,形成一個鏈表,接入我們公司的用戶登錄觸發組件。當用戶登錄了,就把用戶身上的這個任務鏈表遍歷,調用接口進行發送。

容災

其實有了比較好的基礎設施之後,容災會變得簡單。

羣發核心挑戰是深圳的任務如果執行一半失敗了,是否能在天津恢復。 這裏的核心問題就是數據如何同步。

首先我們羣發每次發送的時候都會通過 hippo(騰訊的一款消息隊列組件)進行上報。 原始的號碼包保存在了深圳的 CFS 倉庫。當作業失敗了,天津具有了這個原始號碼包文件,並且從 hippo 獲得作業信息和發送的號碼列表,diff 出差異文件,在天津重建餘量的發送任務。如果深圳任務成功則刪除該號碼包文件。

單 IDC 問題就更簡單了。任何時刻任務崩潰了,我們都可以正確的找到上一次執行的位置,然後重新建立一個任務,根據上一次執行的階段,設置適合的狀態,在狀態機中重新觸發相應的處理邏輯。

現在我們羣發系統,我們除了工程上的技術優化以外,主要在整個效果控制的工程建設上進行。

平臺控制

爲了使得我們平臺能夠良好的運轉,用戶不收到過多對用戶沒有價值的消息 。 我們和產品從各個緯度按照規則和背景制定了下面的控制手段。

B 端控制

配額 ,頻次。 根據業務上一次投放的效果點擊率 配合產品的運營策略 來 分配配額。

C 端控制

頻控,決定一個 pCTR ,平臺策略,新鮮度決定一個用戶能收到什麼樣的消息。頻次均勻隨機釋放,一天用戶可以收到最多6條公衆號消息。

爲了防止系統出現 B 運營者,在第二天 11:45 的時候,直接建立第二天 0:00 分的任務(系統限制只能建立 15 分鐘之後的任務)導致第二天釋放的用戶頻次被立刻搶走(因爲沒有競爭)。 這種先到先得的方式使得整個公衆號平臺不能良性的運轉, 所有B的運營者都爭先恐後的建立任務,出現所謂的火車票搶票情況。

於是我們系統變成了在當天隨機釋放頻次。一種簡單的實現方式是:假設一天一個 C 受限制只能收到 6 條,那麼可以從 0 點開始,每隔 4 小時(這個間隔稱之爲競爭區間)隨機釋放一個,這樣一天 24 小時就釋放了 6 條。

實際運行中我們早上 12 小時有 2 個競爭區間,釋放 2 個 C 頻次。下午 12 小時有 4 個競爭區間,釋放 4 個頻次。 當前競爭區間沒有被消耗的頻次自動保存到下一競爭區間。每個B的任務,只和當前競爭區間內將要發送的任務進行競爭。

Q & A

1、剛纔提到羣發只給在線用戶發,用戶是否在線的查詢是通過什麼樣的方式,這塊有沒有瓶頸?

這裏我們對於用戶上線有實時的收集,然後會放入消息隊列,然後每臺機器都會安裝一個 bitmap 的共享內存,42 億bit , 所以判斷在線,只要訪問共享內存就可以了。

2、系統的升級控制是怎麼做的?多長時間可以完成升級?

我們所有的程序都有管理端口,需要升級了通過管理端口發出指令,業務程序完成自身作業之後,就會從集羣中退出,然後開始升級的流程,升級完畢之後 在自動上線處理任務。是一個平滑的升級過程,全系統升級只需要發送指令即可。完成一次升級的時間,取決於當前系統任務的繁忙程度。

3、現在對於一條需要羣發給上億用戶的消息,最快可以做到多長時間羣發完?影響羣發速度的瓶頸主要在哪一塊?

春節上 10 億的消息,我們用了 15 分鐘,取決於機器數目,都是並行拆分,可以更快。事實上我們能做到秒級別。因爲我們其實系統有一個預送達的策略,也就是實現發送到用戶的終端,在時間到了之後,端進行統一展現,特別適合春節搶紅包的場景。

4、發送時候可能不成功或用戶剛好離線,這一塊怎麼處理的?

沒有查詢到的狀態就進入了 leveldb 的發送任務列表,等到在線了之後,實時觸發,只要用戶打開手 Q 就會實時下發 。

5、上文提到離線用戶是存在本地 leveldb,多個羣發任務如果是分佈在多機,這個文件怎麼 merge ?用戶上線時候怎麼獲取全部的離線消息?

leveldb 的數據來自於分佈式文件系統的文件,所以其實只有一份。

6、這個 CFS 倉庫是跨 IDC 的嗎?如果深圳的機房出現問題,天津機房能獲得號碼包文件嗎?

問題問的好,CFS 不跨機房,天津深圳是兩個倉庫,我們依賴 VLAN 專線進行兩個倉庫的同步。事實上我們明年會使用支持 geo 的分佈式文件系統,ceph 或者 glusterfs 這裏還在做驗證。

7、這個羣發系統會落地嗎?比如手機端重裝了,再打開的話是否能看到之前羣發的消息?

這個依賴於終端的實現,從後臺看只要用戶閱讀過消息,我們後臺存儲會被抹去,就不會再次下發的。

8、請問你們的任務的狀態機有多大規模?

狀態機的規模大概有 2 個狀態空間,二十幾種狀態。中斷之後都是秒級別恢復 。 任務一天的級別在幾十萬 這些都是可以水平擴展的。

9、聽說微信一秒推送 1.2 億,您的數據裏峯值提到了數十萬,這個差在哪?

微信消息通道機器規模是我們的 20 倍甚至更多。

10、每個人收到的消息頻率是不一樣的或者每個人消息內容不一樣,發送的時候做這種過濾,數據量這麼大怎麼計算?

這裏的計算量非常大。我們採用的是對於一個號碼包進行並行的多線程處理拆分。發送的時候處理,統一在預處理層進行處理。 其實按照用戶維度差不多是14億用戶, 每個用戶的信息都存放在 leveldb 上,單機 1T 磁盤就可以 hold 住存儲。leveldb 進行按照 hash 的拆分,可以把 CPU 性能榨乾。

11、羣發時有無流量控制?或者 QOS 級別?cgroup 的 netcls 能否具體一些?

tc 建立兩個 classid ,cgexec -g net_cls 創建2個 cgroup 組,然後把需要限制帶寬的進程加入到這個 cgroup 組中。

12、羣一般都有人數限制,比如1000,2000,這個更多的是考慮業務上再多也沒啥意,還是技術上消息廣播有壓力?還有類似於聊天室可能沒有人數上限這一說,比如一個主播有一千萬的觀衆的彈幕聊天,這種推送和我們的羣處理起來有區別嗎?

聊天室應該在萬級別,而且聊天消息推送的場景 應該類似一個廣播寫入用戶的未讀列表就可以了。是一個 Pull 的過程,羣發是一個過億的 Push 的過程 在 Push 之前並不知道要 Push 的對象 需要實時產生。

13、隨機釋放頻次,會不會導致消息到達客戶端的時間不一致?這種情況怎麼處理?

不會不一致。消息到達客戶端取決於公衆化運營者設置的發送時間和持續時間。這個我們會嚴格遵守。

14、對於 iOS 是用長鏈接發送還是蘋果的消息通知,如果中間經過蘋果怎麼保證不把蘋果搞掛?

這裏對於離線走的是蘋果的 PUSH 通道,然後觸發一次長鏈接拉取。但是我們沒有遇到把他們搞掛的情況,對於營銷性質的發送,我們會選擇走預送達通道。

15、前面提到 IDC 之間數據同步,微信的分佈式文件系統或分佈式數據庫是跨 IDC 的嗎(如果跨 IDC ,對於 IDC 之間通訊時延有要求吧)?這一塊能否介紹詳細一點?

微信應該沒有用 CFS,這個問題剛剛回答過,現在的 CFS 是不支持 geo 的,我們用了 VLAN 專線,我們正在測試 ceph 等文件系統進行替換。

16、剛纔有提到定期增量文件,這個頻率大概是多少?什麼時候會做 merge?

實時 merge,收到一個增量就進行處理。 每天會通過 BT 同步全量。

17、想請教一下發送消息的狀態是怎麼控制的?是傳統的那種在數據庫設置發送表做的麼?

狀態先寫入 ZooKeeper ,然後由總控程序從 ZooKeeper 獲得已經確認的狀態寫入數據庫。

轉載自http://chuansong.me/n/2071796

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