前言:
我们这有几个grpc protobuf的特殊场景,开发的过程就是抓狂的过程,我期间把grpc的源代码和protobuf godoc api粗略看了一遍,又把grpc周边的组件过了一遍。😅 差不多把grpc和protobuf的动态访问及反射整明白了。
简单说就是你提供一个service/method 加 request json body, 我就可以帮你访问下游的grpc server api,不需要你引入import go pb,不管下游grpc server是unary还是stream模式。
该文章后续仍在不断的更新修改中, 请移步到原文地址 http://xiaorui.cc/?p=5815
哪几个场景?
第一个场景
动态的pb类型装换,场景是一个gateway网关,他会把接收到的grpc的请求组装到消息总线里,然后等待结果返回给grpc client客户端。
核心问题点是,如何动态的把grpc请求的protobuf转换成特殊的消息类型,然后再把接收到的请求返回给客户端。
最简单的方法是静态注册,我们一个个的定义好输入和输出的类型,然后再一个个的register注册。但是这样就涉及到一个问题,下游服务加了一个接口,那么每次上线都需要改代码。
// xiaorui.cc func (s *Server) StaticRegister() { account.RegisterAccountServer(s.gs, &service.AccountServer{}) asset.RegisterAssetsServer(s.gs, &service.AssetsServer{}) trade.RegisterTradeServer(s.gs, &service.TradeServer{}) sys.RegisterSystemServer(s.gs, &service.SystemServer{}) ... func (s *SystemServer) GetSystemConfig(ctx context.Context, _ *common.Empty) (*sysp.SystemConfig, error) { return &sysp.SystemConfig{}, nil } func (s *SystemServer) GetSymbolConfig(ctx context.Context, _ *common.Empty) (*sysp.SymbolList, error) { return &sysp.SymbolList{}, nil }
如何不改代码怎么实现动态的grpc注册?
protobuf有个descriptor描述符概念,描述符里会记录每个service / method的pb类型。
第二个场景
动态访问的逻辑,我这边已经用grpc stream实现了一个基于grpc协议的行情推送,内部使用完全没问题,但是让外面人去学习使用grpc的话,学习成本相对高。社区对于实时的api大多使用websocket。为了避免重复开发行情推送,那么我做了一个websocket2grpc的proxy代理层。
websocket代理层逻辑很简单,就是代理websocket协议去访问grpc stream bidi模式。
这里涉及到一个问题,就是需要在proxy层使用grpc client连接后端的grpc stream server。
如果下游不断的增加接口,那么我就需要不断的更改proxy的代码?
如何做到根据service/method和request json body,就可以访问下游服务?
社区里有个老外开源了grpc stub的反射访问库,https://github.com/jhump/protoreflect
开源grpcall模块
为了解决上面的问题,我自己封装了grpcall库,我们可以根据service/method获取所有的输入输出类型(protobuf),及通过stub动态构建客户端访问,及解析返回的结果。
开始开发grpcall的时候真是没啥思路,但是看了envoy,grpc-gateway源码后,真是豁然开朗。所以,grpcall里理所当然的吸取了grpc-gateway和grpcurl的一些思路和部分代码。
下次找个时间给大家聊聊envoy和grpc-gateway的实现原理,envoy在http 2 grpc的实现里用了不少高级手段,但grpc-gateway就显得霸道了,直接构建了一堆堆的硬编码,可以简单的理解为通过模板来生成可以适配http和grpc的go代码。
简单可以说,要么各种reflect逻辑,要么直接像protoc那样生成硬代码。
grpcall的代码已经扔到github里了,主功能从开发到测试也就花了两三天的时间,有兴趣的朋友可以看看,求个star https://github.com/rfyiamcool/grpcall
grpcall不仅实现了从descriptor动态解析类型,而且可以从grpc server的reflect api里获取类型数据并解析,但我们推荐使用descriptor描述符模式。对于grpc的动态方法调用,grpcall实现了grpc的四个模式,有unary,bidi, client stream, server stream模式。
使用grpcall反射库,如何优化上线模式?
如果下游增加了api,或者业务的protobuf有更新,那么我们只需要重新生成protobuf descriptor文件,然后给grpcall传递一个sighup更新信号,那么grpcall会重新加载descriptor文件。 😁
这样避免了服务和连接的终端,当然你也可以使用graceful reload的方案来规避该问题。😅
性能如何?
因为在grpcall里加入了一些缓存逻辑,避免频繁的获取pb类型。使用grpc client池规避单个grpc client的竞争压力及提高tcp层的吞吐。
简单压测了一下,使用grpcall动态访问相比原生少了有5%左右的qps。
总结:
一般来说,开发的库包只要涉及到反射和动态转换的字眼,代码都相对难搞,像json呀,orm呀,template模板引擎。
由于项目前期催的太紧张,grpcall没有好好的打磨一番,几乎没啥文档,有些模块写的相对简陋。好在,代码量本身不多,另外我在testing目录里给出了一个测试的样例,大家可以参考下。 见谅 !!!