将 Node.js 扩展到每日 10M 请求以上:真正重要的是什么
全部文章开发

将 Node.js 扩展到每日 10M 请求以上:真正重要的是什么

Prixelo StudioPrixelo Studio
Jan 12, 2026 7 min

扩展 Node.js,枯燥版

大多数讲“如何扩展 Node.js”的文章,作者往往只把一个服务扩展到每天 100万 次请求,就以为这套经验放之四海而皆准。我们运维过好几个日请求量超过 1000万 的系统,得到的经验一点都不光鲜亮丽。

这是我们四年前就希望有人教过我们的实战手册。

别再纠结微服务了

我们见过团队犯的最昂贵的错误,就是在还没到必要阶段就拆分成微服务。一个模块边界清晰的单体应用,能撑住的规模远超大多数工程师的预期,而且运维成本要低一个数量级。

我们的原则是:在以下任何一条成立之前,都保持单体架构。

  • 某个特定路径的扩展特性与其余部分明显不同(例如一个高负载的机器学习推理接口)。
  • 某个特定路径的部署节奏不同(例如受合规约束、必须按月发布的代码,而其余部分按天发布)。
  • 团队规模超过 25 名工程师,合并冲突已经成为实际问题。

如果以上都不成立,拆分服务只会增加运维成本——服务发现、分布式追踪、部署编排、网络故障模式——却换不来任何干净单体仓库(monorepo)给不了你的东西。

真正的瓶颈是数据库

在我们做过的每一个 Node.js 扩展项目中,瓶颈最终都会转移到数据库上。Node.js 服务的 CPU 和内存通常都没问题,因为 Node 本身擅长处理 I/O 并发。真正的问题在于:

  • 无限制的连接池把 Postgres 拖垮
  • 单个热点索引成为写入瓶颈
  • ORM 中的 N+1 查询,代码审查时看起来毫无问题
  • 业务逻辑执行期间,长事务持有锁不放
  • 读流量打在主库上,而不是读副本

实际可行的第一步:

  1. 在 Postgres 前面部署 PgBouncer 或同类工具,使用事务池化模式,连接数上限依据数据库实际 CPU 情况设定,而不是凭 Node.js 的直觉拍脑袋。
  2. 读副本要显式路由。 不要指望 ORM 自己判断。在代码中准备两个客户端:db.readdb.write,强制每个开发者自己选择。
  3. 在生产环境中记录查询耗时。 Pino 加上慢查询日志。当任何查询超过 200ms 时,你需要收到 Slack 告警。
  4. 在 CI 中对新查询运行 EXPLAIN ANALYZE。 我们把它作为涉及 schema 改动的 PR 检查项。

三层缓存

缓存按这个顺序发挥作用:CDN、内存缓存、Redis。跳过其中任何一层,你都会后悔。

CDN。 任何能在边缘缓存的内容都应该缓存,包括未授权读取的 API 响应。不用耍花招——设置好 Cache-Control 响应头,再配合一个遵守它的 CDN,就能免费解决 60% 的读流量。

内存缓存。 在 Redis 前面加一个小型 LRU 缓存(我们用的是 lru-cache),可以在单个进程内拦截大量重复请求的长尾部分。听起来微不足道,但在我们的实测数据中能减少 30–50% 的 Redis 流量。

Redis。 用于跨进程共享的状态和计算结果。使用时要有明确意图——每个 key 都要有负责人、TTL,以及有文档记录的失效策略。没人追踪的 Redis key,正是生产环境缓存在黑色星期五还在提供 6 个月前旧数据的原因。

能异步的都异步

同步请求路径应该只做最基本的事情:校验、持久化、响应。其他一切——发邮件、触发 webhook、图片处理、第三方 API 调用、搜索索引——都应该放进队列。

我们几乎所有场景都用基于 Redis 的 BullMQ。原因有两个:

  1. 故障隔离。 如果调用发生在 worker 中,一个不稳定的第三方 API 就无法拖垮你的请求路径。
  2. 背压控制。 流量突增时,队列会吸收这波冲击,worker 再以可持续的速率消化它。而同步代码在流量突增下只会直接崩掉。

举个实际例子:我们为一个客户重建的结账流程,原来会同步触发 6 个第三方调用(税务、反欺诈、履约、邮件、短信、数据分析)。结账耗时中位数为 1.4 秒,一旦某个服务商变慢,p99 会飙到 8 秒。在把除税务和反欺诈之外的调用全部搬到 BullMQ worker 之后,中位数降到了 220ms,p99 也稳定控制在 600ms 以内。

先做可观测性,再谈精巧架构

在规模化压力下,人的本能是去设计精巧的系统。但正确的做法是,为你现有的系统打点埋点,直到你能清楚看到时间究竟花在哪里。

我们统一采用以下方案:

  • OpenTelemetry 链路追踪,导出到 Honeycomb 或 Tempo。每个请求都有一个 trace ID,每次外部调用都是一个 span。
  • 结构化日志,JSON 格式,带上 trace ID。用 Pino 就够了。
  • 每个接口的 RED 指标——Rate(速率)、Errors(错误)、Duration(耗时)。用 Grafana 做仪表盘,告警要盯 p99,而不是 p50。
  • 从网络外部发起的合成监控,每分钟对关键路径跑一次。

你不可能准确预测出瓶颈在哪,只能靠实测发现它。率先把可观测性做到位的团队,会在接下来六个月的扩展工作中占据优势。

我们反复见过的坑

以下是我们见过的一些拖垮生产环境的常见问题:

  • 日志打得太多,结果拖垮了日志管道,最后反而没有日志可查。
  • 忘记设置 Node 的 --max-old-space-size。默认值是 1.5GB,而你的容器有 8GB。算一算账就知道问题在哪。
  • 在 Promise.all 循环里写数据库却不限制并发数,结果在最糟糕的时刻耗尽连接池。
  • 健康检查在服务实际已经故障时依然显示通过。要检测整条依赖链,而不只是进程本身。
  • 指望第三方 SDK 自己设置超时。永远要用你自己的超时逻辑包一层。

不酷但真实的结论

在每日 10M 请求的规模下,一个系统能撑住和撑不住之间的差别,很少是因为架构上的天才设计。真正起作用的是:团队会盯着仪表盘看,慢查询一出现当周就修,诚实地做事故复盘,并且在该做监控埋点的时候,克制住重构的冲动。

把这种文化建立起来,Node.js 能撑住的规模会比会议演讲里说的更高。这正是我们的后端团队和云与 DevOps团队为成长期产品提供的服务。

分享这篇文章
Prixelo Studio

Prixelo Studio

Notes from the studio on craft, code, and product.