阿里IM技術分享(四):閒魚億級IM訊息系統的可靠投遞優化實踐

JackJiang發表於2021-09-27

本文由阿里閒魚技術團隊景鬆分享,原題“到達率99.9%:閒魚訊息在高速上換引擎(集大成)”,有修訂和改動,感謝作者的分享。

1、引言

在2020年年初的時候接手了閒魚的IM即時訊息系統,當時的訊息存在各種問題,網上的使用者輿情也是接連不斷。

典型的問題,比如:

1)“聊天訊息經常丟失”;
2)“訊息使用者頭像亂了”;
3)“訂單狀態不對”(相信現在看文章的你還在吐槽閒魚的訊息)。

所以閒魚的即時訊息系統穩定性、可靠性是一個亟待解決的問題。

我們調研了集團內的一些解決方案,例如釘釘的IMPass。如果貿然直接遷移,技術成本和風險都是比較大,包括服務端資料需要雙寫、新老版本相容等。

那麼基於閒魚現有的即時訊息系統架構和技術體系,如何來優化它的訊息穩定性、可靠性?應該從哪裡開始治理?當前系統現狀到底是什麼樣?如何客觀進行衡量?希望本文能讓大家看到一個不一樣的閒魚即時訊息系統。

PS:如果您對IM訊息可靠性還沒有概念,建議先閱讀這篇入門文章《零基礎IM開發入門(三):什麼是IM系統的可靠性?》。

學習交流:

  • 即時通訊/推送技術開發交流5群:215477170 [推薦]
  • 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
  • 開源IM框架原始碼:https://github.com/JackJiang2...

(本文同步釋出於:http://www.52im.net/thread-37...

2、系列文章

本文是系列文章的第4篇,總目錄如下:

《阿里IM技術分享(一):企業級IM王者——釘釘在後端架構上的過人之處》
《阿里IM技術分享(二):閒魚IM基於Flutter的移動端跨端改造實踐》
《阿里IM技術分享(三):閒魚億級IM訊息系統的架構演進之路》
《阿里IM技術分享(四):閒魚億級IM訊息系統的可靠投遞優化實踐》(* 本文)

3、行業方案

經過查閱網上分享的主流訊息可靠投遞技術方案,我進行了簡單總結。

通常IM訊息的投遞鏈路大致分為三步:

1)傳送者傳送;
2)服務端接收然後落庫;
3)服務端通知接收端。

特別是移動端的網路環境比較複雜:

1)可能你發著訊息,網路突然斷掉了;
2)可能訊息正在傳送中,網路突然好了,需要重發。

技術原理圖大如下:

PS:可能很多人對行動網路的複雜性沒有個系統的認知,以下文章有必要系統閱讀:

《通俗易懂,理解行動網路的“弱”和“慢”》
《史上最全移動弱網路優化方法總結》
《為什麼WiFi訊號差?一文即懂!》
《為什麼手機訊號差?一文即懂!》
《高鐵上無線上網有多難?一文即懂!》

那麼,在如此複雜的網路環境下,是如何穩定可靠的進行IM訊息投遞的?

對於傳送者來說,它不知道訊息是否有送達,要想做到確定送達,就需要加一個響應機制。

這個機制類似於下面的響應邏輯:

1)傳送者傳送了一條訊息“Hello”,進入等待狀態;
2)接收者收到這條訊息“Hello”,然後告訴傳送者說我已經收到這條訊息了的確認資訊;
3)傳送者接收到確認資訊後,這個流程就算完成了,否則會重試。

上面流程看似簡單,關鍵是中間有個服務端轉發過程,問題就在於誰來回這個確認資訊,以及什麼時候回這個確認資訊。

網上查到比較多的是如下一個訊息必達模型:

各報文型別解釋如下:

如上面兩圖所示,傳送流程是:

1)A向IM-server傳送一個訊息請求包,即msg:R1;
2)IM-server在成功處理後,回覆A一個訊息響應包,即msg:A1;
3)如果此時B線上,則IM-server主動向B傳送一個訊息通知包,即msg:N1(當然,如果B不線上,則訊息會儲存離線)。

如上面兩圖所示,接收流程是:

1)B向IM-server傳送一個ack請求包,即ack:R2;
2)IM-server在成功處理後,回覆B一個ack響應包,即ack:A2;
3)IM-server主動向A傳送一個ack通知包,即ack:N2。

正如上述模型所示:一個可靠的訊息投遞機制就是靠的6條報文來保證的,中間任何一個環節出錯,都可以基於這個request-ack機制來判定是否出錯並重試。

我們最終採用的方案也正是參考了上面這個模型,客戶端傳送的邏輯是直接基於http的所以暫時不用做重試,主要是在服務端往客戶端推送的時候,會加上重試的邏輯。

限於篇幅,本文就不詳細展開,有興趣可以系統學習以下幾篇:

《從客戶端的角度來談談移動端IM的訊息可靠性和送達機制》
《IM訊息送達保證機制實現(一):保證線上實時訊息的可靠投遞》
《IM訊息送達保證機制實現(二):保證離線訊息的可靠投遞》
《完全自已開發的IM該如何設計“失敗重試”機制?》
《一套億級使用者的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等》
《理解IM訊息“可靠性”和“一致性”問題,以及解決方案探討》
《融雲技術分享:全面揭祕億級IM訊息的可靠投遞機制》

4、當前面臨的具體問題

4.1 概述
在解決訊息可靠投遞這個問題之前,我們肯定首先應該搞清楚當前面臨的具體問題到底有哪些。

然而在接手這套即時訊息系統時,並沒有相關的準確資料可供參考,所以當前第一步還是要對這套訊息系統做一個完整的排查,於是我們對訊息做了全鏈路埋點。

具體的埋點環節如下:

基於訊息的整個鏈路,我們梳理出來了幾個關鍵的指標:

1)傳送成功率;
2)訊息到達率;
3)客戶端落庫率。

這次整個資料的統計都是基於埋點來做的,但在埋點的過程中發現了一個很大的問題:當前這套即時訊息系統沒有一個全域性唯一的訊息ID。這導致在全鏈路埋點的過程中,無法唯一確定這條訊息的生命週期。

4.2 訊息唯一性問題

如上圖所示,當前的訊息是通過3個變數來確定唯一性的:

1)SessionID: 當前會話的ID;
2)SeqID:使用者當前本地傳送的訊息序號,服務端是不關心此資料,完全是透傳;
3)Version:這個比較重要,是訊息在當前會話中的序號,已服務端為準,但是客戶端也會生成一個假的version。

以上圖為例:當A和B同時傳送訊息的時候,都會在本地生成如上幾個關鍵資訊,當A傳送的訊息(黃色)首先到達服務端,因為前面沒有其他version的訊息,所以會將原資料返回給A,客戶端A接收到訊息的時候,再跟本地的訊息做合併,只會保留一條訊息。同時服務端也會將此訊息傳送給B,因為B本地也有一個version=1的訊息,所以服務端過來的訊息就會被過濾掉,這就出現訊息丟失的問題。

當B傳送訊息到達服務端後,因為已經有version=1的訊息,所以服務端會將B的訊息version遞增,此時訊息的version=2。這條訊息傳送給A,和本地訊息可以正常合併。但是當此訊息返回給B的時候,和本地的訊息合併,會出現2條一樣的訊息,出現訊息重複,這也是為什麼閒魚之前總是出現訊息丟失和訊息重複最主要的原因。

4.3 訊息推送邏輯問題
當前訊息的推送邏輯也存在很大的問題,傳送端因為使用http請求,傳送的訊息內容基本不會出問題,問題是出現在服務端給另外一端推送的時候。

如下圖所示:

如上圖所示:服務端在給客戶端推送的時候,會先判斷此時客戶端是否線上,如果線上才會推送,如果不線上就會推離線訊息。

這個做法就非常的簡單粗暴:長連線的狀態如果不穩定,導致客戶端真實狀態和服務端的儲存狀態不一致,就導致訊息不會推送到端上。

4.4 客戶端邏輯問題
除了以上跟服務端有關係外,還有一類問題是客戶端本身設計的問題。

可以歸結為以下幾種情況:

1)多執行緒問題:反饋訊息列表頁面會出現佈局錯亂,本地資料還沒有完全初始化好,就開始渲染介面;
2)未讀數和小紅點的計數不準:本地的顯示資料和資料庫儲存的不一致;
3)訊息合併問題:本地在合併訊息的時候,是分段合併的,不能保證訊息的連續性和唯一性。

諸如以上的幾種情況,我們首先是對客戶端的程式碼做了梳理與重構。

架構如下圖所示:

5、我們的優化工作1:升級通心核心

解決問題第一步就是解決當前訊息系統唯一性的問題。

我們也調研了釘釘的方案,釘釘是服務端全域性維護訊息的唯一ID,考慮到閒魚即時訊息系統的歷史包袱,我們這邊採用UUID作為訊息的唯一ID,這樣就可以在訊息鏈路埋點以及去重上得到很大的改善。

5.1 解決訊息唯一性
在新版本的APP上面,客戶端會生成一個uuid,對於老版本無法生成的情況,服務端也會補充上相關資訊。

訊息的ID類似於 a1a3ffa118834033ac7a8b8353b7c6d9,客戶端在接收到訊息後,會先根據MessageID來去重,然後基於Timestamp排序就可以了,雖然客戶端的時間可能不一樣,但是重複的概率還是比較小。

以iOS端為例,程式碼大致如下:

  • (void)combileMessages:(NSArray<PMessage>)messages {
    ...
    // 1. 根據訊息MessageId進行去重
    NSMutableDictionary *messageMaps = [self containerMessageMap];
    for (PMessage *message in msgs) {

      [messageMaps setObject:message forKey:message.messageId];

    }
    引用
    // 2. 訊息合併後排序
    NSMutableArray *tempMsgs = [NSMutableArray array];
    [tempMsgs addObjectsFromArray:messageMaps.allValues];
    [tempMsgs sortUsingComparator:^NSComparisonResult(PMessage _Nonnull obj1, PMessage _Nonnull obj2) {

      // 根據訊息的timestamp進行排序
      return obj1.timestamp > obj2.timestamp;

    }];
    ...
    }

5.2 實現訊息重發、斷線重連機制

基於本文“3、行業方案”一節中的重發重連模型,我們完善了服務端的訊息重發的邏輯、客戶端完善了斷線重連的邏輯。

具體措施是:

1)客戶端會定時檢測ACCS長連線是否聯通;
2)服務端會檢測裝置是否線上,如果線上會推送訊息,並會有超時等待;
3)客戶端接收到訊息之後,會返回一個Ack。
5.3 優化資料同步邏輯
重發重連解決的基礎網路層的問題,接下來就要看下業務層的問題。

現有訊息系統中,很多複雜情況是通過在業務層增加相容程式碼來解決的,訊息的資料同步就是一個很典型的場景。

在完善資料同步的邏輯之前,我們也調研過釘釘的一整套資料同步方案,他們主要是由服務端來保證的,背後有一個穩定的長連線保證。

釘釘的資料同步方案大致流程如下:

我們的服務端暫時還沒有這種能力,所以閒魚這邊只能從客戶端來控制資料同步的邏輯。

資料同步的方式包括:

1)拉取會話;
2)拉取訊息;
3)推送訊息等。
因為涉及到的場景比較複雜,之前有個場景就是推送會觸發增量同步,如果推送過多的話,會同時觸發多次網路請求,為了解決這個問題,我們也做了相關的推拉佇列隔離。

客戶端控制的策略就是如果在拉取的話,會先將push過來的訊息加到快取佇列裡面,等拉取的結果回來,會再跟本地快取的邏輯做合併,這樣就可以避免多次網路請求的問題。

5.4 客戶端資料模型優化
客戶端在資料組織形式上,主要分2中:會話和訊息,會話又分為:虛擬節點、會話節點和資料夾節點。

