📖 第22课:API网关

阶段五:实战项目

API网关是微服务架构的"前门"——所有外部请求都经过它。它统一处理认证、限流、路由、日志等横切关注点,让后端服务专注业务逻辑。本课实现一个功能完整的API网关,涵盖路由转发、认证中间件、限流、熔断、日志和健康检查。

🏛️ API网关核心功能

功能说明实现方式
路由转发将请求路由到正确的后端服务路径匹配+代理
认证授权统一验证身份和权限JWT验证中间件
限流防止滥用和过载令牌桶/滑动窗口
熔断防止故障级联断路器模式
负载均衡分发请求到多个实例轮询/权重/最少连接
日志监控请求追踪和指标收集请求ID+日志中间件
协议转换REST↔gRPC等grpc-gateway
缓存减少重复请求响应缓存中间件

🛠️ 实战:完整API网关

// api-gateway.js - 完整API网关
const express = require('express');
const http = require('http');
const crypto = require('crypto');

const app = express();

// ===== 配置 =====

const SERVICES = {
  'user-service': { url: 'http://localhost:3001', prefix: '/api/users', healthCheck: '/health' },
  'product-service': { url: 'http://localhost:3002', prefix: '/api/products', healthCheck: '/health' },
  'order-service': { url: 'http://localhost:3003', prefix: '/api/orders', healthCheck: '/health' },
};

// ===== 请求ID中间件 =====
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
  res.setHeader('X-Request-ID', req.requestId);
  res.setHeader('X-Gateway', 'api-gateway/1.0');
  next();
});

// ===== 日志中间件 =====
const requestLogs = [];
app.use((req, res, next) => {
  const start = Date.now();
  const entry = { requestId: req.requestId, method: req.method, path: req.path, start, userId: null };

  res.on('finish', () => {
    entry.duration = Date.now() - start;
    entry.statusCode = res.statusCode;
    entry.userId = req.userId || null;
    requestLogs.push(entry);
    console.log(`→ ${req.method} ${req.path} ${res.statusCode} ${entry.duration}ms [${req.requestId}]`);
  });

  next();
});

// ===== 认证中间件 =====
const JWT_SECRET = 'gateway-secret-key';

function verifyToken(token) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
    if (payload.exp && payload.exp < Date.now() / 1000) return null;
    return payload;
  } catch { return null; }
}

function authenticate(req, res, next) {
  const publicPaths = ['/api/auth/login', '/api/auth/register', '/health', '/api/products'];
  if (publicPaths.some(p => req.path.startsWith(p) && req.method === 'GET')) return next();

  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ success: false, error: { code: 'AUTH_MISSING', message: '缺少认证令牌' } });
  }

  const payload = verifyToken(authHeader.substring(7));
  if (!payload) {
    return res.status(401).json({ success: false, error: { code: 'AUTH_INVALID', message: '令牌无效或已过期' } });
  }

  req.userId = payload.sub;
  req.userRole = payload.role;
  next();
}

app.use(authenticate);

// ===== 限流中间件 =====
const rateLimits = new Map();
function rateLimit(options = {}) {
  const { windowMs = 60000, maxRequests = 100 } = options;
  return (req, res, next) => {
    const key = req.userId || req.ip;
    const now = Date.now();
    let record = rateLimits.get(key);

    if (!record || now - record.windowStart > windowMs) {
      record = { count: 0, windowStart: now };
      rateLimits.set(key, record);
    }

    record.count++;
    const remaining = Math.max(0, maxRequests - record.count);
    res.setHeader('X-RateLimit-Limit', maxRequests.toString());
    res.setHeader('X-RateLimit-Remaining', remaining.toString());

    if (record.count > maxRequests) {
      res.setHeader('Retry-After', Math.ceil((windowMs - (now - record.windowStart)) / 1000).toString());
      return res.status(429).json({ success: false, error: { code: 'RATE_LIMIT', message: '请求频率超限' } });
    }
    next();
  };
}

