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更爲合適。