mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3415 字
10 分钟
综合实战
2025-02-25

历经 19 章的内核机制剖析,已经理解了进程调度、内存管理、文件系统、网络协议栈、同步机制、容器隔离、追踪工具、启动流程等核心子系统。但知识的价值在于运用——当生产环境凌晨三点报警、进程莫名被杀、I/O 延迟飙升、网络丢包不断时,你需要的不是零散的知识点,而是一套系统化的问题定位方法论

本章是整个系列的收官之战。将通过 8 个来自真实生产环境的诊断场景,综合运用前 19 章的知识,建立从”现象观察”到”根因定位”再到”修复验证”的完整思维链路。每个场景都不是孤立的——OOM Killer 牵涉内存管理与 cgroup,D 状态进程关联调度器与 I/O 栈,网络丢包横跨协议栈与 Netfilter——正如内核本身,问题从不按子系统边界出现。

一、通用诊断方法论:观察 → 假设 → 验证 → 修复#

在进入具体场景之前,先建立一套适用于所有 Linux 内核问题的通用方法论。Brendan Gregg 在 Systems Performance 中提出的 USE 方法(Utilization / Saturation / Errors)提供了检查清单的框架,而本系列践行的 观察 → 假设 → 验证 → 修复 四步法则提供了执行路径:

flowchart LR A[" 观察<br/>采集现象数据"] --> B[" 假设<br/>提出可能原因"] B --> C[" 验证<br/>设计实验确认"] C --> D{"假设成立?"} D -->|否| B D -->|是| E[" 修复<br/>实施解决方案"] E --> F["验证修复<br/>确认问题解决"] F --> G{"问题复现?"} G -->|是| A G -->|否| H[" 文档化<br/>记录根因与方案"] style A fill:#bbdefb,stroke:#1565c0 style B fill:#fff9c4,stroke:#f9a825 style C fill:#c8e6c9,stroke:#2e7d32 style E fill:#ffccbc,stroke:#d84315 style H fill:#e1bee7,stroke:#6a1b9a

关键原则

  1. 先量化再定性:用数据描述问题(“延迟从 2ms 升至 800ms”),而非模糊感受(“系统很慢”)
  2. 逐层缩小范围:从系统级 → 子系统级 → 函数级 → 代码行级,像二分查找一样逼近根因
  3. 一次只改一个变量:验证假设时控制变量,避免同时修改多个配置导致无法判断哪个生效
  4. 保留现场:在修复前采集完整现场数据(coredump、trace 日志、/proc 快照),避免”修好了但不知道为什么”

二、场景一:OOM Killer 触发分析——从 dmesg 到 cgroup 内存限制#

1.1 现象#

凌晨 2:17,监控告警显示 Java 进程(PID 28451)消失,业务日志无异常退出记录,进程仿佛”蒸发”了。

1.2 观察:定位 OOM Killer#

第一步,检查内核日志:

dmesg -T | grep -i "oom\|killed process"
# [Sun Apr 19 02:17:03] java invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL), nodemask=(null), order=0, oom_score_adj=0
# [Sun Apr 19 02:17:03] oom-killer: Constraining to node 0
# [Sun Apr 19 02:17:03] Memory cgroup out of memory: Kill process 28451 (java) score 987 or sacrifice child
# [Sun Apr 19 02:17:03] Killed process 28451 (java) total-vm:8388608kB, anon-rss:2097152kB, file-rss:0kB, shmem-rss:0kB

关键信息:Memory cgroup out of memory——这不是系统全局内存不足,而是 cgroup 内存限制被突破。这直接关联到第 6 章:物理内存管理中的 OOM Killer 机制和第 15 章:Cgroups 与 Namespaces中的 memory 控制器。

1.3 假设与验证#

假设 1:cgroup 内存限制设置过小。

# 查看进程所在 cgroup 的内存限制
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
# 2147483648 → 2GB
# 查看当前内存使用
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.usage_in_bytes
# 2147483648 → 已达上限
# 查看 OOM 控制统计
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.oom_control
# oom_kill_disable 0
# under_oom 0
# oom_kill 1 ← 已触发 1 次 OOM Kill

假设 2:JVM 堆内存配置超过 cgroup 限制。

# 检查 JVM 启动参数
ps aux | grep java | grep -o '\-Xmx[0-9]*[mg]'
# -Xmx2048m → 2GB 堆内存
# 但 cgroup 限制也是 2GB!JVM 还有非堆内存(Metaspace、线程栈、直接内存)
# 实际内存占用 = 堆(2GB) + Metaspace(~256MB) + 线程栈(~200MB) + Native内存 → 远超 2GB

1.4 修复#

# 方案一:调大 cgroup 内存限制(治标)
echo 4294967296 > /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
# 方案二:让 JVM 感知 cgroup 限制(JDK 8u191+ / JDK 11+)
# 使用 -XX:+UseContainerSupport(默认开启)+ 限制堆大小
java -XX:MaxRAMPercentage=75.0 -jar app.jar
# JVM 会自动将堆上限设为 cgroup 限制的 75%,留出空间给非堆内存

1.5 内核源码关联#

OOM Killer 的核心逻辑在 mm/oom_kill.c

// mm/oom_kill.c — oom_badness() 决定杀谁
long oom_badness(struct task_struct *p, unsigned long totalpages) {
long points;
// 基于进程的内存占用量计算得分
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
// oom_score_adj 可手动调整优先级
return points;
}

cgroup OOM 的检查路径:memory.usage_in_bytesmem_cgroup_chargetry_chargemem_cgroup_oomoom_kill_process

Note

