很多連鎖故障的場景下的一個常見問題是伺服器正在消耗大量資源處理那些早已經超過客戶端截止時間的請求,這樣的結果是,伺服器消耗大量資源沒有做任何有價值的工作,回覆已經超時的請求是沒有任何意義的。
逾時控制可以說是保證服務穩定性的一道重要的防線,它的本質是快速失敗(fail fast),良好的超時控制策略可以盡快清空高延遲的請求,盡快釋放資源避免請求的堆積。
如果一個請求有多個階段,例如由一系列RPC 呼叫組成,那麼我們的服務應該在每個階段開始前檢查截止時間以避免做無用功,也就是檢查是否還有足夠的剩餘時間處理請求。
一個常見的錯誤實作方式是在每個RPC 服務設定一個固定的超時時間,我們應該在每個服務間傳遞超時時間,超時時間可以在服務呼叫的最上層設置,由初始請求觸發的整個RPC 樹會設定相同的絕對截止時間。例如,在服務請求的最上層設定逾時時間為3s,服務A請求服務B,服務B執行耗時為1s,服務B再請求服務C這時逾時時間剩餘2s,服務C執行耗時為1s,這時服務C再請求服務D,服務D執行耗時為500ms,以此類推,理想情況下在整個呼叫鏈裡都採用相同的逾時傳遞機制。
如果不採用超時傳遞機制,那麼就會出現以下情況:
如果服務B採用了超時傳遞機制,那麼在服務C就應該立刻放棄該請求,因為已經到了截止時間,客戶端可能已經報錯。我們在設定超時傳遞的時候一般會將傳遞出去的截止時間減少一點,例如100毫秒,以便將網路傳輸時間和客戶端收到回覆之後的處理時間考慮在內。
不光服務間需要逾時傳遞進程內同樣需要進行逾時傳遞,例如在一個進程內串行的呼叫了Mysql、Redis和服務B,設定總的請求時間為3s,請求Mysql耗時1s後再次請求Redis這時的超時時間為2s,Redis執行耗時500ms再請求服務B這時候超時時間為1.5s,因為我們的每個中間件或服務都會在設定檔中設定固定的逾時時間,我們需要取剩餘時間和設定時間中的最小值。
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 中可以透過設定檔中的Timeout
設定api gateway
和rpc
服務的逾時,並且會在服務間自動傳遞。
之前的 一文搞懂如何實現 Go 逾時控制 裡面有講解逾時控制如何使用。
《SRE:Google運維解密》
github.com /zeromicro/go-zero
歡迎使用go-zero
並star/fork 支持我們!
推薦:《golang教程#》
以上是一文搞懂微服務的超時傳遞的詳細內容。更多資訊請關注PHP中文網其他相關文章!