本文由馬蜂窩技術團隊電商交易基礎平臺研發工程師"Anti Walker"原創分享。php
即時通信(IM)功能對於電商平臺來講很是重要,特別是旅遊電商。html
從商品複雜性來看,一個旅遊商品可能會包括用戶在將來一段時間的衣、食、住、行等方方面面。從消費金額來看,每每單次消費額度較大。對目的地的陌生、在行程中可能的問題,這些因素使用戶在購買前、中、後都存在和商家溝通的強烈需求。能夠說,一個好用的 IM 能夠在必定程度上對企業電商業務的 GMV 起到促進做用。git
本文咱們將結合馬蜂窩旅遊電商IM系統的發展歷程,單獨介紹基於Go重構分佈式IM系統過程當中的實踐和總結(本文至關於《從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》一文的進階篇),但願能夠給有類似問題的朋友一些借鑑。github
另外:若是你對Go在高併發系統中的應用感興趣,即時通信網的如下兩篇也值得一讀:算法
《 Go語言構建千萬級在線的高併發消息推送系統實踐(來自360公司)》
《 12306搶票帶來的啓示:看我如何用Go實現百萬QPS的秒殺系統(含源碼)》
系列文章:數據庫
《 從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》
《 從游擊隊到正規軍(二):馬蜂窩旅遊網的IM客戶端架構演進和實踐總結》
《 從游擊隊到正規軍(三):基於Go的馬蜂窩旅遊網分佈式IM系統技術實踐》(* 本文)
關於馬蜂窩旅遊網: apache
馬蜂窩旅遊網是中國領先的自由行服務平臺,由陳罡和呂剛創立於2006年,從2010年正式開始公司化運營。馬蜂窩的景點、餐飲、酒店等點評信息均來自上億用戶的真實分享,每一年幫助過億的旅行者制定自由行方案。編程
學習交流:json
- 即時通信/推送技術開發交流5羣: 215477170 [推薦]
- 移動端IM開發入門文章:《 新手入門一篇就夠:從零開發移動端IM》
(本文同步發佈於:http://www.52im.net/thread-2909-1-1.html)後端
《 一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》
《 一套原創分佈式即時通信(IM)系統理論架構方案》
《 從零到卓越:京東客服即時通信系統的技術架構演進歷程》
《 蘑菇街即時通信/IM服務器開發之架構選擇》
《 以微博類應用場景爲例,總結海量社交系統的架構設計步驟》
《 一套高可用、易伸縮、高併發的IM羣聊、單聊架構方案設計實踐》
《 騰訊QQ1.4億在線用戶的技術挑戰和架構演進之路PPT》
《 微信技術總監談架構:微信之道——大道至簡(演講全文)》
《 如何解讀《微信技術總監談架構:微信之道——大道至簡》》
《 快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)》
《 瓜子IM智能客服系統的數據架構設計(整理自現場演講,有配套PPT)》
《 阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處》
與廣義上的即時通信不一樣,電商各業務線有其特有業務邏輯,如客服聊天系統的客人分配邏輯、敏感詞檢測邏輯等,這些每每要耦合進通訊流程中。隨着接入業務線愈來愈多,即時通信服務冗餘度會愈來愈高。同時整個消息鏈路追溯複雜,服務穩定性很受業務邏輯的影響。
以前咱們 IM 應用中的消息推送主要基於輪詢技術,消息輪詢模塊的長鏈接請求是經過 php-fpm 掛載在阻塞隊列上實現。當請求量較大時,若是不能及時釋放 php-fpm 進程,對服務器的性能消耗很大。
爲了解決這個問題,咱們曾用 OpenResty+Lua 的方式進行改造,利用 Lua 協程的方式將總體的 polling 的能力從 PHP 轉交到 Lua 處理,釋放一部 PHP 的壓力。這種方式雖然能提高一部分性能,但 PHP-Lua 的混合異構模式,使系統在使用、升級、調試和維護上都很麻煩,通用性也較差,不少業務場景下仍是要依賴 PHP 接口,優化效果並不明顯。
爲了解決以上問題,咱們決定結合電商 IM 的特定背景對 IM 服務進行重構,核心是實現業務邏輯和即時通信服務的分離。
更多有關馬蜂窩旅遊網的IM系統架構的演進過程,請詳讀:《從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路》一文,在此再也不贅述。
1)業務解耦:
將業務邏輯與通訊流程剝離,使 IM 服務架構更加清晰,實現與電商 IM 業務邏輯的徹底分離,保證服務穩定性。
2)接入方式靈活:
以前新業務接入時,須要在業務服務器上配置 OpenResty 環境及 Lua 協程代碼,很是不便,IM 服務的通用性也不好。考慮到現有業務的實際狀況,咱們但願 IM 系統能夠提供 HTTP 和 WebSocket 兩種接入方式,供業務方根據不一樣的場景來靈活使用。
好比已經接入且運行良好的電商定製化團隊的待辦系統、定製遊搶單系統、投訴系統等下行相關的系統等,這些業務沒有明顯的高併發需求,能夠經過 HTTP 方式迅速接入,不須要熟悉稍顯複雜的 WebSocket 協議,進而下降沒必要要的研發成本。
3)架構可擴展:
爲了應對業務的持續增加給系統性能帶來的挑戰,咱們考慮用分佈式架構來設計即時通信服務,使系統具備持續擴展及提高的能力。
目前,馬蜂窩技術體系主要包括 PHP,Java,Golang,技術棧比較豐富,使業務作選型時能夠根據問題場景選擇更合適的工具和語言。
結合 IM 具體應用場景,咱們選擇 Go 的緣由包括:
總體架構圖以下:
名詞解釋:
架構分層:
步驟一:
如上圖右側所示:
用戶客戶端與消息處理模塊創建 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 來去重展現。
以上步驟的數據流轉大體如圖所示:
(1)消息不丟失:
爲了不消息丟失,咱們設置了超時重傳機制。服務端會在推送給客戶端消息後,等待客戶端的 ACK,若是客戶端沒有返回 ACK,服務端會嘗試屢次推送。
目前默認 18s 爲超時時間,重傳 3 次不成功,斷開鏈接,從新鏈接服務器。從新鏈接後,採用拉取歷史消息的機制來保證消息完整。
(2)多端消息同步:
客戶端現有 PC 瀏覽器、Windows 客戶端、H五、iOS/Android,系統容許用戶多端同時在線,且同一端能夠多個狀態,這就須要保證多端、多用戶、多狀態的消息是同步的。
咱們用到了 Redis 的 Hash 存儲,將用戶信息、惟一鏈接對應值 、鏈接標識、客戶端 IP、服務器標識、角色、渠道等記錄下來,這樣經過 key(uid) 就能找到一個用戶在多個端的鏈接,經過 key+field 能定位到一條鏈接。
上文咱們已經說過,由於是雙層設計,就涉及到兩個 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,若是沒有註冊中心,就要用到配置文件來管理這些配置信息,這挺麻煩的。並且你新增一個後,須要分發模塊馬上發現,不能有延遲。
若是有新的服務,分發模塊但願能快速感知到新的服務。利用 Key 的續租機制,若是在必定時間內,沒有監聽到 Key 有續租動做,則認爲這個服務已經掛掉,就會把該服務摘除。
在進行註冊中心的選型時,咱們主要調研了 ETCD、ZooKeeper、Consul。
三者的壓測結果參考以下:
結果顯示,ETCD 的性能是最好的。另外,ETCD 背靠阿里巴巴,並且屬於 Go 生態,咱們公司內部的 K8S 集羣也在使用。
綜合考量後,咱們選擇使用 ETCD 做爲服務註冊和發現組件。而且咱們使用的是 ETCD 的集羣模式,若是一臺服務器出現故障,集羣其餘的服務器仍能正常提供服務。
小結一下:經過保證服務和進程間的正常通信,及 ETCD 集羣模式的設計,保證了 IM 服務總體具備極高的可用性。
消息分發模塊和消息處理模塊都能進行水平擴展。當總體服務負載高時,能夠經過增長節點來分擔壓力,保證消息即時性和服務穩定性。
處於安全性考慮,咱們設置了黑名單機制,能夠對單一 uid 或者 ip 進行限制。好比在同一個 uid 下,若是一段時間內創建的鏈接次數超過設定的閾值,則認爲這個 uid 可能存在風險,暫停服務。若是暫停服務期間該 uid 繼續發送請求,則限制服務的時間相應延長。
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
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 的開發和學習陣營中來,拓寬本身的技能圖譜,擁抱雲原生。
[1] 有關IM架構設計的文章:
《 淺談IM系統的架構設計》
《 簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端》
《 一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》
《 一套原創分佈式即時通信(IM)系統理論架構方案》
《 從零到卓越:京東客服即時通信系統的技術架構演進歷程》
《 蘑菇街即時通信/IM服務器開發之架構選擇》
《 騰訊QQ1.4億在線用戶的技術挑戰和架構演進之路PPT》
《 微信後臺基於時間序的海量數據冷熱分級架構設計實踐》
《 微信技術總監談架構:微信之道——大道至簡(演講全文)》
《 如何解讀《微信技術總監談架構:微信之道——大道至簡》》
《 快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)》
《 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聊天消息序列號生成實踐(算法原理篇)》
《 微信技術分享:微信的海量IM聊天消息序列號生成實踐(容災方案篇)》
《 新手入門:零基礎理解大型分佈式架構的演進歷史、技術原理、最佳實踐》
《 一套高可用、易伸縮、高併發的IM羣聊、單聊架構方案設計實踐》
《 阿里技術分享:深度揭祕阿里數據庫技術方案的10年變遷史》
《 阿里技術分享:阿里自研金融級數據庫OceanBase的艱辛成長之路》
《 社交軟件紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等》
《 社交軟件紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進》
《 社交軟件紅包技術解密(三):微信搖一搖紅包雨背後的技術細節》
《 社交軟件紅包技術解密(四):微信紅包系統是如何應對高併發的》
《 社交軟件紅包技術解密(五):微信紅包系統是如何實現高可用性的》
《 社交軟件紅包技術解密(六):微信紅包系統的存儲層架構演進實踐》
《 社交軟件紅包技術解密(七):支付寶紅包的海量高併發技術實踐》
《 社交軟件紅包技術解密(八):全面解密微博紅包技術方案》
《 社交軟件紅包技術解密(九):談談手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)