高并发系统设计:我在北京出租屋里踩过的坑和学到的招

后端说没问题
2025-12-20 23:52
阅读 969

上周五晚上十一点,窗外北京五环外的夜色早已沉寂,我盯着屏幕上不断飙升的 CPU 曲线,心里默默问候了产品经理全家。事情是这样的:一个原本只是内部用的小工具 API,因为被某个“聪明”的前端同学不小心暴露在公网,突然之间每秒请求量从个位数飙到了 5000+。而我这个远程办公、孤军奋战的独立开发者,连个能一起背锅的人都没有。

说来有点自嘲——作为一个常年在家 coding 的人,我本以为高并发这种词只存在于大厂 PPT 和面试题里。结果现实狠狠打脸:只要你的服务能被访问,就可能被压垮。那晚,我一边重启崩溃的 Node.js 进程,一边下定决心:得认真搞懂高并发系统设计了。


起因:一个被“误伤”的小接口

事情还得从头说起。我最近接了个外包项目,用 Express + MongoDB 搭了个简单的用户行为日志收集服务。前端每触发一个按钮点击,就往我的 /log 接口发个 POST 请求。一开始日活就几百人,完全没压力。

但上周产品上线了一个新功能,把日志上报逻辑错误地放在了首屏加载里——而且是同步阻塞的那种。更糟的是,他们用了轮询,每 2 秒上报一次。于是,当活动页上线后,瞬间流量爆炸。

“兄弟,你这接口怎么挂了?我们页面白屏了!”
——前端同事凌晨 1 点微信轰炸

我当时就想:这不就是典型的高并发场景吗?虽然规模不大(对比双11那种),但对我这个单机部署、无缓存、无限流的小服务来说,已经算“洪峰”了。


别再只背面试题了,实战才是硬道理

说实话,以前刷 LeetCode、看《高性能 MySQL》的时候,总觉得“高并发”离我很远。毕竟我是个自由开发者,接的多是中小型项目。但这次事故让我意识到:哪怕你写的是 JavaScript,也逃不开系统设计的基本功

很多教程一上来就讲“CAP 理论”、“分布式锁”、“消息队列削峰”,听着高大上,但对像我这样一个人干活的人来说,太重了。我们需要的是可落地、低成本、见效快的方案。

所以,我决定从零开始,用实战的方式重构这个日志服务。目标很明确:扛住 5000 QPS,延迟 < 100ms,资源占用可控


第一步:别让请求直接砸到数据库

最开始的架构简直惨不忍睹:

Client → Express → MongoDB

所有请求直插数据库,连个缓冲都没有。MongoDB 的连接池很快耗尽,CPU 打满,Node.js 主线程阻塞,雪崩效应直接拉胯。

解法:引入内存队列 + 异步写入

我立刻加了一层内存队列(用 bull 这个 Redis-based 的任务队列库),把写操作异步化:

// routes/log.js
const Queue = require('bull');
const logQueue = new Queue('log queue', 'redis://127.0.0.1:6379');

app.post('/log', async (req, res) => {
  // 快速响应,不等 DB
  await logQueue.add({ payload: req.body });
  res.status(200).json({ ok: true });
});

然后后台起一个 worker,批量消费:

// workers/logWorker.js
logQueue.process(async (job) => {
  const { payload } = job.data;
  // 批量插入,减少 DB 压力
  await LogModel.insertMany([payload]);
});

效果立竿见影:接口响应时间从 300ms 降到 10ms 以内,MongoDB 的负载直线下降。

经验教训:高并发下,快速释放请求线程比“做完所有事”更重要。能异步的,绝不同步。


第二步:缓存不是万能的,但没有缓存是万万不能的

虽然日志服务本身不需要读缓存,但我在另一个项目(用户配置中心)里深刻体会到缓存的价值。

有一次,一个高频接口每秒被调几千次,查的是同一个用户配置。数据库天天报警。后来我加了 Redis 缓存,TTL 60 秒,QPS 直接降了 99%。

// 伪代码
async function getUserConfig(userId) {
  const cacheKey = `user:config:${userId}`;
  let config = await redis.get(cacheKey);
  if (!config) {
    config = await db.getUserConfig(userId);
    await redis.setex(cacheKey, 60, JSON.stringify(config));
  }
  return JSON.parse(config);
}

关键点:缓存策略要根据数据更新频率来定。如果是实时性要求高的,可以用 Cache-Aside 模式;如果是允许短暂不一致的,TTL + 异步刷新更稳。


