🚤NestJS 学习 —— 微服务 Microservice
00 分钟
2023-4-11
2023-4-12
type
status
date
slug
summary
tags
category
icon
password
Edited
Apr 12, 2023 02:19 PM
Created
Apr 12, 2023 01:57 PM

基本

除了传统的(有时称为单片)应用程序架构之外,Nest 还支持微服务架构风格的开发。本文档中其他地方讨论的大多数概念,如依赖项注入、装饰器、异常过滤器、管道、保护和拦截器,都同样适用于微服务。Nest 会尽可能地抽象化实现细节,以便相同的组件可以跨基于 HTTP 的平台,WebSocket 和微服务运行。本节特别讨论 Nest 的微服务方面。 在 Nest 中,微服务基本上是一个使用与 HTTP 不同的传输层的应用程序。
notion image
Nest 支持几种内置的传输层实现,称为传输器,负责在不同的微服务实例之间传输消息。大多数传输器本机都支持请求 - 响应和基于事件的消息样式。Nest 在规范接口的后面抽象了每个传输器的实现细节,用于请求 - 响应和基于事件的消息传递。这样可以轻松地从一个传输层切换到另一层,例如,利用特定传输层的特定可靠性或性能功能,而不会影响您的应用程序代码。

安装

首先,我们需要安装所需的软件包:

开始

为了创建微服务,我们使用 NestFactory 类的 createMicroservice() 方法。
main.ts
默认情况下,微服务通过 TCP协议 监听消息。
createMicroservice () 方法的第二个参数是 options 对象。此对象可能有两个成员:
transport
指定传输器,例如Transport.NATS
options
确定传输器行为的传输器特定选项对象
options 对象根据所选的传送器而不同。TCP 传输器暴露了下面描述的几个属性。其他传输器(如Redis,MQTT等)参见相关章节。
host
连接主机名
port
连接端口
retryAttempts
连接尝试的总数
retryDelay
连接重试延迟(ms)

模式(patterns)

微服务通过 模式 识别消息。模式是一个普通值,例如对象、字符串。模式将自动序列化,并与消息的数据部分一起通过网络发送。因此,接收器可以容易地将传入消息与相应的处理器相关联。

请求-响应

当您需要在各种外部服务之间交换消息时,请求-响应消息样式非常有用。使用此范例,您可以确定服务确实收到了消息(不需要手动实现消息 ACK 协议)。然而,请求-响应范式并不总是最佳选择。例如,使用基于日志的持久性的流传输器(如 Kafka 或 NATS 流)针对解决不同范围的问题进行了优化,更符合事件消息传递范例(有关更多细节,请参阅下面的基于事件的消息传递)。
为了使服务能够通过网络交换数据,Nest 创建了两个通道,其中一个负责传输数据,而另一个负责监听传入的响应。对于某些底层传输,比如 NATS,这种双通道是支持开箱即用的。对于其他人,Nest 通过手动创建单独的渠道进行补偿。 这样做可能会产生开销,因此,如果您不需要请求-响应消息样式,则应考虑使用基于事件的方法。
基本上,要创建一个消息处理程序(基于请求 - 响应范例),我们使用 @MessagePattern() ,需要从 @nestjs/microservices 包导入。
math.controller.ts
在上面的代码中,accumulate() 处理程序正在监听符合 {cmd :'sum'} 模式的消息。模式处理程序采用单个参数,即从客户端传递的 data 。在这种情况下,数据是必须累加的数字数组。

异步响应

每个模式处理程序都能够同步或异步响应。因此,支持 async (异步)方法。
math.controller.ts
此外,我们能够返回 Rx Observable,因此这些值将被发出,直到流完成。
以上消息处理程序将响应3次(对数组中的每个项)。

基于事件

虽然 request-response 方法是在服务之间交换消息的理想方法,但是当您的消息样式是基于事件的时(即您只想发布事件而不等待响应时),它不太适合。它会带来太多不必要的开销,而这些开销是完全无用的。例如,您希望简单地通知另一个服务系统的这一部分发生了某种情况。这种情况就适合使用基于事件的消息风格。
为了创建事件处理程序,我们使用 @EventPattern()装饰器, 需要 @nestjs/microservices 包导入。
你可以为单独的事件模式注册多个事件处理程序,所有的事件处理程序都会并行执行
该 handleUserCreated() 方法正在侦听 user_created 事件。事件处理程序接受一个参数,data 从客户端传递(在本例中,是一个通过网络发送的事件有效负载)。

装饰器

在更复杂的场景中,您可能希望访问关于传入请求的更多信息。例如,对于通配符订阅的 NATS,您可能希望获得生产者发送消息的原始主题。同样,在 Kafka 中,您可能希望访问消息头。为了做到这一点,你可以使用内置的装饰如下:
@Payload()@Ctx() 和 NatsContext 需要从 @nestjs/microservices 包导入。
你也可以为 @Payload() 装饰器传入一个属性key值,来获取通过此装饰器拿到的对象的value值,例如 @Payload('id')

