探探的IM長連接技術實踐:技術選型、架構設計、性能優化

本文由探探服務端高級技術專家張凱宏分享,原題“探探長鏈接項目的Go語言實踐”,因原文內容有較多錯誤,有修訂和改動。

1、引言

即時通信長連接服務處於網絡接入層,這個領域非常適合用Go語言發揮其多協程並行、異步IO的特點。

探探自長連接項目上線以後,對服務進行了多次優化:GC從5ms降到100微秒(Go版本均爲1.9以上),主要gRPC接口調用延時p999從300ms下降到5ms。在業內大多把目光聚焦於單機連接數的時候,我們則更聚焦於服務的SLA(服務可用性)。

本文將要分享的是陌生人社交應用探探的IM長連接模塊從技術選型到架構設計,再到性能優化的整個技術實踐過程和經驗總結。

2、關於作者

 

張凱宏:擔任探探服務端高級技術專家。

6年Go語言開發經驗,曾用Go語言構建多個大型Web項目,其中涉及網絡庫、存儲服務、長連接服務等。專注於Go語言實踐、存儲服務研發及大數據場景下的Go語言深度優化。

3、項目緣起

我們這個項目是2018年下半年開始,據今天大概1年半時間。

當時探探遇到一些技術痛點,最嚴重的就是嚴重依賴第三方Push,比如說第三方有一些故障的話,對實時IM聊天的KPS有比較大的影響。

當時通過push推送消息,應用內的push延時比較高,平均延時五六百毫秒,這個時間我們不能接受。   

而且也沒有一個 Ping Pland 機制(心跳檢查機制?),無法知道用戶是否在線。

當時產品和技術同學都覺得是機會搞一個長連接了。

4、一個小插曲

項目大概持續了一個季度時間,首先是拿IM業務落地,我們覺得長連接跟IM綁定比較緊密一些。

IM落地之後,後續長連接上線之後,各個業務比較依賴於長連接服務。

這中間有一個小插曲,主要是取名字那一塊。

項目之初給項目起名字叫Socket,看到socket比較親切,覺得它就是一個長連接,這個感覺比較莫名,不知道爲什麼。但運維提出了異議,覺得UDP也是Socket,我覺得UDP其實也可以做長連接。

運維提議叫Keepcom,這個是出自於Keep Alive實現的,這個提議還是挺不錯的,最後我們也是用了這個名字。

客戶端給的建議是Longlink,另外一個是Longconn,一個是IOS端技術同事取的、一個是安卓端技術同事取的。

最後我們都敗了,運維同學勝了,運維同學覺得,如果名字定不下來就別上線的,最後我們妥協了。

5、爲什麼要做長連接?

爲什麼做長連接?

 

如上圖所示:看一下對比挺明顯,左邊是長連接,右邊是短長連接。

對於長連接來說,不需要重新進入連接,或者是釋放連接,一個X包只需要一個RTT就完事。右邊對於一個短連接需要三次握手發送一個push包,最後做揮手。

結論:如果發送N條消息的數據包,對於長連接是2+N次的RTT,對於短連接是3N次RTT,最後開啓Keep Alive,N是連接的個數。

6、長連接技術優勢

我們決結了一下,長連接有以下四大優勢:

  • 1)實時性:長連接是雙向的通道,對消息的推送也是比較實時;
  • 2)有狀態:長連接本身維護用戶的狀態,通過KeepAlive方式,確定用戶是否在線;
  • 3)省流程:長連接比較省流量,可以做一些用戶自定義的數據壓縮,本身也可以省不少的歸屬包和連接包,所以說比較省流量;
  • 4)更省電:減少網絡流量之後,能夠進一步降低移動客戶端的耗電。

7、TCP在移動端能勝任嗎?

在項目開始之前,我們做了比較多的考量。

首先我們看一下對於移動端的長連接來說,TCP協議是不是能夠Work?

對於傳統的長連接來說,Web端的長連接TCP可以勝任,在移動端來說TCP能否勝任?這取決於TCP的幾個特性。

