微服務元件之限流器與熔斷器

hatlonely發表於2018-06-21

在微服務架構裡面一個很常見的問題就是服務之間的延遲和通訊失敗問題,極端的情況下,甚至會因為某個服務的效能下降或者故障當機,導致訪問超時,層層傳遞,引發雪崩,最終導致整個系統崩潰,而限流器和熔斷器 (這兩個元件都是客戶端的) 能很好的解決這個問題,提高系統的可靠性和穩定性

限流器

限流器,從字面上理解就是用來限制流量,有時候流量突增 (可預期的比如 “雙 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, // 熔斷嘗試恢復時間
    },
)

提供了阻塞和非阻塞兩種使用方式,完整程式碼可以參考如下連結: <https://github.com/hatlonely/hellogolang/blob/master/sample/addservice/cmd/client/main.go>

阻塞使用 Do 方法,返回一個 err

err := hystrix.Do(&quot;addservice&quot;, 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 = &amp;addservice.AddResponse{V: req.A + req.B}
    return nil
})

非阻塞方法使用 Go 方法,返回一個 error 的 channel,建議在有多個資源需要併發訪問的場景下是使用

errc1 := hystrix.Go(&quot;addservice&quot;, 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 &lt;- struct{}{}
    }
    return err
}, nil)

// 有 fallback 處理
errc2 := hystrix.Go(&quot;addservice&quot;, 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 &lt;- struct{}{}
    }
    return err
}, func(err error) error {
    fmt.Println(err)
    res2 = &amp;addservice.AddResponse{V: req.A + req.B}
    success &lt;- struct{}{}
    return nil
})

for i := 0; i &lt; 2; i++ {
    select {
    case &lt;-success:
        fmt.Println(&quot;success&quot;, i)
    case err := &lt;-errc1:
        fmt.Println(&quot;err1:&quot;, err)
    case err := &lt;-errc2:
        // 這個分支永遠不會走到,因為熔斷機制裡面永遠不會返回錯誤
        fmt.Println(&quot;err2:&quot;, err)
    }
}

參考連結

> 轉載請註明出處 > 本文連結:<http://www.hatlonely.com/2018/06/21/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%BB%84%E4%BB%B6%E4%B9%8B%E9%99%90%E6%B5%81%E5%99%A8%E4%B8%8E%E7%86%94%E6%96%AD%E5%99%A8/>

更多原創文章乾貨分享,請關注公眾號
  • 微服務元件之限流器與熔斷器
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章