2-5 高可用实践:nestjs拦截器处理微服务异常+重试请求
gRPC 调用中的异常场景
在微服务架构中,gRPC 调用可能遇到以下异常:
| 异常场景 | gRPC 状态码 | 说明 |
|---|---|---|
| 服务不可用 | 14 (UNAVAILABLE) | 目标服务未启动或网络不通 |
| 连接拒绝 | 14 (UNAVAILABLE) | ECONNREFUSED,端口未监听 |
| 超时 | 4 (DEADLINE_EXCEEDED) | 请求超时未响应 |
| 未知错误 | 2 (UNKNOWN) | 服务端内部错误 |
其中最常见的是 UNAVAILABLE (14),表示目标微服务实例已不可达。
NestJS 拦截器方案
拦截器职责
拦截器的核心职责是:当 gRPC 调用失败时,自动触发服务更新并重试请求。
请求进入
└── 拦截器拦截
└── 调用 gRPC 方法
├── 成功 → 返回结果
└── 失败(UNAVAILABLE)
├── 调用 ConsulService.updateService()
├── 等待新 Client 就绪
└── 使用 axios 重新发起 HTTP 请求
text
拦截器实现
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError, from, switchMap, catchError } from 'rxjs';
import { ConsulService } from '../consul.service';
import axios from 'axios';
@Injectable()
export class GrpcExceptionInterceptor implements NestInterceptor {
constructor(private readonly consulService: ConsulService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
// 检测 gRPC UNAVAILABLE 错误
if (this.isUnavailableError(error)) {
// 触发服务更新
return from(this.consulService.updateService()).pipe(
switchMap(() => {
// 更新完成后,使用 axios 重试请求
const request = context.switchToHttp().getRequest();
return from(this.retryWithAxios(request));
}),
);
}
return throwError(() => error);
}),
);
}
private isUnavailableError(error: any): boolean {
return (
error?.code === 14 || // UNAVAILABLE
error?.message?.includes('ECONNREFUSED') ||
error?.details?.includes('connection refused')
);
}
private async retryWithAxios(request: any) {
const { method, url, body } = request;
const response = await axios({
method,
url,
data: body,
});
return response.data;
}
}
typescript
关键设计点
| 设计点 | 说明 |
|---|---|
| 错误识别 | 通过 code === 14 或 ECONNREFUSED 判断服务不可用 |
| 服务更新 | 捕获异常后立即触发 updateService() |
| 请求重试 | 更新完成后通过 axios 重新发起原始 HTTP 请求 |
| 非阻塞 | 使用 RxJS pipe 链式处理,不阻塞其他请求 |
为什么使用 axios 重试
为什么不直接使用更新后的 gRPC Client 重试?原因在于:
| 方案 | 问题 |
|---|---|
| 直接 gRPC 重试 | Client 更新是异步的,新 Client 可能还未完全就绪 |
| 等待 Client 就绪后重试 | 需要引入额外的等待逻辑,增加复杂度 |
| HTTP 重试(推荐) | 通过 axios 重新发起 HTTP 请求,请求会重新经过完整的 NestJS 管道,自动使用最新的 Client |
HTTP 重试方案的优势:
- 请求重新走完整的 NestJS 请求管道
- 拦截器、Guard、Pipe 等中间件都会重新执行
- 最终会使用 ConsulService 中最新的 gRPC Client
- 实现简单,不需要处理 Client 就绪状态
注册拦截器
在 Module 或 Controller 级别注册拦截器:
// 方式一:全局注册
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: GrpcExceptionInterceptor,
},
],
})
export class AppModule {}
// 方式二:Controller 级别
@UseInterceptors(GrpcExceptionInterceptor)
@Controller('users')
export class UserController {}
typescript
异常处理流程
Client 发起 HTTP 请求
└── NestJS Router 匹配 Controller
└── Controller 调用 ConsulService.getClient()
└── 使用 gRPC Client 调用微服务
├── 成功 → 返回响应
└── 失败(code: 14)
└── GrpcExceptionInterceptor 捕获
├── consulService.updateService()
│ └── 从 Consul 获取新健康实例
│ └── 更新 BehaviorSubject
└── axios 重新发起原始 HTTP 请求
└── 新请求使用更新后的 Client
└── 返回响应
text
注意事项
| 注意点 | 说明 |
|---|---|
| 重试次数限制 | 当前方案只重试一次,极端情况可能需要多次重试 |
| 重试超时 | 需要设置合理的超时时间,避免请求长时间挂起 |
| 循环重试 | 如果重试请求再次失败,不应无限循环 |
| 日志记录 | 建议在拦截器中记录异常和重试信息,便于排查问题 |
参考资源
- NestJS Interceptors - 拦截器文档
- gRPC Status Codes - gRPC 状态码
- RxJS catchError - 错误处理操作符
↑