hystrix-go 使用與原理

Remember發表於2020-12-27

圖片

開篇

這周在看內部一個熔斷限流包時,發現它是基於一個開源專案 hystrix-go 實現了,因此有了這篇文章。

Hystrix

Hystrix 是由 Netflex 開發的一款開源元件,提供了基礎的熔斷功能。 Hystrix 將降級的策略封裝在 Command 中,提供了 runfallback 兩個方法,前者表示正常的邏輯,比如微服務之間的呼叫……,如果發生了故障,再執行 fallback方法返回結果,我們可以把它理解成保底操作。如果正常邏輯在短時間內頻繁發生故障,那麼可能會觸發短路,也就是之後的請求不再執行 run,而是直接執行 fallback。更多關於 Hystrix 的資訊可以檢視 https://github.com/Netflix/Hystrix,而
hystrix-go 則是用 go 實現的 hystrix 版,更確切的說,是簡化版。只是上一次更新還是 2018年 的一次 pr,也就畢業了?

為什麼需要這些工具?
比如一個微服務化的產品線上,每一個服務都專注於自己的業務,並對外提供相應的服務介面,或者依賴於外部服務的某個邏輯介面,就像下面這樣。

假設我們當前是 服務A,有部分邏輯依賴於 服務C服務C 又依賴於 服務E,當前微服務之間進行 rpc或者 http通訊,假設此時 服務C 呼叫 服務E 失敗,比如由於網路波動導致超時或者服務E由於過載,系統E 已經down掉了。

呼叫失敗,一般會有失敗重試等機制。但是再想想,假設服務E已然不可用的情況下,此時新的呼叫不斷產生,同時伴隨著呼叫等待和失敗重試,會導致 服務C對服務E的呼叫而產生大量的積壓,慢慢會耗盡服務C的資源,進而導致服務C也down掉,這樣惡性迴圈下,會影響到整個微服務體系,產生雪崩效應。

雖然導致雪崩的發生不僅僅這一種,但是我們需要採取一定的措施,來保證不讓這個噩夢發生。而 hystrix-go就很好的提供了 熔斷和降級的措施。它的主要思想在於,設定一些閥值,比如最大併發數(當併發數大於設定的併發數,攔截),錯誤率百分比(請求數量大於等於設定 的閥值,並且錯誤率達到設定的百分比時,觸發熔斷)以及熔斷嘗試恢復時間等 。

使用

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

_ = 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
    })

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

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

hystrix.ConfigureCommand("wuqq", hystrix.CommandConfig{
        Timeout:                int(3 * time.Second),
        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並且錯誤率到達這個百分比後就會啟動熔斷

當然你不設定的話,那麼自動走的預設值。

我們再來看一個簡單的例子:

package main

import (
   "fmt"
 "github.com/afex/hystrix-go/hystrix" "net/http" "time")

type Handle struct{}

func (h *Handle) ServeHTTP(r http.ResponseWriter, request *http.Request) {
   h.Common(r, request)
}

func (h *Handle) Common(r http.ResponseWriter, request *http.Request) {
   hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
      Timeout:                int(3 * time.Second),
      MaxConcurrentRequests:  10,
      SleepWindow:            5000,
      RequestVolumeThreshold: 20,
      ErrorPercentThreshold:  30,
   })
   msg := "success"

  _ = hystrix.Do("mycommand", func() error {
      _, err := http.Get("https://www.baidu.com")
      if err != nil {
         fmt.Printf("請求失敗:%v", err)
         return err
  }
      return nil
  }, func(err error) error {
      fmt.Printf("handle  error:%v\n", err)
      msg = "error"
  return nil
  })
   r.Write([]byte(msg))
}

func main() {
   http.ListenAndServe(":8090", &Handle{})
}

我們開啟了一個 http 服務,監聽埠號 8090,所有請求的處理邏輯都在 Common 方法中,在這個方法中,我們主要是發起一次 http請求,請求成功響應success,如果失敗,響應失敗原因。