JDK 8u191 之前的版本不感知 cgroup 内存限制-Xmx2g 会无视容器 2GB 限制直接分配 2GB 堆,加上非堆内存轻松突破 cgroup 上限。这是容器化 Java 应用被 OOM Kill 的最常见原因。


三、场景二:进程 D 状态(Uninterruptible)排查——ps + perf + 内核栈追踪#

2.1 现象#

数据库备份任务卡住,ps 显示进程状态为 D(Uninterruptible Sleep),kill -9 无效。

2.2 观察#

# 确认进程状态
ps -eo pid,stat,comm,wchan | grep " D"
# 31247 D mysqld do_wait
# 查看进程等待的内核函数
cat /proc/31247/wchan
# do_wait
# 查看完整的内核栈
cat /proc/31247/stack
# [<0>] __vfs_get_block+0x0/0x120
# [<0>] ext4_get_block+0x0/0x30
# [<0>] ext4_writepages+0x1a3/0x6e0
# [<0>] do_writepages+0x1b/0x70
# [<0>] __writeback_single_inode+0x36/0xe0
# [<0>] writeback_sb_inodes+0x1b2/0x3f0
# [<0>] __writeback_inodes_wb+0x5e/0xa0
# [<0>] wb_writeback+0xa9/0x2b0
# [<0>] wb_do_writeback+0x4c/0x1a0
# [<0>] wb_workfn+0x3e/0x1e0
# [<0>] process_one_work+0x1a1/0x340
# [<0>] worker_thread+0x30/0x380

内核栈清楚显示:进程卡在 ext4_writepagesext4_get_block__vfs_get_block,即文件系统写回过程中等待块设备 I/O 完成

2.3 假设与验证#

假设:底层块设备 I/O 延迟过高,导致写回无法完成。

# 检查块设备 I/O 延迟
iostat -x 1 3
# Device await r_await w_await %util
# sda 850.3 12.5 980.2 98.7
# w_await = 980ms,写延迟极高!
# 检查是否有 I/O 错误
dmesg | grep -i "sda\|error\|timeout"
# [72341.283] sda: tag#0 timing out command, waited 30s
# [72341.284] sd 0:0:0:0: [sda] tag#0 FAILED Result: hostbyte=DID_TIMEOUT

根因确认:底层磁盘超时,可能是磁盘故障或 HBA 卡问题。D 状态进程无法被杀死,因为第 3 章中已分析——TASK_UNINTERRUPTIBLE 状态的进程不响应信号,只有它等待的 I/O 完成或资源可用才能唤醒。

2.4 修复#

# 短期:将文件系统挂载为只读,停止写回
mount -o remount,ro /data
# 长期:更换故障磁盘,检查 HBA 固件版本
smartctl -a /dev/sda # 查看 SMART 信息

2.5 深入:为什么 D 状态不可杀?#

在内核源码中,信号投递的核心函数 signal_wake_up 检查进程状态:

kernel/signal.c
void signal_wake_up_state(struct task_struct *t, unsigned int state) {
// 只有状态匹配的进程才会被唤醒
// TASK_INTERRUPTIBLE 会被唤醒处理信号
// TASK_UNINTERRUPTIBLE 不会被唤醒——它等待的是内核资源,不是信号
set_tsk_thread_flag(t, TIF_SIGPENDING);
if (!wake_up_state(t, state))
kick_process(t);
}

kill -9(SIGKILL)只是将 TIF_SIGPENDING 标记置位,但 D 状态进程不会被 wake_up_state 唤醒,因此信号无法投递。这是内核的设计选择:D 状态意味着进程正在执行不可中断的内核操作(如等待磁盘 I/O),强行中断可能导致数据不一致。


四、场景三:高负载下的调度延迟分析——perf sched + trace-cmd#

3.1 现象#

系统 load average 飙升至 64(8 核机器),但 CPU 利用率仅 30%,应用响应延迟从 5ms 涨到 500ms。

3.2 观察#

# load average 高但 CPU 利用率低 → 大量进程在等 I/O 或锁
uptime
# load average: 64.12, 58.34, 45.67
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# 2 58 0 123456 89012 890123 0 0 120 8900 1200 3400 12 8 30 50 0
# ↑ b列=58 → 58个进程在D状态等待I/O!

b 列(blocked)= 58,说明大量进程因 I/O 阻塞。但响应延迟是否还与调度有关?

# 使用 perf sched 记录调度事件
perf sched record -- sleep 10
# 分析调度延迟
perf sched latency --sort max
# -----------------------------------------------------------------------------------------------------------------
# -----------------------------------------------------------------------------------------------------------------
# app-worker:28452 | 987.654 | 7890 | 10.123 | 198.432 | 28451.234567 secs |

最大调度延迟 234ms!在第 4 章:进程调度中,我们学到 CFS 通过 vruntime 保证公平性,但当运行队列过长时,即使公平调度,每个进程等待 CPU 的时间也会增加。

3.3 假设与验证#

假设:I/O 瓶颈导致大量 D 状态进程排队,runnable 进程增多,调度延迟上升。

# 用 trace-cmd 追踪调度延迟
trace-cmd record -e sched:sched_switch -e sched:sched_wakeup_new -p function_graph -g sched_tick sleep 5
# 查看每个 CPU 的运行队列长度
cat /proc/schedstat | head -16
# cpu0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
# ...
# 更直观的方式:用 eBPF 追踪调度延迟
bpftrace -e '
tracepoint:sched:sched_wakeup {
@wakeup_pid[args->pid] = nsecs;
}
tracepoint:sched:sched_switch {
$wakeup = @wakeup_pid[args->prev_pid];
if ($wakeup != 0) {
@run_delay = hist((nsecs - $wakeup) / 1000000);
delete(@wakeup_pid, args->prev_pid);
}
}'

