跳到主要内容

服务端性能优化

问题

服务端性能优化有哪些常见手段?如何定位性能瓶颈?如何设计高并发架构?

面试速答版

服务端性能优化有哪些手段? 按 ROI 排序,从「最容易出效果」到「最重的架构改造」:

  • 数据库:索引优化、解决 N+1(用 JOINDataLoader 批量查)、连接池调优、读写分离、分库分表。
  • 缓存:多级缓存(CDN → Nginx → 内存 → Redis),热点 Key 预加载,详见 缓存策略
  • 异步化:耗时任务(发邮件、生成报表)丢消息队列,接口立即返回;大数据导出走流式响应。
  • 架构层:负载均衡 + 水平扩展 + 限流熔断,CDN/Gzip/HTTP/2 加速传输。

怎么定位性能瓶颈?P95/P99 而不是平均值,长尾才是用户真感受:

  • 指标先行Prometheus + Grafana 看 QPS、P99、错误率,发现异常接口。
  • 链路追踪OpenTelemetry/Jaeger 看每次请求各阶段耗时,定位慢的是 DB 还是外部 API。
  • APM 工具Node.js Inspectorclinic.js0x 火焰图找 CPU/内存热点。
  • 慢 SQL 日志:MySQL 开 slow_query_log,PostgreSQL 看 pg_stat_statements

怎么设计高并发架构? 核心思路是无状态 + 横向扩展 + 异步削峰

  • 应用层无状态,会话放 Redis,方便多实例水平扩展。
  • 入口层 Nginx/网关做负载均衡 + 限流,洪峰用消息队列削峰。
  • 数据层读写分离 + 分库分表,热点用 Redis 抗住,冷数据走数据库。
  • 高可用靠多活 + 熔断降级,故障时保住核心链路。

答案

性能优化全景图

服务端性能优化可以从六个维度切入,每个维度下有多种具体手段:

性能指标体系

优化之前,先明确要衡量什么。服务端核心性能指标可以分为三类:

类别指标说明健康参考值
吞吐量QPS每秒处理的请求数(读场景)视业务而定
TPS每秒完成的事务数(写场景)一个 TPS 可能包含多个 QPS
并发数同一时刻正在处理的请求数
延迟P5050% 请求在此时间内完成< 50ms
P9595% 请求在此时间内完成< 200ms
P9999% 请求在此时间内完成< 500ms
TTFB首字节到达时间< 100ms
可用性成功率非 5xx 请求占比> 99.9%
错误率5xx / 超时占比< 0.1%
为什么要看 P95/P99,而不只看平均值?

平均响应时间 50ms 看起来不错,但如果 1% 的请求要 5 秒才返回,用户体验极差。长尾延迟(P99)才是用户真正感受到的性能。面试中经常考察候选人是否理解这一点。


一、数据库优化

数据库通常是服务端性能的最大瓶颈。一条慢 SQL 就能拖垮整个服务。

1. N+1 查询问题

N+1 是最常见的数据库性能杀手:查询 N 条主记录后,又对每条记录发起一次关联查询,最终执行 N+1 条 SQL。

n-plus-one-problem.ts
// ❌ 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
}

三种解决方案:

n-plus-one-solutions.ts
// ✅ 方案 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. 索引优化

index-optimization.sql
-- 查看慢查询
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);
索引的代价

索引不是越多越好。每个索引都会:

  • 增加写入开销:INSERT / UPDATE / DELETE 都需要同步更新索引
  • 占用磁盘空间:大表的索引可能和数据本身一样大
  • 增加优化器决策成本:太多索引反而让优化器选错执行计划

原则:只为高频查询、高选择性列建索引。详见 数据库索引原理SQL 查询优化

3. 连接池调优

数据库连接的创建和销毁开销很大(TCP 三次握手 + 认证 + SSL 协商),连接池复用已有连接,避免重复创建。

connection-pool.ts
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 官方建议的经验公式:

connections=(CPU 核心数×2)+有效磁盘数\text{connections} = (\text{CPU 核心数} \times 2) + \text{有效磁盘数}

一台 4 核机器:4×2+1=9104 \times 2 + 1 = 9 \approx 10 个连接。

连接池不是越大越好。连接数过多会导致:

  • 数据库端上下文切换频繁,CPU 飙升
  • 每个连接占用内存(MySQL 默认约 8MB/连接)
  • 锁竞争加剧

详见 数据库连接与连接池

4. 读写分离

高并发场景下,单个数据库实例无法同时承受大量读写。读写分离把读请求分发到只读副本,写请求发到主库:

read-write-splitting.ts
// 简化的读写分离配置
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写多场景,允许短暂不一致

缓存预热

系统启动或大促前,提前把热点数据加载到缓存,避免冷启动时大量请求穿透到数据库:

cache-warmup.ts
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. 异步处理与消息队列

核心思想:把非核心逻辑从请求链路中剥离,通过消息队列异步执行,让接口快速返回。

