Node.js服务端性能瓶颈分析

张玥 2026年9月17日 阅读时间 80分钟
实战课程 项目演练 Node.js 性能瓶颈 服务端调优
JavaScript编程之Node.js服务端性能瓶颈分析

这是一门实战导向的 Node.js 服务端性能瓶颈分析课程。我们将从一个真实可跑的 Express + PostgreSQL + Redis 服务出发,手把手带你完成 10 个渐进式实战任务:从建立压测基线、定位 CPU 热点、抓取内存泄漏,到排查异步句柄、数据库 N+1、缓存击穿,再到多进程集群、流式背压、V8 GC 调优,最后形成一套可量化的服务端性能监控体系。每个任务都配有可运行的代码、验证命令和验收标准,80 分钟跟着做,你就能把“服务慢了”这种模糊感觉,变成“第 37 行正则在回溯”这种精确结论。

项目准备:搭建你的实验场

性能分析最忌讳“在别人的生产环境上猜”。我们先搭一个可复现、可压测、可回滚的本地实验场:一个 Express 服务,对外提供商品列表、商品详情、下单三个接口,内部使用 PostgreSQL 存储、Redis 缓存,并故意埋入若干典型性能缺陷。

任务 0:初始化项目与性能基线

Step 1: 创建项目并安装依赖
# 创建项目目录
mkdir node-perf-lab && cd node-perf-lab
npm init -y

# 运行时依赖
npm install express pg ioredis compression

# 开发与诊断依赖
npm install -D autocannon clinic 0x

# 用 Docker 快速拉起数据库与缓存
docker run -d --name pg -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16
docker run -d --name redis -p 6379:6379 redis:7
Step 2: 编写一个“故意有毛病”的服务入口,作为后续所有任务的靶子
// src/server.js
const express = require('express');
const { Pool } = require('pg');
const Redis = require('ioredis');

const app = express();
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = new Redis();

// 全局缓存:看起来很方便,其实是内存泄漏的温床
const globalCache = {};

app.get('/api/products', async (req, res) => {
  const { rows } = await db.query('SELECT * FROM products');
  res.json(rows);
});

app.get('/api/products/:id', async (req, res) => {
  const key = `p:${req.params.id}`;
  if (globalCache[key]) return res.json(globalCache[key]);
  const { rows } = await db.query(
    'SELECT * FROM products WHERE id = $1', [req.params.id]
  );
  globalCache[key] = rows[0];
  res.json(rows[0]);
});

app.listen(3000, () => console.log('listening on 3000'));
Step 3: 建立性能基线 —— 用 autocannon 打出第一组数字
# 启动服务
node src/server.js

# 另开终端:30 个连接、持续 20 秒压测列表接口
npx autocannon -c 30 -d 20 http://localhost:3000/api/products

# 输出关注四个指标
# Req/Sec : 每秒吞吐
# Latency p99 : 长尾延迟
# Throughput : 带宽
# Non-2xx : 错误数

验收标准

记录基线四件套:Req/Sec、p99 延迟、CPU 占用率、RSS 内存。后续每一个任务都要与这组数字对比,否则你无法证明“优化真的有效”。建议把结果写进 baseline.md 存档。

实战一:CPU 瓶颈定位与事件循环阻塞

Node.js 是单线程事件循环模型,任何一次同步长计算都会让所有并发请求一起排队。CPU 瓶颈的第一特征不是“CPU 高”,而是“CPU 高的时候吞吐反而掉”。

任务 1:抓住阻塞事件循环的元凶

Step 1: 先做一个“事件循环延迟探针”,把不可见的阻塞变成可测量的数字
// src/eventLoopLag.js
const { monitorEventLoopDelay } = require('perf_hooks');

const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();

setInterval(() => {
  const p99 = histogram.percentile(99) / 1e6;
  const max = histogram.max / 1e6;
  console.log(`EL delay p99=${p99.toFixed(1)}ms max=${max.toFixed(1)}ms`);
  histogram.reset();
}, 5000);
Step 2: 在详情接口里埋一个“正则回溯”陷阱,观察延迟探针如何飙升
// ❌ 危险:嵌套量词 + 未锚定,输入稍长就指数级回溯
const BAD_RE = /^(\w+\s?)*$/;

