再见,micro: 迁移go-micro到纯gRPC框架
micro是基于golang的微服务框架,之前华尔街见闻架构升级中谈到了我们是基于go-micro的后端架构,随着我们对服务网格的调研、测试和实施,为了打通不同语言之间的服务调用,我们选择了gRPC作为服务内部的通用协议。
go-micro框架的架构非常具有拓展性,它拥有自己的RPC框架,通过抽象codec,transport,selector等微服务组件,你既可以使用官方实现的各种插件go-plugins进行组装,又可以根据实际的情况实现自己的组件。然而,我们打算利用服务网格的优势,将微服务的基础组件下沉到基础设施中去,将组件代码从代码库中剥离开来。
这样一来,我们相当于只需要最简的RPC框架,只需要服务之间有统一、稳定、高效的通信协议,由于micro在我们新架构中略显臃肿,于是我们选择逐渐剥除micro。还有一个重要原因,我们选择的服务网格方案是istio,它的代理原生支持gRPC,而micro只是将gRPC当做transport层,相当于复写了gRPC的服务路由逻辑,这样有损于istio的一些特性,譬如流量监控等功能。
出于这些考虑,第一步需要将micro改成纯gRPC模式,这里的改造部分我们考虑只应该去更改基础库的代码,而尽量不要使业务代码更改,减少对已有逻辑的影响,和一些软性的譬如开发人员的工作量。
服务发现模块
istio的服务发现支持etcd、consul等,我们需要将其改成使用kubernetes的服务名进行访问。通过实现istio selector的方式,我们将RPC调用的服务名与k8s的服务名端口做映射。
1 | package istio |
这里注意的是由于服务之间调用需要指定service name和端口,所以在这里我们把端口设置了一个magic number。
传输模块
go-micro采用各种协议作为传输报文的工具,可以从transport中了解到,有http/grpc/tcp/utp等,我们曾先后使用过tcp、utp、gRPC,经过测试gRPC是其中最为稳定的。之前提到过gRPC只是作为传输信道,micro定义了自己的RPC接口,实现了RPC的路由、传输、重试等功能,通过自定义了protobuf的插件生成符合micro标准的proto文件。
为了向后兼容,在使用grpc替换原RPC时,我也需要根据protobuf文件生成新的golang代码,其中包括client端、server端代码的变更,实际上我的更新是针对protoc-gen-go,fork之后修改grpc/grpc.go部分,在func generateService()
中对Client端代码进行micro的适配。
Client
1 | func (c *someClient) DoSomething(ctx context.Context, in *SomeRequest, opts ...grpc.CallOption) (*SomeResponse, error) |
Server
Server端代码的更新除了接口的适配,有一个关注点是micro的接口设计是
1 | DoSomething(context.Context, *SomeRequest, *SomeResponse) error |
而gRPC是
1 | DoSomething(context.Context, *SomeRequest) (*SomeResponse, error) |
micro接口设计的好处是支持服务端缓存的应用,当服务端handler触发时,cache interceptor可以将response缓存,当缓存命中时可以将其返回。而由于gRPC的版本将response放在了返回值中,运行时无法将譬如redis中字符串格式的response解码成SomeResponse
,而micro版本由于将其放在了参数位置,所以可以通过
1 | json.Unmarshall([]byte(""), &SomeResponse{}) |
从字符串恢复response。
我们的做法是不改变接口的设计,而是在自动生成的golang代码中,在interceptor运行之前将response object塞入context。
1 | // Server |
Interceptor模块
一些interceptor的迁移工作,例如logger、cache、recover等。
注意事项
向后兼容
更新的过程开发人员实际上并不是特别感兴趣,我们SRE能做的是对他们基本上无改动,第一个版本不要让开发感知。虽然实际上测试时由于我们的变动,测试环境不断受到稳定性质疑。micro和gRPC的context传递不同
gRPC是基于http2,metadata是基于HTTP header传递,Client利用metadata.NewOutgoingContext(ctx, MD)
携带metadata,而Server端利用metadata.FromIncomingContext(ctx)
提取metadata。而micro则用context.WithValue(metaKey{}, MD)
传递。使用环境变量做一些特殊处理
我们通过环境变量在基础库里做了一些判断,在新老架构并存下进行一些特殊处理,譬如刚才说到的metadata获取逻辑。关于proto自动生成
我们通过在CI中配置istio版本镜像的编译打包逻辑和原有逻辑共存,在编译前我们运行proto编译工具,生成gRPC的golang文件,从而打出新的镜像。
总结
micro陪伴了我们一年多的时间,期间它的架构设计给了我们很大的弹性可以去适配一些我们的架构,可以选择很多好的开源工具,譬如zipkin、prometheus、etcd等,适合于刚上微服务不久的项目进行技术摸索和选择,而这一年的时间,我们的架构也逐渐明晰和稳定,我们更倾向于一个精简的基础库,并且由于在线项目的不断增加,运维成本可以通过下沉基础组件进行降低。