2-2 高效微服务:定时器驱动的Consul健康检查与gRPC Client管理
问题回顾
上一节分析了 Gateway 启动时固定绑定 gRPC Client 的问题:一旦绑定的服务实例宕机,后续所有请求都会失败。本节开始着手解决这一问题,核心思路是引入定时健康检查和动态 Client 切换机制。
ConsulService 设计
创建一个专门的 ConsulService 来管理服务发现和 gRPC Client 生命周期:
ConsulService
├── healthService: any // 当前健康的服务实例
├── client: any // 当前 gRPC Client
├── timeoutControl: any // 定时器控制
├── initClient() // 初始化 gRPC Client
├── getInstance() // 获取当前 Client
├── updateService() // 从 Consul 获取新服务
├── healthCheck() // 定时健康检查(Cron)
└── onModuleInit() // 模块初始化钩子
text
初始化流程
onModuleInit()
└── updateService()
├── Consul.agent.service.list() → 获取服务列表
├── 过滤出目标服务 + 筛选 passing 状态
└── 成功 → initClient() 创建 gRPC Client
└── 失败 → 5秒后重试(setTimeout)
text
核心方法
updateService
从 Consul 获取最新的健康服务实例。关键步骤:
- 通过
Consul.agent.service.list()获取所有已注册服务 - 按服务名称过滤,只取目标微服务的实例
- 调用
Consul.health.service获取健康状态,筛选passing状态的实例 - 随机选取一个健康实例
- 成功后调用
initClient()创建 gRPC Client - 失败则设置 5 秒定时器重试
initClient
根据获取到的 Consul Service 实例(包含 address 和 port),创建对应的 gRPC Client:
initClient(service: any) {
const packageDef = loadPackageDefinition(loadSync('user'));
const client = new packageDef.UserService(
`${service.Address}:${service.Port}`,
credentials.createInsecure()
);
this.client = client;
}
typescript
healthCheck
使用 NestJS 的 @nestjs/schedule 模块,通过 Cron 表达式定期执行健康检查:
@Cron('*/30 * * * * *') // 每30秒执行一次
async healthCheck() {
try {
// 检查当前 Client 对应的服务是否仍然健康
// 不健康则调用 updateService() 获取新实例
} catch (error) {
await this.updateService();
}
}
typescript
NestJS ScheduleModule 配置
安装依赖
pnpm add @nestjs/schedule
bash
注册模块
在 AppModule 中导入 ScheduleModule.forRoot():
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
ScheduleModule.forRoot(),
ConsulModule.forRoot({
// Consul 连接配置
})
],
})
export class AppModule {}
typescript
ConsulModule 动态模块设计
使用 forRoot 模式实现动态配置注入:
@Module({})
export class ConsulModule {
static forRoot(options: ConsulModuleOptions): DynamicModule {
return {
module: ConsulModule,
providers: [
{
provide: 'CONSUL_OPTIONS',
useValue: options,
},
ConsulService,
],
exports: [ConsulService],
global: true,
};
}
}
typescript
在 ConsulService 中通过 inject() 获取配置:
@Injectable()
export class ConsulService implements OnModuleInit {
constructor(
@Inject('CONSUL_OPTIONS') private options: ConsulModuleOptions,
) {}
async onModuleInit() {
await this.updateService();
}
}
typescript
重试机制
当服务不可用时,不能无限循环重试,需要通过 setTimeout 控制重试间隔:
updateService() 失败
└── 清除之前的 timeout(避免重复)
└── 设置新的 setTimeout(5秒后重试)
└── 再次调用 updateService()
├── 成功 → initClient()
└── 失败 → 继续重试
text
关键代码逻辑:
| 步骤 | 说明 |
|---|---|
| 1. 获取服务列表 | Consul.agent.service.list() |
| 2. 过滤目标服务 | 按名称匹配 |
| 3. 检查健康状态 | Consul.health.service() |
| 4. 筛选 passing 状态 | 只保留健康实例 |
| 5. 创建 Client | initClient() |
| 6. 失败重试 | 5 秒后重新执行步骤 1-5 |
下一步优化
当前实现虽然解决了固定绑定问题,但仍需进一步优化:
| 优化方向 | 说明 |
|---|---|
| Client 发布订阅 | 使用 RxJS BehaviorSubject 实现客户端变更通知 |
| 拦截器异常处理 | 在 gRPC 调用失败时自动切换服务实例 |
| 边界条件处理 | 处理 service 为空时的各种异常场景 |
参考资源
- NestJS Schedule - 定时任务文档
- Consul Agent Service API - 服务注册 API
- NestJS Dynamic Modules - 动态模块
↑