app.get('/api/search', (req, res) => {
  const q = req.query.q || '';
  const ok = BAD_RE.test(q); // 输入 30 个字符即可卡死数百毫秒
  res.json({ ok });
});

/* ✅ 修复思路:拆分逻辑,用字符串方法替代回溯正则 */
const tokenRe = /^[\w\u4e00-\u9fa5]{1,64}$/;
function isValidQuery(q) {
  if (typeof q !== 'string') return false;
  if (q.length > 64) return false;
  return tokenRe.test(q);
}
Step 3: 用 clinic flame 生成火焰图,让热点函数自己“站出来”
# 启动带诊断的进程
npx clinic flame -- node src/server.js

# 另开终端施加压力
npx autocannon -c 20 -d 15 "http://localhost:3000/api/search?q=aaaaaaaaaaaaaaaaaaaaaaaaaa!"

# 压测结束后 Ctrl+C,浏览器自动打开火焰图
# 观察:最宽的那条自顶向下栈就是 CPU 热点

验收清单

  • 事件循环延迟探针能稳定输出 p99 与 max
  • 压测 /api/search 时,EL delay 明显上涨(> 50ms 即为异常)
  • 修复正则后,同样压测下 p99 延迟下降 80% 以上
  • 火焰图中不再出现超宽的正则或 JSON 解析栈

实战二:内存泄漏与堆快照分析

内存泄漏的可怕之处在于“它不报错”。服务跑三天突然 OOM 重启,日志里什么都没有。我们要做的,是把泄漏点从几百兆堆里精准挖出来。

任务 2:三步定位内存泄漏

Step 1: 建立内存观测面板,先把“涨”这件事量化
// src/memoryProbe.js
function snapshotMemory() {
  const m = process.memoryUsage();
  return {
    rss: (m.rss / 1048576).toFixed(1) + 'MB',
    heapUsed: (m.heapUsed / 1048576).toFixed(1) + 'MB',
    heapTotal: (m.heapTotal / 1048576).toFixed(1) + 'MB',
    external: (m.external / 1048576).toFixed(1) + 'MB'
  };
}

setInterval(() => console.table(snapshotMemory()), 10000);
Step 2: 触发泄漏 —— 三类最常见的泄漏源,我们把它们全部埋进项目
/* 泄漏源 1:无上限的全局缓存 */
const cache = new Map();
app.get('/leak/cache', (req, res) => {
  const key = req.query.k;
  cache.set(key, new Array(10000).fill(key));
  res.end('ok');
});

/* ✅ 修复:LRU + 容量上限 */
class LRUCache {
  constructor(limit = 500) {
    this.limit = limit;
    this.map = new Map();
  }
  get(k) {
    if (!this.map.has(k)) return undefined;
    const v = this.map.get(k);
    this.map.delete(k);
    this.map.set(k, v);
    return v;
  }
  set(k, v) {
    if (this.map.size >= this.limit) {
      const oldest = this.map.keys().next().value;
      this.map.delete(oldest);
    }
    this.map.set(k, v);
  }
}

/* 泄漏源 2:事件监听器未移除 */
function attachLeak(emitter) {
  const handler = () => doSomething();
  emitter.on('data', handler);
  // ❌ 从未 off,且闭包持有大对象引用
}
Step 3: 抓取堆快照并对比 —— 用 Chrome DevTools 找出“谁在持有内存”
# 以调试模式启动,暴露 inspector 端口
node --inspect=9229 src/server.js

# Chrome 打开 chrome://inspect,点击 inspect 进入 DevTools
# Memory 面板 → Heap snapshot → 拍第一张(基线)
# 用 autocannon 反复打 /leak/cache 接口 1000 次
npx autocannon -c 50 -d 30 "http://localhost:3000/leak/cache?k=test"

# 手动触发一次 GC(需启动时加 --expose-gc)
# 然后在 Memory 面板再拍第二张快照
# 选择 Comparison 视图,按 Size Delta 排序
# 重点关注 (array)、(string)、Closure 三类对象

