Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
xizhibei opened this issue Sep 9, 2018 · 7 comments
Open

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

xizhibei opened this issue Sep 9, 2018 · 7 comments

Comments

@xizhibei
Copy link
Owner

xizhibei commented Sep 9, 2018

在上一篇的 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 。

@xizhibei xizhibei changed the title 如何在 Node.js 中更优雅地使用 gRPC:你需要 gRPC helper 如何在 Node.js 中更优雅地使用 gRPC:grpc-helper Sep 15, 2018
@huan
Copy link

huan commented Feb 10, 2020

好东西,感谢 @xizhibei 兄弟分享。学习了!

@Vibing
Copy link

Vibing commented Apr 7, 2020

有个问题,grpc服务端和客户端都需要知道protocol buffers接口结构,如何保证两端protocol buffers接口结构统一呢?

@xizhibei
Copy link
Owner Author

xizhibei commented Apr 7, 2020

有个问题,grpc服务端和客户端都需要知道protocol buffers接口结构,如何保证两端protocol buffers接口结构统一呢?

  1. 为何要统一?两边都有一份 protocol buffers 接口结构 的前提下,保持兼容即可;
  2. 不存在所谓的时刻都共用一份 protocol buffers 接口结构,毕竟数据结构改了,相关的处理逻辑也得跟着改;

不过,还是不确定你想问什么,给我点上下文?

@Vibing
Copy link

Vibing commented Apr 7, 2020

hello.proto文件为例,grpc 的 server 端需要引用它,而 client 也需要用它。
如果 server 和 client 是两个物理主机, 那每个主机上都要有同样结构的hello.proto,如果小明维护server端,小黑维护client端,那么关于hello.proto的结构,他们俩就得每次进行沟通约定,我觉得这样很麻烦 所以才提这个疑问。

我也是最近才看 nodejs 的 rpc,看到 grpc 后才知道 protocol buffer,可能问的有点小白 请见谅

@xizhibei
Copy link
Owner Author

xizhibei commented Apr 8, 2020

这是必须的,客户端的更新特性决定了它的数据结构不能完全跟服务器同步,服务器需要对客户端的协议进行兼容。在不兼容的场景下,两边为了保证数据不丢失,服务端需要先更新,同时兼容旧协议,然后等待客户端更新后才能把旧协议删掉。

其实你问的更像是个协作问题,而不是技术问题,因为即使你用 RESTful API 也会这样,两边必须要通过沟通(口头、文档等)来进行同步。

两边分开的弊端就会增加沟通成本,这没有办法,要么两边进行磨合,以后协作起来更默契,要么进行全栈开发,一个人都开发客户端与服务端。

@xiaosimon
Copy link

xiaosimon commented Mar 19, 2021

请问我想在metadata里传值怎么操作呢

@xizhibei
Copy link
Owner Author

请问我想在metadata里传值怎么操作呢

https://github.com/xizhibei/grpc-helper/blob/90cb624ef39c5059c09c8ea48c4659ab053c0236/test/helper.test.ts#L303

另外,目前官方已经放弃 grpc-node 了,你需要试试新的了:https://github.com/grpc/grpc-node/tree/master/packages/grpc-js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants