一次 API 性能排查:从 800ms 到 90ms
接口从 200ms 涨到 800ms,监控上没有报错,CPU 也不高。这种问题最容易让人凭直觉改代码,但真正有效的做法只有一个:先量化,再定位。
这篇记录一次典型的接口性能排查过程。
现象:慢,但不知道慢在哪
一个商品详情接口,平均耗时 800ms,P99 超过 1.5s。调用方开始超时重试,数据库连接数随之上涨。
先别急着看业务代码。用 curl 打几次,把时间拆开:
curl -o /dev/null -s -w \ |
结果很稳定:网络和 TLS 都正常,时间几乎全部花在服务端处理上。问题被缩小到了应用内部。
第一步:让调用链说话
给接口加上分段计时,或者直接看 APM 的 span。这个案例里,一次请求里出现了 1 次主查询 + 3 次关联查询 + 1 次远程调用,其中三次关联查询是串行的。
伪代码大概是:
product = repo.get_product(product_id) |
单看每一步都不算慢,但串起来、再乘以并发,就成了瓶颈。
第二步:把串行变并行
没有依赖关系的查询,没有必要排队。用线程池或异步任务把三次独立调用并发出去:
with ThreadPoolExecutor(max_workers=3) as pool: |
耗时从相加变成取最大值,理论收益大约是 2/3。但要给线程池设置合理的队列和超时,避免把下游打垮。
第三步:检查 SQL 和索引
并发解决的是等待,数据库本身的慢查询还得单独处理。打开慢查询日志后,发现关联查询里有一张表走了全表扫描。
-- 优化前:status 区分度很低,优化器放弃索引 |
两个原则:
- 组合索引的顺序:区分度高的列放前面,但也要结合查询模式。
- 避免
SELECT *:减少回表、网络传输和序列化成本。
第四步:给热点数据加缓存
商品详情是典型读多写少。把商品基础信息、店铺信息和评论摘要放进 Redis,设置 5 分钟过期,并在更新时主动删除缓存。
cache_key = f"product:detail:{product_id}" |
缓存的关键不是“加上”,而是想清楚:什么时候失效、缓存穿透怎么办、数据不一致能接受多久。
结果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均耗时 | 800ms | 90ms |
| P99 | 1.5s | 210ms |
| 数据库 QPS | 偏高 | 下降约 60% |
复盘
这次排查没有用到什么高深技巧,关键步骤是:
- 先把“慢”拆成可度量的时间段;
- 用调用链找到真正的等待点;
- 能并行的并行,能缓存的缓存,该建索引的建索引;
- 用数据验证,而不是凭感觉。
性能优化不是把代码写得更复杂,而是让每一次等待都有明确的理由。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 小鱼技术栈!