验收标准

压测 10 分钟后,heapUsed 波动区间收敛在 ±15% 以内,且 GC 后能回落到基线附近。若曲线呈“锯齿上升”且每个锯齿底部越来越高,说明仍有泄漏。

实战三:异步模型与句柄泄漏

Node.js 的高并发来自“异步非阻塞”,但异步也带来两类隐性瓶颈:未释放的句柄与无节制的并发。它们不会立刻崩溃,却会让服务在流量高峰时雪崩。

任务 3:句柄排查与并发控制

Step 1: 打印活动句柄,看看进程到底“握着”什么
app.get('/debug/handles', (req, res) => {
  const handles = process._getActiveHandles();
  const requests = process._getActiveRequests();
  res.json({
    handles: handles.map(h => h.constructor.name),
    handleCount: handles.length,
    requestCount: requests.length
  });
});
Step 2: 修复典型的句柄泄漏
/* ❌ 泄漏:每次请求都创建定时器,从不清理 */
app.get('/leak/timer', (req, res) => {
  setInterval(() => pushMetrics(), 1000);
  res.end('ok');
});

/* ✅ 修复:定时器只创建一次,用 unref 避免阻止进程退出 */
const metricsTimer = setInterval(pushMetrics, 1000);
metricsTimer.unref();
Step 3: 用信号量限制下游并发,避免“自己把自己 DDoS”
// src/utils/semaphore.js
class Semaphore {
  constructor(limit) {
    this.limit = limit;
    this.active = 0;
    this.queue = [];
  }
  async acquire() {
    if (this.active < this.limit) {
      this.active++;
      return;
    }
    await new Promise(r => this.queue.push(r));
    this.active++;
  }
  release() {
    this.active--;
    const next = this.queue.shift();
    if (next) next();
  }
}

const sem = new Semaphore(20); // 下游最多 20 个并发

验收清单

  • /debug/handles 在压测前后句柄数量基本稳定
  • 定时器均带 unref(),进程可被正常关闭
  • 下游调用并发数被信号量限制在设定值以内
  • 压测时不再出现 EMFILE: too many open files

实战四:数据库瓶颈 —— N+1、索引与连接池

在绝大多数 Node.js 服务里,最慢的那一环不是 JS,而是数据库。本节我们用 EXPLAIN、连接池监控和批量查询,把数据库从“黑盒”变成“透明盒”。

任务 4:消灭 N+1 与慢查询

Step 1: 复现 N+1 查询
/* ❌ N+1:先查订单列表,再为每个订单单独查商品 */
app.get('/api/orders', async (req, res) => {
  const { rows: orders } = await db.query(
    'SELECT id, user_id, created_at FROM orders LIMIT 50'
  );

  for (const o of orders) {
    const { rows } = await db.query(
      'SELECT * FROM order_items WHERE order_id = $1', [o.id]
    );
    o.items = rows;
  }
  res.json(orders);
});

/* ✅ 修复:一次 JOIN 或一次 IN 批量查询 */
const ids = orders.map(o => o.id);
const { rows: items } = await db.query(
  'SELECT * FROM order_items WHERE order_id = ANY($1)', [ids]
);
Step 2: 用 EXPLAIN ANALYZE 找出慢在哪
-- 打开执行计划,重点关注 Seq Scan 与大 rows 估算
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products
WHERE category_id = 7 AND price < 100
ORDER BY created_at DESC
LIMIT 20;

-- 若出现 Seq Scan,补上复合索引
CREATE INDEX CONCURRENTLY idx_products_cat_price_created
ON products (category_id, price, created_at DESC);
Step 3: 监控连接池,避免“连接等死”
const db = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
  statement_timeout: 3000
});

app.get('/debug/pool', (req, res) => {
  res.json({
    total: db.totalCount,
    idle: db.idleCount,
    waiting: db.waitingCount
  });
});

验收标准

同一个订单接口,SQL 执行次数从 51 次降到 1 次;p99 延迟下降 60% 以上;pg_stat_statements 中不再出现同一模板的高频调用。

实战五:缓存策略与缓存三大难题

