Context這三個(gè)應(yīng)用場(chǎng)景,你知嗎

用戶發(fā)送 開(kāi)始消費(fèi) 請(qǐng)求時(shí):開(kāi)啟多個(gè)協(xié)程開(kāi)始消費(fèi)消息隊(duì)列某個(gè)topic的信息;
用戶發(fā)送 結(jié)束消費(fèi) 請(qǐng)求時(shí):把消費(fèi)中的topic相關(guān)的協(xié)程關(guān)閉掉,結(jié)束消費(fèi);






跨服務(wù)傳遞信息

現(xiàn)在具備一定規(guī)模的互聯(lián)網(wǎng)公司都用微服務(wù)形式讓各系統(tǒng)組合起來(lái)為用戶提供服務(wù),一個(gè)簡(jiǎn)單的業(yè)務(wù)在流程上可能需要十幾個(gè)甚至幾十個(gè)系統(tǒng)間互相調(diào)用。由于每個(gè)系統(tǒng)內(nèi)部的正確性無(wú)法保證,若出現(xiàn)了case,比如用戶反饋積分少發(fā)了,就需要排查這十幾個(gè)系統(tǒng)的日志信息,看問(wèn)題出在哪里。
此處需要一個(gè)ID憑證,ID是請(qǐng)求級(jí)別的,在各個(gè)系統(tǒng)中記錄著與此請(qǐng)求相關(guān)的日志信息,我們把它叫做trace ID。把日志采集并落盤到ES這樣的存儲(chǔ)中,有case時(shí)只需要拿到請(qǐng)求的trace ID就可以把全流程的關(guān)鍵信息還原出來(lái)。如圖所示:

在Golang web服務(wù)中,每個(gè)請(qǐng)求都是開(kāi)一個(gè)協(xié)程去處理的。系統(tǒng)間傳遞信息時(shí),若通信協(xié)議用HTTP,那trace ID等信息可放在HTTP Header中,在web框架的middle層把這些信息存入Context。demo如下:
//?檢測(cè)上游服務(wù)是否傳遞traceID信息,若傳遞了直接使用
if?v,?ok?:=?req.Header["my-awesome-trace-ID"];?ok?{
???traceID?=?v[0]
}?else?{
??//?若沒(méi)傳則用公共庫(kù)生成一個(gè)全局唯一的traceID信息
??traceID?=?GenTraceID()
??req.Header["my-awesome-trace-ID"]?=?[]string{traceID}
}
//?處理完各種請(qǐng)求上下文信息后,把這些信息統(tǒng)一存儲(chǔ)到ctx中,傳遞給業(yè)務(wù)層的對(duì)應(yīng)Handler
ctx?=?context.WithValue(ctx,?ContentKey,?record)
Context處理請(qǐng)求上下文這塊主要用到了WithValue,這個(gè)函數(shù)接收一個(gè)ctx和一對(duì)k-v。把k-v對(duì)存起來(lái)后返回一個(gè)子ctx,這次我們先簡(jiǎn)單介紹其使用場(chǎng)景,下篇文章會(huì)從源碼層面理解這個(gè)函數(shù)。
ctx的生命周期是 伴隨請(qǐng)求開(kāi)始而誕生、請(qǐng)求結(jié)束而終止的。在請(qǐng)求中ctx會(huì)跨越多個(gè)函數(shù)多個(gè)協(xié)程,在打日志時(shí),第一個(gè)參數(shù)預(yù)留給ctx是因?yàn)槿罩編?kù)需要從Context中抽取trace ID等信息,從而記錄下完整的日志。獲取信息時(shí)只需要調(diào)用context的Value方法,demo如下:
//?從Context中獲取traceID,?打到日志里
v?:=?ctx.Value("my-awesome-trace-ID")
這里畫(huà)個(gè)圖幫助理解:

若我們的系統(tǒng)也需要請(qǐng)求第三方服務(wù),同樣應(yīng)把trace ID等信息放入HTTP Header后發(fā)送請(qǐng)求,其他服務(wù)按照同樣的流程接收到trace ID后開(kāi)始內(nèi)部邏輯處理。這樣一個(gè)請(qǐng)求在多個(gè)系統(tǒng)中就通過(guò)trace ID串聯(lián)起了整個(gè)流程。除trace ID外,Context還可以傳遞 URL Path、請(qǐng)求時(shí)間、Caller等信息。




多協(xié)程消費(fèi)demo:
func?main()?{
?//?此協(xié)程負(fù)責(zé)監(jiān)聽(tīng)錯(cuò)誤信息,開(kāi)啟消費(fèi)
?go?func()?{
??for?{
???select?{
???//?code
???}
??}
?}()
?//?此協(xié)程負(fù)責(zé)監(jiān)聽(tīng)re-balance信息,開(kāi)啟消費(fèi)
?go?func()?{
??for?{
???select?{
???//?code
???}
??}
?}()
??//?...
}

func?main()?{
?ctx,?cancel?:=?context.WithCancel(context.Background())
?//?此協(xié)程負(fù)責(zé)監(jiān)聽(tīng)錯(cuò)誤信息,開(kāi)啟消費(fèi)
?go?func()?{
??for?{
???select?{
???case?<-ctx.Done():
????fmt.Println("退出監(jiān)聽(tīng)錯(cuò)誤協(xié)程")
????return
???default:
????fmt.Println("邏輯處理中...")
???}
??}
?}()
?//?此協(xié)程負(fù)責(zé)監(jiān)聽(tīng)re-balance信息,開(kāi)啟消費(fèi)
?go?func()?{
??for?{
???select?{
???case?<-ctx.Done():
????fmt.Println("退出監(jiān)聽(tīng)re-balance協(xié)程")
????return
???default:
????fmt.Println("邏輯處理中...")
???}
??}
?}()
?//?調(diào)用cancelFunc,?結(jié)束消費(fèi)
?cancel()
}