app.use(rateLimit({ windowMs: 60000, maxRequests: 200 }));

// ===== 熔断器 =====
class CircuitBreaker {
  constructor(name, options = {}) {
    this.name = name;
    this.failureThreshold = options.failureThreshold || 5;
    this.timeout = options.timeout || 30000;
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.lastFailureTime = null;
  }

  recordSuccess() { this.failureCount = 0; this.state = 'CLOSED'; }
  recordFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.failureThreshold) this.state = 'OPEN';
  }

  isAvailable() {
    if (this.state === 'CLOSED') return true;
    if (this.state === 'OPEN' && Date.now() - this.lastFailureTime >= this.timeout) {
      this.state = 'HALF_OPEN';
      return true;
    }
    return this.state !== 'OPEN';
  }

  getStatus() { return { name: this.name, state: this.state, failures: this.failureCount }; }
}

const breakers = {};
Object.keys(SERVICES).forEach(name => { breakers[name] = new CircuitBreaker(name); });

// ===== 路由代理 =====

function proxyRequest(req, res, targetUrl) {
  const url = new URL(req.originalUrl, targetUrl);
  const options = {
    method: req.method,
    hostname: url.hostname,
    port: url.port,
    path: url.pathname + url.search,
    headers: {
      ...req.headers,
      host: url.host,
      'x-request-id': req.requestId,
      'x-user-id': req.userId || '',
      'x-user-role': req.userRole || '',
    },
  };

  const proxyReq = http.request(options, (proxyRes) => {
    res.writeHead(proxyRes.statusCode, proxyRes.headers);
    proxyRes.pipe(res);
  });

  proxyReq.on('error', (err) => {
    console.error(`代理错误 [${req.requestId}]:`, err.message);
    if (!res.headersSent) {
      res.status(502).json({ success: false, error: { code: 'BAD_GATEWAY', message: '上游服务不可用' } });
    }
  });

  if (req.body) proxyReq.write(JSON.stringify(req.body));
  proxyReq.end();
}

// 模拟后端服务(生产中这些是独立进程)
function createMockService(port, name, data) {
  const service = express();
  service.use(express.json());

  service.get('/health', (req, res) => res.json({ status: 'healthy', service: name }));

  service.get('/*', (req, res) => {
    res.json({ success: true, data, meta: { service: name, requestId: req.headers['x-request-id'] } });
  });

  service.post('/*', (req, res) => {
    res.status(201).json({ success: true, data: { id: Date.now(), ...req.body }, meta: { service: name } });
  });

  service.listen(port, () => console.log(`  📦 ${name} 运行在 :${port}`));
}

// 路由分发
app.use('/api/users', (req, res) => {
  const breaker = breakers['user-service'];
  if (!breaker.isAvailable()) return res.status(503).json({ success: false, error: { code: 'CIRCUIT_OPEN', message: '用户服务暂时不可用' } });
  // 模拟代理(简化,直接返回模拟数据)
  res.json({ success: true, data: [{ id: 1, name: '张三', email: 'zhang@example.com' }], meta: { service: 'user-service', requestId: req.requestId } });
});

app.use('/api/products', (req, res) => {
  const breaker = breakers['product-service'];
  if (!breaker.isAvailable()) return res.status(503).json({ success: false, error: { code: 'CIRCUIT_OPEN', message: '商品服务暂时不可用' } });
  res.json({ success: true, data: [{ id: 1, name: '机械键盘', price: 599 }], meta: { service: 'product-service', requestId: req.requestId } });
});

app.use('/api/orders', (req, res) => {
  const breaker = breakers['order-service'];
  if (!breaker.isAvailable()) return res.status(503).json({ success: false, error: { code: 'CIRCUIT_OPEN', message: '订单服务暂时不可用' } });
  res.json({ success: true, data: [{ id: 101, total: 798, status: 'completed' }], meta: { service: 'order-service', requestId: req.requestId } });
});