首先TCP有慢啓動和滑動窗口的特性,TCP通過這種方式控制PU包,避免網絡阻塞。

TCP連接之後走一個慢啓動流程,這個流程從初始窗大小做2個N次方的擴張,最後到一定的域值,比如域值是16包,從16包開始逐步往上遞增,最後到24個數據包,這樣達到窗口最大值。

一旦遇到丟包的情況,當然兩種情況。一種是快速重傳,窗口簡單了,相當於是12個包的窗口。如果啓動一個RTO類似於狀態連接,窗口一下跌到初始的窗口大小。

如果啓動RTO重傳的話,對於後續包的阻塞蠻嚴重,一個包阻塞其他包的發送。

 

▲ 上圖引用自《邁向高階:優秀Android程序員必知必會的網絡基礎

有關TCP協議的基礎知識,可以讀讀以下資料:

  1. TCP/IP詳解 - 第17章·TCP:傳輸控制協議
  2. TCP/IP詳解 - 第18章·TCP連接的建立與終止
  3. TCP/IP詳解 - 第21章·TCP的超時與重傳
  4. 通俗易懂-深入理解TCP協議(上):理論基礎
  5. 通俗易懂-深入理解TCP協議(下):RTT、滑動窗口、擁塞處理
  6. 網絡編程懶人入門(一):快速理解網絡通信協議(上篇)
  7. 網絡編程懶人入門(二):快速理解網絡通信協議(下篇)
  8. 網絡編程懶人入門(三):快速理解TCP協議一篇就夠
  9. 腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手
  10. 網絡編程入門從未如此簡單(二):假如你來設計TCP協議,會怎麼做?

8、TCP還是UDP?

 

▲ 上圖引用自《移動端IM/推送系統的協議選型:UDP還是TCP?

TCP實現長連接的四個問題:

  • 1)移動端的消息量還是比較稀疏,用戶每次拿到手機之後,發的消息總數比較少,每條消息的間隔比較長。這種情況下TCP的間連和保持長鏈接的優勢比較明顯一些;
  • 2)弱網條件下丟包率比較高,丟包後Block後續數據發送容易阻塞;
  • 3)TCP連接超時時間過長,默認1秒鐘,這個由於TCP誕生的年代比較早,那會兒網絡狀態沒有現在好,當時定是1s的超時,現在可以設的更短一點;
  • 4)在沒有快速重傳的情況下,RTO重傳等待時間較長,默認15分鐘,每次是N次方的遞減。

爲何最終還是選擇TCP呢?因爲我們覺得UDP更嚴重一點。

首先UDP沒有滑動窗口,無流量控制,也沒有慢啓動的過程,很容易導致丟包,也很容易導致在網絡中間狀態下丟包和超時。

UDP一旦丟包之後沒有重傳機制的,所以我們需要在應用層去實現一個重傳機制,這個開發量不是那麼大,但是我覺得因爲比較偏底層,容易出故障,所以最終選擇了TCP。

TCP還是UDP?這一直是個比較有爭議的話題:

  1. 網絡編程懶人入門(四):快速理解TCP和UDP的差異
  2. 網絡編程懶人入門(五):快速理解爲什麼說UDP有時比TCP更有優勢
  3. 5G時代已經到來,TCP/IP老矣,尚能飯否?
  4. Android程序員必知必會的網絡通信傳輸層協議——UDP和TCP
  5. 不爲人知的網絡編程(六):深入地理解UDP協議並用好它
  6. 不爲人知的網絡編程(七):如何讓不可靠的UDP變的可靠?

如果你對UDP協議還不瞭解,可以讀讀這篇:《TCP/IP詳解 - 第11章·UDP:用戶數據報協議》。

9、選擇TCP的更多理由

我們羅列一下,主要有這3點:

  • 1)目前在移動端、安卓、IOS來說,初始窗口大小比較大默認是10,綜合TCP慢啓動的劣勢來看;
  • 2)在普通的文本傳輸情況下,對於丟包的嚴重不是很敏感(並不是說傳多媒體的數據流,只是傳一些文本數據,這一塊對於丟包的副作用TCP不是特別嚴重);
  • 3)我們覺得TCP在應用層用的比較多。

