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更为合适。