为什么 NodeJS 是构建微服务的最佳选择?

语言: CN / TW / HK

什么是微服务微服务是一种应用架构,它将每个应用功能都放在自己的服务中,与其他服务隔离。这些服务是松散耦合的,可独立部署。

这种架构的出现是为了解决旧的 Web 应用开发的单体方法。在单体软件中,所有的东西都是作为一个单元构建的,所有的业务逻辑都被归入一个广泛的应用。

这种方法使更新代码库的过程变得复杂化,因为它影响到整个系统,即使是最小的代码改动也需要构建和部署整个软件的新版本。此外,哪怕你只想扩展应用的某个特定功能,却需要扩展整个应用来实现它。

微服务解决了单体系统所面临的这些挑战,它将应用从一个整体分割成几个小部分。

什么时候应该使用微服务?从本质上讲,微服务架构解决了庞大、复杂应用的快速开发问题。

对于“哪个更好?”这一问题,目前还没有通用的答案。答案取决于各种情况,因为每一种情况都有其好处和缺点。

下面是一些微服务架构的优点和缺点,你可能对此已经有所了解:

优点语言不可知性:微服务并不限于特定的编程语言,每个微服务都可以用不同的语言来编写,以支持选定的通信协议。

可扩展性:由于微服务和它的职责可以由开发者共同承担,所以如果有一个大的团队参与到这个项目中,应用就会变得更加易于维护。

无限迭代:由于开发者不会被其他组件所束缚,所以在微服务上迭代会变得更加简单。

单元测试:由于微服务是独立的应用,它的重点是特定的功能,因此,开发者可以很轻松地编写测试脚本,以验证该特定功能。

缺点要作为一个整体来管理是很困难的:凯撒大帝有一句名言“分而治之”(divide et impera,拉丁语),即使在这里也可以大规模应用,但是要谨慎,因为过多的活动部分会变得难以管理。

难以追踪:如果架构变得过于复杂,微服务之间的通信渠道会非常多,出现错误后会很难追溯并确定故障点。

需要大量的专业知识:构建和部署微服务要求非常高的计划和协调方面的软技能。

具有挑战性的测试:测试是一把双刃剑,因为微服务作为一个整体更难测试。集成和端到端的测试同样会有挑战。

审计日志:可能更难获得和调查。

在架构方面,SaaS 微服务非常适合,因为微服务是 SaaS 应用的一个不错的选择。由于这类应用想要用户付钱买单,那么它就需要提供高可用的服务,因此将软件分成小块可以加快恢复速度。同时,SaaS 应用的发展主要是由其社区推动,所以,它也会受到很多变化的影响,而通过微服务和解耦,开发者可以获得了灵活性,这是单体架构无法提供的。

单体应用程序可能难以水平扩展,因为你必须复制整个应用程序,如果它依赖于单个数据库,这个过程将变得更加困难。另一边,微服务却可以根据单个服务进行扩展、复制或负载平衡。比如,如果你需要发送更多的电子邮件,你只需要扩展负责电子邮件功能的微服务。今天你有 10 个用户,明天你有 1000 个;SaaS 应用可以在短时间内维持大规模的增长,这就是为什么他们的架构必须要以最经济的方式进行轻松扩展的原因。

这样还可以减少资源的消耗,因此可以减少账单。所以,可以肯定地说,微服务是 SaaS 企业架构的下一个阶段。

弄清你是否需要微服务的最好方法是问自己:我有关于单体应用的问题吗?如果有的话,或许你应该考虑转向微服务。如果没有,那就坚持下去——没有必要把时间花在一个根本不存在的问题上。

微服务通信是如何工作的?由于服务之间彼此独立,所以与微服务的通信需要好好选择。通信协议的使用不当会造成应用的性能下降,大家必须根据自己应用的具体需求来选择通信协议。

有两种通信方式可以选择:同步通信和异步通信,这是请求 - 响应和基于事件的模式的基础。

