单核CPU打满本质是缓存线程高频空转或死循环,需按“确认单核满载→定位缓存线程→分析栈帧→修复加固”四步排查:先用top -1/mpstat确认某核%usr持续近100%,再用top -H/ps -eo tid,pid,comm,%cpu找高CPU缓存相关线程,接着将TID转十六进制后用jstack/pstack定位空轮询、无限重试或错误刷新逻辑,最后通过设select超时、加退避重试、熔断refresh等措施根治。

高并发压测下缓存管理进程导致单核CPU打满,本质是某个缓存相关线程在该核心上陷入高频空转或死循环,而非整体负载过高。解决的关键在于快速定位、精准干预、分层加固。
第一步:确认是否真为单核打满
不要只看整体CPU使用率。执行以下命令观察各核心实时状态:
- top → 按数字键 1:切换显示所有CPU核心,找持续显示99%–100%的某一列(如 CPU0)
- mpstat -P ALL 1:每秒输出各核详细使用率,重点关注 %usr(用户态) 是否长期接近100%,而非 %wa(IO等待)或 %sy(内核态)
若仅一核飙高而其他核空闲,基本可锁定为某缓存线程(如 Redis 客户端轮询、本地缓存刷新、布隆过滤器校验逻辑)在该核上异常占用。
第二步:定位高CPU缓存线程
用线程视角聚焦缓存相关进程:
- top -H:开启线程模式,按 P 键按CPU排序,记下顶部 TID(线程ID)
- ps -eo tid,pid,comm,%cpu --sort=-%cpu | head -10:直接列出CPU最高的前10个线程及对应进程名
- 重点筛查进程名含 redis、cache、netty、lettuce、jedis、guava、caffeine 的线程
确认该线程属于缓存客户端、本地缓存刷新任务,还是自研缓存代理进程。
第三步:分析线程栈,揪出根因代码
将 TID 转为十六进制后查调用栈:
- printf "%x\n" <TID>:例如 TID=17248 → 输出 4360
- 若为 Java 进程:jstack <PID> | grep -A30 "4360",查看是否卡在:
- 无限重试连接 Redis(如未设超时的 Jedis.connect())
- 空轮询 Selector(NIO 客户端未设 select(timeout) 导致忙等)
- 本地缓存 reload() 中的 while(true) + sleep(1) 但 sleep 被跳过
- 布隆过滤器 bitArray.get() 配合错误的 hash 循环逻辑
- 若为 PHP/Go 进程:pstack <PID> 或 gdb -p <PID> -ex "thread apply all bt" -ex quit,关注 redisClient.Do、Cache.Get、bloom.Contains 等调用点
第四步:针对性修复与加固
根据常见根因,落地以下措施:
-
修复 NIO 空轮询:所有基于 Selector 的缓存客户端(如 Netty Redis 客户端),必须设置 select 超时,例如
selector.select(50),避免 select() 无参或传 0 - 约束重试逻辑:Redis 连接/命令失败重试必须带退避(exponential backoff)和最大次数,禁用 while(true) { try { ... } catch {...} }
- 检查本地缓存刷新:Caffeine/Guava 的 refreshAfterWrite 若依赖外部 HTTP 查询,需加熔断和超时;避免在主线程同步触发 refresh()
-
规避 SecureRandom 阻塞:若缓存 Token 生成依赖 SecureRandom,改用非阻塞熵源(如
/dev/urandom)或预生成池化,避免高并发下熵枯竭卡住 - 增加缓存层健康探针:在网关或服务启动时主动探测 Redis 连通性、响应延迟、连接数,异常时自动降级到本地缓存或直连DB,防止故障扩散
修复后,建议在压测中开启 Arthas watch 监控关键缓存方法的耗时分布,或用 async-profiler 采样热点栈,验证单核打满是否消失。

















