馬蜂窩 IM 系統架構的演化和升級

今天,愈來愈多的用戶被馬蜂窩持續積累的筆記、攻略、嗡嗡等優質的分享內容所吸引,在這裏激發了去旅行的熱情,同時也拉動了馬蜂窩交易的增加。在幫助用戶作出旅行決策、完成交易的過程當中,IM 系統起到了重要的做用。php

IM 系統爲用戶與商家創建了直接溝通的渠道,幫助用戶解答購買旅行產品中的問題,既促成了訂單交易,也幫用戶打消了疑慮,促成用戶旅行願望的實現。伴隨着業務的快速發展,幾年間,馬蜂窩 IM 系統也經歷了幾回比較重要的架構演化和轉型。瀏覽器

 

IM 1.0 —— 初期階段

初期爲了支持業務快速上線,且當時版本流量較低,對併發要求不高,IM 系統的技術架構主要以簡單和可用爲目的,實現的功能也很基礎。服務器

IM 1.0 使用 PHP 開發,實現了 IM 基本的用戶/客服接入、消息收發、諮詢列表管理功能。用戶諮詢時,會經過平均分配的策略分配給客服,記錄用戶和客服的關聯關係。用戶/客服發送消息時,經過調用消息轉發模塊,將消息投遞到對方的 Redis 阻塞隊列裏。收消息則經過 HTTP 長鏈接調用消息輪詢模塊,有消息時即刻返回,沒有消息則阻塞一段時間返回,這裏阻塞的目的是下降輪詢的間隔。消息收發模型以下圖所示:網絡

消息輪詢模塊優化

上圖模型中消息輪詢模塊的長鏈接請求是經過 php-fpm 掛載在阻塞隊列上,當該請求變多時,若是不能及時釋放 php-fpm 進程,會對服務器性能消耗較大,負載很高。架構

爲了解決這個問題,咱們對消息輪詢模塊進行了優化,選用基於 OpenResty 框架,利用 Lua 協程的方式來優化 php-fmp 長時間掛載的問題。Lua 協程會經過對 Nginx 轉發的請求標記判斷是否攔截網絡請求,若是攔截,則會將阻塞操做交給 Lua 協程來處理,及時釋放 php-fmp,緩解對服務器性能的消耗。優化的處理流程見下圖:併發

 

IM 2.0 —— 需求定製階段

伴隨着業務的快速增加,IM 系統在短時間內面臨着大量定製需求的增長,開發了許多新的業務模塊。面對大量的用戶諮詢,客服的服務能力已經招架不住。所以,IM 2.0 將重心放在提高業務功能體驗上,好比在處理用戶的諮詢時,將從前單一的分配方式演變爲採用平均、權重、排隊等多種方式;爲了提高客服的效率,客服的諮詢回覆也增長了可選配置,例如自動回覆、FAQ 等。框架

以一個典型的用戶諮詢場景爲例,當用戶打開 App 或者網頁時,會經過鏈接層創建長鏈接,以後在諮詢入口發起諮詢時,會攜帶着消息線索初始化消息鏈路,創建一條可複用、可檢索的消息線;發送消息時,經過消息服務將消息存儲到 DB 中,同時會根據消息線檢索當前諮詢是否被分配到客服,調用分配服務的目的是爲當前諮詢完善客服信息;最後將客服信息更新到鏈路關係中。php-fpm

這樣,一條完整的消息鏈路就創建完畢,以後用戶/客服發出的消息經過轉發服務傳輸給對方,處理流程以下圖所示:性能

 

IM 3.0 —— 服務拆分階段

業務量在不斷積累,隨着模塊增長,IM 系統的代碼膨脹得很快。因爲代碼規範沒有統1、接口職責不夠單1、模塊間耦合較多等種緣由,改動一個需求極可能會影響到其它模塊,使新需求的開發和維護成本都很高。優化

