ETCD Client 的生命周期影響系統(tǒng) TCP 連接資源

最近發(fā)現(xiàn)一個 ETCD Client 端的實現(xiàn)問題——ETCD 所在機器宕機或者斷網(wǎng)的情況下,ETCD Client 無法快速重連到可用的 etcd 節(jié)點,導致 client 端不可用(該問題的描述后續(xù)發(fā)表文章介紹)。
后來找到一個比較簡單的優(yōu)化方式,即臨時新創(chuàng)建一個新的 ETCD 的 Client 來重試操作,可以立即操作成功。但是每次遇到斷網(wǎng)錯誤或者斷網(wǎng)時間比較長,那么這段時間內所有的請求都要重新創(chuàng)建一個新的 ETCD Client 來重試嗎?頻繁創(chuàng)建 ETCD Client 對系統(tǒng)有什么影響?此外,還聯(lián)想到在使用 ETCD 初期的時候,請教過一個專家同學,關于 ETCD Client 的使用上,全局使用一個 ETCD Client,還是在需要使用的模塊內部使用獨立的 Client,這兩種方式哪個更為合理?
今天,就簡單的為自己解答一下這幾個問題哈。本文主要是做一些簡單的調研和基礎知識的分析哈,引出 ETCD Client 的生命周期管理比較合理的方式。
普及知識
先來普及一些基本的概念,便于我們更好的研究和分析哈。ETCD_API=3,即 v3 Client。
gRPC 相關的概念
etcd clientv3 端是基于 gPRC 實現(xiàn)的。所以,這里先簡單的描述一下 gRPC 的相關的基本內容哈。
首先,計算機網(wǎng)絡的 7 層協(xié)議: 物理層、數(shù)據(jù)鏈路層、網(wǎng)絡層、傳輸層、會話層、表示層和應用層,大家肯定都非常熟悉了。從協(xié)議上來說:
TCP[1] 是傳輸層協(xié)議,主要解決數(shù)據(jù)如何在網(wǎng)絡中傳輸,它解決了第四層傳輸層所指定的功能。 HTTP[2] 是應用層協(xié)議,主要解決如何包裝數(shù)據(jù),是建立在 TCP 協(xié)議之上的應用協(xié)議。因為 TCP 協(xié)議對上層應用的不友好,所以面向應用層的開發(fā)產生了 HTTP 協(xié)議。
RPC 是遠程過程調用,它是一種設計、實現(xiàn)框架,通信協(xié)議只是其中一部分,所以他和 HTTP 并不是對立的,也沒有包含關系,本質上是提供了一種輕量無感知的跨進程通信的方式,通信協(xié)議可以使用 HTTP,也可以使用其他協(xié)議。關于為何有 HTTP 協(xié)議,為何還要在系統(tǒng)之后通信上使用 RPC 調用的原因,相信網(wǎng)上有很多論述,這里就不詳細描述了哈。gRPC 是谷歌開源的一個 RPC 框架,面向移動和 HTTP2 設計的。和很多 RPC 系統(tǒng)一樣,服務端負責實現(xiàn)定義好的接口并處理客戶端的請求,客戶端根據(jù)接口描述直接調用需要的服務??蛻舳撕头斩丝梢苑謩e使用 gPRC 支持的不同語言實現(xiàn)。HTTP2[3] 相對于 HTTP1.x 具有很多新特性,比如多路復用,即多個 request 共用一個 TCP 連接,其他特性這里不詳細敘述了。
TCP 短連接使用的問題
TCP 連接是網(wǎng)絡編程中最基礎的概念,這里就不詳細介紹 TCP 連接過程了。短連接最大的問題在占用大量的系統(tǒng)資源,例如,socket,而導致這個問題的原因其實很簡單:tcp 連接的使用,都需要經過相同的流程:連接建立 -> 數(shù)據(jù)傳輸 -> 連接關閉。
對于系統(tǒng)請求負載較高的情況下,系統(tǒng)出現(xiàn)的最多和最直觀的錯誤應該就是 "too many time wait"。這里簡單說一下 socket 句柄被耗盡的原因,主要因為 TIME_WAIT 這種狀態(tài)的 TCP 連接的存在。
由于 socket 是全雙工的工作模式,一個 socket 的關閉,是需要四次握手來完成的,如下圖所示:

主動關閉連接的一方(成為主動方),調用 close,然后發(fā)送 FIN 包給被動方,表明自己已經準備關閉連接; 被動方收到 FIN 包后,回復 ACK ,然后進入到 CLOSE_WAIT ; 主動方等待對方關閉,則進入 FIN_WAIT_2 狀態(tài);此時,主動方等待被動方的調用 close() 操作; 被動方在完成所有數(shù)據(jù)發(fā)送后,調用 close()操作;此時,被動方發(fā)送 FIN 包給主動方,等待對方的 ACK,被動方進入 LAST_ACK 狀態(tài); 主動方收到 FIN 包,協(xié)議層回復 ACK ;此時,主動方進入 TIME_WAIT 狀態(tài);而被動方,進入 CLOSED 狀態(tài) 等待 2MSL 時間,主動方結束 TIME_WAIT ,進入 CLOSED 狀態(tài)
通過上面的一次 socket 關閉操作,可以得出以下幾點:
主動方最終會進入 TIME_WAIT 狀態(tài); 被動方,有一個中間狀態(tài),即 CLOSE_WAIT,因為協(xié)議層在等待上層的應用程序,主動調用 close 操作后才主動關閉這條連接; TIME_WAIT 會默認等待 2MSL 時間后,才最終進入 CLOSED 狀態(tài); 在一個連接沒有進入 CLOSED 狀態(tài)之前,這個連接是不能被重用的!
所以,由上面的原理可以看出,TCP 連接的頻繁創(chuàng)建和關閉,會導致系統(tǒng)處于 TIME_WAIT 或者 CLOSE_WAIT 狀態(tài)的 TCP 連接變多,占用系統(tǒng)資源,影響正常的功能。
那么,下面我們看看,gRPC 的 Client 如果不合理的使用,會造成什么樣的問題呢?
gRPC Client 生命周期控制問題
寫個簡單的 ETCD Client V3 的小程序,來看看頻繁的創(chuàng)建和關閉 ETCD Client 會有什么樣的影響,程序代碼如下:
// golang
func TestNewETCDClient() {
for {
etcdClient, err := clientv3.New(clientv3.Config{
Endpoints: []string{"10.0.0.2:2379"},
DialTimeout: 3 * time.Second,
})
if err != nil {
logger.Errorf("new client failed due to %v", err)
return
}
etcdClient.Close()
}
}
然后,我們用如下命令看看系統(tǒng)有什么變化,如下所示,不到一分鐘時間 TIME_WAIT 暴漲到了 16325 多個。
$ netstat -n| awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'


結論和建議
前面原理上已經解釋過, ETCD Client v3 基于 gRPC 實現(xiàn),而 gRPC 采用的 HTTP2 協(xié)議,在傳輸層的協(xié)議依然是 tcp。如果對 gRPC 的 Client 的生命周期設置的非常短,那么相當于對這個 TCP 連接資源轉化成了短連接,沒有發(fā)揮其核心功能。
所以,對于 ETCD Client 的使用,應該充分利用其多路復用的原則,全局定義一個 Client 變量,生命同期等同于進程,以降低對 TCP 資源的管理成本。
參考文章
你所不知道的 TIME_WAIT 和 CLODE_WAIT[4]
引用鏈接
TCP: https://tools.ietf.org/html/rfc793
[2]HTTP: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
[3]HTTP2: https://zh.wikipedia.org/wiki/HTTP/2
[4]你所不知道的 TIME_WAIT 和 CLODE_WAIT: https://blog.oldboyedu.com/tcp-wait/
原文鏈接:https://developer.aliyun.com/article/704034


你可能還喜歡
點擊下方圖片即可閱讀

云原生是一種信仰 ??
關注公眾號
后臺回復?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點擊 "閱讀原文" 獲取更好的閱讀體驗!
發(fā)現(xiàn)朋友圈變“安靜”了嗎?


