1. 程式人生 > >Golang 在電商即時通訊服務建設中的實踐

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

馬蜂窩技術原創文章,更多幹貨請搜尋公眾號: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,馬蜂窩旅遊網電商交易基礎平臺研發工程師。

相關推薦

Golang即時通訊服務建設實踐

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

複習筆記-28-Redis高階的set結構和redis事務

  Redis高階中的set結構 Redis的Set是string型別的無序集合。集合成員是唯一的,這就意味著集合中不能出現重複的資料。Redis中集合是通過雜湊表實現的,所以新增,刪除,查詢的複雜度都是O(1)。集合中最大的成員數為232 - 1 (4294967295每個集合可

複習筆記-27-Redis高階的list結構

  Redis高階中的list結構 一個列表最多可以包含232-1個元素(4294967295,每個表超過40億個元素)     問題 Redis的list型別其實就是一個每個子元素都是string型別的雙向連結串列。可以通過push,pop操作

Java從零到企業級專案實戰-服務

第1章 課程介紹(提供4900+問題與答案庫) (提供4900+問題與答案庫,你遇到的坑,別人已經出坑了)本章詳細介紹Java服務端課程內容,專案演示課程安排,高大上的架構從一臺伺服器演變到高效能、高併發、高可用架構的過程,大型架構演進思想以及程式碼演進細節。(

中國本地化雲伺服器如何部署,「雲託管」必須注意哪些法律合規問題?

伴隨著中國網路安全法和中國電子商務法的正式頒佈,已經進入中國或欲進入中國的國際企業,都將面臨諸多法律合規問題必須處理,尤其是伺服器部署、資料儲存與安全方面。否則最高可受到高達200萬人名幣甚至吊銷營業執照的嚴重處罰。 然中國電商法律環境複雜多變,對缺乏經驗的國際企業來說,造

Openfire即時通訊服務搭建與Smack訪問

Openfire 是開源的、基於可拓展通訊和表示協議(XMPP)、採用Java程式語言開發的實時協作伺服器。 Openfire安裝和使用都非常簡單,並利用Web進行管理。單臺伺服器可支援上萬併發使用者。 首先到openfire官網上去下載Openfire的伺服器包與Smac

傢俱物流配送服務的痛點及解決方案

隨著網際網路快速增長,像土巴啊裡平臺這樣的傢俱電商如雨後春筍般的茁壯成長,在傢俱電商巨大的交易量背後,蘊含著數以萬計的訂單,能否在輕鬆迅速的點選過後,順利地收到心愛的傢俱產品,不僅僅是消費者關注的事情。傢俱電商能否在光鮮的銷售數字背後,順利走完“最後一公里”,也是行業亟待解決的問題。因為傢俱商品體積大、運輸十

融雲IM即時通訊服務端Server開發獲取token報錯簽名錯誤,請檢查

公司業務需要,開發即時通訊系統,在融雲基礎上進行開發,在server搭建的時候還是蠻逗比的,真的是各種不會,不過還好都會解決掉!今天遇到的問題就是獲取token的問題,不停的報錯:“getToken:

ActiveMQ 即時通訊服務 淺析

package com.hoo.mq.topic; import javax.jms.DeliveryMode; import javax.jms.MapMessage; import javax.jms.Session; import javax.jms.Topic; import jav

蘑菇街交易平臺服務架構及改造優化歷程

蘑菇街導購時期 業務結構 蘑菇街是做導購起家的,當時所有的業務都是基於使用者和內容這兩大核心展開。那個時候前臺業務主要做的是社交導購,後臺業務主要做的是內容管理。一句話總結就是小而美的狀態,業務相對來也不是很複雜。  當時的技術架構是典型的創業型公司技術架構

JA17-大型分布式系統應用實踐+性能優化+分布式應用架構+負載均衡+高並發設計+持久化存儲視頻教程

war height imageview clas 圖片 進步 pac 點滴 blank JA17-大型電商分布式系統應用實踐+性能優化+分布式應用架構+負載均衡+高並發設計+持久化存儲視頻教程 新年伊始,學習要趁早,點滴記錄,學習就是進步! 不要到處找了,抓緊提升自

初創公司Drop的資料湖實踐

## 歡迎關注微信公眾號:ApacheHudi ## 1. 引入 Drop是一個智慧的獎勵平臺,旨在通過獎勵會員在他們喜愛的品牌購物時獲得的Drop積分來提升會員的生活,同時幫助他們發現與他們生活方式產生共鳴的新品牌。實現這一體驗的核心是Drop致力於在整個公司內推廣以資料為基礎的文化,Drop的資料用於

訊息服務實踐

1 商品和訂單服務中使用MQ 1.1 同步 在訂單生成的時候直接扣庫存,這是最初等的方式扣庫存,這種方式比較簡單,但是也有一系列的問題: 會造成有很多訂單把產品庫存扣除而並沒有支付,這就需要有一個後臺指令碼,將一段時間內沒有支付的訂單的庫存釋放,把訂單取消

scroll()和scrollTop()方法——實現網站的電梯導航

窗口 css樣式 ram 每一個 最新 top index hid none 要想實現電商網站的電梯導航效果,首先需要了解以下知識點: jquery 事件 - scroll() 方法 對元素滾動的次數進行計數,當用戶滾動指定的元素時,會發生 scroll 事件。scroll

小型服務器平臺搭建(一)

服務器 idc機房 運行環境 阿裏雲 雲平臺 一、阿裏雲小型電商服務平臺架構介紹電商平臺初創之初,訪問量不大,但將來可能訪問量暴增,初期階段業務模式調整頻繁,對價格敏感,因此希望服務器平臺架構具有良好的功能拓展性及性能伸縮性,所有平臺軟件最好免費,且性能滿足將來發展,具有冗余高可用設計,平

暴改無人機,探秘活躍在平臺的地下黑工坊

無人機人類一直癡迷於速度。不論是汽車、火車還是飛機都在提速,仿佛速度才能顯示技術的強大。美國空軍計劃開發超音速噴氣式飛機,每小時超過6100公裏,但不載人。原因是人類無法承受這樣的高速運動,其實在1969年,阿波羅10號的三名宇航員乘坐的飛船從月球後方繞過時,他們相對地球的運動速度高達每小時39897公裏。假

十年磨一劍,水產寶與升鮮寶即將橫空出世,將正面與市面上的商業軟件競爭。用小米加步槍對洋槍洋炮。升鮮寶將為杭州生鮮配送企業服務,8年的生鮮行業沈澱。

不一致 img 管控 賺錢 很難 blog 生鮮電商 不知道 同時 一直堅守著.net開發陣營,一直致力於生鮮配送行業的信息化解決方案。用程序員本來的堅持,在這個行業中,默默的承受著,不斷優化流程。不斷的實踐。 生鮮配送企業的

130242014068-(2)-運用敏捷開發在<<某系統模塊>>的初步體驗

數據 什麽 提高 適合 在雲端 ron 開發 協作 初步 1. 小組成員及分工: 楊凱 (用戶故事的細化,即功能設計) 楊凱,徐曉敏 (參與系統的類圖設計及上臺匯報) 林毓鋒 (參與用戶故事的討論與設計) 楊凱,徐曉敏,林毓鋒(參與系統的類圖設計

雙十一臨近,怎樣讓買家流暢地秒殺? ——騰訊WeTest獨家開放產品壓測服務

img 高峰 大促 做出 開始 認證 class display 購物車 WeTest 導讀 十一月臨近,一年一度的電商大戲“雙十一”又將隆重出場,目前各大商家已經開始各類優惠券的發放,各類大促的商品表單也已經提前流出,即將流入各個用戶的購物車中。 作為

網站添加商品到購物車功能模塊2017.12.8

全部 入參 es2017 nal 購物 依次 response .net 臺電 前言: 電商網站中添加商品到購物車功能模塊實現: 根據前一篇博客的介紹,我們看到淘寶網站為了保證購物車數據的同步,直接是強制用戶必須登錄才可以將商品加入購物車。而京東網站是用戶在未登錄的狀態下也