接口响应慢排查
2021年某电商平台的商品详情页接口,在双十一期间响应时间从正常的100ms飙升到5秒,大量用户反馈页面加载不出来。
技术团队排查了整整2小时,发现原因让人哭笑不得:商品图片存储在七牛云CDN上,双十一期间CDN流量费用暴涨,触发了内部的限流策略,导致部分图片加载超时。
但接口超时只是表象,真正的问题是:商品详情接口依赖了图片加载,而图片加载超时导致整个页面渲染超时。
这次问题影响了约100万用户的浏览,直接损失约200万元订单。
这是一个典型的"接口设计问题导致级联超时"的案例。
【面试官手记】
接口响应慢是生产环境最常见的性能问题。我面试过的候选人里,能说清楚"接口响应慢怎么排查"的不超过40%,能说出"逐层拆解延迟"的不超过20%。接口排查的关键是链路思维:接口慢不是某一个点的问题,而是整个链路上所有点的叠加。
一、接口响应慢的常见原因 🔴
1.1 五大原因
接口响应慢的五大原因:
1. 数据库问题
- 慢查询:缺索引、全表扫描
- 连接池耗尽:连接等待时间长
- 锁竞争:长事务阻塞其他请求
- 典型症状:数据库耗时占总耗时 > 50%
2. 远程调用问题
- HTTP/GRPC超时:下游服务慢
- 批量调用:串行改并行没有
- 连接池耗尽:RPC连接不够用
- 典型症状:多个外部调用累加
3. 资源瓶颈
- CPU打满:计算密集型任务
- 内存泄漏:GC频繁导致停顿
- 磁盘IO:日志写入阻塞
- 典型症状:资源使用率 > 80%
4. 代码问题
- 循环查询:N+1查询问题
- 同步阻塞:没用异步
- 复杂计算:算法效率低
- 典型症状:代码执行时间 > 预期
5. 网络问题
- DNS解析慢
- 跨机房调用延迟
- 网络抖动
- 典型症状:特定区域用户慢
1.2 量化分析
接口响应时间分解:
总耗时 = 网络耗时 + 业务代码耗时 + 数据库耗时 + 远程调用耗时
正常比例(参考):
- 网络耗时:< 10ms(同城)
- 业务代码:< 20ms
- 数据库查询:< 30ms(简单查询)
- 远程调用:< 50ms
异常定位:
- 数据库耗时 > 100ms → 查索引
- 远程调用耗时 > 500ms → 查下游
- 代码耗时 > 1s → 查算法
1.3 面试追问
面试官:接口响应慢怎么排查?
候选人:首先看是哪个环节慢,是数据库还是远程调用。
面试官:怎么快速定位是哪个环节慢?
候选人:用链路追踪工具,比如SkyWalking,看每个Span的耗时。
面试官:如果没有链路追踪工具呢?
候选人:那就在代码里打日志,记录每个环节的耗时。
【面试官心理】
接口排查的追问通常很务实。能回答出"链路追踪"的候选人,说明知道工具;能说出"打日志记录耗时"的候选人,说明有实际排查经验。
二、排查流程 🔴
2.1 第一步:确认慢的范围
# 查看接口的平均响应时间
curl -w "@curl-format.txt" -o /dev/null -s http://api.example.com/product/detail
# curl-format.txt内容:
# time_namelookup: %{time_namelookup}\n
# time_connect: %{time_connect}\n
# time_appconnect: %{time_appconnect}\n
# time_pretransfer: %{time_pretransfer}\n
# time_starttransfer: %{time_starttransfer}\n
# time_total: %{time_total}\n
2.2 第二步:链路追踪
链路追踪工具:
1. SkyWalking
- 优点:零侵入、自动埋点
- 缺点:部署复杂
2. Zipkin
- 优点:简单、轻量
- 缺点:功能有限
3. Jaeger
- 优点:支持多语言
- 缺点:UI一般
4. Apache HTrace
- 优点:Hadoop生态
- 缺点:小众
2.3 第三步:逐层分析
// 代码层面的耗时打点
public ProductDetail getProductDetail(Long productId) {
long start = System.currentTimeMillis();
// 1. 查数据库获取商品信息
long t1 = System.currentTimeMillis();
Product product = productDAO.selectById(productId);
log.info("查商品信息耗时: {}ms", System.currentTimeMillis() - t1);
// 2. 查缓存获取SKU信息
long t2 = System.currentTimeMillis();
List<SKU> skus = skuService.getByProductId(productId);
log.info("查SKU耗时: {}ms", System.currentTimeMillis() - t2);
// 3. 查远程服务获取评价
long t3 = System.currentTimeMillis();
List<Review> reviews = reviewService.getByProductId(productId);
log.info("查评价耗时: {}ms", System.currentTimeMillis() - t3);
log.info("总耗时: {}ms", System.currentTimeMillis() - start);
return buildDetail(product, skus, reviews);
}
2.4 第四步:数据库分析
# 查看当前执行的慢查询
SHOW FULL PROCESSLIST;
# 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SELECT * FROM mysql.slow_log;
# EXPLAIN分析查询
EXPLAIN SELECT * FROM product WHERE id = 123456;
三、常见瓶颈与优化 🟡
3.1 数据库慢查询
// 慢查询优化示例
// 优化前:模糊查询,全表扫描
SELECT * FROM product WHERE name LIKE '%手机%';
// 优化后:使用索引 + 全文搜索
SELECT * FROM product WHERE MATCH(name) AGAINST('手机');
// 或使用ElasticSearch
// N+1查询问题
// 优化前:N+1查询
List<Product> products = productDAO.selectAll();
for (Product p : products) {
List<SKU> skus = skuDAO.selectByProductId(p.getId()); // 每个商品查一次
}
// 优化后:批量查询
List<Product> products = productDAO.selectAll();
Set<Long> productIds = products.stream().map(Product::getId).collect(toSet());
List<SKU> skus = skuDAO.selectByProductIds(productIds); // 一次查完
Map<Long, List<SKU>> skuMap = skus.stream().collect(groupingBy(SKU::getProductId));
3.2 远程调用慢
// 串行改并行
// 优化前:串行调用
Product product = productService.get(id);
List<SKU> skus = skuService.getByProductId(id);
List<Review> reviews = reviewService.getByProductId(id);
// 总耗时 = t1 + t2 + t3
// 优化后:并行调用
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() -> productService.get(id));
CompletableFuture<List<SKU>> skuFuture = CompletableFuture.supplyAsync(() -> skuService.getByProductId(id));
CompletableFuture<List<Review>> reviewFuture = CompletableFuture.supplyAsync(() -> reviewService.getByProductId(id));
Product product = productFuture.get();
List<SKU> skus = skuFuture.get();
List<Review> reviews = reviewFuture.get();
// 总耗时 = max(t1, t2, t3)
3.3 超时配置
// 超时配置不合理
// 错误配置:超时时间过长
@FeignClient(name = "sku-service", configuration = FeignConfig.class)
interface SkuClient {
@RequestMapping(method = RequestMethod.GET, value = "/sku/{id}")
Sku getSku(@PathVariable("id") Long id);
}
@Configuration
class FeignConfig {
@Bean
public Request.Options options() {
return new Options(10 * 1000, 60 * 1000); // 连接超时10s,读超时60s,太长!
}
}
// 正确配置:超时时间合理
@Configuration
class FeignConfig {
@Bean
public Request.Options options() {
return new Options(1000, 3000); // 连接超时1s,读超时3s
}
}
四、生产避坑 🟡
4.1 接口排查的五大坑
坑1:只看平均响应时间
问题:平均100ms,但P99是5秒
场景:99%的请求快,1%的请求极慢
解决方案:
- 看P99/P999指标
- 分析慢请求的分布
坑2:只看单个接口
问题:只分析目标接口,不知道下游服务慢
场景:上游接口快,下游接口慢,级联超时
解决方案:
- 链路追踪看完整调用链
- 检查下游服务的响应时间
坑3:忽略网络耗时
问题:只看服务端耗时,忽略网络耗时
场景:跨机房调用、网络抖动
解决方案:
- 用curl的time_*分析各阶段耗时
- 确认是服务端还是网络问题
坑4:缺少监控基线
问题:没有历史数据,不知道正常应该是多少
场景:接口从100ms变成200ms,不知道是否异常
解决方案:
- 建立接口响应时间基线
- 设置告警阈值(如超过基线50%告警)
坑5:改完不复盘
问题:优化了接口,但没有验证效果
场景:以为优化了,实际没效果
解决方案:
- 上线后对比优化前后的P99
- 建立性能回归测试
4.2 优化目标
接口优化目标(参考):
场景 P50 P99 P999
简单查询 < 10ms < 50ms < 100ms
中等复杂 < 50ms < 200ms < 500ms
复杂查询 < 200ms < 1s < 3s
注意:这些是参考值,实际根据业务场景调整。
五、真实面试回放 🟡
面试官:商品详情接口响应慢,怎么排查?
候选人(小陈):分四步走。
第一步:看是哪个环节慢。用SkyWalking看链路,看Span耗时。
第二步:如果数据库慢,用EXPLAIN分析查询,看有没有索引。
第三步:如果远程调用慢,看超时配置和下游服务响应时间。
第四步:如果代码本身慢,分析代码逻辑,看有没有循环、递归等。
面试官:如果商品详情页依赖的图片加载慢,导致整体页面慢,怎么优化?
小陈:三个方向:
一是接口设计:商品详情接口不应该包含图片加载,图片应该在前端懒加载。
二是缓存图片:把热门商品图片缓存到CDN或Nginx,减少回源。
三是接口降级:如果图片加载超时,返回默认图,不要阻塞页面渲染。
面试官:怎么判断是数据库问题还是代码问题?
小陈:两个方法:
一是用链路追踪,看数据库Span耗时占总耗时的比例。
二是加日志,在每个关键步骤打印耗时。
如果数据库耗时占了大部分,比如总耗时500ms,数据库占了400ms,就是数据库问题。
【面试官手记】
小陈这场面试的亮点:
-
排查思路清晰:链路追踪 → 数据库分析 → 代码分析
-
知道接口设计问题:图片不应该阻塞主接口
-
知道降级方案:图片超时返回默认图
-
知道判断标准:数据库耗时占比
接口响应慢是P6工程师必备技能,能完整回答的候选人,说明有性能优化经验。
接口响应慢排查的核心是链路思维 + 逐层拆解。记住四步排查法:
- 链路追踪:看是哪个Span慢
- 数据库分析:EXPLAIN看索引
- 远程调用分析:看超时配置
- 代码分析:看算法和循环
优化目标:P99 < 业务要求的SLA。