關於第“3)”點,這裏有以下三個考量點。

第一個考量點:

基本現在應用程序走HTP協議或者是push方式基本都是TCP,我們覺得TCP一般不會出大的問題。

一旦拋棄TCP用UDP或者是QUIC協議的話,保不齊會出現比較大的問題,短時間解決不了,所以最終用了TCP。

第二個考量點:

我們的服務在基礎層上用哪種方式做LB,當時有兩種選擇,一種是傳統的LVS,另一種是HttpDNS(關於HttpDNS請見《全面瞭解移動端DNS域名劫持等雜症:原理、根源、HttpDNS解決方案等》)。

最後我們選擇了HttpDNS,首先我們還是需要跨機房的LB支持,這一點HttpDNS完全勝出。其次,如果需要跨網端的話,LVS做不到,需要其他的部署方式。再者,在擴容方面,LVS算是略勝一籌。最後,對於一般的LB算法,LVS支持並不好,需要根據用戶ID的LB算法,另外需要一致性哈希的LB算法,還需要根據地理位置的定位信息,在這些方面HttpDNS都能夠完美的勝出,但是LVS都做不到。

第三個考量點:

我們在做TCP的飽和機制時通過什麼樣的方式?Ping包的方式,間隔時間怎麼確定,Ping包的時間細節怎麼樣確定?

當時比較糾結是客戶端主動發ping還是服務端主動發Ping?

對於客戶端保活的機制支持更好一些,因爲客戶端可能會被喚醒,但是客戶端進入後臺之後可能發不了包。

其次:APP前後臺對於不同的Ping包間隔來保活,因爲在後臺本身處於一種弱在線的狀態,並不需要去頻繁的發Ping包確定在線狀態。

所以:在後臺的Ping包的時間間隔可以長一些,前端可以短一些。

再者:需要Ping指數增長的間隔支持,在故障的時候還是比較救命的。

比如說:服務端一旦故障之後,客戶端如果拼命Ping的話,可能把服務端徹底搞癱瘓了。如果有一個指數級增長的Ping包間隔,基本服務端還能緩一緩,這個在故障時比較重要。

最後:Ping包重試是否需要Backoff,Ping包重新發Ping,如果沒有收到Bang包的話,需要等到Backoff發Ping。

10、動態Ping包時間間隔算法

PS:在IM裏這其實有個更專業的叫法——智能心跳算法”。

我們還設計了一個動態的Ping包時間間隔算法。

因爲國內的網絡運營商對於NIT設備有一個保活機制,目前基本在5分鐘以上,5分鐘如果不發包的話,會把你的緩存給刪掉。基本上各運營商都在5分鐘以上,只不過移動4G阻礙了。基本可以在4到10分鐘之內發一個Ping包就行,可以維持網絡運營商設備裏的緩存,一直保持着,這樣就沒有問題,使長連接一直保活着。

增加Ping包間隔可以減少網絡流量,能夠進一步降低客戶端的耗電,這一塊的受益還是比較大的。

在低端安卓設備的情況下,有一些DHCP租期的問題。這個問題集中在安卓端的低版本上,安卓不會去續租過期的IP。

解決問題也比較簡單,在DHCP租期到一半的時候,去及時向DHCP服務器續租一下就能解決了。

限於篇幅,我就不在這裏展開了,有興趣可以讀這些資料:

  1. 爲何基於TCP協議的移動端IM仍然需要心跳保活機制?
  2. 一文讀懂即時通訊應用中的網絡心跳包機制:作用、原理、實現思路等
  3. 微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)
  4. 移動端IM實踐:實現Android版微信的智能心跳機制
  5. 移動端IM實踐:WhatsApp、Line、微信的心跳策略分析
  6. 一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)
  7. 手把手教你用Netty實現網絡通信程序的心跳機制、斷線重連機制

