Golang 在電商即時通訊服務建設中的實踐

来源:https://www.cnblogs.com/mfwtech/archive/2019/12/16/12048365.html
-Advertisement-
Play Games

馬蜂窩技術原創文章,更多乾貨請搜索公眾號:mfwtech ​即時通訊(IM)功能對於電商平臺來說非常重要,特別是旅游電商。 從商品複雜性來看,一個旅游商品可能會包括用戶在未來一段時間的衣、食、住、行等方方面面;從消費金額來看,往往單次消費額度較大;對目的地的陌生、在行程中可能的問題,這些因素使用戶在 ...


馬蜂窩技術原創文章,更多乾貨請搜索公眾號:mfwtech

​即時通訊(IM)功能對於電商平臺來說非常重要,特別是旅游電商。

從商品複雜性來看,一個旅游商品可能會包括用戶在未來一段時間的衣、食、住、行等方方面面;從消費金額來看,往往單次消費額度較大;對目的地的陌生、在行程中可能的問題,這些因素使用戶在購買前、中、後都存在和商家溝通的強烈需求。可以說,一個好用的 IM 可以在一定程度上對企業電商業務的 GMV 起到促進作用。

本文我們將結合馬蜂窩旅游電商 IM 服務的發展歷程,重點介紹基於 Go 的 IM 重構,希望可以給有相似問題的朋友一些借鑒。

 

Part.1 技術背景和問題

與廣義上的即時通訊不同,電商各業務線有其特有業務邏輯,如客服聊天系統的客人分配邏輯、敏感詞檢測邏輯等,這些往往要耦合進通信流程中。隨著接入業務線越來越多,即時通訊服務冗餘度會越來越高。同時整個消息鏈路追溯複雜,服務穩定性很受業務邏輯的影響。

之前我們 IM 應用中的消息推送主要基於輪詢技術,消息輪詢模塊的長連接請求是通過 php-fpm 掛載在阻塞隊列上實現。當請求量較大時,如果不能及時釋放 php-fpm 進程,對伺服器的性能消耗很大。

為瞭解決這個問題,我們曾用 OpenResty+Lua 的方式進行改造,利用 Lua 協程的方式將整體的 polling 的能力從 PHP 轉交到 Lua 處理,釋放一部 PHP 的壓力。這種方式雖然能提升一部分性能,但 PHP-Lua 的混合異構模式,使系統在使用、升級、調試和維護上都很麻煩,通用性也較差,很多業務場景下還是要依賴 PHP 介面,優化效果並不明顯。

為瞭解決以上問題,我們決定結合電商 IM 的特定背景對 IM 服務進行重構,核心是實現業務邏輯和即時通訊服務的分離。

 

Part.2 基於Go的雙層分散式IM架構

2.1 實現目標

1. 業務解耦

將業務邏輯與通信流程剝離,使 IM 服務架構更加清晰,實現與電商 IM 業務邏輯的完全分離,保證服務穩定性。

2. 接入方式靈活

之前新業務接入時,需要在業務伺服器上配置 OpenResty 環境及 Lua 協程代碼,非常不便,IM 服務的通用性也很差。考慮到現有業務的實際情況,我們希望 IM 系統可以提供 HTTP 和 WebSocket 兩種接入方式,供業務方根據不同的場景來靈活使用。

比如已經接入且運行良好的電商定製化團隊的待辦系統、定製游搶單系統、投訴系統等下行相關的系統等,這些業務沒有明顯的高併發需求,可以通過 HTTP 方式迅速接入,不需要熟悉稍顯複雜的 WebSocket 協議,進而降低不必要的研發成本。

3. 架可擴展

為了應對業務的持續增長給系統性能帶來的挑戰,我們考慮用分散式架構來設計即時通訊服務,使系統具有持續擴展及提升的能力。

2.2 語言選擇

目前,馬蜂窩技術體系主要包括 PHP,Java,Golang,技術棧比較豐富,使業務做選型時可以根據問題場景選擇更合適的工具和語言。

結合 IM 具體應用場景,我們選擇 Go 的原因包括:

1. 性能

在性能上,尤其是針對網路通信等 IO 密集型應用場景。Go 系統的性能更接近 C/C++。

2. 開發效率

Go 使用起來簡單,代碼編寫效率高,上手也很快,尤其是對於有一定 C++ 基礎的開發者,一周就能上手寫代碼了。

2.3 架構設計

整體架構圖如下:

 

名詞解釋:

  • 客戶:一般指購買商品的用戶

  • 商家:提供服務的供應商,商家會有客服人員,提供給客戶一個線上咨詢的作用

  • 分發模塊:即 Dispatcher,提供消息分發的給指定的工作模塊的橋接作用

  • 工作模塊:即 Worker 伺服器,用來提供 WebSocket 服務,是真正工作的一個模塊。

架構分層:

  • 展示層:提供 HTTP 和 WebSocket 兩種接入方式。

  • 業務層:負責初始化消息線和業務邏輯處理。如果客戶端以 HTTP 方式接入,會以 JSON 格式把消息發送給業務伺服器進行消息解碼、客服分配、敏感詞過濾,然後下發到消息分發模塊準備下一步的轉換;通過 WebSocket 接入的業務則不需要消息分發,直接以 WebSocket 方式發送至消息處理模塊中。

  • 服務層:由消息分發和消息處理這兩層組成,分別以分散式的方式部署多個 Dispatcher 和 Worker 節點。Dispatcher 負責檢索出接收者所在的伺服器位置,將消息以 RPC 的方式發送到合適的 Worker 上,再由消息處理模塊通過 WebSocket 把消息推送給客戶端。

  • 數據層:Redis 集群,記錄用戶身份、連接信息、客戶端平臺(移動端、網頁端、桌面端)等組成的唯一 Key。

2.4 服務流程

步驟一

如上圖右側所示,用戶客戶端與消息處理模塊建立 WebSocket 長連接。通過負載均衡演算法,使客戶端連接到合適的伺服器(消息處理模塊的某個 Worker)。連接成功後,記錄用戶連接信息,包括用戶角色(客人或商家)、客戶端平臺(移動端、網頁端、桌面端)等組成唯一 Key,記錄到 Redis 集群。

步驟二

如圖左側所示,當購買商品的用戶要給管家發消息的時候,先通過 HTTP 請求把消息發給業務伺服器,業務服務端對消息進行業務邏輯處理。

(1) 該步驟本身是一個 HTTP 請求,所以可以接入各種不同開發語言的客戶端。通過 JSON 格式把消息發送給業務伺服器,業務伺服器先把消息解碼,然後拿到這個用戶要發送給哪個商家的客服的。

(2) 如果這個購買者之前沒有聊過天,則在業務伺服器邏輯里需要有一個分配客服的過程,即建立購買者和商家的客服之間的連接關係。拿到這個客服的 ID,用來做業務消息下發;如果之前已經聊過天,則略過此環節。

(3) 在業務伺服器,消息會非同步入資料庫。保證消息不會丟失。

步驟三

業務服務端以 HTTP 請求把消息發送到消息分發模塊。這裡分發模塊的作用是進行中轉,最終使服務端的消息下發給指定的商家。

步驟四

基於 Redis 集群中的用戶連接信息,消息分發模塊將消息轉發到目標用戶連接的 WebSocket 伺服器(消息處理模塊中的某一個 Worker)

(1) 分發模塊通過 RPC 方式把消息轉發到目標用戶連接的 Worker,RPC 的方式性能更快,而且傳輸的數據也少,從而節約了伺服器的成本。

(2) 消息透傳 Worker 的時候,多種策略保障消息一定會下發到 Worker。

步驟五

消息處理模塊將消息通過 WebSocket 協議推送到客戶端:

(1) 在投遞的時候,接收者要有一個 ACK(應答) 信息來回饋給 Worker 伺服器,告訴 Worker 伺服器,下發的消息接收者已經收到了。

(2) 如果接收者沒有發送這個 ACK 來告訴 Worker 伺服器,Worker 伺服器會在一定的時間內來重新把這個信息發送給消息接收者。

(3) 如果投遞的信息已經發送給客戶端,客戶端也收到了,但是因為網路抖動,沒有把 ACK 信息發送給伺服器,那伺服器會重覆投遞給客戶端,這時候客戶端就通過投遞過來的消息 ID 來去重展示。

以上步驟的數據流轉大致如圖所:

2.5 系統完整性設計

2.5.1 可靠性

(1)消息不丟失

為了避免消息丟失,我們設置了超時重傳機制。服務端會在推送給客戶端消息後,等待客戶端的 ACK,如果客戶端沒有返回 ACK,服務端會嘗試多次推送。

目前預設 18s 為超時時間,重傳 3 次不成功,斷開連接,重新連接伺服器。重新連接後,採用拉取歷史消息的機制來保證消息完整。

(2)多端消息同步

客戶端現有 PC 瀏覽器、Windows 客戶端、H5、iOS/Android,系統允許用戶多端同時線上,且同一端可以多個狀態,這就需要保證多端、多用戶、多狀態的消息是同步的。

我們用到了 Redis 的 Hash 存儲,將用戶信息、唯一連接對應值 、連接標識、客戶端 IP、伺服器標識、角色、渠道等記錄下來,這樣通過 key(uid) 就能找到一個用戶在多個端的連接,通過 key+field 能定位到一條連接。

2.5.2 可用性

上文我們已經說過,因為是雙層設計,就涉及到兩個 Server 間的通信,同進程內通信用 Channel,非同進程用消息隊列或者 RPC。綜合性能和對伺服器資源利用,我們最終選擇 RPC 的方式進行 Server 間通信。在對基於 Go 的 RPC 進行選行時,我們比較了以下比較主流的技術方案: 

  • Go STDRPC:Go 標準庫的 RPC,性能最優,但是沒有治理

  • RPCX:性能優勢 2*GRPC + 服務治理

  • GRPC:跨語言,但性能沒有 RPCX 好

  • TarsGo:跨語言,性能 5*GRPC,缺點是框架較大,整合起來費勁

  • Dubbo-Go:性能稍遜一籌, 比較適合 Go 和 Java 間通信場景使用

最後我們選擇了 RPCX,因為性能也很好,也有服務的治理。

兩個進程之間同樣需要通信,這裡用到的是 ETCD 實現服務註冊發現機制。

當我們新增一個 Worker,如果沒有註冊中心,就要用到配置文件來管理這些配置信息,這挺麻煩的。而且你新增一個後,需要分發模塊立刻發現,不能有延遲。

如果有新的服務,分發模塊希望能快速感知到新的服務。利用 Key 的續租機制,如果在一定時間內,沒有監聽到 Key 有續租動作,則認為這個服務已經掛掉,就會把該服務摘除。

在進行註冊中心的選型時,我們主要調研了 ETCD,ZK,Consul,三者的壓測結果參考如下:

 

 

結果顯示,ETCD 的性能是最好的。另外,ETCD 背靠阿裡巴巴,而且屬於 Go 生態,我們公司內部的 K8S 集群也在使用。

綜合考量後,我們選擇使用 ETCD 作為服務註冊和發現組件。並且我們使用的是 ETCD 的集群模式,如果一臺伺服器出現故障,集群其他的伺服器仍能正常提供服務。

通過保證服務和進程間的正常通訊,及 ETCD 集群模式的設計,保證了 IM 服務整體具有極高的可用性。

2.5.3 擴展性

消息分發模塊和消息處理模塊都能進行水平擴展。當整體服務負載高時,可以通過增加節點來分擔壓力,保證消息即時性和服務穩定性。

