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í)行結(jié)果如下
協(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ā)調(diào)用redis),而在業(yè)務執(zhí)行到并發(fā)調(diào)用redis之前,業(yè)務邏輯就已經(jīng)發(fā)生了超時。
超時后,上游調(diào)用端不再繼續(xù)等待響應,直接返回了超時異常。
前面已經(jīng)提到過,goroutine是無法強制殺死的,此時goroutine攜帶著已經(jīng)超時的context依舊在執(zhí)行著業(yè)務邏輯,在執(zhí)行到并發(fā)調(diào)用redis時,由于context已經(jīng)超時,調(diào)用無一例外的全部拋出超時錯誤(實際上并未真正發(fā)生調(diào)用redis,redis客戶端代碼在調(diào)用前判斷了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),如果判斷超時,則直接結(jié)束后續(xù)的業(yè)務流程)
日志告警清凈了!
但是,這樣的處理方式具有普適性嗎?可以思考一下,在某些超時的情況中,即便上游已經(jīng)返回了超時異常,我們?nèi)匀幌M掠文軌驅(qū)⑦@次業(yè)務完整的執(zhí)行完。
舉一個例子,下游在執(zhí)行完返回之前,會將本次執(zhí)行的結(jié)果進行緩存。而上游在調(diào)用下游之前,也會去取緩存,取到了就直接返回(假設上下游服務共用一套緩存集群)。假如某些請求耗時比較久,而且我們在判斷請求超時之后直接中斷下游任務的執(zhí)行,那么,緩存將永遠不會生成,上游后續(xù)的調(diào)用依舊會超時。這種情況下,即便是超時了,我們也希望下游任務能夠完整執(zhí)行,并生成緩存,后續(xù)上游就可以直接拿到業(yè)務結(jié)果返回,避免大量耗時的調(diào)用。
最后
本篇描述的本身是一個極為常見的問題及處理方案。但是在平時處理問題的過程中,如果勤加思考,仍然會有所收獲和提升。
以上就是Golang服務中context超時處理的方法詳解的詳細內(nèi)容,更多關于Golang context超時處理的資料請關注腳本之家其它相關文章!
相關文章
golang 結(jié)構(gòu)體初始化時賦值格式介紹
這篇文章主要介紹了golang 結(jié)構(gòu)體初始化時賦值格式介紹,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12golang?gorm的關系關聯(lián)實現(xiàn)示例
這篇文章主要為大家介紹了golang?gorm的關系關聯(lián)實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04Go語言結(jié)合validator包實現(xiàn)表單驗證
在現(xiàn)代?Web?開發(fā)中,表單驗證和錯誤處理是至關重要的環(huán)節(jié),本文將演示如何使用?Go?語言的?Gin?框架結(jié)合?validator?包,實現(xiàn)高級的表單驗證功能,需要的可以參考下2024-11-11