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

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