Consul 客户端集成
Consul 服务端部署好之后,下一步是将 NestJS 微服务注册到 Consul,使消费者能够通过服务发现获取实例地址。
安装依赖
# Consul 客户端库
pnpm add consul
# 类型定义(开发环境)
pnpm add -D @types/consul
bash
consul 是 Node.js 生态中使用最广泛的 Consul 客户端库,封装了 Agent、Catalog、Health 等 API。安装 @types/consul 后可以获得完整的 TypeScript 类型提示,避免手动查阅文档。
创建 Consul 客户端服务
在微服务项目中新建 src/consul/consul-client.service.ts,封装服务注册和注销逻辑:
// src/consul/consul-client.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'
import Consul from 'consul'
@Injectable()
export class ConsulClientService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(ConsulClientService.name)
private readonly consul: Consul.Consul
private readonly serviceId = 'nest-service-user'
private readonly serviceName = 'user-service'
private readonly servicePort = 5001
constructor() {
this.consul = new Consul({
host: 'localhost',
port: '8500', // Consul 注册中心端口
secure: false,
// promisify: true, // 如需 Promise API 可启用
})
}
async onModuleInit() {
await this.registerService()
}
async onModuleDestroy() {
await this.deregisterService()
}
/** 注册服务到 Consul */
private async registerService() {
try {
await this.consul.agent.service.register({
id: this.serviceId,
name: this.serviceName,
address: '192.168.31.221', // 宿主机 IP(非 localhost)
port: this.servicePort,
check: {
http: 'http://192.168.31.221:3000/health',
interval: '10s',
timeout: '5s',
},
})
this.logger.log(`Service registered: ${this.serviceId}`)
} catch (error) {
this.logger.error(`Service registration failed: ${error.message}`)
}
}
/** 从 Consul 注销服务 */
private async deregisterService() {
try {
await this.consul.agent.service.deregister(this.serviceId)
this.logger.log(`Service deregistered: ${this.serviceId}`)
} catch (error) {
this.logger.error(`Service deregistration failed: ${error.message}`)
}
}
}
typescript
关键设计说明:
| 设计点 | 说明 |
|---|---|
OnModuleInit | 应用启动时自动调用 registerService() |
OnModuleDestroy | 应用关闭时自动调用 deregisterService() |
address 使用宿主机 IP | Consul 运行在 Docker 容器内,localhost 指向容器自身,无法访问宿主机服务 |
check.http | 健康检查的 HTTP 端点,Consul 会定期请求 |
check.interval | 健康检查间隔,10 秒是一个合理的默认值 |
宿主机 IP 的获取
Docker 容器内的 Consul 无法通过 localhost 访问宿主机上的微服务。需要获取宿主机的局域网 IP:
# macOS
ifconfig | grep "inet " | grep -v 127.0.0.1
# Linux
hostname -I | awk '{print $1}'
bash
将获取到的 IP(如 192.168.31.221)替换到 registerService() 的 address 和 check.http 中。
开发环境简化方案:Docker Desktop 用户可以使用 host.docker.internal 作为宿主机地址:
address: 'host.docker.internal',
check: {
http: 'http://host.docker.internal:3000/health',
interval: '10s',
},
typescript
这在 macOS 和 Windows 的 Docker Desktop 中开箱即用,无需手动查 IP。
注册到模块
将 ConsulClientService 添加到模块的 providers 中,确保 NestJS 会实例化它并触发生命周期钩子:
// src/app.module.ts
import { Module } from '@nestjs/common'
import { ConsulClientService } from './consul/consul-client.service'
@Module({
providers: [ConsulClientService],
// ...
})
export class AppModule {}
typescript
健康检查端点
Consul 通过 HTTP 请求检测服务是否存活。需要在微服务中暴露一个 /health 端点:
// src/consul/consul.controller.ts
import { Controller, Get } from '@nestjs/common'
@Controller('health')
export class ConsulController {
@Get()
check() {
return { status: 'ok' }
}
}
typescript
响应内容并不重要,关键是返回 HTTP 200 状态码。 Consul 收到 200 即认为服务健康。
将控制器也注册到模块:
// src/app.module.ts
import { Module } from '@nestjs/common'
import { ConsulClientService } from './consul/consul-client.service'
import { ConsulController } from './consul/consul.controller'
@Module({
controllers: [ConsulController],
providers: [ConsulClientService],
})
export class AppModule {}
typescript
同时启用微服务和 HTTP 监听
默认情况下,NestJS 的 createMicroservice() 不会监听 HTTP 端口。但健康检查需要一个 HTTP 端点。解决方案是同时创建 Express 实例和微服务连接:
// src/main.ts
import { NestFactory } from '@nestjs/core'
import { Transport, MicroserviceOptions } from '@nestjs/microservices'
import { AppModule } from './app.module'
import { join } from 'path'
async function bootstrap() {
// 1. 创建 HTTP 应用(Express)
const app = await NestFactory.create(AppModule)
// 2. 连接微服务传输层
const microserviceOptions: MicroserviceOptions = {
transport: Transport.GRPC,
options: {
package: 'user',
protoPath: join(__dirname, '../proto/user.proto'),
url: 'localhost:5001',
},
}
app.connectMicroservice(microserviceOptions)
// 3. 启动微服务
await app.startAllMicroservices()
// 4. 监听 HTTP 端口(供健康检查使用)
await app.listen(3000)
console.log('User microservice running on gRPC:5001, HTTP:3000')
}
bootstrap()
typescript
这样微服务同时拥有两个通信通道:
| 端口 | 协议 | 用途 |
|---|---|---|
| 5001 | gRPC | 微服务间通信(业务调用) |
| 3000 | HTTP | 健康检查(Consul 探测) |
优雅退出
OnModuleDestroy 只在 NestJS 正常关闭时触发。调试过程中直接终止进程(Ctrl+C)不会触发它。为保险起见,监听进程退出信号:
// src/main.ts(在 bootstrap() 末尾添加)
process.on('SIGTERM', async () => {
await app.close()
process.exit(0)
})
process.on('SIGINT', async () => {
await app.close()
process.exit(0)
})
typescript
同时在 AppModule 中启用 shutdown hooks:
// src/app.module.ts
import { Module } from '@nestjs/common'
@Module({
// ...
})
export class AppModule {
// 启用 shutdown hooks,确保 OnModuleDestroy 被调用
static enableShutdownHooks = true
}
typescript
或者更推荐的方式,在 main.ts 中调用:
app.enableShutdownHooks()
typescript
验证注册结果
启动微服务后,打开浏览器访问 Consul Web UI:
http://localhost:8500
text
在 Services 页面可以看到:
- 注册成功:
user-service出现在服务列表中 - 健康检查通过:状态显示绿色对勾,表示
http://<host-ip>:3000/health返回了 200
如果看到红色叉号,检查以下几点:
| 问题 | 排查方式 |
|---|---|
| 地址不通 | 在 Consul 容器内 curl http://<host-ip>:3000/health |
| 端口未监听 | 确认 app.listen(3000) 已执行 |
| IP 错误 | 确认 ifconfig 获取的 IP 与注册地址一致 |
| 防火墙拦截 | 临时关闭防火墙测试 |
整体流程
微服务启动
→ ConsulClientService.onModuleInit()
→ consul.agent.service.register()
→ Consul 记录服务信息(名称、地址、端口)
Consul 定期健康检查
→ 每 10 秒请求 http://<host-ip>:3000/health
→ ConsulController.check() 返回 { status: 'ok' }
→ Consul 标记服务为健康状态
微服务关闭
→ SIGTERM/SIGINT 信号
→ app.close()
→ ConsulClientService.onModuleDestroy()
→ consul.agent.service.deregister()
→ Consul 移除服务记录
text
至此,微服务已经完成了向 Consul 的注册和健康检查配置。下一步是从消费者端(网关)通过 Consul 获取服务列表并动态调用。
↑