11、服務架構

11.1 基本介紹

服務架構比較簡單,大概是四個模塊:

  • 1)首先是HttpDNS;
  • 2)另一個是Connector接入層,接入層提供IP,
  • 3)然後是Router,類似於代理轉發消息,根據IP選擇接入層的服務器,最後推到用戶;
  • 4)最後還有認證的模塊Account,我們目前只是探探APP,這個在用戶中心實現。

11.2 部署

部署上相當於三個模塊:

  • 1)一個是Dispatcher;
  • 2)一個是Redis;
  • 3)一個是Cluser。

如下圖所示:客戶端在連接的時候:

  • 1)需要拿到一個協議;
  • 2)第二步通過HttpDNS拿到ConnectorIP;
  • 3)通過IP連長連接,下一步發送Auth消息認證;
  • 4)連接成功,後面發送Ping包保活;
  • 5)之後斷開連接。

11.3 消息轉發流程

消息轉發的流程分爲兩個部分。

首先是消息上行:服務端發起一個消息包,通過Connector接入服務,客戶端通過Connector發送消息,再通過Connector把消息發到微服務上,如果不需要微服務的話直接去轉發到Vetor就行的,這種情況下Connector更像一個Gateway。

對於下行:業務方都需要請求Router,找到具體的Connector,根據Connector部署消息。

各個公司都是微服務的架構,長連接跟微服務的交互基本兩塊。一塊是消息上行時,更像是Gateway,下行通過Router接入,通過Connector發送消息。

11.4 一些實現細節

下面是一些是細節,我們用了GO語言1.13.4,內部消息傳輸上是gRPC,傳輸協議是Http2,我們在內部通過ETCD做LB的方式,提供服務註冊和發現的服務。

如下圖所示:Connector就是狀態,它從用戶ID到連接的一個狀態信息。

我們看下圖的右邊:它其實是存在一個比較大的MAP,爲了防止MAP的鎖競爭過於嚴重,把MAP拆到2到56個子MAP,通過這種方式去實現高讀寫的MAP。對於每一個MAP從一個ID到連接狀態的映射關係,每一個連接是一個Go Ping,實現細節讀寫是4KB,這個沒改過。

我們看一下Router:它是一個無狀態的CommonGRPC服務,它比較容易擴容,現在狀態信息都存在Redis裏面,Redis大概一組一層,目前峯值是3000。

我們有兩個狀態:一個是Connector,一個是Router。

首先以Connector狀態爲主,Router是狀態一致的保證。

這個裏面分爲兩種情況:如果連接在同一個Connector上的話,Connector需要保證向Router複製的順序是正確的,如果順序不一致,會導致Router和Connector狀態不一致。通過統一Connector的窗口實現消息一致性,如果跨Connector的話,通過在Redis Lua腳本實現Compare And Update方式,去保證只有自己Connector寫的狀態才能被自己更新,如果是別的Connector的話,更新不了其他人的信心。我們保證跨Connector和同一Connector都能夠去按照順序通過一致的方式更新Router裏面連接的狀態。

Dispatche比較簡單:是一個純粹的Common Http API服務,它提供Http API,目前延時比較低大概20微秒,4個CPU就可以支撐10萬個併發。

目前通過無單點的結構實現一個高可用:首先是Http DNS和Router,這兩個是無障礙的服務,只需要通過LB保證。對於Connector來說,通過Http DNS的客戶端主動漂移實現連接層的Ordfrev,通過這種方式保證一旦一個Connector出問題了,客戶端可以立馬漂到下一個Connector,去實現自動的工作轉移,目前是沒有單點的。        

12、性能優化

12.1 基本情況

後續有優化主要有以下幾個方面:

  • 1)網絡優化:這一塊拉着客戶端一起做,首先客戶端需要重傳包的時候發三個嗅探包,通過這種方式做一個快速重傳的機制,通過這種機制提高快速重傳的比例;
  • 2)心跳優化:通過動態的Ping包間隔時間,減少Ping包的數量,這個還在開發中;
  • 3)防止劫持:是通過客戶端使用IP直連方式,迴避域名劫持的操作;
  • 4)DNS優化:是通過HttpDNS每次返回多個IP的方式,來請求客戶端的HttpDNS。

