p99 = 尾延迟(tail latency):最慢的 1% 请求要花 3s。第一步是测量并拆解,而不是猜测。对比 p50 与 p99——如果 p50 很快但 p99 是 3s,说明这条路径并非一致地慢;某些东西只在高负载或边界情况下才发作(连接池饱和、GC 暂停、N+1、冷缓存、一个未设超时的外部调用)。
**心智模型:**p99 是最长的结账队列,而不是平均等待时间——加快平均值对卡在队尾的顾客毫无帮助。
// 为每个阶段计时,看 p99 堆积在哪里(不要猜)
async function handler(req, res) {
const t0 = process.hrtime.bigint();
const mark = (label) =>
req.log.info({ label, ms: Number(process.hrtime.bigint() - t0) / 1e6 });
const user = await db.user.find(req.id); mark('db.user');
const courses = await loadCourses(user); mark('loadCourses'); // 嫌疑
res.json(courses); mark('total');
}
// 之前:N+1 —— N 次串行往返,随着 N 增长 p99 爆炸
const courses = await db.course.findByUser(userId);
for (const c of courses) c.lessons = await db.lesson.byCourse(c.id);
// 之后:合并为 1 次查询 -> 尾部下降(不再有 N 次串行往返)
const courses = await db.course.findByUser(userId);
const lessons = await db.lesson.byCourseIds(courses.map((c) => c.id)); // 一次 IN(...)
const byCourse = Map.groupBy(lessons, (l) => l.courseId);
for (const c of courses) c.lessons = byCourse.get(c.id) ?? [];
在这里,p50(N 较小)看起来没问题,但在高负载下 N 增大、连接池饱和,于是 p99 引爆。对于外部调用,永远要设置超时 + 熔断器(circuit breaker),这样一个慢依赖就无法拖累整个端点的 p99。
面试官在考察一个有方法论的过程,而不是撞大运的猜测:你是否区分尾部与均值、是否在改动前先测量、是否先看队列/锁(等待)再看 CPU。弱答案:"加缓存 / 把服务器扩容。"强答案:测量 → 用 tracing 定位 → 修复具体的尾部原因 → 用数字验证。
优化尾部(p99),而不是平均值。大多数"3s p99"来自等待(池/队列/锁),而不是计算——所以先看队列,再看 CPU。
一个包含详细解答的 IT 面试题库——从初级到高级。
捐赠