我們再寫另一個簡單程式,併發 11 次的請求 8090 埠。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
    "time"
)

var client *http.Client

func init() {
    tr := &http.Transport{
        MaxIdleConns:    100,
        IdleConnTimeout: 1 * time.Second,
    }
    client = &http.Client{Transport: tr}
}

type info struct {
    Data interface{} `json:"data"`
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 11; i++ {
        wg.Add(1)
        go func(int2 int) {
            defer wg.Done()
            req, err := http.NewRequest("GET", "http://localhost:8090", nil)
            if err != nil {
                fmt.Printf("初始化http客戶端處錯誤:%v", err)
                return
            }
            resp, err := client.Do(req)
            if err != nil {
                fmt.Printf("初始化http客戶端處錯誤:%v", err)
                return
            }
            defer resp.Body.Close()
            nByte, err := ioutil.ReadAll(resp.Body)
            if err != nil {
                fmt.Printf("讀取http資料失敗:%v", err)
                return
            }
            fmt.Printf("接收到到值:%v\n", string(nByte))
        }(i)
    }
    wg.Wait()

    fmt.Printf("請求完畢\n")
}

由於我們配置 MaxConcurrentRequests 為10,那麼意味著還有個 g 請求會失敗:

和我們想的一樣。

接著我們把網路斷開,併發請求改成10次。再次執行程式併發請求 8090 埠,此時由於網路已關閉,導致請求百度失敗:

接著繼續請求:

熔斷器已開啟,上面我們配置的RequestVolumeThresholdErrorPercentThreshold 生效。

然後我們把網連上,五秒後 (SleepWindow的值)繼續併發呼叫,當前熔斷器處於半開的狀態,此時請求允許呼叫依賴,如果成功則關閉,失敗則繼續開啟熔斷器。

可以看到,有一個成功了,那麼此時熔斷器已關閉,接下來繼續執行函式併發呼叫:

可以看到,10個都已經是正常成功的狀態了。

那麼問題來了,為什麼最上面的圖只有一個是成功的?5秒已經過了,並且當前網路正常,應該是10個請求都成功,但是我們看到的只有一個是成功狀態。通過原始碼我們可以找到答案:
具體邏輯在判斷當前請求是否可以呼叫依賴

if !cmd.circuit.AllowRequest() {
            ......
            return
        }

func (circuit *CircuitBreaker) AllowRequest() bool {
    return !circuit.IsOpen() || circuit.allowSingleTest()
}

func (circuit *CircuitBreaker) allowSingleTest() bool {
    circuit.mutex.RLock()
    defer circuit.mutex.RUnlock()

    now := time.Now().UnixNano()
    openedOrLastTestedTime := atomic.LoadInt64(&circuit.openedOrLastTestedTime)
    if circuit.open && now > openedOrLastTestedTime+getSettings(circuit.Name).SleepWindow.Nanoseconds() {
    /
        swapped := atomic.CompareAndSwapInt64(&circuit.openedOrLastTestedTime, openedOrLastTestedTime, now) //這一句才是關鍵
        if swapped {
            log.Printf("hystrix-go: allowing single test to possibly close circuit %v", circuit.Name)
        }
        return swapped
    }

    return false
}

這段程式碼首先判斷了熔斷器是否開啟,並且當前時間大於 上一次開啟熔斷器的時間+ SleepWindow 的時間,如果條件都符合的話,更新此熔斷器最新的 openedOrLastTestedTime ,是通過 CompareAndSwapInt64 原子操作完成的,意外著必然只會有一個成功。
此時熔斷器還是半開的狀態,接著如果能拿到令牌,執行run 函式(也就是Do傳入的第二個簡單封裝後的函式),發起 http 請求,如果成功,上報成功狀態,關閉熔斷器。如果失敗,那麼熔斷器依舊開啟。

hystrix-go原始碼解析

以上就是大體的流程講解,下一篇文章將解讀核心原始碼以及進一步當思考。

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

相關文章