客户端

为了交换消息或将事件发布到 Nest 微服务,我们使用 ClientProxy 类, 它可以通过几种方式创建实例。此类定义了几个方法,例如send()(用于请求-响应消息传递)和emit()(用于事件驱动消息传递),这些方法允许您与远程微服务通信。使用下列方法之一获取此类的实例。
首先,我们可以使用 ClientsModule 暴露的静态register() 方法。此方法将数组作为参数,其中每个元素都具有 name属性,以及一个可选的transport属性(默认是Transport.TCP),以及特定于微服务的options属性。
name属性充当一个 injection token,可以在需要时将其用于注入 ClientProxy 实例。name 属性的值作为注入标记,可以是任意字符串或JavaScript符号,参考这里
options 属性是一个与我们之前在createMicroservice()方法中看到的相同的对象。
导入模块之后,我们可以使用 @Inject() 装饰器将'MATH_SERVICE'注入ClientProxy的一个实例。
ClientsModule和 ClientProxy类需要从 @nestjs/microservices 包导入。
有时候,我们可能需要从另一个服务(比如 ConfigService )获取微服务配置而不是硬编码在客户端程序中,为此,我们可以使用 ClientProxyFactory 类来注册一个自定义提供程序,这个类有一个静态的create()方法,接收传输者选项对象,并返回一个自定义的 ClientProxy 实例:
ClientProxyFactory 需要从 @nestjs/microservices 包导入 。
另一种选择是使用 @client()属性装饰器。
@Client() 需要从 @nestjs/microservices 包导入 。
但是,使用 @Client() 装饰器不是推荐的方法(难以测试,难以共享客户端实例)。
ClientProxy 是惰性的。 它不会立即启动连接。 相反,它将在第一个微服务调用之前建立,然后在每个后续调用中重用。 但是,如果您希望将应用程序引导过程延迟到建立连接为止,则可以使用 OnApplicationBootstrap 生命周期挂钩内的 ClientProxy 对象的 connect() 方法手动启动连接。
如果无法创建连接,则该 connect() 方法将拒绝相应的错误对象。

发送消息

该 ClientProxy 公开 send() 方法。 此方法旨在调用微服务,并返回带有其响应的 Observable。 因此,我们可以轻松地订阅发射的值。
send() 函数接受两个参数,pattern 和 payloadpattern 具有 @MessagePattern() 修饰符中定义的这个模式,而 payload 是我们想要传输到另一个微服务的消息。该方法返回一个cold Observable对象,这意味着您必须在消息发送之前显式地订阅它。

发布事件

另一种是使用 ClientProxy 对象的 emit()方法。此方法的职责是将事件发布到消息代理。
该 emit()方法有两个参数,pattern 和 payloadpattern 具有 @EventPattern() 修饰符中定义的这个模式,而payload 是我们想要传输到另一个微服务的消息。此方法返回一个 hot Observable(不同于send()方法返回一个 cold Observable),这意味着无论您是否显式订阅该 Observable,代理都将立即尝试传递事件。

作用域

对于不同编程语言背景的人来说,可能会意外地发现,在 Nest 中,几乎所有内容都在传入的请求之间共享。例如,我们有一个到数据库的连接池,带有全局状态的单例服务,等等。请记住,Node.js 并不遵循request-response的多线程无状态模型,在这种模型中,每个请求都由单独的线程处理。因此,对于应用程序来说,使用单例实例是完全安全的。
但是,在某些情况下,当应用程序是基于生命周期的行为时,也存在边界情况,例如 GraphQL 应用程序中的每个请求缓存、请求跟踪或多租户。在这里学习如何控制范围。
请求作用域的处理程序和提供程序可以使用 @Inject() 装饰器结合CONTEXT (上下文)令牌注入RequestContext:
还提供了对 RequestContext 对象的访问,该对象有两个属性:
data 属性是消息生产者发送的消息有效负载。 pattern 属性是用于标识适当的处理程序以处理传入消息的模式。

处理超时

