CPU使用超过阈值分析

发现如下现象:集群中某POD的CPU 持续 90%+,响应变慢。迅速进行资源隔离,使该POD不在注册中心注册,流量将不进入此POD进行处理。随后,进行细致的排查。

1. 排查过程

1.1 定位高 CPU 进程

java 复制代码
# 查看整体 CPU 使用情况
top

# 按 CPU 排序(默认即按 CPU 降序)
# 查看具体进程 PID、%CPU、COMMAND

# 查看线程级 CPU 使用(定位到具体线程)
top -H -p <PID>

# 或使用 ps
ps -eo pid,ppid,cmd,%cpu --sort=-%cpu | head

1.2 jstack 抓取线程栈

jstack 是 JDK 自带的一个命令行工具,用于打印指定 Java 进程的线程堆栈信息(即 thread dump),常用于分析 CPU 占用高、死锁、线程阻塞等问题。

java 复制代码
# 12345即PID
jstack -l 12345 > jstack.log

-l:除了线程堆栈,还会打印关于锁的附加信息(如持有/等待的 monitor 和 synchronizers),建议始终加上。

输出重定向到文件便于后续分析。
注意:需要以运行该 Java 进程的相同用户身份执行(或 root),否则可能无权限。

抓取时机建议

CPU 高时:连续抓取 2~3 次,每次间隔 5~10 秒,对比哪些线程始终处于 RUNNABLE 状态,可能是热点代码。

可手动,也可用如下脚本:

java 复制代码
for i in {1..3}; do
  echo "=== Dump $i at $(date) ===" >> cpu_high_jstack.log
  jstack -l 12345 >> cpu_high_jstack.log
  sleep 10
done

自动化抓取脚本:

java 复制代码
#!/bin/bash
PID=$1
DURATION=${2:-30}
INTERVAL=5

for i in $(seq 1 $((DURATION / INTERVAL))); do
  echo "[$(date)] Capturing jstack ($i)" >> jstack_auto.log
  jstack -l $PID >> jstack_auto.log
  sleep $INTERVAL
done

1.3 jstack文件分析

一般需要通过线程PID的十进制,转化为16进制,以此在jstack文件中搜索

java 复制代码
# 查看进程中哪个线程 CPU 最高
top -H -p 12345

# 假设发现线程 PID(实际是 LWP ID)为 12345(十进制)
# 转换为 16 进制(jstack 中 nid 是 16 进制)
printf "%x\n" 12345  # 输出 3039

# 在 jstack.log 中搜索 nid=0x303e
grep -A 20 "nid=0x303e" jstack.log

关键字段解读(线程状态)

状态 含义 常见场景
RUNNABLE 正在运行或可运行(在 CPU 上或就绪队列) CPU 密集型操作、死循环
BLOCKED 阻塞中,等待获取 monitor 锁 多线程竞争 synchronized
WAITING 无限期等待其他线程执行特定操作 Object.wait(), Thread.join(), LockSupport.park()
TIMED_WAITING 有超时的等待 Thread.sleep(), Object.wait(1000)
TERMINATED 已终止 一般不会出现在 dump 中

1.4 典型问题示例

  1. CPU 占用高
    查找多个 RUNNABLE 线程,且堆栈重复出现在相同方法(如 HashMap.get、正则处理、加密计算等)。可能因非线程安全的 HashMap 在并发下形成环形链表,导致死循环。
java 复制代码
"http-nio-8080-exec-10" #10 prio=5 os_prio=0 tid=0x00007f8a4c00d000 nid=0x3039 runnable [0x00007f8a2d3d2000]
   java.lang.Thread.State: RUNNABLE
      at java.util.HashMap.get(HashMap.java:569)
      at com.example.service.CacheService.getValue(CacheService.java:45)
  1. 死锁
    jstack 会在 dump 末尾自动检测并打印:
    典型的循环等待,需修改加锁顺序。