在第一种情况下,即同步方式,客户端发送请求并等待响应。这种方法有一个缺陷,那就是它是一个阻塞模式。但是,如果你有一个读操作非常多的应用时,那就不一定了,因为你的应用更倾向从外部读取和接受信息。在这种情况下,使用同步方式可能是一个很好的选择,特别是当它涉及实时数据时。

我们的另一个选择是异步通信,这是一个非阻塞模式。如果你想要一种有弹性的微服务,那么,与同步通信相比,异步通信是一种更好的选择。在这种情况下,客户端会发送一个请求,收到请求的确认,并将其遗忘。这种方法最适用于大量写操作、无法承受数据记录丢失的应用。

下面是一些涉及微服务通信的解决方案,你可以从中选择:

基于 HTTP 的 REST

基于 HTTP/2 的 REST

WebSocket

TCP 套接字

UDP 数据包

好好考虑最适合自身需求的通信协议,因为这将使应用响应更快、效率更高。

为什么 NodeJS 用于微服务?在构建微服务时,有很多顶级编程语言可供选择。NodeJS 就是其中之一。那么,为什么 NodeJS 是最佳选择呢?

单线程 & 异步:NodeJS 使用事件循环来执行代码,允许异步代码被执行,从而使服务器能够使用非阻塞机制来响应。

事件驱动:NodeJS 使用事件驱动架构,该架构建立在软件开发的常见模式上,被称为发布 - 订阅或观察者模式,能够构建强大的应用,尤其是实时应用。

快速和高度的可扩展性:运行环境建立在最强大的 JavaScript 引擎之一 V8 JavaScript Engine 之上,因此代码执行速度快,使得服务器能够同时处理多达 10000 个并发请求。

易于开发:创建多个微服务会导致重复的代码。Node.js 的微服务框架很容易创建,因为它抽象了大部分的底层系统。所以用这种编程语言创建一个微服务可以像写几行代码一样简单。

实施微服务架构我们从创建用于用户管理的微服务开始,它将使用 TCP 数据包进行通信,并负责对用户进行 CRUD 操作。我们将使用 PacketSender 对其进行测试,PacketSender 是一个免费的工具,用于发送支持 TCP 的网络数据包。

微服务的架构和作用域被进一步界定。因此,从演示的角度来看,通过 HTTP 实现一个微服务与实现 NodeJS API 没有什么不同。

同时,通过 HTTP 来使用 REST 也很容易,但如果从这个协议切换到其他协议时,会出现一些问题。这也是本文中我们将会使用 TCP 包的异步模式来与微服务通信的原因。

我们将使用 NestJS 作为应用的框架。它并非 NodeJS 微服务框架,而是一个用于构建服务器端应用的框架。但是,由于其内置了多个微服务特性,使得工作变得更加容易。

步骤一:微服务设置用 Node.js 构建微服务相当容易,尤其是用 NestJS 框架。开始时,可以使用 CLI 创建一个新的 NestJS 应用,使用如下命令:

npx @nestjs/cli new user-microservice

该命令会创建并初始化一个新项目。要开始构建一个微服务,你需要安装以下软件包:

npm i --save @nestjs/microservices

最后,为了让微服务启动和运行,我们需要用以下内容更新 main.ts 文件:

import { INestMicroservice } from '@nestjs/common';import { NestFactory } from '@nestjs/core';import { Transport } from '@nestjs/microservices';import { AppModule } from './app.module';async function bootstrap() { const microservicesOptions: any = { transport: Transport.TCP, options: { host: '127.0.0.1', port: 8875, }, }; const app: INestMicroservice = await NestFactory.createMicroservice( AppModule, microservicesOptions, ); app.listen(() => console.log('Microservice is listening'));}bootstrap();

NestJS 支持几个内置的传输层实现,称为传输器。上面的代码将创建一个微服务,通过 TCP 传输层绑定到本地机器的 8875 端口进行通信。

