如何在 Node.js 中更优雅地使用 gRPC:grpc-helper

栏目: 服务器 · 发布时间: 6年前

内容简介:在上一篇的gRPC 的介绍以及实践 中,而在文末,我简单介绍了给 Node.js 做的其实在这之前,我看了另外就是

在上一篇的gRPC 的介绍以及实践 中,而在文末,我简单介绍了给 Node.js 做的 grpc-helper ,但是现在,我觉得得用一篇完整的博客来好好介绍,毕竟还是想要给大家用的,以下我会介绍我实现这个 工具 的过程,以及我的一些实现思路。

其实在这之前,我看了 官方的讨论 ,而且也调研了当中提到一些帮助类工具,比如 grpc-caller ,因该说我不太喜欢这种 API 风格,不够简单明了,并且也没有我想要的一些高级功能。

另外就是 rxjs-grpc 了,只是它是基于 RxJS 来做的,如果你对它不熟悉,怕是也难以选择(当然,可以了解下,号称是 可取代 Promise 的 )。

因此我想了想,除了最重要的 Promise API 功能(毕竟 callback 的风格早就应该被淘汰了),我想要的功能主要有:

  1. 服务发现 :比如支持 DNS 服务发现,其它的可以是 consul etcd 等;
  2. 客户端负载均衡 :支持 Round roubin 负载均衡;
  3. 健康检查 :支持上游的健康检查,剔除不健康的后端以及重新加入健康的后端
  4. 断路器 :一旦上游出错了,能够及时断开;
  5. 监控指标 :能够提供监控指标,方便发现以及处理问题;

好了,相信你也应该看出来了,我想要的无非就是 负载均衡加上 Promise API ,因为上面的几点都是一个负载均衡器应该做的事情。

实现的话,还是用 TypeScript,不明白的可以看看我之前的介绍: 使用 TypeScript 开发 NPM 模块

Promise API

于是首先是需要提供一个非常简便的 Promise API 接口,我们都知道 grpc 以客户端以及服务端是否使用了流分成了四种风格的接口:

  • Unary:客户端 & 服务端没有流;
  • Client stream:客户端有流,服务端没有流;
  • Server stream:客户端没有流,服务端有流;
  • Bidi stream:客户端 & 服务端都有流;

而在这四种接口中,只有 Unary 以及 Client stream 有返回值 callback 风格的接口,这从设计上也符合一致性的风格,只是我们不喜欢用而已。

因此,一开始,我是这么设计的:

将 callback 风格的

client.SayHello({name: 'foo'}, (err, rst) => {
  ...
});

变为

const res = await client.SayHello({name: 'foo'});

但是我忽略了服务端返回的 status 以及 metadata ,应该说大部分情况下,只是 response 就能满足大部分需求,但是我做的是一个比较基础的库,那就应该提供完整的功能,于是,我加入了下设计:

const call = client.SayHello({name: 'foo'}, (err, rst) => {
  ...
});

call.on('status', (status) => {});
call.on('metadata', (metadata) => {});

const peer = call.getPeer();

变为

const { message, status, metadata, peer } = await client.SayHello({name: 'foo'});

这样也就非常简单明了了,实现起来也不难,我同时提供了 resolveFullResponse 参数,默认为 false,这样,大部分情况下,如果不需要 status 之类的返回值,只需要第一种设计,那基本上也不需要改动参数。

同时,我还参考了 @murgatroid99 在 官方讨论 中的设计,将 Client stream 接口也改成了 Promise 风格的接口:

const stream = new stream.PassThrough({ objectMode: true });

const promise = helper.SayMultiHello(stream);

stream.write({ name: 'foo1' });
stream.write({ name: 'foo2' });
stream.write({ name: 'foo3' });
stream.end();

const result = await promise; // { message: 'hello foo1,foo2,foo3' }

负载均衡

应该说这是一个现代的负载均衡器应该做的事情,我参考了 grpc-go 的设计,引入了 Resolver Watcher 以及 Balancer 几个抽象接口。

  • Resolver:目前主要是 static 以及 dns,static 即直接解析服务端的地址,而 dns 则是利用 Node.js 的 dns.resolveSrv 解析 Srv 记录(具体使用场景可参考 这里 );
  • Watcher:即实时 watch 服务发现,及时更新服务端的记录;
  • Balancer:即实现 Round robin 负载均衡算法,挑选可用的服务端;

