一、引言
移動互聯網技術改變了旅游的世界,這個領域過去沉重的信息分銷成本被大大降低。用戶與服務供應商之間、用戶與用戶之間的溝通路徑逐漸打通,溝通的場景也在不斷擴展。這促使所有的移動應用開發者都要從用戶視角出發,更好地滿足用戶需求。
論壇時代的馬蜂窩,用戶之間的溝通形式比較單一,主要為單純的回帖回復等。為了以較小的成本快速滿足用戶需求,當時采用的是非實時性消息的方案來實現用戶之間的消息傳遞。
隨著行業和公司的發展,馬蜂窩確立了「內容+交易」的獨特商業模式。在用戶規模不斷增長及業務形態發生變化的背景下,為用戶和商家提供穩定可靠的售前和售后技術支持,成為電商移動業務線的當務之急。
本文由馬蜂窩電商業務 IM 移動端研發團隊分享了馬蜂窩電商業務 IM 移動端的架構演進過程,以及在IM技術力量和資源有限的情況下所踩過的坑等。
系列文章:
學習交流:
- 即時通訊/推送技術開發交流5群:215477170 [推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
(本文同步發布于:http://www.52im.net/thread-2796-1-1.html)
二、設計思路與整體架構
我們結合 B2C,C2B,C2C 不同的業務場景設計實現了馬蜂窩旅游移動端中的私信、用戶咨詢、用戶反饋等即時通訊業務;同時為了更好地為合作商家賦能,在馬蜂窩商家移動端中加入與會話相關的咨詢用戶管理、客服管理、運營資源統計等功能。
目前 IM 涉及到的業務如下:
為了實現馬蜂窩旅游 App 及商家 IM 業務邏輯、公共資源的整合復用及 UI 個性化定制,將問題拆解為以下部分來解決:
1)IM 數據通道與異常重連機制:解決不同業務實時消息下發以及穩定性保障;
2)IM 實時消息訂閱分發機制:解決消息定向發送、業務訂閱消費,避免不必要的請求資源浪費;
3)IM 會話列表 UI 繪制通用解決方案:解決不同消息類型的快速迭代開發和管理復雜問題。
整體實現結構分為 4 個部分進行封裝,分別為下圖中的數據管理、消息注冊分發管理、通用 UI 封裝及業務管理。
三、技術原理和實現過程
3.1、通用數據通道
對于常規業務展示數據的獲取,客戶端需要主動發起請求,請求和響應的過程是單向的,且對實時性要求不高。但對于 IM 消息來說,需要同時支持接收和發送操作,且對實時性要求高。為支撐這種要求,客戶端和服務器之間需要創建一條穩定連接的數據通道,提供客戶端和服務端之間的雙向數據通信。
3.1.1 數據通道基礎交互原理
為了更好地提高數據通道對業務支撐的擴展性,我們將所有通信數據封裝為外層結構相同的數據包,使多業務類型數據使用共同的數據通道下發通信,統一分發處理,從而減少通道的創建數量,降低數據通道的維護成本。
常見的客戶端與服務端數據交互依賴于 HTTP 請求響應過程,只有客戶端主動發起請求才可以得到響應結果。結合馬蜂窩的具體業務場景,我們希望建立一種可靠的消息通道來保障服務端主動通知客戶端,實現業務數據的傳遞。目前采用的是 HTTP 長鏈接輪詢的形式實現,各業務數據消息類型只需遵循約定的通用數據結構,即可實現通過數據通道下發給客戶端。數據通道不必關心數據的具體內容,只需要關注接收與發送。
3.1.2 客戶端數據通道實現原理
客戶端數據通道管理的核心是維護一個業務場景請求棧,在不同業務場景切換過程中入棧不同的業務場景參數數據。每次 HTTP 長鏈接請求使用棧頂請求數據,可以模擬在特定業務場景 (如與不同的用戶私信) 的不同處理。數據相關處理都集中封裝在數據通道管理中,業務層只需在數據通道管理中注冊對應的接收處理即可得到需要的業務消息數據。
3.2、消息訂閱與分發
在軟件系統中,訂閱分發本質上是一種消息模式。非直接傳遞消息的一方被稱為「發布者」,接受消息處理稱為「訂閱者」。發布者將不同的消息進行分類后分發給對應類型的訂閱者,完成消息的傳遞。應用訂閱分發機制的優勢為便于統一管理,可以添加不同的攔截器來處理消息解析、消息過濾、異常處理機制及數據采集工作。
3.2.1 消息訂閱
業務層只專注于消息處理,并不關心消息接收分發的過程。訂閱的意義在于更好地將業務處理和數據通道處理解耦,業務層只需要訂閱關注的消息類型,被動等待接收消息即可。
業務層訂閱需要處理的業務消息類型,在注冊后會自動監控當前頁面的生命周期,并在頁面銷毀后刪除對應的消息訂閱,從而避免手動編寫成對的訂閱和取消訂閱,降低業務層的耦合,簡化調用邏輯。訂閱分發管理會根據各業務類型維護訂閱者隊列用于消息接收的分發操作。
3.2.2 消息分發
數據通道的核心在于維護多消息類型各自對應的訂閱者集合,并將解析的消息分發到業務層。
數據通道由多業務消息共用,在每次請求收到新消息列表后,根據各自業務類型重新拆分成多個消息列表,分發給各業務類型對應的訂閱處理器,最終傳遞至業務層交予對應頁面處理展示。
3.3、會話消息列表繪制
基于不同的場景,如社交為主的私信、用戶服務為主的咨詢反饋等,都需要會話列表的展示形式;但各場景又不完全相同,需要分析當前會話列表的共通性及可封裝復用的部分,以更好地支撐后續業務的擴展。
3.3.1 消息在列表展示的組成結構
IM 消息列表的特點在于消息類型多、UI 展示多樣化,因此需要建立各類型消息和布局的對應關系,在收到消息后根據消息類型匹配到對應的布局添加至對應消息列表。
3.3.2 消息類型與展示布局管理原理
對于不同消息類型及展示,問題的核心在于建立消息類型、消息數據結構、消息展示布局管理的映射關系。以上三者在實現過程中通過建立映射管理表來維護,各自建立列表存儲消息類型/消息體封裝結構/消息展示布局管理,設置對應關系關聯 3 個列表來完成查找。
3.3.3 一次收發消息 UI 繪制過程
各類型消息在內容展示上各有不同,但整體會話消息展示樣式可以分為 3 種,分別是接收消息、發送消息和處于頁面中間的消息樣式,區別只在于內部的消息樣式。所以消息 UI 的繪制可以拆分成 2 個步驟,首先是創建通用的展示容器,然后再填充各消息具體的展示樣式。
拆分的目的在于使各類型消息 UI 處理只需要關注特有數據。而如通用消息如頭像、名稱、消息時間、是否可舉報、已讀未讀狀態、發送失敗/重試狀態等都可以統一處理,降低修改維護的成本,同時使各消息 UI 處理邏輯更少、更清晰,更利于新類型的擴展管理。
收發到消息后,根據消息類型判斷是「發送接收類型」還是「居中展示類型」,找到外層的布局樣式,再根據具體消息類型找到特有的 UI 樣式,拼接在外層布局中,得到完整的消息卡片,然后設置對應的數據渲染到列表中,完成整個消息的繪制。
四、細節優化 & 踩坑經驗
在實現上述 IM 系統的過程中,我們遇到了很多問題,也做了很多細節優化。在這里總結實現時需要考慮的幾點,以供大家借鑒。
4.1、消息去重
在前面的架構中,我們使用 msg_id 來標記消息列表中的每一條消息,msg_id 是根據客戶端上傳的數據,進行存儲后生成的。
客戶端 A 請求 IM 服務器之后生成 msg_id,再通過請求返回和 Polling 分發到客戶端 A 和客戶端 B。當流程成立的時候,客戶端 A 和客戶端 B 通過服務端分發的 msg_id 來進行本地去重。
但這種方案存在以下問題:
當客戶端 A 因為網絡出現問題,無法接受對應發送消息的請求返回的時候,會觸發重發機制。此時雖然 IM 服務器已經接受過一次客戶端 A 的消息發送請求,但是因為無法確定兩個請求是否來自同一條原始消息,只能再次接受,這就導致了重復消息的產生。解決的方法是引入客戶端消息標識 id。因為我們已經依附舊有的 msg_id 做了很多工作,不打算讓客戶端的消息 id 代替 msg_id 的職能,因此重新定義一個 random_id。
random_id = random + time_stamp。random_id 標識了唯一的消息體,由一個隨機數和生成消息體的時間戳生成。當觸發重試的時候,兩次請求的 random_id 會是相同的,服務端可以根據該字段進行消息去重。
4.2、本地化 Push
當我們在會話頁或列表頁的環境下,可以通過界面的變化很直觀地觀察到收取了新消息并更新未讀數。但從會話頁或者列表頁退出之后,就無法單純地從界面上獲取這些信息,這時需要有其他的機制,讓用戶獲知當前消息的狀態。
系統推送與第三方推送是一個可行的選擇,但本質上推送也是基于長鏈接提供的服務。為彌補推送不穩定性與風險,我們采用數據通道+本地通知的形式來完善消息通知機制。通過數據通道下發的消息如需達到推送的提示效果,則攜帶對應的 Push 展示數據。同時會對當前所處的頁面進行判斷,避免對當前頁面的消息內容進行重復提醒。
通過這種數據通道+本地通知展示的機制,可以在應用處于運行狀態的時間內提高消息抵達率,減少對于遠程推送的依賴,降低推送系統的壓力,并提升用戶體驗。
4.3、數據通道異常重連機制
當前數據通道通過 HTTP 長鏈接輪詢 (Polling) 實現。
不同業務場景下對 Polling 的影響如下圖所示:
由于用戶手機所處網絡請求狀態不一,有時候會遇到網絡中斷或者服務端異常的情況,從而終止 Polling 的請求。為能夠讓用戶在網絡恢復后繼續會話業務,需要引入重連機制。
在重試機制 1.0 版本中,對于可能出現較多重試請求的情況,采取的是添加 60s 內連續 5 次報錯延遲重試的限制。
具體流程如下:
在實踐中發現以下問題:
1)當服務端突然異常并持續超過 1 分鐘后,客戶端啟動執行重試機制,并每隔 1 分鐘重發一次重連請求。這對服務器而言就相當于遭受一次短暫集中的「攻擊」,甚至有可能拖垮服務器;
2)當客戶端斷網后立刻進行重試也并不合理,因為用戶恢復網絡也需要一定時間,這期間的重連請求是無意義的。
基于以上問題分析改進,我們設計了第二版重試機制。此次將 5 次以下請求錯誤的延遲時間修改為 5 - 20 秒隨機重試,將客戶端重試請求分散在多個時間點避免同時請求形成對服務器對瞬時壓力。同時在客戶端斷網情況下也進行延遲重試。
Polling 機制修改后請求量劃分,相對之前請求分布比較均勻,不再出現集中請求的問題。
4.4、唯一會話標識
4.4.1 為何引入消息線 ID
消息線就是用來表示會話的聊天關系,不同消息線代表不同對象的會話,從 DB 層面來看需要一個張表來存儲這種關系 uid + object_id + busi_type = 消息線 ID。
在 IM 初期實現中,我們使用會話配置參數(包含業務來源和會話參數)來標識會話 id,有三個作用:
1)查找商家 id,獲取咨詢來源,進行管家分配;
2)查找已存在的消息線;
3)判斷客戶端頁面狀態,決定要不要下發推送,進行消息提醒。
這種方式存在兩個問題:
1)通過業務來源和會話參數來解析對應的商家 id,兩個參數缺失一個都會導致商家 id 解析錯誤,還要各種查詢數據庫才能得到商家 id,影響效率;
2)通過會話類型切換接口標識當前會話類型,切換頁面會頻繁觸發網絡請求;如果請求接口發生意外容易引發消息內容錯誤問題,嚴重依賴客戶端的健壯性。
用業務來源和會話參數幫助我們進行管家分配是不可避免的,但我們可以通過引入消息線 ID 來綁定消息線的方式,替代業務來源和會話參數查找消息線的作用。另外針對下發推送的問題已通過上方講述的本地推送通知機制解決。
4.4.2 何時創建消息線
1)當進入會話頁發消息時,檢查 DB 中是否存在對應消息線,不存在則將這條消息 id 當作消息線 id 使用,存在即復用;
2)當進入會話時,根據用戶 id 、業務類型 id 等檢查在 DB 中是否已存在對應消息線,不存在則創建消息線,存在即復用。
4.4.3 引入消息線目的
1)減少服務端查詢消息線的成本;
2)移除舊版狀態改變相關的接口請求,間接提高了推送觸達率;
3)降低移動端對于用戶消息匹配的復雜度。
五、展望及近期優化
5.1、數據通道實現方式升級為 Websocket
WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協議。WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,并進行雙向數據傳輸。
與目前的 HTTP 輪詢實現機制相比, Websocket 有以下優點:
1)較少的控制開銷:在連接創建后,服務器和客戶端之間交換數據時,用于協議控制的數據包頭部相對較小。在不包含擴展的情況下,對于服務器到客戶端的內容,此頭部大小只有 2 至 10 字節(和數據包長度有關);對于客戶端到服務器的內容,此頭部還需要加上額外的 4 字節的掩碼。相對于 HTTP 請求每次都要攜帶完整的頭部,開銷顯著減少;
2)更強的實時性:由于協議是全雙工的,服務器可以隨時主動給客戶端下發數據。相對于 HTTP 需要等待客戶端發起請求服務端才能響應,延遲明顯更少;即使是和 Comet 等類似的長輪詢比較,其也能在短時間內更多次地傳遞數據;
3)保持連接狀態:與 HTTP 不同的是,Websocket 需要先創建連接,這就使其成為一種有狀態的協議,在之后通信時可以省略部分狀態信息。而 HTTP 請求可能需要在每個請求都攜帶狀態信息(如身份認證等);
4)更好的二進制支持:Websocket 定義了二進制幀,相對 HTTP,可以更輕松地處理二進制內容;
5)支持擴展:Websocket 定義了擴展,用戶可以擴展協議、實現部分自定義的子協議,如部分瀏覽器支持壓縮等;
6)更好的壓縮效果:相對于 HTTP 壓縮,Websocket 在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著地提高壓縮率。
為了進一步優化我們的數據通道設計,我們探索驗證了 Websocket 的可行性,并進行了調研和設計:
近期將對 HTTP 輪詢實現方案進行替換,進一步優化數據通道的效率。
5.2、業務功能的擴展
計劃將 IM 移動端功能模塊打造成通用的即時通訊組件,能夠更容易地賦予各業務 IM 能力,使各業務快速在自有產品線上添加聊天功能,降低研發 IM 的成本和難度。目前的 IM 功能實現主要有兩個組成,分別是公用的數據通道與 UI 組件。
隨著馬蜂窩業務發展,在現有 IM 系統上還有很多可以建設和升級的方向。比如消息類型的支撐上,擴展對短視頻、語音消息、快捷消息回復等支撐,提高社交的便捷性和趣味性;對于多人場景希望增加群組,興趣頻道,多人音視頻通信等場景的支撐等。
相信未來通過對更多業務功能的擴展及應用場景的探索,馬蜂窩移動端 IM 將更好地提升用戶體驗,并持續為商家賦能。
附錄:更多IM架構設計方面的文章
淺談IM系統的架構設計》
《簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端》
《一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》
《一套原創分布式即時通訊(IM)系統理論架構方案》
《從零到卓越:京東客服即時通訊系統的技術架構演進歷程》
《蘑菇街即時通訊/IM服務器開發之架構選擇》
《騰訊QQ1.4億在線用戶的技術挑戰和架構演進之路PPT》
《微信后臺基于時間序的海量數據冷熱分級架構設計實踐》
《微信技術總監談架構:微信之道——大道至簡(演講全文)》
《如何解讀《微信技術總監談架構:微信之道——大道至簡》》
《快速裂變:見證微信強大后臺架構從0到1的演進歷程(一)》
《17年的實踐:騰訊海量產品的技術方法論》
《移動端IM中大規模群消息的推送如何保證效率、實時性?》
《現代IM系統中聊天消息的同步和存儲方案探討》
《IM開發基礎知識補課(二):如何設計大量圖片文件的服務端存儲架構?》
《IM開發基礎知識補課(三):快速理解服務端數據庫讀寫分離原理及實踐建議》
《IM開發基礎知識補課(四):正確理解HTTP短連接中的Cookie、Session和Token》
《WhatsApp技術實踐分享:32人工程團隊創造的技術神話》
《微信朋友圈千億訪問量背后的技術挑戰和實踐總結》
《王者榮耀2億用戶量的背后:產品定位、技術架構、網絡方案等》
《IM系統的MQ消息中間件選型:Kafka還是RabbitMQ?》
《騰訊資深架構師干貨總結:一文讀懂大型分布式系統設計的方方面面》
《以微博類應用場景為例,總結海量社交系統的架構設計步驟》
《快速理解高性能HTTP服務端的負載均衡技術原理》
《子彈短信光鮮的背后:網易云信首席架構師分享億級IM平臺的技術實踐》
《知乎技術分享:從單機到2000萬QPS并發的Redis高性能緩存實踐之路》
《IM開發基礎知識補課(五):通俗易懂,正確理解并用好MQ消息隊列》
《微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)》
《微信技術分享:微信的海量IM聊天消息序列號生成實踐(容災方案篇)》
《新手入門:零基礎理解大型分布式架構的演進歷史、技術原理、最佳實踐》
《一套高可用、易伸縮、高并發的IM群聊、單聊架構方案設計實踐》
《阿里技術分享:深度揭秘阿里數據庫技術方案的10年變遷史》
《阿里技術分享:阿里自研金融級數據庫OceanBase的艱辛成長之路》
《社交軟件紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等》
《社交軟件紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進》
《社交軟件紅包技術解密(三):微信搖一搖紅包雨背后的技術細節》
《社交軟件紅包技術解密(四):微信紅包系統是如何應對高并發的》
《社交軟件紅包技術解密(五):微信紅包系統是如何實現高可用性的》
《社交軟件紅包技術解密(六):微信紅包系統的存儲層架構演進實踐》
《社交軟件紅包技術解密(七):支付寶紅包的海量高并發技術實踐》
《社交軟件紅包技術解密(八):全面解密微博紅包技術方案》
《社交軟件紅包技術解密(九):談談手Q紅包的功能邏輯、容災、運維、架構等》
《即時通訊新手入門:一文讀懂什么是Nginx?它能否實現IM的負載均衡?》
《即時通訊新手入門:快速理解RPC技術——基本概念、原理和用途》
《多維度對比5款主流分布式MQ消息隊列,媽媽再也不擔心我的技術選型了》
《從游擊隊到正規軍(一):馬蜂窩旅游網的IM系統架構演進之路》
《從游擊隊到正規軍(二):馬蜂窩旅游網的IM客戶端架構演進和實踐總結》
《IM開發基礎知識補課(六):數據庫用NoSQL還是SQL?讀這篇就夠了!》
>> 更多同類文章 ……
(本文同步發布于:http://www.52im.net/thread-2796-1-1.html)