hystrix-go 服務雪崩解決利器

xuefeng發表於2021-09-22

前言

分散式系統中經常會出現因為某個服務不可用導致整個系統變的不可用,這種情況稱為服務雪崩。

服務雪崩


上圖中, A為服務提供者, B為A的服務呼叫者, C和D是B的服務呼叫者. 當A的不可用,引起B的不可用,並將不可用逐漸放大C和D時, 服務雪崩就形成了

造成服務雪崩的原因

我們把服務分為服務提供者和服務呼叫者,造成服務雪崩的流程如下
1、服務提供者不可用
2、服務呼叫者重試請求服務提供者(呼叫放大)
3、服務呼叫者不可用
4、系統雪崩

服務不可用的原因:
1、硬體故障
2、快取擊穿
3、程式bug(死迴圈、沒出口的遞迴等導致cpu打滿)
4、請求量太大

硬體故障可能為硬體損壞造成的伺服器主機當機, 網路硬體故障造成的服務提供者的不可訪問.
快取擊穿一般發生在快取應用重啟, 所有快取被清空時,以及短時間內大量快取失效時. 大量的快取不命中, 使請求直擊後端,造成服務提供者超負荷執行,引起服務不可用.
在秒殺和大促開始前,如果準備不充分,使用者發起大量請求也會造成服務提供者的不可用.

而形成 重試加大流量 的原因有:

  • 使用者重試
  • 程式碼邏輯重試

在服務提供者不可用後, 使用者由於忍受不了介面上長時間的等待,而不斷重新整理頁面甚至提交表單.
服務呼叫端的會存在大量服務異常後的重試邏輯.
這些重試都會進一步加大請求流量.

最後, 服務呼叫者不可用 產生的主要原因是:

  • 同步等待造成的資源耗盡

當服務呼叫者使用 同步呼叫 時, 會產生大量的等待執行緒佔用系統資源. 一旦執行緒資源被耗盡,服務呼叫者提供的服務也將處於不可用狀態, 於是服務雪崩效應產生了.

Hystrix預防服務雪崩

Hystrix的設計原則包括:

  • 資源隔離

  • 熔斷器

  • 命令模式

資源隔離

在一個高度服務化的系統中,我們實現的一個業務邏輯通常會依賴多個服務,比如:
商品詳情展示服務會依賴商品服務, 價格服務, 商品評論服務. 如圖所示:

呼叫三個依賴服務會共享商品詳情服務的執行緒池. 如果其中的商品評論服務不可用, 就會出現執行緒池裡所有執行緒都因等待響應而被阻塞, 從而造成服務雪崩. 如圖所示:

Hystrix通過將每個依賴服務分配獨立的執行緒池進行資源隔離, 從而避免服務雪崩.
如下圖所示, 當商品評論服務不可用時, 即使商品服務獨立分配的20個執行緒全部處於同步等待狀態,也不會影響其他依賴服務的呼叫.

熔斷器模式

熔斷器模式定義了熔斷器開關相互轉換的邏輯:

服務的健康狀況 = 請求失敗數 / 請求總數.
熔斷器開關由關閉到開啟的狀態轉換是通過當前服務健康狀況和設定閾值比較決定的.

  1. 當熔斷器開關關閉時, 請求被允許通過熔斷器. 如果當前健康狀況高於設定閾值, 開關繼續保持關閉. 如果當前健康狀況低於設定閾值, 開關則切換為開啟狀態.

  2. 當熔斷器開關開啟時, 請求被禁止通過.

  3. 當熔斷器開關處於開啟狀態, 經過一段時間後, 熔斷器會自動進入半開狀態, 這時熔斷器只允許一個請求通過. 當該請求呼叫成功時, 熔斷器恢復到關閉狀態. 若該請求失敗, 熔斷器繼續保持開啟狀態, 接下來的請求被禁止通過.

熔斷器的開關能保證服務呼叫者在呼叫異常服務時, 快速返回結果, 避免大量的同步等待. 並且熔斷器能在一段時間後繼續偵測請求執行結果, 提供恢復服務呼叫的可能.

命令模式

