🤖
AI审核中

CPU 明明不高,接口为什么还是卡:用PSI看懂Kubernetes资源压力

Java 30分钟 110浏览 1评论

一、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/cpufull 没有明确定义。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

通常意味着刚刚发生了一次明显的资源竞争。

如果 avg10avg60avg300 都持续升高,则更可能是长期资源不足,而不是一次短暂毛刺。

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 somefull
  • 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 压力数据,并通过以下两个入口暴露:

  1. Kubelet Summary API;
  2. Kubelet /metrics/cadvisor Prometheus 接口。

该能力要求节点使用 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 对应 PSI some
  • stalled 对应 PSI full

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

这些数据说明:

  1. 容器内存在大量已经准备运行、却拿不到 CPU 的线程;
  2. 大量 CPU 配额周期触发了限流;
  3. 问题发生在容器自己的 CPU 配额,而不是宿主机整体容量;
  4. Java 线程池继续接收任务,但执行速度被 CPU Limit 限制;
  5. 队列逐渐积压,最终表现为 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 高 oomoom_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 官方文档同样建议通过 avg10avg300 的差异判断近期突发与长期压力。

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.highmemory.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 结合起来后,许多过去难以解释的延迟尖刺,都会变得有迹可循。

1 条评论
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  1 条评论
召田最帅boy 博主   湖南省衡阳市

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