go-kit微服务:服务熔断

在微服务架构中,单体服务被拆分为若干微服务,一个服务通常需要调用(网络方式)多个服务才能完成预期功能,服务的稳定性受其他服务整体稳定性的制约。若一个服务出现故障,将会影响服务消费方无法正常工作,并将影响逐步放大,甚至导致整个服务集群崩溃,也就是服务雪崩效应。

为防止服务雪崩,研发人员采用了流量控制、改进缓存、服务自动扩容、服务降级与熔断等方式。本文将介绍服务熔断,并使用go-kit+Hystrix实现微服务的熔断方案。

服务熔断

服务熔断是指调用方发现服务提供方响应缓慢或者不可用时,调用方为了自保直接失败,不再调用目标服务。考虑到服务提供方可能会恢复,在一段时间后会进行尝试访问。本质上这是一个“断路器模式”的应用,Martin Fowler有专门的文章对该模式进行讲解。通过下面的断路器开关状态图进行说明:

状态图

  • 初始状态为Closed,若请求一直成功,Closed状态将保持;若失败次数未超时设定阈值时,也将保持Closed状态;若失败次数达到设定阈值,将切换为Open状态。
  • Open状态下,调用方不再调用目标服务,直接失败返回。若达到设定的重试时间,状态切换为Half Open,允许部分请求进行尝试。若尝试成功,则切换为Closed状态,若失败则切换为Open状态。

针对服务熔断,业内使用的最多的当属Netflix退出的Hystrix,它为Spring Cloud构建的微服务提供了便利。以下引自官方对Hystrix的介绍:

Hystrix is a latency and fault tolerance library designed to isolate points of access to remote systems, services and 3rd party libraries, stop cascading failure and enable resilience in complex distributed systems where failure is inevitable.

Hystrix是一个延迟和容错库,旨在隔离对远程系统、服务和第三方库的访问点,停止级联故障,并在故障不可避免的复杂分布式系统中实现恢复能力。

本示例将使用Hystrix的go语言版本afex/hystrix-go实现服务熔断治理。

实战演练

本示例基于arithmetic_trace_demo更改,在gateway中增加服务熔断治理策略,register中除了临时增加一些故障模拟代码不做其他改动。

Step-0:代码准备

复制arithmetic_trace_demo目录,重命名为arithmetic_circuitbreaker_demo。

下载该示例所需的go依赖:

go get github.com/afex/hystrix-go
复制代码

修改docker/docker-compose.yml,增加hystrix-dashboard实例,内容如下:

version: '2'

  consul:
    image: progrium/consul:latest
    ports:
      - 8400:8400
      - 8500:8500
      - 8600:53/udp
    hostname: consulserver
    command: -server -bootstrap -ui-dir /ui

  zipkin:
    image: openzipkin/zipkin
    ports:
      - 9411:9411

  hystrix:
    image: mlabouardy/hystrix-dashboard:latest
    ports:
      - 8181:9002
复制代码

Step-2:新增gateway/router.go

开始之前先简单说下hystrix-go的命令模式,它提供了Do方法通过异步模式执行用户的业务逻辑,在执行成功或发生错误返回之前将被阻塞,定义如下:

func Do(name string, run runFunc, fallback fallbackFunc) error 
复制代码
  • name:为执行的命令名称,一般设置为请求的名称或者服务的名称。
  • run:业务逻辑方法,把对服务提供方的调用逻辑封装在该方法里面。
  • fallback:run运行过程中发生错误时的回调方法,一般做错误信息封装。

为了完成hystrix-go的调用,我把原来反向代理的逻辑封装到Do方法中,并通过HystrixRouter类型实现了ServeHTTP,在其中封装了链路追踪和服务发现逻辑(这么做仅仅为了演示)。

HystrixRouter定义和新建方法如下所示,主要对链路追踪、服务发现进行封装。

// HystrixRouter hystrix路由
type HystrixRouter struct {
	svcMap       *sync.Map      //服务实例,存储已经通过hystrix监控服务列表
	logger       log.Logger     //日志工具
	fallbackMsg  string         //回调消息
	consulClient *api.Client    //consul客户端对象
	tracer       *zipkin.Tracer //服务追踪对象
}

func Routes(client *api.Client, zikkinTracer *zipkin.Tracer, fbMsg string, logger log.Logger) http.Handler {
	return HystrixRouter{
		svcMap:       &sync.Map{},
		logger:       logger,
		fallbackMsg:  fbMsg,
		consulClient: client,
		tracer:       zikkinTracer,
	}
}
复制代码

接下来的主要逻辑将在ServeHTTP中实现,主要思路为:

  • 解析请求路径中的服务名称,检查是否已经加入Hystrix监控:若否,则配置信息,并缓存至服务列表,若是则跳过。
  • 在Do方法中封装服务发现、反向搭理、链路追踪逻辑。服务发现失败,返回错误信息;反向代理失败返回错误信息(这里新增了反向代理错误时的回调方法);

详细代码如下所示,可通过注释进行理解:

func (router HystrixRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	//查询原始请求路径,如:/arithmetic/calculate/10/5
	reqPath := r.URL.Path
	if reqPath == "" {
		return
	}
	//按照分隔符'/'对路径进行分解,获取服务名称serviceName
	pathArray := strings.Split(reqPath, "/")
	serviceName := pathArray[1]

	//检查是否已经加入监控
	if _, ok := router.svcMap.Load(serviceName); !ok {
		//把serviceName作为命令对象,设置参数
		hystrix.ConfigureCommand(serviceName, hystrix.CommandConfig{Timeout: 1000})
		router.svcMap.Store(serviceName, serviceName)
	}

	//执行命令
	err := hystrix.Do(serviceName, func() (err error) {

		//调用consul api查询serviceNam
		result, _, err := router.consulClient.Catalog().Service(serviceName, "", nil)
		if err != nil {
			router.logger.Log("ReverseProxy failed", "query service instace error", err.Error())
			return
		}

		if len(result) == 0 {
			router.logger.Log("ReverseProxy failed", "no such service instance", serviceName)
			return errors.New("no such service instance")
		}

		director := func(req *http.Request) {
			//重新组织请求路径,去掉服务名称部分
			destPath := strings.Join(pathArray[2:], "/")

			//随机选择一个服务实例
			tgt := result[rand.Int()%len(result)]
			router.logger.Log("service id", tgt.ServiceID)

			//设置代理服务地址信息
			req.URL.Scheme = "http"
			req.URL.Host = fmt.Sprintf("%s:%d", tgt.ServiceAddress, tgt.ServicePort)
			req.URL.Path = "/" + destPath
		}

		var proxyError error = nil
		// 为反向代理增加追踪逻辑,使用如下RoundTrip代替默认Transport
		roundTrip, _ := zipkinhttpsvr.NewTransport(router.tracer, zipkinhttpsvr.TransportTrace(true))

		//反向代理失败时错误处理
		errorHandler := func(ew http.ResponseWriter, er *http.Request, err error) {
			proxyError = err
		}

		proxy := &httputil.ReverseProxy{
			Director:     director,
			Transport:    roundTrip,
			ErrorHandler: errorHandler,
		}
		proxy.ServeHTTP(w, r)

		return proxyError

	}, func(err error) error {
		//run执行失败,返回fallback信息
		router.logger.Log("fallback error description", err.Error())

		return errors.New(router.fallbackMsg)
	})

	// Do方法执行失败,响应错误信息
	if err != nil {
		w.WriteHeader(500)
		w.Write([]byte(err.Error()))
	}
}
复制代码

Step-3:修改gateway/main.go

首先创建hystrixRouter对象,按照方法参数列表传递参数;然后将返回值使用zipkin服务中间件进行包装。

hystrixRouter := Routes(consulClient, zipkinTracer, "Circuit Breaker:Service unavailable", logger)

handler := zipkinhttpsvr.NewServerMiddleware(
	zipkinTracer,
	zipkinhttpsvr.SpanName("gateway"),
	zipkinhttpsvr.TagResponseSize(true),
	zipkinhttpsvr.ServerTags(tags),
)(hystrixRouter)
复制代码

为了通过hystrix-dashboard对服务进行监控,需要启用hystrix的实时监控服务,代码如下:

//启用hystrix实时监控,监听端口为9010
hystrixStreamHandler := hystrix.NewStreamHandler()
hystrixStreamHandler.Start()
go func() {
	errc <- http.ListenAndServe(net.JoinHostPort("", "9010"), hystrixStreamHandler)
}()
复制代码

好了,gateway的代码就修改完成了。

Step-4:运行&测试

运行

依次启动docker、register、gateway,然后使用postman的Runner工具进行测试。

#启动consul、zipkin、hystrix-dashboard
sudo docker-compose -f docker/docker-compose.yml up

./register/register -consul.host localhost -consul.port 8500 -service.host 192.168.192.146 -service.port 9000

./gateway/gateway -consul.host localhost -consul.port 8500
复制代码

Postman中新增一个collection,命名为circuitbreaker,新建post请求。打开Runner(左上角),选择新建的集合,设置时间间隔为100毫秒,迭代次数为1000(暂时不运行)。如下图:

Postman配置

打开浏览器,输入http://localhost:8181/hystrix,在输入框输入hystrix的监控地址http://192.168.192.146:9010(这里需要配置你的主机地址),然后启动监控,如下图:

Hystrix配置

Hystrix监控

准备工作完成了,下面开始测试。

测试

在Postman的Runner界面点击“Run circuitbreak”按钮,然后查看Hysytrix监控面板,会看到如下界面,断路器的状态为Closed

Hystrix正常

然后,关闭register服务(直接在终端停止,方便下面快速启动)。会发现断路器状态很快变为Open

Hystrix熔断

再开启register服务,断路器状态恢复为Closed

Hystrix恢复

分析

Hystrix默认设置的参数为:

  • DefaultErrorPercentThreshold = 50:请求失败比例达到50%时,断路器切换为Open状态。
  • DefaultTimeout = 1000:请求超过该时间即视为服务异常。我在代码中设置的也是1秒。
  • DefaultSleepWindow = 5000:在Open状态下,间隔5秒进行重试。

初始时register服务正常,所有请求顺利执行,所以为Closed状态;当关闭register(模拟故障)后,请求失败次数到达设定阈值时,切换为Open状态;服务恢复后,待Hystrix到达重试时机,服务恢复。

总结

本文在go-kit中使用hystrix-go为网关服务gateway增加了服务熔断治理方案,通过模拟register服务从“正常-故障-恢复”,在hystrix-dashboard中观察到断路器状态的变化。

在实际开发过程中,由于服务之间的依赖关系复杂,非常有必要为我们的服务增加服务熔断治理措施,确保及时止损,防止因单个依赖服务的故障影响所有业务线。本文仅作为服务熔断的入门,下来还需要深入研究Hystrix的熔断与降级机制。

本文参考

本文首发于本人微信公众号【兮一昂吧】,欢迎扫码关注!

猜你喜欢

转载自juejin.im/post/5c7e564651882546c846c101