背景
从单体服务拆分到微服务过程中,原来模块间交互逐渐抽离成远程调用,可能http,rpc,tcp,,,等等,那么这些模块在调用中一定存在某种依赖关系。这时一旦下游某个 服务超时或者down,请求量还很大的时候,那么最坏情况是上游服务也会因此超时或者down掉。它的上游也如此,如此“递归”一样的出错在微服务中叫做雪崩效应。那么作为微服务架构中的三剑客之一--熔断,就是为了解决这个问题,熔断器像是一个保险丝。当我们依赖的服务出现问题时,可以及时容错。一方面可以减少依赖服务对自身访问的依赖,防止出现雪崩效应;另一方面降低请求频率以方便上游尽快恢复服务。本文结合调研结果进行讲解,目的也是为了接下来的自研熔断器做准备。
由来
毕竟大家都见识过了2020美股熔断时刻,想必这个词也并不陌生。熔断一词最早是对电路中对引出线过载,使保险丝断掉而进行保护机制,这一过程的描述。后来是指为控制股票、期货或其他金融衍生产品的交易风险,为其单日价格波动幅度规定区间限制,一旦成交价触及区间上下限,交易则自动中断一段时间。所以大家可以认为,所谓熔断即是一种保护机制,出了事,缓一缓,等一等,稍后看看能不能恢复。
一、功能简介
熔断器要最基本完成一下功能list:
| 功能点 |
说明 |
| 通路 |
当请求符合预期结果将和正常调用无区别 |
| 熔断 |
当请求符合不预期结果一定时间内一定数量下将断开方法执行不再请求下游方法 |
| 半通路 |
在熔断情况下,当请求符合预期结果开始符合预期结果一定时间内一定数量下将放行部分请求到下游方法 |
| 熔断休眠 |
在熔断后一段时间后转换成为半通路 |
画图来讲的话:
·初始为close状态,一旦遇到请求失败时,会触发熔断检测,熔断检测来决定是否将状态从closed转为open。 ·当熔断器为open状态时,会熔断所有当前方法的执行,直到冷却时间结束,会从open转变为half-open状态。 ·当熔断器为half-open状态时,以检测时间为周期去发送请求。请求成功则计数器加1,当计数器达到一定阈值时则转为close状态;请求失败则转为open状态。
二、调研方向:
调研中主要调研了目前比较主流的两个熔断器,在设计上有所不同,Hystrix更注重异步请求,统计收集更全面,虽然使用数占上风但是整体太重,gobreaker在使用上更方便,整体及其轻量满足同步调用的各种需求,介绍完使用稍后我们再来做对比。
1、hystrix
Hystrix的golang版本项目地址是:https://github.com/afex/hystrix-go
Hystrix是Netflix开源的一个限流熔断的项目、主要有以下功能:
1)隔离(线程池隔离和信号量隔离):限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
2)优雅的降级机制:超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。
3)融断:当失败率达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复。
4)缓存:提供了请求缓存、请求合并实现。支持实时监控、报警、控制(修改配置)
1、添加配置
使用时建议首先添加配置,其中可配置项有:
Timeout int `json:"timeout"`
MaxConcurrentRequests int `json:"max_concurrent_requests"`
RequestVolumeThreshold int `json:"request_volume_threshold"`
SleepWindow int `json:"sleep_window"`
ErrorPercentThreshold int `json:"error_percent_threshold"`
其中:
| 字段 |
说明 |
| Timeout |
执行command的超时时间。默认时间是1000毫秒 |
| MaxConcurrentRequests |
command的最大并发量 默认值是10 |
| SleepWindow |
当熔断器被打开后,SleepWindow的时间就是控制过多久后去尝试服务是否可用了。默认值是5000毫秒 |
| RequestVolumeThreshold |
一个统计窗口10秒内请求数量。达到这个请求数量后才去判断是否要开启熔断。默认值是20 |
| ErrorPercentThreshold |
错误百分比,请求数量大于等于RequestVolumeThreshold并且错误率到达这个百分比后就会启动熔断 默认值是50 |
添加配置eg:
hystrix.ConfigureCommand(strategyName, hystrix.CommandConfig{
Timeout: 1000,
MaxConcurrentRequests: 100,
RequestVolumeThreshold: 10,
SleepWindow: 2,
ErrorPercentThreshold: 50,
})
2、方法调用
hystrix的使用是非常简单的,同步执行,直接调用Do方法。
datatest := []byte{}
err := hystrix.Do("my_command", func() error {
res, err := http.Get("www.baidu.com")
if err != nil {
return err
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
datatest = data
return nil
}, func(err error) error {
fmt.Println("任务失败")
return nil
})
异步执行Go方法,内部实现是启动了一个gorouting,如果想得到自定义方法的数据,需要你传channel来处理数据,或者输出。返回的error也是一个channel
output := make(chan []byte, 1)
errors := hystrix.Go("my_command", func() error {
res, err := http.Get("www.baidu.com")
if err != nil {
return err
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
output <- data
return nil
}, nil)
select {
case out := <-output:
fmt.Println("任务成功", string(out))
case err := <-errors:
fmt.Println("任务失败", err.Errors())
}
/*
这里能用得上得就是配置里得并发数(不能放任开太多协程)
同步调用底层用得也是异步调用,不过会自己处理等待errors chan
*/
这里介绍一下hystrix的实现,首先整体流程如上图,hystrix在初始化时就已经初始化了一个基于channel实现的令牌桶,当请求到达的时候,首先第一件事先判断是否熔断,此处降会调用熔断对象(circuit.go)的AllowRequest() 函数,该函数从指标类(metrics.go)中统计出请求数和错误指数,前者判断是否执行熔断的必要条件,后者是充分条件(很好理解qps=1基本就告别限流了)。然后逻辑到了限流模块,成功拿到令牌的继续执行,没有的走回调函数或者直接返回。执行中会记录各种事件,比如请求数,成功数,失败数,超时等。。。在这个请求终态时此时触发golang的条件锁(sync.Cond)唤醒协程返回令牌上报事件,事件模块异步统计。 这里值得提的是hystrix的统计模块,采用滑动窗口计数(下图),
hystrix采用滑动窗口计数很好地解决了时间轴上的时间间隔问题同时还支持指标采集,但是在滑动窗口的实现上采用golang的map类型,过期元素将delete掉,这种方法只是标记此块内存不可用并没有真正释放内存,需要设置gc指数进行回收。
2、gobreaker
项目地址为:https://github.com/sony/gobreaker
gobreaker是索尼的开源的一个限流熔断的项目、主要有以下功能:
1)简单的代码结构,一共300多行,极容易阅读。
2)同样提供降级机制:但是只会根据当前连续错误数在熔断时进行降级。
3)融断:当失败数达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复。
1、添加配置
type Settings struct {
Name string
MaxRequests uint32
Interval time.Duration
Timeout time.Duration
ReadyToTrip func(counts Counts) bool
OnStateChange func(name string, from State, to State)
}
| 字段 |
说明 |
| MaxRequests |
最大请求数。当在最大请求数下,均请求正常的情况下,会关闭熔断器 |
| interval |
一个正常的统计周期。如果为0,那每次都会将计数清零 |
| timeout |
进入熔断后,可以再次请求的时间 |
| readyToTrip |
判断熔断生效的钩子函数(通过Counts 判断是否开启熔断。需要自定义也可以走默认配置) |
| onStateChagne |
状态变更的钩子函数(看是否需要) |
var cb *breaker.CircuitBreaker
cb = breaker.NewCircuitBreaker(breaker.Settings{
Name: "Get",
MaxRequests: 100,
Interval: time.Second*2,
Timeout: time.Second*2
})
2、调用过程
熔断器的执行操作,主要包括三个阶段;①请求之前的判定;②服务的请求执行;③请求后的状态和计数的更新
func Get(url string) ([]byte, error) {
body, err := cb.Execute(func() (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
})
if err != nil {
return nil, err
}
return body.([]byte), nil
}
gobreaker不同采用原子计数,加锁统计,过期清零的操作,属于简单粗暴不支持指标采集,是对熔断的这个操作(如下图)的短小精悍的实现。
其中原子计数带来的问题会比较大,如下图,当59秒的时候还没有到达数量阈值,但是1:01时又来大量请求,此时因为进入下一时刻计数早就清空,这样对于故障判断的准确性带来了挑战 ![]()
以上是对golang熔断器的调研结果,相信不同场景会有不同需求,不同需求可以对熔断器来回选择,下一篇会介绍自研的熔断器。