go grpc: connection reset by peer 的一種解決方案

superpigwin發表於2022-07-13

最近添哥一直反映,他手下的裝置以grpc stream的方式向我服務端傳送資料。偶然會收到錯誤。現象如下:

  1. 連線已經建立了一段時間,正常使用。
  2. 突然client.Send 返回 eof。
  3. 客戶端有報錯:connection reset by peer
  4. 在服務端找到錯誤:context canceled

這裡不得不提一下,客戶端上報到服務的網路環境並不是很好,而且服務端每個程式有數十萬個協程在執行,處理上十萬條grpc stream。

選取了幾個裝置在服務端與客戶端tcpdump,通過七七四十九天,終於捕獲到了異常時的抓包。

go grpc: connection reset by peer 的一種解決方案

現象:

  1. 正常情況下,服務端客戶端定期互Ping。
  2. 當異常時,在服務端/客戶端的抓包會發現Ping包未回。很快連線斷開。

猜測和grpc keepalive功能有關。

grpc server keepalive配置

原始配置

	var keepAliveArgs = keepalive.ServerParameters{
		Time:    60 * time.Second,
		Timeout: 5 * time.Second,
	}
	s := grpc.NewServer(
	grpc.KeepaliveParams(keepAliveArgs).....)

為了防止客戶端斷連後資源洩漏,grpc的服務端一般會配置keepalive,每隔一段時間就向空閒的client傳送ping包,並計算回包的時間。當ping沒有回應。則認為連線已失敗(比如被牆),此時在服務端會關閉這個連線並配置svr.Context()為done。

上面的配置代表,每60S向客戶端檢測一次,如果ping的包沒有在5秒內回,則斷開連線。此時就會出現上述的異常事件。

原因分析

為了弄清keepalive的邏輯,檢視原始碼grpc/internal/transport/http2_server.go

grpc ping發包邏輯

每隔預設的時間,就會發一個包。並將kpTimeoutLeft置為keepalive.Timeout

發包之後邏輯

  1. 檢測是否在kpTimeoutLeft為0前收到了任何資料(不僅是ping的回包)。
  2. 此時outstandingPing為true,所以不會再有新的ping被髮出。這是最坑的一點設計。合理的設計應該允許重試幾次,以重試後能收到包為準。
  3. 不停的去sleep,並去減小kpTimeoutLeft。
  4. 當kpTimeoutLeft<0,連線關閉。

預期外斷聯原因

可能是因為網路抖動或者grpc server忙不過來,使得某次的ping包被丟棄或未及時處理。造成了連線被錯誤的切斷。

解決

一開始,想要找一找有沒有retry之類的配置。不要僅丟棄一次就把連線切斷,但沒找到。這時,添哥突發奇想,將Timeout的時間延長。於是,keepalive的配置變成了這樣:

	var keepAliveArgs = keepalive.ServerParameters{
		Time:    30 * time.Second,
		Timeout: 90 * time.Second,
	}

在這個配置下,為ping之後給了更長的反應時間,根據grpc的原始碼,90秒內如果有任意的資料被接收(包含收到客戶端發來的訊息)。連線都不會被切斷。但假如客戶端一直沒有資料回發,猜想應該還是會把連線切斷。因為ping在沒有收到回訊息的時候不會再進行下一次ping。

通過檢視註釋也能應證程式碼的實現:

	// After having pinged for keepalive check, the server waits for a duration
	// of Timeout and if no activity is seen even after that the connection is
	// closed.
	Timeout time.Duration // The current default value is 20 seconds.

只要在ping後timeout內有activity,連線就不會中斷。還好這個業務client和server互動很頻繁,在90秒內一般會有資料的互動。

立馬變更,困擾我們很久的問題,用一種不是很優雅的方式解決了。

相關文章