在客戶端會構建上圖一樣的樹,這棵樹主要儲存的是會話顯示的相關資訊,比如未讀數、紅點以及最新訊息摘要,子節點更新,會順帶更新到父節點,構建樹的過程也是已讀和未讀數更新的過程。

其中比較複雜的場景是閒魚情報社,這個其實是一個資料夾節點,它包含了很多個子的會話,這就決定了他的訊息排序、紅點計數以及訊息摘要的更新邏輯會更復雜,服務端告知客戶端子會話的列表,然後客戶端再去拼接這些資料模型。

5.5 服務端儲存模型優化

在前述內容中,我大概講了客戶端的請求邏輯,即歷史訊息會分為增量和全量域同步。

這個域其實是服務端的一層概念,本質上就是使用者訊息的一層快取,訊息過來之後會暫存在快取中,加速訊息讀取。

但是這個設計也存在一個缺陷:就是域環是有長度的,最多儲存256條,當使用者的訊息數多於256條,只能從資料庫中讀取。

關於服務端的儲存方式,我們也調研過釘釘的方案——是寫擴散,優點就是可以很好地對每位使用者的訊息做定製化,缺點就是儲存量很很大。

我們的這套解決方案,應該是介於讀擴散和寫擴散之間的一種解決方案。這個設計方式不僅使客戶端邏輯複雜,服務端的資料讀取速度也會比較慢,後續這塊也可以做優化。

6、我們的優化工作2:增加質量監控體系

在做客戶端和服務端的全鏈路改造的同時,我們也對訊息線上的行為做了監控和排查的邏輯。

6.1 全鏈路排查

全鏈路排查是基於使用者的實時行為日誌,客戶端的埋點通過集團實時處理引擎Flink,將資料清洗到SLS裡面。

使用者的行為包括:

1)訊息引擎對訊息的處理;
2)使用者的點選/訪問頁面的行為;
3)使用者的網路請求。
服務端側會有一些長連線推送以及重試的日誌,也會清洗到SLS,這樣就組成了從服務端到客戶端全鏈路的排查的方案。

6.2 對賬系統
當然為了驗證訊息的準確性,我們還做了對賬系統:

在使用者離開會話的時候,我們會統計當前會話一定數量的訊息,生成一個md5的校驗碼,上報到服務端。服務端拿到這個校驗碼之後再判定是否訊息是正確的。

經過抽樣資料驗證,訊息的準確性基本都在99.99%。

7、資料指標統計方法優化

我們在統計訊息的關鍵指標時,遇到點問題:之前我們是用使用者埋點來統計的,發現會有3%~5%的資料差。

後來我們採用抽樣實時上報的資料來計算資料指標:

訊息到達率 = 客戶端實際收到的訊息量 / 客戶端應該收到的訊息量

客戶端實際收到的訊息的定義為“訊息落庫才算是”。

該指標不區分離線線上,取使用者當日最後一次更新裝置時間,理論上當天且在此時間之前下發的訊息都應該收到。

經過前述優化工作,我們最新版本的訊息到達率已經基本達到99.9%,從輿情上來看,反饋丟訊息的也確實少了很多。

8、未來規劃

整體看來,經過一年的優化治理,我們的即時訊息系統各項指標在慢慢變好。

但還是存在一些待優化的方面:

1)訊息的安全性不足:容易被黑產利用,藉助訊息傳送一些違規的內容;
2)訊息的擴充套件性較弱:增加一些卡片或者能力就要發版,缺少了動態化和擴充套件的能力。
3)底層的伸縮性不足:現在底層協議比較難擴充套件,後續還是要規範一下協議。

從業務角度看,訊息應該是一個橫向支撐的工具性或者平臺型的產品,且可以快速對接二方和三方的快速對接。

接下來,我們會持續關注訊息相關的使用者輿情,希望閒魚即時訊息系統能幫助使用者更好的完成業務交易。

附錄:更多相關文章

