Go 微服務中的熔斷器和重試

今天我們來討論微服務架構中的自我恢復能力。通常情況下,服務間會通過同步或異步的方式進行通信。我們假定把一個龐大的系統分解成一個個的小塊能將各個服務解耦。管理服務內部的通信可能有點困難了。你可能聽說過這兩個著名的概念:熔斷和重試。

本文是 Go 語言中文網組織的 GCTT 翻譯,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

熔斷器

01

想象一個簡單的場景:用戶發出的請求訪問服務 A 隨後訪問另一個服務 B。我們可以稱 B 是 A 的依賴服務或下游服務。到服務 B 的請求在到達各個實例前會先通過負載均衡器。

後端服務發生系統錯誤的原因有很多,例如慢查詢、network blip 和內存爭用。在這種場景下,如果返回 A 的 response 是 timeout 和 server error,我們的用戶會再試一次。在混亂的局面中我們怎樣來保護下游服務呢?

02

熔斷器可以讓我們對失敗率和資源有更好的控制。熔斷器的設計思路是不等待 TCP 的連接 timeout 快速且優雅地處理 error。這種 fail fast 機制會保護下游的那一層。這種機制最重要的部分就是立刻向調用方返回 response。沒有被 pending request 填充的線程池,沒有 timeout,而且極有可能煩人的調用鏈中斷者會更少。此外,下游服務也有了充足的時間來恢復服務能力。完全杜絕錯誤很難,但是減小失敗的影響範圍是有可能的。

03

通過 hystrix 熔斷器,我們可以採用降級方案,對上游返回降級後的結果。例如,服務 B 可以訪問一個備份服務或 cache,不再訪問原來的服務 C。引入這種降級方案需要集成測試,因爲我們在 happy path(譯註:所謂 happy path,即測試方法的默認場景,沒有異常和錯誤信息。具體可參見 wikipedia[1])可能不會遇到這種網絡模式。

狀態

04

熔斷器有三個主要的狀態:

熔斷器原理

控制熔斷的設置共有 5 個主要參數。

// CommandConfig is used to tune circuit settings at runtime
type CommandConfig struct {
 Timeout                int `json:"timeout"`
 MaxConcurrentRequests  int `json:"max_concurrent_requests"`
 RequestVolumeThreshold int `json:"request_volume_threshold"`
 SleepWindow            int `json:"sleep_window"`
 ErrorPercentThreshold  int `json:"error_percent_threshold"`
}

查看源碼 [2]

可以通過根據兩個服務的 SLA(‎ Service Level Agreement,服務級別協議 [3])來定出閾值。如果在測試時把依賴的其他服務也涉及到了,這些值會得到很好的調整。

一個好的熔斷器的名字應該能精確指出哪個服務連接出了問題。實際上,請求一個服務時可能會有很多個 API endpoint。每一個 endpoint 都應該有一個對應的熔斷器。

生產上的熔斷器

熔斷器通常被放在聚合點上。儘管熔斷器提供了一種 fail-fast 機制,但我們仍然需要確保可選的降級方案可行。如果我們因爲假定需要降級方案的場景出現的可能性很小就不去測試它,那(之前的努力)就是白費力氣了。即使在最簡單的演練中,我們也要確保閾值是有意義的。以我的個人經驗,把參數配置在 log 中 print 出來對於 debug 很有幫助。

Demo

這段實例代碼用的是 hystrix-go[4] 庫,hystrix Netflix 庫在 Golang 的實現。

package main

import (
 "errors"
 "fmt"
 "log"
 "net/http"
 "os"

 "github.com/afex/hystrix-go/hystrix"
)

const commandName = "producer_api"

func main() {

 hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{
  Timeout:                500,
  MaxConcurrentRequests:  100,
  ErrorPercentThreshold:  50,
  RequestVolumeThreshold: 3,
  SleepWindow:            1000,
 })

 http.HandleFunc("/", logger(handle))
 log.Println("listening on :8080")
 http.ListenAndServe(":8080", nil)
}

func handle(w http.ResponseWriter, r *http.Request) {
 output := make(chan bool, 1)
 errors := hystrix.Go(commandName, func() error {
  // talk to other services
  err := callChargeProducerAPI()
  // err := callWithRetryV1()

  if err == nil {
   output <- true
  }
  return err
 }, nil)

 select {
 case out := <-output:
  // success
  log.Printf("success %v", out)
 case err := <-errors:
  // failure
  log.Printf("failed %s", err)
 }
}

