圖解 Go 微服務(wù)熔斷器
前言
說明
原文中
Circuit Breakers這里翻譯為了熔斷器,大家也可以理解叫為斷路器fallback本文翻譯為兜底,大家也可以理解為容錯補(bǔ)救。
本文翻譯自https://ioshellboy.medium.com/circuit-breakers-in-golang-1779da9b001,由于本人翻譯水平有限,翻譯不當(dāng)之處煩請指出。希望大家看了這篇文章能有所幫助。感謝捧場。
什么是熔斷器
當(dāng)你看到 “熔斷器” 這個術(shù)語時,你會想到什么呢?

從圖片字面意思理解是使用一個錘子破壞了一個電路。
我們一般都會在自己家里安裝熔斷器,以阻止異常的電流從電網(wǎng)流向家里。在開始“微服務(wù)的熔斷器”之前,讓我們先看看它是如何工作的。

如上圖所示,一個典型的熔斷器裝置有 2 個主要部件:
用火線緊緊包裹的軟鐵芯 觸體。只要接觸點能夠形成一個連接點,電流就會從外部電源流向我們的房子。相反,如果連接斷開,電流就停止流動。
當(dāng)電流通過纏繞在軟鐵芯周圍的導(dǎo)線時,軟鐵芯就像一塊電磁鐵,當(dāng)流過它的電流高于預(yù)期的安培時,電磁鐵就會變得強(qiáng)大到足以吸引鄰近的觸點,從而導(dǎo)致短路。
你一定在想,這與微服務(wù)架構(gòu)有什么關(guān)系呢?在我看來,這是高度相關(guān)的,正如我們下面將要看到的!
微服務(wù)架構(gòu)中的級聯(lián)故障
微服務(wù)架構(gòu)已經(jīng)很好地取代了單體架構(gòu),但是為了使我們的系統(tǒng)具有高度的彈性,我們還需要解決一些關(guān)鍵問題。
微服務(wù)的一個問題是級聯(lián)故障。舉一個例子來更好地理解它。

在上圖中,參與者調(diào)用我們的主服務(wù),它依賴于上游服務(wù)——A,B,C。現(xiàn)在假定,服務(wù) A 是一個讀取量較大的系統(tǒng),它依賴于數(shù)據(jù)庫。這個數(shù)據(jù)庫有其自身的局限性,并且在過載時,可能導(dǎo)致連接重置。這個問題不僅會影響服務(wù) A 的性能,還會影響主服務(wù)。
這就是人們所說的“一塊臭肉壞了整鍋湯”,喝過這鍋湯的人肯定會有同感。下面讓我們用一個例子來驗證這一點。

