長連接服務處於網絡接入層,這個領域很是適合用Go語言發揮其多協程並行,異步IO特色。探探自長連接項目上線之後,對服務進行了屢次優化:GC從5ms降到100微秒(Go版本均爲1.9以上),主要Grpc接口調用延時p999從300ms降低到5ms。在業內大多把目光聚焦於單機鏈接數的時候,咱們則更聚焦於服務的SLA。前端
張凱宏|探探高級技術專家 golang
擔任探探服務端高級技術專家。6年Go語言開發經驗,曾用Go語言構建多個大型Web項目,其中涉及網絡庫、存儲服務、長連接服務等。專一於Go語言實踐、存儲服務研發及大數據場景下的Go語言深度優化。web
咱們這個項目是2018年下半年開始,據今天大概1年半時間。當時探探遇到幾個問題,首先是比較嚴重依賴第三方Push,好比說第三方有一些故障的話,對聊天的KPS有比較大的影響。當時經過push推送消息,應用內的push延時比較高,平均延時五六百毫秒,這個時間咱們不能接受。
當時沒有一個 Ping Pland 機制,沒法知道用戶是否在線。當時產品和技術同窗都以爲是機會搞一個長連接了。

項目大概持續了一個季度時間,首先是拿IM業務落地,咱們以爲長連接跟IM綁定比較緊密一些。IM落地以後,後續長連接上線以後,各個業務比較依賴於長連接服務。這中間有一個小插曲,主要是取名字那一塊。項目之初給項目起名字叫Socket,看到socket比較親切,以爲它就是一個長連接,這個感受比較莫名,不知道爲何。運維提出了異議,以爲UDP也是Socket,我以爲UDP其實也能夠作長連接。運維提議叫Keppcom,這個是出自於Keep Alive實現的,這個提議仍是挺不錯的,最後咱們也是用了這個名字。客戶端給的建議是Longlink,另一個是Longconn,一個是IOS版,一個是安卓版。最後咱們都敗了,運維同窗勝了,運維同窗以爲,若是名字定不下來就別上線的,最後咱們妥協了。

爲何作長連接?看一下對比挺明顯,左邊是長連接,右邊是短長連接。對於長連接來講,不須要從新進入連接,或者是釋放連接,一個X包只須要一個RTT就完事。右邊對於一個短連接須要三次握手發送一個push包,最後作揮手。
結論,若是發送N條消息的數據包,對於長連接是2+N次的RTT,對於短連接是3N次RTT,最後開啓Keep Alive,N是連接的個數。


第一,實時性方面,長連接是雙向的通道,對消息的推送也是比較實時。
第二,長連接自己維護用戶的狀態,經過KeepAlive方式,肯定用戶是否在線。
第三,長連接比較省流量,能夠作一些用戶自定義的數據壓縮,自己也能夠省很多的歸屬包和鏈接包,因此說比較省流量。在這個前提下,客戶端就比較省量了。
第四,減小網絡流量以後,可以進一步下降客戶端的耗電。

想說一些設計細節,在項目開始以前作了比較多的考量,最終定位一個詳細設計的報告。首先咱們看一下對於移動端的長連接來講,TCP協議是否是可以Work?在傳統的長連接來講,Web端的長連接TCP能夠勝任,在移動端來講TCP可否勝任?取決於TCP幾個特性,首先TCP有慢啓動和滑動窗口的特性,TCP經過這種方式控制PU包,避免網絡阻塞。
TCP連接以後走一個慢啓動流程,這個流程從初始窗大小作2個N次方的擴張,最後到必定的域值,好比域值是16包,從16包開始逐步往上遞增,最後到24個數據包,這樣達到窗口最大值。一旦遇到丟包的狀況,固然兩種狀況。一種是快速重傳,窗口簡單了,至關因而12個包的窗口。若是啓動一個RTO相似於狀態連接,窗口一下跌到初始的窗口大小。
若是啓動RTO重傳的話,對於後續包的阻塞蠻嚴重,一個包阻塞其餘包的發送。

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

爲什麼最終仍是選擇TCP呢?由於咱們以爲UDP更嚴重一點。首先UDP沒有滑動窗口,無流量控制,也沒有慢啓動的過程,很容易致使丟包,也很容易致使在網絡中間狀態下丟包和超時。
UDP一旦丟包以後沒有重傳機制的,因此咱們須要在應用層去實現一個重傳機制,這個開發量不是那麼大,可是我以爲由於比較偏底層,容易出故障,因此最終選擇了TCP。

