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

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

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

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

讓我們構建一個 Netflixisc 應用程序。其中一個微服務負責提供feed頁面的電影服務。此服務還依賴于推薦服務為用戶提供適當的推薦。
// 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))
}
推薦服務暴露一個路由接口 /recommendations,它返回一個推薦電影列表,同時每 500 毫秒打印一次 goroutine 的數量。
// 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
}
電影服務暴露一個路由 /movies,它返回電影列表和推薦列表。為了獲取推薦,它反過來調用上游的推薦服務。
通過此設置,讓我們以每秒 100 個請求的速率訪問電影服務,持續(xù) 3 秒鐘。在 99% 的毫秒范圍內,我們可以獲得 100% 的成功。這是預期的,因為只提供靜態(tài)數據。
現在,假設推薦服務的響應時間過長,并在 recoHandler 添加20秒的等待時間,然后重新進行測試。成功率會下降,而響應時間也會開始受到影響。此外,在測試期間阻塞在推薦服務上的goroutine數量將急劇增加。
推薦服務的停工時間影響了終端用戶,因為本來可以提供給他的電影feed列表都沒有提供。這正是級聯(lián)故障對我們的系統(tǒng)造成的影響。
熔斷器救援
熔斷器是一個非常簡單但相當重要的概念,因為它可以讓我們保持服務的高可用性。熔斷器有三種狀態(tài):
Closed State 關閉狀態(tài)
關閉狀態(tài)是指數據通過的時候連接處關閉的狀態(tài)。這是我們的理想狀態(tài),其中上游服務正如預期的那樣工作。

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

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

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

初始狀態(tài)下熔斷器是關閉的,當故障超過配置的閾值,則會打開 熔斷器是打開狀態(tài),在經過一段熔斷時間后,部分會打開 如果熔斷器是半開的,它可以 再次打開,如果允許通過的請求也失敗了 關閉,如果允許通過的請求成功響應
熔斷器在 Golang 的應用
雖然有多個庫可供選擇,但最常用的是 hystrix[1]。正如文檔建議的那樣,hystrix 是 Netflix 設計的一個延遲和容錯庫,用于隔離遠程系統(tǒng)、服務和第三方庫的訪問,阻止級聯(lián)故障,并在不可避免的故障發(fā)生的復雜分布式系統(tǒng)中實現恢復能力。
Hystrix 熔斷器的實現取決于以下配置:
超時 ー 上游服務響應的等待時間 最大并發(fā)請求 ー 上游服務允許調用的最大并發(fā) 請求容量閾值 ー 在熔斷之前的請求數,斷路器在需要更改狀態(tài)時無法評估的請求數量 睡眠窗口 ー 開放狀態(tài)與半開放狀態(tài)之間的延遲時間 誤差百分比閾值ー熔斷器短路時的誤差百分比閾值
接下來讓我們在電影和推薦示例中使用它,并在獲取推薦時實現熔斷器模式。
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,您還可以在熔斷器打開時實現兜底邏輯。這種邏輯可能因情況而異。如果熔斷器打開,則從緩存中獲取。
使用這個更新的邏輯,讓我們嘗試以每秒100個請求的速率重新攻擊 3 秒鐘。
哇! !100% 的成功率,在打開的情況下,我們只提供 Feed 和返回 0個推薦。此外,由于每當熔斷器熔斷,我們不再調用上游服務,因此推薦服務不會不堪重負,阻塞的 goroutine 數量不會像以前那么多。
擴展閱讀
我的建議:
關于 Netflix Hystrix[2] Hystrix 是怎樣工作的?[3] Hystrix bucketing[4]

參考資料
hystrix: https://github.com/afex/hystrix-go/hystrix
[2]關于 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
推薦閱讀
