服务器指标:负载、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. 业务层
- 用户量/订单量等业务指标增长趋势 + 容量规划预测未来是否会突破单机承载能力
判断方法论
- 先压测,拿到单机在可接受 RT/错误率下的 QPS 上限,这是所有判断的基准线
- 监控 + 分级告警:70%~80% 预警(准备扩容),85%~90% 紧急(触发扩容)
- 常见方法论:USE 方法(Utilization 使用率、Saturation 饱和度、Errors 错误)看资源;RED 方法(Rate、Errors、Duration)看服务
- 看趋势而非瞬时值:连续多个周期(如15分钟均值)超阈值才触发,避免抖动误报
- 有条件上 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)是内核里更广的语义——只要内核需要保证某个操作不被信号打断,就会用它。对普通应用服务器来说,几乎全部来自:
- 本地磁盘/块设备 IO(最主要来源):文件读写、日志同步刷盘(fsync)
- 内存回收/换页(swap):内存不够时等待换页完成,本质也是磁盘IO
- 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.stat的throttled_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 1 的 b 列和 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 的准确定义
不等于"文件上传/下载"。准确定义:本机进程直接对本机挂载的磁盘/块设备做读或写操作,不经过网络传输到另一台机器。
常见场景(不只是文件上传下载)
- 日志写盘:Logback/Log4j2 写本地日志文件——最常见、最容易被忽略,日志量大或同步刷盘配置不当会显著拖累性能
- GC日志、Dump文件:JVM的GC日志、OOM时的heap dump、
jstack线程转储 - Swap换页:内存不够时读写swap分区
- 应用生成的临时文件:导出Excel/CSV、图片处理中间文件
- 配置/资源文件读取:一般量小,影响不大
- 数据库/中间件(MySQL、Redis持久化RDB/AOF)对自己所在机器的数据文件读写
"文件上传"要拆开看,容易和网络IO混淆
"用户上传文件到服务器"包含两段性质完全不同的操作:
- 接收阶段:服务器通过网络接收字节流 → 网络IO,线程等待是S状态
- 落盘阶段:服务器把字节写入本机磁盘 → 本地磁盘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/si 和 sy 都是"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/si 从 sy 里拆出来,是因为它们不是"某个进程主动请求"的,而是"硬件事件驱动、跟当前哪个进程在跑没关系"的系统级开销,混在一起会误判成"某个进程自己在大量系统调用"。
关键点:hi/si 不是"进程状态"
前面讲的 R/S/D 是任务调度状态(某个线程处于什么状态),hi/si 是CPU时间分类,按核统计、跟具体哪个线程无关。一个网络包到达触发hi/si处理时,可能压根没有任何进程在等,也可能有个线程正因为等这个包而处于S状态——这是两个不同的坐标轴,不要混在一起判断。
串起来看一个完整流程
- 远程服务器的响应包到达网卡 → 触发硬中断,CPU执行ISR应急处理 → 计入
hi - 内核把"解析包、走TCP/IP协议栈、放进socket缓冲区"这个重活丢给softirq → 计入
si - 数据放进socket缓冲区后,唤醒之前阻塞在等这个响应的业务线程(之前是S状态,现在被唤醒变成R状态去抢CPU)
- 业务线程抢到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仍在同一个线程的执行流里,内核处理完之后大概率还是回到发起调用的这个线程继续跑——没有切换"是哪个任务在运行",不会计入
vmstat的cs列 - 中断(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里,要重新从内存加载
三种切换场景,开销依次增大
- 同进程内、同核心的线程切换:共享地址空间(不用刷TLB),也没离开原来的Cache,开销最小
- 跨进程、同核心的切换:地址空间不同,触发TLB失效(无PCID时);Cache仍在同一个核上,但会被新进程逐步顶掉
- 跨核心的切换(核间迁移):不管是不是同进程,只要调度器把任务迁移到了别的物理核心,原核心上暖好的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/cache:
buffers(块设备原始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 | 进程直接被内核杀掉,没有异常栈 | dmesg里Out 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状态定位具体哪些线程在跑/等锁/等IOjstat -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是否吃多了
十三、核心结论汇总
- 判断扩容要看压测基准 + 多维度趋势,不能等打满才扩,也不能只看单一指标
- Load average = R状态 + D状态,D状态本质是"本地磁盘IO等待"(含swap),不含网络等待
- MySQL查询、RPC调用都是网络IO,等待时线程是S状态,不计入load、不计入wa——判断数据库/RPC慢,要分别看两台机器各自的指标,且要单独盯线程池/连接池占用和RT
- "本地磁盘IO"不等于"文件上传下载",日志写盘、swap换页比文件传输更常见更隐蔽
- CPU使用率的wa和st最容易导致误判方向:wa高会让人误以为要扩CPU(其实该修数据库),st高会让人误以为自己代码变慢(其实是宿主机超卖)
- hi/si 是CPU时间分类,不是进程状态:硬中断(hi)负责应急记账,软中断(si)负责协议栈等重活,都是硬件事件驱动、和sy的"进程主动系统调用"触发方式不同;网络包从到达网卡到唤醒业务线程,是
hi → si → 线程从S变R → us的完整链路 si集中在单核会把多核机器压缩成单核的网络处理能力,且在整体CPU/内存监控上完全隐身,必须按核看才能发现;容器CPU Limit限流(cgroup throttling)现象上很像st(RT毛刺但CPU使用率不高),但机制是自己的资源配额用尽被强制暂停,而不是宿主机超卖free -h的used高不代表内存紧张,要看available(已把可回收的buff/cache算进去);Swap对Java是灾难级的(GC扫到被换出的堆内存会导致STW暴涨),生产环境建议关闭或调低swappiness;容器里 Linux OOM Killer 和 JVM的OutOfMemoryError 是两回事,-Xmx要给堆外内存(Metaspace/DirectByteBuffer/线程栈)留出空间,不能和容器内存limit设成一样大- 上下文切换的真实开销在TLB失效和Cache污染,不在保存寄存器本身:非自愿切换(nvcswch)多才是CPU资源紧张的直接信号,自愿切换(cswch)多更多指向锁/IO等待;同进程内线程切换比跨进程切换便宜(不用刷TLB);协程/Java虚拟线程把调度搬到用户态,避免了内核态切换的大部分成本,因此能撑更高并发
- JVM 的 GC/线程/内存参数会直接映射到前面各项系统指标上:GC工作线程计入
us、STW拖长RT但不占CPU、GC日志和heap dump是本地磁盘IO(D状态)、JVM线程总数过多会推高非自愿上下文切换;容器化部署优先用-XX:MaxRAMPercentage而不是手写死的-Xmx,能自动适配容器limit、避免升降配后忘记同步调整的隐患
浙公网安备 33010602011771号