go-micro V2 从零开始(十)定制网关(2)——集成断路器Hystrix

本文相关代码: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

针对这两个报错,在代码注释中详细说明了我的解决思路希望能抛砖引玉额。


步骤

一、ResponseWriter

观察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)
}

二、hystrix plugin

2.1 编写插件

新建并编辑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
		}
	})
}

2.2 注册插件

继续修改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

支持一下


版权声明:本文为hotcoffie原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。