// logger is Handler wrapper function for logging
func logger(fn http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  log.Println(r.URL.Path, r.Method)
  fn(w, r)
 }
}

func callChargeProducerAPI() error {
 fmt.Println(os.Getenv("SERVER_ERROR"))
 if os.Getenv("SERVER_ERROR") == "1" {
  return errors.New("503 error")
 }
 return nil
}

demo 中分別測試了請求調用鏈 closed 和 open 兩種情況:

/* Experiment 1: success path */
// server
go run main.go

// client
for i in $(seq 10); do curl -x '' localhost:8080 ;done

/* Experiment 2: circuit open */
// server
SERVER_ERROR=1 Go run main.go

// client
for i in $(seq 10); do curl -x '' localhost:8080 ;done

查看源碼 [5]

重試問題

在上面的熔斷器模式中,如果服務 B 縮容,會發生什麼?大量已經從 A 發出的請求會返回 5xx error。可能會觸發熔斷器切換到 open 的錯誤報警。因此我們需要重試以防間歇性的 network hiccup 發生。

一段簡單的重試代碼示例:

package main

func callWithRetryV1() (err error) {
 for index := 0; index < 3; index++ {
  // call producer API
  err := callChargeProducerAPI()
  if err != nil {
   return err
  }
 }

 // adding backoff
 // adding jitter
 return nil
}

查看源碼 [6]

重試模式

爲了實現樂觀鎖,我們可以爲不同的服務配置不同的重試次數。因爲立即重試會對下游服務產生爆發性的請求,所以不能用立即重試。加一個 backoff 時間可以緩解下游服務的壓力。一些其他的模式會用一個隨機的 backoff 時間(或在等待時加 jitter)。

一起來看下列算法:

【譯註】關於這幾個算法,可以參考這篇文章 [7] 。Full JitterEqual JitterDe-corredlated 等都是原作者自己定義的名詞。

05

客戶端的數量與服務端的總負載和處理完成時間是有關聯的。爲了確定什麼樣的重試模式最適合你的系統,在客戶端數量增加時很有必要運行基準測試。詳細的實驗過程可以在這篇文章 [8] 中看到。我建議的算法是 de-corredlated Jitter 和 full jitter 選擇其中一個。

兩者結合

Example configuration of both tools

熔斷器被廣泛用在無狀態線上事務系統中,尤其是在聚合點上。重試應該用於調度作業或不被 timeout 約束的 worker。經過深思熟慮後我們可以同時用熔斷器和重試。在大型系統中,service mesh 是一種能更精確地編排不同配置的理想架構。

參考文章

  1. https://github.com/afex/hystrix-go/

  2. https://github.com/eapache/go-resiliency

  3. https://github.com/Netflix/Hystrix/wiki

  4. https://www.awsarchitectureblog.com/2015/03/backoff.html

  5. https://dzone.com/articles/go-microservices-part-11-hystrix-and-resilience

via: https://medium.com/@trongdan_tran/circuit-breaker-and-retry-64830e71d0f6

作者:Dan Tran[9] 譯者:lxbwolf[10] 校對:polaris1119[11]

本文由 GCTT[12] 原創編譯,Go 中文網 [13] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

wikipedia: https://en.wikipedia.org/wiki/Happy_path

[2]

查看源碼: https://gist.githubusercontent.com/aladine/18b38b37f838c1938131f67da0648e92/raw/8f97b8ef0b796ea5355b8f895b4009adfe472668/command.go

[3]

服務級別協議: https://zh.wikipedia.org/zh-hans / 服務級別協議

[4]

hystrix-go: http://github.com/afex/hystrix-go/hystrix

[5]

查看源碼: https://gist.github.com/aladine/48d935c44820508e5bca2f061e3a7c1d/raw/930cdc10c41e8b9b37018f2be36bc421e6df481a/demo.sh

[6]

查看源碼: https://gist.githubusercontent.com/aladine/6d65d1db78b020ef9866e3a8ad2516aa/raw/a4d3b65cc4ef920cdfc7e898c130b92371007785/retry.go

[7]

這篇文章: https://amazonaws-china.com/cn/blogs/architecture/exponential-backoff-and-jitter/

[8]

這篇文章: https://amazonaws-china.com/cn/blogs/architecture/exponential-backoff-and-jitter/

[9]

Dan Tran: https://medium.com/@trongdan_tran

[10]

lxbwolf: https://github.com/lxbwolf

[11]

polaris1119: https://github.com/polaris1119

[12]

GCTT: https://github.com/studygolang/GCTT

[13]

Go 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4-n-mpSPrT8kbxRT1qv0Xw