一、目前在移動端、安卓、IOS來講,初始窗口大小比較大默認是10,綜合TCP慢啓動的劣勢來看。
二、在普通的文本傳輸狀況下,對於丟包的嚴重不是很敏感,並非說傳多媒體的數據流,只是傳一些文本數據,這一塊對於丟包的反作用TCP不是特別嚴重。
三、咱們以爲TCP在應用層用的比較多,這裏有三個考量點。第一個考量點:基本如今應用程序走HTP協議或者是push方式基本都是TCP,咱們以爲TCP通常不會出大的問題。一旦拋棄TCP用UDP或者是Q協議的話,保不齊會出現比較大的問題,短期解決不了,因此最終用了TCP。第二個考量點:咱們的服務在基礎層上用哪一種方式作LB,當時有兩種選擇,一種是傳統的LVS,另外一種是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。

咱們還設計了一個動態的Ping包時間間隔算法,國內的網絡運營商對於NIT設備有一個保活機制,目前基本在5分鐘以上,5分鐘若是不發包的話,會把你的緩存給刪掉。基本運營商都在5分鐘以上,只不過移動4G阻礙了。基本能夠在4到10分鐘以內發一個Ping包就行,能夠維持網絡運營商設備裏的緩存,一直保持着,這樣就沒有問題,使長連接一直保活着。能夠減小網絡流量,可以進一步下降客戶端的耗電,這一塊的受益仍是比較大的。

在低端安卓設備的狀況下,有一些DHCP租期的問題,這個問題集中在安卓端的低版本上,安卓不會去續租過時的IP。
解決問題也比較簡單,在DHCP租期到一半的時候,去及時向DHCP服務器續租一下就能解決了。
下一個設計的細節對於長連接來講是比較關鍵的,通常分爲兩個部分,一個是Header,還有Payload。Header通常是硬程,Payload是編程,固然Header也是有編程的,Header協議基本組成裏面丟包括Header裏面,Header會包括一些比較重要的控制字段和Flack字段,這些字段首先會按數據包的功能是哪幾個,而且會作出一些功能的取捨。對於協議來講,還須要考慮擴展性和安全性。

咱們如今用的是Websocket,Websocket協議跟着Web1.1誕生,時間比較早,有些特性用不着了。
Websocket協議首先是Finolway,這個Way表示數據包是否是一個結束包,剛纔三個RCEWay主要是擴展,後面四個Way主要是表示這個包是作什麼用的,好比說是鏈接包、調開包或者是push的消息包,後面包是Mask包,MaskWay就是表示這個包是否是通過數據的Mask操做,後面是7個位的Payload長度,若是不夠用的話,後面還有10個位的Payload長度,若是Payload長度小於127位的話。

MOTT協議稍微複雜一些,咱們看到MOTT自己支持變程的Header,咱們這邊是定程的Header。
左邊是Header詳細協議,左邊有四位是指定Context包的信息,它是一個間連的包仍是斷連的包,右邊是四個Falway,目前基本的MOTT協議就用到四個,後面跟着兩個是QS,QS等級是多少,MOTT通常三個等級,最少一次,最多一次,正好一次的三個語義,最後一個V是保留V,這個消息是否是保持在服務端,以便下一次再鏈接的時候,傳給客戶端。

對於MQTT協議來講,自己涉及比較地道的地方,通常看到長鏈接會實現正好一次傳輸的語義,對這個語義作了簡化。
由於MQTT這種實現更復雜一些,很難理解,咱們作了簡化。
比方說咱們認爲一個語義這麼實現,客戶端反覆重試,直到服務端收到爲止。服務端經過消息的ID去作屈從,服務端保證AT Mose Once,客戶端保證至少一次,這樣就可以輸出正好一次的語義,這樣算是比較簡單。咱們的消息分爲兩種類型,一個是上位消息,從發送方和客戶端推到業務端去,這種方式客戶端就是發送方,由客戶端保證AT Least Once的語義。經過傳輸鏈路最低端,業務微服務保證AT LeastOnce的語義。對於下行消息,通常業務方發送的時候也會保證最少一次的語義,這樣實如今傳輸鏈路上各個端都保證至少一次語義的話,須要在接收方去保證最多一次就好了。下面是服務架構。

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

部署上至關於三個模塊,一個是Dispatcher,一個是Redis,一個是Cluser。

客戶端在鏈接的時候,須要拿到一個協議,第二步經過HttpDNS拿到ConnectorIP,經過IP連長連接,下一步發送Auth消息認證,鏈接成功,後面發送Ping包保活,以後斷開鏈接。

首先是消息上行,服務端發起一個消息包,經過Connector接入服務,客戶端經過Connector發送消息,再經過Connector把消息發到微服務上,若是不須要微服務的話直接去轉發到Vetor就行的,這種狀況下Connector更像一個Gateway。
對於下行,業務方都須要請求Router,找到具體的Connector,根據Connector部署消息。

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

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

這是Connector的一個內室圖,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,去實現自動的工做轉移,目前是沒有單點的。

第一,網絡優化;這一塊拉着客戶端一塊兒作,首先客戶端須要重傳包的時候發三個嗅探包,經過這種方式作一個快速重傳的機制,經過這種機制提升快速重傳的比例。
第二個是經過動態的Ping包間隔時間,減小Ping包的數量,這個還在開發中。
第三個是經過客戶端使用IP直連方式,迴避域名劫持的操做。
第四個是經過HttpDNS每次返回多個IP的方式,來請求客戶端的HttpDNS。