控制協(xié)程關(guān)閉
上面代碼用到了WithCancel方法,調(diào)用它會(huì)返回一個(gè)可被取消的ctx和CancelFunc,需要取消ctx時(shí),調(diào)用cancel函數(shù)即可。context有個(gè)Done方法,這個(gè)方法返回一個(gè)channel,當(dāng)Context被取消時(shí),這個(gè)channel會(huì)被關(guān)閉。消費(fèi)中的協(xié)程通過(guò)select監(jiān)聽(tīng)這個(gè)channel,收到關(guān)閉信號(hào)后一個(gè)return就能結(jié)束消費(fèi)。
CancelFunc可以預(yù)防系統(tǒng)做不必要的工作。比如用戶請(qǐng)求A接口時(shí),A接口內(nèi)部需要請(qǐng)求A database、B cache 、C System獲取各種數(shù)據(jù),把這些數(shù)據(jù)經(jīng)過(guò)計(jì)算后組裝到一起返回給調(diào)用方。這是正常情況的時(shí)序圖:

但如果用戶在訪問(wèn)網(wǎng)站時(shí)覺(jué)得沒(méi)意思,去其他網(wǎng)站了。此時(shí)若你的服務(wù)收到用戶請(qǐng)求后繼續(xù)去訪問(wèn)其他C system、B database就是浪費(fèi)資源。比較符合直覺(jué)的做法是:當(dāng)業(yè)務(wù)請(qǐng)求取消時(shí),你的系統(tǒng)也應(yīng)該停止請(qǐng)求下游系統(tǒng)。前面我們介紹過(guò)context在系統(tǒng)中貫穿請(qǐng)求周期,那么當(dāng)用戶取消訪問(wèn)時(shí),只要context監(jiān)聽(tīng)取消事件并在用戶取消時(shí)發(fā)送取消事件,就可以取消請(qǐng)求了。

這里有份demo代碼,項(xiàng)目啟動(dòng)后,可以用curl localhost:8888訪問(wèn)這個(gè)接口,若1s內(nèi)取消請(qǐng)求,服務(wù)端會(huì)打印出request canceleld,正常情況下,服務(wù)會(huì)返回process finished。
func?main()?{
?http.ListenAndServe(":8888",?http.HandlerFunc(func(w?http.ResponseWriter,?r?*http.Request)?{
??ctx?:=?r.Context()
??fmt.Println("get?request")
??select?{
??case?<-time.After(1?*?time.Second):
???w.Write([]byte("process?finished"))
??case?<-ctx.Done():
???fmt.Println("request?canceleld")
??}
?}))
}
除了用戶中途取消請(qǐng)求的情況,還有一種情況也可以用到cancelFunc:服務(wù)A的返回?cái)?shù)據(jù)依賴服務(wù)B和服務(wù)C的相關(guān)接口,若服務(wù)B或者服務(wù)C掛了,此次請(qǐng)求就算失敗了,沒(méi)必要再訪問(wèn)另一個(gè)服務(wù),此時(shí)也可以用CancelFunc。Demo如下:
func?getUserInfoBySystemA(ctx?context.Context)?error?{
?time.Sleep(100?*?time.Millisecond)
?//?模擬請(qǐng)求出錯(cuò)的情況
?return?errors.New("failed")
}
func?getOrderInfoBySystemB(ctx?context.Context)?{
?select?{
?case?<-time.After(500?*?time.Millisecond):
??fmt.Println("process?finished")
?case?<-ctx.Done():
??fmt.Println("process?cancelled")
?}
}
func?main()?{
?ctx,?cancel?:=?context.WithCancel(context.Background())
?//并發(fā)從兩個(gè)服務(wù)中獲取相關(guān)數(shù)據(jù)
?go?func()?{
??err?:=?getUserInfoBySystemA(ctx)
??if?err?!=?nil?{
???//?發(fā)生錯(cuò)誤,調(diào)用cancelFunc
???cancel()
??}
?}()
?getOrderInfoBySystemB(ctx)
}





控制超時(shí)取消
如果你的服務(wù)對(duì)外承諾的SLA是100ms,但系統(tǒng)依賴的服務(wù)B的HTTP接口有點(diǎn)不穩(wěn)定,有時(shí)50ms就能返回結(jié)果,有時(shí)100ms才能返回結(jié)果,為了保證你服務(wù)的SLA,可以用Context的WithTimeout方法設(shè)置一個(gè)超時(shí)時(shí)間,demo如下:
func?main()?{
?//?設(shè)置超時(shí)時(shí)間100ms
?ctx,?_?:=?context.WithTimeout(context.Background(),?100*time.Millisecond)
?//?構(gòu)建一個(gè)HTTP請(qǐng)求
?req,?_?:=?http.NewRequest(http.MethodGet,?"https://www.baidu.com/",?nil)
?//?把ctx信息傳進(jìn)去
?req?=?req.WithContext(ctx)
?client?:=?&http.Client{}
?//?向百度發(fā)送請(qǐng)求
?res,?err?:=?client.Do(req)
?if?err?!=?nil?{
??fmt.Println("Request?failed:",?err)
? return }
?fmt.Println("Response?received,?status?code:",?res.StatusCode)
}
正常情況下,會(huì)得到這樣的輸出:
Response?received,?status?code:?200
如果我們請(qǐng)求百度超時(shí)了,會(huì)得到這樣的輸出:
Request failed: Get https://www.baidu.com/: context deadline exceeded


歡迎關(guān)注我的公眾號(hào)~
