系统(四):Linux 性能排查与调优
系统(四):Linux 性能排查与调优
导语:前面三篇讲"原理",这一篇讲"怎么用原理定位线上问题"——也是面试从"背过八股"走向"做过线上"的分水岭。围绕 CPU / 内存 / 磁盘 / 网络四类资源,讲清关键指标的正确读法(尤其是
load average与free的两个经典误读)、常用工具的组合用法、关键内核参数,最后给出一套可直接复述的排查 SOP,共 8 题。
一、四类资源的指标与判读
1. load average 是什么?为什么"平均负载高"不等于"CPU 忙"?
答: load average 是"单位时间内处于【可运行状态 + 不可中断睡眠状态】的平均进程数"。
uptime
# 14:32:01 up 120 days, 3 users, load average: 2.15, 1.80, 1.20
# ↑1分钟 ↑5分钟 ↑15分钟
cat /proc/loadavg
# 2.15 1.80 1.20 3/512 12345
# ↑ ↑ ↑ │ └─ 最近创建的 PID
# │ │ └────┴─ 正在运行/可运行的进程数 / 总进程数
# │ └──────────── 15 分钟平均
# └────────────────── 1 分钟平均关键点:它统计的是【两种情况】的进程数:
| 状态 | 含义 | 是否吃 CPU |
|---|---|---|
| R(Running / Runnable) | 正在 CPU 上跑,或在就绪队列里等 CPU | ✅ 是(等 CPU 说明 CPU 不够) |
| D(Uninterruptible Sleep,不可中断睡眠) | 在等 IO(磁盘/网络),且不能被信号打断 | ❌ 不是(说明 IO 有瓶颈) |
所以"负载高"有两种完全不同的成因:
① 真的 CPU 不够(大量 R 状态);
② IO 卡住了(大量 D 状态)。
此时 CPU 使用率可能并不高——这就是"负载高但 CPU 不忙"的经典现象。
怎么判读(经验法则):
| 指标 | 读法 |
|---|---|
| 与 CPU 核数对比 | load ≈ 核数 → 基本满载;load > 核数 → 有明显排队;load > 核数 × 2 → 严重过载。能接受的阈值取决于业务(延迟敏感的 API 服务通常要求 load < 核数 × 0.7) |
| 看趋势(三个值的关系) | 1 分钟 >> 15 分钟 → 突发流量/刚出问题;三个值都高 → 长期资源不足;1 分钟 << 15 分钟 → 正在恢复 |
| 必须结合 CPU 核数 | 8 核机器 load=8 与 2 核机器 load=8 完全是两个概念 |
排查步骤(按 R/D 分流):
# ① 分别统计 R 与 D 状态的进程数
ps -eo stat | grep -c '^R' # 可运行(等 CPU)
ps -eo stat | grep -c '^D' # 不可中断睡眠(等 IO)★ 这是关键
# ② 找 D 状态进程(它们通常卡在某个 IO 或锁上)
ps -eo pid,stat,wchan:30,cmd | awk '$2 ~ /^D/'
# wchan 列会显示内核里阻塞在哪个函数(如 rpc_wait_bit_killable、io_schedule)
# ③ 结合 vmstat 看是否在等 IO(b 列 = 阻塞进程数,wa = IO 等待 CPU 占比)
vmstat 1
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 2 8 0 123456 12345 456789 0 0 4096 8192 500 1000 5 3 60 32 0
# ↑ b=8(8 个进程阻塞在 IO)且 wa=32% → **瓶颈在磁盘/网络 IO,不在 CPU**一句话总结:「load average 高」「CPU 使用率高」「系统变慢」是三件不同的事。看到负载高,第一件事是分 R / D,而不是直接去加 CPU。
2. CPU 使用率由哪几部分组成?CPU 高怎么排查?
答:
top
# Cpu(s): 5.2 us, 3.1 sy, 0.0 ni, 88.5 id, 2.0 wa, 0.0 hi, 1.2 si, 0.0 st
# ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
# 用户态 内核态 nice 空闲 IO等待 硬中断 软中断 被虚拟化占用| 指标 | 含义 | 升高说明什么 |
|---|---|---|
| us(user) | 用户态 CPU 时间 | 应用代码在算(正常,说明业务在干活;但也可能是死循环、频繁 GC、低效算法) |
| sy(system) | 内核态 CPU 时间 | 系统调用/上下文切换/锁竞争/内存管理开销大(频繁 read/write、大量线程切换) |
| ni(nice) | 低优先级进程(nice 调过)的时间 | 一般很小;若高说明有人跑了大量 nice 任务 |
| id(idle) | 空闲 | 正常应有剩余;id=0 且 load 高 → CPU 饱和 |
| wa(iowait) | 等磁盘 IO 的空闲时间占比 | IO 瓶颈信号(注意:它统计的是"CPU 空闲但至少有进程在等 IO") |
| hi(hard irq) | 硬中断处理 | 高 → 中断风暴(网卡/磁盘) |
| si(softirq) | 软中断处理 | 高 → 网络收发包压力大(见《系统(三)》第 7 题) |
| st(steal) | 被宿主机/其他虚拟机抢走的时间 | 云主机上 st 高 = 你的邻居在抢 CPU(此时优化自己的代码无效,要换规格或迁移) |
CPU 高的排查路径(按 us / sy / wa / si 分流):
一个可直接复述的"CPU 高"排查答案:
# ① 全局:确认是哪一类 CPU 高
top # 看 us/sy/wa/si 哪一列突出
mpstat -P ALL 1 # 是否只有个别核高(单线程瓶颈 / 中断集中)
# ② 进程级:找最耗 CPU 的进程
top -o %CPU 或 pidstat -u 1
# ③ 线程级:找最耗 CPU 的线程
top -H -p <pid>
# ④ 代码级:栈/火焰图定位到函数
jstack <pid> | grep -A 30 <hex_tid> # Java
perf top -p <pid> # 通用面试加分点:「单核 100% 而整体不高」 是极常见的现象(如 Redis 单线程、JVM 的 GC 线程、某个锁热点),此时应看
mpstat -P ALL而不是只看全局百分比。
3. free 显示内存快满了,是真的内存不足吗?OOM Killer 是怎么触发的?
答:
free -h
# total used free shared buff/cache available
# Mem: 31Gi 12Gi 1.2Gi 500Mi 18Gi 18Gi
# ↑ ↑ ↑
# 应用+内核占用 文件缓存 ★ 真正该看的
# (可回收缓存后
# 真正可用的内存)| 字段 | 含义 | 说明 |
|---|---|---|
| total | 物理内存总量 | |
| used | 已用 | 含应用、内核、共享内存 |
| free | 完全空闲 | 这个数字小 ≠ 内存不足(Linux 信奉"空闲内存是浪费",会把空闲内存拿去缓存文件) |
| buff/cache | 页缓存 + 缓冲区 | 绝大部分可回收(在内存紧张时会被释放) |
| available | 真正可用的估算值 | 这才是判断内存是否紧张的指标(≈ free + 可回收的 cache) |
核心结论:「
free小」是正常现象,「available小」才是问题。只看free就说"内存爆了"是最典型的新手误判。
内存紧张的三个信号(按严重程度递增):
| 信号 | 命令 | 含义 |
|---|---|---|
| ① available 很小、< 10% | free -h | 接近耗尽 |
| ② swap 在使用(si/so 非 0) | vmstat 1 的 si/so 列 | 物理内存不够,在换页 → 性能急剧下降(最严重的信号) |
| ③ 触发 OOM Killer | dmesg -T | grep -i -E "oom|killed process" | 内核选择并杀死一个进程来救命 |
OOM Killer 的工作机制(面试常问):
四个高频追问:
「容器里的 OOM 和宿主机的 OOM 有何不同?」 →
- 容器 OOM(cgroup 限制):进程被 SIGKILL,k8s 里表现为
OOMKilled(kubectl describe pod能看到Reason: OOMKilled,退出码 137 = 128 + 9); - 宿主机 OOM:OOM Killer 全局选人;
- 关键坑:JVM 在容器里必须正确感知 cgroup 限制(JDK 8u191+/JDK 9+ 的
-XX:+UseContainerSupport默认开启),否则-Xmx会按宿主机内存算(如给容器 2GB 而-Xmx取到 8GB)→ 必然被 OOMKilled。所以显式设置-Xmx仍是生产规范。
- 容器 OOM(cgroup 限制):进程被 SIGKILL,k8s 里表现为
「JVM 的
-Xmx设成容器 limit 的 100% 行不行?」 → 不行。除了堆,还有 Metaspace、直接内存(Netty/零拷贝)、线程栈(每线程 1MB)、Code Cache、JVM 自身。经验值:-Xmx ≈ limit × 70%(或按实际MaxDirectMemorySize+ 线程数估算后留 20~30% 余量)。「怎么定位"内存到底被谁吃了"?」
ps -eo pid,rss,vsz,pmem,comm --sort=-rss | head -20 # RSS 最大的进程
pmap -x <pid> | sort -k3 -n -r | head # 单进程内存映射明细
cat /proc/meminfo | head -20 # 各项内核内存统计
slabtop -o | head -20 # 内核 slab 占用(对象缓存)
cat /proc/<pid>/smaps_rollup # 进程 RSS/PSS 汇总
smem -rs rss 2>/dev/null | head # 按 PSS 统计(更真实,考虑共享页)- 「RSS、VSZ、PSS 有什么区别?」 →
| 指标 | 含义 | 陷阱 |
|---|---|---|
| VSZ(虚拟内存) | 申请的虚拟地址空间 | 可以很大(mmap/未触碰的堆)——不代表真实占用 |
| RSS(常驻内存) | 实际在物理内存中的页 | 会把共享页重复计入(如共享库被 100 个进程各算一次) |
| PSS(比例常驻) | 共享页按进程数分摊后统计 | 最真实(/proc/<pid>/smaps 可查看),但计算成本高 |
4. 磁盘 IO 瓶颈怎么定位?
答:
iostat -xdm 1
# Device r/s w/s rkB/s wkB/s await %util aqu-sz
# sda 120.0 340.0 1536.0 4352.0 8.32 89.6 2.15
# ↑ ↑
# 平均等待(ms) ★ 设备利用率| 命令 | 关注指标 | 判读 |
|---|---|---|
iostat -xdm 1 | %util(设备利用率)、await(平均 IO 等待 ms)、r/s/w/s(IOPS)、aqu-sz(平均队列长度) | %util 接近 100% 且 await 明显上升 → 磁盘饱和。SSD 上 %util 高不一定饱和(并行度高),更要看 await 与 aqu-sz |
iotop -oPa | 按进程统计磁盘读写 | 直接找到"谁在狂写磁盘"(-o 只显示有 IO 的、-P 按进程、-a 累计) |
pidstat -d 1 | 每进程的 kB_rd/s、kB_wr/s、iodelay | 比 iotop 更适合脚本化采集 |
vmstat 1 | bi/bo(块设备读写块数)、wa | 快速判断"是否在等 IO" |
cat /proc/diskstats | 原始计数 | 自定义监控采集 |
sar -d 1 | 历史趋势 | 复盘"当时是不是 IO 高" |
四个高频追问:
- 「
%util100% 但吞吐不高,说明什么?」 → 小 IO 太多(随机小写):磁盘在忙于处理"次数"而不是"字节数"。解法是合并写(batch/顺序写)、异步刷盘、调大缓冲区; - 「如何区分是"读"还是"写"导致的 IO 高?」 → 看
iostat的rkB/svswkB/s;写多常见于日志、刷盘、compaction,读多常见于缓存未命中、全表扫描; - 「为什么数据库常说"随机写比随机读更贵"?」 → 写会触发写放大(尤其 SSD 的 GC 与 LSM 的 compaction),且写还需等待落盘确认(
fsync);读则可能命中缓存; - 「IO 高但
%util不高(如 20%),可能是什么?」 → ① IO 被fsync的延迟掩盖(在等 raid 卡缓存);② 是网络 IO 而非磁盘 IO(此时应看网络);③ 云盘/网络存储(EBS、NAS)的后端抖动。
磁盘 IO 高的排查 SOP:
二、工具与调优
5. 常用性能工具怎么组合使用?各自的定位是什么?
答:
一张"从全局到细节"的工具链(按排查顺序):
几个"什么场景用什么"的对照(面试常问):
| 场景 | 首选工具 | 为什么 |
|---|---|---|
| 系统整体变慢,不知从哪查 | vmstat 1 | 一屏能看到 CPU/内存/IO/切换/中断 6 个维度,快速分流 |
| 某个进程 CPU 高 | top -H -p → jstack/perf | 需要线程级 → 代码级下钻 |
| 压力测试时观察瓶颈 | vmstat + iostat + sar -n DEV 三件套 | 分别覆盖 CPU/磁盘/网络 |
| 服务端偶发高延迟(毛刺) | perf / async-profiler / bpftrace;ss -ti 看重传 | 平均指标看不出来,需要高精度采样与单连接指标 |
| 怀疑网络问题 | ss/netstat -s(先看协议栈统计)→ tcpdump(再抓包) | 先看统计(成本低),再抓包(成本高) |
| 怀疑系统调用慢 | strace -c(谨慎)、perf trace、bpftrace | strace 会显著放大开销,生产上优先用 perf/eBPF |
| Java 应用问题 | Arthas(在线诊断)或 async-profiler(火焰图) | 不重启即可定位;jstack 适合死锁/卡顿 |
面试加分:「排查工具的选择原则是"从低成本、低侵入到高成本、高侵入"——先看
/proc与统计计数,再上采样(perf),最后才用 strace/tcpdump 这类高开销手段。在生产上对应用strace -f是很危险的操作。」
6. 哪些内核参数对生产服务最关键?怎么调?
答: 按资源分类给一份"高频参数清单"(网络相关部分见《网络(四)》第 10 题,这里补齐通用部分)。
① 文件描述符与进程数
fs.file-max = 2000000 # 系统级最大 fd 数
# 进程级:/etc/security/limits.conf 或 systemd 的 LimitNOFILE=1000000
fs.nr_open = 1000000 # 单进程 fd 硬上限(ulimit 不能超过它)
fs.inotify.max_user_watches = 524288 # inotify 监听数(IDE/日志采集/热加载易撞到)
kernel.pid_max = 4194304 # 最大 PID(大量进程/容器场景)
net.ipv4.ip_local_port_range = 10000 65535 # 本地端口范围(缓解端口耗尽)② 内存与虚拟内存
vm.swappiness = 1 # 尽量不用 swap(★ 延迟敏感服务;0 在部分内核会触发 OOM)
vm.overcommit_memory = 0 # 0=启发式 1=总是允许 2=严格(Redis 要求设 1,配合 bgsave fork)
vm.overcommit_ratio = 50 # 仅 overcommit_memory=2 时生效
vm.max_map_count = 262144 # ★ ES/Lucene 必须调大(默认 65530 不够)
vm.dirty_ratio = 10 # 脏页占内存上限(太高 → 同步刷盘卡顿)
vm.dirty_background_ratio = 3 # 后台回写起点
vm.panic_on_oom = 0 # OOM 时不要 panic(保持容器行为可预期)
vm.min_free_kbytes = 内存的 1%~3% # 保留给内核的紧急内存(太小会卡在回收,太大浪费)③ CPU 与调度
kernel.sched_migration_cost_ns = 5000000 # 调大可减少跨核迁移(长任务友好)
kernel.sched_autogroup_enabled = 1 # 桌面场景;服务器常考虑关闭
# cgroup(容器):cpu.max / cpu.weight 控制 CPU 配额与权重④ 内核与安全
kernel.panic = 10 # 内核 panic 后 10s 重启(配合高可用)
kernel.panic_on_oops = 1
net.core.somaxconn = 32768 # 全连接队列(见网络篇)
kernel.msgmax / msgmnb / shmmax # SysV IPC 限制(老中间件可能用到)
kernel.core_pattern = /data/core.%e.%p # core dump 路径(★ 排查崩溃必备)调参的四条原则(面试收尾):
| 原则 | 说明 |
|---|---|
| ① 少即是多 | 默认值经过大量测试,只改有明确瓶颈证据的参数。盲目"堆参数"是负面信号 |
| ② 要能解释 | 每个改动都要能说清"改的是什么行为、带来什么收益、代价是什么" |
| ③ 关注配套 | 很多参数要成对调(如 somaxconn 必须配 listen(backlog);overcommit_memory=1 要配 Redis 的 fork 场景) |
| ④ 可回滚 + 有监控 | 用配置管理(Ansible/云初始化)落地,改动前后对比指标,并保留回滚路径 |
一个高频"送分/送命"题:「Redis 官方建议改哪些内核参数?」→
①vm.overcommit_memory = 1(因为 Redisbgsave会fork,若内存不能 overcommit 会fork失败 → 建议改成 1);
② 关闭 THP(echo never > /sys/kernel/mm/transparent_hugepage/enabled,避免 fork 时 COW 代价暴增与延迟毛刺);
③net.core.somaxconn调大(默认 128,redis.conf的tcp-backlog需与之匹配,否则高并发下连接被丢)。
这三个是 Redis 启动时会在日志里主动告警的配置。
7. 网络问题怎么排查?从哪一步开始?
答: 面试常问「服务连不上/超时了,你怎么查」。标准答法是按网络分层自底向上(与《网络(一)》的分层模型呼应)。
从 curl -w 的输出直接定位(非常实用):
| 哪一段耗时高 | 问题在哪 |
|---|---|
time_namelookup 高 | DNS 解析慢(本地 DNS 慢/解析被劫持/域名解析链长) |
time_connect 高 | TCP 建连慢(网络 RTT 大、SYN 重传、队列溢出) |
time_appconnect - time_connect 高 | TLS 握手慢(证书链长、无会话复用、CPU 不足) |
time_starttransfer(TTFB)高 | 服务端处理慢(后端逻辑/依赖慢)或首包传输慢 |
time_total - time_starttransfer 高 | 响应体传输慢(带宽、大响应、慢客户端) |
四个高频追问:
- 「偶发超时怎么查?」 → 平均指标看不出来,要抓长尾:
# 用 mtr 持续观测丢包在哪一跳 mtr -r -c 100 <ip> # 看协议栈是否重传(重传率 = retrans / total) netstat -s | grep -i retrans ss -ti | grep -E "retrans|rto" # 单连接的重传与 RTO # 用 tcpdump 抓特定时间窗口的包,看是否有 RST/丢包 - 「
TIME_WAIT多怎么办?」 → 见《网络(二)》第 3 题(先问是谁在主动关闭、用长连接根治,再谈tcp_tw_reuse); - 「
ss比netstat好在哪?」 →ss直接读 Netlink 接口,不遍历/proc,在几万连接时netstat会卡死,ss依然秒回;且ss支持更多过滤条件(state、dport)与 TCP 详情(-ti); - 「如何判断是"网络慢"还是"应用慢"?」 → 看
curl -w的分解:若time_connect正常但time_starttransfer高 → 应用慢;若time_connect就高 → 网络慢。这是最快能给出结论的一步。
三、综合
8. 综合实战:线上接口变慢了,请给出完整的排查 SOP
答: 这是最能体现工程经验的一题。按"先定性、再定位、后修复"三段回答,并给出可执行命令。
必须主动说出的"经验性判断"(面试加分项):
| 经验 | 说明 |
|---|---|
| 先怀疑变更 | "什么时候开始的"往往能直接指向最近的上线/配置/数据变更 |
| 先看长尾不看均值 | P99/P999 才是用户感知到的慢;均值会被大量快请求掩盖 |
| 注意"木桶效应" | 一个接口的耗时 = 最慢的那个依赖;先用链路追踪找出最慢的下游,而不是逐个优化 |
| 警惕"雪崩"与"重试放大" | 下游变慢 → 上游超时重试 → 下游压力翻倍 → 全链路崩。必须有超时(且逐层递减)+ 重试退避 + 熔断 |
| 别在没留现场前重启 | 重启会丢失线程栈/堆/连接状态,先 dump 再重启(jstack/jmap/netstat 快照) |
| 区分"自己慢"与"被拖慢" | 自己 CPU 不高却 RT 高,通常是在等下游/等锁/等 IO |
"三分钟版"标准回答(可以背下来):
系统系列小结:(一)进程线程与并发 →(二)内存管理 →(三)文件系统与硬件 →(四)性能排查与调优。四条主线分别是「谁在跑、内存怎么分、数据怎么落盘、出问题怎么看」——前三条是原理,第四条是把原理变成生产力。