[1] 更多阿里巴巴的技術資源:
《阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處》
《現代IM系統中聊天訊息的同步和儲存方案探討》
《阿里技術分享:深度揭祕阿里資料庫技術方案的10年變遷史》
《阿里技術分享:阿里自研金融級資料庫OceanBase的艱辛成長之路》
《來自阿里OpenIM:打造安全可靠即時通訊服務的技術實踐分享》
《釘釘——基於IM技術的新一代企業OA平臺的技術挑戰(視訊+PPT) [附件下載]》
《阿里技術結晶:《阿里巴巴Java開發手冊(規約)-華山版》[附件下載]》
《重磅釋出:《阿里巴巴Android開發手冊(規約)》[附件下載]》
《作者談《阿里巴巴Java開發手冊(規約)》背後的故事》
《《阿里巴巴Android開發手冊(規約)》背後的故事》
《乾了這碗雞湯:從理髮店小弟到阿里P10技術大牛》
《揭祕阿里、騰訊、華為、百度的職級和薪酬體系》
《淘寶技術分享:手淘億級移動端接入層閘道器的技術演進之路》
《難得乾貨,揭祕支付寶的2維碼掃碼技術優化實踐之路》
《淘寶直播技術乾貨:高清、低延時的實時視訊直播技術解密》
《阿里技術分享:電商IM訊息平臺,在群聊、直播場景下的技術實踐》
《阿里技術分享:閒魚IM基於Flutter的移動端跨端改造實踐》
《阿里IM技術分享(三):閒魚億級IM訊息系統的架構演進之路》
《阿里IM技術分享(四):閒魚億級IM訊息系統的可靠投遞優化實踐》
[2] 有關IM架構設計的文章:
《淺談IM系統的架構設計》
《簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端》
《一套海量線上使用者的移動端IM架構設計實踐分享(含詳細圖文)》
《一套原創分散式即時通訊(IM)系統理論架構方案》
《從零到卓越:京東客服即時通訊系統的技術架構演進歷程》
《蘑菇街即時通訊/IM伺服器開發之架構選擇》
《騰訊QQ1.4億線上使用者的技術挑戰和架構演進之路PPT》
《微信後臺基於時間序的海量資料冷熱分級架構設計實踐》
《微信技術總監談架構:微信之道——大道至簡(演講全文)》
《如何解讀《微信技術總監談架構:微信之道——大道至簡》》
《快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)》
《移動端IM中大規模群訊息的推送如何保證效率、實時性?》
《現代IM系統中聊天訊息的同步和儲存方案探討》
《微信朋友圈千億訪問量背後的技術挑戰和實踐總結》
《子彈簡訊光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐》
《微信技術分享:微信的海量IM聊天訊息序列號生成實踐(演算法原理篇)》
《一套高可用、易伸縮、高併發的IM群聊、單聊架構方案設計實踐》
《社交軟體紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等》
《從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》
《從游擊隊到正規軍(二):馬蜂窩旅遊網的IM客戶端架構演進和實踐總結》
《從游擊隊到正規軍(三):基於Go的馬蜂窩旅遊網分散式IM系統技術實踐》
《瓜子IM智慧客服系統的資料架構設計(整理自現場演講,有配套PPT)》
《IM開發基礎知識補課(九):想開發IM叢集?先搞懂什麼是RPC!》
《阿里技術分享:電商IM訊息平臺,在群聊、直播場景下的技術實踐》
《一套億級使用者的IM架構技術乾貨(上篇):整體架構、服務拆分等》
《一套億級使用者的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等》
《從新手到專家:如何設計一套億級訊息量的分散式IM系統》
《企業微信的IM架構設計揭祕:訊息模型、萬人群、已讀回執、訊息撤回等》
《融雲技術分享:全面揭祕億級IM訊息的可靠投遞機制》
《IM開發技術學習:揭祕微信朋友圈這種資訊推流背後的系統設計》
《阿里IM技術分享(三):閒魚億級IM訊息系統的架構演進之路》

本文已同步釋出於“即時通訊技術圈”公眾號。
同步釋出連結是:http://www.52im.net/thread-37...

相關文章