Hystrix使用命令模式(來包裹具體的服務呼叫邏輯(run方法), 並在命令模式中新增了服務呼叫失敗後的降級邏輯(getFallback).
在使用了Command模式構建了服務物件之後, 服務便擁有了熔斷器和執行緒池的功能.

Hystrix的內部處理邏輯

下圖為Hystrix服務呼叫的內部邏輯:

  1. 構建Hystrix的Command物件, 呼叫執行方法.

  2. Hystrix檢查當前服務的熔斷器開關是否開啟, 若開啟, 則執行降級服務getFallback方法.

  3. 若熔斷器開關關閉, 則Hystrix檢查當前服務的執行緒池是否能接收新的請求, 若超過執行緒池已滿, 則執行降級服務getFallback方法.

  4. 若執行緒池接受請求, 則Hystrix開始執行服務呼叫具體邏輯run方法.

  5. 若服務執行失敗, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.

  6. 若服務執行超時, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.

  7. 若服務執行成功, 返回正常結果.

  8. 若服務降級方法getFallback執行成功, 則返回降級結果.

  9. 若服務降級方法getFallback執行失敗, 則丟擲異常.

hystrix go

git地址:github.com/afex/hystrix-go/hystrix

使用

hystrix-go 的使用非常簡單,你可以呼叫它的 Go 或者 Do 方法,只是 Go 方法是非同步的方式。而 Do 方法是同步方式。我們從一個簡單的例子開啟。

_ = hystrix.Do("command", func() error {
        // talk to other services
        _, err := http.Get("https://www.baidu.com/")
        if err != nil {
            fmt.Println("get error:%v",err)
            return err
        }
        return nil
    }, func(err error) error {
        fmt.Printf("handle  error:%v\n", err)
        return nil
    })

Do 函式需要三個引數,第一個引數 commmand 名稱,你可以把每個名稱當成一個獨立當服務,第二個引數是處理正常的邏輯,比如 http 呼叫服務,返回引數是 err。如果處理 | 呼叫失敗,那麼就執行第三個引數邏輯, 我們稱為保底操作。由於服務錯誤率過高導致熔斷器開啟,那麼之後的請求也直接回撥此函式。

既然熔斷器是按照配置的規則而進行是否開啟的操作,那麼我們當然可以設定我們想要的值。

hystrix.ConfigureCommand("command", hystrix.CommandConfig{
        Timeout:                3000,
        MaxConcurrentRequests:  10,
        SleepWindow:            5000,
        RequestVolumeThreshold: 10,
        ErrorPercentThreshold:  30,
    })
    _ = hystrix.Do("wuqq", func() error {
        // talk to other services
        _, err := http.Get("https://www.baidu.com/")
        if err != nil {
            fmt.Println("get error:%v",err)
            return err
        }
        return nil
    }, func(err error) error {
        fmt.Printf("handle  error:%v\n", err)
        return nil
    })

稍微解釋一下上面配置的值含義:

Timeout: 執行 command 的超時時間。
MaxConcurrentRequests:command 的最大併發量 。
SleepWindow:當熔斷器被開啟後,SleepWindow 的時間就是控制過多久後去嘗試服務是否可用了。
RequestVolumeThreshold: 一個統計視窗 10 秒內請求數量。達到這個請求數量後才去判斷是否要開啟熔斷
ErrorPercentThreshold:錯誤百分比,請求數量大於等於 RequestVolumeThreshold 並且錯誤率到達這個百分比後就會啟動熔斷
當然你不設定的話,那麼自動走的預設值。

var (
   // DefaultTimeout is how long to wait for command to complete, in milliseconds
  DefaultTimeout = 1000
  // DefaultMaxConcurrent is how many commands of the same type can run at the same time
  DefaultMaxConcurrent = 10
  // DefaultVolumeThreshold is the minimum number of requests needed before a circuit can be tripped due to health
  DefaultVolumeThreshold = 20
  // DefaultSleepWindow is how long, in milliseconds, to wait after a circuit opens before testing for recovery
  DefaultSleepWindow = 5000
  // DefaultErrorPercentThreshold causes circuits to open once the rolling measure of errors exceeds this percent of requests
  DefaultErrorPercentThreshold = 50
  // DefaultLogger is the default logger that will be used in the Hystrix package. By default prints nothing.
  DefaultLogger = NoopLogger{}
)

測試程式碼

func TestHystrixRequest(t *testing.T) {
    hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
        Timeout:                1000, // 超時時間1秒
        MaxConcurrentRequests:  20,   // 最大併發數量20
        SleepWindow:            1000, // 視窗時間1秒,熔斷開啟1秒後嘗試重試
        RequestVolumeThreshold: 5,    // 10秒鐘請求數量超過5次,啟動熔斷器判斷
        ErrorPercentThreshold:  50,   // 請求數超過5並且錯誤率達到百分之50,開啟熔斷
    })
    wg := new(sync.WaitGroup)

    // 模擬併發10次請求,5次返回err,導致熔斷器開啟
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("請求失敗, err:%s\n", err.Error())
                    return err
                }
                if i % 2 == 0 {
                    return errors.New("測試錯誤!")
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }

    wg.Wait()
    fmt.Println("----------------")

    // 繼續模擬10次請求,熔斷器應該為開啟狀態
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("請求失敗, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()

    fmt.Println("----------------")

    // 睡眠1秒,轉換為半開狀態,併發請求10次,應該會有一個goroutine真正去請求,返回成功,其它請求直接走fallback邏輯
    time.Sleep(1 * time.Second)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("請求失敗, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()

    fmt.Println("----------------")

    // 熔斷器已經由半開轉為關閉狀態,請求應該全部成功
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            _ = hystrix.Do("mycommand", func() error {
                _, err := http.Get("https://baidu.com")
                if err != nil {
                    fmt.Printf("請求失敗, err:%s\n", err.Error())
                    return err
                }
                fmt.Println("success!")
                return nil
            }, func(err error) error {
                fmt.Printf("handle  error:%v\n", err)
                return nil
            })
        }(i)
    }
    wg.Wait()
}

執行結果:
=== RUN TestHystrixRequest
success!
handle error:測試錯誤!
success!
handle error:測試錯誤!
success!
success!
handle error:測試錯誤!
handle error:測試錯誤!
success!
handle error:測試錯誤!


handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open


handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
handle error:hystrix : circuit open
success!


success!
success!
success!
success!
success!
success!
success!
success!
success!
success!

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章