服务器指标:负载、CPU、IO 排查笔记

记录:什么时候该判断服务器"扛不住了、要扩容",以及 load average / CPU 使用率细分 / 磁盘IO / 网络IO 之间的关系和常见误区。
关联图解:CPU时间都去哪儿了(Artifact图解)


一、怎么判断服务器需要扩容

单一指标不可靠,要从四个维度交叉判断,配合压测拿到基准线,而不是等打满了才扩:

1. 资源层

  • CPU:持续 >70%~80%,load average 长期超过核数
  • 内存:使用率持续高位、频繁 Full GC、发生 OOM
  • 磁盘 IO:iowait 高、磁盘使用率 >80%
  • 网络带宽:出入带宽跑满、丢包率上升

2. 应用层

  • RT:P99/P95 明显劣化,超过 SLA
  • QPS/TPS 达到压测得出的单机上限(必须靠压测拿到,不能拍脑袋)
  • 错误率:超时、5xx 增多
  • 线程池/连接池饱和:Tomcat 线程池打满、HikariCP 连接池等待队列变长
  • GC 频率和耗时异常

3. 中间件/依赖层

  • MySQL:慢查询增多、连接数逼近 max_connections、主从延迟变大
  • Redis:内存使用率高、连接数逼近上限、慢日志增多
  • RocketMQ:消息堆积量持续增长、消费延迟拉大
  • XXL-Job:任务执行耗时变长、调度延迟

4. 业务层

  • 用户量/订单量等业务指标增长趋势 + 容量规划预测未来是否会突破单机承载能力

判断方法论

  1. 先压测,拿到单机在可接受 RT/错误率下的 QPS 上限,这是所有判断的基准线
  2. 监控 + 分级告警:70%~80% 预警(准备扩容),85%~90% 紧急(触发扩容)
  3. 常见方法论:USE 方法(Utilization 使用率、Saturation 饱和度、Errors 错误)看资源;RED 方法(Rate、Errors、Duration)看服务
  4. 看趋势而非瞬时值:连续多个周期(如15分钟均值)超阈值才触发,避免抖动误报
  5. 有条件上 HPA(K8s)按 CPU/内存/自定义指标(QPS、队列长度)自动扩缩容

二、CPU 负载(Load Average)到底是什么

uptime / top 里看到的:

load average: 1.20, 1.50, 1.80

三个数字分别是过去 1分钟、5分钟、15分钟 的平均负载。