2.5.4 安全性

處於安全性考慮,我們設置了黑名單機制,可以對單一 uid 或者 ip 進行限制。比如在同一個 uid 下,如果一段時間內建立的連接次數超過設定的閾值,則認為這個 uid 可能存在風險,暫停服務。如果暫停服務期間該 uid 繼續發送請求,則限制服務的時間相應延長。

2.6 性能優化和踩過的坑

2.6.1 性能優化

(1) JSON 編解碼

開始我們使用官方的 JSON 編解碼工具,但由於對性能方面的追求,改為使用滴滴開源的 Json-iterator,使在相容原生 Golang 的 JSON 編解碼工具的同時,效率上有比較明顯的提升。以下是壓測對比的參考圖:

(2) time.After  

在壓測的時候,我們發現記憶體占用很高,於是使用 Go Tool PProf 分析 Golang 函數記憶體申請情況,發現有不斷創建 time.After 定時器的問題,定位到是心跳協程裡面。

原來代碼如下:

優化後的代碼為:

 

優化點在於 for 迴圈里不要使用 select + time.After 的組合。

(3) Map 的使用

在保存連接信息的時候會用到 Map。因為之前做 TCP Socket 的項目的時候就遇到過一個坑,即 Map 在協程下是不安全的。當多個協程同時對一個 Map 進行讀寫時,會拋出致命錯誤:fetal error:concurrent map read and map write,有了這個經驗後,我們這裡用的是 sync.Map

2.6.2 踩坑經驗

(1) 協程異常

基於對開發成本和服務穩定性等問題的考慮,我們的 WebSocket 服務基於 Gorilla/WebSocket 框架開發。其中遇到一個問題,就是當讀協程發生異常退出時,寫協程並沒有感知到,結果就是導致讀協程已經退出但是寫協程還在運行,直到觸發異常之後才退出。這樣雖然從錶面上看不影響業務邏輯,但是浪費後端資源。在編碼時應該註意要在讀協程退出後主動通知寫協程,這樣一個小的優化可以這在高併發下能節省很多資源。

(2) 心跳設計

舉個例子,之前我們在閑時心跳功能的開發中走了一些彎路。最初在伺服器端的心跳發送是定時心跳,但後來在實際業務場景中使用時發現,設計成伺服器讀空閑時心跳更好。因為用戶都在聊天呢,發一個心跳幀,浪費感情也浪費帶寬資源。

這時候,建議大家在業務開發過程中如果代碼寫不下去就暫時不要寫了,先結合業務需求用文字梳理下邏輯,可能會發現之後再進行會更順利。

(3) 每天分割日誌

日誌模塊在起初調研的時候基於性能考慮,確定使用 Uber 開源的 ZAP 庫,而且滿足業務日誌記錄的要求。日誌庫選型很重要,選不好也是影響系統性能和穩定性的。ZAP 的優點包括:

  • 顯示代碼行號這個需求,ZAP 支持而 Logrus 不支持,這個屬於提效的。行號展示對於定位問題很重要。

  • ZAP 相對於 Logrus 更為高效,體現在寫 JSON 格式日誌時,沒有使用反射,而是用內建的 json encoder,通過明確的類型調用,直接拼接字元串,最小化性能開銷。

小坑:

每天寫一個日誌文件的功能,目前 ZAP 不支持,需要自己寫代碼支持,或者請求系統部支持。

 

Part.3 性能表現

壓測 1:

上線生產環境並和業務方對接以及壓測,目前定製業務已接通整個流程,寫了一個 Client。模擬定期發心跳幀,然後利用 Docker 環境。開啟了 50 個容器,每個容器模擬併發起 2 萬個連接。這樣就是百萬連接打到單機的 Server 上。單機記憶體占用 30G 左右。

壓測 2:

同時併發 3000、4000、5000 連接,以及調整發送頻率,分別對應上行:60萬、80 萬、100 萬、200 萬, 一個 6k 左右的日誌結構體。

