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,以及消息队列的使用场景。