IM開發技術學習:揭祕微信朋友圈這種資訊推流背後的系統設計

語言: CN / TW / HK

本文由徐寧發表於騰訊大講堂,原題“程式設計師如何把你關注的內容推送到你眼前?揭祕資訊流推薦背後的系統設計”,有改動和修訂。

1、引言

資訊推流(以下簡稱“Feed流”)這種功能在我們手機APP中幾乎無處不在(尤其是社交/社群產品中),最常用的就是微信朋友圈、新浪微博等。

對Feed流的定義,可以簡單理解為只要大拇指不停地往下劃手機螢幕,就有一條條的資訊不斷湧現出來。就像給牲畜喂飼料一樣,只要它吃光了就要不斷再往裡加,故此得名Feed(飼養)。

大多數帶有Feed流功能的產品都包含兩種Feed流:

  • 1)一種是基於演算法:即動態演算法推薦,比如今日頭條、抖音短視訊;
  • 2)一種是基於關注:即社交/好友關係,比如微信、知乎。

例如下圖中的微博和知乎,頂欄的頁卡都包含“關注”和“推薦”這兩種:

如上圖中這兩種Feed流,它們背後用到的技術差別會比較大。不同於“推薦”頁卡那種千人千面演算法推薦的方式,通常“關注”頁卡所展示的內容先後順序都有固定的規則,最常見的規則是基於時間線來排序,也就是展示“我關注的人所發的帖子、動態、心情,根據釋出時間從晚到早依次排列”。

本文將重點討論的是“關注”功能對應的技術實現:先總結常用的基於時間線Feed流的後臺技術實現方案,再結合具體的業務場景,根據實際需求在基本設計思路上做一些靈活的運用。

2、本文作者

徐寧:騰訊應用開發工程師,騰訊學院講師,畢業於上海交通大學。目前負責騰訊智慧零售業務的後端開發工作,有豐富的視訊直播,自動化營銷系統開發經驗。

3、Feed流技術實現方案1:讀擴散

讀擴散也稱為“拉模式”,這應該是最符合我們認知直覺的一種技術實現方式。

原理如下圖:

如上圖所示:每一個內容釋出者都有一個自己的發件箱(“我釋出的內容”),每當我們發出一個新帖子,都存入自己的發件箱中。當我們的粉絲來閱讀時,系統首先需要拿到粉絲關注的所有人,然後遍歷所有釋出者的發件箱,取出他們所釋出的帖子,然後依據釋出時間排序,展示給閱讀者。

這種設計:閱讀者讀一次Feed流,後臺會擴散為N次讀操作(N等於關注的人數)以及一次聚合操作,因此稱為讀擴散。每次讀Feed流相當於去關注者的收件箱主動拉取帖子,因此也得名——拉模式。

這種模式:

  • 1)好處是:底層儲存簡單,沒有空間浪費;
  • 2)壞處是:每次讀操作會非常重,操作非常多。

設想一下:如果我關注的人數非常多,遍歷一遍我所關注的所有人,並且再聚合一下,這個系統開銷會非常大,時延上可能達到無法忍受的地步。

因此:讀擴散主要適用系統中閱讀者關注的人沒那麼多,並且刷Feed流並不頻繁的場景。

拉模式還有一個比較大的缺點:就是分頁不方便,我們刷微博或朋友圈,肯定是隨著大拇指在螢幕不斷划動,內容一頁一頁的從後臺拉取。如果不做其他優化,只採用實時聚合的方式,下滑到比較靠後的頁碼時會非常麻煩。

4、Feed流技術實現方案2:寫擴散

據統計:大多數Feed流產品的讀寫比大概在100:1,也就是說大部分情況都是刷Feed流看別人發的朋友圈和微博,只有很少情況是自己親自發一條朋友圈或微博給別人看。

因此:讀擴散那種很重的讀邏輯並不適合大多數場景。

我們寧願讓發帖的過程複雜一些,也不願影響使用者讀Feed流的體驗,因此稍微改造一下前面方案就有了寫擴散。寫擴散也稱為“推模式”,這種模式會對拉模式的一些缺點做改進。

原理如下圖:

如上圖所示:系統中每個使用者除了有發件箱,也會有自己的收件箱。當釋出者發表一篇帖子的時候,除了往自己發件箱記錄一下之外,還會遍歷釋出者的所有粉絲,往這些粉絲的收件箱也投放一份相同內容。這樣閱讀者來讀Feed流時,直接從自己的收件箱讀取即可。

這種設計:每次發表帖子,都會擴散為M次寫操作(M等於自己的粉絲數),因此成為寫擴散。每篇帖子都會主動推送到所有粉絲的收件箱,因此也得名推模式。

這種模式可想而知:發一篇帖子,背後會涉及到很多次的寫操作。通常為了發帖人的使用者體驗,當釋出的帖子寫到自己發件箱時,就可以返回釋出成功。後臺另外起一個非同步任務,不慌不忙地往粉絲收件箱投遞帖子即可。

寫擴散的好處在於通過資料冗餘(一篇帖子會被儲存M份副本),提升了閱讀者的使用者體驗。通常適當的資料冗餘不是什麼問題,但是到了微博明星這裡,完全行不通。比如目前微博粉絲量Top2的謝娜與何炅,兩個人微博粉絲過億。

設想一下:如果單純採用推模式,那每次謝娜何炅發一條微博,微博後臺都要地震一次。一篇微博導致後臺上億次寫操作,這顯然是不可行的。

另外:由於寫擴散是非同步操作,寫的太慢會導致帖子發出去半天,有些粉絲依然沒能看見,這種體驗也不太好。

通常寫擴散適用於好友量不大的情況,比如微信朋友圈正是寫擴散模式。每一名微信使用者的好友上限為5000人,也就是說你發一條朋友圈最多也就擴散到5000次寫操作,如果非同步任務效能好一些,完全沒有問題。

關於微信朋友圈的技術資料:

5、Feed流技術實現方案2:讀寫混合模式

讀寫混合也可以稱作“推拉結合”,這種方式可以兼具讀擴散和寫擴散的優點。

我們首先來總結一下讀擴散和寫擴散的優缺點:

見上圖:仔細比較一下讀擴散與寫擴散的優缺點,不難發現兩者的適用場景是互補的。

因此:在設計後臺儲存的時候,我們如果能夠區分一下場景,在不同場景下選擇最適合的方案,並且動態調整策略,就實現了讀寫混合模式。

原理如下圖:

以微博為例:當何炅這種粉絲量超大的人發帖時,將帖子寫入何炅的發件箱,另外提取出來何炅粉絲當中比較活躍的那一批(這已經可以篩掉大部分了),將何炅的帖子寫入他們的收件箱。當一個粉絲量很小的路人甲發帖時,採用寫擴散方式,遍歷他的所有粉絲並將帖子寫入粉絲收件箱。

對於那些活躍使用者登入刷Feed流時:他直接從自己的收件箱讀取帖子即可,保證了活躍使用者的體驗。

當一個非活躍的使用者突然登入刷Feed流時:

  • 1)一方面需要讀他的收件箱;
  • 2)另一面需要遍歷他所關注的大V使用者的發件箱提取帖子,並且做一下聚合展示。

在展示完後:系統還需要有個任務來判斷是否有必要將該使用者升級為活躍使用者。

因為有讀擴散的場景存在,因此即使是混合模式,每個閱讀者所能關注的人數也要設定上限,例如新浪微博限制每個賬號最多可以關注2000人。

如果不設上限:設想一下有一位使用者把微博所有賬號全部關注了,那他開啟關注列表會讀取到微博全站所有帖子,一旦出現讀擴散,系統必然崩潰(即使是寫擴散,他的收件箱也無法容納這麼多的微博)。

讀寫混合模式下,系統需要做兩個判斷:

  • 1)哪些使用者屬於大V,我們可以將粉絲量作為一個判斷指標;
  • 2)哪些使用者屬於活躍粉絲,這個判斷標準可以是最近一次登入時間等。