其中有一半是心跳包 另一半是日誌結構體。在不同的壓力下的下行延遲數據如下:

結論:隨著上行的併發變大,延遲控制在 24-66 毫秒之間。所以對於下行業務屬於輕微延遲。另外針對 60 萬 5k 上行的同時,用另一個腳本模擬開啟 50 個協程併發下行 1k 的數據體,延遲是比沒有併發下行的時候是有所提高的,延遲提高了 40ms 左右。

 

Part.4 總結

基於 Go 重構的 IM 服務在 WebSocket 的基礎上,將業務層設計為配有消息分發模塊和消息處理模塊的雙層架構模式,使業務邏輯的處理前置,保證了即時通訊服務的純粹性和穩定性;同時消息分發模塊的 HTTP 服務方便多種編程語言快速對接,使各業務線能迅速接入即時通訊服務。

最後,我還想為 Go 搖旗吶喊一下。很多人都知道馬蜂窩技術體系主要是基於 PHP,有一些核心業務也在向 Java 遷移。與此同時,Go 也在越來越多的項目中發揮作用。現在,雲原生理念已經逐漸成為主流趨勢之一,我們可以看到在很多構建雲原生應用所需要的核心項目中,Go 都是主要的開發語言,比如 Kubernetes,Docker,Istio,ETCD,Prometheus 等,包括第三代開源分散式資料庫 TiDB。

所以我們可以把 Go 稱為雲原生時代的母語。「雲原生時代,是開發者最好的時代」,在這股浪潮下,我們越早走進 Go,就可能越早在這個新時代搶占關鍵賽道。希望更多小伙伴和我們一起,加入到 Go 的開發和學習陣營中來,拓寬自己的技能圖譜,擁抱雲原生。

本文作者:Anti Walker,馬蜂窩旅游網電商交易基礎平臺研發工程師。


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

-Advertisement-
Play Games
更多相關文章
  • JQuery輪播圖 ...
  • 1、vue初時 vue安裝三種方式: 1:CDN引入 以下推薦國外比較穩定的兩個 CDN,國內還沒發現哪一家比較好,目前還是建議下載到本地。 Staticfile CDN(國內) : https://cdn.staticfile.org/vue/2.2.2/vue.min.js unpkg:http ...
  • 模擬實現 Promise(小白版) 本篇來講講如何模擬實現一個 Promise 的基本功能,網上這類文章已經很多,本篇筆墨會比較多,因為想用自己的理解,用白話文來講講 Promise 的基本規範,參考了這篇: "【翻譯】Promises/A+規範" 但說實話,太多的專業術語,以及基本按照標準規範格式 ...
  • JSP: <%@ page contentType="text/html;charset=UTF-8" language="java" %><%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><html><head> <lin ...
  • 導讀 在spring中委派模式用的比較多,在常用的23種設計模式中其實是沒有委派模式的影子的。 在spring中體現:Spring MVC框架中的DispatcherServlet其實就用到了委派模式。 委派模式的作用:基本作用就是負責任務的調用和分配,跟代理模式很像,可以看做是一種特殊情況下的靜態 ...
  • 好的設計不是免費的。它必須是你不斷投資的東西,這樣小問題就不會積累成大問題。 幸運的是,好的設計最終會收回成本,而且比你想象的要快。 ...
  • 2020年必讀書籍推薦:軟體設計的哲學(A Philosophy of Software Design),本書190多頁,豆瓣的點評分在9分以上,目前只有英文版本,中文版還未上市,英文好的同學建議去直接閱讀原版。 ...
  • springboot開發微服務框架一般使用springcloud全家桶,而整個項目都是容器化的,通過k8s進行編排,而k8s自己也有服務發現機制,所以我們也可以拋棄springcloud里的eureka,而直接使用k8s自己的服務。 添加組件 註意點 1. application.name與k8s的 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...