背景 在 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
创建 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 , }; } }
创建 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 ({ 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 ({ 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 ) {} @Get ("fetchLatestNews" ) async fetchLatestNews (@Query () query ) { const latestTime = query.latestTime || new Date ().getTime (); return await this .pushServer .send ("fetchLatestNews" , latestTime); } }
常见问题
无法连接微服务
自检防火墙是否放开端口 检查微服务是否正常启动
1 2 3 [Nest] 356351 - 2024-11 -09 21:20:11 AM ERROR [ExceptionsHandler] connect ECONNREFUSED xx.xx.xx.xx:3001Error: connect ECONNREFUSED xx.xx.xx.xx:3001 at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1278:16)
1 2 PORT STATE SERVICE 3001/tcp closed nessus
1 2 3 PORT STATE SERVICE 3001/tcp open nessus