缓存是性价比最高的性能手段,但它也是最容易“帮倒忙”的手段。缓存穿透、缓存击穿、缓存雪崩,本质都是“缓存没挡住流量,数据库被打穿”。

任务 5:构建可靠的多级缓存

Step 1: 封装一个带空值保护与随机过期时间的缓存层
// src/cache/redisCache.js
const NULL_SENTINEL = '__NULL__';

async function getOrSet(key, ttlSec, loader) {
  const cached = await redis.get(key);
  if (cached === NULL_SENTINEL) return null;
  if (cached) return JSON.parse(cached);

  const value = await loader();
  const jitter = Math.floor(ttlSec * 0.2 * Math.random());
  const ttl = ttlSec + jitter;

  if (value == null) {
    await redis.set(key, NULL_SENTINEL, 'EX', 60);
    return null;
  }
  await redis.set(key, JSON.stringify(value), 'EX', ttl);
  return value;
}
Step 2: 用互斥锁解决缓存击穿(热点 key 失效瞬间的惊群)
// src/cache/mutex.js —— 单实例内的轻量互斥
const inflight = new Map();

async function getOrSetWithLock(key, ttlSec, loader) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  if (inflight.has(key)) return inflight.get(key);

  const task = (async () => {
    try {
      const value = await loader();
      await redis.set(key, JSON.stringify(value), 'EX', ttlSec);
      return value;
    } finally {
      inflight.delete(key);
    }
  })();

  inflight.set(key, task);
  return task;
}

验收清单

  • 查询不存在的 ID 时,数据库只被打一次(空值缓存生效)
  • 热点 key 过期瞬间,数据库 QPS 无尖峰(互斥锁生效)
  • 批量 key 的过期时间分散,无明显周期性尖峰
  • L1 缓存命中时,Redis 的 keyspace_hits 不再增长

实战六:Cluster 集群与 CPU 横向扩展

Node.js 单进程只能用一个 CPU 核心。在一台 8 核机器上只跑一个 Node 进程,等于浪费了 87% 的算力。Cluster 是最直接的扩容手段,但用好它需要处理会话、定时任务与优雅退出。

任务 6:从单进程到多进程集群

Step 1: 用 cluster 模块按 CPU 核数派生工作进程
// src/cluster.js
const cluster = require('node:cluster');
const os = require('node:os');

const WORKERS = process.env.WEB_CONCURRENCY || os.cpus().length;

if (cluster.isPrimary) {
  console.log(`primary ${process.pid} forking ${WORKERS} workers`);
  for (let i = 0; i < WORKERS; i++) cluster.fork();
  cluster.on('exit', (worker, code, signal) => {
    console.error(`worker ${worker.process.pid} died`);
    cluster.fork();
  });
} else {
  require('./server');
}
Step 2: 用 PM2 管理进程,并开启集群模式
npm install -g pm2

pm2 start src/server.js -i max --name node-perf-lab

pm2 monit

pm2 reload node-perf-lab

pm2 startup && pm2 save
Step 3: 优雅退出 —— 避免 reload 时丢掉正在处理的请求
const server = app.listen(3000);
let shuttingDown = false;

function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  server.close(async () => {
    await db.end();
    await redis.quit();
    process.exit(0);
  });
  setTimeout(() => process.exit(1), 10000).unref();
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

验收标准

8 核机器上,吞吐量应接近单进程的 6~7 倍(受共享资源限制,不会线性)。同时观察:reload 期间 autocannon 的错误数应为 0,证明优雅退出生效。

实战七:流式处理与背压控制

当接口需要返回十万行数据或处理一个 500MB 的上传文件时,“先读进内存再返回”就是灾难。Stream 的价值不仅是省内存,更是让内存占用与数据量解耦。

任务 7:把大响应改造成流

Step 1: 对比“全量读入”与“流式返回”的内存差异
/* ❌ 全量:100 万行 JSON 全部驻留内存 */
app.get('/export/bad', async (req, res) => {
  const { rows } = await db.query('SELECT * FROM big_table');
  res.json(rows);
});

