Skip to content

API Gateway 模式

一、职责与架构

API Gateway 是所有客户端请求的单一入口,将内部微服务拓扑对外隐藏:

客户端(Web/Mobile/Third-party)


   ┌───────────────────────────────┐
   │        API Gateway            │
   │  ┌─────────────────────────┐  │
   │  │  JWT 认证 & 权限验证    │  │
   │  │  请求路由与转发         │  │
   │  │  响应聚合(BFF)        │  │
   │  │  限流 & 熔断            │  │
   │  │  请求日志 & 追踪        │  │
   │  └─────────────────────────┘  │
   └──────────────┬────────────────┘
                  │  (内部通信:Redis/TCP/gRPC)
       ┌──────────┼──────────┐
       ▼          ▼          ▼
  user-service  post-service  email-service

Gateway 的核心职责:

  1. 认证授权:JWT 验证在 Gateway 层集中处理,下游服务信任 Gateway 传递的用户信息
  2. 路由转发:将 HTTP 请求转发给对应微服务
  3. 请求聚合:一个 HTTP 请求聚合多个服务的数据(BFF 模式)
  4. 横切关注点:限流、日志、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 opossum
typescript
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-node
typescript
// 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') → 更新用户文章计数

八、部署注意事项

  1. 服务启动顺序docker-composedepends_on + healthcheck 确保 Redis 先启动
  2. 服务名作为主机名:Docker 网络中,服务名即 DNS 名(host: 'redis' 而非 localhost
  3. 环境变量注入:每个服务通过 .env 或 Docker 环境变量配置 Redis 连接
  4. Gateway 不持有业务数据库:Gateway 只做路由,不应该直接查业务数据库
  5. 健康检查端点:每个服务都暴露 /health,Gateway 聚合展示整体健康状态

常见错误

错误原因解决
微服务错误被 Gateway 吞掉返回 500未处理 RpcException捕获 RpcException,将 statusCode 映射回对应 HTTP 异常
BFF 聚合请求部分失败导致整体失败Promise.all() 任一失败则全部失败改用 Promise.allSettled(),对失败的请求返回默认值
熔断器未触发错误率阈值未达到检查 errorThresholdPercentage 配置;确保错误类型被计入统计
链路追踪 traceId 断链微服务调用时未传递 context在消息 payload 中传递 traceId,各服务用 AsyncLocalStorage 传播

NestJS 深度学习体系