基于Go實(shí)現(xiàn)TCP長(zhǎng)連接上的請(qǐng)求數(shù)控制
一、背景
- 在服務(wù)端開啟長(zhǎng)連接的情況下,四層負(fù)載均衡轉(zhuǎn)發(fā)請(qǐng)求時(shí),會(huì)出現(xiàn)服務(wù)端收到的請(qǐng)求qps不均勻的情況,或是服務(wù)重啟后會(huì)長(zhǎng)時(shí)間無法接受到請(qǐng)求,導(dǎo)致不同服務(wù)端機(jī)器的負(fù)載不一致,qps高的機(jī)器過載的問題;
- 該問題的原因是只有在新建連接時(shí)才會(huì)觸發(fā)負(fù)載四層負(fù)載均衡器的再均衡策略,客戶端隨機(jī)與不同的服務(wù)器新建 TCP 連接,否則現(xiàn)有的 TCP 連接夠用時(shí),會(huì)一致被復(fù)用,在現(xiàn)有的 TCP 連接上傳輸請(qǐng)求,出現(xiàn) qps 不均勻的情況;
- 因此需要服務(wù)端定期主動(dòng)斷開一些長(zhǎng)連接,觸發(fā)四層轉(zhuǎn)發(fā)連接的再均衡策略,實(shí)現(xiàn)類似于七層負(fù)載均衡 Nginx 中的 keepalive_requests 字段的功能,即同一個(gè) TCP 長(zhǎng)連接上的請(qǐng)求數(shù)達(dá)到一定數(shù)量時(shí),服務(wù)端主動(dòng)斷開 TCP 長(zhǎng)連接。
二、基本介紹
1,TCP 的 Keepalive:
- 即 TCP ?;顧C(jī)制,是由 TCP 層(內(nèi)核態(tài)) 實(shí)現(xiàn)的,位于傳輸層,相關(guān)配置參數(shù)在
/proc/sys/net/ipv4
目錄下:tcp_keepalive_intvl
:?;钐綔y(cè)報(bào)文發(fā)送時(shí)間間隔,75s;tcp_keepalive_probes
:?;钐綔y(cè)報(bào)文發(fā)送次數(shù),9次,9次之后直接關(guān)閉;tcp_keepalive_time
:保活超時(shí)時(shí)間,7200s,即該 TCP 連接空閑兩小時(shí)后開始發(fā)送保活探測(cè)報(bào)文;
- TCP 連接傳輸完數(shù)據(jù)后,不會(huì)立馬主動(dòng)關(guān)閉,會(huì)先存活一段時(shí)間,超過存活時(shí)間后,會(huì)觸發(fā)?;顧C(jī)制發(fā)送探測(cè)報(bào)文,多次探測(cè)確認(rèn)沒有數(shù)據(jù)繼續(xù)傳輸后,再進(jìn)行 TCP 四次揮手,關(guān)閉 TCP 連接;
- 在 go 語言中,建立 TCP 連接時(shí),默認(rèn)設(shè)置的 keep-alive 為 15s,詳見
go1.21 src/net/tcp/tcpsocket.go
2,HTTP 的 Keep-Alive
即 HTTP 長(zhǎng)連接,是由應(yīng)用層(用戶態(tài)) 實(shí)現(xiàn)的,位于應(yīng)用層;
需要在 HTTP 報(bào)文的頭部設(shè)置以下信息:
* Connection: keep-alive * Keep-Alive: timeout=7200
上面信息表示,http 采用長(zhǎng)連接,且超時(shí)時(shí)間為7200s;
http 協(xié)議 1.0 默認(rèn)采用短連接,即每次發(fā)送完數(shù)據(jù)后會(huì)設(shè)置
Connection: close
表示需要主動(dòng)關(guān)閉當(dāng)前 TCP 連接,進(jìn)行四次揮手后關(guān)閉;下次再發(fā)送數(shù)據(jù)前,又需要先進(jìn)行三次握手建立 TCP 連接,才能發(fā)送數(shù)據(jù);循環(huán)往復(fù),每次建立的 TCP 連接都只能發(fā)送一次數(shù)據(jù),每次發(fā)送數(shù)據(jù)都需要進(jìn)行三次握手與四次揮手,每次建立連接與斷開連接會(huì)導(dǎo)致網(wǎng)絡(luò)耗時(shí)變長(zhǎng),如下圖;
- http 協(xié)議 1.1 開始默認(rèn)采用長(zhǎng)連接;即每次發(fā)送完數(shù)據(jù)后會(huì)設(shè)置
Connection: keep-alive
表示需要復(fù)用當(dāng)前 TCP 連接,建立一次 TCP 連接后,可以發(fā)送多次的 HTTP 報(bào)文,即多次發(fā)送數(shù)據(jù)也只需要一遍三次握手與四次揮手,省去了每次建立連接與斷開連接的時(shí)間,如下圖:
3,四層負(fù)載均衡
- 四層負(fù)載均衡是一種在網(wǎng)絡(luò)層(第四層)上進(jìn)行負(fù)載均衡的技術(shù),通過傳輸層協(xié)議 TCP 或 UDP,將傳入的請(qǐng)求分發(fā)到多個(gè)服務(wù)器上,以實(shí)現(xiàn)請(qǐng)求的負(fù)載均衡和高可用性;
- 四層負(fù)載均衡主要基于目標(biāo)IP地址和端口號(hào)對(duì)請(qǐng)求進(jìn)行分發(fā),不深入分析請(qǐng)求的內(nèi)容和應(yīng)用層協(xié)議,通常使用負(fù)載均衡器作為中間設(shè)備,接收客戶端請(qǐng)求,并將請(qǐng)求轉(zhuǎn)發(fā)到后端服務(wù)器;
- 負(fù)載均衡器可以根據(jù)預(yù)定義的算法(例如輪詢、最小連接數(shù)、哈希、隨機(jī)、加權(quán)隨機(jī)等)選擇后端服務(wù)器來處理請(qǐng)求;
- 四層負(fù)載均衡只需要解析到傳輸層協(xié)議即可進(jìn)行請(qǐng)求轉(zhuǎn)發(fā),且是直接和真實(shí)服務(wù)器建立 TCP 連接,所以整體耗時(shí)比較?。?/li>
4,七層負(fù)載均衡
- Nginx 可以用于七層負(fù)載均衡器,客戶端與 Nginx 所在的服務(wù)器建立起 TCP 連接,通過解析應(yīng)用層中的內(nèi)容,選擇對(duì)應(yīng)的后端服務(wù)器,Nginx 所在的機(jī)器再與后端服務(wù)器建立起 TCP 連接,將應(yīng)用層數(shù)據(jù)轉(zhuǎn)發(fā)后端服務(wù)器上,這就是所謂的七層負(fù)載均衡,即根據(jù)應(yīng)用層的信息進(jìn)行轉(zhuǎn)發(fā);
- 在 Nginx 中,
keepalive_requests
指令用于設(shè)置在長(zhǎng)連接上可以處理的最大請(qǐng)求數(shù)量,一旦達(dá)到這個(gè)數(shù)量,Nginx 將關(guān)閉當(dāng)前連接并等待客戶端建立新的連接以繼續(xù)處理請(qǐng)求; - 通過限制每個(gè)持久連接上處理的請(qǐng)求數(shù)量,
keepalive_requests
可以幫助控制服務(wù)器資源的使用,并防止連接過度占用服務(wù)器資源,也可以幫助避免潛在的連接泄漏和提高服務(wù)器的性能; - 該值設(shè)置得過小,會(huì)導(dǎo)致經(jīng)常需要 TCP 三次握手和四次揮手,無法有效發(fā)揮長(zhǎng)連接的性能;該值設(shè)置得過大,會(huì)無法發(fā)揮該值的作用,導(dǎo)致長(zhǎng)連接上的請(qǐng)求過多;具體的大小,要根據(jù)實(shí)際請(qǐng)求的 QPS 和響應(yīng)耗時(shí)來設(shè)置;
- 七層負(fù)載均衡能夠根據(jù)應(yīng)用層的請(qǐng)求內(nèi)容實(shí)現(xiàn)更驚喜的請(qǐng)求分發(fā)和處理,但是需要建立兩次 TCP 連接,以及每次將報(bào)文逐步解析到應(yīng)用層再又逐步封裝鏈路層,會(huì)導(dǎo)致耗時(shí)和失敗率上漲;
- 四層負(fù)載均衡與七層負(fù)載均衡的比較如下:
三、具體實(shí)現(xiàn)
1,代碼示例
package main import ( "sync" "time" "github.com/labstack/echo" ) type QpsBalance struct { mu sync.Mutex data map[string]int // key: ip:port num int // 通過配置文件來配置 } // Update 返回 true 表示當(dāng)前的 tcp 連接上的請(qǐng)求數(shù)超過限制,需要斷開連接 // 如果是某個(gè) tcp 連接長(zhǎng)時(shí)間沒有后續(xù)請(qǐng)求了,默認(rèn) 15s 之后會(huì)發(fā)送?;顖?bào)文, func (q *QpsBalance) Update(k string) bool { q.mu.Lock() defer q.mu.Unlock() num := q.data[k] + 1 if num >= q.num { q.data[k] = 0 return true } q.data[k] = num return false } // Reset 通過定時(shí)任務(wù)每天3點(diǎn)重置,避免上游多次不同的擴(kuò)容ip形成臟數(shù)據(jù) func (q *QpsBalance) Reset() { q.mu.Lock() defer q.mu.Unlock() q.data = make(map[string]int) } func (q *QpsBalance) Init(n int) { q.mu.Lock() defer q.mu.Unlock() q.data = make(map[string]int) q.num = n } func main() { balancer := &QpsBalance{} balancer.Init(200) e := echo.New() e.PUT("/handle", func(c echo.Context) error { if balancer.Update(c.Request().RemoteAddr) { c.Response().Header().Set("Connection", "close") } // do other return nil }) go func(b *QpsBalance) { ticker := time.NewTicker(time.Hour) for { t := <-ticker.C if t.Hour() == 3 { b.Reset() } } }(balancer) }
2,基本原理
- 當(dāng)同一個(gè) TCP 連接上的請(qǐng)求數(shù)達(dá)到一定限制時(shí),設(shè)置返回頭部為
Connection: close
,主動(dòng)關(guān)閉 TCP 連接,并重置計(jì)數(shù)器; - 需要注意的是,客戶端如果也是服務(wù)器,并且存在自動(dòng)擴(kuò)容,那么需要定期清理計(jì)數(shù)的 map,避免多次不同的擴(kuò)容ip形成臟數(shù)據(jù);
- 以及某些 TCP 連接可能沒有達(dá)到計(jì)數(shù)的閾值,便不再被復(fù)用了,經(jīng)過一段時(shí)間后會(huì)主動(dòng)斷開,這些 TCP 的計(jì)數(shù)依然存在 map 中,形成了臟數(shù)據(jù);
- 在 TCP 連接中,使用四元組來標(biāo)記的一個(gè)唯一的 TCP 連接,即源ip、源端口、目的ip、目的端口,現(xiàn)在是在服務(wù)端進(jìn)行計(jì)數(shù)的,所以目的ip和目的端口都是一樣的,僅僅通過源ip和源端口便可以分別出 TCP 連接;
四、數(shù)據(jù)驗(yàn)證
- 通過服務(wù)記錄的監(jiān)控,查看上游請(qǐng)求過來的平均耗時(shí)、P99 耗時(shí)、失敗率是否有明顯的變化,以及不同服務(wù)器收到的請(qǐng)求 QPS 是否均勻;
- 通過
tcpdump src port 28080 -A | grep Connection
命令查看服務(wù)端響應(yīng)的HTTP報(bào)文頭部是否有Connection: close
字段,即是否會(huì)主動(dòng)關(guān)閉 TCP 連接; - 通過
netstat -antp | grep main | grep :28080
命令查看不同服務(wù)器上的服務(wù)建立的長(zhǎng)連接數(shù)是否均勻; - 選取某個(gè)長(zhǎng)連接,多次查看其存在的時(shí)間,是否符合預(yù)期;
- 預(yù)期時(shí)間:假如
keepalive_requests
設(shè)置為 100,客戶端記錄的響應(yīng)耗時(shí) 20ms(包括網(wǎng)絡(luò)耗時(shí)和服務(wù)端耗時(shí)),那么平均一個(gè)長(zhǎng)連接一秒能夠發(fā)送 5 個(gè)請(qǐng)求,約 20s 后能夠處理 100 個(gè),那么該長(zhǎng)連接能夠存活 20s; - 注意不能查看某個(gè)長(zhǎng)連接對(duì)應(yīng)的 socket 創(chuàng)建的時(shí)間,因?yàn)橥粋€(gè) socket 會(huì)被不同的長(zhǎng)連接復(fù)用,一般不會(huì)被關(guān)閉;
- 預(yù)期時(shí)間:假如
以上就是基于Go實(shí)現(xiàn)TCP長(zhǎng)連接上的請(qǐng)求數(shù)控制的詳細(xì)內(nèi)容,更多關(guān)于Go TCP請(qǐng)求數(shù)控制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言開發(fā)中有了net/http為什么還要有g(shù)in的原理及使用場(chǎng)景解析
這篇文章主要為大家介紹了Go語言有了net/http標(biāo)準(zhǔn)庫為什么還要有g(shù)in第三方庫的原理及使用場(chǎng)景詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08golang的時(shí)區(qū)和神奇的time.Parse的使用方法
這篇文章主要介紹了golang的時(shí)區(qū)和神奇的time.Parse的使用方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Go語言實(shí)現(xiàn)并發(fā)控制的常見方式詳解
這篇文章主要為大家詳細(xì)介紹了Go語言實(shí)現(xiàn)并發(fā)控制的幾種常見方式,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,有需要的小伙伴可以參考一下2024-03-03基于Go語言實(shí)現(xiàn)高性能文件上傳下載系統(tǒng)
在Web應(yīng)用開發(fā)中,文件上傳下載是一個(gè)非常常見的需求,本文將介紹如何使用Go語言實(shí)現(xiàn)一個(gè)安全、高效的本地文件存儲(chǔ)系統(tǒng),感興趣的小伙伴可以了解下2025-03-03詳解Go多協(xié)程并發(fā)環(huán)境下的錯(cuò)誤處理
這篇文章主要介紹了詳解Go多協(xié)程并發(fā)環(huán)境下的錯(cuò)誤處理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08golang實(shí)現(xiàn)LRU緩存淘汰算法的示例代碼
這篇文章主要介紹了golang實(shí)現(xiàn)LRU緩存淘汰算法的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-12-12