保姆級教程,golang熔斷實踐
這里是Z哥的個人公眾號
當前處于「隨機更」狀態(tài)
何時恢復(fù)「周更」未知……
我的第「231」篇原創(chuàng)敬上
大家好,我是Z哥。 不得不說,我現(xiàn)在已經(jīng)從「周更」變成「隨機更」了,我自己都不知道哪天能更新,工作實在太忙了。

好了快速進入正題,最近團隊里的一個重點工作是增加系統(tǒng)的穩(wěn)定性和可用性,因此避不開的話題就是熔斷、降級、限流。 這三個概念我在之前寫的分布式系統(tǒng)系列中也有提及,有興趣的可以在文末移步到之前的文章中閱讀。
-
《 分布式系統(tǒng)關(guān)注點(8)——如何在到處是“雷”的系統(tǒng)中「明哲保身」?這是第一招 》
-
《 分布式系統(tǒng)關(guān)注點(10)——讓你的系統(tǒng)“堅挺不倒”的最后一個大招——「降級」 》
不過今天我們主要聊的是,在 golang 項目中如何落地「熔斷」。 熔斷是一種通用能力,可以在服務(wù)端做也可以在客戶端做。我們的項目中大多數(shù)都基于 go-zero 框架實現(xiàn),而使用 go-zero 框架實現(xiàn)的項目自帶服務(wù)端熔斷能力,所以本文的目的是闡述如何在客戶端側(cè)實現(xiàn)熔斷機制。 由于 go-zero 內(nèi)置熔斷器能力,因此我們優(yōu)先想到的是能否直接使用 go-zero 框架內(nèi)的熔斷器組件,如果可以滿足需求的話,也避免了增加額外的外部依賴。 扒開 go-zero 的源碼就能找到它的熔斷器使用,以下是在使用 go-zero 構(gòu)建 http 的服務(wù)端時,其通過 AOP 的方式利用 Handler 來注入熔斷器的代碼。這部分代碼現(xiàn)在不用深究,等看完本篇文章,你再回頭來看很容易知道它寫的是什么意思。
// BreakerHandler returns a break circuit middleware.
func BreakerHandler(method, path string, metrics *stat.Metrics) func(http.Handler) http.Handler {
brk := breaker.NewBreaker(breaker.WithName(strings.Join([]string{method, path}, breakerSeparator)))
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promise, err := brk.Allow()
if err != nil {
metrics.AddDrop()
logx.Errorf("[http] dropped, %s - %s - %s",
r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent())
w.WriteHeader(http.StatusServiceUnavailable)
return
}
cw := &response.WithCodeResponseWriter{Writer: w}
defer func() {
if cw.Code < http.StatusInternalServerError {
promise.Accept()
} else {
promise.Reject(fmt.Sprintf("%d %s", cw.Code, http.StatusText(cw.Code)))
}
}()
next.ServeHTTP(cw, r)
})
??}
通過以上代碼繼續(xù)深入源碼,我們發(fā)現(xiàn)了go-zero框架中的熔斷器模塊(https://github.com/zeromicro/go-zero/tree/master/core/breaker)其底層使用了 google 的熔斷器思路來實現(xiàn)。
可能說起熔斷器,很多人腦子里第一印象是 netflix的 hystrix 。但是我認為 google 的思路更棒一些,兩者的區(qū)別從效果來說就是 google 的方案自適應(yīng)能力更強。因為 hystrix 中使用三種狀態(tài)來控制,當狀態(tài)為 open 期間,所有請求都會直接被攔截,相對更粗暴一些。為了便于理解兩者的不同,我用“水管”來比喻畫了一張圖。

好了,那么 go-zero 中的熔斷器該怎么使用呢? 首先,使用它的途徑有三種方式,分別是: 01 ? 持有熔斷器實例 + 負責(zé)管理實例的生命周期
brk := breaker.NewBreaker()
brk.Do(func() error {
//do something.
return nil
})
02 ? 持有熔斷器實例 + 不管理實例的生命周期 將管理每個熔斷器實例的職責(zé)交由框架內(nèi)的「池」來實現(xiàn)。
brk := breaker.GetBreaker(“起個名字”)
brk.Do(func() error {
//do something.
return nil
})
03 ?? 不持有熔斷器實例 直接使用非實例的 Do() 函數(shù),需要定義一個標識 name,這個 name 就是熔斷器的唯一標識。
breaker.Do(“起個名字”, func() error {
//do something.
return nil
})
以上示例代碼中的 Do() 函數(shù)中的 func() error,就是需要在熔斷器的保護下執(zhí)行的具體代碼。 運行以下代碼,就可以看到熔斷器生效的效果:
for i := 0; i < 20; i++ {
err = breaker.Do("func", func() error {
return errors.New(strconv.Itoa(i))
})
fmt.Println("func", err)
}
輸出:
func 0
func 1
func 2
func 3
func 4
func 5
func 6
func 7
func circuit breaker is open
func circuit breaker is open
func circuit breaker is open
func 11
func circuit breaker is open
func circuit breaker is open
func circuit breaker is open
func 15
func 16
func 17
func 18
func circuit breaker is open
以上的輸出內(nèi)容不是固定的,每次運行的結(jié)果都不同(為什么不同后面會提到原因)。其中“func circuit breaker is open”表示 Do()函數(shù)中的 func() error 直接被熔斷器攔截了,沒有實際執(zhí)行。 上面是最基本的使用方式,除此之外,go-zero 封裝的 breaker 還提供以下幾個能力:
- 熔斷器外的代碼實現(xiàn)熔斷
- 主動讓熔斷器失效
- 自定義計數(shù)規(guī)則
- 觸發(fā)熔斷時的回調(diào)函數(shù)
接下來我們來一個個說下。
01 ? 熔斷器外的代碼實現(xiàn)熔斷 前面的三種使用方式中,Do() 函數(shù)的作用是將需要執(zhí)行的代碼放到熔斷器內(nèi)執(zhí)行,而有時候我們可能不便將代碼放到熔斷器內(nèi),但是也想實現(xiàn)熔斷的能力可以嗎?當然可以。 breaker 對象暴露了一個Allow() (Promise, error) 函數(shù),返回一個 Promise 對象。
Allow() (Promise, error)
- 可以通過直接操作 promise.Accept() 實現(xiàn)前面示例代碼中 Do()函數(shù)中的 func()執(zhí)行后返回的 err == nil 的效果
- 也可以通過 promise.Reject(reason string) 實現(xiàn) err != nil 的效果。
前提是,你得使用前兩種持有 breaker 實例的方式。 go-zero 實現(xiàn)服務(wù)端熔斷的 BreakerHandler 就是利用這個機制來實現(xiàn)的,根據(jù)返回的 HttpCode 決定請求算成功還是失敗(前面貼的第一段代碼中的 17~21 行)。
02 ? 主動讓熔斷器失效 如果你使用熔斷器的方式是前面提到的方式二和方式三,那么可以通過調(diào)用下面的函數(shù),將「池」中的 breaker 實例移除。這樣的話,下次申請獲取相同 name 的熔斷器時會重新實例化一個新的 breaker,因此間接達到了清空計數(shù)器數(shù)字的效果。
breaker.NoBreakerFor("起個名字")
在前面熔斷器生效的代碼基礎(chǔ)上,增加三行代碼,就能看到不會再出現(xiàn)“func circuit breaker is open”了。
for i := 0; i < 20; i++ {
err = breaker.Do("func", func() error {
return errors.New(strconv.Itoa(i))
})
fmt.Println("func", err)
if i%5 == 0 {
breaker.NoBreakerFor("func")
}
}
03 ? 自定義計數(shù)規(guī)則 在講自定義計數(shù)規(guī)則之前先得了解一下 googleBreaker 的實現(xiàn)原理。googleBreaker 的底層實現(xiàn)基于一個「客戶端請求拒絕概率」的公式:

- requests: 發(fā)起請求的總數(shù)
- accepts: 后端接受的請求數(shù)
- K: 一般建議該值在1.1~2之間。 數(shù)字越小觸發(fā)熔斷的概率越高,反之則越低。 如果K=2,意味著我們認為每接受 10 個請求,后端正常情況下最多只會拒絕 5 個請求,如果發(fā)現(xiàn)拒絕了6個,就觸發(fā)熔斷。
在 go-zero 提供的 breaker 實現(xiàn)中,基于上面的公式增加了兩處微調(diào)。 第一處是,為了避免極端情況下發(fā)起第一次請求就出現(xiàn)失敗而導(dǎo)致觸發(fā)熔斷,在 go-zero 的代碼中針對上面公式中的「分子」增加了一個 protection 常量,該值固定為 5,因此分子部分實際在代碼中是 requests - protection - K * accepts。 第二處是,當公式計算的結(jié)果 >0 時,不會直接觸發(fā)熔斷,而是會與一個半開半閉區(qū)間 [0.0,1.0) 的偽隨機數(shù)對比,如果大于這個偽隨機數(shù)則該次請求觸發(fā)熔斷。 針對上面公式的中,涉及到的計數(shù)的變量是 requests 和 accepts。默認的計數(shù)規(guī)則是:如果 func() 執(zhí)行返回的 err == nil,則 requests+1,accepts+1;否則 requests +1,accepts 不變。 有時候,有些 error 我們可能不希望將其視作「不可用」的信號,因此,我們可以通過使用以下函數(shù)代替 Do(req func() error) error?
DoWithAcceptable(req func() error, acceptable Acceptable) error
該函數(shù)多了一個 Acceptable 對象,該對象是一個函數(shù),用于判斷 error 是否是可忽略的:
- 返回 true 表示忽略,效果等價于 func() 執(zhí)行返回的 err == nil 的情況
- 返回 false 則等價于 func() 執(zhí)行返回的 err != nil 的情況。
你可以試試運行以下代碼:
for i := 0; i < 20; i++ {
err = breaker.DoWithAcceptable("DoWithAcceptable", func() error {
return errors.New("acceptable")
}, func(err error) bool {
return err == nil || err.Error() == "acceptable"
})
fmt.Println(err)
}
你看不到表示觸發(fā)熔斷的“circuit breaker is open”字眼,都是“acceptable”。
04 ? 觸發(fā)熔斷時的回調(diào)函數(shù) 當某次 func() 的執(zhí)行被熔斷器攔截時,允許觸發(fā)回調(diào)(callback)函數(shù),以便外部調(diào)用方感知到這個事件,并基于此做一些其它的事情。比如使用降級方案來代替原 func() 的實現(xiàn)。 要使用該能力,需要調(diào)用以下函數(shù)代替 Do() 函數(shù):
DoWithFallback(req func() error, fallback func(err error) error) error
該函數(shù)多了一個 fallback 的 func()。當某次請求由于觸發(fā)熔斷器導(dǎo)致被攔截時會被觸發(fā)。觸發(fā)方式是 sync 的,且 fallback 函數(shù)中返回的 err 即為調(diào)用方接收到 DoWithFallback 函數(shù)的返回值。直接上源碼可能更好理解:

其實還有一個函數(shù)
DoWithFallbackAcceptable(req func() error, fallback func(err error) error,
acceptable Acceptable) error
從名字也能看出來,它同時支持上面提到的 03 和 04 能力。
到此為止,相信你應(yīng)該會用這個熔斷器了。
可能有些想更進一步的小伙伴會問,熔斷器的觸發(fā)策略除了計數(shù)規(guī)則之外,其它的規(guī)則可以自定義嗎? 很遺憾,目前框架沒有暴露相關(guān)的參數(shù)出來,都是在代碼中固定寫死的常量。除了前面提到的 protection ,還有 3 個常量與熔斷器的觸發(fā)策略相關(guān)。
window = time.Second * 10
buckets = 40
k = 1.5
protection = 5
K 的含義前面有提到過,主要講一下 window 和 buckets 變量的作用。
googleBreaker 的底層使用了滑動窗口算法,這兩個變量是用來定義滑動窗口的:

好了,總結(jié)一下。 今天呢,Z 哥帶你深入剖析了一下 go-zero 框架中的熔斷器,以及教你如何使用它。 首先,使用熔斷器的方式有三種:
- 持有熔斷器實例 + 負責(zé)管理實例的生命周期。
- 持有熔斷器實例 + 不管理實例的生命周期。
- 不持有熔斷器實例。
其次,熔斷器總共提供 6 種能力:
- 最基礎(chǔ)的,在熔斷器的保護下執(zhí)行代碼:Do(req func() error) error
- 熔斷器外的代碼實現(xiàn)熔斷:Allow() (Promise, error) + promise.Accept() / promise.Reject(reason string)
- 讓「池」里的熔斷器失效:breaker.NoBreakerFor(name string)
- 自定義計數(shù)規(guī)則:DoWithAcceptable(req func() error, acceptable Acceptable) error
- 觸發(fā)熔斷時的回調(diào)函數(shù):DoWithFallback(req func() error, fallback func(err error) error) error
- 自定義計數(shù)規(guī)則+觸發(fā)熔斷時的回調(diào)函數(shù): DoWithFallbackAcceptable(req func() error, fallback func(err error) error,? acceptable Acceptable) error
推薦閱讀:
原創(chuàng)不易,如果你覺得這篇文章還不錯,就「 點贊 」或者「在看」一下吧,鼓勵我的創(chuàng)作 :)
也可以分享我的公眾號名片給有需要的朋友們。
如果你有關(guān)于軟件架構(gòu)、分布式系統(tǒng)、產(chǎn)品、運營的困惑
可以試試點擊「閱讀原文」
評論
圖片
表情
