4、API网关设计:网关的作用与模式、使用http-proxy-middleware实现反向代理、限流与熔断基础实现

好,咱们进入第四章。这一章聊API网关,微服务架构里的“交通枢纽”。

说实话,我早期做单体应用的时候,根本没想过网关这回事。后来服务拆多了,问题就来了——每个服务都要处理认证、日志、限流,代码重复得让人崩溃。这时候,网关就成了必需品。

4.1 网关的作用与常见模式

API网关是什么?说白了,它就是所有客户端请求的“统一入口”。

你想想看,如果没有网关,客户端要直接面对十几个微服务。每个服务都要自己处理鉴权、跨域、限流……维护成本直线上升。

网关的核心作用,我总结为三点:

  • 请求路由:根据路径、域名、Header,把请求转发到对应的后端服务
  • 横切关注点:把认证、日志、限流这些通用逻辑收拢到网关层
  • 协议转换:比如把外部的HTTP请求转为内部的gRPC调用

常见的网关模式,我遇到过这么几种:

模式 适用场景 我踩过的坑
单节点网关 小型项目,服务数少于10个 单点故障,挂了全完
集群网关 中等规模,需要高可用 Session同步问题
BFF模式 多端适配(Web/App/小程序) 每个端一个网关,维护成本高
边缘网关 大型系统,需要CDN、WAF 延迟增加,要注意缓存策略

我的建议:刚开始别搞太复杂。先用单节点网关跑起来,等流量上来了再考虑集群。我见过太多团队一上来就搞BFF+边缘网关,结果两个月都没上线。

4.2 使用http-proxy-middleware实现反向代理

好,理论说完了,咱们动手。Node.js生态里,实现反向代理最常用的就是http-proxy-middleware。它基于http-proxy,但用起来更顺手。

先安装:

npm install http-proxy-middleware express

然后写一个最简单的网关:

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 用户服务路由
app.use('/api/users', createProxyMiddleware({
  target: 'http://localhost:3001',
  changeOrigin: true,
  pathRewrite: {
    '^/api/users': '/users'
  }
}));

// 订单服务路由
app.use('/api/orders', createProxyMiddleware({
  target: 'http://localhost:3002',
  changeOrigin: true,
  pathRewrite: {
    '^/api/orders': '/orders'
  }
}));

app.listen(8080, () => {
  console.log('API Gateway running on port 8080');
});

这段代码干了什么?

  • 客户端请求/api/users/login,网关转发到http://localhost:3001/users/login
  • 客户端请求/api/orders/list,网关转发到http://localhost:3002/orders/list

changeOrigin: true这个配置,我一开始没注意,结果后端收到的Host头一直是网关的地址,导致一些基于Host的校验全挂了。嗯,这里要注意。

避坑指南:我曾经在生产环境遇到过一个问题——代理超时。默认的proxyTimeout是30秒,但有个报表接口要跑2分钟。结果客户端一直报504。解决方案很简单:

createProxyMiddleware({
  target: 'http://localhost:3003',
  proxyTimeout: 120000,  // 2分钟
  timeout: 120000
})

4.3 限流基础实现

网关的另一大职责是限流。为什么要限流?说白了,就是防止某个服务被突发流量打垮。

我常用的限流策略有三种:

  • 计数器法:固定时间窗口内计数,超过阈值就拒绝
  • 滑动窗口:把时间窗口切分成小段,更平滑
  • 令牌桶:按速率生成令牌,有令牌才能通过

咱们先实现一个简单的计数器限流:

const rateLimit = new Map();

function rateLimiter(req, res, next) {
  const ip = req.ip;
  const now = Date.now();
  const windowMs = 60 * 1000; // 1分钟
  const maxRequests = 100;    // 最多100次

  if (!rateLimit.has(ip)) {
    rateLimit.set(ip, { count: 1, startTime: now });
    return next();
  }

  const record = rateLimit.get(ip);
  if (now - record.startTime > windowMs) {
    // 窗口过期,重置
    rateLimit.set(ip, { count: 1, startTime: now });
    return next();
  }

  record.count++;
  if (record.count > maxRequests) {
    return res.status(429).json({ error: 'Too many requests' });
  }

  next();
}

app.use('/api', rateLimiter, createProxyMiddleware({...}));

这个实现很简单,但有个问题——内存泄漏。如果请求量很大,rateLimit这个Map会越来越大。我建议定期清理过期记录,或者直接用node-rate-limiter-flexible这样的库。

4.4 熔断基础实现

熔断和限流不一样。限流是保护自己,熔断是保护下游。

举个例子:订单服务依赖库存服务。如果库存服务挂了,订单服务还在不断重试,结果就是订单服务也被拖垮。熔断就是检测到下游故障时,直接快速失败,避免雪崩。

我实现过一个简单的熔断器:

class CircuitBreaker {
  constructor(options = {}) {
    this.failureCount = 0;
    this.threshold = options.threshold || 5;  // 失败阈值
    this.timeout = options.timeout || 30000;   // 半开状态等待时间
    this.state = 'CLOSED';  // CLOSED, OPEN, HALF_OPEN
  }

  async call(fn) {
    if (this.state === 'OPEN') {
      const elapsed = Date.now() - this.lastFailureTime;
      if (elapsed > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      console.log('Circuit breaker opened');
    }
  }
}

使用方式:

const breaker = new CircuitBreaker({ threshold: 3, timeout: 10000 });

app.use('/api/inventory', async (req, res, next) => {
  try {
    await breaker.call(() => proxyMiddleware(req, res));
  } catch (err) {
    res.status(503).json({ error: 'Service temporarily unavailable' });
  }
});

注意:熔断器的状态判断要谨慎。我曾经把阈值设得太低(2次失败就熔断),结果网络抖动一下,服务就断了半天。建议阈值设在5-10次,超时时间30秒以上。

4.5 网关的扩展思考

到这里,一个基础的网关就搭起来了。但生产环境里,你还需要考虑:

  • 健康检查:定期检查后端服务是否存活
  • 负载均衡:多个实例时,如何分配流量
  • 链路追踪:每个请求经过哪些服务,方便排查问题
  • 灰度发布:根据Header或Cookie,把部分流量引到新版本

我个人习惯用express-gateway或者Kong这样的成熟方案。但如果你需要高度定制,自己写一个也不难——核心就是反向代理+中间件。

好,这一章就到这里。下一章咱们聊聊服务间通信,REST vs gRPC,以及消息队列的使用场景。