NestJS微服务架构实践

文章主要围绕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,主项目和微服务没在一个文件夹下,是单独的,需要各自分别启动,此时的架构图为:

image.png

微服务

步骤

  1. 创建微服务,安装内置的微服务模块
nest new ms-math
yarn add @nestjs/microservices
复制代码
  1. 修改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();

复制代码
  1. 修改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), {});
  }
}
复制代码
  1. 修改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

主项目

步骤

  1. 创建主项目,安装微服务依赖
nest new ms-app
yarn add @nestjs/microservices
复制代码
  1. 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 {}
复制代码
  1. 模块注册成功后,在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(),基于事件的消息发送方法,无论你是否订阅数据,该消息都会被立即发送

  1. 到这里主项目就搭建完了,启动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"
}'
复制代码

输出

image.png

基于事件的传输方式

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服务的终端看到以下打印

image.png

使用redis作为消息代理

什么是消息代理

消息代理(Message broker)是一个中间程序模块,在计算机网络中用于交换消息,它是面向消息的中间件的建造模块,因此它的职责并不包括负责远程过程调度(RPC).

消息代理也是一种架构模式,用于消息验证、变换、路由。调节应用程序的通信,极小化互相感知(依赖),有效实现解耦合。例如,消息代理可以管理一个工作负荷队列或消息队列,用于多个接收者,提供可靠存储、保证消息分发、以及事务管理

为什么选用redis

  • Redis本身足够轻量级与高效,使用率非常高,比较受欢迎
  • 个人本身对redis有一定的了解,上手起来会更快

服务架构的变化

在这里引用一张图,非常清晰明了:

image.png

demo实现

  1. 创建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查看服务状态:

  1. 在ms-app与ms-math中安装 Redis依赖yarn add redis

  2. 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就改造完成了

  1. 然后打开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',
        },
      },
    ]),
  ],
复制代码
  1. 最后通过curl进行测试即可

demo2

此处的demo主项目和微服务在一个文件夹下,统一进行管理。只需启动最外层的yarn start:dev,不是单独启动主项目和微服务。

  1. 生成外层文件项目,该项目将作为主项目使用
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
复制代码
  1. 再生成另一个项目作为微服务项目:
cd nest-app
nest g app nest-service
复制代码

此时的目录结构更新成了

image.png

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项目使其提供微服务被调用的能力。

步骤

  1. 添加微服务依赖yarn add @nestjs/microservices
  2. 创建微服务,修改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();
复制代码
  1. 添加消息处理器

/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

  1. 修改/nest-service/src/nest-service.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class NestServiceService {
  getHello(name: string): string {
    return `Hello ${name}!`;
  }
}
复制代码
  1. 修改启动命令

启动单个项目

  • 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对象作为统一的客户端,完成初始化之后使用者无需关心不同类型微服务的差异,该代理对象对外提供了统一的调用接口。

步骤

  1. 依赖注入

/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') 进行注入

  1. 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();
  }
}

复制代码

测试

image.png

demo1代码仓库地址:github.com/xiaoqiao112…

demo2代码仓库地址:github.com/xiaoqiao112…

参考网址:

juejin.cn/post/684490… juejin.cn/post/705881… wayou.github.io/2020/07/17/…

Guess you like

Origin juejin.im/post/7075233589977153549