
我不知道你有没有类似的经历:一个接口刚上线的时候飞快,RT 只有 20~30ms,大家都说「稳得很」。结果一个季度过去,业务又多了点逻辑,数据库表又多了点字段,Redis 键多了三倍,接口忽然变成 120ms、180ms、甚至偶尔超时。
我们团队前几年就遇到过这种情况,某个用户画像接口,最开始 QPS 500,毫无压力;半年后业务膨胀,峰值 QPS 800 时直接把线程池干满,CPU 从 30% 干到 85%,接口开始出现毛刺一样的 RT 抽风。排查了两天一夜,最后发现是三个问题叠加:
我那时才意识到:接口不是「写多了变慢」,而是「每一次小优化缺失,都会变成系统级灾难」。
下面这些点,基本都是我一次次被线上教做人总结出来的。
很多人排查问题时第一步就是看日志,没错,日志重要,但它只能告诉你「结果」,很难告诉你「原因」。
有一次线上报警,说某个下单接口 RT 飙到了 600ms,我们翻了两小时日志,只看到:
order created success, cost=612ms屁用没有。
后来我直接在接口加了最原始、最土但最有效的方式——分段计时:
long t1 = System.currentTimeMillis();
User user = userService.getUser(uid);
long t2 = System.currentTimeMillis();
Order order = orderService.create(req);
long t3 = System.currentTimeMillis();
logger.info("user={}, userCost={}ms, orderCost={}ms",
uid, (t2 - t1), (t3 - t2));一分钟后指标出来了:
罪魁祸首终于找到了。
所以排查性能问题,有三个绝对不能省的手段:
很多人光靠“感觉”排查性能问题,其实是在玩火。
数据库慢查询永远是性能问题的前排,但很多时候不是 SQL 写得烂,而是“业务不知不觉把 SQL 弄烂了”。
有一次我们一个“用户搜索”接口突然变慢,从原来的 50ms 飙到 300ms+,CPU 居高不下。
后来查 SQL:
SELECT * FROM user WHERE name LIKE '%abc%';是的,就是这个 %keyword%。
最骚的是,这个接口被一个新业务用了,每分钟调用 2000 次,它直接把数据库 I/O 打穿。
LIKE '%xxx%'EXPLAIN 一下就知道尤其是 N+1 查询,它比你想得更阴险:
List<User> users = userMapper.selectAll();
for (User u : users) {
u.setOrders(orderMapper.selectByUid(u.getId()));
}这是我见过的最杀性能的写法之一,接口卡顿直接从 40ms → 300ms。
大多数人都遇到过这种情况:
你自己的接口都优化得飞起,结果第三方服务一顿卡,就把你拖死。
我们之前对接某个支付渠道,他们有时稳定、但一旦波动,RT 可以从 80ms → 2000ms。然后你本地线程池瞬间被打爆。
很多人不知道 HttpClient 或 OkHttp 默认超时是多少。
比如:
OkHttpClient client = new OkHttpClient(); // 默认 10 秒超时十秒?你的接口能撑这么久吗?
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(300, TimeUnit.MILLISECONDS)
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();同时搭配重试策略:
你救不了第三方服务,但你能救自己的线程池。
你可能不信,但很多接口的瓶颈根本不是数据库,也不是网络,而是 —— JSON 反序列化。
之前我们做一个导出接口,处理 200 条记录,数据库耗时 30ms,结果序列化耗时 80ms。我们当时傻了。
尤其是:
这些都是性能杀手。
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);更狠一点:
使用 JSON-B、Fastjson2、Kryo 做内部对象序列化(不是对外接口)。
线程池是线上最容易被忽略、却最能搞出事故的点。
我之前有一次看到线程池队列长度从 0 一路飙到 500,接口 RT 从 30ms → 2000ms。
原因是线程池写得太“佛系”:
new ThreadPoolExecutor(
10, 50,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), // 无限队列!排队排死你
);因为线程池不会拒绝请求,而是疯狂排队。
用户感觉就是:怎么接口突然变慢了?
其实你的线程在排队排得要死。
new ThreadPoolExecutor(
20,
50,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy()
);至少一旦队列满了,你能在日志里看到拒绝信息,而不是默默变慢。
GC 是性能问题中的“隐形刺客”。
大多数人都只看 RT,不看 GC。
我们那次排查接口抖动,从 20ms 偶尔飙到 400ms。看了两天都没找出来,最后是 GC 日志给了我答案:
[GC pause (G1 Evacuation Pause) 92ms]接口延迟的 92ms 就这么来了。
因为 Stop-The-World。
哪怕只停 50ms,用户都能感知到。
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xms4g
-Xmx4gXms = Xmx 是非常关键的,因为堆大小动态扩容本身也会触发停顿。
曾经我见过有人在循环里 new 对象:
for (...) {
new BigDecimal("0.00");
new SimpleDateFormat("yyyy-MM-dd");
}疯了。
尤其是:
有次我们一个接口创建了 18000 个临时对象,Young GC 频率从原来的 每 30 秒一次 → 每 3 秒一次,RT 波动特别明显。
你优化再多,也不能保证业务永远不爆炸,所以限流、降级、缓存,这些“稳态系统的基石”一定得提前想好。
我们线上曾遇到一个 Redis 热 Key,TPS 4000,多节点 Redis 直接被压死。
后来我们通过以下方式解决:
hotkeys)比如:
RateLimiter limiter = RateLimiter.create(1000); // 每秒 1000 个请求否则冲击瞬间接口就被打穿。
背景:用户权限计算接口
QPS:700
原来的 RT:40ms
突然报警:RT 均值 350ms,最大 1.2s。
最终 RT 回到 50ms 左右。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。