/* ✅ 流式:用 pg 游标 + JSON 流,内存恒定 */
const QueryStream = require('pg-query-stream');
const { pipeline } = require('node:stream/promises');

app.get('/export/good', async (req, res) => {
  res.setHeader('Content-Type', 'application/x-ndjson');
  const client = await db.connect();
  const qs = new QueryStream('SELECT * FROM big_table', [], { batchSize: 500 });
  const source = client.query(qs);
  const transform = new Transform({
    objectMode: true,
    transform(row, _enc, cb) { cb(null, JSON.stringify(row) + '\n'); }
  });
  try { await pipeline(source, transform, res); }
  finally { client.release(); }
});
Step 2: 理解背压 —— 下游慢时,上游必须暂停
function writeWithBackpressure(stream, chunk) {
  const ok = stream.write(chunk);
  if (!ok) {
    return new Promise(resolve => stream.once('drain', resolve));
  }
  return Promise.resolve();
}

app.post('/upload', async (req, res) => {
  const dest = fs.createWriteStream('./uploads/tmp.bin');
  try {
    await pipeline(req, dest);
    res.json({ ok: true });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

验收清单

  • 导出 100 万行数据时,RSS 增长不超过 100MB
  • 客户端中途断开时,服务端流被正确销毁(无句柄泄漏)
  • 上传 500MB 文件时,内存占用保持平稳
  • CPU 占用因压缩上升不超过 10%,但传输体积下降 70%

实战八:V8 参数与 GC 调优

GC 是“看不见的停顿”。当你在压测中看到 p99 延迟周期性尖刺,而 CPU 曲线也呈现规律波动时,大概率是 GC 在作祟。这一节我们学习如何观察并调优它。

任务 8:读懂 GC 并调整堆参数

Step 1: 打印 GC 日志,观察停顿频率与类型
node --trace-gc src/server.js

# Scavenge = 新生代 GC,频繁但很快,通常 < 5ms
# Mark-Compact = 老生代 GC,不频繁但可能停顿数十毫秒

node --trace-gc --trace-gc-verbose src/server.js
Step 2: 按业务特征调整堆参数
# 场景 A:内存充足、追求低延迟的服务
node --max-semi-space-size=64 --max-old-space-size=2048 src/server.js

# 场景 B:容器内存受限,需要主动约束堆大小
node --max-old-space-size=384 src/server.js

# 场景 C:短生命周期任务
node --max-old-space-size=256 --optimize-for-size src/cli.js
Step 3: 用 --cpu-prof 生成 CPU Profile,找到 V8 优化失效的函数
node --cpu-prof --cpu-prof-dir=./profiles src/server.js

/* 常见导致 deopt 的写法: */
// ❌ 同一函数收到多种类型参数,导致隐藏类变化
function add(a, b) { return a + b; }
add(1, 2);
add('1', '2');

// ✅ 保持参数类型稳定
function addNumbers(a, b) { return a + b; }
function concatStrings(a, b) { return a + b; }

验收标准

压测 5 分钟后,--trace-gc 日志中单次 GC 停顿不超过 20ms;p99 延迟曲线不再出现规律性尖刺;堆内存在大致范围内稳定震荡。

实战九:压测方法与性能监控体系

没有度量就没有优化。本节我们搭建一套完整的度量体系:从压测脚本、指标采集,到可视化与告警,让性能问题在用户投诉之前就被发现。

任务 9:建立性能度量闭环

Step 1: 编写分级压测脚本,覆盖不同负载场景
// scripts/loadtest.js
const autocannon = require('autocannon');

const scenarios = [
  { name: '低负载', connections: 10, duration: 10 },
  { name: '正常负载', connections: 50, duration: 20 },
  { name: '峰值负载', connections: 200, duration: 30 },
  { name: '过载', connections: 500, duration: 30 }
];

(async () => {
  for (const s of scenarios) {
    const result = await autocannon({
      url: 'http://localhost:3000/api/products',
      connections: s.connections,
      duration: s.duration
    });
    console.log(`[${s.name}]`, result.requests.average, result.latency.p99);
  }
})();
Step 2: 采集运行时指标并暴露 /metrics 端点
// src/metrics.js
const client = require('prom-client');
client.collectDefaultMetrics();

const httpDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP 请求耗时分布',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 3]
});

app.use((req, res, next) => {
  const end = httpDuration.startTimer({
    method: req.method,
    route: req.route?.path || req.path,
    status: 'pending'
  });
  res.on('finish', () => end({ status: String(res.statusCode) }));
  next();
});

app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});

