Kitex 框架入门系列(2)Middleware

1,310 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

开篇

在第一期文章中,我们了解了 Kitex 的一些基本背景以及上手用法。其实作为开发者,很多时候我们更在意的是用法,比如业务有多种多样的需求,框架能否灵活支持。

从一个 RPC 框架的角度看,对于服务发现,负载均衡,编解码,元信息传递等能力,都可能根据场景有所区分,Kitex 作为一款在字节内部充分验证的优质框架,在扩展性上也非常优秀(这部分如果感兴趣,可以看看官方此前对 Kitex 扩展性设计方面做的分享,对开发者来说有很多借鉴意义 视频地址

作为框架的使用方,普通的业务开发者们,最经常使用的其实是 Kitex 提供的中间件扩展。今天我们来结合官方文档以及源码,看看这一点是怎样支持的,以及给出几个常见的案例。

Endpoint

Kitex 对于 Middleware 的定义在 pkg/endpoint/endpoint.go 目录下

// Endpoint represent one method for calling from remote.
type Endpoint func(ctx context.Context, req, resp interface{}) (err error)

// Middleware deal with input Endpoint and output Endpoint.
type Middleware func(Endpoint) Endpoint

Endpoint 和 Middleware 都是函数定义。我们可以看到,Endpoint 包含了RPC调用的上下文 context.Context,以及请求 req,响应 resp,返回一个 error 标识调用的状态。在调用发起前,resp 还是一个空对象,直到 invoke 远端的接口才会给 resp 赋值。

从便于理解的角度看,可以理解为,业务 server 所实现的类似下面这样函数签名的接口,本质上就是一个 Endpoint。

func(ctx context.Context, req interface{}) (resp interface{}, err error)

Middleware

从上一节的函数签名就可以看出,Middleware 就是基于 Endpoint 实现的扩展能力。

作为一个业务开发者,我们可能有各种各样针对「请求」,「响应」,「context.Context」,乃至耗时的观察处理,这样的业务诉求可能在请求前,也可能在拿到响应后。

Middleware 提供的能力就是对于 Endpoint 的嵌套,Context + req + resp + error 都给你,要做什么处理你直接自己看就 ok。开发者无需,也不能感知外层还有什么 Endpoint 包裹着自己。你只需要知道,在给你的这个 Endpoint 最内层,包含了实际执行 RPC 接口逻辑的那个 Endpoint 就好,你可以基于这个假设来开发自己的中间件逻辑。至于中间是不是还有别的 Middleware,乃至自己这个 Middleware 执行之后会不会还有别的更上一层的 Middleware,这些无需关心。

中间件是串连使用的,通过调用传入的 next,可以得到后一个中间件返回的 response(如果有)和 err,据此作出相应处理后,向前一个中间件返回 err(务必判断 next err 返回,勿吞了 err)或者设置 response。

通过源码里组合多个 Middleware 的函数也可以发现,这里就是链式调用,在你的 Middleware 中 return 的 Endpoint 就会作为外层 Middleware 的参数。

// Chain connect middlewares into one middleware.
func Chain(mws ...Middleware) Middleware {
	return func(next Endpoint) Endpoint {
		for i := len(mws) - 1; i >= 0; i-- {
			next = mws[i](next)
		}
		return next
	}
}

// Build builds the given middlewares into one middleware.
func Build(mws []Middleware) Middleware {
	if len(mws) == 0 {
		return DummyMiddleware
	}
	return func(next Endpoint) Endpoint {
		return mws[0](Build(mws[1:])(next))
	}
}

// DummyMiddleware is a dummy middleware.
func DummyMiddleware(next Endpoint) Endpoint {
	return next
}

// DummyEndpoint is a dummy endpoint.
func DummyEndpoint(ctx context.Context, req, resp interface{}) (err error) {
	return nil
}

生效逻辑

参考官方示例,我们先写一个 Middleware,假设我们需要打印请求和响应出来:

func PrintRequestResponseMW(next endpoint.Endpoint) endpoint.Endpoint {
    return func(ctx context.Context, request, response interface{}) error {
        fmt.Printf("request: %v\n", request)
        err := next(ctx, request, response)
        fmt.Printf("response: %v", response)
        return err
    }
}

上一节的 DummyMiddleware 大家可以注意一下,如果什么都不做,只是把原有的入参 Endpoint 返回,就相当于一个空实现,只是包了一层,没有任何逻辑。

那么在 PrintRequestResponseMW 里面,我们其实只是在调用 next 这个 Endpoint 前后打印了 request 和 response。(还记得么?这里的 err := next(ctx, request, response) 可以简化理解为就是实际 RPC 调用的接口逻辑)

下面我们来看看应该如何让中间件生效。

参考官方文档说明:

在扩展过程中,要记得两点原则:

  • 中间件和套件都只允许在初始化 Server、Client 的时候设置,不允许动态修改。
  • 后设置的会覆盖先设置的。

简单说,我们只能设置 server 或 client 级别的 middleware,一个 server/client 对应一组中间件,不能更细粒度,比如到方法级。下面我们分别看看怎样在双端设置中间件。

Server 中间件

server/option.go 里面提供了 WithMiddleware 函数,我们可以据此来添加 server middleware:

// WithMiddleware adds middleware for server to handle request.
func WithMiddleware(mw endpoint.Middleware) Option {
	mwb := func(ctx context.Context) endpoint.Middleware {
		return mw
	}
	return Option{F: func(o *internal_server.Options, di *utils.Slice) {
		di.Push(fmt.Sprintf("WithMiddleware(%+v)", utils.GetFuncName(mw)))
		o.MWBs = append(o.MWBs, mwb)
	}}
}

使用起来其实很简单,import "github.com/cloudwego/kitex/server",然后直接 server.WithMiddleware(PrintRequestResponseMW) ,随后在 NewServer 时传入即可,当然可以把 PrintRequestResponseMW 替换成任何符合业务场景的中间件。

image.png

(kstudy 就是第一篇文章中我们创建的 demo 项目)

这里的 Option 其实我们在系列第一篇文章也提到过,经典的设计模式,Kitex 提供了相关服务治理的能力,如果你需要,比如这个创建 server Middleware 的诉求,那么就直接一个 WithMiddleware 传入进来就ok。框架会感知这个 Opiton,应用到 server 的配置中。

// Option is the only way to config a server.
type Option struct {
	F func(o *Options, di *utils.Slice)
}

Client 中间件

跟 server 中间件类似,client middleware 也是通过 option 的形式配置的。

你需要 import "github.com/cloudwego/kitex/client",然后直接用 client.WithMiddleware(XXX) 即可。

// WithMiddleware adds middleware for client to handle request.
func WithMiddleware(mw endpoint.Middleware) Option {
	mwb := func(ctx context.Context) endpoint.Middleware {
		return mw
	}
	return Option{F: func(o *client.Options, di *utils.Slice) {
		di.Push(fmt.Sprintf("WithMiddleware(%+v)", utils.GetFuncName(mw)))
		o.MWBs = append(o.MWBs, mwb)
	}}
}

二者实现唯一的区别在于 F 依赖的 Options 结构体不同,一个是 *internal_server.Options,一个是 *client.Options。

实战场景

打印请求响应

func LogParam(next endpoint.Endpoint) endpoint.Endpoint {
	return func(ctx context.Context, req, resp interface{}) (err error) {
		fmt.Printf("request:\n%s", req)

		err = next(ctx, req, resp)
		if err != nil {
			fmt.Printf("request faild, error: %+v", err)
		}

		fmt.Printf("response:\n%s", resp)
		return err
	}
}

线上直接打印确实有风险,因为你不知道,或者知道了也可能不注意,请求体或者响应体可能会很大,每次全都打印其实是一个很耗性能的操作。另一点在于安全,直接在 server/client 层面全部打印,会导致你的日志里出现很多敏感信息,尤其是B端业务。数据的安全性如果不能得到保障,客户的信息随随便便一搜日志就全都明文出现,是有很大的隐患的。如果必须要做,请一定要评估业务场景,做好加密。

recover panic

func RecoverPanic(next endpoint.Endpoint) endpoint.Endpoint {
	return func(ctx context.Context, req, resp interface{}) (err error) {
		defer func() {
			if e := recover(); e != nil {
				const size = 64 << 10
				buf := make([]byte, size)
				buf = buf[:runtime.Stack(buf, false)]
				fmt.Printf("KITEX: panic in handler: %s: %s", e, buf)
			}
		}()
		return next(ctx, req, resp)
	}
}

打印慢请求


func SlowReqLogMW(next endpoint.Endpoint) endpoint.Endpoint {
        // 默认慢请求阈值为500ms
        var threshold int64 = 500
	return func(ctx context.Context, req, resp interface{}) (err error) {
		begin := time.Now()
		err = next(ctx, req, resp)
		took := time.Since(begin).Nanoseconds() / 1e6 // ms
		if took > threshold {
			fmt.Printf("slow request, cost=%v", took)
		}
		return err
	}
}

Suite 扩展

Suite 是一种更高层次的组合和封装,更加推荐第三方开发者能够基于 Suite 对外提供 Kitex 的扩展,Suite 可以允许在创建的时候,动态地去注入一些值,或者在运行时动态地根据自身的某些值去指定自己的 middleware 中的值,这使得用户的使用以及第三方开发者的开发都更加地方便,无需再依赖全局变量,也使得每个 client 使用不同的配置成为可能。

Suite 是对于 Option 和 Middleware(通过 Option 设置)的组合和封装。使得我们可以直接定制自己的业务插件,只要实现 Options() []Option 方法即可。

// A Suite is a collection of Options. It is useful to assemble multiple associated
// Options as a single one to keep the order or presence in a desired manner.
type Suite interface {
	Options() []Option
}

// WithSuite adds an option suite for server.
func WithSuite(suite Suite) Option {
	return Option{F: func(o *internal_server.Options, di *utils.Slice) {
		var nested struct {
			Suite   string
			Options utils.Slice
		}
		nested.Suite = fmt.Sprintf("%T(%+v)", suite, suite)

		for _, op := range suite.Options() {
			op.F(o, &nested.Options)
		}
		di.Push(nested)
	}}
}

Server 端和 Client 端都是通过 WithSuite 这个方法来启用新的套件。用法跟前一节提到的 Middleware 基本一样,在 client 以及 server 两个包下都提供了 WithSuite 的函数,直接用即可。 image.png

事实上字节内部也是依赖的开源 Kitex 实现,对于一些内部的配置,比如 mesh,限流,ACL 等,都是通过 Suite 实现的。只是调整了代码生成工具的逻辑,补充上 byted suite,这一点也体现了强大的扩展能力。生成的代码类似这样:

// NewServer creates a server.Server with the given handler and options.
func NewServer(handler XXXXService, opts ...server.Option) server.Server {
	var options []server.Option
	options = append(options, byted.ServerSuite(serviceInfo()))
	options = append(options, opts...)

	svr := server.NewServer(options...)
	if err := svr.RegisterService(serviceInfo(), handler); err != nil {
		panic(err)
	}
	return svr
}

附录