首頁>科技>

一、引言

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

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

本文我們將結合馬蜂窩旅遊電商IM系統的發展歷程,單獨介紹基於Go重構分散式IM系統過程中的實踐和總結(本文相當於《從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》一文的進階篇),希望可以給有相似問題的朋友一些借鑑。

另外:如果你對Go在高併發系統中的應用感興趣,即時通訊網的以下兩篇也值得一讀:

《Go語言構建千萬級線上的高併發訊息推送系統實踐(來自360公司)》

《12306搶票帶來的啟示:看我如何用Go實現百萬QPS的秒殺系統(含原始碼)》

系列文章:

《從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》

《從游擊隊到正規軍(二):馬蜂窩旅遊網的IM客戶端架構演進和實踐總結》

《從游擊隊到正規軍(三):基於Go的馬蜂窩旅遊網分散式IM系統技術實踐》(* 本文)

關於馬蜂窩旅遊網:

馬蜂窩旅遊網是中國領先的自由行服務平臺,由陳罡和呂剛創立於2006年,從2010年正式開始公司化運營。馬蜂窩的景點、餐飲、酒店等點評資訊均來自上億使用者的真實分享,每年幫助過億的旅行者制定自由行方案。

學習交流:

- 即時通訊/推送技術開發交流5群:215477170 [推薦]

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》

(本文同步釋出於:http://www.52im.net/thread-2909-1-1.html)

二、相關文章

《一套海量線上使用者的移動端IM架構設計實踐分享(含詳細圖文)》

《從零到卓越:京東客服即時通訊系統的技術架構演進歷程》

《蘑菇街即時通訊/IM伺服器開發之架構選擇》

《以微博類應用場景為例,總結海量社交系統的架構設計步驟》

《一套高可用、易伸縮、高併發的IM群聊、單聊架構方案設計實踐》

《騰訊QQ1.4億線上使用者的技術挑戰和架構演進之路PPT》

《瓜子IM智慧客服系統的資料架構設計(整理自現場演講,有配套PPT)》

《阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處》

三、技術背景和問題

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

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

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

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

更多有關馬蜂窩旅遊網的IM系統架構的演進過程,請詳讀:《從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》一文,在此不再贅述。

四、基於Go的雙層分散式IM架構4.1、實現目標

1)業務解耦:

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

2)接入方式靈活:

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

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

3)架構可擴充套件:

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

4.2、語言選擇

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

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

1)執行效能:在效能上,尤其是針對網路通訊等 IO 密集型應用場景。Go 系統的效能更接近 C/C++;

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

4.3、架構設計

整體架構圖如下:

名詞解釋:

1)客戶:一般指購買商品的使用者;

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

3)分發模組:即 Dispatcher,提供訊息分發的給指定的工作模組的橋接作用;

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

架構分層:

1)展示層:提供 HTTP 和 WebSocket 兩種接入方式;

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

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

4)資料層:Redis 叢集,記錄使用者身份、連線資訊、客戶端平臺(移動端、網頁端、桌面端)等組成的唯一 Key。

4.4、服務流程

步驟一:

如上圖右側所示:

使用者客戶端與訊息處理模組建立 WebSocket 長連線;

通過負載均衡演算法,使客戶端連線到合適的伺服器(訊息處理模組的某個 Worker);

連線成功後,記錄使用者連線資訊,包括使用者角色(客人或商家)、客戶端平臺(移動端、網頁端、桌面端)等組成唯一 Key,記錄到 Redis 叢集。

步驟二:

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

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

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

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

步驟三:

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

步驟四:

基於 Redis 叢集中的使用者連線資訊,訊息分發模組將訊息轉發到目標使用者連線的 WebSocket 伺服器(訊息處理模組中的某一個 Worker)

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

步驟五:

訊息處理模組將訊息通過 WebSocket 協議推送到客戶端。

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

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

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

以上步驟的資料流轉大致如圖所示:

4.5、系統完整性設計4.5.1 可靠性

(1)訊息不丟失:

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

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

(2)多端訊息同步:

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

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

4.5.2 可用性

上文我們已經說過,因為是雙層設計,就涉及到兩個 Server 間的通訊,同進程內通訊用 Channel,非同程序用訊息佇列或者 RPC。綜合性能和對伺服器資源利用,我們最終選擇 RPC 的方式進行 Server 間通訊。

在對基於 Go 的 RPC 進行選行時,我們比較了以下比較主流的技術方案:

1)Go STDRPC:Go 標準庫的 RPC,效能最優,但是沒有治理;

2)RPCX:效能優勢 2*GRPC + 服務治理;

3)GRPC:跨語言,但效能沒有 RPCX 好;

4)TarsGo:跨語言,效能 5*GRPC,缺點是框架較大,整合起來費勁;

5)Dubbo-Go:效能稍遜一籌, 比較適合 Go 和 Java 間通訊場景使用。

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

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

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

