本文相关代码:gitee
本章继续增强网关功能,集成之前已经在普通项目中使用过的断路器Hystrix。
集成这个插件,我在网上看到了很多个版本,但试运行发现他们都没有真正实现了熔断。虽然超时第一时间将错误信息反馈到前端,但是其实并没有真的断开请求,整个调用链仍处于阻塞中,等到阻塞结束你还会在网关日志中收到两条报错信息,大意是不必要的WriteHeader,以及返回信息大于header中声明的Length。
> http: superfluous response.WriteHeader call from github.com/gorilla/handlers.(*responseLogger).WriteHeader (handlers.go:65) > suppressing panic for copyResponse error in test; copy error: http: wrote more than the declared Content-Length针对这两个报错,在代码注释中详细说明了我的解决思路希望能抛砖引玉额。
观察plugin的处理函数http.Handler,他的两个参数w http.ResponseWriter, r *http.Request都来自于go语言http包,具备基础的http服务能力,但是对自定义插件并不友好。 首先,执行完h.ServeHTTP(w, r)(也就是交给下一个插件继续处理)并不能直观的获得运行结果和报错信息,也就无法触发熔断。 另外,熔断器自动熔断后需要返回熔断信息,此时如果在插件中调用了w.Write([]byte),就会和后续操作中的返回值操作造成冲突。 因此先编写一个http.ResponseWriter的子类来增强response扩展性。 新建并编辑go-todolist\common\util\web\response.go:
package web import "net/http" const ( SuccessCode = 200 FailCode = 500 ) // 标准返回结构 type JsonResult struct { Code int `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data"` } func Ok(data interface{}) *JsonResult { return &JsonResult{ Code: SuccessCode, Data: data, } } func Fail(msg string) *JsonResult { return &JsonResult{ Code: FailCode, Msg: msg, } } // http.ResponseWriter的子类(借用java概念) // 增加status字段以便 // 增加written字段 type ResponseWriterPlus struct { http.ResponseWriter // 在写操作之后通过状态判断调用是否成功 Status int // 判断是否已完成返回,避免重复写入 Written bool } // 重写父方法,记录返回状态码,同时避免重复写入 func (w *ResponseWriterPlus) WriteHeader(status int) { if w.Written { return } w.Status = status w.ResponseWriter.WriteHeader(status) } // 重写父方法,记录判断是否已完成返回,避免重复写入 func (w *ResponseWriterPlus) Write(data []byte) (int, error) { if w.Written { return 0, nil } w.Written = true return w.ResponseWriter.Write(data) }新建并编辑go-todolist/gateway/plugins/hystrix/hystrix.go:
package hystrix import ( "context" "github.com/afex/hystrix-go/hystrix" "github.com/coreos/pkg/httputil" "github.com/micro/micro/v2/plugin" "go-todolist/common/util/web" "log" "net/http" ) func NewPlugin() plugin.Plugin { return plugin.NewPlugin( plugin.WithName("hystrix"), plugin.WithHandler( handler, ), ) } func handler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 配置断路器 name := r.Method + "-" + r.RequestURI config := hystrix.CommandConfig{ Timeout: 300, } hystrix.ConfigureCommand(name, config) // 增强http.ResponseWriter // 利用重写的Write()和WriteHeader()保证只写入一次返回值的特性 newW := &web.ResponseWriterPlus{ ResponseWriter: w, Status: http.StatusOK, Written: false, } // 增强*http.Request // 为原有的请求上下文增加一个cancel()函数 ctx, cancel := context.WithCancel(r.Context()) newR := r.WithContext(ctx) if err := hystrix.Do(name, func() error { defer cancel() h.ServeHTTP(newW, newR) return nil }, func(err error) error { // 熔断后直接执行cancel()结束调用 // 执行此操作会看到一条报错日志:http: proxy error: context canceled // 因为我们事实上就是通过cancel()强行结束调用,因此属于正常情况 defer cancel() return httputil.WriteJSONResponse(newW, http.StatusBadGateway, web.Fail(err.Error())) }, ); err != nil { log.Println("hystrix breaker err: ", err) return } }) }继续修改main.go:
package main import ( "github.com/micro/micro/v2/client/api" "github.com/micro/micro/v2/cmd" "go-todolist/common/tracer" "go-todolist/gateway/plugins/auth" "go-todolist/gateway/plugins/hystrix" "go-todolist/gateway/plugins/opentracing" "log" "os" ) func main() { // 配置鉴权 err = api.Register(auth.NewPlugin()) if err != nil { log.Fatal("auth register") } // 配置断路器 err = api.Register(hystrix.NewPlugin()) if err != nil { log.Fatal("hystrix register") } cmd.Init() }这个插件已经是老朋友了,验证可以参考之前的章节,在task-srv接口中增加延时,这里不再赘述。
本章我们以plugin的方式为网关集成了断路器hystrix。再次建议读者更多的学习hystrix其他功能,实现更加完善的熔断控制。
到这里本系列的第二部分内容就结束了,本来还写了如何集成jeager,不过实测并不能和被调用的api等后续步骤聚合在一条调用链中,再加上集成到网关并不会对日常问题排查起到很大改善,因此把写好的文章删除了。分析原因发现虽然通过newR := r.WithContext(spanCtx)的方式向*request中注入了调用链信息,但下一步task-api服务的gin.context中并没有获取到这些信息,有兴趣的朋友可以自己试试追一下官方http代理的代码和gin.constext代码。
下个部分,我们会陆续介绍一些官方封装的便捷工具库,如读取yaml配置文件,k-v缓存等,这类功能你很可能在实际开发中已经有很好用的第三方库,或者自己封装了简单的使用工具,根据实际需要做一些了解即可。
原创不易,买杯咖啡,谢谢:p