从 0 到 1 构建基于 Node.js 的 RPC 服务

 
 

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

大家好,非常高兴能够与大家分享有关 Node 的一些话题。本次分享的主题是《从 0 到 1 构建基于 Node.js 的 Rpc 服务》。

可能对于前端的同学来说,这个话题可能不太熟悉,或者对于从事 Node.js 开发的同学来说,可能并没有广泛运用。但在进行 Node.js 微服务时,我们通常会用到这些内容,特别是涉及到 Rpc 服务的时候。虽然我们说是从 0 到 1,但实际上,我们是从一个基础的 Node.js 的 HTTP 服务开始,然后通过一些简单的改造,使我们的 HTTP 服务能够直接支持 RPC 服务,从而实现与公司,或者说与 Java 微服务体系的整合。

简单自我介绍一下,我叫喻哲常,目前在长沙兴盛优选技术体验部担任前端客户开发。我的专长主要集中在 Web 和 Node.js 方面。目前正在从事两个项目,一个是关于前端工作台的项目,涵盖了一些工作流和 CICD 相关的内容。

51c363bc8e0b1441bad4f8753f6afe24.png

今天的内容将分为三个部分。

  • 第一部分将介绍我们项目的业务背景和一些现实问题,因为这是一个时间类的话题。

  • 第二部分将涉及我们探索和实践的过程

  • 第三部分将进行思考和总结。

ade453a2fc6021a0e6fc79f51b3ef34d.png

业务背景&客观问题

业务背景

我们首先看一下我们具体的业务背景和一些客观问题。在这张图中,为了方便大家更好地理解今天的分享,我使用了一些类似的图表,可以在这里清晰地看到。

d11104aaf4a4c120c90de07969d9809f.png

整个业务背景是我们大前端业务团队,包括 C 端的 APP、C 端的小程序,以及 B 端的部分物流项目,都需要一个 AB 实验平台来进行产品实验。由于我们本身就是前端团队,我们对业务需求能够比较精准地把握,所以 AB 实验平台完全由我们前端团队主导完成,也就是说,它落到了我们小组的责任范围。

在建设完 AB 实验平台后,最初我们完全基于 HTTP 服务提供整个服务,也就是前端通过 SD 服务直接获取所需的实验。当然,这里所说的 AB 实验平台只是一个类比,我们当前的 Node.js 项目是基于一个 AB 实验平台做的,所以我会用它来给大家做一个示例。

后面随着实验平台的完善和业务团队产品需求的不断更新,一些具体场景下,前端通过 SD 服务获取实验已经不能够满足需求。变化是,我们必须在业务后端先获取实验结果,然后再决定返回什么内容给前端。这时,业务后端就需要以一种较好的方式通过我们的实验平台获取结果。由于我们的业务后端全部使用 Java 构建在微服务之上,因此他们提出了一些要求,希望我们能够提供一个统一的 RPC 接口给他们使用。

对于这个问题,最初我们觉得会有一些压力,因为我们对此并不是特别熟悉。之前对 RPC 也没有过多的研究,但我们仍然愿意去尝试。为什么呢?首先,如果我们完成了这个任务,那等于是我们就直接进入了业务后端的整个微服务的体系,并且我们整个变成了他们的上游,这肯定会给到我们整个 Node.js 团队打造一声非常有力的鸡血,所以我们肯定愿意去做这个事情。

直接使用HTTP?

讲到这儿其实会有一个疑问,可不可以直接使用 HTTP?那肯定是可以的。那只是说刚才讲到的我们业务后端的整个的意愿是吧?以及我们自己的寻求的这种发展来驱动着我们会去做一个更好的选择。当然我们使用 HTTP 的话还有一个更重要的两个,两个点一个是关于性能的,就是 C 端的优后端对性能是有比较极致的追求的,稍微能好一点就好一点,他们肯定会更加希望我们的性能会更好,但明显 HTTP 的性能会满,那个不是特别满足他们的需要。另外的话就是我们能够满足公司微服务体系,这也是一个非常重要的点。

0bb704eba8f3c78e734652ca806518dd.png

什么是 RPC?

刚才其实也说到可能我们前端的同学对 RPC 不是那么的了解了,所以我们在这个讲这个实践的过程之前,我们还是先稍微了解一下 RPC 到底是一个什么东西。用非常简单的话来说,RPC 全程我们叫做远程服务调用,就 remote producer,,它其实是一种框架,或者说是一个架构,也可以归纳成是一种方法思想,但它并不是实际上的具体的协议,它的目标是为了让我们在嗯,调用远程服务的时候更加简单透明。

当涉及到在两台服务器之间进行方法调用时,我们可以打个比方,假设我有两台服务器,通过 a 服务器调用 b 服务器的方法。在这个过程中,需要经过一个网络层,但我希望调用远程服务的体验能够与调用本地方法一样。这种场景就被称为 RPC(远程过程调用),这是一个通俗的解释。目前,RPC 框架一般由三部分组成。

acd2689fecfeafd1c2add8d3114cb7f9.png

首先是像我们的 Websocket 一样的服务提供者,称为 Server。然后有一个注册中心,服务提供者通过注册中心进行注册,告诉注册中心它有一个服务。最后是服务消费者,也就是客户端。客户端通过注册中心获取对应的服务提供者的地址,然后通过服务提供者进行调用。这三部分构成了 RPC 框架。

接下来我们看一下整个 RPC 调用的过程以及调用链路。在这里,我简单画了一个链路图,以便大家更好地理解。调用过程如下:

  1. 客户端发起请求,对请求进行编码,通常编码为二进制形式。

  2. 编码完成后,将整个请求对象发送给客户端的存根,即客户端存根。

  3. 客户端存根是从各个中心获取的,获取一次后会缓存在本地,不需要每次都去中心获取。

  4. 请求通过网络发送到服务端,服务端同样有一个存根。

  5. 服务端存根接收到请求后进行解码,调用具体的执行,然后返回原始响应。

  6. 在响应过程中,经历了编码,通过网络发送给客户端。

  7. 客户端接收响应,进行解码,获取执行结果。

这就是 RPC 的调用流程。当然,这里只是简单介绍了 RPC 的工作原理。如果你对 RPC 这一块有更深层次的兴趣,可以查找其他有关 RPC 的资料进行学习。在 Node.js 开发者看来,这部分内容可能会有一定的意义。

7ecea0377c604c5ad03675d34df80773.png

RPC vs HTTP

接下来,我们来简单比较一下 RPC 和 HTTP,特别是 RPC 与 SDP 协议之间的区别,这是我们比较关注的一个方面。首先,最大的区别可能在于底层协议。一般来说,RPC 使用的底层协议是基于 TCP 的,当然,有趣的一点是,RPC 也可以使用 HTTP 来实现,而 SDP 服务则基于 SDP 协议。

我们知道 HTTP 协议也是基于 TCP 实现的,在效率方面,RPC 可以完全使用自定义的 TCP,因此其报文体积会非常小,效率自然更高。而 SDP 协议则会包含很多层,因为它基于多层封装,对 SDP 的整个请求可能会包含一些没有用的内容。不过,我们现在可以基于 2.0 版本来进行封装,通过对 2.0 版本的优化,封装 RPC 服务时将带来一些改变。使用 2.0 版本封装 RPC 服务的最大区别在于,RPC 框架更多地体现在服务治理方面。

在性能消耗方面,因为 RPC 通常基于高效的二进制传输,其传输效率会比 HTTP 更高。由于 HTTP 通常使用 JSON,其体积和序列化耗时都比二进制更加消耗性能。至于负载均衡,我们知道 HTTP 的负载均衡通常需要配置代理服务器,即使使用原 CLB 或者维护的 Nginx,都需要一层服务器来处理。而 RPC 通常会自带负载均衡策略,这一点相对较为方便。

另一个关键的区别是服务治理,即如何有效地管理许多服务,以减少对其他上游服务的影响。RPC 可以实现自动通知,不会影响除了自身需要动的服务之外的其他服务。而 SDP 则需要主动监控,如果要对某个服务进行升级或维护,需要事先通知并修改服务代理来进行相关操作。

14612dda459c90208a816e1ca1ea81d8.png

从上面的比较来看,RPC 和 HTTP 还是存在一些区别的。 目前市场上比较成熟的一些 RPC 框架有 gRPC、Apache 的 Thrift 和 Dubbo,其中 Dubbo 最初是由阿里巴巴开发的,后来贡献给 Apache。 另外,还有我们熟悉的 Spring Cloud,其中 Dubbo 在市场上相对较为常用,许多 Java 后端微服务都基于 Dubbo 构建。
b21f843d0c1f827666fe94274823e93f.png

DUBBO

关于 Dubbo,这是一张我从官网截取的图,它被描述为一个为微服务提供框架通信服务治理能力的框架,易用且高性能。Dubbo 提供 Web 或 RPC,同时为企业提供服务发现、流量治理、可观测、认证和鉴权等能力。

Dubbo 3 已经在阿里巴巴内部广泛使用,成功替代了多年的 HSF 框架。HSF(Hypertext Service Framework)是阿里巴巴为服务共享而开发的框架。在后端使用 Dubbo 后,对于前端的落地,我们选择以 Dubbo 作为示例,或者说作为我们实践的目标。当然,我们也要确保与 Dubbo 的兼容性。

2519b4da371a7580692a00d463e7a795.png

探索与实践

基于 KOA.JS 的 HTTP 服务

在我们的探索和实践过程中,首先了解了 RPC 的基本概念。接下来,我们回顾了原有的服务框架,该框架是基于传统的 KOA.JS 服务的。这是一个传统的 HTTP 服务框架的架构。我们看一下整体的架构,首先客户端发送 SCP 请求,经过 CLB 负载均衡,命中一台机器,最终获取数据并返回给客户端。

1a24f484518f7cd38ba185831453d2d4.png

我们使用的 Node.js 框架可能会有一些自己的改动和中间件,但是在今天的讨论中,我们强调不同的 Node.js 框架只需要进行一些简单的改动和改造,就可以使 SCP 服务兼容 RPC 服务或具备 RPC 服务的能力。

目标

我们的目标是建立在基于 Dubbo 的微服务体系下,同时支持前端的 HTTP 直接调用。在这个基础上,我们考虑了两个选择,一是创建一个独立的服务,二是考虑统一的服务来支持 HTTP 和 RPC。我们选择了后者,以实现统一并支持两种调用方式。

8cc39d7ce6039ca2030a3427dfd9f1f8.png

统一的 API 路由注册

接下来,我们具体探讨了如何实现这个目标。首先,我们对 HTTP 服务进行了改造,不再单独注册路由,而是采用动态 API 路由的注册方式。这种方式通过传递路由参数,构建路由地址,实现了一种统一的、动态的 API 路由注册方式。这个方式灵感来自于 Java 的 Spring 框架。

55e2bf72ff058bcdb33ec413d50b1456.png

将 HTTP 服务统一分发到业务方法

在第二步中,我们看到了如何在 process 类中处理这些路由参数。简单来说,我们通过 Promise 获取参数,并在process 类中构建路径。这里示范了一个简单的单一路由的例子,当然,我们也可以单独注册其他特殊路由,以实现更灵活的处理。

然后在 control 中,我们接收了两个参数,并进行了处理,将其引入并进行了其他代码和处理。在这个处理中,我们对客户端传递过来的查询参数或者 body 参数进行了统一处理,然后通过调用该类的 call 方法执行对应 Node.js 文件中的某个方法。通常情况下,我们只会有一个方法,通常是 action,我们直接执行这个文件中 action 方法,然后将其返回。

ac71c7ef351fa4d0b4ad699a602fab9e.png

剖离业务方法

接下来,我们具体看了一下命中路由的方法,该文件中的路径可能对应 control 下的某个文件,我们可以有多个这样的文件。以一个简单的例子来说,假设有一个 project,路径为 detail,detail 文件中有一个查询 project 的 action方法,接收一个 ID 参数。我们通过这个统一的方法执行了这个文件中的 action 方法。

首先,对于这个接口,我们对其进行了一些规则上的处理。这个部分是可以进行扩展的,大家可以发挥一下想象力,我们可以定义很多参数,在这个位置对接口进行限制、验证,甚至还可以进行一些构建等操作。接着,我们看了一下action方法,实际上就是接收到了参数之后,按照需要的业务逻辑进行处理,包括查询库、查询risk,进行计算,最后统一返回一个内容。关于返回的内容,今天我们也讲到了规范的问题,然后直接将其返回。

d5df08df08fba44531f03b039c4146f0.png

对 HTTP 的改动,总结就是通过以下步骤完成:

  1. 统一注册 API 路由。

  2. 将 HTTP 服务统一分发到业务方法中,通过动态 require。

  3. 将业务方法剥离出来,使每一个路由都对应一个独立的文件和方法。

完成了这些改动后,我们理解到接下来加入 RPC 的目的很明确,就是让 RPC 也像 SEP 一样,通过路径判断执行相应的 action 方法,实现两种服务的同样执行效果,执行的是同一份代码。

经过改造后

在这个改动之后,我们再来看一下变成了什么样子。首先,客户端发起请求,例如路径是/API/project/detail,这个路由命中之后会经过我们的 Node.js 中间件,然后到了我们的 process.js 进行分发。接着,我们会动态地命中相应的文件,这个时候它是一个文件,我们会去执行这个文件下的 action 方法。每个文件都是一个类,执行完之后直接返回,这样我们的维护变得非常简单。每当要添加一个接口时,只需要在 control 文件夹下添加对应的内容即可,而且大量的同步代码都是不需要编写的,只需关注核心逻辑部分。

a482d58a119c8ec1a9ab9b072b933337.png

注册 Dubbo

经过上述步骤,我们开始考虑在这个基础上注册 Dubbo 服务。服务的注册在这里变得非常简单。我们不是主动实现一个 RPC 服务框架,而是通过一个低成本的方式将我们的 HTTP 服务改造成一个具备 RPC 协议的服务,可以是Dubbo,也可以是其他的 RPC 服务。

在这里,我们引入了 dubbo.js。可能有同学会疑问,为什么不直接实现一个 RPC 服务框架呢?实际上,这就好比我们使用 Node.js 框架一样,自己动手实现一个,虽然理论上是可行的,但在短时间内支持庞大的生产业务,其实是有一定风险的。上面的代码看起来可能会有一些冗长,但其核心代码实际上很简短。上面的代码涉及到Dubbo 应用的注册,而每个 Dubbo 框架的文档都会详细说明注册的过程。

在这个地方,我们使用了 dubbo.js 作为注册中心,它的文档里面有很清晰的说明,实际上就是通过这个注册中心将我们的方法注册进去。注册中心的选择可能对于前端同学来说,zookeeper 这个名词可能不太清楚,但它实际上是一个分布式的管理工具。在注册的位置,我们的目的是将我们的方法注册进去,就是将接口地址添加到我们的注册中心,非常简单。

接下来,我们会将服务提供者注册进去,类似于使用 node socket,我们会监听一系列事件,需要按需处理这些事件。最关键的部分在这里,就是我们在这个位置会收到请求,服务会接收到他们发过来的参数,这个地方需要对这些参数进行处理。

接下来我们关注的重点是在这个红框中,我们将从这个 service 中选择一个方法。可以看出,这个 service 下实际上定义了我们 Controller 下的所有方法,我们将选择其中的一个方法。因为我们传递了一个方法,也就是接口地址,我们要调用哪个方法?这就是 RPC 的调用方式,我们传递过来之后直接执行它,执行之后完成与我们 HTTP服务一致的逻辑,并将结果返回。这时整个服务提供者已经算是完成了。

此外,调用 Java 服务的方式与这里完全相同,我们会在这个地方进行注册并初始化一个消费者,将其抛出。然后,在需要使用的时候,例如将这个消费者挂载在 context 上,我们通过 context 调用消费者对象的相应内容,传递参数即可完成调用。

320f7dd586a9d2b4f58a013def1554db.png

多进程模型支持

关于服务调用,我们不再详细描述,重点仍然是服务的提供。完成了这一步之后,我们再看下一步。另一个很重要的问题是,因为我们执行的是同一个方法,如果同时有很高并发的请求进来,同时命中了那个方法,可能会导致一些我们意想不到的异常,这是我们需要避免的。那我们如何来避免呢?这个避免其实也很简单,我们直接利用了Node.js 的 custom 模块,通过这个多进程的模型来做。这一部分的内容可能会比前面更加清晰,因为做过 Node.js  的同学肯定也都用过这个东西。