第三步:限流——保护自己,也保护下游

回到日志服务,虽然用了队列,但如果流量持续暴涨,内存还是会爆。于是我又加上了限流。

Node.js 生态里,express-rate-limit 是个简单好用的选择:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 分钟
  max: 100, // 每 IP 最多 100 次
  message: 'Too many requests from this IP',
});

app.use('/log', limiter);

但注意:IP 限流在 NAT 或 CDN 场景下会失效(比如所有用户都走 Cloudflare)。这时候可以结合 Token Bucket 或滑动窗口算法,用 Redis 实现更精准的限流。

我后来改用 rate-limiter-flexible + Redis,支持按用户 ID 限流,效果更好。


数据库设计:别让慢查询拖垮整个系统

高并发下,一个慢查询就能让整个 DB 卡死。我在重构时做了几件事:

  1. 加索引:所有查询字段必须有索引,尤其是时间戳、用户 ID。
  2. 分表分库?先别急:日志数据我按天分 collection(MongoDB 的集合),避免单表过大。
  3. 写优化:用 insertMany 批量写,而不是一条条 insert。
  4. 读写分离?没必要:小项目搞主从太重,不如先优化查询。

下面是我优化前后的性能对比:

优化项 QPS (单机) 平均延迟 CPU 使用率
原始版本 ~80 320ms 95%+
加队列 + 异步写 ~3000 12ms 40%
再加限流 + 缓存 ~5000+ 8ms 35%

关于 JavaScript 的“高并发”能力

很多人说:“JS 是单线程,搞不了高并发”。这话一半对一半错。

  • :Node.js 主线程确实是单线程,CPU 密集型任务会阻塞。
  • :I/O 密集型场景(比如 API 网关、日志收集)正是 Node.js 的强项,靠事件循环 + 非阻塞 I/O,轻松 handle 上万并发连接。

关键在于别在主线程干重活。像加密、图像处理这种,要么扔给 Worker Thread,要么用外部服务(比如 Rust 写的微服务,嘿嘿,最近正研究这个)。

说到 Rust——上周我试着用 Actix 写了个日志接收器,同样 5000 QPS,内存占用只有 Node.js 的 1/3,延迟更低。虽然生态不如 JS 成熟,但性能确实香。不过对于大多数 Web 项目,Node.js + 合理架构完全够用。


面试题背后的真相

现在回想那些“高并发系统设计”面试题,比如:

“如果让你设计一个秒杀系统,你会怎么做?”

以前我只会背答案:缓存预热、库存扣减用 Redis Lua、消息队列削峰、限流熔断……

但真正动手后才发现:理论是骨架,实践才是血肉。比如:

  • Redis Lua 脚本写错了,会导致库存超卖;
  • 消息队列积压了,怎么监控和告警?
  • 限流阈值设多少?怎么动态调整?

这些细节,只有在线上跑过才知道。


我的实战 Checklist

经过这次折腾,我总结了一套“小团队高并发应急包”:

  • 快速响应:接口先返回,异步处理
  • 队列削峰:用 Redis 或 Kafka 缓冲写压力
  • 合理限流:按 IP / 用户 / Token 限制
  • 缓存兜底:热点数据缓存,减少 DB 压力
  • 监控告警:至少要有 CPU、内存、QPS、错误率监控
  • 压测验证:上线前用 artilleryk6 压一下
# 用 artillery 做简单压测
artillery quick --count 1000 -n 50 http://localhost:3000/log

最后:孤独开发者的自救指南

作为在北京租房、每天通勤一小时(其实是去咖啡馆写代码)、偶尔参加技术分享会的独立开发者,我深知一个人扛系统的压力。没有 SRE,没有 DBA,出问题只能自己修。

但这也逼着我成长。每一次线上事故,都是一次免费的“高并发实战课”。

如果你也在做类似的小项目,别怕。高并发不是大厂专利,而是每个认真对待线上服务的开发者的必修课。从 10 QPS 到 10000 QPS,中间没有魔法,只有一个个具体的优化点。

下次再遇到流量洪峰,我希望你能笑着说:“来吧,我队列都 ready 了。”


本文完。写于北京回龙观某出租屋,窗外正在下雨。刚跑完压测,服务稳如老狗。
下周打算用 Rust 重写核心模块,欢迎交流。
—— 一个不想再半夜被微信叫醒的独立开发者

评论 0

最热最新
暂无评论
后端说没问题Lv.1
0
影响力
0
文章
0
粉丝