在微服務架構裡面一個很常見的問題就是服務之間的延遲和通信失敗問題,極端的情況下,甚至會因為某個服務的性能下降或者故障當機,導緻通路逾時,層層傳遞,引發雪崩,最終導緻整個系統崩潰,而限流器和熔斷器(這兩個元件都是用戶端的)能很好的解決這個問題,提高系統的可靠性和穩定性
限流器
限流器,從字面上了解就是用來限制流量,有時候流量突增(可預期的比如“雙11”,不可預期的微網誌的熱門話題等),會将後端服務壓垮,甚至直接當機,使用限流器能限制通路後端的流量,起到一個保護作用,被限制的流量,可以根據具體的業務邏輯去處理,直接傳回錯誤或者傳回預設值等等
golang 提供了拓展庫(golang.org/x/time/rate)提供了限流器元件,用法上也很簡單直覺,通過下面這段代碼就可以建立一個限流器
// 每 800ms 産生 1 個 token,最多緩存 1 個 token,如果緩存滿了,新的 token 會被丢棄
limiter := rate.NewLimiter(rate.Every(time.Duration(800)*time.Millisecond), 1)
限流器提供三種使用方式,Allow, Wait, Reserve
Allow: 傳回是否有 token,沒有 token 傳回 false,或者消耗 1 個 token 傳回 true
Wait: 阻塞等待,知道取到 1 個 token
Reserve: 傳回 token 資訊,Allow 其實相當于 Reserve().OK,此外還會傳回需要等待多久才有新的 token
一般使用 Wait 的場景會比較多一些
if err := limiter.Wait(context.Background()); err != nil {
panic(err)
}
// do you business logic
熔斷器
和限流器對依賴服務的保護機制不一樣,熔斷器是當依賴的服務已經出現故障時,為了保證自身服務的正常運作不再通路依賴的服務,防止雪崩效應
熔斷器有三種狀态:
關閉狀态:服務正常,并維護一個失敗率統計,當失敗率達到閥值時,轉到開啟狀态
開啟狀态:服務異常,調用 fallback 函數,一段時間之後,進入半開啟狀态
半開啟裝态:嘗試恢複服務,失敗率高于閥值,進入開啟狀态,低于閥值,進入關閉狀态
github.com/afex/hystrix-go,提供了 go 熔斷器實作,使用上面也很友善,首先建立一個熔斷器
hystrix.ConfigureCommand(
"addservice", // 熔斷器名字,可以用服務名稱命名,一個名字對應一個熔斷器,對應一份熔斷政策
hystrix.CommandConfig{
Timeout: 100, // 逾時時間 100ms
MaxConcurrentRequests: 2, // 最大并發數,超過并發傳回錯誤
RequestVolumeThreshold: 4, // 請求數量的閥值,用這些數量的請求來計算閥值
ErrorPercentThreshold: 25, // 錯誤數量閥值,達到閥值,啟動熔斷
SleepWindow: 1000, // 熔斷嘗試恢複時間
},
)
阻塞使用 Do 方法,傳回一個 err
err := hystrix.Do("addservice", func() error {
// 正常業務邏輯,一般是通路其他資源
var err error
// 設定總體逾時時間 10 ms 逾時
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(50*time.Millisecond))
defer cancel()
res, err = client.Add(
ctx, req,
// 這裡可以再次設定重試次數,重試時間,重試傳回碼
grpc_retry.WithMax(3),
grpc_retry.WithPerRetryTimeout(time.Duration(5)*time.Millisecond),
grpc_retry.WithCodes(codes.DeadlineExceeded),
)
return err
}, func(err error) error {
// 失敗處理邏輯,通路其他資源失敗時,或者處于熔斷開啟狀态時,會調用這段邏輯
// 可以簡單構造一個response傳回,也可以有一定的政策,比如通路備份資源
// 也可以直接傳回 err,這樣不用和遠端失敗的資源通信,防止雪崩
// 這裡因為我們的場景太簡單,是以我們可以在本地在作一個加法就可以了
fmt.Println(err)
res = &addservice.AddResponse{V: req.A + req.B}
return nil
})
非阻塞方法使用 Go 方法,傳回一個 error 的 channel,建議在有多個資源需要并發通路的場景下是使用
errc1 := hystrix.Go("addservice", func() error {
var err error
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(50*time.Millisecond))
defer cancel()
res1, err = client.Add(ctx, req)
if err == nil {
success
}
return err
}, nil)
// 有 fallback 處理
errc2 := hystrix.Go("addservice", func() error {
var err error
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(50*time.Millisecond))
defer cancel()
res2, err = client.Add(ctx, req)
if err == nil {
success
}
return err
}, func(err error) error {
fmt.Println(err)
res2 = &addservice.AddResponse{V: req.A + req.B}
success
return nil
})
for i := 0; i < 2; i++ {
select {
case
fmt.Println("success", i)
case err :=
fmt.Println("err1:", err)
case err :=
// 這個分支永遠不會走到,因為熔斷機制裡面永遠不會傳回錯誤
fmt.Println("err2:", err)
}
}
參考連結