Go 每日一庫:tproxy 是個啥?
你有同感嗎?
當(dāng)大家在開發(fā)服務(wù)端代碼的時候,會不會經(jīng)常有如下疑問?
納悶 MySQL 連接池到底有多少連接? 每個連接的生命周期持續(xù)多久? 連接異常斷開的時候到底是服務(wù)端主動斷的,還是客戶端主動斷的? 當(dāng)長時間沒有請求的時候,底層庫是否有 KeepAlive 請求?
復(fù)雜網(wǎng)絡(luò)情況的處理從來都是后端開發(fā)的重點和難點之一,你是不是也為各種網(wǎng)絡(luò)情況的調(diào)試而頭頂發(fā)涼呢?
所以我寫了 tproxy
當(dāng)我在做后端開發(fā)和寫 go-zero 的時候,經(jīng)常會需要監(jiān)控網(wǎng)絡(luò)連接,分析請求內(nèi)容。比如:
分析 gRPC 連接何時連接、何時重連,并據(jù)此調(diào)整各種參數(shù),比如:MaxConnectionIdle 分析 MySQL 連接池,當(dāng)前多少連接,連接的生命周期是什么策略 也可以用來觀察和分析任何 TCP 連接,看服務(wù)端主動斷,還是客戶端主動斷等等
tproxy 的安裝
$ GOPROXY=https://goproxy.cn/,direct go install github.com/kevwan/tproxy@latest
或者使用 docker 鏡像:
$ docker run --rm -it -p <listen-port>:<listen-port> -p <remote-port>:<remote-port> kevinwan/tproxy:v1 tproxy -l 0.0.0.0 -p <listen-port> -r host.docker.internal:<remote-port>
arm64 系統(tǒng):
$ docker run --rm -it -p <listen-port>:<listen-port> -p <remote-port>:<remote-port> kevinwan/tproxy:v1-arm64 tproxy -l 0.0.0.0 -p <listen-port> -r host.docker.internal:<remote-port>
tproxy 的用法
$ tproxy --help
Usage of tproxy:
-d duration
the delay to relay packets
-l string
Local address to listen on (default "localhost")
-p int
Local port to listen on
-q Quiet mode, only prints connection open/close and stats, default false
-r string
Remote address (host:port) to connect
-t string
The type of protocol, currently support grpc
分析 gRPC 連接
tproxy -p 8088 -r localhost:8081 -t grpc -d 100ms
偵聽在 localhost 和 8088 端口 重定向請求到 localhost:8081識別數(shù)據(jù)包格式為 gRPC 數(shù)據(jù)包延遲100毫秒

其中我們可以看到 gRPC 的一個請求的初始化和來回,可以看到第一個請求其中的 stream id 為 1。
再比如 gRPC 有個 MaxConnectionIdle 參數(shù),用來設(shè)置 idle 多久該連接會被關(guān)閉,我們可以直接觀察到時間到了之后服務(wù)端會發(fā)送一個 http2 的 GoAway 包。

比如我把 MaxConnectioinIdle 設(shè)為 5 分鐘,連接成功之后 5 分鐘沒有請求,連接就被自動關(guān)閉了,然后重新建了一個連接上來。
分析 MySQL 連接
我們來分析一下 MySQL 連接池設(shè)置對連接池的影響,比如我把參數(shù)設(shè)為:
maxIdleConns = 3
maxOpenConns = 8
maxLifetime = time.Minute
...
conn.SetMaxIdleConns(maxIdleConns)
conn.SetMaxOpenConns(maxOpenConns)
conn.SetConnMaxLifetime(maxLifetime)
我們把 MaxIdleConns 和 MaxOpenConns 設(shè)為不同值,然后我們用 hey 來做個壓測:
hey -c 10 -z 10s "http://localhost:8888/lookup?url=go-zero.dev"
我們做了并發(fā)為10QPS且持續(xù)10秒鐘的壓測,連接結(jié)果如下圖:

我們可以看到:
10秒鐘內(nèi)建立了2000+的連接 過程中在不停的關(guān)閉已有連接,重開新的連接 每次連接使用完放回去,可能超過 MaxIdleConns 了,然后這個連接就會被關(guān)閉 接著來新請求去拿連接時,發(fā)現(xiàn)連接數(shù)小于 MaxOpenConns,但是沒有可用請求了,所以就又新建了連接
這也就是我們經(jīng)常會看到 MySQL 很多 TIME_WAIT 的原因。
然后我們把 MaxIdleConns 和 MaxOpenConns 設(shè)為相同值,然后再來做一次相同的壓測:

我們可以看到:
一直維持著8個連接不變 壓測完過了一分鐘(ConnMaxLifetime),所有連接被關(guān)閉了
這里的 ConnMaxLifetime 一定要設(shè)置的小于 wait_timeout,可以通過如下方式查看 wait_timeout 值:

我建議設(shè)置小于5分鐘的值,因為有些交換機會5分鐘清理一下空閑連接,比如我們在做社交的時候,一般心跳包不會超過5分鐘。具體原因可以看
https://github.com/zeromicro/go-zero/blob/master/core/stores/sqlx/sqlmanager.go#L65
其中 go-sql-driver 的 issue 257 里有一段也在說 ConnMaxLifetime,如下:
14400 sec is too long. One minutes is enough for most use cases.
Even if you configure entire your DC (OS, switch, router, etc...), TCP connection may be lost from various reasons. (bug in router firmware, unstable power voltage, electric nose, etc...)
所以如果你不知道 MySQL 連接池參數(shù)怎么設(shè)置,可以參考 go-zero 的設(shè)置。
另外,ConnMaxIdleTime 對上述壓測結(jié)果沒有影響,其實你也不需要設(shè)置它。
如果你對上述設(shè)置有疑問,或者覺得哪里有誤,歡迎在 go-zero 群里一起討論。
項目地址
tproxy: https://github.com/kevwan/tproxy
推薦閱讀