這兩處判斷標準就需要在系統發展過程中動態地識別和調整,沒有固定公式了。

可以看出:讀寫結合模式綜合了兩種模式的優點,屬於最佳方案。

然而他的缺點是:系統機制非常複雜,給程式設計師帶來無數煩惱。通常在專案初期,只有一兩個開發人員,使用者規模也很小的時候,一步到位地採用這種混合模式還是要慎重,容易出bug。當專案規模逐漸發展到新浪微博的水平,有一個大團隊專門來做Feed流時,讀寫混合模式才是必須的。

6、Feed流中的分頁問題

上面幾節已經敘述了基於時間線的幾種Feed流常見設計方案,但實操起來會比理論要麻煩許多。

接下來專門討論一個Feed流技術方案中的痛點——Feed流的分頁。

不管是讀擴散還是寫擴散,Feed流本質上是一個動態列表,列表內容會隨著時間不斷變化。傳統的前端分頁引數使用page_size和page_num,分表表示每頁幾條,以及當前是第幾頁。

對於一個動態列表會有如下問題:

如上圖所示:在T1時刻讀取了第一頁,T2時刻有人新發表了“內容11”,在T3時刻如果來拉取第二頁,會導致錯位出現,“內容6”在第一頁和第二頁都被返回了。事實上,但凡兩頁之間出現內容的新增或刪除,都會導致錯位問題。

為了解決這一問題:通常Feed流的分頁入參不會使用page_size和page_num,而是使用last_id來記錄上一頁最後一條內容的id。前端讀取下一頁的時候,必須將last_id作為入參,後臺直接找到last_id對應資料,再往後偏移page_size條資料,返回給前端,這樣就避免了錯位問題。

如下圖:

採用last_id的方案有一個重要條件:就是last_id本身這條資料不可以被硬刪除。

設想一下:

  • 1)上圖中T1時刻返回5條資料,last_id為內容6;
  • 2)T2時刻內容6被髮布者刪除;
  • 3)那麼T3時刻再來請求第二頁,我們根本找不到last_id對應的資料了,也就無法確認分頁偏移量。

通常碰到刪除的場景:我們採用軟刪除方式,只是在內容上置一個標誌位,表示內容已刪除。

由於已經刪除的內容不應該再返回給前端,因此軟刪除模式下,找到last_id並往後偏移page_size條,如果其中有被刪除的資料會導致獲得足夠的資料條數給前端。

這裡一個解決方案是找不夠繼續再往下找,另一種方案是與前端協商,允許返回條數少於page_size條,page_size只是個建議值。甚至大家約定好了以後,可以不要page_size引數。

7、Feed流技術方案在某直播應用中的實踐

7.1 需求背景

本節將結合實際業務,分享一下實際場景中碰到的一個非常特殊的Feed流設計方案。

xx 直播是一款直播帶貨工具,主播可以建立一場未來時刻的直播,到時間後開播賣貨,直播結束後,主播的粉絲可以檢視直播回放。

這樣,每個直播場次就有三種狀態——預告中(建立一場直播但還未開播)、直播中、回放。

作為觀眾,我可以關注多位主播,這樣從粉絲視角來看,也會有個直播場次的Feed流頁面。

這個Feed流最特殊的地方在於它的排序規則:

解釋一下這個Feed流排序規則:

  • 1)我關注的所有主播:正在直播中的場次排在最前;預告中的場次排中間;回放場次排最後;
  • 2)多場次都在直播中的:按開播時間從晚到早排序;
  • 3)多場次都在預告中的:按預計開播時間從早到晚排序;
  • 4)多場次都在回放的:按直播結束時間從晚到早排序。

7.2 問題分析

本需求最複雜的點在於Feed流內容融入的“狀態”因素,狀態的轉變會直接導致Feed流順序不同。

為了更清晰解釋一下對排序的影響,我們可以用下圖詳細說明:

上圖中:展示了4個主播的5個直播場次,作為觀眾,當我在T1時刻開啟頁面,看到的順序是場次3在最上方,其餘場次均在預告狀態,按照預計開播時間從早到晚展示。當我在T2時刻開啟頁面,場次5在最上方,其餘有三場在預告狀態排在中間,場次3已經結束了所以排在最後。以此類推,直到所有直播都結束,所有場次最終的狀態都會變為回放。

這裡需要注意一點:如果我在T1時刻開啟第一頁,然後盯著頁面不動,一直盯到T4時刻再下劃到第二頁,這時上一頁的last_id,即分頁偏移量很有可能因為直播狀態變化而不知道飛到了什麼位置,這會導致嚴重的錯位問題,以及直播狀態展示不統一的問題(第一頁展示的是T1時刻的直播狀態,第二頁展示的是T4時刻的直播狀態)。

7.3 解決方案

直播系統是個單向關係鏈,和微博有些類似,每個觀眾會關注少量主播,每個主播會可能有非常多的關注者。

由於有狀態變化的存在,寫擴散幾乎無法實現。

因為:如果採用寫擴散的方式,每次主播建立直播、直播開播、直播結束這三個事件發生時導致的場次狀態變化,會擴散為非常多次的寫操作,不僅操作複雜,時延上也無法接受。

微博之所以可以寫擴散:就是因為一條微博發出後,這篇微博就不會再有任何影響排序的狀態轉變。

而在我們場景中:“預告中”與“直播中”是兩個中間態,而“回放”狀態才是所有直播的最終歸宿,一旦進入回放,這場直播也就不會再有狀態轉變。因此“直播中”與“預告中”狀態可以採用讀擴散方式,“回放”狀態採取寫擴散方式。

最終的方案如下圖所示:

如上圖:會影響直播狀態的三種事件(建立直播、開播、結束直播)全部採用監聽佇列非同步處理。

我們為每一位主播維護一個直播中+預告中狀態的優先順序佇列:

  • 1)每當監聽到有主播建立直播時,將直播場次加入佇列中,得分為開播的時間戳的相反數(負數);
  • 2)每當監聽到有主播開播時,把這場直播在佇列中的得分修改為開播時間(正數);
  • 3)每當監聽到有主播結束直播,則非同步地將播放資訊投遞到每個觀眾的回放佇列中。

這裡有一個小技巧:前文提到,直播中狀態按照開播時間從大到小排序,而預告中狀態則按照開播時間從小到大排序,因此如果將預告中狀態的得分全部取開播時間相反數,那排序同樣就成為了從大到小。這樣的轉化可以保證直播中與預告中同處於一個佇列排序。預告中得分全都為負數,直播中得分全都為正數,最後聚合時可以保證所有直播中全都自然排在預告中前面。

另外:前文還提到的另一個問題是T1時刻拉取第一頁,T4時刻拉取第二頁,導致第一頁和第二頁直播間狀態不統一。

解決這個問題的辦法是通過快照方式:當觀眾來拉取第一頁Feed流時,我們依據當前時間,將全部直播中和預告中狀態的場次建立一份快照,使用一個session_id標識,每次前端分頁拉取時,我們直接從快照中讀取即可。如果快照中讀取完畢,證明該觀眾的直播中和預告中場次全部讀完,剩下的則使用回放佇列進行補充。

照此一來,我們的Feed流系統,前端分頁拉取的引數一共有4個:

每當碰到session_id和last_id為空,則證明使用者想要讀取第一頁,需要重新構建快照。

這裡還有一個衍生問題:session_id的如何取值?

答案是:

  • 1)如果不考慮同一個觀眾在多端登入的情況,其實每一位觀眾維護一個快照id即可,也就是直接將系統使用者id設為session_id;
  • 2)如果考慮多端登入的情況,則session_id中必須包含每個端的資訊,以避免多端快照相互影響;
  • 3)如果不心疼記憶體,也可以每次隨機一個字串作為session_id,並設定一個足夠長的過期時間,讓快照自然過期。

以上設計:其實系統計算量最大的時刻就是拉取第一頁,構建快照的開銷。

