看看指针类型 rolling.Number:
rolling.Number 是真正存储各个执行事件状态信息的底层存储结构。它是如何只保存 10 秒内的信息的。
惊不惊喜?
newMetricExchange(name) 还有一个细节,会单独开启一个 g 运行 go m.Monitor()
去接收 channel 类型的 Updates 信息,即执行事件状态信息。
在接收到事件信息后,调用 IncrementMetrics 先做状态信息的整合,最终把整合后的执行状态事件信息上报 collector.Update(r)
。
接着回头看初始化流量控制中心 newExecutorPool(name)。先看看 executorPool 结构。
主要关注两个字段,Tickets 表示的就是访问令牌带缓冲通道的 channel ,初始化 channel 容量取决于一开始你设置的 MaxConcurrentRequests。当有请求到来时,从 channel 中拿出一个令牌,调用后重新归还。 poolMetrics 就是流量控制的具体指标。
Executed 表示当前桶已经处理的请求数量。此外在 newExecutorPool(name) 函数中,和刚才套路一样,启动一个 go m.Monitor()
专门去更新当前桶的最大值。
到这里,GetCircuit(name) 获取一个熔断器的代码讲完了。
回到 GoC ,我们得到一个 circuitBreakers 的指针。
接下来我们创建一个条件变量 sync.NewCond。条件变量的场景是当共享资源发生变化时,通知那些被互斥锁锁住的线程。
在这里它是用来协调通知你可以归还访问令牌了。
接着有一句 returnOnce := &sync.Once{},
关于 sync.Once, 之前解析过一篇文章你真的了解 sync.Once 吗。这里它存在的意义是什么?我们往下看,就会发现其实到后面开启了两个 Goroutine。
它的作用是确保由最快那个 Goroutine 运行 errWithFallback()
和 reportAllEvent()
,而且保证只会执行一次。
接着往下看,
这个函数就是就是用来上报执行事件的。
前面都好懂,我们从这段开始看:
这里操作这句话是什么意思?因为存在一种情况:当前熔断器是开启的,并且已经过了 SleepWindow 时间,此时请求就属于半开的状态,允许尝试执行,如果执行成功,那么就说明服务恢复了,可以关闭熔断器了。接下来,
组装执行状态状态事件,然后塞进 Updates 通道中。正好被初始化 metricExchange 另开的 Goroutine 接收,这样,这个上报流程就对应上了。
接下来就是刚才截图的两个 Goroutine, 我们先看第一个。
截图了上前半部分。上来一个 defer func() { cmd.finished <- true }()
作为正常运行结束的通知。然后就是 cmd.circuit.AllowRequest()
判断是否能请求。
有两种情况可以往下走,第一种熔断器是关闭的。对应 circuit.IsOpen()
。
首先判断熔断器是否被强制开启或者已经开启,如果是,直接返回 true。否则说明是当前熔断器处于关闭。接着判断过去十秒内各个桶值的和是否小于设置的 RequestVolumeThreshold 值,如果小于,说明熔断器还应该是关闭状态,返回 false 。如果大于等于,那么应该进一步去判断错误百分比 是否超出自己的设置的 ErrorPercentThreshold。如果超出了,那么说明错误率过高,此时需要开启熔断器。
这段代码很好懂,有意思的是 int(errPct + 0.5)
。+0.5 是为了四舍五入,如果直接浮点数转换成整形,那么必然就是去除小数点。加了 0.5,那么假设ErrorPercentThreshold 设置 50,如果是 <45.5, 熔断器就继续关闭,否则开启熔断器。
如果 circuit.IsOpen()
不符合,那么再看 circuit.allowSingleTest()
。虽然熔断器是开启的,但是如果当前的时间已经大于 (上次开启熔断器的时间 +SleepWindow 的时间),这时候熔断器属于半开的状态,可以执行下一步。那么就会返回 true。
如果 cmd.circuit.AllowRequest 返回 false,那么就是执行 returnTicket 归还令牌 (尽管这时候还没有令牌可言)。这段代码很有趣,通过变量 ticketChecked 加 sync.NewCond 实现的逻辑。cmd.errorWithFallback(),上报熔断器已开启事件 (circuit open) 以及运行 fallBakck 保底函数 (如果存在的话),执行结束,响应。
如果返回 true,接着玩下走,
如果拿不到访问令牌,那么和刚才一样,上报当前请求已超过并发数事件 (max concurrency),运行保底操作,响应。
如果拿到访问令牌,那么真正执行自己的业务代码 run(ctx)。套路和上面相似。
再看第二个 Goroutine
三个 case 分别表示:正常执行结束、业务执行被取消以及超时。至于为什么说 Do 是同步操作,因为在 Doc 最后,
而 Go 则是一个异步过程。
到这里,从 Do 开始执行的大体流程就走完了。
最后我们可以直接给出它的大体流程图。
他们结构体之间的一些关系图。