API Gateway 模式
一、职责与架构
API Gateway 是所有客户端请求的单一入口,将内部微服务拓扑对外隐藏:
客户端(Web/Mobile/Third-party)
│
▼
┌───────────────────────────────┐
│ API Gateway │
│ ┌─────────────────────────┐ │
│ │ JWT 认证 & 权限验证 │ │
│ │ 请求路由与转发 │ │
│ │ 响应聚合(BFF) │ │
│ │ 限流 & 熔断 │ │
│ │ 请求日志 & 追踪 │ │
│ └─────────────────────────┘ │
└──────────────┬────────────────┘
│ (内部通信:Redis/TCP/gRPC)
┌──────────┼──────────┐
▼ ▼ ▼
user-service post-service email-serviceGateway 的核心职责:
- 认证授权:JWT 验证在 Gateway 层集中处理,下游服务信任 Gateway 传递的用户信息
- 路由转发:将 HTTP 请求转发给对应微服务
- 请求聚合:一个 HTTP 请求聚合多个服务的数据(BFF 模式)
- 横切关注点:限流、日志、CORS、监控
二、Gateway 项目结构
gateway/src/
├── app.module.ts # 注册所有 ClientsModule
├── main.ts # HTTP 入口
├── auth/
│ ├── guards/
│ │ └── jwt-auth.guard.ts # JWT 验证(在 Gateway 层)
│ └── strategies/
│ └── jwt.strategy.ts
├── users/
│ ├── users.controller.ts # HTTP 路由 → 转发到 user-service
│ └── users.proxy.service.ts # 封装微服务调用
└── posts/
├── posts.controller.ts
└── posts.proxy.service.ts三、完整 Gateway 实现
模块配置
typescript
// gateway/src/app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ClientsModule.registerAsync([
{
name: 'USER_SERVICE',
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
transport: Transport.REDIS,
options: {
host: config.get('REDIS_HOST', 'localhost'),
port: config.get('REDIS_PORT', 6379),
},
}),
},
{
name: 'CONTENT_SERVICE',
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
transport: Transport.REDIS,
options: {
host: config.get('REDIS_HOST', 'localhost'),
port: config.get('REDIS_PORT', 6379),
},
}),
},
]),
AuthModule,
UsersModule,
PostsModule,
],
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, // 全局 JWT 认证
],
})
export class AppModule {}用户代理服务
typescript
// gateway/src/users/users.proxy.service.ts
@Injectable()
export class UsersProxyService {
constructor(
@Inject('USER_SERVICE') private readonly client: ClientProxy,
) {}
findAll(query: { page: number; limit: number }) {
return firstValueFrom(
this.client.send('find_users', query).pipe(
timeout(5000),
catchError(this.handleRpcError),
),
);
}
findOne(id: number) {
return firstValueFrom(
this.client.send('find_user', { id }).pipe(
timeout(3000),
catchError(this.handleRpcError),
),
);
}
// 统一错误处理
private handleRpcError = (err: any) => {
const status = err?.statusCode ?? 500;
const message = err?.message ?? '服务暂时不可用';
const exceptions = {
400: () => new BadRequestException(message),
401: () => new UnauthorizedException(message),
403: () => new ForbiddenException(message),
404: () => new NotFoundException(message),
409: () => new ConflictException(message),
};
const factory = exceptions[status] ?? (() => new InternalServerErrorException(message));
return throwError(factory);
};
}路由控制器
typescript
// gateway/src/users/users.controller.ts
@ApiTags('用户')
@ApiBearerAuth()
@Controller('users')
export class UsersController {
constructor(
private usersProxy: UsersProxyService,
private postsProxy: PostsProxyService,
) {}
@Get()
@Roles(Role.ADMIN)
findAll(@Query() query: PaginationDto) {
return this.usersProxy.findAll(query);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersProxy.findOne(id);
}
// BFF:聚合用户 + 该用户的文章
@Get(':id/profile')
async getUserProfile(@Param('id', ParseIntPipe) id: number) {
const [user, posts] = await Promise.all([
this.usersProxy.findOne(id),
this.postsProxy.findByAuthor(id),
]);
return { ...user, recentPosts: posts.items.slice(0, 5) };
}
@Delete(':id')
@Roles(Role.ADMIN)
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersProxy.remove(id);
}
}四、BFF(Backend for Frontend)模式
BFF 是 Gateway 的扩展——针对不同客户端(Web、Mobile)提供不同的聚合接口:
typescript
// 移动端首页:需要用户信息 + 推荐文章 + 未读通知数
@Get('mobile/home')
async getMobileHome(@CurrentUser() user: User) {
const [profile, recommendations, unreadCount] = await Promise.allSettled([
this.usersProxy.findOne(user.id),
this.postsProxy.getRecommendations(user.id),
this.notificationsProxy.getUnreadCount(user.id),
]);
return {
user: profile.status === 'fulfilled' ? profile.value : null,
recommendations: recommendations.status === 'fulfilled' ? recommendations.value : [],
unreadCount: unreadCount.status === 'fulfilled' ? unreadCount.value : 0,
};
// 使用 allSettled 而非 all:某个服务挂了不影响其他数据返回
}五、熔断器(Circuit Breaker)
防止级联故障——当下游服务不可用时,快速失败而不是等待超时:
bash
npm install opossumtypescript
import CircuitBreaker from 'opossum';
@Injectable()
export class UsersProxyService {
private breaker: CircuitBreaker;
constructor(@Inject('USER_SERVICE') private client: ClientProxy) {
this.breaker = new CircuitBreaker(
(pattern: string, data: any) =>
firstValueFrom(this.client.send(pattern, data).pipe(timeout(3000))),
{
timeout: 3000, // 超过 3 秒视为失败
errorThresholdPercentage: 50, // 50% 失败率时打开熔断
resetTimeout: 30000, // 30 秒后尝试半开状态
},
);
this.breaker.fallback(() => ({ error: 'user-service 暂时不可用' }));
this.breaker.on('open', () => console.warn('熔断器打开:user-service 不可用'));
this.breaker.on('close', () => console.log('熔断器关闭:user-service 已恢复'));
}
async findOne(id: number) {
return this.breaker.fire('find_user', { id });
}
}六、分布式追踪
bash
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-nodetypescript
// tracing.ts(在 main.ts 最顶部引入)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'api-gateway',
}),
// 自动给 HTTP、Express、Redis 等添加追踪
});
sdk.start();typescript
// 在 Interceptor 中传递 traceId
@Injectable()
export class TraceInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const traceId = request.headers['x-trace-id'] ?? randomUUID();
// 将 traceId 注入请求,可以通过 @TraceId() 装饰器获取
request.traceId = traceId;
return next.handle().pipe(
tap(() => {
context.switchToHttp().getResponse().setHeader('X-Trace-Id', traceId);
}),
);
}
}七、本仓库实战方案
practice/05-microservices/
├── docker-compose.yml
├── gateway/ # HTTP :3000,JWT 认证,转发请求
│ └── src/
│ ├── auth/ # 登录接口(调用 user-service 验证)
│ ├── users/ # 代理到 user-service
│ └── posts/ # 代理到 content-service
├── user-service/ # 用户注册/查询/JWT 签发
│ └── src/
│ └── users/ # @MessagePattern 处理 find_user / create_user
└── content-service/ # 文章 CRUD
└── src/
└── posts/ # @MessagePattern 处理 find_posts / create_post通信流程示例(创建文章):
POST /posts → Gateway
├── JwtAuthGuard 验证 Token(req.user = { id, role })
├── contentProxy.createPost({ ...dto, authorId: user.id })
│ └── client.send('create_post', data)
└── content-service: @MessagePattern('create_post')
├── 保存到自己的数据库
├── emit('post_created', { postId, authorId })
└── user-service: @EventPattern('post_created') → 更新用户文章计数八、部署注意事项
- 服务启动顺序:
docker-compose用depends_on+healthcheck确保 Redis 先启动 - 服务名作为主机名:Docker 网络中,服务名即 DNS 名(
host: 'redis'而非localhost) - 环境变量注入:每个服务通过
.env或 Docker 环境变量配置 Redis 连接 - Gateway 不持有业务数据库:Gateway 只做路由,不应该直接查业务数据库
- 健康检查端点:每个服务都暴露
/health,Gateway 聚合展示整体健康状态
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
| 微服务错误被 Gateway 吞掉返回 500 | 未处理 RpcException | 捕获 RpcException,将 statusCode 映射回对应 HTTP 异常 |
| BFF 聚合请求部分失败导致整体失败 | 用 Promise.all() 任一失败则全部失败 | 改用 Promise.allSettled(),对失败的请求返回默认值 |
| 熔断器未触发 | 错误率阈值未达到 | 检查 errorThresholdPercentage 配置;确保错误类型被计入统计 |
| 链路追踪 traceId 断链 | 微服务调用时未传递 context | 在消息 payload 中传递 traceId,各服务用 AsyncLocalStorage 传播 |