动手写一个基于golang的微服务熔断器
上一篇咱们介绍了一些熔断器的概念和原理,咱们今天就谈一下功能组件的划分和具体的实现。git
对目前开源熔断器的对比
针对上一篇内容出现的hystrix和go-breaker,我梳理了两方优势github
熔断器名称 | hystrix | go-breaker |
---|---|---|
滑动窗口计数 | 支持 | 不支持 |
限流 | 支持 | 不支持 |
阻塞读 | 是 | 是 |
对半开启的处理 | 滑动计数器+阈值 | 连续成功则转移到close |
对监控的支持 | 支持metric采集 | 不支持 |
降级处理 | 支持hook | 支持hook |
解决并发尖刺 | 不支持 | 支持 |
代码结构易读性 | 稍差 | 较好 |
咱们一一展开来说golang
计数模块
计数模块是熔断器的核心,网上有针对计数器的大篇幅的分析针对这里引用知乎上一位大佬的比较类型的文章,根据最后的比较咱们选择滑动窗口的算法来完成计数需求。 在hystrix的设计中,滑动窗口的比较重要的是写入时刻和读取时刻,由于咱们很容易想到在这两个环节涉及到对一块内存并发读写的问题,首先咱们不建议采用go-breaker的全加锁(读写都加锁)的设计,由于锁在发生竞争时会挂起线程,从而下降了CPU的使用率和共享内存总线上的同步通讯量,那么咱们参考hystrix,采用异步提交的方法,也就是将结果放入一个队列中,不断消费这个队列,这么作有几点好处算法
消息串行化,减小写入读取数据没必要要竞争 在数据生产层--->数据存储层中间构造出中间层,方便进行监控统计收集等操做 方便控制消息的消费状况
在实现上采用channel的数据结构,消费有高效保证。可是事物都有两面性,这种设计带来的问题有markdown
滑动窗口统计须要访问当前窗口內全部数据 串行化没有将统计性能发挥最大(虽然在计数丰方面表现很快) 业务要容许流量尖刺的出现(假设没有加限流)
其中2,3点通过调研都在业务容许范围內,且针对第三点咱们能够增长限流策略来完善这一点。 数据结构
限流
hystrix天生限流,全部请求先过令牌桶而后进入熔断统计,go-breaker尚未这方面支持,在限流这里咱们怀疑要不要在一块儿作(毕竟熔断是熔断,限流是限流),因此作了另外一个方案,在半开启时进行限流放行请求,这样比较符合半开启时的请求经过策略,同时进行统计,限流策略采用退化版本令牌桶,方法以下:并发
type limitPoolManager struct { max int tickets chan *struct{} lock *sync.RWMutex } /* 方法返回一个限流器 */ func NewLimitPoolManager(max int) *limitPoolManager { lpm := new(limitPoolManager) tickets := make(chan *struct{}, max) for i := 0; i < max; i++ { tickets <- &struct{}{} } lpm.max = max lpm.tickets = tickets lpm.lock = &sync.RWMutex{} return lpm } /* 方法填充限流器全部令牌 */ func (this *limitPoolManager) ReturnAll() { this.lock.Lock() defer this.lock.Unlock() if len(this.tickets) == 0 { for i := 0; i < this.max; i++ { this.tickets <- &struct{}{} } } } /* 方法返回一个令牌,获得令牌返回true,令牌用完后返回false */ func (this *limitPoolManager) GetTicket() bool { this.lock.RLock() defer this.lock.RUnlock() select { case <-this.tickets: return true default: return false } } /* 方法返回剩余令牌数 */ func (this *limitPoolManager) GetRemaind() int { this.lock.RLock() defer this.lock.RUnlock() return len(this.tickets) }
阻塞读
由于串行化设计因此在每次收失败请求时能够对窗口內数据进行错误率转化。避免hystrix与go-breaker的锁争抢异步
半开启处理
以上+本节主流程基本完结,现梳理整个流程明确half-open时处理:函数
当熔断器为close时。只有当出现错误请求时,才进行错误率统计,统计过阈值则状态转移到open,正确请求则正常计数。 当熔断器为half-open时,仅当令牌桶中还有令牌时接收请求不然熔断。令牌桶中还有令牌时,出现错误请求则更新熔断休眠时间并返回全部令牌等待下次半开启,正常请求则进入半开启时统计达到阈值则状态转移到close。 当熔断器为open时,仅当熔断休眠时间小于当前时间时,当熔断器状态转移到half-open,能够进行第二条,不然执行熔断
首先判断是不是半开启状态微服务
switch this.counter.GetStatus() { case STATE_OPEN: if this.cycleTime < time.Now().Local().Unix() { return OPEN_TO_HALF_ERROR } return BREAKER_OPEN_ERROR }
其次若是是半开启状态则取令牌,取到令牌则执行请求,进入熔断时计数,不然直接熔断
/*取令牌*/ if !this.lpm.GetTicket() { this.safelCalllback(fallback, BREAKER_OPEN_ERROR) return nil } /*执行方法*/ runErr := run() if runErr != nil { this.fail() this.safelCalllback(fallback, runErr) return runErr } this.success() return nil
流量尖刺处理
流量尖刺的削峰伴随着限流的逻辑,因此能够在请求到达时优先进入令牌桶
监控&降级
提供hook函数,在限流或者执行失败时能够提供降级或者回掉
/* 执行函数 */ type runFunc func() error /* 回调函数 */ type fallbackFunc func(error) /* Do方法结合熔断策略执行run函数 其中参数包括:上下文ctx,策略名name,将要执行方法run,以及回调函数fallback.其中ctx,name,run必传 run函数的错误会直接同步返回,回调函数fallback接收除了run错误之外还会接收熔断时错误,调用方若是须要降级可在fallback中本身判断 */ func Do(ctx context.Context, name string, run runFunc, fallback fallbackFunc) error { ........ }
代码结构易读性
hystrix代码会比go-breaker冗余一些(毕竟go-breaker300行搞定。。。不要杠)。go-breaker将请求阶段分为请求前,请求中,请求后三个阶段,这里能够借鉴一下,容易理清思路。 以上是动手写个熔断器的设计和分析思路。 下面是github地址:https://github.com/EAHITechnology/breaker.git ,欢迎给出意见。