验收清单

  • 四级压测场景均可跑通,并输出 rps / p50 / p99 / 错误数
  • /metrics 端点能返回 Prometheus 格式指标
  • Grafana 面板能看到 QPS、延迟分位、事件循环延迟、堆内存四条曲线
  • 慢请求日志能定位到具体路由与耗时
  • 告警规则在阈值被突破时能触发

综合实战:从 3200ms 到 180ms 的完整优化

把前面九个实战串起来,形成一条完整的服务端性能优化流水线。这是你真正的“毕业项目”。

任务 10:端到端瓶颈分析与调优演练

Step 1: 按照“先测量、后优化、再验证”的顺序推进,每一步都记录数据
/* 优化路线图(以 p99 延迟为观测指标) */

1. 建立压测基线与监控         // p99 = 3200ms,RPS = 45
2. 消灭正则回溯阻塞           // p99 → 2400ms
3. 修复 N+1 查询               // p99 → 900ms
4. 补齐复合索引               // p99 → 480ms
5. 引入 Redis + L1 缓存        // p99 → 260ms
6. 连接池与并发信号量         // p99 → 210ms
7. 修内存泄漏与句柄泄漏     // p99 → 195ms
8. Cluster 多进程扩展          // RPS 45 → 320
9. 大响应改造为流式          // RSS 1.8GB → 220MB
10. GC 调优与持续监控         // p99 → 180ms,稳定
Step 2: 用同一套压测脚本做最终对比验证
/* 优化前后对比 */

优化前:
  RPS       : 45
  p50       : 860 ms
  p99       : 3200 ms
  EL delay : 420 ms
  RSS       : 1.8 GB(持续上涨)
  SQL/请求 : 51

优化后:
  RPS       : 320   // +611%
  p50       : 42 ms  // -95%
  p99       : 180 ms // -94%
  EL delay : 8 ms    // -98%
  RSS       : 220 MB(稳定)
  SQL/请求 : 1      // -98%

毕业验收清单

  • 压测场景可重复运行,数据可对比
  • p99 延迟相比基线下降 90% 以上
  • RSS 内存在压测中保持稳定,无泄漏
  • 数据库查询次数与慢查询显著减少
  • 监控指标可采集、可视化、告警
  • 所有优化点都能在代码中找到对应实现

实战避坑指南

性能优化有不少“看起来对、实际有坑”的做法。这里总结最常见的 5 个陷阱,帮你少走弯路。

/* ❌ 陷阱 1:所有同步计算都放主线程 */
const result = heavySyncCompute(bigData);

/* ✅ 正确做法:worker_threads 或拆分为异步任务 */
const worker = new Worker('./worker.js');

/* ❌ 陷阱 2:无上限缓存 */
const cache = {};
cache[key] = data;

/* ✅ 正确做法:LRU + TTL */
const cache = new LRUCache(500);

/* ❌ 陷阱 3:N+1 查询 */
for (const id of ids) await db.query('... WHERE id=$1', [id]);

/* ✅ 正确做法:IN 批量查询 */
await db.query('... WHERE id = ANY($1)', [ids]);

/* ❌ 陷阱 4:不设连接池上限 */
new Pool({ max: 999 });

/* ✅ 正确做法:与数据库 max_connections 匹配 */
new Pool({ max: 20 });

/* ❌ 陷阱 5:忽略优雅退出 */
process.on('SIGTERM', () => process.exit(0));

/* ✅ 正确做法:drain 连接、关闭资源再退出 */
❌ 同步阻塞主线程
事件循环延迟飙升
❌ 无上限缓存
内存持续上涨直至 OOM
❌ N+1 查询
数据库连接被打满
✅ 先测量后优化
用数据证明,用压测验证

性能优化的前提是“不引入新的问题”