定义(内核源码 kernel/sched/loadavg.c

Load average = R 状态(TASK_RUNNING,运行中/等待CPU)+ D 状态(TASK_UNINTERRUPTIBLE,不可中断睡眠) 的任务数平均值。

和 CPU 使用率的区别

指标 衡量什么 局限性
CPU 使用率 CPU 时间被实际占用的百分比 看不出"排队"情况
Load Average 运行中 + 等待中的任务数量 不区分是等CPU还是等IO

负载高但CPU使用率不高 → 很可能是磁盘/数据库慢导致大量线程在排队等IO,不是算力不够,扩CPU没用。

判断负载是否"高":要对比核数

  • 负载 < 核数:CPU有空闲,不算高
  • 负载 ≈ 核数:CPU基本跑满,需关注
  • 负载 > 核数:任务在排队,明显过载信号

nproc 查核数。


三、D 状态的本质:不是"专指磁盘IO",而是"内核不可中断等待"

D 状态(TASK_UNINTERRUPTIBLE)是内核里更广的语义——只要内核需要保证某个操作不被信号打断,就会用它。对普通应用服务器来说,几乎全部来自:

  1. 本地磁盘/块设备 IO(最主要来源):文件读写、日志同步刷盘(fsync)
  2. 内存回收/换页(swap):内存不够时等待换页完成,本质也是磁盘IO
  3. NFS 等网络文件系统(特例):语义上走网络,但因为要保证文件系统一致性,客户端等待时仍用D状态

结论:应用服务器负载高 ≈ R状态(CPU争抢/计算)+ D状态(本地磁盘IO,含swap换页)——这个结论对没挂NFS、纯TCP访问数据库/RPC的服务器成立,可以直接用于排查。

负载高的常见原因分类

R 状态多(真 CPU 瓶颈)

  • 计算密集型代码(死循环、复杂算法、正则灾难性回溯)
  • 并发请求量超过单机处理能力
  • 频繁 GC(Java 高发,GC线程占CPU + STW期间业务线程排队)
  • 线程数远超核数导致上下文切换开销大(展开见九节)
  • 定时任务集中触发(如 XXL-Job 多任务并发)

D 状态多(IO/资源等待,不是CPU问题)

  • 磁盘IO瓶颈:慢查询导致大量磁盘读写、磁盘性能差
  • 数据库慢查询、锁等待
  • 磁盘空间将满/inode耗尽

容易被忽略的情况

  • 容器 CPU limit 限流(cgroup throttled),K8s环境常见,需看 cpu.statthrottled_time
  • NUMA 跨节点访问内存延迟

排查命令

top -H -p <pid>           # 定位具体线程
jstack <pid>               # Java线程栈,看RUNNABLE线程对应代码
vmstat 1                   # r=运行队列, b=阻塞(D状态)队列
iostat -x 1                # %util, await 看磁盘瓶颈
pidstat -d -p <pid>        # 具体进程的磁盘等待

四、网络 IO(MySQL 查询 / RPC 调用)为什么不计入 load

这是最容易搞混的一点,专门梳理清楚。

核心结论

等待 MySQL 响应、等待 RPC 调用返回,本质是网络 socket 等待,在 Linux 层面是 S 状态(TASK_INTERRUPTIBLE,可中断睡眠),不是 D 状态,因此不会计入 load average。

原因

  • 本地磁盘 IO:内核为保证操作原子性,用不可中断的 D 状态
  • 网络 socket 阻塞读(recv()):可以被信号打断,用可中断的 S 状态
  • RPC 框架即使是"异步转同步"(如 Dubbo/gRPC 用 Netty NIO + Future.get()),业务线程本质是 park/等 futex,同样是 S 状态

证据

ps aux / top 看 STAT 列:等磁盘IO的进程显示 D,等网络响应的显示 S。可以自己验证:应用线程阻塞等慢查询返回时,vmstat 1b 列和 r 列都不会明显上升。

那两台机器分别体现什么

机器 表现
应用服务器(调用方) load average 不一定升高;但 RT变长、线程池/连接池被占满、QPS下降
MySQL / 下游RPC服务器 如果慢查询根因是它自己在等磁盘(没走索引、buffer pool命中率低),它自己的 D状态和load才会升高

RPC 调用的额外风险:级联雪崩

RPC 等待不拉高负载,但会占住调用方线程资源:

  • 下游变慢/超时未设置 → 大量线程阻塞在等RPC返回 → 调用方自己的线程池被打满
  • 调用方线程池打满后,它的上游调用方也会变慢 —— 整条链路卡住,但每台机器CPU/负载可能都不高
  • 因为不影响load average,纯看负载监控发现不了,必须专门监控:RPC调用超时率、P99 RT、线程池活跃线程数/队列长度、熔断器状态

防护手段:合理设置调用超时、熔断降级(Sentinel/Hystrix)、线程池隔离(不同下游用不同线程池)、非关键调用异步化

结论:判断"是不是数据库/RPC慢"不能只看应用服务器的load

  • 应用服务器:看线程池/连接池占用率、RT、超时率
  • 数据库/RPC下游服务器:单独看它自己的load average、iostat、慢查询日志

五、本地磁盘 IO 的准确定义

不等于"文件上传/下载"。准确定义:本机进程直接对本机挂载的磁盘/块设备做读或写操作,不经过网络传输到另一台机器

常见场景(不只是文件上传下载)

  1. 日志写盘:Logback/Log4j2 写本地日志文件——最常见、最容易被忽略,日志量大或同步刷盘配置不当会显著拖累性能
  2. GC日志、Dump文件:JVM的GC日志、OOM时的heap dump、jstack线程转储
  3. Swap换页:内存不够时读写swap分区
  4. 应用生成的临时文件:导出Excel/CSV、图片处理中间文件
  5. 配置/资源文件读取:一般量小,影响不大
  6. 数据库/中间件(MySQL、Redis持久化RDB/AOF)对自己所在机器的数据文件读写

"文件上传"要拆开看,容易和网络IO混淆

"用户上传文件到服务器"包含两段性质完全不同的操作:

  1. 接收阶段:服务器通过网络接收字节流 → 网络IO,线程等待是S状态
  2. 落盘阶段:服务器把字节写入本机磁盘 → 本地磁盘IO,同步写入时线程进入D状态

判断标准:这个读写最终有没有经过本机的磁盘/块设备驱动,而不是看业务语义上是不是"文件相关操作"。


六、CPU 使用率细分(us/sy/ni/id/wa/hi/si/st)

top 里的 %Cpu(s) 一行,或 mpstat -P ALL 1(按核看,避免多核平均掉掩盖单核打满的问题)。

工厂比喻:CPU是工人,网卡是传达室,磁盘是仓库

指标 大白话 发生在哪 异常时说明什么
us 干私活:工人在工作台上干活 执行业务代码 真·计算密集,查代码逻辑/算法
sy 走流程:跑去登记、领料、交接 系统调用(内核态) 频繁IO、频繁建线程、锁竞争
ni 被优先级调整过的活 nice/renice过的进程 日常基本可忽略
id 真闲着 CPU无事可做 正常,越高越好
wa 干等着:手续办完但仓库没送货 CPU闲着但有本地磁盘IO排队 对应D状态,磁盘/数据库瓶颈,加CPU没用
hi 硬件敲门 处理硬件中断(多是网卡) 硬件/驱动异常,较少见
si 软件敲门 处理软中断(多是网络协议栈) 网络包量异常大,查流量/网卡配置
st 被借走:车间主任把工人借给隔壁 宿主机把CPU时间分给了别的租户 云环境资源争抢(超卖),换规格/找厂商

关键澄清:网络IO等待(RPC/MySQL)不算这8项里的任何一项

  • si/hi 是CPU处理网络中断/协议栈本身的开销,跟业务线程等不等待响应无关
  • 业务线程阻塞等RPC/MySQL响应时,线程本身被挂起(S状态),没有占用CPU时间片,所以既不算wa(wa专指本地磁盘IO等待),也不算si/hi——这时候对应核心是id空闲,或者被别的可运行线程占用

排查思路:组合判断,不要只看一个数字

现象 大概率原因
us高,wa/st 真计算密集,优化代码或加核数
sy 系统调用频繁,查IO/线程创建/锁竞争
wa高,us/sy 磁盘IO瓶颈,对应D状态,扩CPU没用,查磁盘/数据库
si异常高 网络包量异常,查流量/网卡配置
st > 0 且明显 宿主机资源争抢(虚拟化超卖),不是自己服务的问题

七、hi 和 si 到底是用来干嘛的

hi/sisy 都是"CPU在跑内核代码",但触发方式和干的活完全不同,容易被当成一回事,专门拆开说清楚。

先搞清楚"中断"是什么

硬件设备(网卡、磁盘控制器、定时器)没法主动"占用CPU",需要一种方式告诉CPU"有事发生了"——这就是中断(Interrupt):设备给中断控制器发信号,中断控制器再通知CPU。CPU不管当前在执行什么(哪怕用户代码正跑到一半),都会立刻暂停、保存现场、跳转去执行专门注册好的处理代码。这是"异步、被动挨打断"的机制,和 sy 完全不是一回事。

hi(硬中断)—— 应急、越快越好的活

网卡收到数据包时触发硬件中断,CPU响应后执行的这段代码叫 ISR(中断服务例程)hi 统计的就是执行ISR本身消耗的CPU时间。这段代码被设计得极短极简,通常只做"确认收到、给设备一个应答、把真正的活记下来待会处理"——因为执行ISR期间同级/更低优先级的中断是被屏蔽的,处理时间越长就越可能错过别的中断,内核刻意把硬中断里的活压到最少。

si(软中断)—— 硬中断甩出来的重活

真正把包解析出来、跑一遍TCP/IP协议栈、校验和计算、塞进对应socket接收缓冲区——这些重活如果也放进硬中断里做,会长时间关着中断,系统响应会变差。所以内核把这部分工作延后softirq(软中断) 机制里执行,这时中断是打开的、也能被更高优先级任务抢占。si 统计的就是这部分被延后执行的、比较重的处理工作

硬中断和软中断是上半部/下半部的关系:硬中断(hi)负责"接锅、记一笔账、赶紧撒手",软中断(si)负责"真正把这锅事干完"。

和 sy 的本质区别:谁触发的、能不能预判

谁触发 触发方式 干什么活
sy 用户进程 同步——进程主动调 read/write/socket 等系统调用,明确是"这个进程"在找内核办事 处理这个进程发起的具体请求
hi 硬件设备 异步——设备说来就来,打断任何正在跑的东西 应急确认,转手记账
si 内核自己(承接hi甩出来的活) 硬中断触发后延后执行 协议栈处理等重活

三者本质上都是内核态执行,内核特意把 hi/sisy 里拆出来,是因为它们不是"某个进程主动请求"的,而是"硬件事件驱动、跟当前哪个进程在跑没关系"的系统级开销,混在一起会误判成"某个进程自己在大量系统调用"。

关键点:hi/si 不是"进程状态"

前面讲的 R/S/D 是任务调度状态(某个线程处于什么状态),hi/siCPU时间分类,按核统计、跟具体哪个线程无关。一个网络包到达触发hi/si处理时,可能压根没有任何进程在等,也可能有个线程正因为等这个包而处于S状态——这是两个不同的坐标轴,不要混在一起判断。

串起来看一个完整流程

  1. 远程服务器的响应包到达网卡 → 触发硬中断,CPU执行ISR应急处理 → 计入 hi
  2. 内核把"解析包、走TCP/IP协议栈、放进socket缓冲区"这个重活丢给softirq → 计入 si
  3. 数据放进socket缓冲区后,唤醒之前阻塞在等这个响应的业务线程(之前是S状态,现在被唤醒变成R状态去抢CPU)
  4. 业务线程抢到CPU后执行反序列化/业务逻辑 → 计入 us

这样 hi/si/us/S状态/R状态 就串成了一条完整的因果链,也印证了前面"网络IO等待不计入wa、也不计入load"的结论——线程等待的那一段(第2步到第3步之间)不占CPU时间片,跟hi/si是两回事。


八、CPU 使用率补充:容易被忽略的四个点

1. 网卡中断集中在单核,把多核机器压缩成单核处理能力

si 处理网络包的软中断,默认可能全部落在同一个CPU核心上(取决于网卡驱动和中断亲和性配置)。这会带来四个实打实的问题:

  • 单核成为整机吞吐量天花板:哪怕机器有32个核、其他31个核全空闲,只要这一个核的si跑到100%,就没法处理更多进来的包了。表现是整机CPU使用率显示很低(比如平均20%),但网络已经打满,QPS上不去、延迟升高,极容易被误判成"CPU还有富余,不是瓶颈"。
  • 抢占业务线程的CPU时间:中断优先级通常高于普通线程调度,如果业务线程恰好调度到这个"扛中断"的核上,网络包一来会直接打断正在跑的业务线程,中断越密集业务线程被打断次数越多,RT因频繁被打断而变差,会体现在cs(上下文切换)升高上。
  • 跨核Cache Miss,更隐蔽的性能损耗:数据包到达时网卡DMA把数据写入内存,处理中断的核心会把数据加载进自己的L1/L2 Cache;如果真正处理数据的业务线程在另一个核心上运行,就要跨核/穿透到内存重新拉取数据,这个开销不会直接体现在任何单一指标上。
  • 接收队列溢出、丢包:核心处理软中断的速度跟不上收包速率时,内核网络协议栈的接收队列(ring buffer)会溢出,后续包被丢弃,导致TCP重传增多、吞吐下降,而CPU/内存使用率可能都正常。

验证命令

mpstat -P ALL 1               # 看是不是某一个核si长期接近100%,其他核很低
cat /proc/net/softnet_stat     # 每行对应一个核,第2列(16进制)是因队列满被丢弃的包数,持续增长说明处理不过来
ethtool -S <网卡名> | grep -i drop   # 网卡层面的丢包计数器

解决方向

  • RSS(Receive Side Scaling):网卡硬件层面支持多队列,按源/目的IP+端口哈希分发到不同队列(对应不同CPU核),首选方案,前提是网卡和驱动支持
  • RPS(Receive Packet Steering):网卡不支持多队列时,软件方式把包分发到多个核处理
  • irqbalance:让系统自动把各种硬件中断动态分配到不同核,避免全堆在CPU0
  • 手动配置中断亲和性(/proc/irq/<n>/smp_affinity),适合对延迟极致敏感、想把网络中断和业务线程分别绑到不同核组的场景

2. 容器/K8s 环境下的 CPU Limit 限流(Throttling)

现象和st(宿主机超卖)很像——"服务变慢,但CPU使用率看着不高"——但机制完全不同:

  • st:宿主机把CPU时间分给了别的租户(虚拟机层面的资源争抢)
  • CPU Throttling:容器设置了limit(如K8s的resources.limits.cpu),cgroup按周期(默认100ms)分配配额,一旦周期内配额用完,进程会被强制暂停直到下个周期,即使宿主机CPU还有大把空闲也不让用

Java服务上特别隐蔽:GC或突发流量瞬间用满配额,被限流暂停几十毫秒,表现为RT毛刺,但top/docker stats看到的CPU使用率平均值完全正常。

cat /sys/fs/cgroup/cpu/cpu.stat        # cgroup v1,关注 nr_throttled(被限流次数)和 throttled_time(累计被限流时长)

3. GC 算 CPU 使用率的哪一类

GC 工作线程执行的代码属于JVM进程自身,计入us(用户态),不是sy。Stop-The-World(STW)期间应用线程被挂起等GC完成,这些被挂起的线程不产生CPU消耗,只是"暂停",不会额外体现在任何一项里,但会直接拖长RT——这也是为什么"CPU使用率不高但RT很差"经常和GC有关,得专门看GC日志而不是CPU指标。

4. 采样窗口会掩盖瞬时毛刺

top默认3秒刷新一次,反映的是这3秒内的平均值。如果CPU尖峰只持续几十~几百毫秒(很常见,比如一次批量任务、一次GC),会被平均"抹平",看起来风平浪静,但用户侧已经感受到一次明显的RT抖动。想抓这种毛刺,需要更细粒度采样(vmstat 1/mpstat 1配合脚本连续记录),或者上专门的性能分析工具(perf、Arthas的dashboard/thread命令、APM的秒级监控)。


九、上下文切换(Context Switch)到底是什么

前面几节多次提到"上下文切换"(三、D状态原因里的"线程数远超核数";六节sy对应的"频繁建线程";八节cs列升高),专门拆开讲清楚它是什么、和系统调用/中断的区别、开销在哪、怎么排查。

定义

CPU 从正在执行的一个任务切换到另一个任务时,需要保存当前任务的执行现场(寄存器、程序计数器、栈指针等),再恢复目标任务的现场,这个过程叫上下文切换,由内核调度器完成。触发时机:时间片用完、被抢占、主动阻塞让出CPU(等锁/等IO/sleep)。

先澄清一个容易混淆的点:系统调用/中断 ≠ 上下文切换

六、七节讲的 sy(系统调用)和 hi/si(中断)容易让人以为"进了内核态"就是一次上下文切换,其实不是:

  • 系统调用:只是特权级切换(用户态→内核态,mode switch)。CPU仍在同一个线程的执行流里,内核处理完之后大概率还是回到发起调用的这个线程继续跑——没有切换"是哪个任务在运行",不会计入vmstatcs
  • 中断(hi/si):会打断当前线程的执行去跑中断处理程序,但处理完之后通常直接返回被打断的线程继续执行;只有在中断返回前内核发现有更高优先级任务需要运行(need_resched标志被置位)时,才会真正触发一次上下文切换

一句话:只有内核调度器决定"换一个任务上CPU"时,才是真正的上下文切换;系统调用和中断本身只是"借用"了一下CPU的内核态执行权,不必然伴随任务切换。

两种触发方式:自愿 vs 非自愿

  • 自愿切换(voluntary):线程主动让出CPU,比如等锁、等IO、sleep/wait。对应 /proc/<pid>/status 里的 voluntary_ctxt_switches
  • 非自愿切换(involuntary):调度器强制把CPU换给别的任务,常见于两种情况——时间片耗尽,或者有更适合运行的任务被唤醒触发抢占(以CFS调度器为例:新唤醒任务的vruntime明显小于当前任务时,即使当前任务时间片没用完也会被立刻抢占;Linux 6.6起默认调度器已换成EEVDF,抢占逻辑类似,只是用"虚拟截止时间"代替vruntime做判断)。对应 nonvoluntary_ctxt_switches

两者含义不同:自愿切换多,通常说明程序里锁竞争或IO等待频繁;非自愿切换多,通常说明并发线程数超出了CPU处理能力,是真正的CPU资源紧张信号。

真正的开销在哪:不是"保存几个寄存器"这么简单

保存/恢复寄存器现场本身的耗时很短(量级上通常在百纳秒级,具体因硬件架构/内核版本差异很大,这里只是数量级参考,不是精确基准),真正的大头在:

  • TLB(Translation Lookaside Buffer)失效:TLB缓存"虚拟地址→物理地址"映射。切换到不同进程(地址空间不同)时,CPU要把页表基址寄存器(x86的CR3)指向新进程的页表;如果没有用PCID(Process Context ID)区分TLB条目,就要整个刷掉TLB,之后新进程每次内存访问都可能先触发TLB miss
  • L1/L2 Cache 污染:被切走任务留下的热数据会被新任务的数据逐步顶掉,原任务重新调度回来时热数据已经不在Cache里,要重新从内存加载

三种切换场景,开销依次增大

  1. 同进程内、同核心的线程切换:共享地址空间(不用刷TLB),也没离开原来的Cache,开销最小
  2. 跨进程、同核心的切换:地址空间不同,触发TLB失效(无PCID时);Cache仍在同一个核上,但会被新进程逐步顶掉
  3. 跨核心的切换(核间迁移):不管是不是同进程,只要调度器把任务迁移到了别的物理核心,原核心上暖好的L1/L2 Cache就完全用不上了,等于冷启动;如果还跨了NUMA节点,会叠加跨节点内存访问延迟(呼应三节提到的"NUMA跨节点访问内存延迟"),开销最大

Java(同进程多线程模型)天然比多进程模型省一些切换成本,但线程切换依然要陷入内核态、保存/恢复内核栈和完整寄存器组,仍比用户态调度贵得多;如果对延迟极度敏感,可以用taskset或cgroup cpuset把关键线程绑定到固定核心,减少跨核迁移带来的Cache冷启动。

排查命令

vmstat 1                          # cs列:整机每秒上下文切换次数,需要结合in(中断数)一起看
pidstat -w -p <pid> 1             # cswch/s(自愿) 和 nvcswch/s(非自愿),按进程/线程细分
cat /proc/<pid>/status | grep ctxt_switches   # voluntary/nonvoluntary_ctxt_switches 累计值

常见误区:cs 高不等于一定有问题

高并发、IO密集型的服务本身cs值就会比较高,这是正常现象,不能只看绝对值。真正要警惕的信号是:

  • cs突增并伴随RT上升,而不是长期稳定在一个较高水平
  • 非自愿切换(nvcswch)占比高且持续增长——这才是"线程数超过CPU处理能力"的直接证据

延伸:协程/虚拟线程为什么能大幅降低这部分成本

传统线程的调度和上下文切换都在内核态完成,需要陷入内核、保存完整寄存器组和内核栈。协程(Go goroutine)、Java 21+ 的虚拟线程(Project Loom)把调度下放到用户态运行时自己完成,多个虚拟线程复用少量内核线程(载体线程),大部分切换不需要陷入内核,成本比内核线程切换低一个数量级——这也是虚拟线程能支撑海量并发的核心原因之一。


十、内存指标的常见误区

free -h 到底在告诉你什么

              total        used        free      shared  buff/cache   available
Mem:           16Gi       6.2Gi       1.1Gi       120Mi        8.7Gi        9.3Gi
  • used:进程实际占用、无法被回收的内存
  • free:真正一点没用过的内存
  • buff/cachebuffers(块设备原始IO的缓存,如文件系统元数据)+ cache(页缓存,缓存过的文件内容)
  • available在不触发swap的前提下,新进程实际还能拿到多少内存(内核3.14之后才有这一列)

为什么 used 高不代表紧张

Linux 的设计哲学是"闲着的内存就是被浪费的内存"——只要有空闲物理内存,内核就会拿去缓存磁盘数据(放进buff/cache),下次读同一文件能直接命中内存。这部分内存不是"占用走不了",而是"暂存的,随时可以让位":有进程真正申请内存时,内核会立刻回收buff/cache里可以清的部分来满足分配,几乎没有额外延迟。

所以used里包含大量buff/cache,看着很高,实际可能只是"缓存了很多文件"——真正该看的是available,这个值已经把可回收的buff/cache算进"可用"里了。

常见误区补充

  • 手动清缓存"腾内存"没必要echo 3 > /proc/sys/vm/drop_caches 清空页缓存后,后续读这些文件要重新走磁盘,等于主动制造一次性能下降,除非是做内存基准测试排除缓存干扰,否则生产环境不建议
  • 只看瞬时值、不看趋势available真正危险的信号是持续下降,说明有进程在真实地、越来越多地吃掉不可回收的内存(如内存泄漏),而不是buff/cache的正常波动

Swap 对 Java 是灾难级的

内存不够时系统会把部分内存页换到磁盘swap分区——一旦JVM堆的一部分被换出,GC扫描到这部分内存时要从磁盘换入,导致GC耗时(尤其STW时间)暴涨,表现为服务间歇性卡顿几秒甚至更久。生产环境的Java服务通常建议直接关闭swap,或把 vm.swappiness 调到很低(如1)。

free -h                        # 看Swap那一行,只要用量不是0就要警惕
cat /proc/sys/vm/swappiness    # 查看当前swappiness配置

容器场景下,OOM Killer 和 JVM 的 OutOfMemoryError 是两回事

触发条件 表现 排查位置
Linux OOM Killer 整个容器(cgroup)内存使用超过limit 进程直接被内核杀掉,没有异常栈 dmesgOut of memory: Killed process,K8s里Pod状态显示OOMKilled
JVM 的 OutOfMemoryError JVM堆(或Metaspace等)内存耗尽 抛Java异常,有异常栈可查 应用日志、GC日志

容易踩的坑:只设置了-Xmx却没考虑堆外内存(Metaspace、线程栈、DirectByteBuffer——Netty大量使用、JIT编译代码缓存),容器总内存占用(堆+堆外)会超过K8s的limits.memory,导致JVM堆自己还远没用满、没抛OutOfMemoryError,容器却先被OOM Killer干掉。这种情况看JVM自己的监控完全正常,只有查dmesg/K8s事件才能看到真相。

建议-Xmx 要预留堆外内存空间,一般设置为容器内存limit的 60%~70%,不要和容器limit设成一样大。


十一、JVM 与本文档指标的关联排查

只聚焦JVM那些直接体现在前面各项系统指标上、排查时绕不开的点,不展开JIT/类加载等和"服务器指标排查"关系不大的内容。

1. GC 在各项指标上的具体表现(串联三、六、八节)

  • us:GC工作线程执行的是JVM自身代码,计入us不是sy(八节已提),Full GC/Young GC频率升高会直接体现为us抬升
  • R状态:GC线程本身在跑(尤其Full GC或并发标记阶段)会挤占CPU,是三节"R状态多"的常见诱因之一
  • STW期间:应用线程被挂起不产生CPU消耗,但直接拖长RT——这是"CPU使用率不高但RT很差"的典型场景(八节已提,这里给出具体的Java侧诱因)
  • 本地磁盘IO(D状态):GC日志(-Xlog:gc)、heap dump(-XX:+HeapDumpOnOutOfMemoryError)都是本地磁盘写入(五节已归类),日志级别开太细或频繁OOM触发dump会叠加真实磁盘IO压力
  • 扩容信号(一节"内存"维度提到的"频繁Full GC、发生OOM"):Full GC频率/耗时应纳入常规监控基线,不是等OOM才发现

2. JVM 线程数与上下文切换(呼应九节)

Tomcat线程池、业务线程池、GC worker线程(并行GC默认线程数=CPU核数)等所有JVM内部线程都是操作系统线程,总数远超核数时直接推高九节讲的非自愿上下文切换。常见坑:线程池核心线程数"随便给个大数"而不是按核数结合压测设置,看着"线程池够用",实际上下文切换开销已经吃掉不少CPU。

3. 容器内存 limit 与 JVM 堆参数(深化十节已有内容)

十节提到"-Xmx留出堆外空间,设为容器limit的60%~70%",这是手动估算的保守做法。行业更规范的做法

  • JDK 10+(含回溯到 OpenJDK 8u191+)默认已支持容器感知(cgroup-aware),能自动识别容器CPU/内存limit
  • 推荐用 -XX:MaxRAMPercentage(如70.0)代替手写死的-Xmx数值——按容器limit百分比动态算堆上限,比手算固定值更适应"同一镜像部署到不同规格容器"的弹性伸缩场景,避免升降配后忘记同步改-Xmx
  • 需要区分对待的还有 -XX:InitialRAMPercentage(初始堆百分比)和小内存场景下生效的 -XX:MinRAMPercentage

4. 排查工具串起来用

  • jstack <pid>:看Java线程状态,结合操作系统R/S/D状态定位具体哪些线程在跑/等锁/等IO
  • jstat -gcutil <pid> 1000:实时看各代内存占用和GC次数/耗时,判断Full GC是否频繁
  • jcmd <pid> VM.native_memory summary:(需开NMT)看堆外内存明细,十节已提到,用于定位容器OOM是不是堆外吃多了
  • jmap -histo <pid> 或 heap dump + MAT/JProfiler:定位具体哪些对象占满了堆

十二、常用排查命令速查

# 负载与CPU
uptime                      # load average
top                         # 整体+分进程,按1看每核
mpstat -P ALL 1             # 按核细分 us/sy/wa/hi/si/st,避免平均掉单核打满
vmstat 1                    # r=运行队列(R状态), b=阻塞队列(D状态), cs=上下文切换

# 上下文切换细分
pidstat -w -p <pid> 1              # cswch/s(自愿) 和 nvcswch/s(非自愿),按进程/线程细分
cat /proc/<pid>/status | grep ctxt_switches   # voluntary/nonvoluntary_ctxt_switches 累计值

# 网络中断分布(排查si集中单核)
cat /proc/net/softnet_stat     # 每行对应一个核,第2列(16进制)是因队列满被丢弃的包数
ethtool -S <网卡名> | grep -i drop   # 网卡层面的丢包计数器

# 容器/K8s CPU限流
cat /sys/fs/cgroup/cpu/cpu.stat   # cgroup v1,关注 nr_throttled(被限流次数)、throttled_time(累计限流时长)

# 磁盘IO
iostat -x 1                 # %util, await 判断磁盘瓶颈
pidstat -d -p <pid>         # 具体进程磁盘等待

# 线程定位
top -H -p <pid>             # 定位高CPU线程
jstack <pid>                # Java线程栈,结合RUNNABLE线程定位代码

# JVM GC与堆
jstat -gcutil <pid> 1000    # 实时看各代内存占用和GC次数/耗时,判断Full GC是否频繁
jmap -histo <pid>           # 定位具体哪些对象占满了堆(大对象/内存泄漏排查)

# 内存(易误区)
free -h                        # 注意看available列,不是used列(buff/cache可回收),并留意Swap是否非0
cat /proc/meminfo              # 更细,能看MemAvailable、Cached、SReclaimable等具体字段
cat /proc/sys/vm/swappiness    # 查看当前swappiness配置,Java生产环境建议调低或关闭swap
dmesg | grep -i "out of memory"      # 查是否发生过Linux OOM Killer
jcmd <pid> VM.native_memory summary  # (需开NMT) 查JVM堆外内存明细,定位Metaspace/DirectByteBuffer是否吃多了

十三、核心结论汇总

  1. 判断扩容要看压测基准 + 多维度趋势,不能等打满才扩,也不能只看单一指标
  2. Load average = R状态 + D状态,D状态本质是"本地磁盘IO等待"(含swap),不含网络等待
  3. MySQL查询、RPC调用都是网络IO,等待时线程是S状态,不计入load、不计入wa——判断数据库/RPC慢,要分别看两台机器各自的指标,且要单独盯线程池/连接池占用和RT
  4. "本地磁盘IO"不等于"文件上传下载",日志写盘、swap换页比文件传输更常见更隐蔽
  5. CPU使用率的wa和st最容易导致误判方向:wa高会让人误以为要扩CPU(其实该修数据库),st高会让人误以为自己代码变慢(其实是宿主机超卖)
  6. hi/si 是CPU时间分类,不是进程状态:硬中断(hi)负责应急记账,软中断(si)负责协议栈等重活,都是硬件事件驱动、和sy的"进程主动系统调用"触发方式不同;网络包从到达网卡到唤醒业务线程,是 hi → si → 线程从S变R → us 的完整链路
  7. si 集中在单核会把多核机器压缩成单核的网络处理能力,且在整体CPU/内存监控上完全隐身,必须按核看才能发现;容器CPU Limit限流(cgroup throttling)现象上很像st(RT毛刺但CPU使用率不高),但机制是自己的资源配额用尽被强制暂停,而不是宿主机超卖
  8. free -hused 高不代表内存紧张,要看 available(已把可回收的buff/cache算进去);Swap对Java是灾难级的(GC扫到被换出的堆内存会导致STW暴涨),生产环境建议关闭或调低swappiness;容器里 Linux OOM KillerJVM的OutOfMemoryError 是两回事,-Xmx 要给堆外内存(Metaspace/DirectByteBuffer/线程栈)留出空间,不能和容器内存limit设成一样大
  9. 上下文切换的真实开销在TLB失效和Cache污染,不在保存寄存器本身:非自愿切换(nvcswch)多才是CPU资源紧张的直接信号,自愿切换(cswch)多更多指向锁/IO等待;同进程内线程切换比跨进程切换便宜(不用刷TLB);协程/Java虚拟线程把调度搬到用户态,避免了内核态切换的大部分成本,因此能撑更高并发
  10. JVM 的 GC/线程/内存参数会直接映射到前面各项系统指标上:GC工作线程计入us、STW拖长RT但不占CPU、GC日志和heap dump是本地磁盘IO(D状态)、JVM线程总数过多会推高非自愿上下文切换;容器化部署优先用-XX:MaxRAMPercentage而不是手写死的-Xmx,能自动适配容器limit、避免升降配后忘记同步调整的隐患