目前的線上資料,對於只關注不到10個主播的觀眾(這也是大多數場景),拉取第一頁的QPS可以達到1.5萬。如果將第二頁以後的請求也算進來,Feed流的綜合QPS可以達到更高水平,支撐目前的使用者規模已經綽綽有餘。如果我們拉取第一頁時只獲取到前10條即可直接返回,將構建快照操作改為非同步,也許QPS可以更高一些,這可能是後續的優化點。

8、本文小結

幾乎所有基於時間線和關注關係的Feed流都逃不開三種基本設計模式:

  • 1)讀擴散;
  • 2)寫擴散;
  • 3)讀寫混合。

具體到實際業務中,可能會有更復雜的場景,比如本文所說的:

  • 1)狀態流轉影響排序;
  • 2)微博朋友圈場景中會有廣告接入、特別關注、熱點話題等可能影響到Feed流排序的因素。

這些場景就只能根據業務需求,做相對應的變通了。

附錄:更多社交應用架構設計文章

淺談IM系統的架構設計

簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端

一套海量線上使用者的移動端IM架構設計實踐分享(含詳細圖文)

一套原創分散式即時通訊(IM)系統理論架構方案

從零到卓越:京東客服即時通訊系統的技術架構演進歷程

蘑菇街即時通訊/IM伺服器開發之架構選擇

騰訊QQ1.4億線上使用者的技術挑戰和架構演進之路PPT

微信後臺基於時間序的海量資料冷熱分級架構設計實踐

微信技術總監談架構:微信之道——大道至簡(演講全文)

如何解讀《微信技術總監談架構:微信之道——大道至簡》

快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)

移動端IM中大規模群訊息的推送如何保證效率、實時性?

現代IM系統中聊天訊息的同步和儲存方案探討

微信朋友圈千億訪問量背後的技術挑戰和實踐總結

以微博類應用場景為例,總結海量社交系統的架構設計步驟

子彈簡訊光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐

知乎技術分享:從單機到2000萬QPS併發的Redis高效能快取實踐之路

微信技術分享:微信的海量IM聊天訊息序列號生成實踐(演算法原理篇)

微信技術分享:微信的海量IM聊天訊息序列號生成實踐(容災方案篇)

一套高可用、易伸縮、高併發的IM群聊、單聊架構方案設計實踐

社交軟體紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等

社交軟體紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進

社交軟體紅包技術解密(三):微信搖一搖紅包雨背後的技術細節

社交軟體紅包技術解密(四):微信紅包系統是如何應對高併發的

社交軟體紅包技術解密(五):微信紅包系統是如何實現高可用性的

社交軟體紅包技術解密(六):微信紅包系統的儲存層架構演進實踐

社交軟體紅包技術解密(七):支付寶紅包的海量高併發技術實踐

社交軟體紅包技術解密(八):全面解密微博紅包技術方案

社交軟體紅包技術解密(九):談談手Q紅包的功能邏輯、容災、運維、架構等

從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路

從游擊隊到正規軍(二):馬蜂窩旅遊網的IM客戶端架構演進和實踐總結

從游擊隊到正規軍(三):基於Go的馬蜂窩旅遊網分散式IM系統技術實踐

瓜子IM智慧客服系統的資料架構設計(整理自現場演講,有配套PPT)

阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處

微信後臺基於時間序的新一代海量資料儲存架構的設計實踐

阿里技術分享:電商IM訊息平臺,在群聊、直播場景下的技術實踐

一套億級使用者的IM架構技術乾貨(上篇):整體架構、服務拆分等

一套億級使用者的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等

從新手到專家:如何設計一套億級訊息量的分散式IM系統

企業微信的IM架構設計揭祕:訊息模型、萬人群、已讀回執、訊息撤回等

融雲技術分享:全面揭祕億級IM訊息的可靠投遞機制

IM開發技術學習:揭祕微信朋友圈這種資訊推流背後的系統設計

>> 更多同類文章 ……

本文已同步釋出於“即時通訊技術圈”公眾號。

同步釋出連結是:http://www.52im.net/thread-3675-1-1.html

「其他文章」