API 延迟优化
减少请求到响应时间,聚焦 p95 和 p99 非平均。
为什么
API 延迟是用户等待时间,且复合——一页 5 个 API 调用各 200ms 加载 1 秒,慢连接移动用户等更久。平均延迟误导因藏慢尾:平均 100ms p99 2s 意味 1% 用户等 20 倍久,那些是见超时和错误的用户。优化延迟是优化尾——p95 和 p99——因快请求已快,慢的是用户受苦处。每 100ms API 延迟降转化约 1%(Amazon 发现),故延迟直接是收入指标。
何时使用
用 APM 建延迟基线后。先优化最慢端点(帕累托——20% 端点致 80% 延迟)。查询或功能变更可能回退延迟时再优化。
如何做
1. 用 APM 测延迟按端点分解 - Datadog、New Relic 或 OpenTelemetry + Jaeger - 每端点看 p50、p95、p99——非仅平均 - 识别最慢端点和最慢百分位 2. profile 慢请求找时间花哪 - 分布式追踪显 span 分解: DB、缓存、外部 API、计算 - 是慢查询?缺索引?N+1?同步外部调用? - 先修主导 span——DB 500ms 时优化 10ms span 是浪费 3. 优化数据库(通常最大 span) - 为 WHERE、JOIN、ORDER BY 中列加索引 - 消 N+1 查询: 用预加载(includes/join)或批量加载 - 用连接池——每请求新连接加 50-100ms 握手 - 读重端点考虑读副本 4. 为重复响应加缓存 - 全响应缓存到 Redis/Memcached 带 TTL(如商品目录: 5 分钟) - 用 HTTP 缓存头(Cache-Control、ETag)供 CDN 级缓存 - 应用级缓存贵计算(记忆化) - 写时失效——陈旧缓存是正确性 bug 5. 并行化独立操作 - 端点调 3 个独立服务用 Promise.all 非顺序 - 3 × 100ms 顺序 = 300ms;3 × 100ms 并行 = 100ms - 全程 async/await——永不阻塞事件循环做同步 I/O
示例
// 之前: N+1 查询、顺序外部调用——总 800ms
async function getOrderDetails(orderId: string) {
const order = await db.query("SELECT * FROM orders WHERE id = $1", [orderId]); // 50ms
const items = await db.query("SELECT * FROM order_items WHERE order_id = $1", [orderId]); // 50ms
// N+1: 每商品一条查询取产品信息
const products = await Promise.all(items.map(i =>
db.query("SELECT * FROM products WHERE id = $1", [i.product_id]) // 50ms × N
));
const shipping = await fetchShippingStatus(orderId); // 200ms 外部 API
const tracking = await fetchTrackingInfo(orderId); // 200ms 外部 API
return { order, items, products, shipping, tracking };
}
// 之后: 批量查询、并行外部调用——总 250ms
async function getOrderDetails(orderId: string) {
const [order, items, products] = await Promise.all([
db.query("SELECT * FROM orders WHERE id = $1", [orderId]), // 50ms
db.query("SELECT * FROM order_items WHERE order_id = $1", [orderId]), // 50ms
// 单查询带 JOIN 替代 N+1
db.query(`
SELECT p.* FROM products p
JOIN order_items oi ON p.id = oi.product_id
WHERE oi.order_id = $1
`, [orderId]), // 50ms
]);
// 并行替代顺序: 200ms 替代 400ms
const [shipping, tracking] = await Promise.all([
fetchShippingStatus(orderId), // 200ms
fetchTrackingInfo(orderId), // 200ms(并行)
]);
return { order, items, products, shipping, tracking };
}常见问题
- ⚠优化平均延迟宣布胜利而 p99 仍 5 秒——1% 用户仍见超时,那些是支持工单和弃购物车。
- ⚠加缓存无失效策略——写后缓存服务陈旧数据,用户见错误信息,bug 间歇难复现。
- ⚠并行化非真独立的调用(一个依赖另一个结果)——并行调用因输入未就绪失败,错误令人困惑。
- ⚠加索引但不测改善——索引帮一查询但慢写,净延迟可能不变或更糟。
最佳实践
- ✓生产中用 APM 每端点测 p95 和 p99 并设 SLO(如 p95 < 200ms)——尾是用户受苦处,平均藏它。
- ✓用分布式追踪找主导 span(通常数据库)先优化——500ms 查询使 10ms 序列化优化相形见绌。
- ✓用单 JOIN 或批量加载消 N+1 查询——这是最常见延迟 bug 且最易修,常减半延迟。
- ✓用 Promise.all(或 asyncio.gather)并行化独立 I/O——对独立服务的顺序调用浪费并行化免费消的时间。