在分布式系统中,有时微服务可能宕机或者无法访问。要避免无限等待,可以使用超时,超时是一个和其他服务通讯的可信赖的方法。要在微服务中应用超时,你可以使用RxJS超时操作符。如果微服务没有在指定时间内返回响应,会抛出异常以便正确捕获与处理。
要处理该问题,可以使用[rxjs](https://github.com/ReactiveX/rxjs)包,并在管道中使用timeout操作符。
timeout操作符从rxjs/operators中引入
5秒后,如果微服务没有响应,将抛出错误。
 

使用

前提

想必大家都写过 http 服务,接受 http 请求、做一些处理、返回 http 响应。
notion image
这样完成 web 服务器的功能没问题,但随着功能的越来越多,比如现在有一百多个模块了,总不能都放在一个服务里吧,这样管理不方便。
于是就有了拆分的需求,也就有了微服务的概念。
notion image
拆分微服务是很自然的事情,但有个问题,微服务和 http 服务之间怎么通信呢?
用 HTTP?
这个是可以,但是 HTTP 是文本协议,传输效率太低了。
所以一般都直接用 TCP 通信。
微服务架构是主流了,各种服务端开发框架都提供了微服务的支持,Nest 自然也不例外。
而且,Nest 对微服务封装的很好,写个微服务是特别简单的事情。

使用

首先全局安装 nest 的 cli 工具:
然后用 nest 的 cli 快速创建一个 nest 项目:
选一个依赖管理工具,我这里用的 yarn。
执行 yarn start 就可以看到跑起来的 http 服务了:
浏览器访问下 http://localhost:3000
到这一步,http 服务就启动成功了。

创建微服务

然后我们创建个微服务,同样的方式,用 nest new 创建个项目:
这里要创建微服务,需要安装一个包:
然后改下 main.ts:
之前创建 http 服务是这样的:
notion image
现在要改成这样:
很容易理解,之前是启 http 服务,现在是起微服务了嘛,所以启动方式不一样。
启动的时候指定用 TCP 来传输消息,然后指定 TCP 启动的端口为 8888。
之后在 AppController 里注册下怎么处理 TCP 的消息:
这里用 MessagePattern 的方式来声明处理啥消息:
notion image
这个 sum 方法就是接受 sum 消息,返回求和的结果的 handler。
然后同样是 yarn start 把这个微服务跑起来:
notion image
现在我们有两个服务了:
一个 http 服务,一个 TCP 协议的微服务,然后把两者连起来就可以了。
怎么连起来呢?
我们来改造下 http 服务。

主服务调用微服务先

安装 @nestjs/microservices 依赖:
然后在 app.module.ts 里注册 calc 那个微服务:
notion image
调用 ClientModule.register 指定名字、传输方式为 TCP、端口为 8888。
这样就注册完了。
然后就可以用了,在 Controller 里注入这个微服务的 clientProxy,也就是客户端代理。
这样就可以接收到 http 请求的时候调用微服务来处理了。
比如上面我们在收到请求的时候,调用代理对象的 send 方法发了一个 TCP 消息给微服务。
这也是为啥叫做 ClientProxy 了,不用你自己发 TCP 消息,你只要调用 send 方法即可。
然后把它重新跑起来:
然后,看:
notion image
 
我们 num 传了 1,2,3,这里返回了 6 ,这明显就是 calc 微服务处理的。
这样,我们第一个 Nest 微服务就跑成功了!
是不是挺简单的?
其实微服务还有一种消息传输的类型,这里我们需要响应,所以是 message 的方式,如果不需要响应,那就可以直接声明 event 的方式。
我们再来创建个微服务,用来打印日志。

log微服务

用 nest new mirco-app-log 创建项目,然后安装 @nestjs/microservices 包,之后像上一个微服务一样改用 createMicroservice 的 api 启动服务。
notion image
这个微服务起在 9999 端口。
然后 Controller 改一下:
notion image
这里不需要响应,只是处理事件,所以不用 MessagePattern 注册消息了,用 EventPattern。
然后在 main 项目里注册下:
notion image
名字叫做 LOG_SERVICE,端口 9999。
然后在 Controller 里注入这个微服务的 clientProxy:
notion image
这样我们在这个 http 请求的 handler 里同时用到了两个微服务:
用 calc 微服务来做计算,用 log 微服务来记录日志。
yarn start 重跑一下。
浏览器刷新下:
notion image
同样返回了 6,说明 calc 微服务正常。
再去 log 微服务的控制台看看:
notion image
log 的微服务打印了日志,说明 log 微服务正常。
至此,Nest 微服务跑成功了!

总结

http 服务大了难免要拆分,现在都是拆成微服务的方式,http 服务负责处理 http 请求,微服务完成不同模块的业务逻辑处理。
微服务和 http 服务之间用 TCP 通信。
用 nest 跑个微服务的步骤如下:
  • 用 nest new 创建一个 main 服务,一个微服务
  • 都要安装 @nestjs/microservices 包,因为用到其中的 api
  • 微服务里用 createMicroservice 启动服务,选择传输方式为 TCP,指定端口
  • 微服务里在 Controller 使用 MessagePattern 或者 EventPattern 注册处理消息的 handler
  • main 服务使用 ClientsModule.register 来注册微服务
  • main 服务里注入 ClientProxy 对象,调用它的 send 方法给微服务发消息
这就是 Nest 跑微服务的方式。
当然,现在都是本机部署的,你完全可以把微服务放到不同的服务器,甚至可以不同微服务用不同的集群部署。
Nest 里跑微服务以及 http 服务里注册微服务的方式,还是挺简单的,这块封装的确实好。
 

参考链接

  1. Nest.js 的微服务,写起来也太简单了吧! - 掘金 (juejin.cn)
 
上一篇
Docker 基本使用
下一篇
NestJS 学习 —— 网关 Gateway

评论
Loading...