如何在 Node.js 中更优雅地使用 gRPC:grpc-helper
在上一篇的 gRPC 的介绍以及实践 中,而在文末,我简单介绍了给 Node.js 做的 grpc-helper,但是现在,我觉得得用一篇完整的博客来好好介绍,毕竟还是想要给大家用的,以下我会介绍我实现这个工具的过程,以及我的一些实现思路。
其实在这之前,我看了官方的讨论,而且也调研了当中提到一些帮助类工具,比如 grpc-caller,因该说我不太喜欢这种 API 风格,不够简单明了,并且也没有我想要的一些高级功能。
另外就是 rxjs-grpc 了,只是它是基于 RxJS 来做的,如果你对它不熟悉,怕是也难以选择(当然,可以了解下,号称是可取代 Promise 的)。
因此我想了想,除了最重要的 Promise API 功能(毕竟 callback 的风格早就应该被淘汰了),我想要的功能主要有:
- 服务发现:比如支持 DNS 服务发现,其它的可以是 consul etcd 等;
- 客户端负载均衡:支持 Round roubin 负载均衡;
- 健康检查:支持上游的健康检查,剔除不健康的后端以及重新加入健康的后端
- 断路器:一旦上游出错了,能够及时断开;
- 监控指标:能够提供监控指标,方便发现以及处理问题;
好了,相信你也应该看出来了,我想要的无非就是负载均衡加上 Promise API,因为上面的几点都是一个负载均衡器应该做的事情。
实现的话,还是用 TypeScript,不明白的可以看看我之前的介绍:使用 TypeScript 开发 NPM 模块。
Promise API
于是首先是需要提供一个非常简便的 Promise API 接口,我们都知道 grpc 以客户端以及服务端是否使用了流分成了四种风格的接口:
- Unary:客户端 & 服务端没有流;
- Client stream:客户端有流,服务端没有流;
- Server stream:客户端没有流,服务端有流;
- Bidi stream:客户端 & 服务端都有流;
而在这四种接口中,只有 Unary 以及 Client stream 有返回值 callback 风格的接口,这从设计上也符合一致性的风格,只是我们不喜欢用而已。
因此,一开始,我是这么设计的:
将 callback 风格的
1 | client.SayHello({name: 'foo'}, (err, rst) => { |
变为
1 | const res = await client.SayHello({name: 'foo'}); |
但是我忽略了服务端返回的 status 以及 metadata,应该说大部分情况下,只是 response 就能满足大部分需求,但是我做的是一个比较基础的库,那就应该提供完整的功能,于是,我加入了下设计:
1 | const call = client.SayHello({name: 'foo'}, (err, rst) => { |
变为
1 | const { message, status, metadata, peer } = await client.SayHello({name: 'foo'}); |
这样也就非常简单明了了,实现起来也不难,我同时提供了 resolveFullResponse
参数,默认为 false,这样,大部分情况下,如果不需要 status 之类的返回值,只需要第一种设计,那基本上也不需要改动参数。
同时,我还参考了 @murgatroid99 在官方讨论中的设计,将 Client stream 接口也改成了 Promise 风格的接口:
1 | const stream = new stream.PassThrough({ objectMode: true }); |
负载均衡
应该说这是一个现代的负载均衡器应该做的事情,我参考了 grpc-go 的设计,引入了 Resolver Watcher 以及 Balancer 几个抽象接口。
- Resolver:目前主要是 static 以及 dns,static 即直接解析服务端的地址,而 dns 则是利用 Node.js 的 dns.resolveSrv 解析 Srv 记录(具体使用场景可参考这里);
- Watcher:即实时 watch 服务发现,及时更新服务端的记录;
- Balancer:即实现 Round robin 负载均衡算法,挑选可用的服务端;
而在上次的文章中,我也提到了 grpc-node 中,现在还没有实现负载均衡能力,而且它目前的实现,还不能很方便的提供给我们很方便定制这个功能的接口,于是,目前能做的便是直接给每个服务端生成一个 client,然后在这个基础之上进行负载均衡的实现。
于是,最初的设计是:
1 | class Helper() { |
但是显然这样不够简便,于是我直接在 helper 的 constructor 中加入了这些方法,使得初始化之后直接将方法绑定到 helper 上面:
1 | each(methodNames, method => { |
于是,我们最终的 API 就很简单了:
1 | helper.SayHello() |
其它的负载均衡功能限于篇幅不再详细介绍,可参考源码实现。
其它功能
主要是监控指标以及全局 deadline,我直接使用了 grpc-node 提供 interceptors,拿监控指标举例:
1 | const histogram = new promClient.Histogram({ |
你也可以根据自己的需求,禁用默认的监控指标,创建 helper 的时候将 metrics
设置为 false
,然后将自己实现的 interceptors 传入 grpcOpts 即可:
1 | const helper = new GRPCHelper({ |
总结
好了,总体来说,这个工具的实现不复杂,但是需要花费挺多精力去具体实现,同时我也觉得如果不在这里给这个工具好好宣传一下的话,很容易就会变成只有我自己使用的一个工具,一些问题也不会发现,工具本身也无法进一步发展。
同时,我也相信,我这个工具最终会被官方的功能所取代,但是如果官方能够采用或者参考我的设计的话,那也是不错的结果。
另外,工具现在正在我们的测试环境中使用,正式环境也有部分在使用,所以各位如果有机会也不妨试试。
最后,给个 Star 也是极好的 :P 。
首发于 Github issues: https://github.com/xizhibei/blog/issues/86 ,欢迎 Star 以及 Watch
本文采用 署名-非商业性使用-相同方式共享(BY-NC-SA)进行许可作者:习之北 (@xizhibei)
原链接:https://blog.xizhibei.me/zh-cn/2018/09/09/an-introduction-to-grpc-helper/