loadlj / blog

19 stars 6 forks source link

Hystrix Go相关 #25

Open loadlj opened 4 years ago

loadlj commented 4 years ago

微服务熔断与隔离

什么是微服务熔断与隔离

微服务熔断(circuit breaker) 可以理解为是一个保护自身服务以及其调用服务的开关。假设我们服务需要调用一个外部服务B,当B不可用的时候如果我们没有这个熔断器,请求本身还会一直打到B,相应自身系统的latency也会增加,从而造成整体服务的不可用,产生雪崩效应。如果加入了熔断机制,这时候有一个可靠的fallback,牺牲系统的部分准确性可以换来整体的可用性提高。 一些更加详细的介绍可以参考

Hystrix

Circuit breaker的实现有很多种,主流的解决方案大多都是基于Netflix的 Hystrix 来做的。 下面是Hystrix的概念图

一些概念

实现原理

这里以Golang的版本来作为参考,实现起来应该是有几个要点需要注意的: 最大并发的控制(queue实现),同步实现(hystrix.Do),异步实现(Hystrix.Go)。 代码实现并不复杂,先看 Hystrix.Go 的实现,简化的代码如下

go func() {
        case cmd.ticket = <-circuit.executorPool.Tickets:
            ticketChecked = true
            ticketCond.Signal()
            cmd.Unlock()
        default:
            ticketChecked = true
            ticketCond.Signal()
            cmd.Unlock()
            returnOnce.Do(func() {
                returnTicket()
                cmd.errorWithFallback(ctx, ErrMaxConcurrency)
                reportAllEvent()
            })
            return
        }

        runStart := time.Now()
        runErr := run(ctx)
        returnOnce.Do(func() {
            defer reportAllEvent()
            cmd.runDuration = time.Since(runStart)
            returnTicket()
            if runErr != nil {
                cmd.errorWithFallback(ctx, runErr)
                return
            }
            cmd.reportEvent("success")
        })
}()
go func() {
        timer := time.NewTimer(getSettings(name).Timeout)
        defer timer.Stop()

        select {
        case <-cmd.finished:
            // returnOnce has been executed in another goroutine
        case <-ctx.Done():
            returnOnce.Do(func() {
                returnTicket()
                cmd.errorWithFallback(ctx, ctx.Err())
                reportAllEvent()
            })
            return
        case <-timer.C:
            returnOnce.Do(func() {
                returnTicket()
                cmd.errorWithFallback(ctx, ErrTimeout)
                reportAllEvent()
            })
            return
        }
    }()

先判断cb状态是否允许请求,然后再尝试从ticketPool里面去拿一个ticket执行对应的命令,执行完就放回ticket。另外一个goroutine用来控制前一个goroutine的生命周期(判断是否超时,ctx结束等)。

Hystrix.Do 的方法较为简单,只有一些简单的逻辑判断就不贴代码了。

Pool的实现很简练,就是一个buffered channel,完整逻辑如下.

package hystrix

type executorPool struct {
    Name    string
    Metrics *poolMetrics
    Max     int
    Tickets chan *struct{}
}

func newExecutorPool(name string) *executorPool {
    p := &executorPool{}
    p.Name = name
    p.Metrics = newPoolMetrics(name)
    p.Max = getSettings(name).MaxConcurrentRequests

    p.Tickets = make(chan *struct{}, p.Max)
    for i := 0; i < p.Max; i++ {
        p.Tickets <- &struct{}{}
    }

    return p
}

func (p *executorPool) Return(ticket *struct{}) {
    if ticket == nil {
        return
    }

    p.Metrics.Updates <- poolMetricsUpdate{
        activeCount: p.ActiveCount(),
    }
    p.Tickets <- ticket
}

func (p *executorPool) ActiveCount() int {
    return p.Max - len(p.Tickets)
}

其他的像包括错误率统计以及allowSingleTest等逻辑,具体可以参考源代码,实现的都不复杂。

小结

基于Hystrix 的circuit breaker功能还是十分强大的,但是需要注意的是每一个请求都会额外产生两个goroutine, 统计错误率等一系列的方法也是比较吃CPU的,当服务对于性能要求较高的时候酌情使用。

在使用cb的时候也需要考虑到 goroutine的生命周期,合理利用Ctx去释放掉相应的资源,不然是会很容易造成goroutine leakd的。

hyndaniel commented 4 years ago

可以借鉴 踩踩