Skip to main content

性能与高并发

性能指标速查(面试必背)

面试题:QPS / TPS / RPS / 并发数 / P99 这些指标分别代表什么?

指标含义关注场景
QPS (Queries Per Second)每秒查询数读接口、缓存查询
TPS (Transactions Per Second)每秒事务数支付、下单(强一致)
RPS (Requests Per Second)每秒请求数HTTP 接口通用
并发数同时处理的请求数QPS = 并发数 / 平均响应时间
P95 / P9995% / 99% 的请求在该延迟内完成比平均值重要(平均被极值拉偏)

核心结论

  • 不要用平均值汇报性能!P99(最慢的 1% 请求的延迟)才是衡量系统稳定性的关键指标。
  • 优化目标:P99 < 200ms(业界公认「可感知流畅」阈值)。
  • P99 异常飙升 = 系统有长尾问题(GC 暂停、慢 SQL、热点 key)。

限流算法(高频手写)

面试题:常见限流算法有哪些?区别是什么?

算法思想优点缺点
固定窗口单位时间内计数,超限拒绝简单窗口临界点会有 2 倍突发流量
滑动窗口把窗口细分,平滑统计解决临界问题实现略复杂
漏桶请求入桶,固定速率流出平滑流量、强制匀速无法应对正常突发
令牌桶固定速率发令牌,有令牌才放行允许一定突发-

令牌桶实现(最常用):

class TokenBucket {
constructor(capacity, rate) {
this.capacity = capacity; // 桶容量
this.rate = rate; // 每秒生成令牌数
this.tokens = capacity;
this.last = Date.now();
}

tryAcquire(n = 1) {
const now = Date.now();
// 按时间差补充令牌
this.tokens = Math.min(
this.capacity,
this.tokens + ((now - this.last) / 1000) * this.rate,
);
this.last = now;
if (this.tokens >= n) {
this.tokens -= n;
return true;
}
return false;
}
}

分布式限流:用 Redis + Lua 脚本保证原子性(如 INCR + EXPIRE 实现固定窗口,或用 redis-cell)。

滑动窗口实现

class SlidingWindow {
constructor(limit, windowMs) {
this.limit = limit; // 窗口内最大请求数
this.windowMs = windowMs; // 窗口大小(毫秒)
this.logs = []; // 请求时间戳队列
}

tryAcquire() {
const now = Date.now();
// 弹出窗口外的旧时间戳
while (this.logs.length && this.logs[0] <= now - this.windowMs) {
this.logs.shift();
}
if (this.logs.length < this.limit) {
this.logs.push(now);
return true;
}
return false;
}
}

漏桶实现

class LeakyBucket {
constructor(capacity, rate) {
this.capacity = capacity; // 桶容量
this.rate = rate; // 每秒流出速率
this.water = 0; // 当前水量
this.last = Date.now();
}

tryAcquire() {
const now = Date.now();
// 按时间匀速漏水
this.water = Math.max(
0,
this.water - ((now - this.last) / 1000) * this.rate,
);
this.last = now;
if (this.water < this.capacity) {
this.water++;
return true;
}
return false;
}
}

熔断与降级(限流的最佳拍档)

面试题:什么是熔断?什么是降级?它们和限流有什么区别?

概念触发条件行为
限流流量超过阈值直接拒绝多余请求
降级系统压力大 / 下游故障返回兜底数据(如默认值、缓存)
熔断下游错误率超阈值快速失败,避免雪崩

熔断器三态机

        失败率 < 阈值
┌──────────────────────┐
▼ │
[关闭] ─失败率超阈值─→ [开启] ─超时后─→ [半开]
▲ │ │
└──── 试探请求成功 ─────┴─────────────┘

试探请求失败 → 重新 [开启]
  • 关闭 (Closed):正常调用下游,记录失败率
  • 开启 (Open):直接快速失败,不调用下游(保护下游)
  • 半开 (Half-Open):放少量试探请求,成功则恢复,失败则回到开启

常用库opossum(Node.js 生态最成熟的熔断库)。生产环境微服务调用必须配熔断,否则一个慢服务会拖垮整个调用链。

缓存策略

  • 多级缓存:浏览器缓存 → CDN → 网关缓存 → 应用本地缓存(如 LRU)→ Redis → DB。
  • 本地缓存 vs 分布式缓存:本地快但多实例不一致;Redis 一致但有网络开销。热点小数据可本地 + Redis 组合。
  • 缓存穿透/击穿/雪崩见 Redis 篇

