一、CPU 只有 30%,接口为什么还是超时了
线上经常会遇到一种令人困惑的现象:
- 节点 CPU 使用率只有 30%;
- Pod 内存使用率不到 70%;
- JVM 没有发生 Full GC;
- 数据库连接池没有耗尽;
- 但接口 P99 延迟却从 200 毫秒上涨到了 2 秒。
这时继续盯着 CPU、内存和磁盘使用率,往往很难找到真正原因。
因为传统资源监控主要回答的是:
系统消耗了多少资源?
而业务真正关心的是:
任务因为拿不到资源,被迫等待了多长时间?
例如,一个容器被限制为 500m CPU。即使宿主机还有大量空闲 CPU,只要容器已经耗尽自己的 CPU 配额,容器内线程仍然可能被内核限流。
这时节点总体 CPU 使用率可能并不高,但容器里的请求已经开始排队。
在 Linux 节点上,Kubernetes 会通过 cgroup 应用容器的资源限制。CPU Limit 是一个硬上限,超过对应调度周期内的可用配额后,内核会暂停该 cgroup,直到下一个周期恢复;内存限制则通常通过内存回收和 OOM 机制执行。
这正是 PSI 要解决的问题。
二、PSI 关注的不是使用率,而是损失的执行时间
PSI 全称为 Pressure Stall Information,可以翻译为“资源压力停顿信息”。
Linux 内核使用 PSI 统计任务因为 CPU、内存或 I/O 资源竞争而无法继续推进的时间。
相比单纯的资源使用率,PSI 更接近业务实际感受到的卡顿。
当 CPU、内存或 I/O 出现竞争时,工作负载可能产生:
- 延迟尖刺;
- 吞吐量下降;
- 线程排队;
- 请求超时;
- 严重情况下触发 OOM。
PSI 的目标就是量化这些资源竞争给任务带来的时间损失。
传统监控和 PSI 的区别可以概括为:
| 指标类型 | 回答的问题 | 常见指标 |
|---|---|---|
| 资源利用率 | 资源已经用了多少 | CPU Usage、Memory Usage、Disk Throughput |
| 资源压力 | 任务因为资源不足等待了多久 | CPU PSI、Memory PSI、I/O PSI |
| 业务结果 | 用户最终感受到什么 | QPS、P95、P99、超时率 |
完整关系如下:
graph LR
A["请求进入系统"] --> B["应用线程开始处理"]
B --> C{"资源是否充足"}
C -->|是| D["线程继续执行"]
C -->|否| E["等待 CPU 内存或磁盘"]
E --> F["PSI 压力指标上升"]
F --> G["线程排队和吞吐下降"]
G --> H["P99 延迟和超时率上升"]
D --> I["请求正常完成"]
传统监控经常直接从资源使用率跳到业务延迟,中间缺少了“任务究竟等待了多久”这一层。
PSI 正好补上了这部分观测能力。
三、如何读取 Linux PSI
Linux 会在 /proc/pressure 目录下暴露系统级 PSI 数据。
ls -l /proc/pressure
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io
典型输出如下:
$ cat /proc/pressure/cpu
some avg10=8.24 avg60=2.10 avg300=0.48 total=183924022
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
$ cat /proc/pressure/memory
some avg10=0.15 avg60=0.08 avg300=0.03 total=9823456
full avg10=0.02 avg60=0.01 avg300=0.00 total=113251
$ cat /proc/pressure/io
some avg10=1.36 avg60=0.72 avg300=0.31 total=88346711
full avg10=0.21 avg60=0.10 avg300=0.04 total=9387212
Linux 分别通过 /proc/pressure/cpu、/proc/pressure/memory 和 /proc/pressure/io 暴露 CPU、内存和 I/O 压力。
1. some 代表什么
some 表示在统计窗口内,至少有一个任务因为对应资源不足而发生停顿。
例如:
some avg10=8.24
表示最近 10 秒的移动统计中,有约 8.24% 的时间至少存在一个任务等待 CPU。
按照直观方式换算,大约相当于:
10 秒 × 8.24% = 0.824 秒
需要注意,这不是 CPU 使用率,也不是所有线程等待时间的简单累加。
它描述的是系统处于“至少有任务因为资源不足而无法推进”状态的时间比例。
2. full 代表什么
full 表示所有非空闲任务同时因为对应资源发生停顿。
这意味着整个工作负载都难以取得有效进展,严重程度通常高于 some。
对于内存和 I/O:
some上升,说明部分任务受到了影响;full上升,说明整个工作负载同时被资源压力拖住。
需要特别注意,系统级 /proc/pressure/cpu 的 full 没有明确定义。Linux 内核从 5.13 开始为了兼容继续暴露该字段,但系统级 CPU full 会保持为零。排查宿主机 CPU 压力时,应重点关注 cpu.some;在 cgroup 或容器级别,CPU full 仍可能具有实际值。
3. avg10、avg60 和 avg300
三个字段分别代表不同时间尺度的移动平均值:
| 字段 | 时间窗口 | 主要用途 |
|---|---|---|
avg10 |
最近 10 秒 | 发现突发竞争和短时抖动 |
avg60 |
最近 60 秒 | 判断问题是否持续 |
avg300 |
最近 5 分钟 | 判断是否存在长期容量不足 |
如果出现:
avg10=12.00
avg60=3.20
avg300=0.50
通常意味着刚刚发生了一次明显的资源竞争。
如果 avg10、avg60 和 avg300 都持续升高,则更可能是长期资源不足,而不是一次短暂毛刺。
4. total 代表什么
total 是累计停顿时间,单位为微秒。
它是一个持续递增的计数器,适合通过前后差值或者 Prometheus 的 rate()、increase() 函数观察压力变化。
Linux 同时提供 10 秒、60 秒、300 秒移动平均值和微秒级累计停顿时间。累计值可以帮助发现那些持续时间很短、没有明显反映在移动平均值中的延迟尖刺。
四、系统级 PSI 和容器级 PSI 不是一回事
/proc/pressure 反映的是整个操作系统的资源压力。
但在 Kubernetes 环境中,一个 Pod 可能因为自己的 cgroup 限制发生严重压力,而宿主机整体仍然非常空闲。
因此,仅查看:
cat /proc/pressure/cpu
只能说明节点整体 CPU 压力,并不能说明某个 Pod 是否因为 CPU Limit 被限流。
在 cgroup v2 中,每个 cgroup 目录都可以包含:
cpu.pressure
memory.pressure
io.pressure
这些文件使用与 /proc/pressure 相同的 PSI 格式,可以统计当前 cgroup 中任务受到的资源压力。
如果命令在容器内部执行,并且运行环境正确挂载了 cgroup v2,可以直接查看:
cat /sys/fs/cgroup/cpu.pressure
cat /sys/fs/cgroup/memory.pressure
cat /sys/fs/cgroup/io.pressure
如果从宿主机排查某个容器进程,则需要先找到该进程对应的 cgroup 路径。
PID="<容器在宿主机上的进程 PID>"
CGROUP_PATH=$(awk -F: '$1=="0" {print $3}' "/proc/${PID}/cgroup")
CGROUP_DIR="/sys/fs/cgroup${CGROUP_PATH}"
cat "${CGROUP_DIR}/cpu.pressure"
cat "${CGROUP_DIR}/memory.pressure"
cat "${CGROUP_DIR}/io.pressure"
不要直接在宿主机执行:
cat /sys/fs/cgroup/cpu.pressure
然后把结果当成某个容器的压力。
在宿主机上直接读取 cgroup 根目录,看到的通常是根 cgroup 或当前命名空间对应的统计结果,而不是目标 Pod 的数据。
五、CPU PSI:线程已经准备好了,但拿不到 CPU
CPU PSI 上升,表示任务已经处于可运行状态,但无法及时获得 CPU 时间。
常见原因包括:
- Pod 的 CPU Limit 设置过低;
- 节点部署了过多 CPU 密集型服务;
- Java 线程池配置过大;
- 批处理任务和在线服务混部;
- 节点 CPU 严重超卖;
- 邻居容器突然消耗大量 CPU;
- 大量线程争抢少量可用 CPU 配额。
CPU PSI 高并不一定意味着宿主机 CPU 使用率接近 100%。
例如,一个 16 核节点上的 Java 容器只配置了 500m CPU Limit。即使节点还有十几个空闲核心,容器在一个调度周期内耗尽自身配额后,仍然会被内核暂停。
Kubernetes 默认通过 CFS 配额执行 Pod 的 CPU Limit。达到配额后,容器会受到 CPU throttling,而不会因为 CPU 使用过高被终止。
排查时可以同时查看:
cat "${CGROUP_DIR}/cpu.pressure"
cat "${CGROUP_DIR}/cpu.stat"
cat "${CGROUP_DIR}/cpu.max"
cpu.stat 中常见字段包括:
usage_usec
user_usec
system_usec
nr_periods
nr_throttled
throttled_usec
nr_bursts
burst_usec
其中:
nr_periods表示已经经过的 CPU 配额周期数;nr_throttled表示发生过限流的周期数;throttled_usec表示累计被限流的时间;cpu.pressure表示任务实际受到 CPU 等待影响的程度。
这些字段由 cgroup v2 的 CPU 控制器提供。
可以建立如下初步判断:
| CPU PSI | CPU 限流数据 | 更可能的原因 |
|---|---|---|
| 高 | throttled_usec 快速增长 |
CPU Limit 过低或突发流量耗尽配额 |
| 高 | 限流数据基本不变 | 节点 CPU 竞争、线程过多或调度延迟 |
| 低 | 限流数据增长 | 存在限流,但暂未明显影响业务推进 |
| 低 | 限流数据稳定 | CPU 暂时不是主要瓶颈 |
需要注意,这只是排障方向,不是绝对结论。
CPU PSI 和 throttling 同时升高,可以强烈说明 CPU 配额正在影响应用,但仍应继续结合请求量、线程池队列、节点负载和上下文切换进行验证。
六、Memory PSI:没有 OOM,不代表内存没有问题
很多内存排障只关注两个指标:
- JVM 堆使用率;
- 容器是否出现 OOMKilled。
但在真正发生 OOM 之前,系统可能已经经历了大量页面扫描、内存回收和直接回收。
当应用申请内存时,如果内核无法快速提供可用页面,任务可能被迫参与回收过程。在这个阶段,进程虽然没有被杀死,却可能已经产生明显停顿。
对于 Java 服务来说,容器内存也不只有 Java Heap。
通常还包括:
- Metaspace;
- 线程栈;
- JIT Code Cache;
- JVM 内部原生内存;
- Direct Buffer;
- JNI 或原生库分配;
- 文件页缓存;
- Socket Buffer;
- 内核数据结构。
cgroup v2 会统计匿名内存、文件页缓存、内核数据结构和 TCP Socket Buffer 等多种内存,而不是只统计 JVM Heap。
因此可能出现:
JVM Heap 使用率:55%
容器内存使用率:88%
Memory PSI:持续升高
这时只查看 Java 堆,很容易得出“内存正常”的错误结论。
应继续检查:
cat "${CGROUP_DIR}/memory.current"
cat "${CGROUP_DIR}/memory.max"
cat "${CGROUP_DIR}/memory.high"
cat "${CGROUP_DIR}/memory.events"
cat "${CGROUP_DIR}/memory.stat"
cat "${CGROUP_DIR}/memory.pressure"
1. memory.current
memory.current 表示当前 cgroup 及其子 cgroup 正在使用的总内存。
2. memory.high
memory.high 是内存限速边界。
如果 cgroup 使用量超过该边界,进程会受到限速并承受较重的内存回收压力,但仅超过 memory.high 本身不会触发 OOM Killer。
如果该值设置得过于激进,应用可能不会被杀死,却会因为持续直接回收而逐渐变慢。
3. memory.max
memory.max 是内存硬限制。
当 cgroup 到达该边界且无法通过回收降低使用量时,内核可能在该 cgroup 内触发 OOM。
4. memory.events
memory.events 中值得关注的字段包括:
| 字段 | 含义 |
|---|---|
low |
在低保护边界内仍然发生回收的次数 |
high |
超过 memory.high 后被限速并执行直接回收的次数 |
max |
内存即将超过 memory.max 的次数 |
oom |
到达内存限制且分配即将失败的次数 |
oom_kill |
被 OOM Killer 杀死的进程数 |
这些事件可以帮助判断 Memory PSI 上升究竟来自普通节点内存竞争、memory.high 限速,还是已经接近 OOM。
Memory PSI 的真正价值在于:
它能够在 OOM 发生之前,告诉我们业务已经开始因为内存回收而变慢。
七、I/O PSI:吞吐量不高,磁盘仍然可能很慢
磁盘吞吐量只有几十 MB/s,并不代表 I/O 一定没有问题。
应用仍然可能因为以下原因发生 I/O 等待:
- 云盘 IOPS 达到上限;
- 磁盘请求队列过长;
- 随机读写比例过高;
- 日志同步刷盘;
- 数据库频繁执行
fsync; - 容器可写层性能不足;
- 多个 Pod 共享同一块低性能磁盘;
- 页面回写和业务读写互相竞争;
- 存储网络抖动;
- 磁盘吞吐未满,但单次 I/O 延迟很高。
排查时可以结合:
cat /proc/pressure/io
cat "${CGROUP_DIR}/io.pressure"
iostat -x 1
pidstat -d 1
iotop
重点关注:
- PSI
some和full; await;aqu-sz;- IOPS;
- 读写吞吐;
- 单次 I/O 延迟;
- 应用 P99;
- 日志写入量;
- 数据库刷盘耗时。
如果 I/O PSI 很高,而 Memory PSI 较低,可以优先检查磁盘吞吐、IOPS 和存储延迟,而不是继续扩大 JVM Heap。Kubernetes 官方 PSI 文档也将“高 I/O 压力、低内存压力”作为判断应用可能正在等待磁盘的重要组合。
八、Kubernetes 如何暴露 PSI
从 Kubernetes v1.36 开始,KubeletPSI 已进入稳定状态并锁定为开启。
Kubelet 可以采集节点、Pod 和容器三个层级的 CPU、内存和 I/O 压力数据,并通过以下两个入口暴露:
- Kubelet Summary API;
- Kubelet
/metrics/cadvisorPrometheus 接口。
该能力要求节点使用 Linux 4.20 或更高版本、启用 CONFIG_PSI=y,并运行在 cgroup v2 环境中。部分发行版还需要通过内核启动参数 psi=1 显式开启 PSI。
1. 检查 cgroup 版本
stat -fc %T /sys/fs/cgroup
如果输出:
cgroup2fs
说明当前使用的是 cgroup v2。
2. 检查系统级 PSI
ls -l /proc/pressure
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io
3. 检查内核配置
zgrep CONFIG_PSI /proc/config.gz 2>/dev/null
也可以尝试:
grep CONFIG_PSI "/boot/config-$(uname -r)" 2>/dev/null
期望看到:
CONFIG_PSI=y
如果内核已经编译 PSI,但 /proc/pressure 仍然不存在,可以继续检查启动参数:
cat /proc/cmdline
确认是否需要增加:
psi=1
九、通过 Summary API 查询 Pod 和容器 PSI
可以通过 Kubelet Summary API 查询节点上的 Pod 和容器压力数据。
NODE_NAME="worker-01"
kubectl get --raw \
"/api/v1/nodes/${NODE_NAME}/proxy/stats/summary" |
jq '
.pods[] |
{
namespace: .podRef.namespace,
pod: .podRef.name,
containers: [
.containers[] |
{
name: .name,
cpuPsi: .cpu.psi,
memoryPsi: .memory.psi,
ioPsi: .io.psi
}
]
}
'
只查看指定容器:
NODE_NAME="worker-01"
CONTAINER_NAME="order-service"
kubectl get --raw \
"/api/v1/nodes/${NODE_NAME}/proxy/stats/summary" |
jq --arg name "${CONTAINER_NAME}" '
.pods[].containers[] |
select(.name == $name) |
{
name: .name,
cpu: .cpu.psi,
memory: .memory.psi,
io: .io.psi
}
'
返回结果中会包含:
avg10
avg60
avg300
total
Summary API 能提供节点、Pod 和容器层级的 PSI 数据。
实际执行时,当前用户需要具备访问对应节点代理接口的 RBAC 权限。
十、通过 Prometheus 监控 PSI
Kubelet 的 /metrics/cadvisor 接口可以暴露 PSI 累计计数器。
常见指标包括:
container_pressure_cpu_waiting_seconds_total
container_pressure_cpu_stalled_seconds_total
container_pressure_memory_waiting_seconds_total
container_pressure_memory_stalled_seconds_total
container_pressure_io_waiting_seconds_total
container_pressure_io_stalled_seconds_total
其中可以将命名理解为:
waiting对应 PSIsome;stalled对应 PSIfull。
Kubernetes 官方文档示例展示了 CPU、内存和 I/O 的 waiting_seconds_total 指标;Kubernetes 与 cAdvisor 的实现还会生成对应的 stalled_seconds_total 指标。
可以直接查询:
NODE_NAME="worker-01"
kubectl get --raw \
"/api/v1/nodes/${NODE_NAME}/proxy/metrics/cadvisor" |
grep "container_pressure_"
由于这些指标都是累计计数器,Prometheus 中不应该直接比较原始值,而应该使用 rate() 或 increase()。
1. CPU 部分任务等待速率
sum by (namespace, pod, container) (
rate(
container_pressure_cpu_waiting_seconds_total{
namespace!="",
pod!="",
container=~".+",
container!="POD"
}[5m]
)
)
2. 内存部分任务等待速率
sum by (namespace, pod, container) (
rate(
container_pressure_memory_waiting_seconds_total{
namespace!="",
pod!="",
container=~".+",
container!="POD"
}[5m]
)
)
3. I/O 部分任务等待速率
sum by (namespace, pod, container) (
rate(
container_pressure_io_waiting_seconds_total{
namespace!="",
pod!="",
container=~".+",
container!="POD"
}[5m]
)
)
要查看 full 压力,可以将 waiting 替换为 stalled。
例如:
sum by (namespace, pod, container) (
rate(
container_pressure_memory_stalled_seconds_total{
namespace!="",
pod!="",
container=~".+",
container!="POD"
}[5m]
)
)
部分 Kubernetes 和容器运行时环境还会为 container=""、container="POD" 或 Pod cgroup 生成额外的 PSI 序列,因此 PromQL 中通常需要过滤非业务容器,避免重复统计和不必要的指标基数。
对于单个 cgroup 序列,如果 rate() 结果为 0.08,可以近似理解为统计窗口内平均每秒产生了 0.08 秒停顿,也就是约 8% 的时间受到压力。
但如果对多个容器使用 sum() 聚合,结果表示所有容器停顿时间的总和,不再能直接当作百分比,甚至可能大于 1。
十一、一次典型的 Java 服务排障
假设一个订单服务出现以下现象:
接口 P99:180ms 上升到 1.8s
节点 CPU:35%
容器 CPU:长期接近 500m
JVM Heap:58%
Full GC:0 次
Pod 重启:0 次
从传统指标看:
- 节点 CPU 不高;
- JVM 堆内存没有打满;
- 没有 Full GC;
- Pod 没有 OOM;
- 服务进程也没有重启。
第一反应很容易是数据库或者网络变慢。
查看系统级 CPU PSI:
cat /proc/pressure/cpu
结果并不明显。
继续查看容器 cgroup:
cat "${CGROUP_DIR}/cpu.pressure"
cat "${CGROUP_DIR}/cpu.stat"
得到以下示例数据:
some avg10=19.42 avg60=12.31 avg300=4.87 total=91823422
full avg10=1.20 avg60=0.73 avg300=0.18 total=3812201
同时:
nr_periods 18420
nr_throttled 13762
throttled_usec 182340921
这些数据说明:
- 容器内存在大量已经准备运行、却拿不到 CPU 的线程;
- 大量 CPU 配额周期触发了限流;
- 问题发生在容器自己的 CPU 配额,而不是宿主机整体容量;
- Java 线程池继续接收任务,但执行速度被 CPU Limit 限制;
- 队列逐渐积压,最终表现为 P99 延迟上升。
完整因果链如下:
graph TD
A["请求量突然上升"] --> B["Java 可运行线程增加"]
B --> C["容器耗尽 CPU 配额"]
C --> D["cgroup 触发 CPU 限流"]
D --> E["CPU PSI 持续升高"]
E --> F["线程池任务开始积压"]
F --> G["接口 P99 和超时率上升"]
H["宿主机仍有空闲 CPU"] --> I["节点总体 CPU 使用率不高"]
I --> J["只看节点 CPU 容易误判"]
最终优化方向可能包括:
- 将 CPU Limit 从
500m调整到更符合实际峰值的范围; - 避免直接按照平均 CPU 使用量设置 Limit;
- 调整 Java 线程池大小;
- 对突发流量增加限流和背压;
- 增加服务副本;
- 将 CPU 密集型任务从在线请求链路中拆出;
- 检查节点是否存在严重 CPU 超卖;
- 对延迟敏感服务考虑使用 Guaranteed QoS 和 CPU Manager。
关键不是看到延迟上涨后简单“多加 CPU”,而是先判断线程究竟在等待什么。
十二、Java 内存正常,为什么 Memory PSI 仍然很高
假设另一个 Java 服务出现:
JVM Heap:2.4GiB
-Xmx:4GiB
容器内存使用:5.7GiB
容器内存 Limit:6GiB
Memory PSI:持续上升
只看堆内存时,使用率只有 60%,似乎还有大量空间。
但容器实际内存已经接近上限。
这时应继续查看 JVM 原生内存:
jcmd <pid> GC.heap_info
jcmd <pid> Thread.print
jcmd <pid> VM.native_memory summary
GC.heap_info 用于查看 Java 堆信息,Thread.print 可以查看线程和线程栈,VM.native_memory 用于查看 JVM 原生内存使用情况。Native Memory Tracking 需要在 JVM 启动时提前开启。
例如:
java \
-XX:NativeMemoryTracking=summary \
-jar application.jar
继续检查:
jcmd <pid> VM.native_memory baseline
sleep 60
jcmd <pid> VM.native_memory summary.diff
如果发现线程数量过多、Metaspace 持续增长、Direct Buffer 占用较高,或者容器页缓存快速膨胀,就可能解释为什么 Java Heap 看起来正常,但容器仍然承受明显 Memory PSI。
因此,-Xmx 只能限制 Java 堆,不等于限制整个 Java 进程的内存占用。
十三、PSI 应该如何与传统指标组合
PSI 不是为了替代 CPU、内存、磁盘和 JVM 指标,而是帮助这些指标建立因果关系。
可以按照下面的矩阵进行初步判断:
| 现象 | 关联指标 | 可能原因 |
|---|---|---|
| CPU PSI 高 | throttled_usec 快速增长 |
CPU Limit 过低 |
| CPU PSI 高 | 节点 Load 高,限流不明显 | 节点 CPU 竞争或严重超卖 |
| CPU PSI 高 | Java Runnable 线程很多 | 线程池过大或 CPU 密集计算 |
| Memory PSI 高 | memory.events high 增长 |
触发 memory.high 和直接回收 |
| Memory PSI 高 | oom、oom_kill 增长 |
内存硬限制不足 |
| Memory PSI 高 | JVM Heap 不高 | 原生内存、线程栈、页缓存或 Socket Buffer |
| I/O PSI 高 | await、磁盘队列升高 |
存储延迟或 IOPS 不足 |
| I/O PSI 高 | 日志量突然增加 | 日志风暴或同步刷盘 |
| I/O PSI 和 Memory PSI 同时升高 | 页扫描、回写增加 | 页面缓存竞争和磁盘回写互相放大 |
| PSI 正常但 P99 高 | 下游耗时升高 | 数据库、RPC、锁竞争或应用逻辑问题 |
推荐的排障流程如下:
graph TD
A["发现 P99 或超时率上升"] --> B{"PSI 是否升高"}
B -->|否| C["检查锁竞争 GC 数据库 RPC 和业务逻辑"]
B -->|是| D{"哪类 PSI 升高"}
D -->|CPU| E["检查 cpu.stat CPU Limit 线程池和节点竞争"]
D -->|内存| F["检查 memory.events memory.stat 和 JVM 原生内存"]
D -->|磁盘| G["检查磁盘延迟 IOPS 日志和文件系统"]
E --> H["调整资源配置或应用并发模型"]
F --> H
G --> H
H --> I["通过压测和业务指标验证"]
十四、不要把 PSI 告警做成单一固定阈值
PSI 很有价值,但也很容易被错误使用。
例如,看到 cpu.some avg10 高于某个数值就立即扩容,可能造成不必要的资源浪费。
不同业务能够接受的 PSI 完全不同:
- 在线交易接口对短时 CPU 等待很敏感;
- 普通后台管理系统可以接受一定抖动;
- 消息消费者更关注长期吞吐和积压;
- 批处理任务通常可以接受较高 CPU 压力;
- 数据库对 I/O
full压力更敏感。
更合理的告警设计应遵循三个原则。
1. 先建立业务基线
至少观察一个完整业务周期:
- 工作日和周末;
- 高峰和低峰;
- 定时任务执行前后;
- 发布前后;
- 扩缩容前后;
- 大促或活动期间;
- 正常状态和故障状态。
比较正常状态和异常状态下的 PSI,而不是直接采用网上的统一阈值。
2. 区分短时尖刺和持续压力
如果出现:
avg10 很高
avg60 较低
avg300 接近零
更可能是一次短时突发。
如果出现:
avg10 持续升高
avg60 持续升高
avg300 也不断升高
则更可能是长期容量不足。
Kubernetes 官方文档同样建议通过 avg10 和 avg300 的差异判断近期突发与长期压力。
3. 将资源压力和业务 SLO 关联
真正需要告警的不是“PSI 大于多少”,而是:
资源压力是否正在破坏业务目标?
推荐组合条件包括:
- CPU PSI 上升,同时接口 P99 上升;
- CPU PSI 上升,同时 CPU throttling 增长;
- Memory PSI 上升,同时
memory.events.high增长; - Memory PSI 上升,同时超时率上升;
- I/O PSI 上升,同时数据库写入耗时上升;
- I/O PSI 上升,同时日志刷盘耗时上升;
- PSI
full持续非零,同时业务吞吐下降。
单一指标只能描述现象,多个指标同时变化才能建立更可靠的因果关系。
十五、不同资源压力的优化方向
1. CPU 压力
优先检查:
- CPU Request 和 Limit 是否合理;
- CPU Limit 是否只按照平均值配置;
- Java 线程池是否明显大于可用 CPU;
- 是否存在死循环、序列化、压缩或加密热点;
- 同一节点是否部署了过多 CPU 密集型 Pod;
- 是否需要增加副本;
- 是否需要拆分异步任务;
- 是否存在过多 Runnable 线程;
- 节点是否发生 CPU 超卖;
- 服务是否适合使用 CPU Manager 静态策略。
2. 内存压力
优先检查:
- JVM
-Xmx是否为非堆内存预留了空间; - Direct Memory 是否持续增长;
- 线程数量是否过多;
- Metaspace 是否持续膨胀;
- 页缓存是否被大量文件读写占用;
memory.high和memory.max是否设置过于激进;- 是否存在频繁直接回收;
- 容器 Request 是否明显低于实际工作集;
- 是否存在原生库内存泄漏;
- Pod 是否和其他高内存服务混部。
3. I/O 压力
优先检查:
- 是否在请求主链路执行同步刷盘;
- 日志级别是否突然调整为 DEBUG;
- 是否重复写入大文件;
- 容器可写层是否承载大量持久化数据;
- 云盘 IOPS 和带宽是否达到上限;
- 多个高 I/O 服务是否共用同一节点或磁盘;
- 日志、数据库和临时文件是否混用同一存储;
- 是否存在大量随机小文件读写;
- 数据库是否频繁执行
fsync; - 页面回写是否与业务读写互相竞争。
十六、线上排障检查清单
当再次遇到“CPU 不高,但接口很卡”时,可以按照以下顺序检查。
1. 系统层
uptime
vmstat 1
mpstat -P ALL 1
iostat -x 1
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io
2. cgroup 层
cat "${CGROUP_DIR}/cpu.stat"
cat "${CGROUP_DIR}/cpu.max"
cat "${CGROUP_DIR}/cpu.pressure"
cat "${CGROUP_DIR}/memory.current"
cat "${CGROUP_DIR}/memory.max"
cat "${CGROUP_DIR}/memory.high"
cat "${CGROUP_DIR}/memory.events"
cat "${CGROUP_DIR}/memory.stat"
cat "${CGROUP_DIR}/memory.pressure"
cat "${CGROUP_DIR}/io.pressure"
3. Kubernetes 层
kubectl top node
kubectl top pod -A
kubectl describe pod <pod-name> -n <namespace>
kubectl describe node <node-name>
kubectl get pod <pod-name> -n <namespace> -o yaml
重点查看:
- CPU Request;
- CPU Limit;
- Memory Request;
- Memory Limit;
- QoS Class;
- Pod 重启次数;
- OOMKilled;
- 节点资源分配;
- 驱逐事件;
- HPA 扩缩容状态。
4. Java 层
jcmd <pid> VM.info
jcmd <pid> GC.heap_info
jcmd <pid> Thread.print
jcmd <pid> VM.native_memory summary
重点查看:
- Runnable 线程数量;
- 线程池活跃数;
- 任务队列长度;
- GC 停顿;
- Java Heap;
- Metaspace;
- Thread Stack;
- Code Cache;
- 原生内存;
- Direct Buffer。
5. 业务层
重点关联:
- QPS;
- P95;
- P99;
- 超时率;
- 错误率;
- 线程池队列长度;
- 数据库连接池等待;
- RPC 下游耗时;
- 消息消费积压;
- 限流次数;
- 熔断次数;
- 重试次数;
- 发布和配置变更时间。
十七、总结
过去我们习惯使用 CPU、内存和磁盘使用率判断系统是否繁忙。
但使用率只能告诉我们资源消耗了多少,无法直接告诉我们业务线程因为资源不足损失了多少执行时间。
PSI 改变了资源排障的观察角度:
- CPU PSI 告诉我们线程是否在等待 CPU 调度;
- Memory PSI 告诉我们任务是否被内存回收拖慢;
- I/O PSI 告诉我们业务是否在等待存储完成操作;
some反映部分任务受到影响;full反映整个工作负载同时受到影响;avg10适合发现短时突发;avg60适合观察持续压力;avg300适合判断长期容量不足;total适合通过速率发现累计停顿增长。
真正有效的性能分析,不应该停留在:
CPU 有没有打满?
而应该继续追问:
请求慢下来的那段时间,线程究竟在等待什么?
当 PSI 与 cgroup、Kubernetes、Prometheus、JVM 和业务 SLO 结合起来后,许多过去难以解释的延迟尖刺,都会变得有迹可循。

主播最近工作太忙了,后续更新文章频率会比较低了