2024-10-28-【研发】nestjs多节点部署微服务

背景

在 nestjs 微服务开发过程中,本地开发机启动一个主服务,以及多个微服务时,是能够直接访问主服务的端口访问到各个微服务的接口;但我们将微服务部署到多台服务器上后,微服务的注册地址即使改为公网 ip 仍然无法直接访问到各个微服务的接口,这时就需要使用 Consul 来进行服务发现与注册,实现高效的微服务节点管理。

示例中,假定一个主服务 MAIN_SERVER,一个微服务 PUSH_SERVER 部署在两台服务器上,分别为 SERVER1 和 SERVER2。consul 服务部署在另一台服务器 SERVER3 上。

第一步 consul 服务搭建

安装 consul 1.x(服务器中安装)

1.1 通过 docker-compose 安装 consul curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
1.2 设置 docker-compose 执行权限 sudo chmod +x /usr/local/bin/docker-compose
1.3 创建 docker-compose.yml 文件 vi docker-compose.yml
1.4 写入以下内容:

1
2
3
4
5
6
7
8
9
10
version: "3"

services:
consul:
image: consul:1.10.1
container_name: consul_dev
command: agent -server -node=consul_dev -bootstrap-expect=1 -bind=127.0.0.1 -client=0.0.0.0 -datacenter=dc1 -ui
ports:
- "18500:8500"
network_mode: bridge

2.5 启动 consul docker-compose up -d
2.6 验证 consul 运行状态 docker ps

第二步 创建 consul module

    1. 创建 consul/consul.service.ts 文件,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Inject, Injectable } from "@nestjs/common";
import * as Consul from "consul";
import { ConsulServiceNode } from "./consul.interface";

@Injectable()
export class ConsulService {
constructor(@Inject("CONSUL") private consul: Consul.Consul) {}

async register(options: Consul.Agent.Service.RegisterOptions) {
return await this.consul.agent.service.register(options);
}

async deregister(id: string) {
return await this.consul.agent.service.deregister(id);
}

async maintenance(options: Consul.Agent.Service.MaintenanceOptions) {
return await this.consul.agent.service.maintenance(options);
}

async findService(
serviceName: string
): Promise<{ host: string; port: number }> {
const services = await this.consul.catalog.service.nodes<
ConsulServiceNode[]
>(serviceName);
if (!services.length) {
throw new Error(`Service ${serviceName} not found`);
}
const service = services[0];
return {
host: service.ServiceAddress,
port: service.ServicePort,
};
}
}
  1. 创建 consul/consul.module.ts 文件,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Global, Module } from "@nestjs/common";
import { ConsulService } from "./consul.service";
import * as Consul from "consul";
import { ConfigService } from "@nestjs/config";

@Global()
@Module({})
export class ConsulModule {
static forRoot() {
const provider = {
provide: "CONSUL",
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return new Consul({
// 也可以直接写死 consul 地址
host: config.get("CONSUL_HOST"),
port: config.get("CONSUL_PORT"),
promisify: true,
});
},
};

return {
module: ConsulModule,
providers: [provider, ConsulService],
exports: [provider, ConsulService],
};
}
}

第三步:nestjs 微服务注册

借助 onModuleInit 生命周期,在主服务启动时,注册微服务到 consul
在 app.module.ts 中注入微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { Module, OnModuleInit } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { NewsModule } from "./news/news.module";
import { ConsulModule } from "./consul/consul.module";
import { ConsulService } from "./consul/consul.service";
import { ConfigModule, ConfigService } from "@nestjs/config";

const envFilePath = `.env.${process.env.NODE_ENV || "prod"}`;

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath,
}),
NewsModule,
ConsulModule.forRoot(),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements OnModuleInit {
constructor(
private readonly consulService: ConsulService,
private readonly config: ConfigService
) {}

async onModuleInit() {
const config = this.config;
await this.consulService.register({
// 可以直接写死ip;如果服务器间组网可内网 ip 互联则写内网 ip,否则写公网 ip
name: config.get("APP_NAME"),
address: config.get("APP_HOST"),
port: Number(config.get("APP_PORT") || 3001),
});
}
}

第四步 nestjs 主服务(网关)微服务发现

app.module.ts 中注入微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 全局注册
@Global()
@Module({
imports: [
ClientsModule.registerAsync([
{
name: 'PUSH_SERVER',
useFactory: async (consulService: ConsulService) => {
const serverName = 'pushServer_' + process.env.NODE_ENV || 'prod';
const { host, port } = await consulService.findService(serverName);
return {
transport: Transport.TCP,
options: {
host,
port,
},
};
},
inject: [ConsulService],
},
]),
],
})

第五步 主服务(网关)微服务调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ClientProxy } from "@nestjs/microservices";

export class AppController {
// 注入微服务
constructor(@Inject("PUSH_SERVER") private pushServer: ClientProxy) {}

// 调用方 - 主服务(网关)send
@Get("fetchLatestNews")
async fetchLatestNews(@Query() query) {
const latestTime = query.latestTime || new Date().getTime();
// this.pushServer.emit('fetchNewsTask', {});
return await this.pushServer.send("fetchLatestNews", latestTime);
}
}

常见问题

  1. 无法连接微服务

    自检防火墙是否放开端口
    检查微服务是否正常启动

1
2
3
[Nest] 356351  - 2024-11-09 21:20:11 AM   ERROR [ExceptionsHandler] connect ECONNREFUSED xx.xx.xx.xx:3001
Error: connect ECONNREFUSED xx.xx.xx.xx:3001
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1278:16)
  • 1.1 微服务自检端口

  • lsof -i:3001 (如果为 TCP localhost:3001 则需要将微服务启动监听 0.0.0.0)

  • node\x20/ 158029 root 19u IPv4 901314 0t0 TCP *:3001 (LISTEN)

  • 1.2 主服务访问微服务端口

  • nmap -p 3001 10.0.0.4

1
2
PORT     STATE SERVICE
3001/tcp closed nessus
1
2
3

PORT STATE SERVICE
3001/tcp open nessus

2024-10-28-【研发】nestjs多节点部署微服务
https://zhangyingxuan.github.io/2024-10-28-【研发】nestjs多节点部署微服务/
作者
blowsysun
更新于
2026年1月23日
许可协议