在微服务架构中,单体服务被拆分为若干微服务,一个服务通常需要调用(网络方式)多个服务才能完成预期功能,服务的稳定性受其他服务整体稳定性的制约。若一个服务出现故障,将会影响服务消费方无法正常工作,并将影响逐步放大,甚至导致整个服务集群崩溃,也就是服务雪崩效应。
为防止服务雪崩,研发人员采用了流量控制、改进缓存、服务自动扩容、服务降级与熔断等方式。本文将介绍服务熔断,并使用go-kit+Hystrix实现微服务的熔断方案。
服务熔断是指调用方发现服务提供方响应缓慢或者不可用时,调用方为了自保直接失败,不再调用目标服务。考虑到服务提供方可能会恢复,在一段时间后会进行尝试访问。本质上这是一个“断路器模式”的应用,Martin Fowler有专门的文章对该模式进行讲解。通过下面的断路器开关状态图进行说明:
针对服务熔断,业内使用的最多的当属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
中除了临时增加一些故障模拟代码不做其他改动。
复制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 复制代码
开始之前先简单说下hystrix-go的命令模式,它提供了 Do
方法通过异步模式执行用户的业务逻辑,在执行成功或发生错误返回之前将被阻塞,定义如下:
func Do(name string, run runFunc, fallback fallbackFunc) error 复制代码
为了完成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中实现,主要思路为:
详细代码如下所示,可通过注释进行理解:
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())) } } 复制代码
首先创建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的代码就修改完成了。
依次启动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(暂时不运行)。如下图:
打开浏览器,输入 http://localhost:8181/hystrix
,在输入框输入hystrix的监控地址 http://192.168.192.146:9010
(这里需要配置你的主机地址),然后启动监控,如下图:
准备工作完成了,下面开始测试。
在Postman的Runner界面点击“Run circuitbreak”按钮,然后查看Hysytrix监控面板,会看到如下界面,断路器的状态为 Closed
:
然后,关闭register服务(直接在终端停止,方便下面快速启动)。会发现断路器状态很快变为 Open
:
再开启register服务,断路器状态恢复为 Closed
:
Hystrix默认设置的参数为:
初始时register服务正常,所有请求顺利执行,所以为Closed状态;当关闭register(模拟故障)后,请求失败次数到达设定阈值时,切换为Open状态;服务恢复后,待Hystrix到达重试时机,服务恢复。
本文在go-kit中使用hystrix-go为网关服务 gateway
增加了服务熔断治理方案,通过模拟register服务从“正常-故障-恢复”,在 hystrix-dashboard
中观察到断路器状态的变化。
在实际开发过程中,由于服务之间的依赖关系复杂,非常有必要为我们的服务增加服务熔断治理措施,确保及时止损,防止因单个依赖服务的故障影响所有业务线。本文仅作为服务熔断的入门,下来还需要深入研究Hystrix的熔断与降级机制。