Golang服務中context超時處理的方法詳解
前言
公司運行的服務代碼中,隨處可見各種各樣的日志信息,其中大多數(shù)是用來記錄各種異常的日志,一方面,當出現(xiàn)問題時,通過日志我們可以快速的定位引發(fā)問題的原因;另外我們可以通過日志平臺,對一些錯誤級別比較高的日志進行監(jiān)控,從而能夠快速響應系統(tǒng)可能會出現(xiàn)的問題。
起因:日志告警引發(fā)的思考
雖然日志告警很有用,但如果告警次數(shù)過于頻繁,反而會降低開發(fā)人員對于系統(tǒng)異常的敏感度,使得告警變得毫無意義。因此,我們需要對告警進行治理。最近,由于一次治理線上頻發(fā)的超時告警,使得筆者開始思考起context deadline exceed異常的問題。
什么是context
在Go語言中,Context是一個非常重要的概念,它存在于一個完整的業(yè)務生命周期內(nèi),Context類型是一個接口類型,它定義了四個方法:Deadline()、Done()、Err()和Value()。其中,Deadline()方法返回context的截止日期,Done()方法返回一個只讀的channel,當Context被取消或超時時,該channel會被關閉,Err()方法返回Context被取消的原因,Value()方法返回Context中與key相關聯(lián)的值。
context的作用
在實際應用中,我們可以使用Context包來傳遞請求的元數(shù)據(jù),例如請求ID、超時信息等等。此外,我們還可以使用context包來控制goroutine的生命周期(最常見的),例如在HTTP請求處理程序中,我們可以使用context包來取消正在處理的請求。
可以說,我們的服務里,隨處可見攜帶context參數(shù)的方法。
context超時之后
先來看一段例子
package main
import (
"context"
"fmt"
"time"
)
func timeConsuming(ctx context.Context, costTime int) {
ctx.Done()
for i := 1; i <= costTime; i++ {
// 模擬一些耗時操作
time.Sleep(1 * time.Second)
fmt.Printf("協(xié)程正在運行第%v次...\n", i)
}
}
func main() {
// 創(chuàng)建一個父級 context,設置超時時間為 5 秒鐘
parentCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 創(chuàng)建一個子級 context,用于控制協(xié)程
childCtx, childCancel := context.WithCancel(parentCtx)
defer childCancel()
costTime := 5 // 模擬耗時 5 秒鐘
// 啟動一個協(xié)程
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
// 如果收到取消信號,退出協(xié)程
fmt.Println("協(xié)程退出")
return
case <-time.After(15 * time.Second):
fmt.Println("協(xié)程超時")
default:
timeConsuming(childCtx, costTime)
}
}
}(childCtx)
// 等待 3 秒鐘,然后取消子級 context
time.Sleep(3 * time.Second)
fmt.Println("取消協(xié)程")
childCancel()
// 繼續(xù)等待 3 秒鐘,模擬主協(xié)程的一些其他操作
time.Sleep(3 * time.Second)
fmt.Println("主協(xié)程退出")
}
上面代碼的執(zhí)行結果如下
協(xié)程正在運行第1次...
協(xié)程正在運行第2次...
取消協(xié)程
協(xié)程正在運行第3次...
協(xié)程正在運行第4次...
協(xié)程正在運行第5次...
協(xié)程退出
主協(xié)程退出
雖然說Context可以用來管理goroutine,但是可以看到,Context超時之后,goroutine仍然在執(zhí)行完成之后才會退出,Context無法真正做到強制殺死goroutine
回到文章最開始提到的線上超時告警頻發(fā)的問題,經(jīng)過排查我們發(fā)現(xiàn),一波超時告警的出現(xiàn)實際上只是幾條請求引起的(都是同一個trace_id)。究其原因,是我們下游的服務在單次業(yè)務請求中,會與很多第三方接口發(fā)生交互(在本篇文章的case是并發(fā)調用redis),而在業(yè)務執(zhí)行到并發(fā)調用redis之前,業(yè)務邏輯就已經(jīng)發(fā)生了超時。
超時后,上游調用端不再繼續(xù)等待響應,直接返回了超時異常。
前面已經(jīng)提到過,goroutine是無法強制殺死的,此時goroutine攜帶著已經(jīng)超時的context依舊在執(zhí)行著業(yè)務邏輯,在執(zhí)行到并發(fā)調用redis時,由于context已經(jīng)超時,調用無一例外的全部拋出超時錯誤(實際上并未真正發(fā)生調用redis,redis客戶端代碼在調用前判斷了context的狀態(tài)),
從而導致個位數(shù)的超時請求卻引起了大量日志的超時告警。
...
//If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
if ctx.Err() != nil { // 這里拋出了context deadline exceeded 異常
return nil, ctx.Err()
}
...
繼續(xù)執(zhí)行 or 中斷
知道了問題,其實處理起來就比較容易了,我們將context的狀態(tài)的判斷改寫到了合適的位置(在一些耗時的節(jié)點之間判斷了context的狀態(tài),如果判斷超時,則直接結束后續(xù)的業(yè)務流程)
日志告警清凈了!
但是,這樣的處理方式具有普適性嗎?可以思考一下,在某些超時的情況中,即便上游已經(jīng)返回了超時異常,我們?nèi)匀幌M掠文軌驅⑦@次業(yè)務完整的執(zhí)行完。
舉一個例子,下游在執(zhí)行完返回之前,會將本次執(zhí)行的結果進行緩存。而上游在調用下游之前,也會去取緩存,取到了就直接返回(假設上下游服務共用一套緩存集群)。假如某些請求耗時比較久,而且我們在判斷請求超時之后直接中斷下游任務的執(zhí)行,那么,緩存將永遠不會生成,上游后續(xù)的調用依舊會超時。這種情況下,即便是超時了,我們也希望下游任務能夠完整執(zhí)行,并生成緩存,后續(xù)上游就可以直接拿到業(yè)務結果返回,避免大量耗時的調用。
最后
本篇描述的本身是一個極為常見的問題及處理方案。但是在平時處理問題的過程中,如果勤加思考,仍然會有所收獲和提升。
以上就是Golang服務中context超時處理的方法詳解的詳細內(nèi)容,更多關于Golang context超時處理的資料請關注腳本之家其它相關文章!
相關文章
Go?處理大數(shù)組使用?for?range?和?for?循環(huán)的區(qū)別
這篇文章主要介紹了Go處理大數(shù)組使用for?range和for循環(huán)的區(qū)別,對于遍歷大數(shù)組而言,for循環(huán)能比for?range循環(huán)更高效與穩(wěn)定,這一點在數(shù)組元素為結構體類型更加明顯,下文具體分析感興趣得小伙伴可以參考一下2022-05-05
golang簡易實現(xiàn)?k8s?的yaml上傳并應用示例方案
這篇文章主要為大家介紹了golang簡易實現(xiàn)?k8s?的yaml上傳并應用示例方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07