// ===== 网关管理端点 =====

app.get('/health', (req, res) => {
  const services = {};
  for (const [name, breaker] of Object.entries(breakers)) {
    services[name] = breaker.getStatus();
  }
  res.json({ status: 'healthy', gateway: '1.0', services, uptime: process.uptime() });
});

app.get('/gateway/stats', (req, res) => {
  const total = requestLogs.length;
  const byService = {};
  const byStatus = {};

  requestLogs.slice(-1000).forEach(log => {
    const service = log.path.split('/')[2] || 'unknown';
    byService[service] = (byService[service] || 0) + 1;
    const statusGroup = Math.floor(log.statusCode / 100) + 'xx';
    byStatus[statusGroup] = (byStatus[statusGroup] || 0) + 1;
  });

  const avgDuration = total > 0 ? Math.round(requestLogs.slice(-100).reduce((s, l) => s + (l.duration || 0), 0) / Math.min(100, total)) : 0;

  res.json({
    total,
    byService,
    byStatus,
    avgDurationMs: avgDuration,
    circuits: Object.fromEntries(Object.entries(breakers).map(([n, b]) => [n, b.getStatus()])),
  });
});

app.use(express.json());

app.listen(8080, () => {
  console.log('🌉 API网关运行在 http://localhost:8080');
  console.log('  健康检查: http://localhost:8080/health');
  console.log('  统计信息: http://localhost:8080/gateway/stats');
});

测试API网关

# 启动网关
node api-gateway.js

# 1. 公开接口(无需认证)
curl -s http://localhost:8080/api/products | jq .

# 2. 受保护接口(需要认证)
curl -s http://localhost:8080/api/orders | jq .

# 3. 带Token访问
curl -s http://localhost:8080/api/orders \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.xxx" | jq .
{
  "success": true,
  "data": [{"id":101,"total":798,"status":"completed"}],
  "meta": {"service":"order-service","requestId":"..."}
}
# 4. 健康检查
curl -s http://localhost:8080/health | jq .

# 5. 统计信息
curl -s http://localhost:8080/gateway/stats | jq .

📝 本课小结

  1. API网关统一处理路由、认证、限流、熔断、日志
  2. 请求ID贯穿整个调用链,便于追踪
  3. 熔断器保护后端服务,防止故障级联
  4. 限流防止滥用,不同用户不同配额
  5. 健康检查和统计端点支持运维监控
  6. 生产环境使用Nginx/Kong/Envoy等专业网关

📐 网关设计模式

API网关设计模式对比

模式说明优点缺点
单一网关所有请求经过一个网关简单、统一单点故障
BFF网关每类客户端一个网关定制化多实例维护
网格网关Sidecar代理模式无单点、可观测复杂度高
// BFF(Backend for Frontend)网关模式

// Web端BFF
const webBff = express();
webBff.get('/dashboard', async (req, res) => {
  // 聚合多个服务的数据
  const [user, orders, notifications] = await Promise.all([
    fetch('http://user-service/api/users/me'),
    fetch('http://order-service/api/orders/recent'),
    fetch('http://notification-service/api/notifications/unread'),
  ]);
  res.json({ user: user.data, orders: orders.data, notifications: notifications.data });
});

// 移动端BFF(更精简的响应)
const mobileBff = express();
mobileBff.get('/dashboard', async (req, res) => {
  const [user, orders] = await Promise.all([
    fetch('http://user-service/api/users/me?fields=id,name,avatar'),
    fetch('http://order-service/api/orders/recent?limit=3&fields=id,total,status'),
  ]);
  res.json({ user: user.data, recentOrders: orders.data });
});

💪 练习

练习1:实现负载均衡

为product-service配置3个实例,实现轮询负载均衡。提示:维护实例列表和当前索引。

练习2:实现请求缓存

在网关层添加LRU缓存,对GET请求的结果缓存30秒。写操作自动失效缓存。

🏆 本课成就:网关架构师