步骤 2:微服务监听消息我们可以使用消息模式或事件模式来与微服务通信。

消息模式的作用就像一个请求 - 响应方法,它适用于在服务之间交换消息,而当你只想发布事件而不等待响应时,就可以使用事件模式。

在我们的案例中,我们只实现根据给定的输入创建一个用户的功能,并且将获得创建的用户。因此,我们将在 app.controller.ts 文件中注册一个名为 create_user 的消息模式。

@Controller()export class AppController { constructor(private readonly appService: AppService) {} @MessagePattern('create_user') async createUser(@Payload() payload: CreateUserDto) { const user = await this.appService.createUser(payload); return user; }}

我们抽象出创建新用户的逻辑,因为它可以根据需求和使用的数据库以各种方式实现,我们将只关注与微服务相关的主题。

我们用来创建一个新用户的有效负载有以下格式:

import { IsString, IsEmail } from 'class-validator';export class CreateUserDto { @IsEmail() email: string; @IsString() password: string;}

一个带有 email 和 password 的简单对象

步骤 3:测试微服务为了测试这个微服务,我们将使用 PacketSender 向应用发送一个 TCP 包。为此,将地址和端口设置为 127.0.0.1:8875,并从右侧的下拉菜单中选择 TCP。要对我们的信息进行编码,请使用 ASCII 字段,并用以下值来完成:

122#{"pattern":"create_user","data":{"email":"[email protected]","password":"12345678"},"id":"ce51ebd3-32b1-4ae6-b7ef-e018126c4cc4"}

pattern:是我们正在寻找的信息,create_user。

data:是我们要发送的 JSON 对象,一个带有 email 和 password 的对象。

值 122 代表我们的消息的长度,从第一个大括号开始到最后一个大括号(包括两个)。

数据包发送器配置

如果我们点击 Send 按钮,我们会看到如下日志:

日志活动

第二个是我们发送给微服务的内容,第一个是我们收到的内容。里面的响应是由我们的微服务返回的对象,即被创建的用户。

步骤 4:API 网关现在我们有了微服务,并进行了快速测试,看它是否能接收请求并返回响应,现在是时候创建一个 API 网关并将其连接到微服务上了。

为此,我们将使用上面描述的相同步骤创建一个新的 NestJS 应用,然后用以下内容更新 app.module.ts 文件。

import { Module } from '@nestjs/common';import { AppController } from './app.controller';import { AppService } from './app.service';import { ConfigService } from "./config/config.service";@Module({ imports: [], controllers: [AppController], providers: [ { provide: 'USER_MICROSERVICE', useFactory: (configService: ConfigService) => { const options = { transport: Transport.TCP, options: { host: configService.get('USERS_MICROSERVICE_HOST'), port: Number(configService.get('USERS_MICROSERVICE_PORT')), }, }; return ClientProxyFactory.create(options as ClientOptions); }, inject: [ConfigService], }, AppService, ],})export class AppModule {}

我们将使用 .env 文件,我们将在其中存储任何与配置有关的值。这些文件将在一个配置服务的帮助下被读取。该微服务可以在 host 127.0.0.1:8875 处找到,其中 port 为 8875。

通过上面的代码,我们使用 ClientProxy 注入一个新的对象,代表与我们的用户 - 微服务的连接。这个 NestJS 类提供了几个内置的工具来与远程微服务交换信息。

为了使用这个链接对象,我们可以在 AppController 或 AppService 中注入它,如下所示:

@Controller()export class AppController { constructor( @Inject('USER_MICROSERVICE') private readonly client: ClientProxy, private readonly appService: AppService ) {} @Post('create-user') async createUser(@Body() payload: CreateUserDto) { return this.client.send('create_user', payload).toPromise(); }}

现在,每次 API 在路由 create-user 处受到 POST 请求时,API 网关将把请求和有效载荷一起转发给微服务,然后从微服务返回响应给用户。