爲了解決這種局面,IM 系統必需要進行架構升級,首要任務就是服務的拆分。目前,通過拆分後的 IM 系統總體分爲 4 塊大的服務,包括客服服務、用戶服務、IM 服務、數據服務,以下圖所示:

  • 客服服務:圍繞提高客服效率和用戶體驗提供多種方式,如提供羣組管理、成員管理、質檢服務等來提高客服團隊的運營和管理水平;經過分配服務、轉接服務來使用戶的接待效率更靈活高效;支持自動回覆、FAQ、知識庫服務等來提高客服諮詢的回覆效率等。

  • 用戶服務:分析用戶行爲,爲用戶作興趣推薦及用戶畫像,以及統計用戶對馬蜂窩商家客服的滿意度。

  • IM 服務:支持單聊和羣聊模式,提供實時消息通知、離線消息推送、歷史消息漫遊、聯繫人列表、文件上傳與存儲、消息內容風控檢測等。

  • 數據服務:經過採集用戶諮詢的來源入口、是否諮詢下單、是否有客服接待、用戶諮詢以及客服回覆的時間信息等,定義數據指標,經過數據分析進行離線數據運算,最終對外提供數據統計信息。主要的指標信息有 30 秒、1 分鐘回覆率、諮詢人數、無應答次數、平均應答時間、諮詢銷售額、諮詢轉化率、推薦轉化率、分時接待壓力、值班狀況、服務評分等。

用戶狀態流轉

現有的 IM 系統 中,用戶諮詢時一個完整的用戶狀態流轉以下圖所示:

用戶點擊諮詢按鈕觸發事件,此時用戶狀態進入初始態。發送消息時,系統更改用戶狀態爲待分配,經過調用分配服務分配了對應的客服後,用戶狀態更改成已分配、未解決。當客服解決了用戶或者客服回覆後用戶長時間未說話,觸發系統自動解決的操做,此時用戶狀態更改成已解決,一個諮詢流程結束。

IM 服務的重構

在服務拆分的過程當中,咱們須要考慮特定服務的通用性、可用性和降級策略,同時須要儘量地下降服務間的依賴,避免因爲單一服務不可用致使總體服務癱瘓的風險。在這期間,公司其它業務線對 IM 服務的使用需求也愈來愈多,使用頻次和量級也開始加大。初期階段的 IM 服務當鏈接量大時,只能經過修改代碼實現水平擴容;新業務接入時,還須要在業務服務器上配置 Openresty 環境及 Lua 協程代碼,業務接入很是不便,IM 服務的通用性也不好。

考慮到以上問題,咱們對 IM 服務進行了全面重構,目標是將 IM 服務抽取成獨立的模塊,不依賴其它業務,對外提供統一的集成和調用方式。考慮到 IM 服務對併發處理高和損耗低的要求,選擇了 Go 語言來開發此模塊,新的 IM 服務設計以下圖:

其中,比較重要的 Proxy 層和 Exchange 層提供瞭如下服務:

1. 路由規則,例如 ip-hash、輪詢、最小鏈接數等,經過規則將客戶端散列到不一樣的 ChannelManager 實例上。

2. 對客戶端接入的管理,接入後的鏈接信息會同步到 DispatchTable 模塊,方便 Dispatcher 進行檢索。

3.ChannelManager 與客戶端間的通訊協議,包括客戶端請求創建鏈接、斷線重連、主動斷開、心跳、通知、收發消息、消息的 QoS 等。

4. 對外提供單發、羣發消息的 REST 接口。這裏須要根據場景來決定是否使用,例如用戶諮詢客服的場景就須要經過這個接口下發消息,主要緣由在如下 3 點:

  • 發消息時會有建立消息線、分配管家等邏輯,這些邏輯目前是 PHP 實現,IM 服務須要知道 PHP 的執行結果,一種方式是使用 Go 從新實現,另一種方式是經過 REST 接口調用 PHP 返回,這樣會帶來 IM 服務和 PHP 業務過多的網絡交互,影響性能。

  • 轉發消息時,ChannelManager 多個實例間須要互相通訊,例如 ChannelManager1 上的用戶 A 給 ChannelManager2 上的客服 B 發消息,若是實例間無通訊機制,消息沒法轉發。當要再擴展 ChannelManager 實例時,新增實例須要和其它已存在實例分別創建通訊,增長了系統擴展的複雜度。

  • 若是客戶端不支持 WebSocket 協議,做爲降級方案的 HTTP 長鏈接輪循只能用來收消息,發消息須要經過短鏈接來處理。其它場景不須要消息轉發,只用來給 ChannelManager 傳輸消息的場景,可經過 WebSocket 直接發送。

改造後的 IM 服務調用流程

