hystrix-go——微服務裏的“及時止損”

編者薦語:

良心推薦一個#公衆號:非典型後端碼農 。騰訊大佬在線帶你探究後端開發的奧祕,打開從編程語言到架構設計的新視角,歡迎圍觀。

爲什麼需要熔斷?怎麼做?

爲什麼需要熔斷機制?

在分佈式系統中,服務間相互調用非常常見,單個用戶請求背後會調用多個服務,而被調用的服務可能依賴其他服務,一直形成一條調用鏈。當被調用的服務出現故障時,可能出現遠端負載增加雪崩效應兩種情況。


遠端負載增加:當遠程服務出現異常時,如果不適當減少下發到對端的請求。以最近線上服務遇到的case爲例,遠端服務因爲一個DB慢查詢導致響應超時,由於調用者沒有進行限制,導致DB負載增加,進一步加劇服務響應超時的情況。


雪崩效應:假設有A、B、C三個服務,A會依賴B,B會依賴C。當C響應過慢或超時時,B服務會堆積過多的等待連接,甚至出現故障,而這個故障也可能進一步像雪崩一樣傳導到A服務。


熔斷機制

熔斷機制即在遠程服務調用前增加一個斷路器,當遠程調用出現過多超時或失敗時,後面來的請求都不會下發到遠端服務,而是進行快速失敗(fast-fail)。

   

當斷路器爲閉合時(closed),請求能夠順利下發,而當總的失敗的請求超過特定閾值時,電路斷開(open),此後所有請求都會快速失敗而不發起遠程調用。


此時打開一個定時器,倒計時結束後進入半開狀態(half open),此時能夠嘗試下發一個請求,如果成功則轉成閉合,否則回到斷開狀態,重新倒計時。


hystrix-go使用

// hystrix.gofunc main() {    // 設置一個命令名爲"getfail"的斷路器    hystrix.ConfigureCommand("getfail", hystrix.CommandConfig{        Timeout:                int(3 * time.Second),        MaxConcurrentRequests:  10,        SleepWindow:            5000,        RequestVolumeThreshold: 10,        ErrorPercentThreshold:  30,    })    // 使用getfail這個斷路來保護以下指令,採用同步的方式,hystrix.Go則是異步方式    _ = hystrix.Do("getfail", func() error {        // 嘗試調用遠端服務        _, err := http.Get("https://localhost:8080")        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:一個遠程調用的超時時間。

  • MaxConcurrentRequests:最大併發數,超過併發數的請求直接快速失敗。

  • SleepWindow:斷路器從開路狀態轉入半開狀態的睡眠時間,單位:ms。

  • RequestVolumeThreshold:爲了避免斷路器一啓動就進入斷路,當超過這個請求之後,斷路器纔開始工作。

  • ErrorPercentThreshold:開路閾值,表示觸發開路的失敗請求的百分比。



核心源碼與實現

整體流程

hystrix-go的核心組件是指令command和斷路器CircuitBreakercommand在每次發起Do或者Go時根據傳入的調用函數和回調函數構成。CircuitBreak則由一個全局唯一的map來維護——map[string]*CircuitBreaker,key則是ConfigureCommand時設置的命令名。在利用斷路器對某個調用進行保護時,會創建一個command對象,並根據傳入的key獲取對應斷路器。


不同指令對應不同的斷路器,可以保證不同的依賴服務間不會相互影響,這個優化點稱作艙壁模式(bulkhead)。


接下來我們看下hystrix-go的整體流程


如圖所示,當斷路器不處於開路或者超過併發數時,會進行遠程調用,並將成功/失敗/超時的結果上報到斷路器的健康檢查器;否則進行快速失敗。


這裏以hystrix-go.Go爲例進行解讀,Go最後會調用GoC

// hystrix.gofunc GoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) chan error {    // 將待執行函數和失敗回調函數封裝成command對象    cmd := &command {...}    // 獲取斷路器    circuit, _, err := GetCircuit(name)    ...    // 開啓一個協程,用於處理遠程調用    go func {        if !cmd.circuit.AllowRequest() {            // 斷路器開路或半開時在這裏進行嘗試或快速失敗            ...             }                // 嘗試從協程池中獲取Token,如果獲取失敗則快速返回失敗        ...
// 執行遠程調用,並上報結果(成功/失敗) ... } // 開啓另一個協程,用於處理請求超時的情況 go func { timer := time.NewTimer(getSettings(name).Timeout) defer timer.Stop() select { case <- timer.C: returnOnce.Do(func() { returnTicket() // 返回Ticket進協程池 cmd.errorWithFallback(ctx, ErrTimeout) // command設爲超時失敗 reportAllEvent() // 向健康檢查器上報超時失敗 }) return } }
return cmd.errChan}


可以看到整個流程跟斷路器處理一個請求的流程是一致的。對於一個請求的處理,hystrix-go開啓了一個協程,這是考慮到Client在執行的過程中也可能會出現非網絡異常,這些都應該被隔離。


另外有趣的是,斷路器使用一個buffered chan對最大併發數進行控制,只有從通道中成功獲取Token的請求能夠進行遠程調用。


接下來簡單看下hystrix.DoC的實現,可以看見核心還是調用了GoC,只是在最後會通過<-chan操作阻塞返回,達到同步調用的效果。

// hystrix.gofunc DoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) error {    ...    if fallback == nil {    errChan = GoC(ctx, name, r, nil)  } else {    errChan = GoC(ctx, name, r, f)  }    select {        case <-done:    return nil  case err := <-errChan:    return err    }}


如何檢查電路是否健康

前文提到,每個circuit都會有一個metrics屬性,而在源代碼每個可能上報請求狀態的點都會調用reportAllEvent,最終其實是將所有的請求情況推進circuit.metrics。而這個屬性會維護過去發生過的所有請求,以及所有失敗或超時的請求。在AllowRequest裏會查看失敗率是否超過上線,如果是則將斷路器轉成開路。


維護斷路器狀態

hystrix-go沒有像理論模型一樣維護三種狀態,而是隻維護了一個布爾值。半開狀態則是通過CAS的方式來保證只能有一個嘗試的請求發出:CompareAndSawp在修改一個值前,會先嚐試用變量的老的值去比較(Compare),只有舊值匹配上才能成功附上新值,這可以保證併發時多個請求只有一個能夠修改成功。利用這個特性,hystrix-go可以保證在斷路器開路且計時器到期進入半開狀態時只有一個嘗試請求能夠達到遠端。

func (circuit *CircuitBreaker) allowSingleTest() bool {  ...  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}


啓發

1. 當需要復用同一段代碼,同時需要向調用者暴露同步調用的接口和異步調用的接口時,嘗試返回一個表示完成或結果的chan,在異步任務中返回chan,同步任務中則嘗試用<-chan來阻塞返回。

2. golang中控制協程最大併發數:使用buffered chan並指定其大小爲最大協程數,每個請求來的時候嘗試從通道中獲取一個token,在請求結束後將token放回通道中。

3. 使用CAS可以保證併發請求下來時只有一個請求能夠修改成功。

4. hystrix-go裏涉及到不少訪問共享變量或臨界區的設計,比如sync.Once等,都比較值得學習。

本文分享自微信公衆號 - 編程三分鐘(coding3min)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章