讓我們構(gòu)建一個 Netflixisc 應(yīng)用程序。其中一個微服務(wù)負(fù)責(zé)提供feed頁面的電影服務(wù)。此服務(wù)還依賴于推薦服務(wù)為用戶提供適當(dāng)?shù)耐扑]。
// Recommendation Service
func main() {
logGoroutines()
http.HandleFunc("/recommendations", recoHandler)
log.Fatal(http.ListenAndServe(":9090", nil))
}
func logGoroutines() {
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
fmt.Printf("\n%v - %v", t, runtime.NumGoroutine())
}
}
}()
}
func recoHandler(w http.ResponseWriter, r *http.Request) {
a := `{"movies": ["Few Angry Men", "Pride & Prejudice"]}`
w.Write([]byte(a))
}
推薦服務(wù)暴露一個路由接口 /recommendations,它返回一個推薦電影列表,同時每 500 毫秒打印一次 goroutine 的數(shù)量。
// Movies App
type MovieResponse struct {
Feed []string
Recommendation []string
}
func main() {
http.HandleFunc("/movies", fetchMoviesFeedHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func fetchMoviesFeedHandler(w http.ResponseWriter, r *http.Request) {
mr := MovieResponse{
Feed: []string{"Transformers", "Fault in our stars", "The Old Boy"},
}
rms, err := fetchRecommendations()
if err != nil {
w.WriteHeader(500)
}
mr.Recommendation = rms
bytes, err := json.Marshal(mr)
if err != nil {
w.WriteHeader(500)
}
w.Write(bytes)
}
func fetchRecommendations() ([]string, error) {
resp, err := http.Get("http://localhost:9090/recommendations")
if err != nil {
return []string{}, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []string{}, err
}
var mvsr map[string]interface{}
err = json.Unmarshal(body, &mvsr)
if err != nil {
return []string{}, err
}
mvsb, err := json.Marshal(mvsr["movies"])
if err != nil {
return []string{}, err
}
var mvs []string
err = json.Unmarshal(mvsb, &mvs)
if err != nil {
return []string{}, err
}
return mvs, nil
}
電影服務(wù)暴露一個路由 /movies,它返回電影列表和推薦列表。為了獲取推薦,它反過來調(diào)用上游的推薦服務(wù)。
通過此設(shè)置,讓我們以每秒 100 個請求的速率訪問電影服務(wù),持續(xù) 3 秒鐘。在 99% 的毫秒范圍內(nèi),我們可以獲得 100% 的成功。這是預(yù)期的,因為只提供靜態(tài)數(shù)據(jù)。
現(xiàn)在,假設(shè)推薦服務(wù)的響應(yīng)時間過長,并在 recoHandler 添加20秒的等待時間,然后重新進(jìn)行測試。成功率會下降,而響應(yīng)時間也會開始受到影響。此外,在測試期間阻塞在推薦服務(wù)上的goroutine數(shù)量將急劇增加。
推薦服務(wù)的停工時間影響了終端用戶,因為本來可以提供給他的電影feed列表都沒有提供。這正是級聯(lián)故障對我們的系統(tǒng)造成的影響。
熔斷器救援
熔斷器是一個非常簡單但相當(dāng)重要的概念,因為它可以讓我們保持服務(wù)的高可用性。熔斷器有三種狀態(tài):
Closed State 關(guān)閉狀態(tài)
關(guān)閉狀態(tài)是指數(shù)據(jù)通過的時候連接處關(guān)閉的狀態(tài)。這是我們的理想狀態(tài),其中上游服務(wù)正如預(yù)期的那樣工作。

Open State 開放狀態(tài)
打開狀態(tài)指的是由于上游服務(wù)未按預(yù)期響應(yīng)而導(dǎo)致電路短路的狀態(tài)。這種短路可以避免上游服務(wù)在已經(jīng)掙扎的情況下不堪重負(fù)。此外,下游服務(wù)的業(yè)務(wù)邏輯可以更快地獲得上游可用性狀態(tài)的反饋,而無需等待上游的響應(yīng)。

Half Open State 半開狀態(tài) 
如果熔斷器是打開狀態(tài),我們希望它在上游服務(wù)再次可用時立即關(guān)閉它。雖然你可以通過手動干預(yù)來實現(xiàn),但首選的方法應(yīng)該是在電路最后一次打開,讓一些請求延遲之后通過電路,
如果這些請求請求上游服務(wù)成功,我們就可以安全地接通整個鏈路。

另一方面,如果這些請求失敗,熔斷器仍然處于打開狀態(tài)。
熔斷器的狀態(tài)圖如下:

初始狀態(tài)下熔斷器是關(guān)閉的,當(dāng)故障超過配置的閾值,則會打開 熔斷器是打開狀態(tài),在經(jīng)過一段熔斷時間后,部分會打開 如果熔斷器是半開的,它可以 再次打開,如果允許通過的請求也失敗了 關(guān)閉,如果允許通過的請求成功響應(yīng)
熔斷器在 Golang 的應(yīng)用
雖然有多個庫可供選擇,但最常用的是 hystrix[1]。正如文檔建議的那樣,hystrix 是 Netflix 設(shè)計的一個延遲和容錯庫,用于隔離遠(yuǎn)程系統(tǒng)、服務(wù)和第三方庫的訪問,阻止級聯(lián)故障,并在不可避免的故障發(fā)生的復(fù)雜分布式系統(tǒng)中實現(xiàn)恢復(fù)能力。
Hystrix 熔斷器的實現(xiàn)取決于以下配置:
超時 ー 上游服務(wù)響應(yīng)的等待時間 最大并發(fā)請求 ー 上游服務(wù)允許調(diào)用的最大并發(fā) 請求容量閾值 ー 在熔斷之前的請求數(shù),斷路器在需要更改狀態(tài)時無法評估的請求數(shù)量 睡眠窗口 ー 開放狀態(tài)與半開放狀態(tài)之間的延遲時間 誤差百分比閾值ー熔斷器短路時的誤差百分比閾值
接下來讓我們在電影和推薦示例中使用它,并在獲取推薦時實現(xiàn)熔斷器模式。
var downstreamErrCount int
var circuitOpenErrCount int
func main() {
downstreamErrCount = 0
circuitOpenErrCount = 0
hystrix.ConfigureCommand("recommendation", hystrix.CommandConfig{
Timeout: 100,
RequestVolumeThreshold: 25,
ErrorPercentThreshold: 5,
SleepWindow: 1000,
})
http.HandleFunc("/movies", fetchMoviesFeedHandlerWithCircuitBreaker)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func fetchMoviesFeedHandlerWithCircuitBreaker(w http.ResponseWriter, r *http.Request) {
mr := MovieResponse{
Feed: []string{"Transformers", "Fault in our stars", "The Old Boy"},
}
output := make(chan bool, 1)
errors := hystrix.Go("recommendation", func() error {
// talk to other services
rms, err := fetchRecommendations()
if err != nil {
return err
}
mr.Recommendation = rms
output <- true
return nil
}, func(err error) error {
// 寫你的fallback(兜底)邏輯
return nil
})
select {
case err := <-errors:
if err == hystrix.ErrCircuitOpen {
circuitOpenErrCount = circuitOpenErrCount + 1
} else {
downstreamErrCount = downstreamErrCount + 1
}
case _ = <-output:
}
bytes, err := json.Marshal(mr)
if err != nil {
w.WriteHeader(500)
}
fmt.Printf("\ndownstreamErrCount=%d, circuitOpenErrCount=%d", downstreamErrCount, circuitOpenErrCount)
w.Write(bytes)
}
使用 Hystrix,您還可以在熔斷器打開時實現(xiàn)兜底邏輯。這種邏輯可能因情況而異。如果熔斷器打開,則從緩存中獲取。
使用這個更新的邏輯,讓我們嘗試以每秒100個請求的速率重新攻擊 3 秒鐘。
哇! !100% 的成功率,在打開的情況下,我們只提供 Feed 和返回 0個推薦。此外,由于每當(dāng)熔斷器熔斷,我們不再調(diào)用上游服務(wù),因此推薦服務(wù)不會不堪重負(fù),阻塞的 goroutine 數(shù)量不會像以前那么多。
擴(kuò)展閱讀
我的建議:
關(guān)于 Netflix Hystrix[2] Hystrix 是怎樣工作的?[3] Hystrix bucketing[4]

參考資料
hystrix: https://github.com/afex/hystrix-go/hystrix
[2]關(guān)于 Netflix Hystrix: https://github.com/Netflix/Hystrix/wiki
[3]Hystrix 是怎樣工作的?: https://github.com/Netflix/Hystrix/wiki/How-it-Works
[4]Hystrix bucketing: https://raw.githubusercontent.com/wiki/Netflix/Hystrix/images/circuit-breaker-1280.png
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。
堅持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
