当后端拆分成十几个微服务之后,前端同学的日子并不好过:一个页面要拉用户信息、订单列表、推荐内容,就得发三次请求,还要自己处理跨域和鉴权。API网关的出现就是为了把这类脏活集中收口——统一入口、统一鉴权、统一路由,必要的时候还能把多个接口的结果聚合后再吐给客户端。Node.js基于事件驱动和非阻塞I/O的特性,天然适合做这种I/O密集型的转发层,本文就用原生模块加少量依赖,实现一个包含路由、聚合、熔断降级三个核心能力的网关。

一、路由转发:网关的第一职责
路由的本质是把进来的请求按某种规则映射到对应的后端服务。最直观的做法是用路径前缀匹配,比如/api/user/**打到用户服务,/api/order/**打到订单服务。我们先把路由表抽象成配置,再写一个匹配函数,这样后续要加服务只需要改配置,不用动代码。
下面这段代码用Node.js原生的http模块实现了一个最小可用的转发器,核心在于把客户端请求原样透传给上游服务,再把上游响应写回客户端:
const http = require('http');
const https = require('https');
const { URL } = require('url');
// 路由表:路径前缀 -> 上游服务地址
const routes = [
{ prefix: '/api/user', target: 'http://127.0.0.1:8001' },
{ prefix: '/api/order', target: 'http://127.0.0.1:8002' },
];
function matchRoute(pathname) {
// 按前缀长度倒序,保证最长前缀优先匹配
const sorted = [...routes].sort((a, b) => b.prefix.length - a.prefix.length);
return sorted.find(r => pathname.startsWith(r.prefix));
}
function proxy(req, res, route) {
const target = new URL(req.url, route.target);
const client = target.protocol === 'https:' ? https : http;
const upstreamReq = client.request(target, {
method: req.method,
headers: { ...req.headers, host: target.host },
}, upstreamRes => {
res.writeHead(upstreamRes.statusCode, upstreamRes.headers);
upstreamRes.pipe(res);
});
upstreamReq.on('error', err => {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ code: 502, msg: '上游服务不可用' }));
});
req.pipe(upstreamReq);
}
const server = http.createServer((req, res) => {
const { pathname } = new URL(req.url, 'http://localhost');
const route = matchRoute(pathname);
if (!route) {
res.writeHead(404, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ code: 404, msg: '路由未匹配' }));
}
proxy(req, res, route);
});
server.listen(3000, () => console.log('API网关已监听 3000 端口'));这段实现里有几个细节值得注意。第一,pipe是流式转发,响应体不会整体缓冲在内存里,即使上游返回一个大文件,网关的内存占用也很平稳,这正是Node.js做转发层的优势。第二,请求头里的host必须改成上游地址,否则有些基于虚拟主机路由的后端会返回404。第三,如果要做灰度发布,可以在matchRoute里读取请求头的版本号或用户标识做加权分发,路由表的target改成数组即可。
二、接口聚合:一次请求拿齐所有数据
聚合是网关区别于简单反向代理的地方。典型的BFF场景下,首页需要同时展示用户画像、订单摘要和营销文案,让客户端发三次请求既慢又浪费连接资源。在网关层用Promise.all并行请求三个下游,再组装成一个JSON返回,客户端只需要一次往返。
const { request } = require('./lib/httpClient'); // 封装好的promise请求工具
async function aggregateHandler(req, res) {
const token = req.headers.authorization || '';
// 并行发起三个下游请求
const [profile, orders, banner] = await Promise.all([
request('http://127.0.0.1:8001/api/user/profile', { headers: { authorization: token } }),
request('http://127.0.0.1:8002/api/order/summary', { headers: { authorization: token } }),
request('http://127.0.0.1:8003/api/cms/banner', { timeout: 3000 }),
]);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
code: 0,
data: {
profile: profile.data,
recentOrders: orders.data,
banner: banner.data,
},
}));
}并行请求比串行快得多,但也要考虑部分失败的情况。营销文案挂了不应该导致整个首页打不开,这时可以换成Promise.allSettled,对失败的字段返回兜底值:
const results = await Promise.allSettled([
request('http://127.0.0.1:8001/api/user/profile'),
request('http://127.0.0.1:8003/api/cms/banner', { timeout: 3000 }),
]);
const [profileRes, bannerRes] = results;
const banner = bannerRes.status === 'fulfilled'
? bannerRes.value.data
: { title: '默认文案', img: '' }; // 降级兜底数据聚合还有一个隐藏价值:鉴权收敛。下游服务不需要各自校验token,网关统一解析后把用户ID塞进自定义请求头传下去,下游只管信任这个头。这样 token 的解析逻辑只维护一份,密钥轮换时也不用挨个服务改代码。另外要提醒一点,聚合接口必须给每个下游请求设置独立的超时时间,否则最慢的那个服务会拖垮整个聚合响应,一般建议控制在2到3秒。
三、熔断降级:防止故障雪崩的关键防线
想象一个场景:订单服务因为数据库慢查询开始大面积超时,网关上的请求堆积等待,事件循环被占满,最终连正常的用户服务请求也无法处理,这就是故障雪崩。断路器模式的思想是统计一段时间内的失败率,失败率超过阈值就直接拒绝请求走降级逻辑,不再真正打向下游,给故障服务恢复的时间。
断路器有三个状态:closed(正常放行)、open(熔断,直接走降级)、half-open(放一个探测请求试探下游是否恢复)。下面是一个可直接使用的实现:
class CircuitBreaker {
constructor(fn, { failureThreshold = 5, resetTimeout = 10000, fallback = null } = {}) {
this.fn = fn;
this.failureThreshold = failureThreshold; // 连续失败多少次触发熔断
this.resetTimeout = resetTimeout; // 熔断多久后进入半开状态
this.fallback = fallback; // 降级函数
this.failures = 0;
this.state = 'closed';
this.nextAttempt = 0;
}
async exec(...args) {
if (this.state === 'open') {
if (Date.now() < this.nextAttempt) {
// 熔断窗口内,直接降级,不请求下游
return this.fallback ? this.fallback(...args) : Promise.reject(new Error('熔断中'));
}
this.state = 'half-open'; // 进入半开,放一个探测请求
}
try {
const result = await this.fn(...args);
this.success();
return result;
} catch (err) {
this.failure();
if (this.fallback) return this.fallback(...args);
throw err;
}
}
success() {
this.failures = 0;
this.state = 'closed';
}
failure() {
this.failures++;
if (this.failures >= this.failureThreshold && this.state !== 'open') {
this.state = 'open';
this.nextAttempt = Date.now() + this.resetTimeout;
console.warn('断路器已打开,进入熔断状态');
}
}
}
// 使用示例:包装订单服务请求
const orderBreaker = new CircuitBreaker(
() => request('http://127.0.0.1:8002/api/order/summary', { timeout: 3000 }),
{
failureThreshold: 5,
resetTimeout: 10000,
fallback: () => ({ code: 0, data: [], degraded: true, msg: '订单服务暂时不可用' }),
}
);降级策略要根据业务价值来定。订单数据不能给假数据,那就返回明确的"服务繁忙"提示码,让前端展示引导页;推荐列表、营销文案这类非核心数据则可以返回缓存的上一次结果或静态兜底内容。更完善的方案是把降级数据放进Redis,网关在请求成功时顺手更新缓存,熔断打开后直接读缓存,用户几乎无感知。
还有一个工程上的建议:断路器应该按服务粒度而非全局粒度创建,每个下游服务一个实例,各自的失败计数互不影响。如果后续接入了监控体系,把断路器的状态变更暴露成指标上报(比如打点给Prometheus),运维就能在告警里第一时间看到哪个服务被熔断了,而不是等用户投诉才发现。
四、生产环境还需要补齐什么
上面的三个模块拼起来已经是一个能跑的网关,但离生产可用还有几步路要走。首先是健康检查,网关需要定期探测各上游服务,把不可用的节点从路由表里剔除,这个可以用setInterval加简单的HEAD请求实现。其次是连接管理,Node.js默认的http.globalAgent对同一host的并发连接数有限制,转发场景下务必开启keepAlive并调大maxSockets:
http.globalAgent.keepAlive = true; http.globalAgent.maxSockets = 256; http.globalAgent.keepAliveMsecs = 30000;
再者是进程模型,单进程的Node.js网关无法利用多核,建议用cluster模块或者PM2以CPU核数启动多个实例,前面再挂一层Nginx做TLS卸载和静态资源分流。最后别忘了统一错误格式和请求日志,日志里带上traceId,跨服务排查问题时能省下大量时间。
整体来看,用Node.js写API网关的代码量并不大,核心难点在于对失败场景的穷举和兜底设计。路由、聚合、熔断三者层层递进:路由解决请求去哪,聚合解决请求怎么合并,熔断解决下游坏了怎么办。把这三块的边界情况都想清楚,这个网关就足以支撑中型规模的微服务体系了。如果不想重复造轮子,也可以基于Express或Fastify加中间件的方式重构本文代码,思路完全一致。
Node.js API网关路由转发熔断降级修改时间:2026-09-16 20:26:54