性能与高并发
性能指标速查(面试必背)
面试题:QPS / TPS / RPS / 并发数 / P99 这些指标分别代表什么?
| 指标 | 含义 | 关注场景 |
|---|---|---|
| QPS (Queries Per Second) | 每秒查询数 | 读接口、缓存查询 |
| TPS (Transactions Per Second) | 每秒事务数 | 支付、下单(强一致) |
| RPS (Requests Per Second) | 每秒请求数 | HTTP 接口通用 |
| 并发数 | 同时处理的请求数 | QPS = 并发数 / 平均响应时间 |
| P95 / P99 | 95% / 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 种解法:
worker_threads线程池处理 CPU 任务(详见 多进程与多线程)child_process/ Cluster 多进程分摊负载- 把计算交给专门服务(C++ / Go / Python 微服务)
- 大任务分片,
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]);
调优经验:
connectionLimit≈CPU 核数 × 2是常见起点。过大会打满 DB 的max_connections,反而拖垮数据库。
性能分析工具速查
| 场景 | 工具 | 用法 |
|---|---|---|
| 压测 QPS / 延迟 | wrk / autocannon | wrk -t4 -c100 -d30s http://localhost:3000 |
| 定位 CPU 热点 | clinic flame | 生成火焰图,宽顶 = 耗时函数 |
| 定位内存泄漏 | clinic doctor + Heap Snapshot | 见 V8内存管理.md |
| 持续监控 | Prometheus + Grafana | 暴露 /metrics 端点,告警 P99 |
| 跨服务追踪 | OpenTelemetry | TraceId 串联全链路 |
| 错误聚合 | Sentry | 抓异常 + SourceMap 反解 |
优化 Checklist(快速自检)
面试时可作为"你做过哪些优化"的问题模板,按这 6 类组织答案:
- ☐ 数据层:加索引、慢 SQL 分析、读写分离、分库分表
- ☐ 缓存层:Redis 缓存热点、LRU 本地缓存防穿透、设置合理 TTL
- ☐ 接口层:聚合接口减少 RTT、
Promise.all并行调用、GraphQL 按需取字段 - ☐ 传输层:Gzip / Brotli 压缩、CDN 静态资源、HTTP/2 多路复用
- ☐ 架构层:Cluster 多进程、无状态化、读写分离、消息队列削峰
- ☐ 保护层:限流、熔断、降级、超时控制(防雪崩三件套)