初始化消息線及分配客服過程由 PHP 業務完成。須要消息轉發時,PHP 業務調用 Dispatcher 服務的發消息接口,Dispatcher 服務經過共享的 Dispatcher Table 數據,檢索出接收者所在的 ChannelManager 實例,將消息經過 RPC 的方式發送到實例上,ChannelManager 經過 WebSocket 將消息推送給客戶端。IM 服務調用流程以下圖所示:

當鏈接數超過當前 ChannelManager 集羣承載的上限時,只需擴展 ChannelManager 實例,由 ETCD 動態的通知到監聽側,從而作到平滑擴容。目前瀏覽器版本的 JS-SDK 已經開發完畢,其它業務線經過接入文檔,就能方便地集成 IM 服務。

在 Exchange 層的設計中,有 3 個問題須要考慮:

1. 多端消息同步

如今客戶端有 PC 瀏覽器、Windows 客戶端、H五、iOS/Android,若是一個用戶登陸了多端,當有消息過來時,須要查找出這個用戶的全部鏈接,當用戶的某個端斷線後,須要定位到這一個鏈接。

上面提到過,鏈接信息都是存儲在 DispatcherTable 模塊中,所以 DispatcherTable 模塊要能根據用戶信息快速檢索出鏈接信息。DispatcherTable 模塊的設計用到了 Redis 的 Hash 存儲,當客戶端與 ChannelManager 創建鏈接後,須要同步的元數據有 uid(用戶信息)、uniquefield(惟一值,一個鏈接對應的惟一值)、wsid(鏈接標示符)、clientip(客戶端 ip)、serverip(服務端 ip)、channel(渠道),對應的結構大體以下:

這樣經過 key(uid) 能找到一個用戶多個端的鏈接,經過 key+field 能定位到一條鏈接。鏈接信息的默認過時時間爲 2 小時,目的是避免因客戶端鏈接異常中斷致使服務端沒有捕獲到,從而在 DispatcherTable 中存儲了一些過時數據。

2. 用戶在線狀態同步

好比一個用戶前後和 4 個客服諮詢過,那麼這個用戶會出如今 4 個客服的諮詢列表裏。當用戶上線時,要保證 4 個客服看到用戶都是在線狀態。

要作到這一點有兩種方案,一種是客服經過輪詢獲取用戶的狀態,但這樣當用戶在線狀態沒有變化時,會發起不少無效的請求;另一種是用戶上線時,給客服推送上線通知,這樣會形成消息擴散,每個諮詢過的客服都須要擴散通知。咱們最終採起的是第二種方式,在推送的過程當中,只給在線的客服推送用戶狀態。

3. 消息的不丟失,不重複

爲了不消息丟失,對於採用長鏈接輪詢方式的咱們會在發起請求時,帶上客戶端已讀消息的 ID,由服務端計算出差值消息而後返回;使用 WebSocket 方式的,服務端會在推送給客戶端消息後,等待客戶端的 ACK,若是客戶端沒有 ACK,服務端會嘗試屢次推送。

這時就須要客戶端根據消息 ID 作消息重複的處理,避免客戶端可能已收到消息,可是因爲其它緣由致使 ACK 確認失敗,觸發重試,致使消息重複。

IM 服務的消息流

上文提到過 IM 服務須要支持多終端,同時在角色上又分爲用戶端和商家端,爲了能讓通知、消息在輸出時根據域名、終端、角色動態輸出差別化的內容,引入了 DDD (領域驅動設計)的建模方法來對消息進行處理,處理過程以下圖所示:


 

總結和展望

伴隨着馬蜂窩「內容+交易」模式的不斷深化,IM 系統架構也經歷着演化和升級的不一樣階段,從初期粗曠無序的模式走向統一管理,逐漸規範、造成規模。 

咱們取得了一些進步,固然,還有更長的路要走。將來,結合公司業務的發展腳步和團隊的技術能力,咱們將不斷進行 IM 系統的優化。目前咱們正在計劃將消息輪詢模塊中的服務端代碼用 Go 替換,使其再也不依賴 PHP 及 OpenResty 環境,實現更好地解耦;另外,咱們將基於 TensorFlow 實現向智慧客服的探索,經過訓練數據模型、分析數據,進一步提高人工客服的解決效率,提高用戶體驗,更好地爲業務賦能。

本文做者:馬蜂窩電商平臺 IM 研發團隊。

(馬蜂窩技術原創內容,轉載務必註明出處保存文末二維碼圖片,謝謝配合。)

相關文章
相關標籤/搜索