async-processing.ts
// ❌ 同步处理:接口响应 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。流式处理可以边读边处理边释放,内存占用稳定在很低的水平:

stream-processing.ts
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. 并发控制与批量操作

concurrency-batch.ts
// ❌ 串行请求:逐个调用,总耗时 = 所有请求的耗时之和
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 是单线程的,任何阻塞操作都会让所有请求排队等待。

avoid-blocking.ts
// ❌ 阻塞事件循环: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. 内存优化

memory-optimization.ts
// ❌ 内存泄漏:全局缓存无上限增长
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 协商)可能比请求本身还慢:

http-keepalive.ts
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. 批量操作减少网络往返

batch-operations.ts
// ❌ 逐条写入 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. 响应压缩

compression.ts
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. 负载均衡

nginx-load-balance.conf
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 ✅ 正确的级联超时 │
timeout-retry.ts
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)可以看到每个服务的耗时,快速定位瓶颈。

request-tracing.ts
// 简化的请求链路追踪中间件
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 使用率看是否还有余量

压测注意事项

压测的坑
  1. 不要压测生产环境:使用独立的压测环境,数据库用测试数据
  2. 逐步加压:从低并发开始,逐步增加,观察拐点
  3. 关注系统指标:CPU、内存、连接数、GC 频率要同步监控
  4. 预热服务:JIT 编译、连接池初始化需要时间,先发一轮预热请求
  5. 压测时间足够长:至少 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×平均响应时间(RT)\text{并发数} = \text{QPS} \times \text{平均响应时间(RT)}

举例:

  • QPS = 1000,RT = 50ms → 并发数 = 1000×0.05=501000 \times 0.05 = 50
  • 如果线程池大小是 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

解决方案

  1. Eager Loading:ORM 的 include / populate / relations
  2. 手动批量查询:先收集所有 ID,用 IN 一次查完
  3. DataLoader:合并同一事件循环内的多次查询(GraphQL 场景)
  4. JOIN 查询:在 SQL 层面直接关联

Q8: Node.js 服务的 CPU 占用率很高,怎么排查?

答案

  1. 生成 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 # 生成火焰图
  1. 分析火焰图:横轴是时间占比,找出最宽的方块(耗时最多的函数)

  2. 常见 CPU 瓶颈

    • JSON 序列化/反序列化大对象 → 流式 JSON / 减少响应体大小
    • 正则表达式回溯 → 优化正则或使用非回溯引擎
    • 加密/哈希计算 → 移到 Worker Thread
    • 同步文件操作 → 换成异步 API
    • 频繁 GC → 减少对象创建、优化内存使用

Q9: 如何避免服务雪崩?

答案

雪崩的典型链路:一个下游服务变慢 → 上游超时堆积 → 线程池打满 → 上游也变慢 → 级联传导 → 全局不可用。

防雪崩三板斧

手段作用实现
限流控制入口流量,不让太多请求进来令牌桶 / 滑动窗口
熔断下游故障时快速失败,不再调用错误率触发、半开探测
降级返回兜底数据,保障核心链路缓存兜底 / 默认值

补充手段

  • 超时控制:所有外部调用必须设超时
  • 隔离:线程池隔离、进程隔离、服务隔离
  • 弹性扩缩:流量激增时自动扩容

详见 限流与熔断

Q10: 服务端性能优化的优先级怎么排?

答案

遵循二八原则:20% 的优化手段解决 80% 的性能问题。

优化顺序(投入产出比从高到低):

  1. 加缓存(成本低,收益极高)
  2. 修 N+1 和慢 SQL(成本低,收益高)
  3. 加索引(一行 SQL,收益巨大)
  4. 异步化非核心逻辑(架构调整,收益明显)
  5. 连接池和批量操作(小改动,减少 I/O)
  6. 压缩和 CDN(基础设施层面)
  7. 读写分离(需要主从架构)
  8. 水平扩展(需要无状态设计)
  9. 分库分表(复杂度高,最后考虑)
过早优化是万恶之源

Donald Knuth 的名言。不要在没有性能问题的时候做优化。正确的流程是:

  1. 发现问题:通过监控发现接口慢 / 资源不足
  2. 定位瓶颈:用工具定位具体是哪里慢
  3. 针对性优化:只优化瓶颈点
  4. 验证效果:压测确认改善

Q11: 如何做数据库连接池监控?

答案

连接池打满是常见的线上事故根因。必须监控以下指标:

指标说明告警阈值
活跃连接数当前正在使用的连接> 80% 容量
空闲连接数连接池中空闲的连接< 2
等待队列长度排队等待获取连接的请求数> 0 持续
获取连接耗时从请求连接到获取连接的时间> 100ms
连接泄漏借出后长时间未归还的连接存在即告警
pool-monitoring.ts
// 定期采集连接池指标
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 帮你自动处理了这个过程。

相关链接