你瞭解微服務的超時傳遞嗎?

kevwan發表於2021-10-18

為什麼需要超時控制?

很多連鎖故障的場景下的一個常見問題是伺服器正在消耗大量資源處理那些早已經超過客戶端截止時間的請求,這樣的結果是,伺服器消耗大量資源沒有做任何有價值的工作,回覆已經超時的請求是沒有任何意義的。

超時控制可以說是保證服務穩定性的一道重要的防線,它的本質是快速失敗 (fail fast),良好的超時控制策略可以儘快清空高延遲的請求,儘快釋放資源避免請求的堆積。

服務間超時傳遞

如果一個請求有多個階段,比如由一系列 RPC 呼叫組成,那麼我們的服務應該在每個階段開始前檢查截止時間以避免做無用功,也就是要檢查是否還有足夠的剩餘時間處理請求。

一個常見的錯誤實現方式是在每個 RPC 服務設定一個固定的超時時間,我們應該在每個服務間傳遞超時時間,超時時間可以在服務呼叫的最上層設定,由初始請求觸發的整個 RPC 樹會設定同樣的絕對截止時間。例如,在服務請求的最上層設定超時時間為 3s,服務 A 請求服務 B,服務 B 執行耗時為 1s,服務 B 再請求服務 C 這時超時時間剩餘 2s,服務 C 執行耗時為 1s,這時服務 C 再請求服務 D,服務 D 執行耗時為 500ms,以此類推,理想情況下在整個呼叫鏈裡都採用相同的超時傳遞機制。

如果不採用超時傳遞機制,那麼就會出現如下情況:

  1. 服務 A 給服務 B 傳送一個請求,設定的超時時間為 3s
  2. 服務 B 處理請求耗時為 2s,並且繼續請求服務 C
  3. 如果使用了超時傳遞那麼服務 C 的超時時間應該為 1s,但這裡沒有采用超時傳遞所以超時時間為在配置中寫死的 3s
  4. 服務 C 繼續執行耗時為 2s,其實這時候最上層設定的超時時間已截止,如下的請求無意義
  5. 繼續請求服務 D

如果服務 B 採用了超時傳遞機制,那麼在服務 C 就應該立刻放棄該請求,因為已經到了截止時間,客戶端可能已經報錯。我們在設定超時傳遞的時候一般會將傳遞出去的截止時間減少一點,比如 100 毫秒,以便將網路傳輸時間和客戶端收到回覆之後的處理時間考慮在內。

程式內超時傳遞

不光服務間需要超時傳遞程式內同樣需要進行超時傳遞,比如在一個程式內序列的呼叫了 Mysql、Redis 和服務 B,設定總的請求時間為 3s,請求 Mysql 耗時 1s 後再次請求 Redis 這時的超時時間為 2s,Redis 執行耗時 500ms 再請求服務 B 這時候超時時間為 1.5s,因為我們的每個中介軟體或者服務都會在配置檔案中設定一個固定的超時時間,我們需要取剩餘時間和設定時間中的最小值。

context 實現超時傳遞

context 原理非常簡單,但功能卻非常強大,go 的標準庫也都已實現了對 context 的支援,各種開源的框架也實現了對 context 的支援,context 已然成為了標準,超時傳遞也依賴 context 來實現。

我們一般在服務的最上層通過設定初始 context 進行超時控制傳遞,比如設定超時時間為 3s

ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()

當進行 context 傳遞的時候,比如上圖中請求 Redis,那麼通過如下方式獲取剩餘時間,然後對比 Redis 設定的超時時間取較小的時間

dl, ok := ctx.Deadline()
timeout := time.Now().Add(time.Second * 3)
if ok := dl.Before(timeout); ok {
    timeout = dl
}

服務間超時傳遞主要是指 RPC 呼叫時候的超時傳遞,對於 gRPC 來說並不需要要我們做額外的處理,gRPC 本身就支援超時傳遞,原理和上面差不多,是通過 metadata 進行傳遞,最終會被轉化為 grpc-timeout 的值,如下程式碼所示 grpc-go/internal/transport/handler_server.go:79

if v := r.Header.Get("grpc-timeout"); v != "" {
        to, err := decodeTimeout(v)
        if err != nil {
            return nil, status.Errorf(codes.Internal, "malformed time-out: %v", err)
        }
        st.timeoutSet = true
        st.timeout = to
}

超時傳遞是保證服務穩定性的一道重要防線,原理和實現都非常簡單,你們的框架中實現了超時傳遞了嗎?如果沒有的話就趕緊動起手來吧。

go-zero 中的超時傳遞

go-zero 中可以通過配置檔案中的 Timeout 配置 api gatewayrpc 服務的超時,並且會在服務間自動傳遞。

之前的 一文搞懂如何實現 Go 超時控制 裡面有講解超時控制如何使用。

參考

《SRE:Google 運維解密》

專案地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar/fork 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。

更多原創文章乾貨分享,請關注公眾號
  • 你瞭解微服務的超時傳遞嗎?
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章