12.2 網絡優化

對於接入層來說,其實Connector的連接數比較多,並且Connector的負載也是比較高。

我們對於Connector做了比較大的優化,首先看Connector最早的GC時間到了4、5毫秒,慘不忍睹的。

我們看一下下面這張圖(圖上)是優化後的結果,大概平均100微秒,這算是比較好。第二張圖(圖下)是第二次優化的結果,大概是29微秒,第三張圖大概是20幾微秒。

12.3 消息延遲

看一下消息延遲,探探對im消息的延遲要求比較高,特別注重用戶的體驗。

這一塊剛開始大概到200ms,如果對於一個操作的話,200ms還是比較嚴重的。

第一次優化之後(下圖-上)的狀態大概1點幾毫秒,第二次優化之後(下圖-下)現在降到最低點差不多100微秒,跟一般的Net操作時間維度上比較接近。

12.4 Connector優化過程

優化過程是這樣的:

  • 1)首先需要關鍵路徑上的Info日誌,通過採樣實現Access Log,info日誌是接入層比較重的操作;
  • 2)第二通過Sync.Poll緩存對象;
  • 3)第三通過Escape Analysis對象儘可能在線上分配。

後面還實現了Connector的無損發版:這一塊比較有價值。長連接剛上線發版比較多,每次發版對於用戶來說都有感,通過這種方式讓用戶儘量無感。

實現了Connector的Graceful Shutdown的方式,通過這種方式優化連接。

首先:在HttpDNS上下線該機器,下線之後緩慢斷開用戶連接,直到連接數小於一定閾值。後面是重啓服務,發版二進制。

最後:是HttpDNS上線該機器,通過這種方式實現用戶發版,時間比較長,當時測了挺長時間,去衡量每秒鐘斷開多少個連接,最後閾值是多少。

後面是一些數據:剛纔GC也是一部分,目前連接數都屬於比較關鍵的數據。首先看連接數單機連接數比較少,不敢放太開,最多是15萬的單機連接數,大約100微秒。

Goroutine數量跟連接數一樣,差不多15萬個:

看一下內存使用狀態,下圖(上)是GO的內存總量,大概是2:3,剩下五分之一是屬於未佔用,內存總量是7.3個G。

下圖是GC狀態,GC比較健康,紅線是GC每次活躍內存數,紅線遠遠高於綠線。

看到GC目前的狀況大概是20幾微秒,感覺目前跟GO的官方時間比較能對得上,我們感覺GC目前都已經優化到位了。

12.5 後續要做的優化

最後是規劃後續還要做優化。

首先:對系統上還是需要更多優化Connector層,更多去減少內存的分配,儘量把內存分配到堆上而不是站上,通過這種方式減少GC壓力,我們看到GO是非Generational Collection GE,堆的內存越多的話,掃的內存也會越多,這樣它不是一個線性的增長。

第二:在內部更多去用Sync Pool做短暫的內存分配,比如說Context或者是臨時的Dbyle。

協議也要做優化:目前用的是WebSocket協議,後面會加一些功能標誌,把一些重要信息傳給服務端。比如說一些重傳標誌,如果客戶端加入重傳標誌的話,我們可以先校驗這個包是不是重傳包,如果是重傳包的話會去判斷這個包是不是重複,是不是之前發過,如果發過的話就不需要去解包,這樣可以少做很多的服務端操作。

另外:可以去把Websocket目前的Mask機制去掉,因爲Mask機制防止Web端的改包操作,但是基本是客戶端的傳包,所以並不需要Mask機制。

業務上:目前規劃後面需要做比較多的事情。我們覺得長連接因爲是一個接入層,是一個非常好的地方去統計一些客戶端的分佈。比如說客戶端的安卓、IOS的分佈狀況。

