Node 服务遇到血崩,汤过坑才知道,限流与熔断是你绕不过的坑

前两天,一个朋友在群里问:"我的 BFF 服务(Backend For Frontend)老是被打挂,Redis 明明好好的,接口偶尔就开始 504,是不是 teledb 不稳?"

我笑了下,说:

"不,这不是 teledb 的锅。是你自己的服务太'实诚'了。"

什么意思? 太多 Node.js 初级后端都在一个误区:他们的服务只会"硬抗"。来多少请求都照单全收,下游挂了就等,直到超时、线程堆积、内存飙升、服务崩掉。 然后再开始复盘、扩容、加机器。

这篇文章我们就聊两个词------限流熔断。 这俩是服务"自我保护"的基本功。 我尽量用接地气的方式说,代码也能直接跑。


一、限流:先学会说"不"

你想想,一个人一天最多能喝 10 杯咖啡。 第 11 杯来了,不拒绝,你就猝死。

限流,就是系统的"自控力"。

Node.js 里我们最常用的库express-rate-limit。如果你项目里是 Express 框架,那几行代码就能起飞。

但很多人装完库,直接:

js 复制代码
app.use(rateLimit({ windowMs: 60*1000, max: 100 }))

就完事了。

问题来了------你有好几个实例跑在 Docker 集群里。每个容器都自己算次数,那 100 次变成了 100 × N。你以为你限的是 100 QPS,其实打在后端的已经上千了。

解决办法其实很简单:用 Redis。 所有实例共用一个计数器,才叫真正的限流。

看个最小代码例子:

js 复制代码
const express = require('express');
const { createClient } = require('redis');
const RedisStore = require('rate-limit-redis');
const rateLimit = require('express-rate-limit');

const app = express();

const redisClient = createClient({ url: 'redis://redis-server:6379' });
redisClient.connect();

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
  message: { status: 429, message: '请求太频繁,请稍后再试。' }
});

app.use(limiter);
app.get('/api/projects', (req, res) => res.json({ ok: true }));

app.listen(3000);

跑起来,这就是真·限流。

如果有人用爬虫猛刷接口,达到阈值,就会被 429 打回来。 不会影响其他人,更不会把服务打挂。

我第一次加限流的时候,其实挺抵触的,总觉得"挡用户"不太好。后来上线后,我发现夜里系统终于不崩了,Redis 也稳如老狗。那一刻我才明白,限流不是冷漠,而是成熟。


二、熔断:有时候"断掉"才是聪明

限流解决了外部的"洪水",熔断解决的是内部的"内伤"。

你想象下这个场景: BFF 服务每次都要去查一个 teledb 数据库。 平时挺快,几十毫秒就回来。 突然某天 teledb 卡住了,所有请求都卡在等待。

此时 BFF 就成了"堵车现场"------所有请求在等那个慢数据库。 最终你整条服务链路都挂了。

这就是熔断该出手的时候。

熔断的逻辑特别简单:

"连续失败太多次,就先别再请求下游了。先歇会儿。"

Node 里有个库opossum,非常好用。

比如你有个函数:

js 复制代码
async function queryDatabase(id) {
  const result = await db.query('SELECT * FROM projects WHERE id=?', [id]);
  return result;
}

现在我们用熔断器包一下它:

js 复制代码
const CircuitBreaker = require('opossum');

const breaker = new CircuitBreaker(queryDatabase, {
  timeout: 3000,               // 超过3秒算失败
  errorThresholdPercentage: 50, // 失败率超过50%就熔断
  resetTimeout: 30000           // 30秒后再试一次
});

breaker.fallback(() => ({
  message: '服务临时不可用,已返回缓存结果。',
  data: { id: 'cached-001', name: 'Cached Project' }
}));

这样,当 teledb 卡成狗时,breaker 会自动"跳闸"。 此时再有请求进来,它不会再去访问 teledb,而是立刻走 fallback 返回缓存。 30 秒后再尝试放行一个请求,如果成功,再恢复正常。

我第一次用熔断的时候,简直像发现了"防自杀神器"。 以前出问题都是整条链崩,现在服务自己会"拉闸保护",稳得离谱。


三、什么才算"失败"?

熔断器并不懂什么是失败。它只认 Promise 是"resolved"还是"rejected"。 你得自己定义。

常见几种情况:

  1. 超时 :函数执行太久没结果。opossum 会自动处理;
  2. 数据库连接失败:Driver 会抛错;
  3. 业务失败 :请求虽然成功返回,但数据不对,这种你得自己 throw new Error()
  4. 连接池满了:拿连接失败;
  5. HTTP 5xx:axios 会 reject。

所以逻辑上,你只要觉得"这次不行",就 throw。 比如:

js 复制代码
async function queryDatabase(id) {
  const result = await db.query(...);
  if (!result.rows.length) {
    throw new Error(`项目 ${id} 不存在`);
  }
  return result.rows[0];
}

这样熔断器才知道要记一次失败。


四、限流 + 熔断的组合拳

很多初学者以为:加了限流,就不用熔断。 其实这俩是两道不同的防线。

场景 机制 目的
外部用户疯狂请求 限流 保护自己不被打爆
下游服务出故障 熔断 保护自己不被拖死
临时抖动 重试 给对方一个机会
持续故障 降级 保核心功能不崩

真正的弹性系统,是这几层叠加在一起的。


五、一些经验碎片

写到这我忽然想到几个小经验,不系统,但挺有用的:

  • 别怕限流,怕的是不控制。 就像高速入口限流,不是让车不跑,而是让车安全跑。

  • 熔断器别只加在外部调用上。 内部数据库、Redis、消息队列都可以加。

  • 要监控熔断状态。 熔断打开的那一刻,一定要有告警。否则你以为系统稳了,其实早在"假稳"。

  • fallback 不是摆设。 最好准备一份缓存或者默认值,让前端感知最小化。


结尾

Node.js 生态这些年成熟了太多,我们不缺库、不缺方案,缺的是那种"工程上的敬畏"。 服务要学会拒绝,要学会保护自己。

我特别喜欢一句话:

"好的架构不是让系统更强,而是让系统能承认自己的弱。"

限流和熔断,就是 Node.js 服务"成熟"的第一步。 当你写完那几行代码,再看一次日志,你会发现: 以前让你心惊胆战的 504,终于安静下来了。

相关推荐
Moment4 小时前
NestJS 在 2025 年:对于后端开发者仍然值得吗 ❓︎❓︎❓︎
前端·javascript·后端
milanyangbo4 小时前
从C10K到Reactor:事件驱动,如何重塑高并发服务器的网络架构
服务器·网络·后端·架构
Json____4 小时前
最近我用springBoot开发了一个二手交易管理系统,分享一下实现方式~
java·spring boot·后端
Jolyne_4 小时前
一些我推荐的前端代码写法
前端
自由会客室4 小时前
Ubuntu 24.04 上安装 Sonatype Nexus Repository(Maven 私服)
架构·maven
调试人生的显微镜4 小时前
前端一般用什么开发工具?一文看懂从入门到专业的完整工具链
后端
互联网工匠4 小时前
分布式操作的一致性方案
分布式·架构
哥哥还在IT中4 小时前
Redis多线程架构深度解析-从单线程到I/O Threading
redis·架构·bootstrap
赵小川4 小时前
Taro 包升级实录 — 从 3.3 到 3.6.3 完整指南
前端·架构