java 复制代码
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8a2c00a123 (object 0x000000076b8a1234, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x00007f8a2c00b456 (object 0x000000076b8a5678, a java.lang.Object),
  which is held by "Thread-1"
  1. 线程池耗尽
    大量线程处于 WAITING(如 Tomcat 的 http-nio-xxx 线程全在 park),但请求堆积。
    可能原因:下游依赖慢(DB、RPC)、线程池配置过小。

2. 原因分析

在执行top -H -p 12345命令之后,出现如下结果,显示c2 CompilerThread和lettuce-eventEx共同占用CPU超过90%。

java 复制代码
top - 10:15:32 up 20 days,  3:42,  2 users,  load average: 8.75, 7.20, 6.80
Threads: 128 total,   4 running, 124 sleeping,   0 stopped,   0 zombie
%Cpu(s): 92.3 us,  2.1 sy,  0.0 ni,  5.1 id,  0.2 wa,  0.0 hi,  0.3 si,  0.0 st
MiB Mem :  16384.0 total,   2100.5 free,   9876.3 used,   4407.2 buff/cache
MiB Swap:   2048.0 total,   1800.0 free,    248.0 used.   5600.1 avail Mem

    PID    USER    PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  22345    app     20   0 8567808 3.2g  25664 R  48.2   20.1   5:23.41 C2 CompilerThre
  12346    app     20   0 8567808 3.2g  25664 R  45.7   20.1   4:56.12 lettuce-eventEx
  12347    app     20   0 8567808 3.2g  25664 S   0.3   20.1   0:12.05 GC Thread#0
  12348    app     20   0 8567808 3.2g  25664 S   0.0   20.1   0:05.33 VM Thread
  12349    app     20   0 8567808 3.2g  25664 S   0.0   20.1   1:02.77 http-nio-8080-e

2.1 jstack文件分析

在jstack文件中发现出现PID的22345对应的十六进制ox5749处出现BLOCKED,是否证明发生阻塞一直持续CPU占用90%?

这个是难以确定的,jstack 中出现 BLOCKED 状态的线程 通常不会导致 CPU 占用高达 90%,反而更常见于 CPU 使用率较低但系统响应慢 的场景。

BLOCKED 线程本身几乎不消耗 CPU ,因为它们处于"等待锁 "的挂起状态,不会在 CPU 上执行指令。

所以,如果系统 CPU 持续 90%+,而你只看到大量 BLOCKED 线程,那高 CPU 的"真凶"大概率是其他 RUNNABLE 线程

  • BLOCKED 是 Java 线程在 等待 synchronized 锁 时的状态。
  • 此时线程被操作系统挂起(进入阻塞队列),不参与
  • CPU 调度。 它既不执行代码,也不轮询(不像自旋锁),因此 CPU 占用接近 0%。

高 CPU(90%+)的原因:一般由一个或多个 RUNNABLE 线程引起(如死循环、正则回溯、频繁 GC、加密计算等)。

2.2 C2 CompilerThread

C2 CompilerThread 和 C1 CompilerThread 是 HotSpot JVM 中 JIT(Just-In-Time)编译器的两个核心组件,它们共同负责将 Java 字节码动态编译为高效的本地机器码,以提升运行时性能。二者在优化级别、适用场景和资源消耗上有显著区别。

Java 程序启动时,JVM 默认以解释模式执行字节码(慢)。

当某段代码被频繁执行(称为"热点代码"),JVM 会触发 JIT 编译,将其编译为平台相关的本地机器码(快)。

在 JDK 8 及以后,默认启用 分层编译(Tiered Compilation),即:
解释执行 → C1 编译 → C2 编译,逐级优化。
关键区别:

对比项 C1 Compiler C2 Compiler
别名 Client Compiler Server Compiler
优化目标 快速启动 + 适度性能 最大化吞吐量
编译速度
代码质量 中等 极高
资源消耗 低 CPU / 内存 高 CPU / 内存
适用应用 桌面应用、短生命周期程序 服务器、长期运行服务
是否默认启用 分层编译下会使用 分层编译下会使用
JDK 默认行为 JDK 8+ 默认开启 分层编译(C1 → C2)

1. 影响原因1:内存

通常不会直接导致。

若因内存不足引发 频繁 GC → 应用行为异常 → 热点代码突变 → 触发更多 JIT 编译,则可能间接加剧 C2 负载。

编译一个方法可能消耗几 MB 内存,但现代服务器通常有足够 native 内存。

即使堆只有 512MB,只要 CodeCache 和 native 内存充足,C2 仍可正常工作。

先看 GC 日志!如果 GC 正常,则问题在代码/JIT 本身,而非堆大小。

java 复制代码
jstat -gc 12345

执行结果:

java 复制代码
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
512.0  512.0   0.0   512.0   2048.0   1980.2    204800.0   85632.4  120832.0 115200.5 14592.0 13824.3   42    2.120   0      0.000    2.120
512.0  512.0 512.0    0.0    2048.0   2010.5    204800.0   86100.1  120832.0 115210.2 14592.0 13825.0   43    2.170   0      0.000    2.170
512.0  512.0   0.0   512.0   2048.0   1995.8    204800.0   86520.7  120832.0 115215.6 14592.0 13825.8   44    2.220   0      0.000    2.220

字段含义详解

列名 含义 说明
S0C / S1C Survivor 0/1 容量 当前 Survivor 区大小(KB)
S0U / S1U Survivor 0/1 已使用 总有一个为 0(G1 中可能不严格交替)
EC / EU Eden 区容量 / 已使用 高频对象分配区,快满时触发 Young GC
OC / OU Old 区容量 / 已使用 长期存活对象区域,接近 OC 可能触发 Mixed GC 或 Full GC
MC / MU Metaspace 容量 / 已使用 存放类元数据(非堆)
CCSC / CCSU Compressed Class Space 容量 / 使用 用于压缩类指针(64 位 JVM)
YGC Young GC 次数 自启动以来 Young GC 总次数
YGCT Young GC 总耗时(秒) 如 2.120 表示共花了 2.12 秒
FGC Full GC 次数 关键指标!应尽量为 0
FGCT Full GC 总耗时 若 >0 且增长,需警惕
GCT 所有 GC 总耗时 YGCT + FGCT

2. 影响原因2:频繁编译

执行此命令,查看哪个线程频繁编译

java 复制代码
jstat -printcompilation 12345 1000

12345:Java 进程 PID(这里仅为示例)

1000:每隔 1000 毫秒(1 秒)输出一次

-printcompilation:显示 最近被 JIT 编译的方法

java 复制代码
Compiled  Size  Type Method
   12345     89    4 java/util/HashMap get
   12346    156    4 com/example/service/OrderService processOrder
   12347    210    4 io/lettuce/core/protocol/CommandHandler channelRead
   12348     64    3 java/lang/StringBuilder toString
   12349    302    4 com/example/cache/RedisCache getValue

字段含义详解

列名 含义 说明
Compiled 编译任务 ID 自增编号,唯一标识一次编译
Size 字节码大小(字节) 被编译方法的字节码长度
Type 编译类型 * 3 = C1 编译(Client) * 4 = C2 编译(Server)
Method 方法全限定名 格式:包/类 方法名

2.3 lettuce-eventExecutor

Lettuce 是基于 Netty NIO 的 Redis 客户端。

lettuce-eventExecutor 实际是 Netty 的 EventLoop 线程,负责:

处理 Redis 连接的网络 I/O(读/写)

解码 Redis 响应

触发命令回调(CompletableFuture)

心跳、重连、超时处理

关键特性:

默认单线程处理所有连接事件(即使你有多个 Redis 实例,也可能共享同一个 EventLoop)

必须快速完成任务,否则会阻塞整个事件循环 → 导致 CPU 100% + 命令堆积

解决思路:

  1. 检查 Redis 服务端健康度
  2. 排查大 Key / 高频请求
  3. 确保不在回调中阻塞
  4. 升级 Lettuce/Netty

如果使用 Lettuce / Jedis 操作 Redis 的 Bitmap(位图),会调用:

java 复制代码
redis.setbit("user:login", 1000, true);

发现如下堆栈:

java 复制代码
"http-nio-8080-exec-5" #25 prio=5 os_prio=0 tid=... nid=... runnable
   java.lang.Thread.State: RUNNABLE
      at com.example.service.UserActivityService.recordLogin(UserActivityService.java:45)
      at io.lettuce.core.RedisAsyncCommandsImpl.setbit(RedisAsyncCommandsImpl.java:XXX)
      ...

为什么 setBit 相关线程会是 RUNNABLE 且高 CPU?

正常情况(短暂)

单次 SETBIT 命令很快(Redis 本身 O(1)),线程短暂 RUNNABLE 属正常。

异常情况(持续高 CPU)

以下场景会导致 setBit 调用成为热点,线程长期 RUNNABLE:

场景 1:高频批量 SETBIT 操作

例如:每秒处理 10 万用户登录,每个用户执行一次 SETBIT

结果:

业务线程池(如 Tomcat http-nio-*)持续忙碌

Lettuce 的 eventExecutor 也因大量命令而高 CPU(见你之前的问题)

场景 2:在循环中调用 setBit(无批处理)

java 复制代码
// 反模式:逐个设置位
for (int day = 0; day < 365; day++) {
    redis.setbit("user:" + userId, day, isActive[day]);
}

→ 365 次网络往返 + 365 次命令解析 → CPU 和网络双高

场景 3:操作超大 offset(如 offset = 10亿)

Redis 的 Bitmap 是按需分配内存的,但 offset 极大时:

Redis 需要分配巨大内存(10^9 / 8 / 1024 / 1024 ≈ 120MB)

客户端序列化/反序列化耗时增加

可能触发 Full GC 或网络阻塞

场景 4:同步调用 + 高并发

使用 redis.setbit(...).get()(阻塞等待)在高并发下:

大量线程阻塞在 CompletableFuture.get()

但发起调用的线程仍是 RUNNABLE(直到被 park)

2.3 汇总分析

以上两者,通常不是两个独立问题,而是相互关联的性能风暴。根本原因往往是:Lettuce 高频/异常操作触发大量新代码路径 → JVM 将其识别为热点 → C2 编译器疯狂优化 → 双线程 CPU 飙升。

线程 角色 CPU 高的原因
lettuce-eventExecutor Netty 事件循环线程,处理 Redis 网络 I/O、命令编解码、回调 - 高频 Redis 请求 - 大 Value 传输 - Redis 异常(重连/超时) - 回调中执行耗时操作
C2 CompilerThread HotSpot 的 Server JIT 编译器,对热点代码做深度优化 - 新热点方法激增(如 Lettuce 内部类、Codec、回调逻辑) - Deoptimization(逆优化)频繁 → 反复编译

二、典型场景分析

场景 1:突发流量 + Redis 批量操作

应用启动或促销活动导致:

每秒数万次 SETBIT / GET / MGET

Lettuce 事件线程持续处理网络事件(RUNNABLE)

这些操作涉及:

Lettuce 内部 CommandHandler, RedisStateMachine, 自定义 RedisCodec

这些方法首次成为热点 → C2 开始编译

结果:Lettuce 线程 + C2 线程同时吃满 CPU

场景 2:操作大 Key 或复杂序列化

例如:GET 一个 10MB 的 JSON 字符串

Lettuce 需要:

从 Socket 读取大量字节

调用你的 StringCodec.decode()(可能含 JSON 解析)

如果 decode() 是新路径 → C2 编译它

结果:Lettuce 线程忙于 I/O + C2 忙于编译 codec

场景 3:Redis 服务端异常 → 事件风暴

Redis 宕机/网络抖动 → Lettuce:

不断触发 exceptionCaught

执行重连逻辑

调用大量超时回调(CompletableFuture.completeExceptionally)

这些异常路径在正常运行时从未执行 → 成为"新热点"

C2 开始编译这些冷路径 → CPU 双高

场景 4:动态生成 Lambda / 匿名类(在回调中)

java 复制代码
redis.get("key").thenAccept(value -> {
    // 每次创建新的 Lambda 类!
    process(value);
});

高频调用 → JVM 加载大量 LambdaForm$MH 类

C2 需要编译每个新类的方法

Lettuce 线程忙于触发回调

结果:Metaspace 增长 + C2 + Lettuce 双高 CPU

四、解决方案

  1. 优化 Redis 使用方式
    批量操作:用 Pipeline 合并多个 SETBIT
java 复制代码
connection.setAutoFlushCommands(false);
for (int offset : offsets) {
    commands.setbit(key, offset, 1);
}
connection.flushCommands();

避免大 Key:Bitmap 按天/月分 key(如 user:login:202504)

设置合理超时:防止重试风暴

  1. 避免在回调中创建动态类

缓存 Lambda 或改用静态方法引用:

java 复制代码
// 不好
redis.get(k).thenAccept(v -> process(v));

// 好
redis.get(k).thenAccept(MyService::process);
  1. 简化 Codec 逻辑
    自定义 RedisCodec 避免 JSON 解析等重操作
    或将解码移出 EventLoop:
java 复制代码
redis.get(key).thenApplyAsync(this::decodeJson, businessPool);
  1. JVM 调优(临时缓解)
bash 复制代码
# 限制 C2 线程数(默认根据核数计算)
-XX:CICompilerCount=2

# 延迟 C2 编译(适合短期任务)
-XX:TieredStopAtLevel=1
  1. 升级依赖
    Lettuce ≥ 6.2.0
    Netty ≥ 4.1.80.Final
    JDK ≥ 17(JIT 更稳定)

3. 总结

分析过程如上,应迅速止血,隔离问题POD,保证应用可用性。

相关推荐
特种加菲猫1 小时前
透过源码看本质:list 的模拟实现与核心原理
开发语言·c++
李日灐1 小时前
改造红黑树实现封装 map/set:感受C++ 标准容器的精妙设计与底层实现
开发语言·数据结构·c++·后端·算法·红黑树
故事和你912 小时前
sdut-程序设计基础Ⅰ-期末测试(重现)
大数据·开发语言·数据结构·c++·算法·蓝桥杯·图论
Heo2 小时前
这才称得上是提示词工程!
java·架构·代码规范
重庆兔巴哥2 小时前
如何在Dev-C++中使用MinGW-w64编译器?
linux·开发语言·c++
魔道不误砍柴功2 小时前
Java Function 高级使用技巧:从工程实战中来
java·开发语言·python
三佛科技-187366133972 小时前
LP3783A芯茂微5V2.1A低功耗原边反馈充电器芯片替代PL3378/C
c语言·开发语言
不知名。。。。。。。。2 小时前
仿muduo库实现高并发服务器----EventLoop与线程整合起来
java·开发语言·jvm
编程大师哥2 小时前
JAVA 集合框架进阶
java·开发语言