golang hystrix 熔斷器

golang hystrix 熔斷器

熔斷器是爲了保護被調方健康的一種方式。通過錯誤率,超時,併發等機制來使第三方處於一個健康且提供性能最佳的方式。hystrix 是比較通用的熔斷器庫。以下爲介紹該熔斷器源碼以及處理思想。

核心思想

先看看需要配置什麼參數,都是從參數玩出的花。

  • Timeout: 執行command的超時時間。默認時間是1000毫秒
  • MaxConcurrentRequests:command的最大併發量 默認值是10
  • SleepWindow:當熔斷器被打開後,SleepWindow的時間就是控制過多久後去嘗試服務是否可用了。默認值是5000毫秒
  • RequestVolumeThreshold: 一個統計窗口10秒內請求數量。達到這個請求數量後纔去判斷是否要開啓熔斷。默認值是20
  • ErrorPercentThreshold:錯誤百分比,請求數量大於等於RequestVolumeThreshold並且錯誤率到達這個百分比後就會啓動熔斷 默認值是50
    當然如果不配置他們,會使用默認值

通過配置參數我們就可以瞭解到他大致提供了這些功能。

功能模塊

配置模塊

通過以下代碼註冊name 爲mycommand 的熔斷器配置。

hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
		Timeout:                int(time.Second * 3),
		MaxConcurrentRequests:  100,
		SleepWindow:            int(time.Second * 5),
		RequestVolumeThreshold: 30,
		ErrorPercentThreshold: 50,
	})

統計模塊

爲了實現熔斷判斷就必須要統計一定時間內的成功失敗請求,這次採用了10s 的桶形式進行統計。在每次更新時都會刪除時間大於10s 的桶。時間戳就是key。這裏不夠靈活 桶的大小也應該提供可選和默認參數讓調用方選擇

type DefaultMetricCollector struct {
	mutex *sync.RWMutex

	numRequests *rolling.Number
	errors      *rolling.Number

	successes               *rolling.Number
	failures                *rolling.Number
	rejects                 *rolling.Number
	shortCircuits           *rolling.Number
	timeouts                *rolling.Number
	contextCanceled         *rolling.Number
	contextDeadlineExceeded *rolling.Number

	fallbackSuccesses *rolling.Number
	fallbackFailures  *rolling.Number
	totalDuration     *rolling.Timing
	runDuration       *rolling.Timing
}

上報模塊

上報模塊 非主流程,沒有太關注。這裏應該開放給客戶端,如果客戶端需要監控,或者有其他上報機制,提供接口更爲合適。

流量控制

採用令牌算法,拿到令牌開始工作,執行完成返回令牌。等不到令牌時返回maxconcurent。

熔斷開關控制

熔斷器狀態判斷

當請求數量大於 統計桶內(10s 間隔)的 大於 RequestVolumeThreshold 並且錯誤率大於 ErrorPercentThreshold 時熔斷器打開。

func (circuit *CircuitBreaker) IsOpen() bool {
	circuit.mutex.RLock()
	o := circuit.forceOpen || circuit.open
	circuit.mutex.RUnlock()

	if o {
		return true
	}

	if uint64(circuit.metrics.Requests().Sum(time.Now())) < getSettings(circuit.Name).RequestVolumeThreshold {
		return false
	}

	if !circuit.metrics.IsHealthy(time.Now()) {
		// too many failures, open the circuit
		circuit.setOpen()
		return true
	}

	return false
}

熔斷器關閉

打開熔斷器的判斷比較明顯,那麼何時去關閉熔斷器,什麼去觸發就比較關鍵了。

關鍵代碼是set close。向上追溯,看到在統計的時候只要success 就會去打來熔斷器。那麼問題來了,多久纔會去嘗試調用呢?

func (circuit *CircuitBreaker) setClose() {
	circuit.mutex.Lock()
	defer circuit.mutex.Unlock()

	if !circuit.open {
		return
	}

	log.Printf("hystrix-go: closing circuit %v", circuit.Name)

	circuit.open = false
	circuit.metrics.Reset()
}

sleep window

判斷是否能請求是這個函數,顯然現在的情形是open 所以,allowSingleTest 爲true 時就可以發送請求。

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

可以看到當過了sleep window 時間窗口的時候就會有一次機會去調用。這裏沒看明白加了鎖又用了CompareAndSwapInt64 是個什麼騷操作。仔細一看這是個R Lock,額差點去提issue。

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
}

如果是這樣 在併發的情況沒有嚴格的時間順序,第一個可能成功,第二個還是可能失敗。

熱加載的坑

項目上對第三方調用較多,就需要使用代理模式來擴展以維持穩定性。當然針對第三方不穩定的狀態,在運行時肯定是會去調整熔斷等機制的。這時候就需要熱加載了。在使用過程中發現修改了配置並沒有使用新的配置,但是從GetCircuitSettings 函數返回的配置確實是最新的。源碼之下無祕密。那就只能上源碼看了。

原來源碼中區分了 circuitSettings 和 circuitBreakers。這兩個都是以全局變量的形式維護。但是 circuitBreakers 在初始化時會讀取circuitSettings 的配置 new 一個實例出來。後面去更新只會更新到circuitSettings,讀取配置也是從circuitSettings中讀取,而circuitBreakers沒有被改變,實際運行的正是circuitBreakers 導致最後沒有生效。

最終看到這個函數可以刷掉數據,刪除circuitBreakers 所有配置,再重新加載。在我看來這邊應該在創建新的之後從settings new 一個對象更新的circuitBreakers更爲合適。

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