文章主要围绕Nest文档中的微服务展开,选择其中的TCP方式作为微服务间的通讯机制。通过demo的形式进行实践。在此之前先简单了解下微服务。
单个服务的痛点
- 代码更新时,整个系统所涉及到的测试、部署都会重新执行,整个流程十分缓慢。
- 遇到问题时,整个服务器不可用,由于在一个系统仓库中,修复Bug定位困难。
- 扩展服务与引入新的特性会变得十分困难,期间可能会涉及到整个系统的重构,牵一发动全身
为了解决单个服务的业务痛点,微服务架构产生了,微服务可以将巨石应用按照微服务的架构进行重新设计。整个项目变得简单可控。
微服务简介
微服务架构是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦。
微服务它不是具体指某一技术,而是关于某种架构风格的集合,因此微服务本身是没有明确定义的,但是可以知道的是它是有不只一个的独立服务组成的一个整体架构。
微服务架构特点
- 功能模块独立,去除了服务间代码的相互依赖,增强了应用的扩展性
- 每个模块可以单独部署,修改起来缩短了应用的部署时间,也能更快的对错误进行定位
微服务设计准则
微服务本身相较于传统架构,会带来许多优点,但同时又会增加额外的复杂度与管理成本,所以不要为了微服务而微服务,看项目,看业务场景,有时单个服务就可以解决的问题,就不要再用微服务去解决。微服务的应用场景是庞大的项目、或者是巨石应用。
Nest微服务架构
服务间通讯协议
微服务架构是Nest.js支持的功能之一,通过将功能分解到各个离散的服务中以实现对解决方案的解耦。
Nest内置了几种不同的微服务传输层实现,默认是TCP
协议,定义在@nestjs/microservices包的Transport模块内,简单归类为:
- 直接传输:TCP
- 消息中转:REDIS、NATS、MQTT、RMQ、KAFKA
- 远程过程调度:GRPC
我们需要选取一种通讯协议来作为彼此微服务间的通讯机制,对于Nest框架来说切换传输协议是十分方便的,我们需要根据自身项目的特性来决定。
服务间通讯模式
Nest microservice中,通讯模式有两种:
- Request-response模式,当需要在内部服务间交互讯息时使用,异步的response函数也是支持的,返回结果可以是一个Observable对象。
- Event-based模式,当服务间是基于事件的时候—我们仅仅想发布事件,而不是订阅事件时,就不需要等待response函数的响应,此时Event-based模式就是最好的选择。
为了在微服务间进行准确的传输数据和事件,需要用到一个称作模式(pattern)的值,pattern是由我们进行自定义的一个普通的对象值,或者是字符串,模式相当于微服务之间交流的语言,当进行通讯时,它会被自动序列化并通过网络请求找到与之匹配的服务模块。
构建一个简单的微服务架构
demo1
这里使用Nest微服务默认的通讯协议为TCP,主项目和微服务没在一个文件夹下,是单独的,需要各自分别启动,此时的架构图为:
微服务
步骤
- 创建微服务,安装内置的微服务模块
nest new ms-math
yarn add @nestjs/microservices
复制代码
- 修改ms-math微服务中main.ts文件
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
},
);
// app.listen(() => console.log('Microservice is listening')); 此处代码会报错
app.listen();
}
bootstrap();
复制代码
- 修改ms-math微服务中app.service.ts文件
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
calculateWordCount(str: string) {
const words = str.trim().split(/\s+/);
return words.reduce((a, c) => ((a[c] = (a[c] || 0) + 1), a), {});
}
}
复制代码
- 修改ms-math微服务中app.controller.ts文件
在控制器中,不再使用@Get
或是@Post
暴露接口,而是通过@MessagePattern
进行设置模式(pattern),供微服务间识别身份。
import { Controller } from '@nestjs/common';
import { AppService } from './app.service';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@MessagePattern('math:wordcount')
wordCount(text: string): { [key: string]: number } {
return this.appService.calculateWordCount(text);
}
}
复制代码
到这里微服务已经创建好了,我们来启动它,yarn start:dev
主项目
步骤
- 创建主项目,安装微服务依赖
nest new ms-app
yarn add @nestjs/microservices
复制代码
- app.module中注册微服务客户端
注册一个用于对微服务进行数据传输的客户端,在这里使用ClientsModule
提供的 register()
方法进行mathService的注册
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ClientsModule, Transport } from '@nestjs/microservices'; // 注册一个用于对微服务进行数据传输的客户端
@Module({
imports: [
ClientsModule.register([
{ name: 'MATH_SERVICE', transport: Transport.TCP },
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
复制代码
- 模块注册成功后,在app.controller中使用依赖注入的方式进行引用
constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {}
复制代码
具体代码如下所示:
import { Controller, Get, Inject, Post, Body } 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 client: ClientProxy
) { }
@Post('/math/wordcount')
wordCount(
@Body() { text }: { text: string },
): Observable<{ [key: string]: number }> {
this.client.emit('math:wordcount_log', text)
return this.client.send('math:wordcount', text);
}
}
复制代码
在这里利用ClientProxy
的send的方法与另一个微服务进行通信息。
ClientProxy对象有两个核心方法
-
send()
,请求响应模式下的消息发送方法,该方法会调用微服务并返回一个Observable对象的响应体,因此能很简单的去订阅该微服务返回的数据,需要注意的是,只有你对该对象进行订阅后,相应的消息体才会被发送。 -
emit()
,基于事件的消息发送方法,无论你是否订阅数据,该消息都会被立即发送
- 到这里主项目就搭建完了,启动ms-app主项目
测试
通过curl进行测试:
cd ms-app
,输入如下命令
curl --location --request POST 'http://localhost:3000/math/wordcount' \
--header 'Content-Type: application/json' \
--data-raw '{
"text": "a b c c"
}'
复制代码
输出:
基于事件的传输方式
ms-app中app.controller
事件名称定为:`math:wordcount_log` ,在原`/math/wordcount`路由方法里添加如下代码:
this.client.emit('math:wordcount_log', text)
复制代码
ms-math中,在app.controller中注册相应的订阅器
@EventPattern('math:wordcount_log')
wordCountLog(text: string): void {
console.log(text, '基于事件的传输方式');
}
复制代码
执行curl命令,在ms-math服务的终端看到以下打印
使用redis作为消息代理
什么是消息代理
消息代理(Message broker)是一个中间程序模块,在计算机网络中用于交换消息,它是面向消息的中间件的建造模块,因此它的职责并不包括负责远程过程调度(RPC).
消息代理也是一种架构模式,用于消息验证、变换、路由。调节应用程序的通信,极小化互相感知(依赖),有效实现解耦合。例如,消息代理可以管理一个工作负荷队列或消息队列,用于多个接收者,提供可靠存储、保证消息分发、以及事务管理
为什么选用redis
- Redis本身足够轻量级与高效,使用率非常高,比较受欢迎
- 个人本身对redis有一定的了解,上手起来会更快
服务架构的变化
在这里引用一张图,非常清晰明了:
demo实现
- 创建
docker-compose.yml
,用于管理redis服务
version: '3.7'
services:
redis:
image: redis:latest
container_name: service-redis
command: redis-server --requirepass rootroot
ports:
- "16377:6379"
volumes:
- ./data:/data
复制代码
通过docker-compose up -d
后,执行docker ps
查看服务状态:
-
在ms-app与ms-math中安装 Redis依赖
yarn add redis
-
ms-math中的bootstrap函数内,将Transport替换为redis,并附上服务地址
// before
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
},
);
// after
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.REDIS,
options: {
url: "redis://:rootroot@localhost:16377",
}
},
);
复制代码
在这里ms-math就改造完成了
- 然后打开ms-app,在注册客户端的地方,进行相应的替换
// before
ClientsModule.register([
{ name: 'MATH_SERVICE', transport: Transport.TCP },
]),
// after
ClientsModule.register([
{
name: 'MATH_SERVICE',
transport: Transport.REDIS,
options: {
url: 'redis://:rootroot@localhost:16377',
},
},
]),
],
复制代码
- 最后通过curl进行测试即可
demo2
此处的demo主项目和微服务在一个文件夹下,统一进行管理。只需启动最外层的yarn start:dev
,不是单独启动主项目和微服务。
- 生成外层文件项目,该项目将作为主项目使用
nest new nest-app -p yarn
复制代码
此时的目录结构为:
.
├── README.md
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
复制代码
- 再生成另一个项目作为微服务项目:
cd nest-app
nest g app nest-service
复制代码
此时的目录结构更新成了
nest-cli.json文件说明
此时主项目nest-app
和微服务nest-service
同在一个仓库中,称为monorepo
,注意到根目录有个nest-cli.josn
文件,可以配置monorepo 的参数,nest-cli.json文件的其余配置可参考官方文档:docs.nestjs.com/cli/monorep…
{
"collection": "@nestjs/schematics",
"sourceRoot": "apps/nest-app/src",
"monorepo": true,
"root": "apps/nest-app", // 指定了哪个项目是主项目
"compilerOptions": {
"webpack": true,
"tsConfigPath": "apps/nest-app/tsconfig.app.json" // 为每个项目指定自己的tsconfig.json文件路径等
},
"projects": {
"nest-app": {
"type": "application",
"root": "apps/nest-app",
"entryFile": "main",
"sourceRoot": "apps/nest-app/src",
"compilerOptions": {
"tsConfigPath": "apps/nest-app/tsconfig.app.json"
}
},
"nest-service": {
"type": "application",
"root": "apps/nest-service",
"entryFile": "main",
"sourceRoot": "apps/nest-service/src",
"compilerOptions": {
"tsConfigPath": "apps/nest-service/tsconfig.app.json"
}
}
}
}
复制代码
微服务
改造nest-service
项目使其提供微服务被调用的能力。
步骤
- 添加微服务依赖
yarn add @nestjs/microservices
- 创建微服务,修改nest-service/main.ts文件
createMicroservice用于创建一个微服务实例。它接收两个参数,第一个和正常创建nest app一样,另一个则用于控制要创建的微服务的具体属性,比如端口,地址等.
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { NestServiceModule } from './nest-service.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
NestServiceModule,
{
transport: Transport.TCP,
options: {
port: 4000,
},
},
);
await app.listen();
}
bootstrap();
复制代码
- 添加消息处理器
/nest-service/src/nest-service.controller.ts中
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { NestServiceService } from './nest-service.service';
@Controller()
export class AppController {
constructor(private readonly nestServiceService: NestServiceService) {}
@MessagePattern({ cmd: 'getHello' })
getHello(name: string): string {
return this.nestServiceService.getHello(name);
}
}
复制代码
MessagePattern
定义了个消息处理器,它将监听并处理调用方发来的指令为getHello
的消息,其依赖的服务为/nest-service/src/nest-service.service.ts
- 修改/nest-service/src/nest-service.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class NestServiceService {
getHello(name: string): string {
return `Hello ${name}!`;
}
}
复制代码
- 修改启动命令
启动单个项目
nest start nest-app
nest start nest-service
开发过程中因为需要从主项目调用微服务提供的服务,通过concurrently
来同时启动两个项目,
yarn add -D concurrently
复制代码
修改package.json中script:
"start:dev": "concurrently --kill-others "nest start nest-app --watch" "nest start nest-service --watch"",
复制代码
最终cd nest-app执行
yarn start:dev
复制代码
主项目
即微服务调用方,要调用微服务,需要先初始化一个客户端对象。因为nest支持多种类型的微服务,所以提供ClientProxy对象作为统一的客户端,完成初始化之后使用者无需关心不同类型微服务的差异,该代理对象对外提供了统一的调用接口。
步骤
- 依赖注入
/nest-app/apps/nest-app/src/app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ClientsModule.register([
{
name: 'NEST_SERVICE',
transport: Transport.TCP,
options: {
port: 4000,
},
},
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
复制代码
使用时通过@Inject('NEST_SERVICE')
进行注入
- app.controller修改
/nest-app/apps/nest-app/src/app.controller.ts
import { Controller, Get, Inject, Query } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(@Inject('NEST_SERVICE') private readonly client: ClientProxy) {}
@Get('hello')
getHello(@Query() query: any): Promise<string> {
return this.client.send<string>({ cmd: 'getHello' }, query.name).toPromise();
}
}
复制代码
测试
demo1代码仓库地址:github.com/xiaoqiao112…
demo2代码仓库地址:github.com/xiaoqiao112…
参考网址:
juejin.cn/post/684490…
juejin.cn/post/705881…
wayou.github.io/2020/07/17/…