服务端性能优化
问题
服务端性能优化有哪些常见手段?如何定位性能瓶颈?如何设计高并发架构?
服务端性能优化有哪些手段? 按 ROI 排序,从「最容易出效果」到「最重的架构改造」:
- 数据库:索引优化、解决 N+1(用
JOIN或DataLoader批量查)、连接池调优、读写分离、分库分表。 - 缓存:多级缓存(CDN → Nginx → 内存 → Redis),热点 Key 预加载,详见 缓存策略。
- 异步化:耗时任务(发邮件、生成报表)丢消息队列,接口立即返回;大数据导出走流式响应。
- 架构层:负载均衡 + 水平扩展 + 限流熔断,CDN/Gzip/HTTP/2 加速传输。
怎么定位性能瓶颈? 看 P95/P99 而不是平均值,长尾才是用户真感受:
- 指标先行:
Prometheus+Grafana看 QPS、P99、错误率,发现异常接口。 - 链路追踪:
OpenTelemetry/Jaeger看每次请求各阶段耗时,定位慢的是 DB 还是外部 API。 - APM 工具:
Node.js Inspector、clinic.js、0x火焰图找 CPU/内存热点。 - 慢 SQL 日志:MySQL 开
slow_query_log,PostgreSQL 看pg_stat_statements。
怎么设计高并发架构? 核心思路是无状态 + 横向扩展 + 异步削峰:
- 应用层无状态,会话放 Redis,方便多实例水平扩展。
- 入口层 Nginx/网关做负载均衡 + 限流,洪峰用消息队列削峰。
- 数据层读写分离 + 分库分表,热点用 Redis 抗住,冷数据走数据库。
- 高可用靠多活 + 熔断降级,故障时保住核心链路。
答案
性能优化全景图
服务端性能优化可以从六个维度切入,每个维度下有多种具体手段:
性能指标体系
优化之前,先明确要衡量什么。服务端核心性能指标可以分为三类:
| 类别 | 指标 | 说明 | 健康参考值 |
|---|---|---|---|
| 吞吐量 | QPS | 每秒处理的请求数(读场景) | 视业务而定 |
| TPS | 每秒完成的事务数(写场景) | 一个 TPS 可能包含多个 QPS | |
| 并发数 | 同一时刻正在处理的请求数 | — | |
| 延迟 | P50 | 50% 请求在此时间内完成 | < 50ms |
| P95 | 95% 请求在此时间内完成 | < 200ms | |
| P99 | 99% 请求在此时间内完成 | < 500ms | |
| TTFB | 首字节到达时间 | < 100ms | |
| 可用性 | 成功率 | 非 5xx 请求占比 | > 99.9% |
| 错误率 | 5xx / 超时占比 | < 0.1% |
平均响应时间 50ms 看起来不错,但如果 1% 的请求要 5 秒才返回,用户体验极差。长尾延迟(P99)才是用户真正感受到的性能。面试中经常考察候选人是否理解这一点。
一、数据库优化
数据库通常是服务端性能的最大瓶颈。一条慢 SQL 就能拖垮整个服务。
1. N+1 查询问题
N+1 是最常见的数据库性能杀手:查询 N 条主记录后,又对每条记录发起一次关联查询,最终执行 N+1 条 SQL。
// ❌ N+1 问题:查询 100 个用户,每个用户再查一次订单 = 101 条 SQL
const users = await db.user.findMany(); // 1 条 SQL
for (const user of users) {
user.orders = await db.order.findMany({ where: { userId: user.id } }); // 100 条 SQL
}
三种解决方案:
// ✅ 方案 1:手动批量查询(2 条 SQL)
const users = await db.user.findMany();
const userIds = users.map(u => u.id);
const orders = await db.order.findMany({ where: { userId: { in: userIds } } });
// 用 Map 做内存关联,避免嵌套循环
const orderMap = new Map<number, Order[]>();
orders.forEach(order => {
const list = orderMap.get(order.userId) || [];
list.push(order);
orderMap.set(order.userId, list);
});
users.forEach(user => { user.orders = orderMap.get(user.id) || []; });
// ✅ 方案 2:ORM 的 include / eager loading(ORM 帮你生成 JOIN 或 IN 查询)
const users = await prisma.user.findMany({
include: { orders: true },
});
// ✅ 方案 3:DataLoader 模式(GraphQL 场景常用)
// DataLoader 会自动把同一事件循环内的多次 load 合并成一次批量查询
const orderLoader = new DataLoader(async (userIds: number[]) => {
const orders = await db.order.findMany({
where: { userId: { in: userIds } },
});
const orderMap = groupBy(orders, 'userId');
return userIds.map(id => orderMap[id] || []); // 按请求顺序返回
});
// 即使在不同的 resolver 中多次调用,也只会发一条 SQL
const user1Orders = await orderLoader.load(1);
const user2Orders = await orderLoader.load(2);
2. 索引优化
-- 查看慢查询
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'pending' ORDER BY created_at DESC;
-- 如果 type 是 ALL(全表扫描),说明缺少索引
-- 创建复合索引(遵循最左前缀原则)
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);
-- 覆盖索引:查询的列都在索引中,不需要回表
CREATE INDEX idx_orders_covering
ON orders(user_id, status, created_at, amount);
3. 连接池调优
数据库连接的创建和销毁开销很大(TCP 三次握手 + 认证 + SSL 协商),连接池复用已有连接,避免重复创建。
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'mydb',
connectionLimit: 10, // 最大连接数
waitForConnections: true, // 连接池满时排队等待(而非直接报错)
queueLimit: 50, // 排队上限,超出直接拒绝(0 表示无限)
idleTimeout: 60_000, // 空闲连接 60s 后释放
});
// 使用连接池
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [userId]);
PostgreSQL 官方建议的经验公式:
一台 4 核机器: 个连接。
连接池不是越大越好。连接数过多会导致:
- 数据库端上下文切换频繁,CPU 飙升
- 每个连接占用内存(MySQL 默认约 8MB/连接)
- 锁竞争加剧
详见 数据库连接与连接池。
4. 读写分离
高并发场景下,单个数据库实例无法同时承受大量读写。读写分离把读请求分发到只读副本,写请求发到主库:
// 简化的读写分离配置
const writeDb = createConnection({ host: 'master.db.internal' });
const readDb = createConnection({ host: 'replica.db.internal' });
class UserService {
// 写操作 → 主库
async createUser(data: CreateUserDto) {
return writeDb.query('INSERT INTO users SET ?', data);
}
// 读操作 → 从库
async getUser(id: number) {
return readDb.query('SELECT * FROM users WHERE id = ?', [id]);
}
// ⚠️ 写后立即读 → 走主库(从库可能有复制延迟)
async createAndReturn(data: CreateUserDto) {
const result = await writeDb.query('INSERT INTO users SET ?', data);
return writeDb.query('SELECT * FROM users WHERE id = ?', [result.insertId]);
}
}
MySQL 主从复制通常有 几十毫秒到几秒 的延迟。如果写入后立即从从库读,可能读到旧数据。常见解决方案:
- 写后读走主库:写入后短期内的读请求路由到主库
- 强制读主:对实时性要求高的查询显式指定读主库
- 等待复制完成:通过
MASTER_POS_WAIT()或半同步复制
二、缓存优化
缓存是性能优化中投入产出比最高的手段。详细的缓存策略见 服务端缓存策略,这里只讲核心思路。
缓存策略选择
| 策略 | 读 | 写 | 适用场景 |
|---|---|---|---|
| Cache Aside | 先读缓存,miss 后读 DB 并回填缓存 | 先更新 DB,再删除缓存 | 通用方案,最常用 |
| Read Through | 缓存层自动从 DB 加载 | 同 Cache Aside | 需要缓存中间件支持 |
| Write Through | 同上 | 先写缓存,缓存同步写 DB | 写少读多、一致性要求高 |
| Write Behind | 同上 | 先写缓存,异步批量写 DB | 写多场景,允许短暂不一致 |
缓存预热
系统启动或大促前,提前把热点数据加载到缓存,避免冷启动时大量请求穿透到数据库:
class CacheWarmupService {
// 应用启动时执行缓存预热
async onApplicationBootstrap() {
console.log('开始缓存预热...');
const startTime = Date.now();
await Promise.all([
this.warmupHotProducts(),
this.warmupConfigs(),
this.warmupUserSessions(),
]);
console.log(`缓存预热完成,耗时 ${Date.now() - startTime}ms`);
}
private async warmupHotProducts() {
// 预热最近 7 天的热销商品(按销量 Top 1000)
const hotProducts = await this.db.query(`
SELECT * FROM products
WHERE sold_count > 100
ORDER BY sold_count DESC
LIMIT 1000
`);
// 使用 pipeline 批量写入 Redis,减少网络往返
const pipeline = this.redis.pipeline();
for (const product of hotProducts) {
pipeline.set(
`product:${product.id}`,
JSON.stringify(product),
'EX', 3600, // 1 小时过期
);
}
await pipeline.exec();
console.log(`预热了 ${hotProducts.length} 个热门商品`);
}
}
三、代码层优化
1. 异步处理与消息队列
核心思想:把非核心逻辑从请求链路中剥离,通过消息队列异步执行,让接口快速返回。
// ❌ 同步处理:接口响应 1s+
app.post('/api/orders', async (req, res) => {
const order = await createOrder(req.body); // 100ms(核心逻辑)
await sendEmail(order); // 500ms(非核心)
await sendSMS(order); // 300ms(非核心)
await updateInventory(order); // 200ms(非核心)
await updateRecommendation(order); // 400ms(非核心)
res.json(order); // 总共 1500ms
});
// ✅ 异步处理:接口仅 100ms
app.post('/api/orders', async (req, res) => {
const order = await createOrder(req.body); // 100ms(核心逻辑)
// 非核心逻辑丢入消息队列,异步执行
await queue.add('order:post-process', {
orderId: order.id,
tasks: ['send-email', 'send-sms', 'update-inventory', 'update-recommendation'],
});
res.json(order); // 快速返回
});
// 消费者进程:独立消费消息队列中的任务
queue.process('order:post-process', async (job) => {
const { orderId, tasks } = job.data;
const order = await getOrder(orderId);
// 并行执行互不依赖的任务
const results = await Promise.allSettled([
tasks.includes('send-email') && sendEmail(order),
tasks.includes('send-sms') && sendSMS(order),
tasks.includes('update-inventory') && updateInventory(order),
tasks.includes('update-recommendation') && updateRecommendation(order),
]);
// 记录失败的任务,方便重试
results.forEach((result, index) => {
if (result.status === 'rejected') {
logger.error(`Task ${tasks[index]} failed`, result.reason);
}
});
});
2. 流式处理
处理大数据量时,一次性把数据全部加载到内存会导致 OOM。流式处理可以边读边处理边释放,内存占用稳定在很低的水平:
import { pipeline } from 'node:stream/promises';
import { Transform } from 'node:stream';
// ❌ 全量加载:10 万条记录全部放内存,可能 OOM
async function exportAllOrders() {
const orders = await db.query('SELECT * FROM orders'); // 全部加载到内存
return orders.map(order => formatCSV(order)).join('\n');
}
// ✅ 流式处理:内存只保留当前正在处理的那条记录
async function exportAllOrdersStream(res: Response) {
const cursor = db.query('SELECT * FROM orders').stream(); // 游标逐条读取
const transformer = new Transform({
objectMode: true,
transform(order, _encoding, callback) {
callback(null, formatCSV(order) + '\n');
},
});
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=orders.csv');
// pipeline 自动处理背压和错误
await pipeline(cursor, transformer, res);
}
3. 并发控制与批量操作
// ❌ 串行请求:逐个调用,总耗时 = 所有请求的耗时之和
async function getMultipleUsers(ids: number[]) {
const users = [];
for (const id of ids) {
users.push(await fetchUser(id)); // 串行执行
}
return users;
}
// ⚠️ 无限并发:100 个请求同时发出,可能打崩下游服务
async function getMultipleUsers(ids: number[]) {
return Promise.all(ids.map(id => fetchUser(id))); // 并发不受控
}
// ✅ 限流并发:最多 10 个请求同时执行
import pLimit from 'p-limit';
async function getMultipleUsers(ids: number[]) {
const limit = pLimit(10); // 最大并发数 10
return Promise.all(ids.map(id => limit(() => fetchUser(id))));
}
// ✅ 批量操作:一次查询替代多次
async function getMultipleUsers(ids: number[]) {
return db.query('SELECT * FROM users WHERE id IN (?)', [ids]); // 1 条 SQL
}
4. 避免阻塞事件循环
Node.js 是单线程的,任何阻塞操作都会让所有请求排队等待。
// ❌ 阻塞事件循环:CPU 密集型计算直接在主线程执行
app.get('/api/report', (req, res) => {
const result = heavyComputation(data); // 耗时 3 秒,期间所有请求都被阻塞
res.json(result);
});
// ✅ 方案 1:Worker Threads(适合纯 CPU 计算)
import { Worker } from 'node:worker_threads';
app.get('/api/report', async (req, res) => {
const result = await runInWorker('./heavy-computation.js', data);
res.json(result);
});
function runInWorker(script: string, data: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const worker = new Worker(script, { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
});
}
// ✅ 方案 2:时间切片(把大任务切成小块,每块执行完让出事件循环)
async function processLargeArray(items: unknown[]) {
const CHUNK_SIZE = 1000;
const results = [];
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
results.push(...chunk.map(processItem));
// 每处理 1000 条,让出事件循环,让其他请求有机会被处理
if (i + CHUNK_SIZE < items.length) {
await new Promise(resolve => setImmediate(resolve));
}
}
return results;
}
5. 内存优化
// ❌ 内存泄漏:全局缓存无上限增长
const cache: Record<string, unknown> = {};
function cacheResult(key: string, value: unknown) {
cache[key] = value; // 永远不清理,最终 OOM
}
// ✅ 使用 LRU 缓存,限制大小
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, unknown>({
max: 5000, // 最多 5000 个 key
maxSize: 50 * 1024 * 1024, // 最大约 50MB
sizeCalculation: (value) => Buffer.byteLength(JSON.stringify(value), 'utf8'),
ttl: 5 * 60 * 1000, // 5 分钟过期
});
// ❌ 大 JSON 序列化(阻塞 + 内存翻倍)
// JSON.stringify 会创建一个和原对象差不多大的字符串
const hugeData = await db.query('SELECT * FROM logs LIMIT 1000000');
res.json(hugeData); // 100 万条记录先全部序列化为字符串
// ✅ 流式 JSON 响应
import { Readable } from 'node:stream';
function streamJsonArray(items: AsyncIterable<unknown>, res: Response) {
res.setHeader('Content-Type', 'application/json');
res.write('[');
let first = true;
const readable = Readable.from(
(async function* () {
for await (const item of items) {
yield (first ? '' : ',') + JSON.stringify(item);
first = false;
}
yield ']';
})(),
);
readable.pipe(res);
}
四、I/O 优化
网络 I/O 和磁盘 I/O 是服务端另一个常见瓶颈。
1. HTTP 连接复用(Keep-Alive)
调用内部服务时,如果每次请求都新建 TCP 连接,连接建立的开销(DNS + TCP 三次握手 + TLS 协商)可能比请求本身还慢:
import { Agent } from 'node:http';
// ✅ 创建全局 Agent,复用 TCP 连接
const keepAliveAgent = new Agent({
keepAlive: true, // 启用连接复用
maxSockets: 50, // 单个 host 最大并发连接数
maxFreeSockets: 10, // 空闲连接保留数
timeout: 30_000, // 30s 超时
keepAliveMsecs: 60_000, // 空闲连接保活 60s
});
// 所有对内部服务的请求都使用这个 Agent
const response = await fetch('http://user-service.internal/api/users/1', {
agent: keepAliveAgent,
});
2. 批量操作减少网络往返
// ❌ 逐条写入 Redis:100 个 key = 100 次网络往返
for (const item of items) {
await redis.set(`item:${item.id}`, JSON.stringify(item), 'EX', 3600);
}
// ✅ Pipeline 批量写入:100 个 key = 1 次网络往返
const pipeline = redis.pipeline();
for (const item of items) {
pipeline.set(`item:${item.id}`, JSON.stringify(item), 'EX', 3600);
}
await pipeline.exec(); // 一次性发送所有命令
// ❌ 逐条插入数据库
for (const user of users) {
await db.query('INSERT INTO users (name, email) VALUES (?, ?)', [user.name, user.email]);
}
// ✅ 批量插入
await db.query(
'INSERT INTO users (name, email) VALUES ?',
[users.map(u => [u.name, u.email])], // 一条 SQL 插入所有记录
);
3. 响应压缩
import compression from 'compression';
app.use(compression({
level: 6, // 压缩级别 1-9,6 是速度和压缩率的最佳平衡
threshold: 1024, // 只压缩大于 1KB 的响应
filter: (req, res) => {
// 不压缩 SSE(Server-Sent Events)流
if (req.headers['accept'] === 'text/event-stream') return false;
return compression.filter(req, res);
},
}));
| 算法 | 压缩率 | 速度 | 浏览器支持 | 适用场景 |
|---|---|---|---|---|
| Gzip | 中等 | 快 | 所有浏览器 | 通用,兜底方案 |
| Brotli | 高(比 Gzip 好 15-25%) | 稍慢 | 现代浏览器(HTTPS) | 静态资源预压缩 |
| Zstd | 高 | 非常快 | Chrome 123+ | 新兴标准 |
五、架构优化
1. 负载均衡
upstream backend {
# 加权轮询:性能好的机器分配更多流量
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000 weight=2;
server 10.0.0.3:3000 weight=1;
# 健康检查:自动剔除不健康的节点
# 30 秒内 3 次失败则标记为不可用,60 秒后重新检测
server 10.0.0.4:3000 max_fails=3 fail_timeout=30s;
# 连接保活
keepalive 32;
}
server {
listen 80;
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection ""; # 启用长连接
# 超时设置
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 10s;
}
}
常见的负载均衡算法:
| 算法 | 原理 | 适用场景 |
|---|---|---|
| 轮询 | 依次分配 | 服务器性能相近 |
| 加权轮询 | 按权重分配 | 服务器性能差异大 |
| IP Hash | 相同 IP 固定到同一后端 | 需要会话保持 |
| 最少连接 | 优先分配给连接数最少的 | 请求处理时间差异大 |
| 一致性哈希 | 哈希环映射 | 缓存场景,减少节点变更时的缓存失效 |
2. 超时与重试设计
合理的超时设置是保障服务稳定的基础。级联超时原则:上游的超时必须大于下游的超时,否则下游还在处理但上游已经超时返回了,导致资源浪费。
客户端 API 网关 服务 A 服务 B 数据库
│ timeout: 30s │ timeout: 15s │ timeout: 5s │ timeout: 3s │
│─────────────────>│─────────────────>│─────────────────>│─────────────────>│
│ │ │ │ │
│ 30s > 15s > 5s > 3s ✅ 正确的级联超时 │
import axios from 'axios';
const httpClient = axios.create({
timeout: 5000, // 默认 5 秒超时
});
// 指数退避重试
async function fetchWithRetry<T>(
fn: () => Promise<T>,
options: { maxRetries?: number; baseDelay?: number } = {},
): Promise<T> {
const { maxRetries = 3, baseDelay = 1000 } = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
// 只重试可重试的错误(网络超时、5xx),不重试 4xx
if (axios.isAxiosError(error)) {
const status = error.response?.status;
if (status && status >= 400 && status < 500) throw error; // 4xx 不重试
}
// 指数退避 + 随机抖动,避免多个客户端同时重试(惊群效应)
const delay = baseDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5);
console.warn(`第 ${attempt + 1} 次重试,等待 ${Math.round(delay)}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('unreachable');
}
只有幂等的操作才能安全重试。GET、DELETE 天然幂等,POST 需要额外设计(如请求 ID 去重)。否则超时重试可能导致重复创建订单。详见 限流与熔断。
3. 限流、熔断与降级
这三者共同构成系统的容错防护体系:
详见 限流与熔断。
六、性能定位与排查
面试中经常被问到「一个接口响应很慢,你怎么排查?」,这里给出系统化的排查思路。
排查流程图
关键工具
# 1. 系统资源检查
top -c # CPU、内存使用情况
iostat -x 1 # 磁盘 I/O 状况(%util > 70% 说明 I/O 瓶颈)
ss -s # TCP 连接状态统计
vmstat 1 # 系统整体性能快照
# 2. 数据库检查
# MySQL 当前正在执行的查询
SHOW PROCESSLIST;
# 慢查询日志
SHOW VARIABLES LIKE 'slow_query%';
# 3. Node.js 进程诊断
node --inspect app.js # 开启 Chrome DevTools 调试
node --prof app.js # 生成 CPU Profile
链路追踪
在微服务架构中,一个请求可能经过多个服务。通过链路追踪(Tracing)可以看到每个服务的耗时,快速定位瓶颈。
// 简化的请求链路追踪中间件
function tracingMiddleware(req: Request, res: Response, next: NextFunction) {
// 生成或传递 traceId
const traceId = req.headers['x-trace-id'] as string || crypto.randomUUID();
const startTime = process.hrtime.bigint();
// 注入到请求上下文,后续所有日志都带上 traceId
req.traceId = traceId;
res.setHeader('x-trace-id', traceId);
// 响应结束时记录耗时
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - startTime) / 1e6; // 转为 ms
logger.info({
traceId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration.toFixed(2)}ms`,
});
});
next();
}
生产环境建议使用成熟的可观测性方案:
- 链路追踪:OpenTelemetry + Jaeger / Zipkin
- 日志聚合:ELK(Elasticsearch + Logstash + Kibana)
- 指标监控:Prometheus + Grafana
详见 日志与监控体系。
七、压力测试
性能优化的效果必须通过压测来验证,不能凭感觉。
常用压测工具
# autocannon(Node.js 生态,推荐)
npx autocannon -c 100 -d 30 -p 10 http://localhost:3000/api/users
# -c 100: 100 个并发连接
# -d 30: 持续 30 秒
# -p 10: 每个连接每秒发 10 个请求(pipeline)
# wrk(高性能,C 语言编写)
wrk -t12 -c400 -d30s http://localhost:3000/api/users
# -t12: 12 个线程
# -c400: 400 个并发连接
# -d30s: 持续 30 秒
# ab(Apache Bench,简单快速)
ab -n 10000 -c 100 http://localhost:3000/api/users
# -n 10000: 总共 10000 个请求
# -c 100: 100 个并发
压测结果解读
autocannon 输出示例:
┌─────────┬──────┬──────┬───────┬──────┬─────────┬──────────┬───────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Average │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼──────┼─────────┼──────────┼───────────┤
│ Latency │ 2 ms │ 5 ms │ 23 ms │ 48ms │ 7.32 ms │ 10.15 ms │ 312.45 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴──────────┴───────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Average │ Req/Sec │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┤
│ Req/Sec │ 8,200 │ 9,100 │ 12,500 │ 14,200 │ 12,150 │ — │
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴─────────┘
| 关注点 | 说明 |
|---|---|
| P99 延迟 | 99% 的请求延迟。上面的 48ms 说明绝大多数请求体验良好 |
| Max 延迟 | 最差情况。312ms 可以接受,如果 > 1s 需要排查 |
| Stdev(标准差) | 延迟波动大不大。Stdev 过大说明性能不稳定 |
| Req/Sec | 每秒处理的请求数。结合 CPU 使用率看是否还有余量 |
压测注意事项
- 不要压测生产环境:使用独立的压测环境,数据库用测试数据
- 逐步加压:从低并发开始,逐步增加,观察拐点
- 关注系统指标:CPU、内存、连接数、GC 频率要同步监控
- 预热服务:JIT 编译、连接池初始化需要时间,先发一轮预热请求
- 压测时间足够长:至少 30 秒以上,短时间压测可能掩盖内存泄漏等问题
性能优化清单
按投入产出比排序,优先做上面的:
| 优先级 | 优化手段 | 预期收益 | 实施难度 |
|---|---|---|---|
| 🔴 P0 | 加缓存(Redis) | 响应时间降 10-100 倍 | 低 |
| 🔴 P0 | 修复 N+1 查询 | SQL 数量降 N 倍 | 低 |
| 🔴 P0 | 添加数据库索引 | 查询耗时降 10-1000 倍 | 低 |
| 🟡 P1 | 异步处理非核心逻辑 | 接口 RT 降 50-80% | 中 |
| 🟡 P1 | 连接池调优 | 吞吐量提升 2-5 倍 | 低 |
| 🟡 P1 | 响应压缩 | 传输体积降 60-80% | 低 |
| 🟡 P1 | 批量操作替代循环 | I/O 次数降 N 倍 | 中 |
| 🟢 P2 | 读写分离 | 读吞吐提升 2-5 倍 | 中 |
| 🟢 P2 | 负载均衡 + 水平扩展 | 线性提升吞吐量 | 中 |
| 🟢 P2 | 流式处理 | 内存占用降 10-100 倍 | 中 |
| 🔵 P3 | 微服务拆分 | 独立扩展、故障隔离 | 高 |
| 🔵 P3 | 分库分表 | 突破单机存储和性能瓶颈 | 高 |
常见面试问题
Q1: 一个接口响应很慢,怎么排查?
答案:
系统化排查分五步:
第一步:确认范围
- 是所有接口都慢,还是特定接口慢?
- 是所有用户都慢,还是特定用户/地区?
- 是一直慢,还是突然变慢?
第二步:检查基础设施
top/htop看 CPU、内存使用率iostat看磁盘 I/O 是否瓶颈ss -s/netstat看 TCP 连接状态(是否连接池打满)- 检查数据库连接数是否达到上限
第三步:定位慢操作
- 查看请求链路追踪(traceId),找出耗时最长的环节
- 检查数据库慢查询日志(
SHOW PROCESSLIST、慢查询日志) - 检查外部服务调用耗时(是否超时、重试)
第四步:深入分析
- 数据库慢 →
EXPLAIN分析执行计划,检查索引 - CPU 高 → Node.js
--inspect+ Chrome DevTools CPU Profile - 内存高 → Heap Snapshot 分析内存泄漏
- 网络慢 → 检查 DNS 解析、连接复用、响应体大小
第五步:验证修复
- 修改后通过压测验证效果
- 监控 P95/P99 延迟确认改善
Q2: 如何应对高并发?
答案:
高并发优化是一个分层防御的过程,从入口到存储层层减压:
用户请求 → CDN(静态资源)→ 负载均衡 → 限流(保护后端)→ 缓存(拦截 80%+ 读请求)
→ 异步处理(非核心逻辑)→ 数据库(读写分离 / 分库分表)
具体手段:
| 层级 | 手段 | 说明 |
|---|---|---|
| 接入层 | CDN + 负载均衡 | 分散流量,就近访问 |
| 防护层 | 限流 + 熔断 + 降级 | 保护核心链路,防止雪崩 |
| 缓存层 | 多级缓存 | 拦截绝大部分读请求 |
| 应用层 | 异步 + 消息队列 | 削峰填谷,快速返回 |
| 数据层 | 读写分离 / 分库分表 | 提升存储层吞吐量 |
| 扩展 | 水平扩展 | 增加服务实例 |
Q3: QPS、TPS、RT、并发数之间的关系?
答案:
核心公式(Little's Law):
举例:
- QPS = 1000,RT = 50ms → 并发数 =
- 如果线程池大小是 50,刚好能撑住;RT 涨到 100ms,并发数翻倍到 100,线程池就不够了
| 指标 | 含义 |
|---|---|
| QPS | 每秒查询/请求数 |
| TPS | 每秒事务数(一个事务可能包含多次查询) |
| RT | 响应时间(Response Time) |
| 并发数 | 同一时刻正在处理的请求数 |
Q4: 如何设置合理的超时时间?
答案:
级联超时原则:上游超时 > 下游超时。
| 调用类型 | 建议超时 | 说明 |
|---|---|---|
| 数据库查询 | 3-5 秒 | 超过说明缺索引或锁等待 |
| 内部服务调用 | 5-10 秒 | 包含下游处理 + 网络延迟 |
| 外部 API 调用 | 10-30 秒 | 不可控,必须设上限 |
| 客户端等待 | 30-60 秒 | 用户可接受的最长等待 |
- 必须设超时:没有超时 = 无限等待 = 线程/连接池耗尽 = 系统雪崩
- 级联递减:网关 30s > 服务 A 15s > 服务 B 5s > DB 3s
- 超时后快速失败:返回降级数据或错误,释放资源
Q5: 数据库优化有哪些手段?
答案:
| 手段 | 说明 | 适用场景 |
|---|---|---|
| 索引优化 | 为高频查询添加合适的索引 | 第一步,成本最低 |
| 查询优化 | EXPLAIN 分析、避免 SELECT * | 配合索引 |
| 连接池 | 复用连接,避免频繁创建销毁 | 所有场景 |
| 读写分离 | 读走从库,写走主库 | 读多写少 |
| 缓存前置 | 热点数据放 Redis | 减少 DB 压力 |
| 批量操作 | IN 查询替代循环查询 | 消除 N+1 |
| 分库分表 | 突破单机瓶颈 | 数据量 > 千万级 |
| 异步写入 | 先写缓存/消息队列,异步持久化 | 写多场景 |
Q6: 缓存穿透、缓存击穿、缓存雪崩分别是什么?怎么解决?
答案:
| 问题 | 场景 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询一定不存在的数据,每次都打到 DB | 布隆过滤器 / 缓存空值 |
| 缓存击穿 | 某个热点 key 过期的瞬间,大量请求同时穿透 | 互斥锁 / 永不过期 + 异步刷新 |
| 缓存雪崩 | 大量 key 同时过期,DB 瞬间被打爆 | 过期时间加随机值 / 多级缓存 |
Q7: 什么是 N+1 查询?怎么发现和解决?
答案:
N+1 问题:查询 N 条主记录后,对每条主记录的关联数据又发起 1 次查询,总共执行 N+1 条 SQL。
如何发现:
- 开启 ORM 的 SQL 日志,观察是否有大量重复模式的查询
- 使用 APM 工具(如 New Relic、DataDog)监控 SQL 调用次数
- Prisma 的
prisma.$on('query')事件可以记录每条 SQL
解决方案:
- Eager Loading:ORM 的
include/populate/relations - 手动批量查询:先收集所有 ID,用
IN一次查完 - DataLoader:合并同一事件循环内的多次查询(GraphQL 场景)
- JOIN 查询:在 SQL 层面直接关联
Q8: Node.js 服务的 CPU 占用率很高,怎么排查?
答案:
- 生成 CPU Profile:
# 方式 1:--inspect 连接 Chrome DevTools
node --inspect app.js
# 打开 chrome://inspect → 点击 "inspect" → Performance 面板录制
# 方式 2:使用 clinic.js
npx clinic doctor -- node app.js
npx clinic flame -- node app.js # 生成火焰图
-
分析火焰图:横轴是时间占比,找出最宽的方块(耗时最多的函数)
-
常见 CPU 瓶颈:
- JSON 序列化/反序列化大对象 → 流式 JSON / 减少响应体大小
- 正则表达式回溯 → 优化正则或使用非回溯引擎
- 加密/哈希计算 → 移到 Worker Thread
- 同步文件操作 → 换成异步 API
- 频繁 GC → 减少对象创建、优化内存使用
Q9: 如何避免服务雪崩?
答案:
雪崩的典型链路:一个下游服务变慢 → 上游超时堆积 → 线程池打满 → 上游也变慢 → 级联传导 → 全局不可用。
防雪崩三板斧:
| 手段 | 作用 | 实现 |
|---|---|---|
| 限流 | 控制入口流量,不让太多请求进来 | 令牌桶 / 滑动窗口 |
| 熔断 | 下游故障时快速失败,不再调用 | 错误率触发、半开探测 |
| 降级 | 返回兜底数据,保障核心链路 | 缓存兜底 / 默认值 |
补充手段:
- 超时控制:所有外部调用必须设超时
- 隔离:线程池隔离、进程隔离、服务隔离
- 弹性扩缩:流量激增时自动扩容
详见 限流与熔断。
Q10: 服务端性能优化的优先级怎么排?
答案:
遵循二八原则:20% 的优化手段解决 80% 的性能问题。
优化顺序(投入产出比从高到低):
- 加缓存(成本低,收益极高)
- 修 N+1 和慢 SQL(成本低,收益高)
- 加索引(一行 SQL,收益巨大)
- 异步化非核心逻辑(架构调整,收益明显)
- 连接池和批量操作(小改动,减少 I/O)
- 压缩和 CDN(基础设施层面)
- 读写分离(需要主从架构)
- 水平扩展(需要无状态设计)
- 分库分表(复杂度高,最后考虑)
Donald Knuth 的名言。不要在没有性能问题的时候做优化。正确的流程是:
- 发现问题:通过监控发现接口慢 / 资源不足
- 定位瓶颈:用工具定位具体是哪里慢
- 针对性优化:只优化瓶颈点
- 验证效果:压测确认改善
Q11: 如何做数据库连接池监控?
答案:
连接池打满是常见的线上事故根因。必须监控以下指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 活跃连接数 | 当前正在使用的连接 | > 80% 容量 |
| 空闲连接数 | 连接池中空闲的连接 | < 2 |
| 等待队列长度 | 排队等待获取连接的请求数 | > 0 持续 |
| 获取连接耗时 | 从请求连接到获取连接的时间 | > 100ms |
| 连接泄漏 | 借出后长时间未归还的连接 | 存在即告警 |
// 定期采集连接池指标
setInterval(() => {
const stats = pool.pool; // mysql2 连接池对象
metrics.gauge('db.pool.active', stats._allConnections.length);
metrics.gauge('db.pool.idle', stats._freeConnections.length);
metrics.gauge('db.pool.waiting', stats._connectionQueue.length);
}, 5000);
Q12: 什么是背压(Backpressure)?Node.js 如何处理?
答案:
背压是指生产者产生数据的速度 > 消费者处理数据的速度,导致数据在中间堆积。
生产者(数据库读取 100MB/s)─→ 中间缓冲区(不断膨胀)─→ 消费者(HTTP 响应 10MB/s)
如果不处理背压,中间缓冲区会无限增长,最终 OOM。
Node.js Stream 内置了背压机制:
// pipeline 自动处理背压
import { pipeline } from 'node:stream/promises';
await pipeline(
readableStream, // 生产者
transformStream, // 处理器
writableStream, // 消费者
);
// 当消费者处理不过来时,pipeline 会自动暂停生产者的读取
原理:当 writable.write() 返回 false 时,表示内部缓冲区已满,上游应暂停写入;等 drain 事件触发后再恢复。pipeline 帮你自动处理了这个过程。
相关链接
- 服务端缓存策略 - 多级缓存、缓存穿透/击穿/雪崩
- 限流与熔断 - 限流算法、熔断器、降级策略
- 消息队列 - 异步处理、削峰填谷
- 日志与监控体系 - 日志、链路追踪、指标监控
- 数据库索引原理 - B+ 树、索引优化
- SQL 查询优化 - EXPLAIN、慢查询
- 数据库连接与连接池 - 连接池原理与配置
- 缓存与数据库一致性 - Cache Aside、延迟双删
- Redis 基础 - Redis 数据结构、持久化、集群
- 常见性能问题与排查 - 性能指标、DevTools 排查
- Node.js Event Loop - 事件循环、微任务与宏任务
- Node.js Buffer 与 Stream - 流式处理、背压