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

来源:https://www.cnblogs.com/mfwtech/archive/2019/07/22/11224732.html
-Advertisement-
Play Games

今天,越來越多的用戶被馬蜂窩持續積累的筆記、攻略、嗡嗡等優質的分享內容所吸引,在這裡激發了去旅行的熱情,同時也拉動了馬蜂窩交易的增長。在幫助用戶做出旅行決策、完成交易的過程中,IM 系統起到了重要的作用。 IM 系統為用戶與商家建立了直接溝通的渠道,幫助用戶解答購買旅行產品中的問題,既促成了訂單交易 ...


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

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 中,同時會根據消息線檢索當前咨詢是否被分配到客服,調用分配服務的目的是為當前咨詢完善客服信息;最後將客服信息更新到鏈路關係中。

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

 

IM 3.0 —— 服務拆分階段

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

為瞭解決這種局面,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 客戶端、H5、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 研發團隊。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 講乾貨,不啰嗦,通過prototype可以獲取到JavaScript的原型對象,進而可以在對象原型上添加新的屬性和方法,當該方法與原方法名稱一樣時會覆蓋原方法既:重寫,當不一樣時既:添加 如:實現數組Array的push方法的重寫 能力有限,水平一般,錯誤之處,歡迎指正,感謝關註和評論! ...
  • 講乾貨,不啰嗦,大家在做vue開發過程中經常遇到父組件需要調用子組件方法或者子組件需要調用父組件的方法的情況,現做一下總結,希望對大家有所幫助。 父組件調用子組件方法: 1.設置子組件的ref,父組件通過this.$refs.xxx.method_name(data)調用子組件方法,data參數可選 ...
  • [canvas url]: https://www.cnblogs.com/cnyball/p/11154930.html 經過 [canvas 教程(二) 繪製直線][canvas url] 我們知道了 canvas 的直線是怎麼繪製的 而本次是給大家帶來曲線相關的繪製 繪製圓形 在 canvas ...
  • 1. var arr = [ ]; 數組:[ ] 空數組 console.log為Array(0) 功能:定義空數組用來接收的 2. var str = ’ ’ 空字元串 功能:定義空字元串用來接收的 3. arr[arr.length] = ‘在最後數組添加’ arr.push(‘添加’) 常用 ...
  • 大體思路 後端書寫REST api時,有一些api是非常敏感的,比如獲取用戶個人信息,查看所有用戶列表,修改密碼等。如果不對這些api進行保護,那麼別人就可以很容易地獲取並調用這些 api 進行操作。 所以對於一些api,在調用之前,我們在服務端必須先對操作者進行“身份認證”,這就是所謂的鑒權。 J ...
  • title: 基於SpringBoot+Redis的Session共用與單點登錄 date: 2019 07 23 02:55:52 categories: 架構 author: mrzhou tags: SpringBoot redis session 單點登錄 基於SpringBoot+Redi ...
  • 語音直播房間項目文章彙總。當時對標的競品是音遇、撕歌。2個主力後端研發,3個月,1500+ commit,996、007,紀念那段充滿福報的經歷😂。 系統設計 SD項目架構圖 SD項目:基於狀態機和MQ的語音游戲方案 設計模式系列:狀態模式 狀態機框架選型簡單比較 stateless4j踩坑經歷 ...
  • 架構雜談《六》 超時處理模式 在服務化或者微服務架構里,傳統的整體應用拆分成多個職責單一的微服務,微服務之間通過某種網路通信協議互相通信和交互,完成特定的功能,然而由於網路通信的不穩定,在設計系統時必須考慮到對網路通信的容錯,特別是對調用超時問題的處理。 一、微服務的交互模式 1、同步調用模式 在同 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...