進一步:可以做用戶畫像的統計,男的女的,年齡是多少,地理位置是多少。大概是這些,謝謝!

13、熱門問題回覆

* 提問:剛纔說連接層對話重啓,間接的過程中那些斷掉的用戶就飄到其他的,是這樣做的嗎?

張凱宏:目前是這樣的,客戶端做自動飄移。

* 提問:現在是1千萬日活,如果服務端往客戶端一下推100萬,這種場景怎麼做的?

張凱宏:目前我們沒有那麼大的消息推送量,有時候會發一些業務相關的推送,目前做了一個限流,通過客戶端限流實現的,大概三四千。

* 提問:如果做到後端,意味着會存在安全隱患,攻擊者會不停的建立連接,導致很難去做防禦,會有這個問題嗎?因爲惡意的攻擊,如果攻擊的話建立連接就可以了,不需要認證的機制。

張凱宏:明白你的意思,這一塊不只是長連接,短連接也有這個問題。客戶端一直在僞造訪問結果,流量還是比較大的,這一塊靠防火牆和IP層防火牆實現。

* 提問:長連接服務器是掛在最外方,中間有沒有一層?

張凱宏:目前接着如下層直接暴露在外網層,前面過一層IP的防DNSFre的防火牆。除此之外沒有別的網絡設備了。

* 提問:基於什麼樣的考慮中間沒有加一層,因爲前面還加了一層的情況。

張凱宏:目前沒有這個計劃,後面會在Websofte接入層前面加個LS層可以方便擴容,這個收益不是特別大,所以現在沒有去計劃。

* 提問:剛剛說的斷開重傳的三次嗅探那個是什麼意思?

張凱宏:我們想更多的去觸發快速重傳,這樣對於TCP的重傳間隔更短一些,服務端根據三個循環包判斷是否快速重傳,我們會發三個循環包避免一個RTO重傳的開啓。

* 提問:探探最開始安卓服務器是使用第三方的嗎?

張凱宏:對的,剛開始是極光推送的。

* 提問:從第三方的安卓服務器到自研。

張凱宏:如果極光有一些故障的話,對我們影響還是蠻大。之前極光的故障頻率挺高,我們想是不是自己能把服務做起來。第二點,極光本身能提供一個用戶是否在線的判斷,但是它那個判斷要走通道,延時比較高,本身判斷是連接把延時降低一些。

* 提問:比如說一個新用戶上線連接過來,有一些用戶發給他消息,他是怎麼把一線消息拿到的?

張凱宏:我們通過業務端保證的,未發出來的消息會存一個ID號,當用戶重新連的時候,業務端再拉一下。

14、參考資料

[1] 移動端IM/推送系統的協議選型:UDP還是TCP?

[2] 5G時代已經到來,TCP/IP老矣,尚能飯否?

[3] 爲何基於TCP協議的移動端IM仍然需要心跳保活機制?

[4] 一文讀懂即時通訊應用中的網絡心跳包機制:作用、原理、實現思路等

[5] 微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)

[6] 移動端IM實踐:實現Android版微信的智能心跳機制

[7] 邁向高階:優秀Android程序員必知必會的網絡基礎

[8] 全面瞭解移動端DNS域名劫持等雜症:原理、根源、HttpDNS解決方案等

[9] 技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解

[10] 新手入門一篇就夠:從零開發移動端IM

[11] 長連接網關技術專題(二):知乎千萬級併發的高性能長連接網關技術實踐

[12] 長連接網關技術專題(三):手淘億級移動端接入層網關的技術演進之路

[13] 長連接網關技術專題(五):喜馬拉雅自研億級API網關技術實踐

[14] 一套億級用戶的IM架構技術乾貨(上篇):整體架構、服務拆分等

[15] 一套億級用戶的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等

[16] 從新手到專家:如何設計一套億級消息量的分佈式IM系統

學習交流:

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

- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK 

本文已同步發佈於:http://www.52im.net/thread-3780-1-1.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章