性能瓶颈分析:CPU密集型 vs IO密集型、内存泄漏排查、GC调优、数据库连接池优化
各位同学,咱们今天聊点硬核的。性能瓶颈分析,说白了就是给系统“看病”。我做了这么多年风控,见过太多系统“死”得不明不白。有的CPU飙到100%,有的内存泄漏像水管漏水,还有的数据库连接池直接罢工。嗯,今天我就把这几类典型问题,掰开揉碎了讲清楚。
一、CPU密集型 vs IO密集型:你得先分清“敌人”是谁
我个人习惯,拿到一个性能问题,第一件事不是看代码,而是先判断:这到底是CPU累,还是IO累?你想想看,方向错了,调优就是白费劲。
1. CPU密集型
说白了,就是计算太多。比如风控里的复杂规则引擎、大量加解密、模型推理。这类任务,CPU一直处于“忙死”状态,上下文切换少,但每个任务都占着CPU不放。
典型特征:
- CPU利用率持续 > 90%
- 线程数不多,但每个线程都在疯狂计算
- 系统响应时间随并发数增加而线性增长
我在项目中遇到过,一个实时风控规则引擎,每次请求要跑200多条规则。上线后CPU直接拉满。怎么解?
- 减少计算量:规则预编译、缓存中间结果
- 并行计算:用ForkJoinPool把规则拆成子任务
- 升级硬件:换更高主频的CPU,或者增加核心数
2. IO密集型
这类问题更隐蔽。系统看起来CPU不高,但就是慢。为什么?因为线程都在等IO——等数据库、等Redis、等外部接口。
我的经验:IO密集型系统的CPU利用率通常只有20%-40%,但线程数可能上千。这时候加CPU没用,得加线程池大小。
我曾经调过一个风控系统,每次请求要查5个外部数据源。IO等待时间占了总耗时的80%。优化方案:
- 异步化:用CompletableFuture并行发起IO请求
- 连接池调大:让更多线程同时等待IO
- 缓存:对高频查询做本地缓存
二、内存泄漏排查:别等OOM了才后悔
内存泄漏,我愿称之为“慢性毒药”。系统跑着跑着,内存一点点涨上去,最后OOM。嗯,这里要注意,OOM不一定是内存泄漏,也可能是内存真的不够用。但泄漏,一定是代码的锅。
排查工具链
| 工具 | 用途 | 我的用法 |
|---|---|---|
| jstat | 查看GC情况 | 先看老年代是不是一直在涨 |
| jmap | dump堆内存 | OOM时自动dump,或者手动触发 |
| MAT / JProfiler | 分析dump文件 | 找GC Root引用链,定位泄漏点 |
避坑指南:我曾经在生产环境直接jmap dump,结果系统卡死了一分钟。后来我学乖了,用jmap -dump:live只dump存活对象,或者用-XX:+HeapDumpOnOutOfMemoryError自动触发。
常见泄漏场景
- ThreadLocal未清理:线程池里的线程复用,ThreadLocal里的对象一直不被回收。我见过一个风控系统,每次请求往ThreadLocal里塞用户信息,结果内存里堆了几十万个用户对象。
- 静态集合类:HashMap、ArrayList定义为static,只增不减。说白了就是“只吃不吐”。
- 资源未关闭:数据库连接、文件流、网络连接,用完不close。这类问题在try-catch里特别常见。
三、GC调优:别让垃圾回收成了“垃圾”
GC调优,很多人一上来就调各种参数。我个人建议,先搞清楚GC在干什么。说白了,GC就是帮我们打扫内存垃圾。但如果打扫得太频繁,或者打扫一次太久,系统就卡了。
GC调优的核心指标
- 吞吐量:应用运行时间 / (应用运行时间 + GC时间)。我一般要求 > 99%
- 暂停时间:每次GC导致应用停顿的时间。实时风控系统,我要求 < 50ms
- GC频率:Young GC每秒不超过1次,Full GC每10分钟不超过1次
我的调优步骤
- 先看日志:加上-XX:+PrintGCDetails -XX:+PrintGCDateStamps,分析GC日志
- 确定目标:是吞吐量优先,还是低延迟优先?风控系统,我选低延迟
- 选GC器:JDK 8用G1,JDK 17+用ZGC。我最近在项目里用ZGC,暂停时间基本在1ms以内
- 调参数:比如-XX:MaxGCPauseMillis=50,-XX:G1HeapRegionSize=4m
一个真实案例:我之前调一个风控系统,Full GC每3分钟一次,每次暂停2秒。分析发现,老年代里有个巨大的HashMap,存了所有用户的规则缓存。解决方案:把缓存拆成多个小Map,并设置过期时间。Full GC降到了每30分钟一次。
四、数据库连接池优化:别让连接成了瓶颈
数据库连接池,看着简单,坑却不少。你想想看,连接池太小,请求排队;连接池太大,数据库扛不住。我见过一个系统,连接池设了200,结果数据库连接数打满,直接挂了。
连接池大小怎么定?
有个经典公式:连接数 = (CPU核心数 * 2) + 有效磁盘数。但这是理论值。实际项目中,我一般这样算:
- 先压测,看单个连接的QPS
- 再算目标QPS,反推连接数
- 留20%的余量
| 场景 | 推荐连接数 | 说明 |
|---|---|---|
| 纯查询,无事务 | 10-20 | 连接复用率高 |
| 混合读写,短事务 | 20-50 | 根据TPS调整 |
| 长事务,复杂查询 | 5-10 | 连接占用时间长,不宜过多 |
HikariCP的配置经验
我现在基本只用HikariCP。它的性能确实好。几个关键参数:
- maximumPoolSize:别超过数据库最大连接数的80%
- minimumIdle:建议等于maximumPoolSize,避免频繁创建连接
- connectionTimeout:30000ms,超过就报错,别让请求死等
- maxLifetime:1800000ms(30分钟),比数据库的wait_timeout短一点
我的小技巧:在HikariCP里加上leakDetectionThreshold参数,设为60000ms。如果连接被占用超过60秒,日志里会打印堆栈信息。我曾经靠这个参数,抓到一个在事务里调外部接口的坑。
知识体系总览
下面这张图,是我自己总结的性能瓶颈分析框架。你照着这个思路走,基本不会跑偏。
好了,这一章的内容就到这里。性能瓶颈分析,说白了就是“望闻问切”。先分清楚是CPU还是IO,再查内存有没有泄漏,然后调GC和连接池。每一步都有章可循。你按照这个框架去排查,大部分问题都能找到根因。
公众号:蓝海资料掘金营,微信deep3321