• Ukieweb

    佳的博客

    曾梦想仗剑天涯,后来工作忙没去。

服务熔断 之GO实现 hystrix-go 和 kratos

hystrix-go

熔断器中比较典型的实现就是 hystrix,Golang 也有对应的版本,我们先来看一下 hystrix-go 是怎么实现的

案例

先看一个使用案例,首先我们使用 gin 启动一个服务端,这个服务端主要是前 200ms 的请求都会返回 500,之后的请求都会返回 200

func server() {
	e := gin.Default()
	e.GET("/ping", func(ctx *gin.Context) {
		if time.Since(start) < 201*time.Millisecond {
			ctx.String(http.StatusInternalServerError, "pong")
			return
		}
		ctx.String(http.StatusOK, "pong")
	})
	e.Run(":8080")
}

然后配置 hystrix,hystrix.ConfigureCommand(command name, config) hystrix 的配置是按照每个 command 进行配置,使用的时候我们也需要传递一个 command,下面的配置就是我们的请求数量大于等于 10 个并且错误率大于等于 20% 的时候就会触发熔断器开关,熔断器打开 500ms 之后会进入半打开的状态,尝试放一部分请求去访问

func main(){
	hystrix.ConfigureCommand("test", hystrix.CommandConfig{
		// 执行 command 的超时时间
		Timeout: 10,

		// 最大并发量
		MaxConcurrentRequests: 100,

		// 一个统计窗口 10 秒内请求数量
		// 达到这个请求数量后才去判断是否要开启熔断
		RequestVolumeThreshold: 10,

		// 熔断器被打开后
		// SleepWindow 的时间就是控制过多久后去尝试服务是否可用了
    // 单位为毫秒
		SleepWindow: 500,

		// 错误百分比
		// 请求数量大于等于 RequestVolumeThreshold 并且错误率到达这个百分比后就会启动熔断
		ErrorPercentThreshold: 20,
	})
}

然后我们使用一个循环当做客户端代码,会请求 20 次,每一个请求消耗 100ms

func main() {
	go server()
	
  // 这里是 config 代码

	for i := 0; i < 20; i++ {
		_ = hystrix.Do("test", func() error {
			resp, _ := resty.New().R().Get("http://localhost:8080/ping")
			if resp.IsError() {
				return fmt.Errorf("err code: %s", resp.Status())
			}
			return nil
		}, func(err error) error {
			fmt.Println("fallback err: ", err)
			return err
		})
		time.Sleep(100 * time.Millisecond)
	}

}

所以我们执行的结果就是,前面 2 个请求报 500,等到发起了 10 个请求之后就会进入熔断, 500ms 也就是发出 5 个请求之后就会重新去请求服务端

image.png

hystrix-go 核心实现

核心实现的方法是 AllowRequest,IsOpen判断当前是否处于熔断状态,allowSingleTest就是去看是否过了一段时间需要重新进行尝试

func (circuit *CircuitBreaker) AllowRequest() bool {
	return !circuit.IsOpen() || circuit.allowSingleTest()
}

IsOpen先看当前是否已经打开了,如果已经打开了就直接返回就行了,如果还没打开就去判断

  • 请求数量是否满足要求

  • 请求的错误率是否过高,如果两个都满足就会打开熔断器

func (circuit *CircuitBreaker) IsOpen() bool {
	circuit.mutex.RLock()
	o := circuit.forceOpen || circuit.open
	circuit.mutex.RUnlock()

	if o {
		return true
	}

	if uint64(circuit.metrics.Requests().Sum(time.Now())) < getSettings(circuit.Name).RequestVolumeThreshold {
		return false
	}

	if !circuit.metrics.IsHealthy(time.Now()) {
		// too many failures, open the circuit
		circuit.setOpen()
		return true
	}

	return false
}

hystrix-go 已经可以比较好的满足我们的需求,但是存在一个问题就是一旦触发了熔断,在一段时间之类就会被一刀切的拦截请求,所以我们来看看 google sre 的一个实现

Google SRE 过载保护算法

image.png

算法如上所示,这个公式计算的是请求被丢弃的概率[3]

  • requests: 一段时间的请求数量

  • accepts: 成功的请求数量

  • K: 倍率,K 越小表示越激进,越小表示越容易被丢弃请求

这个算法的好处是不会直接一刀切的丢弃所有请求,而是计算出一个概率来进行判断,当成功的请求数量越少,K越小的时候image.png的值就越大,计算出的概率也就越大,表示这个请求被丢弃的概率越大

Kratos 实现分析

func (b *sreBreaker) Allow() error {
	// 统计成功的请求,和总的请求
	success, total := b.summary()

	// 计算当前的成功率
	k := b.k * float64(success)
	if log.V(5) {
		log.Info("breaker: request: %d, succee: %d, fail: %d", total, success, total-success)
	}
	// 统计请求量和成功率
	// 如果 rps 比较小,不触发熔断
	// 如果成功率比较高,不触发熔断,如果 k = 2,那么就是成功率 >= 50% 的时候就不熔断
	if total < b.request || float64(total) < k {
		if atomic.LoadInt32(&b.state) == StateOpen {
			atomic.CompareAndSwapInt32(&b.state, StateOpen, StateClosed)
		}
		return nil
	}
	if atomic.LoadInt32(&b.state) == StateClosed {
		atomic.CompareAndSwapInt32(&b.state, StateClosed, StateOpen)
	}

	// 计算一个概率,当 dr 值越大,那么被丢弃的概率也就越大
	// dr 值是,如果失败率越高或者是 k 值越小,那么它越大
	dr := math.Max(0, (float64(total)-k)/float64(total+1))
	drop := b.trueOnProba(dr)
	if log.V(5) {
		log.Info("breaker: drop ratio: %f, drop: %t", dr, drop)
	}
	if drop {
		return ecode.ServiceUnavailable
	}
	return nil
}

// 通过随机来判断是否需要进行熔断
func (b *sreBreaker) trueOnProba(proba float64) (truth bool) {
	b.randLock.Lock()
	truth = b.r.Float64() < proba
	b.randLock.Unlock()
	return
}

总结

可用性仅靠服务端来保证是不靠谱的,只有整条链路上的所有服务都做好了自己可用性相关的建设我们的服务 SLA 最后才能够有保证。今天我们讲了 hystrix-go 和 kratos 两种熔断的实现方式,kratos采用 Google SRE 的实现的好处就是没有半开的状态,也没有完全开启的状态,而是通过一个概率来进行判断我们的流量是否应该通过,这样没有那么死板,也可以保证我们错误率比较高的时候不会大量请求服务端,给服务端喘息恢复的时间。

摘自

https://lailin.xyz/post/go-training-week6-6-breaker.html



0
0
下一篇:过载保护-服务【限流】之-令牌桶 Token bucket

0 条评论

老佳啊

85后,大专学历,中原人士,家里没矿。

由于年轻时长的比较帅气,导致在别人眼里,我一直不谈恋爱的原因是清高,实则是自己的小自卑。最大的人生目标就是找一个相知相爱相容的人,共度余生。

和人相处时如果能感受到真诚,会非常注重彼此的关系,对别人没有什么心机,即使有利益冲突,一般也会以和为贵,因为在这个世界上,物质的东西,从来不会吸引到我。

特别迷恋那些大山大水,如果现在还能隐居,可能早就去了。对那些宏伟的有底蕴的人文景观比较不感冒。

从事于IT行业,却一直对厨房念念不忘,由于身材魁梧,总觉得自己上辈子是个将军,可惜这辈子没当兵,也不会打架。