熔断器

客户端上的限流措施:熔断

如上图所示,熔断器存在三个状态:

  • 关闭(closed): 关闭状态下没有触发断路保护,所有的请求都正常通行
  • 打开(open): 当错误阈值触发之后,就进入开启状态,这个时候所有的流量都会被节流,不运行通行
  • 半打开(half-open): 处于打开状态一段时间之后,会尝试尝试放行一个流量来探测当前 server 端是否可以接收新流量,如果这个没有问题就会进入关闭状态,如果有问题又会回到打开状态

熔断器中比较典型的实现就是 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 的配置是按照每个 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 个请求之后就会重新去请求服务端

hystrix-go 核心实现

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

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