3.4 修复#

根因是 I/O 瓶颈(与场景二相关联),但调度层面也有优化空间:

# 方案一:将关键进程设为 SCHED_FIFO 实时调度(第 4 章知识)
chrt -f -p 50 28451 # PID 28451 设为 FIFO 优先级 50
# 方案二:通过 cgroup 限制 I/O 密集型进程的 CPU 份额
echo 512 > /sys/fs/cgroup/cpu/io_heavy/cpu.shares
echo 2048 > /sys/fs/cgroup/cpu/app_critical/cpu.shares
# 方案三:调整内核调度参数
echo 1000000 > /proc/sys/kernel/sched_min_granularity_ns # 最小调度粒度
echo 5000000 > /proc/sys/kernel/sched_wakeup_granularity_ns # 唤醒抢占粒度

五、场景四:文件 I/O 性能瓶颈定位——iostat + blktrace + 页缓存分析#

4.1 现象#

数据库查询延迟 P99 从 10ms 升至 200ms,怀疑 I/O 瓶颈。

4.2 观察:多层 I/O 栈分析#

I/O 请求从用户空间到磁盘要经过多层——这是第 8 章:页缓存与 I/O 路径第 10 章:块设备与 I/O 栈的核心内容:

flowchart TD A["用户 write()"] --> B["VFS 层<br/>Page Cache 命中?"] B -->|命中| C["直接返回<br/>延迟 ~μs"] B -->|未命中| D["文件系统层<br/>ext4_writepages"] D --> E["块设备层<br/>bio → blk-mq"] E --> F["I/O 调度器<br/>Deadline/MQ-Deadline"] F --> G["设备驱动<br/>NVMe/SATA"] G --> H["物理磁盘<br/>延迟 ~ms"] style C fill:#c8e6c9,stroke:#2e7d32 style H fill:#ffccbc,stroke:#d84315

逐层排查:

# 第一层:页缓存命中率
cat /proc/meminfo | grep -E "Cached|Buffers|Dirty|Writeback"
# Cached: 8901234 kB
# Buffers: 89012 kB
# Dirty: 456789 kB ← 脏页积压!
# Writeback: 123456 kB
# 页缓存命中率(通过 /proc/vmstat 计算)
cat /proc/vmstat | grep -E "pgpgin|pgpgout|pswp"
# pgpgin 123456789
# pgpgout 98765432
# 第二层:块设备层统计
iostat -x 1
# Device rrqm/s wrqm/s r/s w/s rMB/s wMB/s await %util
# sda 0.00 45.2 12.3 89.5 0.5 12.3 45.6 78.9
# %util = 78.9%,设备接近饱和
# 第三层:blktrace 追踪 I/O 生命周期
blktrace -d /dev/sda -o - | blkparse -i -
# 8,0 0 28451 A WS 12345678 + 8 <- (8,0) 12345670
# 8,0 0 28451 Q WS 12345678 + 8 [app]
# 8,0 0 28451 G WS 12345678 + 8 [app] ← I/O 调度器入队
# 8,0 0 28451 I WS 12345678 + 8 [app] ← 发往设备
# 8,0 0 28451 D WS 12345678 + 8 [app] ← 设备驱动处理
# 8,0 0 28451 C WS 12345678 + 8 [0] ← I/O 完成

4.3 假设与验证#

假设:脏页写回积压导致 I/O 带宽被写操作占满,读请求被挤占。

# 验证:查看脏页写回参数
cat /proc/sys/vm/dirty_ratio
# 20 ← 脏页达总内存 20% 时,阻塞写操作
cat /proc/sys/vm/dirty_background_ratio
# 10 ← 脏页达 10% 时,后台开始写回
# 当前脏页 456MB / 总内存 16GB = 2.8%,未达阈值
# 但 await = 45ms,说明 I/O 调度器合并了过多请求
# 用 perf 追踪 I/O 延迟分布
perf record -e block:block_rq_issue -e block:block_rq_complete -a sleep 10
perf script | python3 -c "
import sys
issues = {}
for line in sys.stdin:
parts = line.split()
if 'block_rq_issue' in line:
issues[parts[0]] = float(parts[-1])
elif 'block_rq_complete' in line:
dev = parts[0]
if dev in issues:
latency = float(parts[-1]) - issues[dev]
print(f'I/O latency: {latency:.2f} ms')
"

4.4 修复#

# 方案一:调整脏页写回策略,平滑 I/O 负载
echo 5 > /proc/sys/vm/dirty_background_ratio # 更早开始后台写回
echo 10 > /proc/sys/vm/dirty_ratio # 降低阻塞阈值
# 方案二:切换 I/O 调度器(第 10 章知识)
cat /sys/block/sda/queue/scheduler
# [mq-deadline] none bfq
# 对于数据库随机读场景,none(noop)可能更优
echo none > /sys/block/sda/queue/scheduler
# 方案三:使用 cgroup I/O 限流(第 15 章知识)
echo "8:0 1048576" > /sys/fs/cgroup/blkio/io_throttle_write_bps # 限制写带宽 1MB/s

六、场景五:网络丢包排查——dropwatch + tcpdump + ss + Netfilter#

5.1 现象#

应用层统计显示 TCP 重传率 5%,部分请求超时。

5.2 观察:从协议栈各层定位丢包点#

第 12 章:网络协议栈中追踪了数据包从网卡到套接字的完整路径。丢包可能发生在路径上的任何一层:

# 第一层:网卡统计
ethtool -S eth0 | grep -i "drop\|error\|miss"
# rx_dropped: 0
# tx_dropped: 0
# rx_missed_errors: 1234 ← 网卡 FIFO 溢出!
# 第二层:软中断统计
cat /proc/net/softnet_stat
# 00012345 00000000 00000012 ← 第三列非零 = 软中断丢包
# 第三个字段是 time_squeeze:软中断未处理完就被调度器抢占
# 第三层:IP/TCP 层统计
netstat -s | grep -i "drop\|retrans"
# 1234 segments retransmitted
# 567 packet receive errors
# 第四层:Netfilter 丢包
cat /proc/net/stat/nf_conntrack
# entries searched found new invalid delete delete_list insert insert_failed drop early_drop icmp_error expect_new expect_create expect_delete search_restart
# 000003e8 00001234 00000890 00000123 00000045 00000067 00000012 00000089 00000001 00000023 00000000 00000000 00000000 00000000 00000000 00000000
# insert_failed = 1, drop = 0x23 = 35 ← conntrack 表满导致丢包!
# 第五层:套接字层
ss -tm | head -5
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# ESTAB 0 524288 10.0.0.1:8080 10.0.0.2:54321
# Send-Q = 524288 → 发送缓冲区满!

5.3 假设与验证#

假设:conntrack 表满导致新连接被丢弃。

# 查看当前 conntrack 使用量和上限
cat /proc/sys/net/netfilter/nf_conntrack_count
# 65536
cat /proc/sys/net/netfilter/nf_conntrack_max
# 65536 ← 已达上限!
# 用 dropwatch 追踪内核丢包位置
dropwatch -l kas
# Initalizing dropwatch service...
# 1 drops at nf_conntrack_confirm+0x3a ← 确认:conntrack 确认时丢包
# 5 drops at tcp_v4_rcv+0x1f2
# 12 drops at __netif_receive_skb_core+0x8a

5.4 修复#

# 方案一:增大 conntrack 表上限
echo 131072 > /proc/sys/net/netfilter/nf_conntrack_max
# 方案二:缩短 conntrack 超时时间,加速回收
echo 60 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
# 默认 432000 秒(5天),改为 60 秒
# 方案三:对不需要 conntrack 的流量使用 NOTRACK
iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK
# 方案四:增大网卡 Ring Buffer(解决 rx_missed_errors)
ethtool -G eth0 rx 4096 tx 4096

5.5 内核源码关联#

conntrack 丢包的代码路径:

net/netfilter/nf_conntrack_core.c
static int nf_conntrack_confirm(struct sk_buff *skb) {
struct nf_conn *ct = (struct nf_conn *)skb_nfct(skb);
if (ct && !nf_ct_is_confirmed(ct)) {
struct nf_conntrack_tuple_hash *h;
h = nf_conntrack_find_get(ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
if (h) {
// 已存在相同 tuple → 冲突,丢包
NF_CT_STAT_INC_ATOMIC(net, insert_failed);
goto out; // 丢包路径
}
}
}

七、场景六:内存泄漏追踪——slabtop + kmemleak + eBPF#

6.1 现象#

服务器可用内存持续下降,每天减少约 500MB,但进程 RSS 总和远小于已用内存——“内存去哪了”?

6.2 观察#

# 系统内存使用 vs 进程 RSS 总和
free -h
# total used free shared buff/cache available
# Mem: 62Gi 48Gi 2.0Gi 256Mi 12Gi 4.0Gi
# 进程 RSS 总和
ps -eo rss | awk '{sum+=$1} END {printf "%.1f GB\n", sum/1024/1024}'
# 28.5 GB
# 48GB used - 28.5GB RSS = 19.5GB 去哪了?
# buff/cache 只有 12GB,还有 ~7.5GB 差异
# 检查内核 Slab 分配器
slabtop -o -s c | head -20
# OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
# 123456 120000 97% 0.19K 6173 20 98768K dentry
# 98765 95000 96% 0.12K 3023 32 48368K kmalloc-128
# 87654 85000 97% 1.00K 21914 4 350624K ext4_inode_cache
# 65432 63000 96% 0.56K 9347 7 149552K radix_tree_node
# dentry 和 ext4_inode_cache 占用异常高!

6.3 假设与验证#

假设 1:dentry 缓存泄漏(目录项未回收)。

# 查看可回收的 slab 数量
cat /proc/meminfo | grep -E "SReclaimable|SUnreclaim"
# SReclaimable: 8901234 kB ← 可回收 8.5GB!
# SUnreclaim: 4567890 kB
# 手动触发 slab 回收
echo 2 > /proc/sys/vm/drop_caches
echo 3 > /proc/sys/vm/drop_caches
# 回收后检查
free -h
# Mem: 62Gi 38Gi 12Gi 256Mi 12Gi 14Gi
# 回收了约 10GB,但还在持续增长 → 确认有泄漏

假设 2:内核内存泄漏(kmalloc 后未 kfree)。

# 启用 kmemleak(需要内核配置 CONFIG_DEBUG_KMEMLEAK)
echo scan > /sys/kernel/debug/kmemleak
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak
# unreferenced object 0xffff888123456000 (size 256):
# comm "kworker/0:1", pid 12, jiffies 4294967295 (age 3600.000s)
# hex dump (first 32 bytes):
# 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
# backtrace:
# [<ffffffff81234567>] kmalloc_trace+0x27/0x60
# [<ffffffff82345678>] ext4_xattr_block_get+0x128/0x2a0
# [<ffffffff83456789>] ext4_xattr_get+0x89/0x1a0

6.4 用 eBPF 精确定位#

# 使用 bpftrace 追踪 kmalloc/kfree 调用对
bpftrace -e '
tracepoint:kmem:kmalloc {
@alloc[args->ptr] = args->bytes_alloc;
@alloc_info[args->ptr] = kstack;
}
tracepoint:kmem:kfree {
delete(@alloc, args->ptr);
delete(@alloc_info, args->ptr);
}
interval:s:10 {
printf("Unfreed allocations:\n");
print(@alloc_info);
clear(@alloc);
clear(@alloc_info);
}'

6.5 修复#

# 短期:调整 slab 回收水位
echo 300 > /proc/sys/vm/vfs_cache_pressure # 默认 100,增大加速回收
# 长期:修复内核模块的内存泄漏(如果 kmemleak 定位到具体模块)
# 或升级内核版本(某些泄漏是已知 bug)

八、场景七:容器性能隔离验证——cgroup 限制效果实测#

7.1 现象#

容器 A 设置了 CPU 限制为 2 核、内存限制 4GB,但在宿主机负载高时,容器 A 的性能仍受影响——cgroup 隔离”不灵”?

7.2 验证 CPU 隔离#

第 15 章中已分析,cgroup v1 的 cpu 控制器使用 cpu.shares(相对权重)而非绝对限制。CFS 在 CPU 空闲时允许容器使用超过限额的 CPU:

# 容器 A 的 CPU 限制
cat /sys/fs/cgroup/cpu/docker/<container_a>/cpu.cfs_quota_us
# 200000 ← 200ms/100ms = 2 核
cat /sys/fs/cgroup/cpu/docker/<container_a>/cpu.cfs_period_us
# 100000
# 在容器 A 内跑 CPU 密集任务
docker exec container_a stress-ng --cpu 8 --timeout 60
# 在宿主机观察:容器 A 实际使用了多少 CPU?
# 当宿主机 CPU 空闲时:
cat /sys/fs/cgroup/cpu/docker/<container_a>/cpuacct.usage
# 容器可能使用 8 核!CFS quota 只在 CPU 争抢时生效

7.3 验证内存隔离#

# 容器 A 内存限制
cat /sys/fs/cgroup/memory/docker/<container_a>/memory.limit_in_bytes
# 4294967296 → 4GB
# 在容器 A 内分配超过 4GB 内存
docker exec container_a stress-ng --vm 4 --vm-bytes 2G --timeout 30
# 观察结果:
# 方式一:OOM Kill(默认行为)
dmesg | tail -5
# Memory cgroup out of memory: Kill process ...
# 方式二:关闭 OOM Kill 后,进程在物理内存分配时被阻塞
echo 1 > /sys/fs/cgroup/memory/docker/<container_a>/memory.oom_control
# 此时进程会卡在 page fault 处理中,等待内存回收

7.4 验证 I/O 隔离#

# cgroup v1 的 blkio 控制器限制
cat /sys/fs/cgroup/blkio/docker/<container_a>/blkio.throttle.read_bps_device
# 8:0 104857600 ← 限制读 100MB/s
# 实测:容器内顺序读
docker exec container_a dd if=/dev/sda of=/dev/null bs=1M count=1000
# 宿主机观察实际 I/O 速率
iostat -x 1 | grep sda
# 当宿主机 I/O 空闲时:容器可能超过 100MB/s 限制!
# blkio throttle 在某些路径上不生效(如 Direct I/O + buffer)

7.5 关键发现#

资源cgroup v1 隔离效果失效场景
CPUquota 在争抢时生效,空闲时可超限CPU 空闲时容器可突破限额
内存硬限制,超限触发 OOM Kill页缓存共享、slab 共享无法隔离
I/Othrottle 对缓冲 I/O 效果有限Direct I/O、页缓存写回绕过限制
网络无原生 cgroup 网络带宽限制需配合 TC + net_cls
# 更可靠的隔离方案:cgroup v2 + CPUSET
# 将容器绑定到专用 CPU 核心
echo 0-1 > /sys/fs/cgroup/cpuset/docker/<container_a>/cpuset.cpus
# 这样容器 A 只能在 CPU 0-1 上运行,无论宿主机多空闲

九、场景八:系统启动优化——systemd-analyze + initramfs 精简#

8.1 现象#

云服务器启动耗时 45 秒,影响弹性扩容速度。

8.2 观察#

第 19 章:Linux 启动流程中追踪了从 startup_64systemd 的完整路径。现在用工具量化每个阶段的耗时:

# 总体启动时间
systemd-analyze
# Startup finished in 8.234s (firmware) + 3.456s (loader) + 2.123s (kernel) + 31.789s (userspace) = 45.602s
# userspace 占 70%!
# 各服务启动时间
systemd-analyze blame | head -20
# 12.345s NetworkManager-wait-online.service
# 8.901s docker.service
# 5.678s firewalld.service
# 3.456s tuned.service
# 2.345s postfix.service
# ...
# 关键路径分析
systemd-analyze critical-chain
# graphical.target @31.789s
# └─multi-user.target @31.787s
# └─docker.service @31.787s
# └─network-online.target @22.885s
# └─NetworkManager-wait-online.service @22.885s
# └─NetworkManager.service @10.540s

8.3 假设与验证#

假设 1NetworkManager-wait-online.service 不必要地等待网络完全就绪。

# 查看该服务的作用
systemctl cat NetworkManager-wait-online.service
# [Unit]
# Description=Network Manager Wait Online
# Before=network-online.target ← 阻塞所有依赖网络的目标
# 大多数服务不需要网络"完全就绪",只需要网络接口存在
# 禁用等待
systemctl disable NetworkManager-wait-online.service
# 节省 ~12 秒

假设 2:initramfs 过大,解压耗时。

# 查看 initramfs 大小
ls -lh /boot/initramfs-$(uname -r).img
# -rw------- 1 root root 89M ← 89MB!
# 分析 initramfs 内容
lsinitrd /boot/initramfs-$(uname -r).img | head -30
# 包含了不必要的驱动:nfs、iscsi、raid、dm-crypt...
# 精简 initramfs(只包含当前硬件需要的驱动)
dracut --force --no-hostonly-default /boot/initramfs-$(uname -r).img $(uname -r)
# 或
dracut --force --hostonly /boot/initramfs-$(uname -r).img $(uname -r)
# 精简后
ls -lh /boot/initramfs-$(uname -r).img
# -rw------- 1 root root 21M ← 从 89MB 降至 21MB

8.4 修复汇总#

# 1. 禁用不必要的服务
systemctl disable NetworkManager-wait-online.service
systemctl disable postfix.service # 不需要邮件服务
systemctl mask lvm2-monitor.service # 不使用 LVM
# 2. 精简 initramfs
dracut --force --hostonly /boot/initramfs-$(uname -r).img $(uname -r)
# 3. 内核命令行优化(第 19 章知识)
# /etc/default/grub
GRUB_CMDLINE_LINUX="quiet rhgb console=tty0 mitigations=off"
# mitigations=off 关闭 Spectre/Meltdown 缓解措施,提升性能(安全场景慎用)
grub2-mkconfig -o /boot/grub2/grub.cfg
# 4. 并行启动服务
# /etc/systemd/system.conf
DefaultTimeoutStartSec=10s # 降低服务启动超时

优化后验证:

systemd-analyze
# Startup finished in 8.234s (firmware) + 3.456s (loader) + 1.890s (kernel) + 12.345s (userspace) = 25.925s
# 从 45 秒降至 26 秒,提升 42%

十、综合方法论:问题定位的通用框架#

8 个场景看似各异,但遵循统一的方法论。将它们抽象为一张全景图:

flowchart TD subgraph "第一步:现象分类" A1["性能下降"] --> B["USE 方法检查"] A2["功能异常"] --> C["日志 + 状态检查"] A3["资源耗尽"] --> D["配额 + 使用量检查"] end subgraph "第二步:逐层下钻" B --> E["CPU: perf top / perf record"] B --> F["内存: slabtop / proc/meminfo"] B --> G["I/O: iostat / blktrace"] B --> H["网络: ss / tcpdump / dropwatch"] C --> I["dmesg / journalctl"] C --> J["/proc/pid/stack"] C --> K["ftrace / trace-cmd"] D --> L["cgroup 限制检查"] D --> M["OOM 日志分析"] D --> N["conntrack / fd 统计"] end subgraph "第三步:根因确认" E --> O["源码确认<br/>Bootlin Elixir"] F --> O G --> O H --> O I --> O J --> O K --> O L --> O M --> O N --> O end O --> P["修复 + 验证 + 文档化"] style B fill:#bbdefb,stroke:#1565c0 style C fill:#bbdefb,stroke:#1565c0 style D fill:#bbdefb,stroke:#1565c0 style O fill:#ffccbc,stroke:#d84315 style P fill:#c8e6c9,stroke:#2e7d32

核心心法

  1. USE 方法(Brendan Gregg):对每个资源检查 Utilization(使用率)、Saturation(饱和度)、Errors(错误)。场景一检查了内存的 saturation(cgroup 满载),场景五检查了网络的 errors(丢包)。
  2. 红线优先:先排除最危险的情况——内核 panic、磁盘故障、OOM——再处理性能问题。
  3. 工具组合:单一工具往往不够。场景四需要 iostat(宏观)+ blktrace(微观)+ perf(关联)的组合;场景五需要 dropwatch(定位丢包点)+ tcpdump(确认内容)+ ss(查看缓冲区)的配合。
  4. 源码是终极真相:当工具给出的信息不足以确认根因时,阅读内核源码。8 个场景都关联了具体的内核源码路径——这不是巧合,而是因为所有用户态工具的输出都源自内核数据结构。

十一、动手实践#

实践一:OOM Killer 复现与分析#

# 1. 创建一个 cgroup 并设置小内存限制
sudo mkdir /sys/fs/cgroup/memory/oom_test
echo 52428800 > /sys/fs/cgroup/memory/oom_test/memory.limit_in_bytes # 50MB
# 2. 在 cgroup 中运行内存分配程序
sudo cgexec -g memory:oom_test stress-ng --vm 1 --vm-bytes 100M --timeout 30
# 3. 观察 OOM 事件
dmesg | tail -20
# 4. 检查 cgroup OOM 统计
cat /sys/fs/cgroup/memory/oom_test/memory.oom_control
# 思考题:如果设置 oom_kill_disable=1,进程会怎样?

实践二:D 状态进程构造与排查#

# 1. 使用 vfork() 创建 D 状态进程(子进程不 exec 时父进程 D 状态等待)
python3 -c "
import os, time
pid = os.fork()
if pid == 0:
time.sleep(300) # 子进程长时间运行
else:
os.waitpid(pid, 0) # 父进程 D 状态等待
" &
# 2. 观察进程状态
ps -eo pid,stat,comm | grep " D"
# 3. 查看内核栈
cat /proc/<pid>/stack
# 4. 尝试 kill -9,观察结果
kill -9 <pid> # 无效!
# 思考题:如何在不杀死子进程的情况下让父进程脱离 D 状态?

实践三:调度延迟测量#

# 1. 制造 CPU 争抢
taskset -c 0 stress-ng --cpu 4 --timeout 60 &
# 2. 用 perf sched 记录调度事件
sudo perf sched record -- sleep 10
# 3. 分析调度延迟
sudo perf sched latency --sort max
# 4. 用 bpftrace 测量唤醒延迟
sudo bpftrace -e '
tracepoint:sched:sched_wakeup {
@wakeup[args->pid] = nsecs;
}
tracepoint:sched:sched_switch {
$t = @wakeup[args->next_pid];
if ($t > 0) {
@delay = hist((nsecs - $t) / 1000);
delete(@wakeup, args->next_pid);
}
}'
# 思考题:将 stress-ng 进程设为 SCHED_IDLE 后,延迟如何变化?

实践四:I/O 瓶颈模拟与定位#

# 1. 制造 I/O 压力
fio --name=randwrite --ioengine=libaio --iodepth=64 --rw=randwrite --bs=4k \
--direct=1 --size=1G --numjobs=4 --runtime=60 --time_based &
# 2. 多窗口同时观察
# 窗口 1: iostat -x 1
# 窗口 2: cat /proc/meminfo (观察 Dirty 页变化)
# 窗口 3: perf record -e block:block_rq_issue -a sleep 10
# 3. 分析 blktrace
sudo blktrace -d /dev/sda -o - | blkparse -i - | head -50
# 思考题:切换 I/O 调度器为 none 后,延迟有何变化?为什么?

实践五:网络丢包模拟与追踪#

# 1. 用 tc 模拟丢包
sudo tc qdisc add dev lo root netem loss 5%
# 2. 测试丢包率
ping -c 100 127.0.0.1 | tail -5
# 3. 用 dropwatch 追踪丢包位置
sudo dropwatch -l kas
# 4. 用 tcpdump 抓包分析
sudo tcpdump -i lo -w /tmp/drop.pcap
# 在另一个终端运行 curl http://127.0.0.1:8080
# 5. 清理
sudo tc qdisc del dev lo root netem
# 思考题:如何区分是网络层丢包还是应用层丢包?

实践六:内存泄漏检测#

# 1. 启用 kmemleak(需要内核支持)
echo scan > /sys/kernel/debug/kmemleak 2>/dev/null || \
echo "kmemleak 未启用,需要 CONFIG_DEBUG_KMEMLEAK=y"
# 2. 用 bpftrace 追踪 kmalloc/kfree 不匹配
sudo bpftrace -e '
tracepoint:kmem:kmalloc {
@size[args->ptr] = args->bytes_alloc;
}
tracepoint:kmem:kfree {
delete(@size, args->ptr);
}
interval:s:5 {
printf("Potential leaks: %d allocations\n", count(@size));
}'
# 3. 用 slabtop 监控 slab 变化趋势
slabtop -d 5 -o -s c | head -15
# 思考题:dentry 缓存持续增长一定是泄漏吗?什么情况下是正常的?

实践七:容器隔离效果测试#

# 1. 启动一个 CPU 限制为 1 核的容器
docker run -d --name cpu_test --cpus=1 stress-ng --cpu 4 --timeout 60
# 2. 在容器内观察 CPU 使用
docker stats cpu_test
# 当宿主机 CPU 空闲时,容器是否真的只用 1 核?
# 3. 在宿主机启动 CPU 压力
stress-ng --cpu $(nproc) --timeout 30 &
# 4. 再次观察容器 CPU 使用
docker stats cpu_test
# 争抢时容器是否被限制在 1 核?
# 5. 测试内存隔离
docker run -d --name mem_test -m 256m stress-ng --vm 2 --vm-bytes 200M --timeout 30
docker logs mem_test --follow
# 思考题:为什么 -m 256m 的容器在宿主机 free -h 中看到的内存使用可能超过 256MB?

实践八:启动时间优化#

# 1. 记录当前启动时间
systemd-analyze > /tmp/before.txt
systemd-analyze blame > /tmp/before_blame.txt
# 2. 分析关键路径
systemd-analyze critical-chain
# 3. 禁用不必要的服务(根据你的环境选择)
sudo systemctl disable NetworkManager-wait-online.service
sudo systemctl disable ModemManager.service
sudo systemctl disable avahi-daemon.service
# 4. 查看 initramfs 大小
ls -lh /boot/initramfs-*
# 5. 精简 initramfs(CentOS/RHEL)
sudo dracut --force --hostonly /boot/initramfs-$(uname -r).img $(uname -r)
# 6. 重启后对比
systemd-analyze > /tmp/after.txt
diff /tmp/before.txt /tmp/after.txt
# 思考题:为什么 --hostonly 精简的 initramfs 在更换硬件后可能导致无法启动?

十二、系列总结与进阶路线图#

20 章知识图谱回顾#

第 1 章的内核架构全景到本章的综合实战,走过了 Linux 内核的每一个核心子系统:

阶段章节核心收获
认知建立Ch1-2用户/内核空间隔离、系统调用是连接两界的桥梁
进程世界Ch3-5task_struct 是进程的”身份证”、CFS 保证公平、信号是内核与进程的”电话”
内存迷宫Ch6-8伙伴系统管物理页、VMA 管虚拟地址、页缓存是 I/O 性能的命脉
持久存储Ch9-11VFS 统一文件接口、blk-mq 重构块设备、设备模型用 kobject 组织一切
并发与隔离Ch12-16sk_buff 穿越协议栈、中断分两半、RCU 读无锁、容器是视角+配额
观测与扩展Ch17-19内核模块动态扩展、eBPF 可编程观测、启动流程串联所有子系统
综合实战Ch20观察→假设→验证→修复,8 个场景串联全部知识

进阶路线图#

flowchart TD A["本系列完成<br/>Linux 内核核心机制"] --> B{选择方向} B --> C[" 内核开发"] B --> D[" 性能工程"] B --> E[" 云原生基础设施"] B --> F[" 安全与合规"] C --> C1["Linux Device Drivers<br/>设备驱动开发"] C1 --> C2["内核社区贡献<br/>kernelnewbies.org"] C2 --> C3["LWN.net<br/>内核开发周报"] D --> D1["Brendan Gregg<br/>Systems Performance"] D1 --> D2["eBPF 深入<br/>BCC/BPF CO-RE"] D2 --> D3["持续性能分析<br/>Parca/Pixie"] E --> E1["容器运行时<br/>containerd/CRI-O"] E1 --> E2["Kubernetes 调度器<br/>cgroup v2 集成"] E2 --> E3["eBPF 网络与安全<br/>Cilium/Tetragon"] F --> F1["SELinux 策略开发"] F1 --> F2["Seccomp-BPF 沙箱"] F2 --> F3["内核漏洞分析<br/>CVE 复现"] style A fill:#e1bee7,stroke:#6a1b9a style C fill:#bbdefb,stroke:#1565c0 style D fill:#c8e6c9,stroke:#2e7d32 style E fill:#fff9c4,stroke:#f9a825 style F fill:#ffccbc,stroke:#d84315

推荐进阶阅读

方向书籍/资源为什么
内核开发Linux Device Drivers (Corbet et al.)设备驱动开发的权威指南
内核开发LWN.net内核社区最权威的新闻与深度分析
性能工程Systems Performance (Brendan Gregg)性能分析的百科全书,本系列多次引用
性能工程BPF Performance Tools (Brendan Gregg)eBPF 工具的完整参考
云原生Kubernetes in Action (Marko Lukša)理解容器编排如何使用内核能力
安全Linux Kernel Security (William Cohen)内核安全机制的工程实践
源码阅读Bootlin Elixir在线内核源码交叉引用,本系列全程使用

最后的建议:Linux 内核是一个持续演进的活体——6.x 内核引入了 Rust 语言支持、MGLRU 内存回收策略、eBPF 程序类型持续扩展。本系列建立的知识框架不会过时,但具体实现细节会随版本变化。保持阅读 LWN.net、跟踪内核 changelog、在真实环境中实践,才是持续精进的唯一路径。


“理论是灰色的,而生命之树常青。” ——歌德

20 章的理论已经铺就,现在,去真实的服务器上实践吧。


参考资料#

  1. Brendan Gregg, Systems Performance: Enterprise and the Cloud, 2nd Edition, Addison-Wesley, 2020 — 性能分析方法论(USE 方法)与工具链的权威参考
  2. Brendan Gregg, BPF Performance Tools, Addison-Wesley, 2019 — eBPF 性能工具的完整指南
  3. Daniel P. Bovet, Marco Cesati, Understanding the Linux Kernel, 3rd Edition, O’Reilly, 2005 — 内核各子系统的经典教材
  4. Robert Love, Linux Kernel Development, 3rd Edition, Addison-Wesley, 2010 — 内核设计与实现的实践导向参考
  5. Jonathan Corbet, Alessandro Rubini, Greg Kroah-Hartman, Linux Device Drivers, 3rd Edition, O’Reilly, 2005 — 设备驱动开发权威指南
  6. Linux 内核源码 (v6.12), https://git.kernel.org — 本章引用的源码路径:mm/oom_kill.ckernel/signal.cnet/netfilter/nf_conntrack_core.c
  7. Brendan Gregg, Linux Performance, https://www.brendangregg.com/linuxperf.html — 性能分析工具与方法的在线参考
  8. LWN.net, https://lwn.net/ — 内核开发社区新闻与深度分析
  9. Bootlin Elixir Cross Referencer, https://elixir.bootlin.com/ — 在线内核源码浏览与交叉引用
  10. systemd 官方文档, https://www.freedesktop.org/wiki/Software/systemd/ — 启动优化参考
  11. Cgroups v2 Documentation, https://www.kernel.org/doc/Documentation/admin-guide/cgroup-v2.rst — 容器资源隔离的内核文档

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

部分信息可能已经过时

相关文章 智能推荐
1
追踪与可观测性
Linux内核 深入解析 Linux 内核追踪与可观测性——ftrace 函数追踪器、perf 性能计数器分析、eBPF 革命性可编程框架(程序类型/Map/验证器)、bpftrace 一行命令接口、BCC 工具集——从 printk 到 eBPF 的可观测性演进。
2
系列导读
eBPF 本系列从 eBPF 的底层原理出发,系统讲解 eBPF 虚拟机、验证器、Map 数据结构、Hook 机制、CO-RE 可移植性,再到 XDP/TC 网络处理、LSM 安全、Cilium 实践、Wasm 融合、Kubernetes 集成与生产部署,带你从「听说过 eBPF」进阶到「能用 eBPF 解决真实问题」。
3
系列导读
Linux内核 本系列从 Linux 内核的用户可见接口出发,自顶向下剖析现代 Linux 操作系统的所有核心机制——进程调度、内存管理、文件系统、网络协议栈、同步与容器,每章配有可在自己系统上验证的实践操作,让你从「会用 Linux」进阶到「理解 Linux」。
4
块设备与 I/O 栈
Linux内核 深入解析 Linux 块设备与 I/O 栈——bio 请求结构、通用块层与 blk-mq 多队列设计、I/O 调度器演进(CFQ→Deadline→BFQ)、I/O 合并与排序、NVMe 驱动模型——从 I/O 请求到磁盘的完整路径。
5
内核架构全景
Linux内核 宏观视角下的 Linux 内核设计——用户空间与内核空间的隔离、七大子系统概览、执行上下文、内核通用数据结构,以及 /proc 和 /sys 文件系统——内核向用户空间暴露信息的窗口。