而在 上次的文章中 ,我也提到了 grpc-node 中,现在还没有实现负载均衡能力,而且它目前的实现,还不能很方便的提供给我们很方便定制这个功能的接口,于是,目前能做的便是直接给每个服务端生成一个 client,然后在这个基础之上进行负载均衡的实现。

于是,最初的设计是:

class Helper() {
  constructor() {
    const resolver = new Resolver(addr);
    const clientCreator = new ClientCreator()
    this.lb = new Balancer(resolver, clientCreator);
    this.lb.start();
  }
  getClient() {
    return this.lb.get();
  }
}
const helper = new Helper();
helper.getClient().SayHello()

但是显然这样不够简便,于是我直接在 helper 的 constructor 中加入了这些方法,使得初始化之后直接将方法绑定到 helper 上面:

each(methodNames, method => {
  this[method] = (...args) => {
    const client = this.lb.get(); // 从 balancer 获取 client
    return client[method](...args);
  };
});

于是,我们最终的 API 就很简单了:

helper.SayHello()

其它的负载均衡功能限于篇幅不再详细介绍,可参考源码实现。

其它功能

主要是监控指标以及全局 deadline,我直接使用了 grpc-node 提供 interceptors,拿监控指标举例:

const histogram = new promClient.Histogram({
  name: 'grpc_response_duration_seconds',
  help: 'Histogram of grpc response in seconds',
  labelNames: ['peer', 'method', 'code'],
});

export function getMetricsInterceptor() {
  return function metricsInterceptor(options, nextCall) {
    const call = nextCall(options);

    const endTimer = histogram.startTimer({
      peer: call.getPeer(),
      method: options.method_definition.path,
    });

    const requester = (new grpc.RequesterBuilder())
        .withStart(function(metadata: grpc.Metadata, _listener: grpc.Listener, next: Function) {
          const newListener = (new grpc.ListenerBuilder())
            .withOnReceiveStatus(function(status: grpc.StatusObject, next: Function) {
              endTimer({
                code: status.code,
              });
              next(status);
            }).build();
          next(metadata, newListener);
        }).build();

    return new grpc.InterceptingCall(call, requester);
  };
}

你也可以根据自己的需求,禁用默认的监控指标,创建 helper 的时候将 metrics 设置为 false ,然后将自己实现的 interceptors 传入 grpcOpts 即可:

const helper = new GRPCHelper({
  packageName: 'helloworld',
  serviceName: 'Greeter',
  protoPath: path.resolve(__dirname, './hello.proto'),
  sdUri: 'dns://_grpc._tcp.greeter',
  metrics: false,
  grpcOpts: {
    interceptors: [you-metrics-interceptor-here]
  }
});

总结

好了,总体来说,这个工具的实现不复杂,但是需要花费挺多精力去具体实现,同时我也觉得如果不在这里给这个工具好好宣传一下的话,很容易就会变成只有我自己使用的一个工具,一些问题也不会发现,工具本身也无法进一步发展。

同时,我也相信,我这个工具最终会被官方的功能所取代,但是如果官方能够采用或者参考我的设计的话,那也是不错的结果。

另外,工具现在正在我们的测试环境中使用,正式环境也有部分在使用,所以各位如果有机会也不妨试试。

最后,给个 Star 也是极好的 :P 。

首发于 Github issues: https://github.com/xizhibei/blog/issues/86 ,欢迎 Star 以及 Watch

本文采用 署名-非商业性使用-相同方式共享(BY-NC-SA) 进行许可 作者:习之北 (@xizhibei)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript

JavaScript

David Flanagan / O'Reilly Media / 2011-5-13 / GBP 39.99

The book is a programmer's guide and comprehensive reference to the core JavaScript language and to the client-side JavaScript APIs defined by web browsers. The sixth edition covers HTML 5 and ECMA......一起来看看 《JavaScript》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

SHA 加密
SHA 加密

SHA 加密工具