在進行註冊中心的選型時,我們主要調研了 ETCD、ZooKeeper、Consul。

三者的壓測結果參考如下:

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

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

小結一下:通過保證服務和程序間的正常通訊,及 ETCD 叢集模式的設計,保證了 IM 服務整體具有極高的可用性。

4.5.3 擴充套件性

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

4.5.4 安全性

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

4.6、效能優化和踩過的坑4.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

4.6.2 踩坑經驗

1)協程異常:

基於對開發成本和服務穩定性等問題的考慮,我們的 WebSocket 服務基於 Gorilla/WebSocket 框架開發。其中遇到一個問題,就是當讀協程發生異常退出時,寫協程並沒有感知到,結果就是導致讀協程已經退出但是寫協程還在執行,直到觸發異常之後才退出。

這樣雖然從表面上看不影響業務邏輯,但是浪費後端資源。在編碼時應該注意要在讀協程退出後主動通知寫協程,這樣一個小的優化可以這在高併發下能節省很多資源。

2)心跳設計:

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

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

3)每天分割日誌:

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

ZAP 的優點包括:

1)顯示程式碼行號這個需求,ZAP 支援而 Logrus 不支援,這個屬於提效的。行號展示對於定位問題很重要;

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

小坑:每天寫一個日誌檔案的功能,目前 ZAP 不支援,需要自己寫程式碼支援,或者請求系統部支援。

五、效能表現

壓測 1:

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

壓測 2:

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

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

結論:

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

六、本文小結

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

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

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

附錄:更多IM架構設計方面的文章

[1] 有關IM架構設計的文章:

《淺談IM系統的架構設計》

《簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端》

《一套海量線上使用者的移動端IM架構設計實踐分享(含詳細圖文)》

《從零到卓越:京東客服即時通訊系統的技術架構演進歷程》

《蘑菇街即時通訊/IM伺服器開發之架構選擇》

《騰訊QQ1.4億線上使用者的技術挑戰和架構演進之路PPT》

《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群聊、單聊架構方案設計實踐》

《阿里技術分享:深度揭祕阿里資料庫技術方案的10年變遷史》

《阿里技術分享:阿里自研金融級資料庫OceanBase的艱辛成長之路》

《社交軟體紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等》

《社交軟體紅包技術解密(七):支付寶紅包的海量高併發技術實踐》

《社交軟體紅包技術解密(八):全面解密微博紅包技術方案》

《社交軟體紅包技術解密(九):談談手Q紅包的功能邏輯、容災、運維、架構等》

《即時通訊新手入門:一文讀懂什麼是Nginx?它能否實現IM的負載均衡?》

《即時通訊新手入門:快速理解RPC技術——基本概念、原理和用途》

《多維度對比5款主流分散式MQ訊息佇列,媽媽再也不擔心我的技術選型了》

《從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》

《從游擊隊到正規軍(二):馬蜂窩旅遊網的IM客戶端架構演進和實踐總結》

《IM開發基礎知識補課(六):資料庫用NoSQL還是SQL?讀這篇就夠了!》

《瓜子IM智慧客服系統的資料架構設計(整理自現場演講,有配套PPT)》

《阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處》

>> 更多同類文章 ……

[2] 更多其它架構設計相關文章:

《騰訊資深架構師乾貨總結:一文讀懂大型分散式系統設計的方方面面》

《快速理解高效能HTTP服務端的負載均衡技術原理》

《子彈簡訊光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐》

《知乎技術分享:從單機到2000萬QPS併發的Redis高效能快取實踐之路》

《新手入門:零基礎理解大型分散式架構的演進歷史、技術原理、最佳實踐》

《阿里技術分享:深度揭祕阿里資料庫技術方案的10年變遷史》

《阿里技術分享:阿里自研金融級資料庫OceanBase的艱辛成長之路》

《達達O2O後臺架構演進實踐:從0到4000高併發請求背後的努力》

《優秀後端架構師必會知識:史上最全MySQL大表優化方案總結》

《小米技術分享:解密小米搶購系統千萬高併發架構的演進和實踐》

《一篇讀懂分散式架構下的負載均衡技術:分類、原理、演算法、常見方案等》

《通俗易懂:如何設計能支撐百萬併發的資料庫架構?》

《多維度對比5款主流分散式MQ訊息佇列,媽媽再也不擔心我的技術選型了》

《從新手到架構師,一篇就夠:從100到1000萬高併發的架構演進之路》

《美團技術分享:深度解密美團的分散式ID生成演算法》

《12306搶票帶來的啟示:看我如何用Go實現百萬QPS的秒殺系統(含原始碼)》

>> 更多同類文章 ……

(本文同步釋出於:http://www.52im.net/thread-2909-1-1.html)

最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • 釘釘被迫喊話小學生:"爸爸,饒了我吧!"