本地缓存 LRU 实现(高频手写)

class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map(); // Map 保持插入顺序
}

get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val); // 重新插入,移到末尾(最新)
return val;
}

put(key, val) {
if (this.cache.has(key)) this.cache.delete(key);
this.cache.set(key, val);
if (this.cache.size > this.capacity) {
// 删除最久未使用的(Map 的第一个)
this.cache.delete(this.cache.keys().next().value);
}
}
}

面试要点:JS 中实现 LRU 推荐用 Map 而不是普通对象,因为 Map 保持插入顺序(ES6 规范保证),keys().next().value 直接拿到最老 key,时间复杂度 O(1)。

性能调优方法论(5 步法)

面试题:面对一个慢接口,你会怎么系统性地优化?

按下面顺序排查,不要跳步——性能优化的关键是先度量再动手

步骤核心动作工具 / 手段
1. 度量找到真正的瓶颈在哪压测 wrk / autocannon、火焰图 clinic flame、P99 监控
2. 缓存读多写少 → 加缓存,DB 压力立刻下来LRU(本地)→ Redis(分布式)
3. 异步同步阻塞 → 异步非阻塞,串行 → 并行Promise.all、消息队列、Worker 线程
4. 扩展单机扛不住 → 多机 / 多进程横向:Cluster / PM2 / K8s;纵向:换更好的硬件
5. 兜底无论怎么优化都有极限,必须防雪崩限流(拒绝)/ 熔断(快速失败)/ 降级(兜底数据)

核心原则先压测、再优化。没有数据支撑的优化都是瞎调——可能你优化的不是真正的瓶颈。


Node 适合什么场景?

Node 的事件循环 + 非阻塞 I/O,只擅长 IO 密集型(高并发接口、网关、BFF)。遇到 CPU 密集型任务(巨量计算、加密、图像处理),4 种解法:

  1. worker_threads 线程池处理 CPU 任务(详见 多进程与多线程
  2. child_process / Cluster 多进程分摊负载
  3. 把计算交给专门服务(C++ / Go / Python 微服务)
  4. 大任务分片setImmediate(() => task(), 0) 让出主线程

横向扩展与负载均衡

        用户


┌──────────┐
│ Nginx/LB │ ← 负载均衡(轮询 / 加权 / IP hash / 最少连接)
└────┬─────┘

┌────┼────┬────┬────┐
▼ ▼ ▼ ▼ ▼
Node Node Node Node Node ← 多实例无状态化
│ │ │ │ │
└────┴────┴────┴────┘

Redis ← 共享会话/缓存

关键原则应用必须无状态化——会话、文件上传等状态都存 Redis,才能让任意实例处理任意请求(这是横向扩展的前提)。

Cluster 端口共享的底层原理(句柄传递 + SCM_RIGHTS)见 多进程与多线程


连接池

数据库、Redis、HTTP 客户端都应使用连接池复用连接,避免频繁 TCP 三次握手。

// MySQL 连接池示例(mysql2)
const pool = mysql.createPool({
connectionLimit: 10, // ⚠️ 不是越大越好!
host: "localhost",
database: "test",
});
const [rows] = await pool.query("SELECT * FROM users WHERE id = ?", [1]);

调优经验connectionLimitCPU 核数 × 2 是常见起点。过大会打满 DB 的 max_connections,反而拖垮数据库。


性能分析工具速查

场景工具用法
压测 QPS / 延迟wrk / autocannonwrk -t4 -c100 -d30s http://localhost:3000
定位 CPU 热点clinic flame生成火焰图,宽顶 = 耗时函数
定位内存泄漏clinic doctor + Heap SnapshotV8内存管理.md
持续监控Prometheus + Grafana暴露 /metrics 端点,告警 P99
跨服务追踪OpenTelemetryTraceId 串联全链路
错误聚合Sentry抓异常 + SourceMap 反解

优化 Checklist(快速自检)

面试时可作为"你做过哪些优化"的问题模板,按这 6 类组织答案:

  • 数据层:加索引、慢 SQL 分析、读写分离、分库分表
  • 缓存层:Redis 缓存热点、LRU 本地缓存防穿透、设置合理 TTL
  • 接口层:聚合接口减少 RTT、Promise.all 并行调用、GraphQL 按需取字段
  • 传输层:Gzip / Brotli 压缩、CDN 静态资源、HTTP/2 多路复用
  • 架构层:Cluster 多进程、无状态化、读写分离、消息队列削峰
  • 保护层:限流、熔断、降级、超时控制(防雪崩三件套)