go-resilency 源码阅读
💡 golang 的弹性模式。部分基于 Hystrix、Semian 等- 断路器在 (在 breaker 目录)
- 信号量(在 semaphore 目录)
- deadline/超时(在 dealine 目录)
- 批处理 (在 batcher 目录)
- 可恢复(或者翻译为可重试?)(retriable) (在 retrier 目录)
项目地址: github.com/eapache/go-…
batcher
golang 的批处理弹性模式
创建批处理需要两个参数
- 收集批处理时等待的超时
- 批处理完成后运行的函数
还可以选择设置一个预过滤器,以便在进入批处理之前,过滤掉,不让它进入批处理
仓库给的使用例子:
b := batcher.New(10*time.Millisecond, func(params []interface{}) error {
// do something with the batch of parameters
return nil
})
b.Prefilter(func(param interface{}) error {
// do some sort of sanity check on the parameter, and return an error if it fails
return nil
})
for i := 0; i < 10; i++ {
go b.Run(i)
}
网上搜索了一下这个库的详细解释,大部份文章都是对这个库如何使用或者简要的流程分析,很少有对源码一行行进行分析的资料,我认为知道这个库是如何使用的以及中间执行的简要流程,学习到的东西是表象的,应该深入代码,看看它内部具体是如何实现的,才能达到我们学习的目的。
上面一段是废话,现在让我们开始进入源码分析阶段。
// New constructs a new batcher that will batch all calls to Run that occur within
// `timeout` time before calling doWork just once for the entire batch. The doWork
// function must be safe to run concurrently with itself as this may occur, especially
// when the timeout is small.
func New(timeout time.Duration, doWork func([]interface{}) error) *Batcher {
return &Batcher{
timeout: timeout,
doWork: doWork,
}
}
根据注释,我们可以知道,这里新创建一个新的批处理器,它将批处理在 timeout 的时间内发生的所有对 Run 的调用,然后对整个批处理器只调用一次 doWork ,这个 dowork 的函数必须能安全能安全地与自身并发运行。
Batcher 的结构体所含有的参数如下所示:
type Batcher struct {
timeout time.Duration
prefilter func(interface{}) error
lock sync.Mutex
submit chan *work
doWork func([]interface{}) error
done chan bool
}
其中 timeout 和 dowork 是我们 New 这个 Batcher 时所传入的,根据上面仓库给出的使用例子,当我们需要过滤掉一些不需要进行批处理阶段的的参数时,可以在 New 完 Batcher 后,给 prefilter 这个字段,设置我们的过滤函数,其他的字段在后面会一一介绍。
当我们创建好 Batcher 并按需设置了过滤函数后,调用 Run 函数,Run 函数根据我们传入的参数执行 work 函数,并将在超时时间内发生的调用一起批量运行
func (b *Batcher) Run(param interface{}) error {
// 当过滤函数不等于 nil时,执行过滤函数,过滤掉不需要进入批处理的参数
if b.prefilter != nil {
if err := b.prefilter(param); err != nil {
return err
}
}
// 当 timeout 时间为 0 时,直接执行我们设置的 work 函数,并返回
// 意味着当我们设置的 timeout 参数为 0 时,就相当于不会批量处理了
if b.timeout == 0 {
return b.doWork([]interface{}{param})
}
// 根据我们传入的参数初始化要执行的任务
w := &work{
param: param,
future: make(chan error, 1),
}
// 提交任务
b.submitWork(w)
// 阻塞在这里,等待提交的任务执行完成,等待 future 的通信,然后执行完成
return <-w.future
}
提交任务时,内部的实现:
func (b *Batcher) submitWork(w *work) {
// 加锁,保证并发运行不会出问题
b.lock.Lock()
defer b.lock.Unlock()
if b.submit == nil {
// 当 submit 等于 nil时,初始化 done 和 submit 字段,都是 chan 类型
// 这里的 submit 为什么的 chan 长度为什么要设置为 4 ?
b.done = make(chan bool)
b.submit = make(chan *work, 4)
// 并发执行批处理,这里并发执行了,那么不会出现执行 batch 方法时 ,submit 为空的吗?
// 这里就涉及到 golang 中 channel 的知识点了
// 当循环没有 close 掉的channel 时,range 遍历是会发生阻塞的
go b.batch()
}
// 将要执行的任务传递给 submit 字段
b.submit <- w
}
执行批处理,内部的实现:
func (b *Batcher) batch() {
var params []interface{}
var futures []chan error
// 将 submit 设置为输入参数
input := b.submit
// 并发执行定时器,定时器做了什么处理,后面会说到
go b.timer()
// 遍历输入参数,搜集超时时间内的所有传入的参数
for work := range input {
params = append(params, work.param)
futures = append(futures, work.future)
}
// 将搜集到的所有参数,传入我们要执行的真正的函数,也就是上面讲的只执行一个的 dowork 函数
ret := b.doWork(params)
// 遍历所有要执行的 work 的 futures
for _, future := range futures {
// 将批处理后的值传递给它们
future <- ret
// 关闭每个要执行的 work 的 future 通道
close(future)
}
// 关闭批处理的 done 通道
close(b.done)
}
定时器内部实现:
func (b *Batcher) timer() {
// 根据我们设定的超时时长,进行休眠
time.Sleep(b.timeout)
// 然后保存所有的变更
b.flush()
}
func (b *Batcher) flush() {
b.lock.Lock()
defer b.lock.Unlock()
// 当 submit 为 nil 时,直接返回
if b.submit == nil {
return
}
// 关闭 submit 通道
close(b.submit)
// 重新设置 submit 为 nil
// 我刚开始困惑,会不会超时时间到了之后,后面传入的参数就被丢弃了呢?
// 这里解答了我的疑惑,当超时时间到了之后,重新设置 submit 为 nil
// 然后进入一个新的收集阶段
b.submit = nil
}
到此基本分析完了,此时我还发现测试用例,还可以执行 shutdown 操作,
// Shutdown flush the changes and wait to be saved
func (b *Batcher) Shutdown(wait bool) {
// 保存所有的变更
b.flush()
// 如果我们传入的参数是 true 的话,会等待当前批处理操作执行完
if wait {
if b.done != nil {
// wait done channel
<-b.done
}
}
}
总结
可以看出 go-resilency 这个项目设计到了大量 channel 通信和并发的知识点,如果让我推荐学习 channel 通信和并发的东西, go-resilency 将会是我的首选。
breaker
golang 的熔断弹性模式
创建断路器需要三个参数
- 错误阈值(用于打开断路器)
- 成功阈值 (用于关闭断路器)
- 超时(断路器保持打开状态的时间)
仓库给的使用例子:
b := breaker.New(3, 1, 5*time.Second)
for {
result := b.Run(func() error {
// communicate with some external service and
// return an error if the communication failed
return nil
})
switch result {
case nil:
// success!
case breaker.ErrBreakerOpen:
// our function wasn't run because the breaker was open
default:
// some other error
}
}
源码分析阶段
// New constructs a new circuit-breaker that starts closed.
// From closed, the breaker opens if "errorThreshold" errors are seen
// without an error-free period of at least "timeout". From open, the
// breaker half-closes after "timeout". From half-open, the breaker closes
// after "successThreshold" consecutive successes, or opens on a single error.
func New(errorThreshold, successThreshold int, timeout time.Duration) *Breaker {
return &Breaker{
errorThreshold: errorThreshold,
successThreshold: successThreshold,
timeout: timeout,
}
}
根据注释我们知道,这里新建了一个新的断路器,初始状态是闭合的。从闭合开始,如果 "errorThreshold"(错误阈值)错误出现,而无错时间至少为 "timeout"(超时),则断路器断开。从打开开始,断路器在 "timeout" 后半闭。 从半开开始,断路器在 "成功阈值 "连续成功后关闭,或在出现一次错误后打开。
Breaker 的结构体所含有的参数如下所示:
// Breaker implements the circuit-breaker resiliency pattern
type Breaker struct {
errorThreshold, successThreshold int
timeout time.Duration
lock sync.Mutex
state uint32
errors, successes int
lastError time.Time
}
其中 errorThreshold、successThreshold 、 timeout 是我们 New 这个 breaker 时所传入的,lock 字段在接下来的 processResult 函数和 timer 函数出现, 不言而喻就是为了防止并发执行出现竞争的情况。
state 状态 有三种,分别时关闭状态,打开状态和半打开状态,从这里我们也可以看出来,由于我们 New 一个 Breaker时,并没有传入 state 相关的状态,所以默认的状态相应的零值就是 closed
const (
closed uint32 = iota
open
halfOpen
)
errors,success 是记录出现失败和成功的次数,当断路器更改状态的时候,会将它们重新置为 0
lastError 记录了最后一次出现错误的时间点
让我们进入执行函数:
// Run will either return ErrBreakerOpen immediately if the circuit-breaker is
// already open, or it will run the given function and pass along its return
// value. It is safe to call Run concurrently on the same Breaker.
func (b *Breaker) Run(work func() error) error {
// 利用原子的方式取出,取出熔断器的状态,对 atomic 感兴趣的,可以看下我的第一篇文章最后一节
state := atomic.LoadUint32(&b.state)
if state == open {
return ErrBreakerOpen
}
return b.doWork(state, work)
根据注释我们知道,如果断路器已经打开,Run 将立即返回 ErrBreakerOpen,否则它将运行给定函数并传递其返回值。并且在同一个断路器上同时调用 Run 是安全的。
执行相应的 work 函数的内部实现:
func (b *Breaker) doWork(state uint32, work func() error) error {
var panicValue interface{}
//这里的func()已经执行了,result其实是个error类型,
//之前一直以为是把result当作一个func()来用的
result := func() error {
defer func() {
panicValue = recover()
}()
return work()
}()
//如果work函数返回值为nil,panicValue为nil,并且熔断器的状态为关闭时,直接返回 nil
if result == nil && panicValue == nil && state == closed {
// short-circuit the normal, success path without contending
// on the lock
return nil
}
// 否则,根据 result 和 panicValue 进行处理
// oh well, I guess we have to contend on the lock
b.processResult(result, panicValue)
// 如果 panicValue 不为 nil,返回 panic
if panicValue != nil {
// GO让我们尽可能地接近“重抛”,尽管不幸的是我们失去了原来的产生panic的地方
// as close as Go lets us come to a "rethrow" although unfortunately
// we lose the original panicing location
panic(panicValue)
}
// 最后返回相应的 result 值
return result
}
当上述代码 if result == nil && panicValue == nil && state == closed
不成立时,接下来要执行的 processResult 的内部实现:
func (b *Breaker) processResult(result error, panicValue interface{}) {
// 加锁
b.lock.Lock()
// 最后需要释放锁
defer b.lock.Unlock()
//当没有错误产生时
if result == nil && panicValue == nil {
//这时如果熔断器的状态处于半开的状态
if b.state == halfOpen {
//熔断器成功的次数+1
b.successes++
//如果熔断器的成功次数等于最大的成功次数
if b.successes == b.successThreshold {
//关闭熔断器
b.closeBreaker()
}
}
} else {
//当有错误产生的情况
//如果错误的次数大于0
if b.errors > 0 {
//失效的时间加上熔断器的超时时间
expiry := b.lastError.Add(b.timeout)
//如果失效时间小于现在的时间
if time.Now().After(expiry) {
//将错误的次数置为0
b.errors = 0
}
}
//检查熔断器的状态
switch b.state {
//如果处于关闭的状态
case closed:
//将熔断器的错误次数+1
b.errors++
//如果熔断器的错误次数等于熔断器最大的错误次数
if b.errors == b.errorThreshold {
//打开熔断器
b.openBreaker()
} else {
//否则将熔断器最后一次产生错误的时间置为现在这个时刻
b.lastError = time.Now()
}
//如果处于半开的状态
// 这里当容易处于半打开状态时,出现一次出现就打开熔断器
case halfOpen:
//打开熔断器
b.openBreaker()
}
}
}
打开熔断器和关闭熔断器的内部实现:
func (b *Breaker) openBreaker() {
//打开熔断器
b.changeState(open)
//执行定时器
go b.timer()
}
func (b *Breaker) closeBreaker() {
// 关闭断路器
b.changeState(closed)
}
这里对比之下,有一点特别的点就是,打开熔断器的内部实现,里面还利用 go 的协程执行了一个定时器,我们来看以下定时器的逻辑:
func (b *Breaker) timer() {
//打开熔断器的同时,隔固定的时间,将熔断器置为半打开的状态,以免熔断器一直处于打开的状态
time.Sleep(b.timeout)
//加锁
b.lock.Lock()
defer b.lock.Unlock()
//将熔断器改为半开的状态
b.changeState(halfOpen)
}
这个定时器的逻辑,其实就是根据我们 New 一个熔断器时传入的 timeout,进行休眠 timeout 时间后,将熔断器改为半开的状态
我们继续看下 changeState 的内部实现:
func (b *Breaker) changeState(newState uint32) {
//更改断路器的状态,同时将失败和成功的次数置为 0
b.errors = 0
b.successes = 0
atomic.StoreUint32(&b.state, newState)
}
从这里我们可以看到,改为熔断器的状态时,会将出现 errors 和 success 的次数重新置为 0,然后更改熔断器的状态
到此我们基本已经分析完毕,但是它内部还有一个 Go 函数
// Go will either return ErrBreakerOpen immediately if the circuit-breaker is
// already open, or it will run the given function in a separate goroutine.
// If the function is run, Go will return nil immediately, and will *not* return
// the return value of the function. It is safe to call Go concurrently on the
// same Breaker.
func (b *Breaker) Go(work func() error) error {
state := atomic.LoadUint32(&b.state)
if state == open {
return ErrBreakerOpen
}
// errcheck complains about ignoring the error return value, but
// that's on purpose; if you want an error from a goroutine you have to
// get it over a channel or something
go b.doWork(state, work)
return nil
}
这个 Go 函数的逻辑其实跟上面的 Run 函数大部分是一致的,唯一的区别点是它用了协程执行去执行 dowork 函数,然后立即返回 nil 值,并且不会返回我们要执行的函数的返回值。
deadline
golang 的 deadline/timeout(超时) 弹性模式。
创建 deadline 只需一个参数:等待的时间。
💡 前几天发布后,发现末尾的地方,写得不是很清楚,于是删掉了,今天补充后,重新进行发布
仓库给的使用例子:
dl := deadline.New(1 * time.Second)
err := dl.Run(func(stopper <-chan struct{}) error {
// do something potentially slow
// give up when the `stopper` channel is closed (indicating a time-out)
// 做一些可能很慢的事情
// 当 `stopper` 通道关闭(表示超时)时放弃
return nil
})
switch err {
case deadline.ErrTimedOut:
// execution took too long, oops
// 执行时间太长了
default:
// some other error
// 其他错误
}
首先通过 New 函数,新建一个超时器,传入的参数,是所期待的超时时长
func New(timeout time.Duration) *Deadline {
return &Deadline{
timeout: timeout,
}
然后调用超时器的 Run 方法即可,在运行时间超出所期待的超时时长就会退出,返回 ErrTimedOut
的错误
// ErrTimedOut is the error returned from Run when the deadline expires.
var ErrTimedOut = errors.New("timed out waiting for function to finish")
// Run runs the given function, passing it a stopper channel. If the deadline passes before
// the function finishes executing, Run returns ErrTimeOut to the caller and closes the stopper
// channel so that the work function can attempt to exit gracefully. It does not (and cannot)
// simply kill the running function, so if it doesn't respect the stopper channel then it may
// keep running after the deadline passes. If the function finishes before the deadline, then
// the return value of the function is returned from Run.
// Run 运行给定函数,并传递给它一个 stopper 通道。如果在
// 函数执行完毕之前截止时间已过,Run 会向调用者返回 ErrTimeOut,并关闭 stopper
// 通道,以便工作函数可以优雅地退出。它不会(也不能)
// 简单地杀死正在运行的函数,因此如果它不尊重 stopper 通道,那么它可能
// 在截止日期过后继续运行。如果函数在截止日期前结束,那么
// 函数的返回值将从 Run 返回。
func (d *Deadline) Run(work func(<-chan struct{}) error) error {
result := make(chan error)
stopper := make(chan struct{})
go func() {
value := work(stopper)
select {
case result <- value:
case <-stopper:
}
}()
select {
case ret := <-result:
return ret
case <-time.After(d.timeout):
close(stopper)
return ErrTimedOut
}
}
测试用例所要执行的 work 函数定义:
func takesFiveMillis(stopper <-chan struct{}) error {
time.Sleep(5 * time.Millisecond)
return nil
}
func takesTwentyMillis(stopper <-chan struct{}) error {
time.Sleep(20 * time.Millisecond)
return nil
}
func returnsError(stopper <-chan struct{}) error {
return errors.New("foo")
}
可以看出我们要执行的函数的参数都必须是 chan 类型,然后定义的字段名是 stopper,其实定义为其他字段名也是一样的
如果只是想简单地将我们执行的函数超时就不执行了,那么直接定义完相应的执行函数和新建超时器进行执行即可,比如:
func takesFiveMillis(stopper <-chan struct{}) error {
time.Sleep(5 * time.Millisecond)
return nil
}
func takesTwentyMillis(stopper <-chan struct{}) error {
time.Sleep(20 * time.Millisecond)
return nil
}
func returnsError(stopper <-chan struct{}) error {
return errors.New("foo")
}
func TestDeadline(t *testing.T) {
dl := New(10 * time.Millisecond)
if err := dl.Run(takesFiveMillis); err != nil {
t.Error(err)
}
if err := dl.Run(takesTwentyMillis); err != ErrTimedOut {
t.Error(err)
}
}
但是如果你想在超时后执行一些操作,那么就可以利用我们执行函数的传入 stopper 参数,比如:
dl := New(10 * time.Millisecond)
done := make(chan struct{})
err := dl.Run(func(stopper <-chan struct{}) error {
<-stopper
// 可以在这里执行在超时后,进行的一些处理
close(done)
return nil
})·
if err != ErrTimedOut {
t.Error(err)
}
<-done
}
这里有一个大家可能会很疑惑的点,就是超时之后,Run 函数不应该是立即退出了吗?其实是的,超时之后就立即退出了,但是我们的 work 函数执行的时候是用协程去执行的,所以我们的 work 还是在执行中,
所以就需要 done 通道,去判断 work 函数是否执行完了,否则这个测试用例就直接退出了, 为了我这里说的是否正常,我们将测试用例注释相应的改动:
查看执行结果后,可以看出我这里所说的是正确的
这里大家可能还有一个疑惑点,就是我们这里的 stopper 阻塞在这里,那么超时之后,不是就进行关闭了吗?
这里就设计到 channel 的知识点了,读已经关闭的 channel 进行读操作的时候,依然是可以读的,我们这里的 stopper 是无缓冲区的 channel,那么读出来的就是相应类型的零值,但是如果是有缓存区的呢?如果关闭前,有缓存区的 channel 里面有数据,我们仍然能读出来,读到没有数据的时候,再读就是相应类型的零值。
这里我们引申一下,如果去写已经关闭的缓存区呢?这种情况就会 **panic:send on closed channel ****的情况,对于有无缓冲区的 channel 都是一样的结果
semaphore
golang 的信号弹性模式
创建信号量需要两个参数:
票数(一次发多少张票)。
超时(如果当前没有可用的票证,则等待多长时间)
开始分析前,我们需要先知道信号量的用途是什么,我用国内的智谱清言问了一下。
信号量的用途主要包括:
- 限制并发访问:通过信号量,可以限制同时访问某个资源或执行某个操作的并发 goroutine 的数量,防止过度的并发导致资源耗尽,如数据库连接、文件描述符或硬件设备等
- 流量控制:在处理大量请求时,可以用来控制请求处理的速率,防止后端服务因请求过多而崩溃
- 资源池管理:例如数据库连接池、线程池等,信号量可以确保池中的资源被合理地分配和回收
- 提高系统稳定性:在面临突发高流量或系统异常时,信号量机制能够保证系统按照预设的并发级别工作,避免雪崩效应
- 分布式系统中的协调:在分布式系统中,信号量可以用于不同服务或节点之间的协调,保证全局资源的合理使用
知道了信号量的用途后,我们来了解一下 go-resiliency 是如何实现信号量的
仓库给的例子:
sem := semaphore.New(3, 1*time.Second)
if err := sem.Acquire(); err != nil {
// could not acquire semaphore
// 无法获取信号量
return err
}
defer sem.Release()
创建信号量时,传入了两个参数,一个是票证的数量,一个是等待的时间
Semaphore 的结构如下:
// Semaphore implements the semaphore resiliency pattern
// 信号量实现信号量弹性模式
type Semaphore struct {
sem chan struct{} // 票证
timeout time.Duration // 等待时间
}
获取信号量
// Acquire tries to acquire a ticket from the semaphore. If it can, it returns nil.
// If it cannot after "timeout" amount of time, it returns ErrNoTickets. It is
// safe to call Acquire concurrently on a single Semaphore.
// Acquire 尝试从信号量获取票证。
// 如果可以,则返回 nil。
// 如果在“超时”时间后无法返回,则返回 ErrNoTickets。在单个信号量上同时调用 Acquire 是安全的。
func (s *Semaphore) Acquire() error {
select {
case s.sem <- struct{}{}:
return nil
case <-time.After(s.timeout):
return ErrNoTickets
}
}
// ErrNoTickets is the error returned by Acquire when it could not acquire
// a ticket from the semaphore within the configured timeout.
// ErrNoTickets 是 Acquire 在配置的超时时间内无法从信号量获取票证时返回的错误。
var ErrNoTickets = errors.New("could not acquire semaphore ticket")
释放信号量
// Release releases an acquired ticket back to the semaphore. It is safe to call
// Release concurrently on a single Semaphore. It is an error to call Release on
// a Semaphore from which you have not first acquired a ticket.
// Release 将获取的票证释放回信号量。
// 在单个信号量上同时调用 Release 是安全的。
// 在未首先获得票证的信号量上调用 Release 是错误的。
func (s *Semaphore) Release() {
<-s.sem
}
这个实现比较简单,创建信号量的时候,根据我们传入的票证数量,创建 sem 信号量通道,如果传入的票证数量不为 0,那么这个 sem 是带缓冲区的通道,获取信号量的时候,往 sem 写数据,如果缓冲区数据没有满,写成功后,返回 nil,成功获取信号量;如果 sem 已经满了,等到 select 到超时的那个分支时,返回 ErrNoTickets 的错误
释放信号量的时候,从 sem 通道读数据,释放缓冲区的数据,让后面尝试去获取信号量的,能够正常往 sem 写数据,从而成功获取信号量、
注意点1
这里要注意的点是,我们看代码信号量的代码还有另外一个函数
// IsEmpty will return true if no tickets are being held at that instant.
// It is safe to call concurrently with Acquire and Release, though do note
// that the result may then be unpredictable.
// 如果当时没有持有票证,
// IsEmpty 将返回 true。
// 与 Acquire 和 Release 同时调用是安全的,
// 但请注意,结果可能是不可预测的
func (s *Semaphore) IsEmpty() bool {
return len(s.sem) == 0
}
这里注意的点是,如果对 channel 不熟悉,可以会误以为这里判断的 sem 通道的长度,比如一下代码,我们创建票证数量的数为3
func TestSemaphoreEmpty2(t *testing.T) {
sem := New(3, 200*time.Millisecond)
t.Log(sem.IsEmpty())
if !sem.IsEmpty() {
t.Error("semaphore should be empty")
}
}
output:
=== RUN TestSemaphoreEmpty2
semaphore_test.go:71: true
--- PASS: TestSemaphoreEmpty2 (0.00s)
PASS
Process finished with the exit code 0
可以看到 IsEmpty 函数返回的是 true,即 len(s.sem) == 0 条件成立,是不是觉得很奇怪,其实当我们去判断 channel 的长度时,判断的是它的缓冲区的还未被读取的数据 (学到了吧 😃)
注意点2
在没获取到信号量的时候,进行释放信号量操作,会导致死锁
func TestSemaphoreEmpty(t *testing.T) {
sem := New(2, 200*time.Millisecond)
if !sem.IsEmpty() {
t.Error("semaphore should be empty")
}
sem.Release()
}
output:
=== RUN TestSemaphoreEmpty
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
testing.(*T).Run(0xc000051380, {0x91f60a?, 0x84d513?}, 0x929520)
C:/Program Files/Go/src/testing/testing.go:1630 +0x405
testing.runTests.func1(0xa13280?)
C:/Program Files/Go/src/testing/testing.go:2036 +0x45
testing.tRunner(0xc000051380, 0xc000117c88)
C:/Program Files/Go/src/testing/testing.go:1576 +0x10b
testing.runTests(0xc0000781e0?, {0xa0e1c0, 0x3, 0x3}, {0xc0001212a0?, 0x100c000117d10?, 0x0?})
C:/Program Files/Go/src/testing/testing.go:2034 +0x489
testing.(*M).Run(0xc0000781e0)
C:/Program Files/Go/src/testing/testing.go:1906 +0x63a
main.main()
_testmain.go:51 +0x1aa
goroutine 6 [chan receive]:
github.com/eapache/go-resiliency/semaphore.(*Semaphore).Release(...)
C:/code/go_code/good/go-resiliency/semaphore/semaphore.go:44
github.com/eapache/go-resiliency/semaphore.TestSemaphoreEmpty(0xc000051520)
C:/code/go_code/good/go-resiliency/semaphore/semaphore_test.go:56 +0x97
testing.tRunner(0xc000051520, 0x929520)
C:/Program Files/Go/src/testing/testing.go:1576 +0x10b
created by testing.(*T).Run
C:/Program Files/Go/src/testing/testing.go:1629 +0x3ea
Process finished with the exit code 1
Release 操作,是从 sem 通道读取数据,如果这时没有数据,就阻塞在这里的
注意点3,创建信号量的时候,不要误传了票证的数量为 0
func TestSemaphoreEmpty(t *testing.T) {
sem := New(0, 200*time.Millisecond)
if !sem.IsEmpty() {
t.Error("semaphore should be empty")
}
sem.Acquire()
sem.Release()
}
output:
=== RUN TestSemaphoreEmpty
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
testing.(*T).Run(0xc000051380, {0x87f60a?, 0x7ad513?}, 0x889520)
C:/Program Files/Go/src/testing/testing.go:1630 +0x405
testing.runTests.func1(0x973280?)
C:/Program Files/Go/src/testing/testing.go:2036 +0x45
testing.tRunner(0xc000051380, 0xc000117c88)
C:/Program Files/Go/src/testing/testing.go:1576 +0x10b
testing.runTests(0xc000078140?, {0x96e1c0, 0x3, 0x3}, {0xc000121378?, 0x100c000117d10?, 0x0?})
C:/Program Files/Go/src/testing/testing.go:2034 +0x489
testing.(*M).Run(0xc000078140)
C:/Program Files/Go/src/testing/testing.go:1906 +0x63a
main.main()
_testmain.go:51 +0x1aa
goroutine 6 [chan receive]:
github.com/eapache/go-resiliency/semaphore.(*Semaphore).Release(...)
C:/code/go_code/good/go-resiliency/semaphore/semaphore.go:44
github.com/eapache/go-resiliency/semaphore.TestSemaphoreEmpty(0xc000051520)
C:/code/go_code/good/go-resiliency/semaphore/semaphore_test.go:58 +0x9f
testing.tRunner(0xc000051520, 0x889520)
C:/Program Files/Go/src/testing/testing.go:1576 +0x10b
created by testing.(*T).Run
C:/Program Files/Go/src/testing/testing.go:1629 +0x3ea
Process finished with the exit code 1
这个原因跟注意点 2 一样,sem.Acquire() 获取信号量这一步是没有问题的,但是因为票证数量为 0,那么我们创建的信号量的 sem 字段是不带缓冲区的通道类型,这时候再去释放信号量的时候,就会阻塞在那里了
!!! select 的 case 是随机的,而 switch 里的 case 是顺序执行
retrier
golang 的可重构弹性模式。
创建重试器需要两个参数:
重试间隔的时间(隐含重试次数) 决定重试哪些错误的分类器
仓库给的例子:
r := retrier.New(retrier.ConstantBackoff(3, 100*time.Millisecond), nil)
err := r.Run(func() error {
// do some work
return nil
})
if err != nil {
// handle the case where the work failed three times
}
创建重试器时,传入了两个参数,一个是重试时间的间隔(它是一个 time.Duration 类型的数组,数组的长度就是它隐含的重试次数),另一个是分类器,可以决定哪些错误需要重试,哪些错误不需要重试。
重试器的结构体
// Retrier implements the "retriable" resiliency pattern, abstracting out the process of retrying a failed action
// a certain number of times with an optional back-off between each retry.
// Retrier 实现了 "可重试 "弹性模式,将重试失败操作的过程抽象为
// 重试一定次数,每次重试之间可选择后退。
type Retrier struct {
// 重试时间的间隔
backoff []time.Duration
// 分类器
class Classifier
// 基数
jitter float64
// 随机数种子
rand *rand.Rand
// 计算休眠时间的锁
randMu sync.Mutex
}
新建一个重试器的函数
// New constructs a Retrier with the given backoff pattern and classifier. The length of the backoff pattern
// indicates how many times an action will be retried, and the value at each index indicates the amount of time
// waited before each subsequent retry. The classifier is used to determine which errors should be retried and
// which should cause the retrier to fail fast. The DefaultClassifier is used if nil is passed.
// New 使用给定的后退模式和分类器构建一个 Retrier。后退模式的长度
// 每个索引的值表示每次重试前等待的时间。
// 每次重试前等待的时间。分类器用于确定哪些错误应重试,哪些错误应导致重试。
// 哪些错误会导致重试快速失败。如果传入的是 nil,则使用 DefaultClassifier。
func New(backoff []time.Duration, class Classifier) *Retrier {
// 如果分类器为 nil,则使用默认的分类器
if class == nil {
class = DefaultClassifier{}
}
return &Retrier{
backoff: backoff,
class: class,
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
如果传入的分类器为 nil,则使用默认的分类器
有三种不同的分类器
- 默认分类器
- 白名单分类器
- 黑名单分类器
默认分类器以最简单的方式对错误进行分类。如果错误为 nil,则返回 Succeed,否则返回 Retry
WhitelistClassifier 根据白名单对错误进行分类。如果错误为 nil,则返回 Succeed;如果错误在白名单中,则返回 Retry;否则,它将返回 Fail。
BlacklistClassifier 根据黑名单对错误进行分类。如果错误为 nil,则返回 Succeed;如果错误在黑名单中,则返回 Fail;否则,它将返回 Retry。
重试器的执行有两个函数
一个是执行时,不用传入上下文字段的,实际执行还是调用了需要传入上下文字段的 RunCtx函数,只是传了个非 nil 的空 Context
// Run executes the given work function by executing RunCtx without context.Context.
func (r *Retrier) Run(work func() error) error {
return r.RunCtx(context.Background(), func(ctx context.Context) error {
// never use ctx
return work()
})
}
一个是执行时,需要传入上下文字段的
// RunCtx executes the given work function, then classifies its return value based on the classifier used
// to construct the Retrier. If the result is Succeed or Fail, the return value of the work function is
// returned to the caller. If the result is Retry, then Run sleeps according to the its backoff policy
// before retrying. If the total number of retries is exceeded then the return value of the work function
// is returned to the caller regardless.
// 分类器对其返回值进行分类。
// 构造 Retrier 所使用的分类器对其返回值进行分类。如果结果是 "成功 "或 "失败",工作函数的返回值将
// 返回给调用者。如果结果是重试,运行将根据其后退策略休眠,然后再重试。
// 在重试之前休眠。如果超过了重试的总次数,则工作函数的返回值
// 返回给调用者。
func (r *Retrier) RunCtx(ctx context.Context, work func(ctx context.Context) error) error {
// 刚开始重试次数为 0
retries := 0
for {
// 执行工作函数(即我们想要进行处理的逻辑)
ret := work(ctx)
// 分类器根据返回值,判断是否需要重试
switch r.class.Classify(ret) {
case Succeed, Fail:
return ret
case Retry:
// 如果重试次数大于等于隐含的重试次数,返回工作函数的返回值
if retries >= len(r.backoff) {
return ret
}
// 如果重试次数小于隐含的重试次数,根据当前已重试的次数,计算休眠的时间
timeout := time.After(r.calcSleep(retries))
// 执行休眠函数
if err := r.sleep(ctx, timeout); err != nil {
return err
}
retries++
}
}
}
计算休眠时间的函数
这里不理解的是为什么要加锁,看了测试用例,有可能会并发执行 Run 函数,但实际有场景会用得上吗?
这里还有一个基数的作为休息时间的随机性种子,可以通过 SetJitter
函数设置,jitter 的范围在 [0,1],否则设置无效,设置了基数后,回退时间在一定的范围内,比如你设置了基数为 0.25, backoff[i] 为 10 * time.Millisecond,那么这时的回退时间在 (7500 * time.Microsecond,12500*time.Microsecond)的范围内
func (r *Retrier) calcSleep(i int) time.Duration {
// lock unsafe rand prng
r.randMu.Lock()
defer r.randMu.Unlock()
// take a random float in the range (-r.jitter, +r.jitter) and multiply it by the base amount
return r.backoff[i] + time.Duration(((r.rand.Float64()*2)-1)*r.jitter*float64(r.backoff[i]))
}
休眠函数
从代码可以看出,如果到达时间范围了会返回 nil,然后 RunCtx 函数增加重试次数,继续重试,如果传入的上下文有带超时时长,这时候超时时间到了,返回错误,RunCtx 直接退出,这点也就是使用 Run 和 RunCtx 函数的唯一区别
func (r *Retrier) sleep(ctx context.Context, t <-chan time.Time) error {
select {
case <-t:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
创建重试器,传递超时时长时,有三个辅助函数
- ConstantBackoff
- ConstantBackoff 生成一个简单的回退策略,即重试“n”次,并在每次重试后等待相同的时间。
- ExponentialBackoff
- ExponentialBackoff 生成一个简单的回退策略,即重试 'n' 次,并将每次重试后等待的时间加倍。
- LimitedExponentialBackoff
- LimitedExponentialBackoff 生成一个简单的回退策略,即重试 'n' 次,并将每次重试后等待的时间加倍。如果回退达到 'limitAmount' ,则此后回退将填充 'limitAmount' 。