一、定义
微服务的定义即为将相同模块或相关业务的操作,封装在一个服务中,达到独立运行、独立部署的效果。目的是为了功能的解耦,并且做到互不影响。
此时的服务可以采用不用的语言、不一样的架构实现,便于适合不同的开发人员根据自身的技术情况进行灵活选择。
设计微服务的时候,最主要的是根据业务逻辑、安全、稳定、高效等方面进行服务功能的分离,但在此之外,还要设计服务之间相互通讯的方式。
最普遍的是通过服务间HTTP接口进行功能的调用,该方式相对来说最易于实现,整个调用流程也较为清晰易懂。但采用HTTP请求进行微服务间通讯也有些缺点,如不必要的请求头信息,导致发送的数据包过大,再比如很难实现任务队列的功能等,所以微服务之间还可以选择RPC、mq、MQTT等方式进行信息流转。
微服务还有很多注意事项,如服务管理、负载均衡、服务监控等问题。与本篇文章无太大关联,在此不会进行阐述
二、NESTJS微服务
nestjs是一种类似angular与spring boot的nodejs后端架构,其架构思想包含DI(依赖注入)、OOP(面向对象编程)、AOP(切面编程)等特点,使得原本较为松散的后端js工程代码能够有较为清晰的管理方式。详情请戳nestjs官网。
nestjs本身除了可设计普通的API服务外,还可以以Microservice的方式设计微服务,在其官方文档中可以看到相关的描述与定义:地址。通过左侧的导航栏可以看到,该框架支持多种介质的服务实现方式,包括redis、MQTT、rabbitmq、gRPC等:
本文将会按照官方文档,选取几种方式进行简单demo实现。
三、TCP方式
nestjs实现微服务主要依赖包@nestjs/microservices
,所以在开始之前需要提前在项目中安装该依赖:$ npm i --save @nestjs/microservices
。
首先准备两个nestjs项目,使用官方提供的脚手架工具进行项目构建:
$ npm i -g @nestjs/cli
$ nest new project-name
项目名可以自定义,本文中暂时采用[项目一]与[项目二]进行描述。项目一为API与微服务混合模式,项目二为单微服务模式,前者为调用方,后者为服务功能提供方。
在两个项目中安装@nestjs/microservices
依赖后,即可开始代码编写。
3.1 微服务模式(服务提供方)
先改造[项目二],将src/main.ts
文件改写为如下:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
async function bootstrap() {
// const app = await NestFactory.create(AppModule);
// await app.listen(3000);
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
},
);
app.listen();
}
bootstrap();
对比改动前后的代码,可以发现app
的生成方式发生的变化,更改前是通过NestFactory.create
方法生成,更改后则是通过NestFactory.createMicroservice
生成,并且多了相关的参数。
前者是nestjs生成普通应用程序对象(INestApplication),该对象只启用了HTTP监听器,所以只能处理HTTP接口消息,而后者生成INestMicroservice对象,该对象可监听TCP消息,该对象即是框架实现TCP消息流转的入口。
上述man文件只是提供了基础的能力支持,如果想体验具体功能,还需要编写相应的处理代码。
打开文件src/app.controller.ts
,这是工程默认生成的一个控制器(控制器主要服务接受请求与返回响应),可以看到已经使用@Get
注解定义了一个接口。
此时如果使用命令npm run start
启动项目的话,使用请求发送工具(linux的curl命令或者postman等工具),发送get请求:http://localhost:3000,该请求是无法得到响应的。原因是我们在main.ts中实现的是微服务对象,该对象不支持HTTP监听。
这里感觉框架的处理方式不太合理,如果是不支持的话,可以返回一些错误信息,或者在控制台打印日志,而不是一直让请求pending直到超时
将app.controller.ts
中的@Get
注解及其下方的函数实现先注释掉(不注释也可,没有影响)。
实现一个微服务接收函数,在nestjs中,不管是TCP方式还是其他,都是通过使用注解@MessagePattern
定义所有的微服务接口。将app.controller.ts
文件改写为如下形式:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// @Get()
// getHello(): string {
// return this.appService.getHello();
// }
@MessagePattern('accumulate')
accumulate(data: number[]): number {
return (data || []).reduce((a, b) => a + b);
}
}
在这里,我们定义了一个接口叫accumulate
,接口作用是接收一个数字型数组,把所有元素相加后,将结果返回。
至此,我们实现了一个很基础的微服务,这个微服务有个接口,接口功能是计算数组之和。但我们要怎么调用这个功能呢?前面在启动项目后,无法通过HTTP接口进行调用,这时候就需要使用另一个项目充当客户端的角色,对该功能进行调用使用了。
也可以在这个项目里实现HTTP、TCP混合的应用程序,同时实现HTTP接口与TCP接口,进行相互调用。但这样就没有缺乏了微服务服务之间的味道了,所以不以这种例子作为说明
3.2 HTTP服务(调用方)
前面我们实现了一个微服务,接下来我们定义一个新的服务(项目一),对该微服务的功能进行调用。
还是先关注入口文件main.ts
,我们将监听的端口号换一下,例如改为10086
。
不使用3000的原因是在上一个项目中已经占用了。虽然上一个项目中没有明确定义,但nestjs会给设置一个默认的端口号为3000
端口号基本由开发人员自己定义,但不要与linux系统端口号或常见端口重复,如80、3679等,基本超过5位数的端口号使用的情况比较少
接下来我们在程序中引入一个客户端,在app.module.ts
中,改造为如下形式:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Transport, ClientsModule } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{ name: 'MATH_SERVICE', transport: Transport.TCP },
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
对比改造前后,会发现我们在imports
中引入了一个客户端,该客户端通过ClientsModule
进行定义,名字叫做MATH_SERVICE
,介质为Transport.TCP
。
客户端名称可自定义,其目的也是在调用方有多个微服务时区分使用
此时没有定义地址与端口号,采用默认的localhost与3000。如果服务不在同一环境,可添加options参数定义host与port
接下来,还是改造app.controller.ts文件,改写为以下形式:
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';
import { ClientProxy } from '@nestjs/microservices';
import { Observable } from 'rxjs';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
@Inject('MATH_SERVICE') private readonly client: ClientProxy,
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('testMicroservice')
testMicroservice(): Observable<number> {
return this.client.send('accumulate', [1, 2, 3, 4, 5]);
}
}
对比更改前后,有两点地方值得注意:
- 使用
@Inject
注解引入名为MATH_SERVICE
的客户端 - 实现一个get接口,名为
testMicroservice
上述引入的客户端即为app.module.ts
中定义的客户端,而testMicroservice
接口中,操作该客户端发送数据。
发送数据的函数中,第一个参数为接收方的接口名(即为上一个项目中所定义的名称),第二个参数为发送的数据(即为上个项目中的接收参数)。
Observable为观察者对象,在本文中并不重要,读者可自行了解
运行项目:npm run start
,可以看到控制台存在以下提示:
该提示也表示在app.module.ts
中的客户端已启用并被初始化。
使用http请求工具,如curl http://localhost:10086/testMicroservice
,可以得到请求响应,结果为15,即为传入数组的总和,说明我们服务之间调用成功。
被调用方也要启动,要不然会请求不到,在控制台报错:connect ECONNREFUSED 127.0.0.1:3000
至此,我们实现了一个微服务,并且定义了一个客户端,对该服务进行调用。
四、与HTTP接口对比
服务与服务之间相互调用,HTTP接口相对来说最为简单、调试最为方便,但是TCP在高并发情境下还是有一定的使用场景,高并发下,请求数量陡升,携带的信息差之毫厘,也会影响资源的使用,进而影响服务的速度,下面是分别使用HTTP方式与TCP方式,用wireshark工具进行抓包分析时,所展示的数据包大小:
(前者为HTTP,后者为TCP。携带的数据都是{ data: [1,2,3,4,5] }
)
从上面可以看到,当携带的信息较少时,TCP方式明显比HTTP方式所发送的数据包要小,HTTP数据包大小占比主要集中在请求头headers
当请求中所发送的数据比较大,在数据包中的占比较大时,两种方式基本没区别。所以要看情况选择使用
五、不同框架间通讯
不同语言,不同框架之间数据接收、数据解析的方式可能不同,可以使用相对独立的介质,如gRPC、RabbitMQ等方式进行信息传递,通过单独的配置文件或者数据流转进行通讯。