對於接入層來講,其實Connector的鏈接數比較多,而且Connector的負載也是比較高。
咱們對於Connector作了比較大的優化,首先看Connector最先的GC時間到了四、5毫秒,慘不忍睹的。
咱們看一下這張圖是優化後的結果,大概平均100微秒,這算是比較好。
第二張圖是第二次優化的結果,大概是29微秒,第三張圖大概是20幾微秒。

看一下消息延遲,探探對消息的延遲要求比較高,特別注重用戶的體驗。
這一塊剛開始大概到200ms,若是對於一個操做的話,200ms仍是比較嚴重的。
第一次優化以後上一張圖的狀態大概1點幾毫秒,第二次優化以後如今降到最低點差很少100微秒,跟通常的Net操做時間維度上比較接近。

首先須要關鍵路徑上的Info日誌,經過採樣實現Access Log,info日誌是接入層比較重的操做;
第三經過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目前都已經優化到位了。
最後是後續要作的事情,規劃後續還要作優化,首先對系統上仍是須要更多優化Connector層,更多去減小內存的分配,儘可能把內存分配到堆上而不是站上,經過這種方式減小GC壓力,咱們看到GO是非Generational Collection GE,堆的內存越多的話,掃的內存也會越多,這樣它不是一個線性的增加。
第二,在內部更多去用Sync Pool作短暫的內存分配,好比說Context或者是臨時的Dbyle。
協議也要作優化,目前用的是Websocket協議,後面會加一些功能標誌,把一些重要信息傳給服務端。好比說一些重傳標誌,若是客戶端加入重傳標誌的話,咱們能夠先校驗這個包是否是重傳包,若是是重傳包的話會去判斷這個包是否是重複,是否是以前發過,若是發過的話就不須要去解包,這樣能夠少作不少的服務端操做。
第二點,能夠去把Websocket目前的Mask機制去掉,由於Mask機制防止Web端的改包操做,可是基本是客戶端的傳包,因此並不須要Mask機制。

業務上,目前規劃後面須要作比較多的事情。
咱們以爲長鏈接由於是一個接入層,是一個很是好的地方去統計一些客戶端的分佈。
好比說客戶端的安卓、IOS的分佈情況。
第二步能夠作用戶畫像的統計,男的女的,年齡是多少,地理位置是多少。大概是這些,謝謝!

提問:剛纔說鏈接層對話重啓,間接的過程當中那些斷掉的用戶就飄到其餘的,是這樣作的嗎?
提問:
如今是1千萬日活,若是服務端往客戶端一下推100萬,這種場景怎麼作的?
張凱宏:
目前咱們沒有那麼大的消息推送量,有時候會發一些業務相關的推送,目前作了一個限流,經過客戶端限流實現的,大概三四千。
提問:
若是作到後端,意味着會存在安全隱患,攻擊者會不停的創建鏈接,致使很難去作防護,會有這個問題嗎?
由於惡意的攻擊,若是攻擊的話創建鏈接就能夠了,不須要認證的機制。
張凱宏:明白你的意思,這一塊不僅是長連接,短連接也有這個問題。客戶端一直在僞造訪問結果,流量仍是比較大的,這一塊靠防火牆和IP層防火牆實現。
提問:
長連接服務器是掛在最外方,中間有沒有一層?
張凱宏:目前接着以下層直接暴露在外網層,前面過一層IP的防DNSFre的防火牆。除此以外沒有別的網絡設備了。
提問:
基於什麼樣的考慮中間沒有加一層,由於前面還加了一層的狀況。
張凱宏:目前沒有這個計劃,後面會在Websofte接入層前面加個LS層能夠方便擴容,這個收益不是特別大,因此如今沒有去計劃。
提問:
剛剛說的斷開重傳的三次嗅探那個是什麼意思?
張凱宏:咱們想更多的去觸發快速重傳,這樣對於TCP的重傳間隔更短一些,服務端根據三個循環包判斷是否快速重傳,咱們會發三個循環包避免一個RTO重傳的開啓。
張凱宏:若是極光有一些故障的話,對咱們影響仍是蠻大。以前極光的故障頻率挺高,咱們想是否是本身能把服務作起來。第二點,極光自己能提供一個用戶是否在線的判斷,可是它那個判斷要走通道,延時比較高,自己判斷是經過用戶的Ping包來作權衡,咱們以爲偏高一些,咱們想本身實現長連接把延時下降一些。
提問:
好比說一個新用戶上線鏈接過來,有一些用戶發給他消息,他是怎麼把一線消息拿到的?
張凱宏:咱們經過業務端保證的,未發出來的消息會存一個ID號,當用戶從新連的時候,業務端再拉一下。
本文分享自微信公衆號 - GoCN(golangchina)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。