实际上,我们首先声明当前注册的服务类型。以一个具体的例子来说,假设我们有一台适合的机器,希望在 Node.js 中运行四个进程,其中两个是 dubbo 进程,两个是 HTTP 进程,它们相互独立运行。在这种情况下,我们就会声明一个相应的配置。

例如,如果有两个核心,我们期望运行一个 HTTP 进程和一个大本进程,我们可以直接声明一个包含两个元素的数组,然后开始 fork 进程。当遇到 HTTP 进程时,在 fork 的同时,我会为它添加一个自定义属性。我们都知道 cluster 模块允许我们直接添加自定义属性,所以在这里我们会用来标识当前进程是 HTTP Server,给它一个值为 true。对于 dubbo 进程,我在 fork 进程时会创建一个 dubbo 进程,并将 dubboServer 的属性设置为 true。接下来,在我的主进程中,我会根据 process.env 对象获取属性,并判断该属性是否为 dubboServer,如果是,我就直接初始化我的 dubbo 服务。

如果属性为 HTTP Server,那我就在这个位置初始化我的 HTTP 服务,从而避免整个进程或服务之间的一些差异化问题,以及可能导致一些不同寻常、不顺畅、难以预料的错误。

0dc840b022944bdff20694cf4e8fa653.png

最终框架

经过这样的改动后,整个系统最终演变成了现在的样子,我们可以分开来看这些改动。这样分步处理,使得整个过程更为清晰。

首先,关于 HTTP 服务的改动,实际上并没有太多的变化。我们仍然会像往常一样发送请求,比如请求 API 的项目详情,其中包括动态参数。然后,我们来看一下 dubbo 服务的改动,它采用的是 dubbo 协议,后跟一个地址,再接着是.project,.detail。实际上,我们的最终目标是让它命中相应的方法,使得不论是 HTTP 接口还是 dubbo 接口,都执行相同的代码,并返回高度一致的内容。通过这样的改动,我们的维护成本将变得非常低。

62120fddb97692fa3aa19b0fcd5fa6a5.png

性能表现

接下来,我们将进行性能测试,以一台四核 8G 的机器为例,测试 dubbo 服务的性能。在中等数据计算量的情况下,我们的响应时长将比 HTTP 好大约 30% - 50%。这个结果仅供参考。

d7d728ea2e52914dbb8d8a2d471d4f72.png

思考

最后,对整个改动进行小结。这次的改动非常简单,成本极低,且兼容多个场景,具有高灵活性。通过这种方式,几乎可以涵盖目前所有 Node.js 框架,并且可以轻松接入公司的微服务体系。虽然刚开始可能健壮性还不够好,但社区中有一些解决方案,比如 egg.js 框架。我们在这里借鉴了 egg.js 的一些做法。但考虑到未来的发展,我们也会朝着更健壮、易用的方向进行改进。我们希望通过这种方式向 Node.js 社区做出一些贡献,即使能够为一个同学提供一点启发,我们也会觉得这次努力是有意义的。此外,我们还希望在后续能够实现将 Node.js 作为服务的形式,形成一个更加完善的体系,为 Node.js 社区做出贡献。今天的内容就差不多是这些,希望对大家有所启发。

b900cea5e53795c3ad10e3b38323900f.png

最后


最后,惯例地推荐一本书籍。这本由马丁·福勒编著的《重构》由人民邮电出版社出版。去年我有幸阅读了这本书。书中有一句话深深吸引了我:“重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。”今天分享的问题以及在这个项目中所做的工作,实际上也是一种重构。在这其中能够得到一些启发。

谢谢大家,以上就是我的全部分享内容。

9d415959cdcdab6bbfb7ed5e98998952.png

- END -

3fce7cf84d77a60b7c24db4722d9dc80.gif

最后

Node 社群

 
 

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

2fa414821f28ae01ae139d4605159aa3.png

“分享、点赞、在看” 支持一下
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值