本文相关代码:gitee
断路器是微服务体系中的保险丝,避免小的故障通过连锁反应造成整个系统瘫痪甚至损坏,这一章我们演示如何在项目中集成知名断路器Hystrix
。
为了实现集成,本章会涉及到go-micro wrapper的概念。
简单说,wrapper是一个函数装饰器,类似java springMVC中拦截器,或者gin 中间件的概念,可以无侵入的设定函数执行前后的行为,我们借此可以做很多工作,例如本章的熔断,还有后面即将介绍的鉴权、日志等。
更多关于wrapper的知识请自行搜索学习。
Netflix/Hystrix是一个由Netflix开源的容错库,旨在隔离指向远程系统,服务和第三方库的请求,杜绝级联故障。
Netflix/Hystrixgithub.com而afex/hystrix-go项目脱胎于Hystrix,是他的go语言实现 。
afex/hystrix-gogithub.com在正式开始编码前,我们还需要介绍一个micro拓展项目go-plugins
。
他是一个go-micro
的功能增强包,其中包含了很多独立的插件程序,以插件的形式为我们提供即插即用的服务功能扩展,你可以直接下载他们中的某一款,也可以参考这些实现自己编写自己的插件。
go-plugins
为我们分别实现了hystrix
和gobreaker
两款断路器的插件化,他们的调用方法完全一致。
以task-api
服务为例,改写之前的task-api/main.go
注意:
go-plugins
包是独立于go-micro
之外的,写完上述代码你还需要go mod tidy
下载相关依赖,不要漏掉路径最后的v2,否则会默认引入v1版本 。package main
import (
"github.com/gin-gonic/gin"
"github.com/micro/go-micro/v2"
"github.com/micro/go-micro/v2/registry"
"github.com/micro/go-micro/v2/registry/etcd"
"github.com/micro/go-micro/v2/web"
"github.com/micro/go-plugins/wrapper/breaker/hystrix/v2"
pb "go-todolist/task-srv/proto/task"
"log"
)
// task-srv服务的restful api映射
func main() {
etcdRegister := etcd.NewRegistry(
registry.Addrs("172.18.0.58:2379"),
)
// 之前我们使用client.DefaultClient注入到pb.NewTaskService中
// 现在改为标准的服务创建方式创建服务对象
// 但这个服务并不真的运行(我们并不调用他的Init()和Run()方法)
// 如果是task-srv这类本来就是用micro.NewService服务创建的服务
// 则直接增加包装器,不需要再额外新增服务
app := micro.NewService(
micro.Name("go.micro.client.task"),
micro.Registry(etcdRegister),
micro.WrapClient(
// 引入hystrix包装器
hystrix.NewClientWrapper(),
),
)
taskService := pb.NewTaskService("go.micro.service.task", app.Client())
webHandler := gin.Default()
// 这个服务才是真正运行的服务
service := web.NewService(
web.Name("go.micro.api.task"),
web.Address(":8888"),
web.Handler(webHandler),
web.Registry(etcdRegister),
)
// 配置web路由
router(webHandler, taskService)
service.Init()
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
// web路由实现部分略
// 详情见https://gitee.com/xieyu1989/go-micro-study-notes/tree/master/go-todolist4
func router(g *gin.Engine, taskService pb.TaskService){}
接下来验证一下成果。hystrix的默认请求超时阈值为1秒,修改task-srv/handler/task.go
的Search
函数,在第一行增加3秒延时:
func (t *TaskHandler) Search(ctx context.Context, req *pb.SearchRequest, resp *pb.SearchResponse) error {
// 增加3秒延时
time.Sleep(3 * time.Second)
...
}
然后用postman中调用localhost:8888/task/search
,就会收到hystrix的超时消息:
插件默认调用1秒熔断,最大并发数是10,我们可以进一步调整,注意这里需要引入github.com/afex/hystrix-go/hystrix
包,起别名hystrixGo
避免与hystrix
插件包名冲突:
import hystrixGo "github.com/afex/hystrix-go/hystrix"
...
app := micro.NewService(
micro.Name("go.micro.client.task"),
micro.Registry(etcdRegister),
micro.WrapClient(
// 引入hystrix包装器
hystrix.NewClientWrapper(),
),
)
// 自定义全局默认超时时间和最大并发数
hystrixGo.DefaultSleepWindow = 200
hystrixGo.DefaultMaxConcurrent = 3
// 针对指定服务接口使用不同熔断配置
// 第一个参数name=服务名.接口.方法名,这并不是固定写法,而是因为官方plugin默认用这种方式拼接命令name
// 之后我们自定义wrapper也同样使用了这种格式
// 如果你采用了不同的name定义方式则以你的自定义格式为准
hystrixGo.ConfigureCommand("go.micro.service.task.TaskService.Search",
hystrixGo.CommandConfig{
MaxConcurrentRequests: 50,
Timeout: 2000,
})
...
下面我们参考官方插件编写自己的Hystrix Wrapper,相对于官方插件,我们自定义调用失败时的降级操作。
创建并编辑task-api/wrapper/breaker/hystrix/hystrix.go
(这个路径也参考了官方)设置超时时间为2秒:
package hystrix
import (
"github.com/afex/hystrix-go/hystrix"
"github.com/micro/go-micro/v2/client"
pb "go-todolist/task-srv/proto/task"
"log"
"context"
)
type clientWrapper struct {
client.Client
}
// 这是我们真正需要实现的方法
func (c *clientWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
// 命令名的写法参考官方插件,服务名和方法名拼接
name := req.Service() + "." + req.Endpoint()
// 自定义当前命令的熔断配置,除了超时时间还有很多其他配置请自行研究
// 这些配置在wrapper调用时才执行,因此具有最高的优先级
// ---如果打算使用全局参数配置,请注释掉下面几行---
config := hystrix.CommandConfig{
Timeout: 2000,
}
hystrix.ConfigureCommand(name, config)
// ---如果打算使用全局参数配置,请注释掉上面几行---
return hystrix.Do(name,
func() error {
// 这里调用了真正的服务
return c.Client.Call(ctx, req, rsp, opts...)
},
// 这个叫做降级函数,用来自定义调用失败后的处理
// 一般我们可以选择返回特定错误信息,或者返回预设默认值
// 这个方法尽量简单,尽量不要加入额外的服务调用和IO操作,避免降级函数自身异常
func(err error) error {
// 因为是示例程序,只处理请求超时这一种错误的降级,其他错误仍抛给上级调用函数
if err != hystrix.ErrTimeout {
return err
}
// 直接返回默认的查询结果并记录日志
switch r:=rsp.(type) {
// 这个服务我只实现了search一个接口的调用*pb.SearchResponse
case *pb.SearchResponse:
log.Print("search task fail: ", err)
*r = pb.SearchResponse{
PageSize: 20,
PageCode: 1,
SortBy: "createTime",
Order: -1,
Rows: []*pb.Task{
{
Body: "示例任务"},
},
}
default:
log.Print("unknown err: ", err)
}
return nil
})
}
// NewClientWrapper returns a hystrix client Wrapper.
func NewClientWrapper() client.Wrapper {
return func(c client.Client) client.Client {
return &clientWrapper{
c}
}
}
然后修改main.go
的hystix包路径:
// 官方插件
// import "github.com/micro/go-plugins/wrapper/breaker/hystrix/v2"
// 自定义插件
import "go-todolist/task-api/wrapper/breaker/hystrix"
重启task-api
,然后用postman中调用localhost:8888/task/search
,会发现界面卡了2S才返回,并且没有报超时而是返回了预设结果:
本章我们以wrapper的方式为服务集成了断路器hystrix,实现了服务调用的熔断。
建议读者更多的学习hystrix其他功能,实现更加完善的熔断控制。
同时go-plugins
包中还有大量插件,分门别类等待咱们的使用和学习,仔细阅读文档,可以在项目中少造很多轮子: