🐧 《Linux内核技术实战:从Page Cache到CPU调度的深度解构》博客大纲(26讲精编版)
博客定位 :不讲理论堆砌,只解决真实线上问题
每讲结构 :故障现象 → 根因分析 → 内核机制 → 实验复现 → 修复方案 → 长期预防
配套资源 :可运行的 eBPF 脚本 / perf 命令集 / sysctl 调优模板 / 内核模块 demo
实验环境:华为云香港 ECS ecs-ee63 集群 (4×c6.large.2, Ubuntu 24.04)
🔹 开篇篇(1讲|建立内核思维)
第1讲:如何让Linux内核更好地服务应用程序?
🎯 核心问题
为什么"配置调优"常失效? ------ 缺乏对内核行为的可观测性与因果链理解。
🧠 三大认知升级
升级 1:内核不是黑盒
text
┌─────────────────────────────────────────────────────────────┐
│ Linux 内核可观测接口 │
├─────────────────────────────────────────────────────────────┤
│ │
│ /proc 文件系统 运行时内核数据 │
│ ├── /proc/meminfo 内存使用详情 │
│ ├── /proc/vmstat 虚拟内存统计 │
│ ├── /proc/slabinfo 内核对象缓存 │
│ ├── /proc/interrupts 中断统计 │
│ └── /proc/net/dev 网络接口统计 │
│ │
│ /sys 文件系统 内核参数动态调整 │
│ ├── /sys/kernel/mm/ 内存管理参数 │
│ ├── /sys/class/net/ 网络设备参数 │
│ └── /sys/fs/cgroup/ 控制组参数 │
│ │
│ tracefs (ftrace) 内核函数追踪 │
│ ├── available_events 所有可追踪事件 │
│ └── tracing/ 追踪控制接口 │
│ │
│ eBPF 动态内核探针 │
│ ├── kprobe 任意内核函数插入探针 │
│ ├── tracepoint 稳定内核追踪点 │
│ └── uprobe 用户态函数探针 │
│ │
└─────────────────────────────────────────────────────────────┘
升级 2:性能问题 = 资源争用 + 策略失配 + 误用接口
text
性能问题分类:
1. 资源争用 (Resource Contention)
├── CPU: 多进程争用同一个 CPU 核心
├── 内存: Page Cache 回收 vs 应用分配
├── I/O: 磁盘 IOPS 争用
└── 网络: 网卡带宽打满
2. 策略失配 (Policy Mismatch)
├── 调度策略: CFS 不适合实时任务
├── 内存策略: NUMA 访问远程内存
└── 网络策略: 默认 QoS 不适合视频流
3. 误用接口 (Misused API)
├── 频繁 fsync() 导致 I/O 风暴
├── 错误使用 O_DIRECT 反而降低性能
└── 在多线程程序中用 fork()
升级 3:最佳实践 ≠ 参数调优,而是架构适配内核行为
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 数据库 | 盲目调大 vm.swappiness |
理解 Page Cache 优先级,使用 mlock() 保护关键页 |
| 网络服务器 | 调大 net.core.somaxconn 就够 |
同时调整应用 listen() backlog 和 net.ipv4.tcp_max_syn_backlog |
| 实时应用 | 设置 nice -20 |
使用 SCHED_FIFO + 隔离 CPU 核心 + 关闭中断迁移 |
🔬 实战预览:用 bpftrace 一行脚本定位高延迟请求
bash
# 问题:某 Web 服务偶发延迟 > 1s,但 CPU/内存/网络都正常
# 使用 bpftrace 追踪所有 > 100ms 的系统调用
bpftrace -e '
kprobe:do_syscall_64 {
@start[tid] = nsecs;
}
kretprobe:do_syscall_64 /@start[tid]/ {
$latency = (nsecs - @start[tid]) / 1000000;
if ($latency > 100) {
printf("PID %d (%s): syscall %d took %d ms\n",
pid, comm, arg0, $latency);
}
delete(@start[tid]);
}
'
# 输出示例:
# PID 12345 (nginx): syscall 0 took 150 ms ← 读文件慢?
# PID 23456 (mysql): syscall 1 took 1200 ms ← 写磁盘慢?
诊断结论 :syscall 0 = read(),syscall 1 = write()。进一步用 ext4:ext4_sync_file_enter tracepoint 定位是文件系统 fsync 导致的延迟。
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 盲目跟风调优 | 照搬博客参数不验证效果 | 每次只改一个参数,用基准测试验证 |
| 2 | 🟡 忽略内核版本差异 | 旧版本内核参数在新版本无效 | 查内核文档 Documentation/admin-guide/sysctl/ |
| 3 | 🟡 生产环境直接试 | 在线上机器直接改 /proc/sys/ |
先在测试环境验证,再用 Ansible 批量部署 |
| 4 | 🟢 过度依赖监控面板 | 只看 Grafana,不理解底层指标 | 学会用 perf/bpftrace 做深度诊断 |
🔭 扩展思考
- 如何系统学习 Linux 内核? → 从《Linux 内核设计与实现》入手,配合
lxr.melnichenko.msu.ru在线代码浏览 - eBPF 会取代 ftrace 吗? → 不会,ftrace 更低开销,适合生产环境长期开启
- 容器化后内核调优还重要吗? → 更重要!容器共享内核,一个容器的错误调优影响所有容器
🔹 Page Cache 管理问题(5讲|内存子系统核心)
第2讲:如何用数据观测Page Cache?
🎯 核心问题
Page Cache 占了 80% 内存,但应用还是 OOM 了? ------ 你需要学会正确观测 Page Cache。
📊 关键指标监控
基础命令
bash
# 实时观测
cat /proc/meminfo | grep -E "Cached|SReclaimable|Dirty|Writeback"
# 输出示例:
# Cached: 8192000 kB ← Page Cache 总量
# SReclaimable: 512000 kB ← 可回收的 slab 缓存 (dentry/inode)
# Dirty: 2048 kB ← 待写回磁盘的数据
# Writeback: 0 kB ← 正在写回磁盘的数据
vmstat 1 5 # 查看 cache/buff 变化
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 1 0 0 123456 78901 8192000 0 0 12 34 123 456 1 1 98 0 0
sar -r 1 # cache 使用趋势 (sysstat 包)
# Linux 6.8.0-52-generic (hostname) 06/06/2026 _x86_64_ (4 CPU)
# 12:00:01 PM kbmemfree kbmemused %memused kbbuffers kbcached kbdirty kbbuffers
# 12:00:02 PM 123456 7890123 98.45 78901 8192000 2048 78901
高级观测:按文件查看 Page Cache
bash
# 查看某个文件在 Page Cache 中的页数
vmtouch -v /var/log/nginx/access.log
# Output:
# Files: 1
# Directories: 0
# File Page Cache Info:
# File: /var/log/nginx/access.log
# File Size: 1048576 (1024 pages)
# Resident Pages: 512/1024 50.0% ← 50% 在 Cache 中
# Elapsed: 0.001223 seconds
# 查看某个进程占用的 Page Cache
ps -p 12345 -o pid,rss,vsz,comm
cat /proc/12345/smaps | grep -E "Rss|FilePmdMapped"
🔬 eBPF 实战:统计每个进程的 Page Cache 命中/未命中
bash
bpftrace -e '
kprobe:__page_cache_alloc {
@alloc[pid, comm] = count();
}
kretprobe:__page_cache_alloc /retval/ {
@hit[pid, comm] = count();
}
interval:1s {
printf("%-10s %-20s %10s %10s %8s\n",
"PID", "Comm", "Alloc", "Hit", "Hit%");
foreach (key in @alloc) {
$pid = key[0];
$comm = str(key[1]);
$alloc = @alloc[key];
$hit = @hit[key];
$ratio = $hit * 100 / $alloc;
printf("%-10d %-20s %10d %10d %7.1f%%\n",
$pid, $comm, $alloc, $hit, $ratio);
}
clear(@alloc);
clear(@hit);
}
'
输出示例:
text
PID Comm Alloc Hit Hit%
12345 nginx 1000 950 95.0%
23456 mysql 500 480 96.0%
34567 python 200 50 25.0% ← 大量未命中!
📊 Page Cache 相关 /proc 文件详解
| 文件 | 关键字段 | 含义 |
|---|---|---|
/proc/meminfo |
Cached |
Page Cache 总量 (含文件映射) |
/proc/meminfo |
SReclaimable |
可回收的 slab 缓存 |
/proc/vmstat |
pgpgin/pgpgout |
Page Cache 换入/换出计数 |
/proc/vmstat |
pgscan_kswapd |
kswapd 扫描页数 |
/proc/zoneinfo |
nr_inactive_file |
非活跃文件页数量 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 用 free -h 判断内存是否充足 | available 包含可回收的 Page Cache,不代表可用内存 |
看 SReclaimable + Cached 的实际可回收量 |
| 2 | 🟡 认为 Page Cache 越多越好 | 过多 Page Cache 导致 kswapd 频繁回收,引发延迟 | 调整 vm.vfs_cache_pressure |
| 3 | 🟡 忽略 Dirty Page 积累 | 大量 Dirty Page 写回时阻塞应用 | 调整 vm.dirty_ratio 和 vm.dirty_background_ratio |
| 4 | 🟢 容器环境下误读 /proc/meminfo | 容器内的 /proc/meminfo 显示宿主机内存 | 读 /sys/fs/cgroup/memory/memory.stat |
🔭 扩展思考
- Page Cache 和 buffer cache 有什么区别? → Linux 2.4+ 已统一,buffer cache 是块设备层的缓存,Page Cache 是页缓存,现在 buffer cache 是 Page Cache 的一部分
- 如何强制回收 Page Cache? →
echo 1 > /proc/sys/vm/drop_caches(仅测试环境!) - mmap() 和 read() 哪个更高效? → 随机访问用 mmap(),顺序读用 read() + 预读
第3讲:Page Cache是怎样产生和释放的?
🎯 核心问题
理解 Page Cache 的生命周期,才能知道为什么它有时回收太快,有时回收太慢。
📐 内核机制图解
Page Cache 产生流程
text
┌─────────────────────────────────────────────────────────────┐
│ Page Cache 产生流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 应用层调用 read()/write() │
│ │ │
│ ▼ │
│ VFS 层: generic_file_read_iter() │
│ ├── 检查页缓存中是否有对应 page │
│ ├── 有 → 直接返回数据 (Cache Hit) │
│ └── 无 → 触发缺页异常 (Page Fault) │
│ │ │
│ ▼ │
│ 缺页处理: do_read_fault() │
│ ├── 分配新页: alloc_page(GFP_KERNEL) │
│ ├── 添加到页缓存: add_to_page_cache_lru() │
│ │ ├── 添加到 address_space->i_pages (XA_ARRAY) │
│ │ └── 添加到 LRU 链表 (inactive_list) │
│ └── 从磁盘读取数据: mapping->a_ops->readpage() │
│ │ │
│ ▼ │
│ 预读 (Readahead): │
│ ├── 顺序读 → 触发异步预读 (readahead) │
│ └── 随机读 → 不预读 │
│ │
└─────────────────────────────────────────────────────────────┘
关键数据结构
c
/* include/linux/fs.h */
struct address_space {
struct inode *host; /* 所属 inode */
struct xarray i_pages; /* 页缓存 radix tree */
unsigned long nrpages; /* 缓存页数 */
const struct address_space_operations *a_ops; /* 操作函数集 */
/* ... */
};
/* include/linux/mm_types.h */
struct page {
/* ... */
union {
struct { /* Page Cache 相关字段 */
struct list_head lru; /* LRU 链表指针 */
struct address_space *mapping; /* 所属的 address_space */
pgoff_t index; /* 文件内偏移 (页大小单位) */
/* ... */
};
/* ... */
};
};
/* mm/vmscan.c */
struct lruvec {
struct list_head lists[NR_LRU_LISTS]; /* LRU 链表数组 */
/* NR_LRU_LISTS = LRU_INACTIVE_ANON + LRU_ACTIVE_ANON +
LRU_INACTIVE_FILE + LRU_ACTIVE_FILE + ... */
};
Page Cache 释放流程
text
┌─────────────────────────────────────────────────────────────┐
│ Page Cache 释放流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 触发回收的三种场景: │
│ │
│ 1. 内存不足 (Direct Reclaim) │
│ └── __alloc_pages() 失败 → call reclaim_pages() │
│ │
│ 2. kswapd 后台回收 │
│ └── kswapd() 线程定期扫描 LRU 链表 │
│ │
│ 3. 手动回收 │
│ └── echo 1 > /proc/sys/vm/drop_caches │
│ │
│ 回收流程: │
│ ┌─────────────────────────────────────────┐ │
│ │ 1. 扫描 LRU 链表 (shrink_inactive_list) │ │
│ │ ├── 尝试回收 inactive_list 中的页 │ │
│ │ └── 回收失败 → 移动到 active_list │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 2. 回写脏页 (shrink_page_list) │ │
│ │ ├── 脏页 → 添加到回写队列 │ │
│ │ └── 调用 writepage() 写回磁盘 │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 3. 释放页 (shrink_active_list) │ │
│ │ ├── 从 LRU 链表移除 │ │
│ │ ├── 从 address_space->i_pages 移除 │ │
│ │ └── 释放页 (free_page) │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
🔬 实验:用 perf record 观察顺序读 vs 随机读的缺页差异
bash
# 准备测试文件
dd if=/dev/urandom of=/tmp/testfile bs=1M count=100
# 实验 1: 顺序读
perf record -e page-faults -o perf-seq.data \
cat /tmp/testfile > /dev/null
# 实验 2: 随机读 (用 dd 跳过读取)
perf record -e page-faults -o perf-rand.data \
dd if=/tmp/testfile of=/dev/null bs=4k skip=0 seek=0 \
count=1000 2>/dev/null & \
dd if=/tmp/testfile of=/dev/null bs=4k skip=10000 seek=0 \
count=1000 2>/dev/null & \
wait
# 分析结果
perf report -i perf-seq.data | head -20
# 预期: 顺序读缺页次数少 (预读生效)
perf report -i perf-rand.data | head -20
# 预期: 随机读缺页次数多 (预读失效)
预期输出对比:
text
# 顺序读 (perf-seq.data)
# Samples: 50 of event 'page-faults'
# Overhead Command Shared Object Symbol
# ........ ....... ................ .......................
# 20.00% cat [kernel.kallsyms] [k] do_read_fault
# 15.00% cat [kernel.kallsyms] [k] filemap_fault
# ...
# 随机读 (perf-rand.data)
# Samples: 500 of event 'page-faults' ← 缺页次数多 10×
# Overhead Command Shared Object Symbol
# ........ ....... ................ .......................
# 25.00% dd [kernel.kallsyms] [k] do_read_fault
# 18.00% dd [kernel.kallsyms] [k] filemap_fault
# ...
📊 预读 (Readahead) 参数调优
bash
# 查看当前预读大小 (单位: 512B 扇区)
blockdev --getra /dev/sda
# 输出: 256 ← 表示 128KB 预读 (256 * 512 = 131072)
# 调整预读大小 (推荐 4096 = 2MB)
blockdev --setra 4096 /dev/sda
# 针对特定文件调整 (应用程序内)
posix_fadvise(fd, 0, file_size, POSIX_FADV_SEQUENTIAL);
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 顺序读性能差 | 预读大小不够 | 调大 blockdev --setra (如 4096 = 2MB) |
| 2 | 🟡 随机读性能差 | 预读反而浪费内存 | 用 posix_fadvise(POSIX_FADV_RANDOM) 禁用预读 |
| 3 | 🟡 脏页回写阻塞应用 | 大量脏页积累,突然回写 | 调小 vm.dirty_ratio (默认 20 → 10) |
| 4 | 🟢 不理解 active/inactive LRU | 误以为所有 Page Cache 都能立即回收 | inactive 链表中的页才能被回收 |
🔭 扩展思考
- 为什么 Page Cache 不回收? → 可能是
mlock()锁住了页,或者页正在回写 (Writeback) - 如何查看某个文件是否被缓存? →
vmtouch -v /path/to/file或pcstat /path/to/file - Direct I/O (O_DIRECT) 会绕过 Page Cache 吗? → 是,但要求对齐 (扇区大小),否则性能更差
第4讲:如何处理Page Cache难以回收产生的load飙高问题?
🎯 核心问题
服务器 load 飙到 100+,但 CPU 使用率很低? ------ 可能是 kswapd 疯狂回收 Page Cache,或者 Direct Reclaim 阻塞了应用线程。
📐 根因分析
场景 1:kswapd 高负载
text
┌─────────────────────────────────────────────────────────────┐
│ kswapd 高负载导致的 load 飙高 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 现象: │
│ ├── load average > CPU 核心数 × 2 │
│ ├── CPU 使用率不高 (user + system < 30%) │
│ ├── `top` 看到 kswapd0 占满一个 CPU 核心 │
│ └── `vmstat` 看到 si/so (swap in/out) 飙升 │
│ │
│ 根因: │
│ ├── Page Cache 占用过高,超过 `vm.vfs_cache_pressure` 阈值 │
│ ├── 可用内存不足,触发 kswapd 后台回收 │
│ └── kswapd 扫描 LRU 链表速度跟不上分配速度 │
│ │
└─────────────────────────────────────────────────────────────┘
场景 2:Direct Reclaim 阻塞应用
text
┌─────────────────────────────────────────────────────────────┐
│ Direct Reclaim 阻塞应用线程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 现象: │
│ ├── 应用线程状态为 D (Uninterruptible Sleep) │
│ ├── `cat /proc/PID/wchan` 显示 `shrink_inactive_list` │
│ ├── 应用延迟飙高 (如 MySQL 查询从 10ms → 5s) │
│ └── `sar -B 1` 看到 `pgscank`/`pgscand` 很高 │
│ │
│ 根因: │
│ ├── 内存分配时,可用内存低于 `vm.min_free_kbytes` │
│ ├── 分配器同步回收页 (Direct Reclaim) │
│ └── 回收过程中阻塞了当前线程 │
│ │
└─────────────────────────────────────────────────────────────┘
🔬 诊断三板斧
第 1 斧:vmstat 观察 si/so
bash
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 1 0 0 123456 78901 8192000 0 0 12 34 123 456 1 1 98 0 0
# 2 5 0 123456 78901 8192000 0 0 1234 5678 123 456 1 1 98 0 0
# ↑ ↑ ↑↑ ↑↑
# r b si so wa
# (运行)(阻塞) (swap in)(swap out) (iowait)
# 解读:
# - si/so > 0 → 发生了 swap,说明内存真的不足
# - b (blocked) > 0 → 有进程在等待 I/O (可能是 Direct Reclaim)
# - wa > 10% → I/O 等待高,可能是脏页回写
第 2 斧:/proc/vmstat 查看扫描/偷取统计
bash
cat /proc/vmstat | grep -E "pgscan|pgsteal|pgshared"
# 输出示例:
# pgscan_kswapd_high 0 ← kswapd 扫描 high 区页数
# pgscan_kswapd_normal 123456 ← kswapd 扫描 normal 区页数 (高说明 kswapd 忙)
# pgscan_direct_normal 7890 ← Direct Reclaim 扫描页数 (高说明应用被阻塞)
# pgsteal_kswapd_normal 100000 ← kswapd 回收页数
# pgsteal_direct_normal 5000 ← Direct Reclaim 回收页数
# pgshared 8192000 ← Page Cache 页数
# 判断标准:
# - pgscan_direct_normal > 0 → 有 Direct Reclaim,应用被阻塞
# - pgscan_kswapd_normal 持续增长 → kswapd 在拼命回收
第 3 斧:perf top 定位内存分配热点
bash
perf top -e 'kmem:*'
# 输出示例:
# Overhead Command Shared Object Symbol
# ........ ....... ................ .......................
# 30.00% kswapd0 [kernel.kallsyms] [k] shrink_inactive_list
# 20.00% nginx [kernel.kallsyms] [k] __alloc_pages_direct_reclaim
# 15.00% mysql [kernel.kallsyms] [k] shrink_page_list
# ...
# 解读:
# - shrink_inactive_list 占比高 → kswapd 在回收页
# - __alloc_pages_direct_reclaim 占比高 → 应用在被 Direct Reclaim 阻塞
🛠️ 解决方案
方案 1:调整 vm.vfs_cache_pressure
bash
# 查看当前值
sysctl vm.vfs_cache_pressure
# 输出: vm.vfs_cache_pressure = 100 ← 默认值,倾向回收 Page Cache
# 调低该值 (减少 dentry/inode 回收压力)
sysctl -w vm.vfs_cache_pressure=50
echo "vm.vfs_cache_pressure=50" >> /etc/sysctl.conf
# 原理:
# - 值越低 → 越倾向回收 slab 缓存 (dentry/inode)
# - 值越高 → 越倾向回收 Page Cache
# - 推荐值: 数据库服务器用 50,文件服务器用 100-200
方案 2:谨慎使用 drop_caches
bash
# ⚠️ 仅测试环境使用!生产环境会导致大量 Cache Miss!
# 释放 Page Cache (不影响脏页)
echo 1 > /proc/sys/vm/drop_caches
# 释放 slab 缓存 (dentry/inode)
echo 2 > /proc/sys/vm/drop_caches
# 释放所有可回收缓存
echo 3 > /proc/sys/vm/drop_caches
方案 3:应用层优化 ------ 使用 posix_fadvise()
c
/* 告诉内核:我后续会顺序读这个文件,请预读 */
posix_fadvise(fd, 0, file_size, POSIX_FADV_WILLNEED);
/* 告诉内核:这个文件我短期内不会再访问,可以优先回收 */
posix_fadvise(fd, 0, file_size, POSIX_FADV_DONTNEED);
/* 告诉内核:我会随机读这个文件,不要预读 */
posix_fadvise(fd, 0, file_size, POSIX_FADV_RANDOM);
python
#!/usr/bin/env python3
import os
import ctypes
import ctypes.util
# 加载 libc
libc = ctypes.CDLL(ctypes.util.find_library('c'))
# 定义 POSIX_FADV_DONTNEED
POSIX_FADV_DONTNEED = 4
def fadvise_dontneed(fd, offset=0, len=0):
"""告诉内核:这个文件可以优先回收"""
libc.posix_fadvise(fd, offset, len, POSIX_FADV_DONTNEED)
# 使用示例
with open('/var/log/bigfile.log', 'r') as f:
data = f.read(1024) # 只读前 1KB
fadvise_dontneed(f.fileno(), 1024) # 告诉内核:后续内容可以回收
📊 调优参数速查表
| 参数 | 默认值 | 建议值 | 作用 |
|---|---|---|---|
vm.vfs_cache_pressure |
100 | 50 (DB) / 200 (文件服务器) | 控制 slab 缓存回收倾向 |
vm.min_free_kbytes |
动态调整 | 1-2GB (大内存服务器) | 保留最小空闲内存,减少 Direct Reclaim |
vm.dirty_ratio |
20 (%) | 10 | 脏页占总内存百分比,超过后阻塞写入 |
vm.dirty_background_ratio |
10 (%) | 5 | 后台回写脏页的阈值 |
vm.swappiness |
60 | 1-10 (数据库) | 倾向回收匿名页还是文件页 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 盲目设置 vm.swappiness=0 | 可能导致 OOM Killer 杀进程 | 设置 vm.swappiness=1 (而不是 0) |
| 2 | 🔴 在生产环境用 drop_caches | 导致大量 Cache Miss,延迟飙升 | 仅在测试环境验证问题 |
| 3 | 🟡 忽略 NUMA 影响 | 跨 NUMA 节点访问内存更慢 | 用 numactl --membind=0 绑定内存节点 |
| 4 | 🟡 不理解 min_free_kbytes 的作用 | 设置过小导致频繁 Direct Reclaim | 设置 vm.min_free_kbytes=2097152 (2GB) |
🔭 扩展思考
- 为什么 kswapd 不能彻底解决内存压力? → kswapd 是后台线程,回收速度跟不上分配速度时,会触发 Direct Reclaim
- 如何监控 Page Cache 回收频率? →
sar -B 1查看pgscank/pgscand - 容器环境下如何限制 Page Cache 使用? → 用
memory.high(cgroup v2) 限制内存使用上限
第5讲:如何处理Page Cache容易回收引起的业务性能问题?
🎯 核心问题
Page Cache 命中率突然下降,业务 QPS 骤降? ------ 可能是 Page Cache 被过早回收了。
📐 典型场景:数据库缓存被挤出
text
┌─────────────────────────────────────────────────────────────┐
│ 数据库缓存被 Page Cache 回收的问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 现象: │
│ ├── MySQL QPS 从 10000 骤降到 1000 │
│ ├── 慢查询日志中大量全表扫描 │
│ ├── `free -h` 看到 buff/cache 很高,但 available 很低 │
│ └── `sar -B 1` 看到 `pgsteal` 很高 │
│ │
│ 根因: │
│ ├── Page Cache 优先级低于匿名页 (anon > file) │
│ ├── 应用申请大量匿名内存 (如 Java 堆扩容) │
│ ├── 内核回收 Page Cache 以满足匿名内存需求 │
│ └── 数据库缓存 (在 Page Cache 中) 被回收 → 大量磁盘 I/O │
│ │
└─────────────────────────────────────────────────────────────┘
📊 内核 Page 回收优先级
text
┌─────────────────────────────────────────────────────────────┐
│ Linux 页回收优先级 (简版) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 最容易回收 ←──────────────────────────────────→ 最难回收 │
│ │
│ Page Cache (clean) │
│ ↓ │
│ Slab Cache (dentry/inode) │
│ ↓ │
│ Page Cache (dirty) → 先回写再回收 │
│ ↓ │
│ 匿名页 (anon) → 需要 swap │
│ ↓ │
│ mlock() 锁定的页 ← 不回收 │
│ │
│ 内核倾向: │
│ - 默认: 优先回收 Page Cache (file-backed 页) │
│ - 可调: 通过调整 vm.vfs_cache_pressure 改变倾向 │
│ │
└─────────────────────────────────────────────────────────────┘
🛠️ 实战对策
对策 1:锁定关键页 (谨慎使用!)
c
/* 锁定关键内存页,防止被回收或 swap */
#include <sys/mman.h>
int main() {
void *ptr = malloc(1024 * 1024 * 100); // 分配 100MB
/* 锁定所有内存 (包括未来分配的) */
mlockall(MCL_CURRENT | MCL_FUTURE);
/* 或者只锁定当前分配的内存 */
mlock(ptr, 1024 * 1024 * 100);
/* ... 业务逻辑 ... */
munlockall(); // 解锁
return 0;
}
⚠️ 慎用! mlock() 会导致:
- 内存无法被 swap,可能触发 OOM
- 内存无法被回收,可能挤压其他进程的可用内存
对策 2:调整回收策略
bash
# 方案 A: 降低 file cache 回收倾向 (推荐)
sysctl -w vm.vfs_cache_pressure=10
echo "vm.vfs_cache_pressure=10" >> /etc/sysctl.conf
# 方案 B: 增大 dirty_ratio,延迟回写 (适合大量顺序写场景)
sysctl -w vm.dirty_ratio=20
sysctl -w vm.dirty_background_ratio=10
# 方案 C: 极端情况 ------ 禁用 swap (仅适合内存充足且可控的场景)
sysctl -w vm.swappiness=1
echo "vm.swappiness=1" >> /etc/sysctl.conf
对策 3:应用层优化 ------ 预热关键数据
bash
#!/bin/bash
# 预热 MySQL InnoDB Buffer Pool
# 方法 1: 重启后手动加载热数据
mysql -u root -p -e "
SELECT /*+ NO_CACHE */ COUNT(*) FROM important_table WHERE ...;
SELECT /*+ NO_CACHE */ * FROM hot_table LIMIT 1000;
"
# 方法 2: 使用 mysqldump 导出热数据再导入 (强制加载到 Page Cache)
mysqldump -u root -p important_db important_table | \
mysql -u root -p important_db
# 方法 3: 用 posix_fadvise(POSIX_FADV_WILLNEED) 预热
# (需要修改应用程序代码)
python
#!/usr/bin/env python3
import os
import mmap
def preload_file_to_page_cache(filepath):
"""预热文件到 Page Cache"""
with open(filepath, 'rb') as f:
# mmap 会将文件映射到 Page Cache
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
# 顺序访问触发预读
for i in range(0, len(mm), 4096):
_ = mm[i] # 触发缺页异常,加载到 Page Cache
print(f"[Preload] {filepath} loaded to Page Cache")
# 使用示例
preload_file_to_page_cache('/var/lib/mysql/important_db/hot_table.ibd')
📊 不同场景的调优策略
| 场景 | 问题 | 推荐策略 |
|---|---|---|
| 数据库服务器 | Page Cache 被回收,查询变慢 | vm.vfs_cache_pressure=10 + 增大 InnoDB Buffer Pool |
| 文件服务器 | Page Cache 命中率低 | vm.vfs_cache_pressure=200 + 增大 vm.dirty_ratio |
| 混合部署 | 应用和数据库争用 Page Cache | 用 cgroup 限制各自的内存使用上限 |
| 容器环境 | 容器间争用 Page Cache | 用 memory.high (cgroup v2) 隔离 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 滥用 mlock() | 锁定过多内存导致 OOM | 仅锁定关键元数据,不超过物理内存的 10% |
| 2 | 🟡 忽略透明大页 (THP) 的影响 | THP 导致内存碎片,触发回收 | 数据库场景用 echo never > /sys/kernel/mm/transparent_hugepage/enabled |
| 3 | 🟡 不理解 vm.swappiness 的作用 | 设置 vm.swappiness=0 反而导致 Page Cache 被过早回收 |
设置 vm.swappiness=1-10 |
| 4 | 🟢 容器环境下误调宿主机参数 | 容器内的 sysctl 不影响其他容器 |
在宿主机或用 cgroup 限制 |
🔭 扩展思考
- 为什么数据库都自己管理缓存 (如 InnoDB Buffer Pool)? → 避免 Page Cache 的二次缓存,减少内存浪费
- 如何查看某个进程占用的 Page Cache 大小? →
cat /proc/PID/smaps | grep -i "^Shared_Clean\|Shared_Dirty" - io_uring 对 Page Cache 有什么影响? → io_uring 支持异步 I/O,减少缺页异常对应用线程的阻塞
第6讲:如何判断问题是否由Page Cache产生的?
🎯 核心问题
服务器性能下降,如何快速判断是不是 Page Cache 的问题?
📐 决策树诊断法
text
┌─────────────────────────────────────────────────────────────┐
│ Page Cache 问题诊断决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 开始 → 高 Load? │
│ │ │
│ ├── 否 → 查 CPU/网络/应用逻辑 │
│ │ │
│ └── 是 → 查 vmstat si/so │
│ │ │
│ ├── si/so > 0 → 内存不足,发生 swap │
│ │ 解决: 加内存/优化内存使用 │
│ │ │
│ └── si/so = 0 → 查 iostat %util │
│ │ │
│ ├── %util > 80% → 磁盘 I/O 瓶颈 │
│ │ 解决: 优化 I/O 调度/升级 SSD │
│ │ │
│ └── %util < 80% → 查 Page Cache │
│ │ │
│ └── /proc/meminfo Cached │
│ │ │
│ ├── Cached 高 → │
│ │ 可能是 Page │
│ │ Cache 回收问题 │
│ │ │
│ └── Cached 低 → │
│ 不是 Page │
│ Cache 问题 │
│ │
└─────────────────────────────────────────────────────────────┘
🔬 eBPF 快速检测脚本
bash
#!/usr/bin/env bpftrace
# 监控 page cache 回收事件
BEGIN {
printf("Monitoring page cache reclaim events... (Ctrl+C to stop)\n");
}
/* 追踪 kswapd 回收事件 */
kprobe:shrink_inactive_list {
@kswapd_reclaim[pid, comm] = count();
}
/* 追踪 direct reclaim 事件 */
kprobe:__alloc_pages_direct_reclaim {
@direct_reclaim[pid, comm] = count();
}
/* 每秒汇总 */
interval:1s {
printf("\n=== Page Cache Reclaim Events (last 1s) ===\n");
if (count(@kswapd_reclaim) > 0) {
printf("\n[kswapd] Top 5 reclaim pids:\n");
print(@kswapd_reclaim, 5);
clear(@kswapd_reclaim);
}
if (count(@direct_reclaim) > 0) {
printf("\n[Direct Reclaim] Top 5 reclaim pids (BLOCKING!):\n");
print(@direct_reclaim, 5);
clear(@direct_reclaim);
}
if (count(@kswapd_reclaim) == 0 && count(@direct_reclaim) == 0) {
printf("(no reclaim events)\n");
}
}
END {
clear(@kswapd_reclaim);
clear(@direct_reclaim);
}
使用方法:
bash
chmod +x pagecache_reclaim.bt
sudo bpftrace pagecache_reclaim.bt
# 输出示例:
# === Page Cache Reclaim Events (last 1s) ===
#
# [kswapd] Top 5 reclaim pids:
# @kswapd_reclaim[1234, kswapd0]: 1000
#
# [Direct Reclaim] Top 5 reclaim pids (BLOCKING!):
# @direct_reclaim[5678, mysql]: 50 ← mysql 被阻塞了!
📊 完整诊断流程图
text
┌─────────────────────────────────────────────────────────────┐
│ Page Cache 问题诊断流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 确认是否是内存问题 │
│ ├── free -h → 看 available │
│ ├── vmstat 1 → 看 si/so │
│ └── 如果 si/so > 0 → 是内存问题 → 跳到 Step 2 │
│ │
│ Step 2: 确认是否是 Page Cache 问题 │
│ ├── cat /proc/meminfo | grep -E "Cached|SReclaimable" │
│ ├── 如果 Cached 很高 → 可能是 Page Cache 回收问题 │
│ └── 用 bpftrace 脚本确认是否有大量回收事件 │
│ │
│ Step 3: 定位问题进程 │
│ ├── perf top -e 'kmem:*' │
│ ├── 看哪个进程在分配大量内存 │
│ └── 用 slabtop 看是哪个 slab 占用最多 │
│ │
│ Step 4: 确定解决方案 │
│ ├── 如果是 kswapd 忙 → 调大内存或调小 vm.vfs_cache_pressure │
│ ├── 如果是 direct reclaim → 调大 vm.min_free_kbytes │
│ └── 如果是特定进程 → 优化该进程的内存使用 │
│ │
└─────────────────────────────────────────────────────────────┘
📊 快速诊断命令速查表
| 命令 | 作用 | 关键输出 |
|---|---|---|
free -h |
查看内存使用 | available 列 |
vmstat 1 |
查看内存/交换/IO | si/so (swap in/out) |
iostat -x 1 |
查看磁盘 IO | %util, await |
sar -B 1 |
查看页回收统计 | pgscank/pgscand |
slabtop -o |
查看 slab 缓存 | OBJS, USE |
cat /proc/meminfo |
详细内存信息 | Cached, SReclaimable |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 仅凭 free -h 的 "available" 判断内存充足 | available 包含可回收的 Page Cache,不代表真的可用 |
结合 vmstat si/so 判断 |
| 2 | 🟡 看到 high load 就认为是 CPU 问题 | high load 可能是 I/O 等待 (包括 Page Cache 回收) | 用 top 看 wa 列 |
| 3 | 🟡 忽略容器环境的内存隔离 | 容器内的 free -h 显示宿主机内存 |
读 cgroup 内存统计 |
| 4 | 🟢 过度依赖自动化监控 | 监控面板可能漏掉瞬时的 Page Cache 回收风暴 | 用 eBPF 做深度追踪 |
🔭 扩展思考
- 如何区分 Page Cache 问题和 Swap 问题? → Swap 问题看
vmstat si/so,Page Cache 问题看sar -B的pgsteal - 如何在生产环境长期监控 Page Cache 命中率? → 用
bcc-tools/cachestat或pcp(Performance Co-Pilot) - io_uring 是否会影响 Page Cache 回收? → io_uring 的异步 I/O 可能触发大量缺页异常,间接影响回收
🔹 内存泄漏问题(5讲|从Shmem到分析方法论)
第7讲:进程的哪些内存类型容易引起内存泄漏?
🎯 核心问题
top 看到进程 RES 不高,但系统内存却快被耗尽了? ------ 可能是 Shmem、slab 缓存或 Page Cache 在泄漏。
📐 四类高危内存类型
类型 1:Heap(堆内存)
c
/* 典型泄漏代码 */
#include <stdlib.h>
void leaky_function() {
char *ptr = malloc(1024 * 1024); // 分配 1MB
// 忘记 free(ptr)!
}
int main() {
while (1) {
leaky_function();
sleep(1);
}
}
检测命令:
bash
# 查看进程堆内存使用
pmap -x 12345 | grep -E "total|heap"
# 输出示例:
# Address Kbytes RSS Dirty Mode Mapping
# 00007f... 1048576 1048576 1048576 rw-s [heap]
# total 2097152 1048576 1048576
# 用 massif (Valgrind 工具) 详细分析
valgrind --tool=massif ./leaky_program
ms_print massif.out.12345
类型 2:Shmem(共享内存)
bash
# 查看系统全局 Shmem 使用
ipcs -m
# 输出示例:
# ------ Shared Memory Segments --------
# key shmid owner perms bytes
# 0x12345678 12345 root 666 1073741824 ← 1GB 共享内存
# 查看 /dev/shm 使用
df -h /dev/shm
# 输出示例:
# Filesystem Size Used Avail Use%
# tmpfs 7.8G 5.0G 2.8G 65% ← /dev/shm 被占用了 5GB
# 查看进程的 Shmem 使用
cat /proc/12345/smaps | grep -E "^Size|^Shared|shm"
典型案例:Redis AOF rewrite 导致 /dev/shm 占满
text
现象:
- Redis 执行 BGREWRITEAOF
- 创建子进程进行 AOF 重写
- 子进程使用 fork(),父子进程共享内存页 (Copy-On-Write)
- 如果有大量写操作,COW 导致私有页变成共享页
- /dev/shm 被占满 → 系统卡死
解决方案:
1. 限制 AOF rewrite 频率:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
2. 使用 bigger AOF rewrite buffer:
config set aof-rewrite-incremental-fsync yes
类型 3:Kernel Objects(内核对象)
bash
# 查看 slab 缓存使用 (内核对象缓存)
slabtop -o
# 输出示例:
# OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
# 123456 123400 99% 0.25K 12345 10 3072K dentry ← 目录项缓存
# 100000 95000 95% 0.50K 12500 8 4096K inode_cache ← inode 缓存
# 50000 45000 90% 1.00K 12500 4 8192K tcp_sock ← TCP 套接字
# 解读:
# - dentry/inode_cache 高 → 可能是应用频繁创建/删除文件
# - tcp_sock 高 → 可能是 TCP 连接未正确关闭 (CLOSE_WAIT)
检测命令:
bash
# 查看详细 slab 信息
cat /proc/slabinfo | awk '{if ($3 > 10000) print $0}' | sort -k3 -n -r
# 查看内核模块占用的内存
lsmod | sort -k2 -n -r | head -10
# 输出示例:
# Module Size Used by
# ext4 1234567 1 ← ext4 模块占用 1.2MB
# btrfs 2345678 0
类型 4:Page Cache(页缓存)
bash
# 查看进程的 Page Cache 使用
cat /proc/12345/smaps | grep -E "^Size|FilePmdMapped"
# 查看系统级 Page Cache 使用
cat /proc/meminfo | grep -E "Cached|SReclaimable"
# 定位哪个文件占用了大量 Page Cache
# (需要安装 pcstat 工具)
pcstat /var/log/*.log
📊 内存类型对比表
| 类型 | 特征 | 检测命令 | 常见泄漏场景 |
|---|---|---|---|
| Heap | malloc()/new 未释放 |
pmap -x, massif |
忘记 free()/delete |
| Shmem | 计入 Cached,不计入 RSS |
ipcs -m, df -h /dev/shm |
共享内存未删除 |
| Kernel Objects | slabtop 看到异常高的使用 |
slabtop -o, cat /proc/slabinfo |
文件描述符泄漏 |
| Page Cache | 文件缓存异常增长 | /proc/meminfo, vmtouch |
大量小文件读取 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 只看 RSS 判断进程内存使用 | Shmem 不计入 RSS,但计入系统内存 | 看 top 的 RSsh 列或 `cat /proc/PID/status |
| 2 | 🟡 忽略 slab 缓存增长 | slab 缓存泄漏不会体现在进程内存中 | 用 slabtop 定期监控 |
| 3 | 🟡 容器环境下误判内存使用 | 容器内的 slabtop 显示宿主机数据 |
在宿主机查看 |
| 4 | 🟢 过早优化 | 未发现真实泄漏就盲目调优 | 先用 valgrind 确认泄漏点 |
🔭 扩展思考
- 如何区分正常的内存增长和内存泄漏? → 正常增长会稳定在某个值,泄漏会持续增长直到 OOM
- 为什么 Java 应用的 RSS 比 Xmx 大很多? → 除了堆内存,还有 metaspace、CodeCache、DirectByteBuffer 等
- 如何监控容器内的内存泄漏? → 用
docker stats或 cgroup 内存统计
第8讲:如何预防内存泄漏导致的系统假死?
🎯 核心问题
如何构建多层防御体系,在内存泄漏导致系统假死前就发现问题?
📐 系统级防护
防护 1:启用 cgroup v2 + memory.limit
bash
# 创建 cgroup v2 限制
mkdir /sys/fs/cgroup/limited_group
echo "1073741824" > /sys/fs/cgroup/limited_group/memory.max # 限制 1GB
echo "0-3" > /sys/fs/cgroup/limited_group/cpuset.cpus # 限制 CPU 核心
# 将进程加入 cgroup
echo $$ > /sys/fs/cgroup/limited_group/cgroup.procs
# 监控 cgroup 内存使用
cat /sys/fs/cgroup/limited_group/memory.current
cat /sys/fs/cgroup/limited_group/memory.events
# 输出示例:
# low 0
# high 0
# max 0 ← 如果非 0,说明发生过 OOM
# oom 0
# hugetlb 0
防护 2:配置 OOM Killer 策略
bash
# 查看进程 OOM 优先级 (值越高越容易被杀)
cat /proc/12345/oom_score
# 输出: 600 ← 0-1000,越高越容易被 OOM Killer 杀掉
# 调整进程 OOM 优先级
echo -500 > /proc/12345/oom_score_adj
# 范围: -1000 (永远不被杀) 到 1000 (最先被杀)
# 系统级配置: OOM 时直接 panic (而不是杀进程)
sysctl -w vm.panic_on_oom=1
echo "vm.panic_on_oom=1" >> /etc/sysctl.conf
# 或者: OOM 时杀进程但记录日志
sysctl -w vm.panic_on_oom=0 # 默认值
echo "vm.oom_kill_allocating_task=1" >> /etc/sysctl.conf
防护 3:设置 vm.panic_on_oom (关键系统)
bash
# 对于关键系统 (如数据库),OOM 后直接 panic 比杀进程更安全
# 因为:
# 1. 杀进程可能导致数据不一致
# 2. Panic 后可以由集群管理器自动重启
# 3. 配合 kdump 可以捕获崩溃现场
# 启用 panic_on_oom
sysctl -w vm.panic_on_oom=1
# 设置 panic 后自动重启
sysctl -w kernel.panic=10 # 10 秒后重启
echo "kernel.panic=10" >> /etc/sysctl.conf
📐 应用级防御
防御 1:使用 jemalloc 替代 glibc malloc
bash
# 安装 jemalloc
apt install -y libjemalloc-dev
# 预加载 jemalloc
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
# 运行程序
./your_program
# jemalloc 自带泄漏检测
MALLOC_CONF=prof:true,prof_prefix:/tmp/jeprof ./your_program
# 然后用 jeprof 分析
jeprof --show_bytes --pdf ./your_program /tmp/jeprof.* > leak.pdf
防御 2:定期 malloc_stats() 输出 (C/C++)
c
#include <malloc.h>
#include <stdio.h>
void print_memory_stats() {
malloc_stats(); // 输出到 stderr
// 或者输出到文件
FILE *fp = fopen("/tmp/malloc_stats.txt", "w");
malloc_stats_fp(fp);
fclose(fp);
}
int main() {
// 定期调用 print_memory_stats()
// 对比不同时刻的输出,看是否有泄漏
}
防御 3:Python 使用 tracemalloc
python
#!/usr/bin/env python3
import tracemalloc
import time
tracemalloc.start()
# ... 业务代码 ...
# 定期快照对比
snapshot1 = tracemalloc.take_snapshot()
time.sleep(60) # 等待 60 秒
snapshot2 = tracemalloc.take_snapshot()
# 对比快照
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[Top 10 memory growth]")
for stat in top_stats[:10]:
print(stat)
🔬 实战脚本:自动检测内存持续增长进程
bash
#!/bin/bash
# auto_detect_memory_leak.sh
# 自动检测内存持续增长的进程
LOG_FILE="/var/log/memory_leak_detector.log"
INTERVAL=60 # 60 秒检查一次
THRESHOLD=10 # 连续 10 次增长,则认为是泄漏
declare -A PROCESS_MEMORY
declare -A PROCESS_GROWTH_COUNT
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
}
while true; do
# 获取所有进程的 RSS (单位: KB)
while read -r line; do
pid=$(echo "$line" | awk '{print $1}')
rss=$(echo "$line" | awk '{print $2}')
comm=$(echo "$line" | awk '{print $3}')
if [ -z "${PROCESS_MEMORY[$pid]}" ]; then
# 第一次记录
PROCESS_MEMORY[$pid]="$rss"
else
# 对比上次记录
old_rss=${PROCESS_MEMORY[$pid]}
if [ "$rss" -gt "$old_rss" ]; then
# 内存增长了
PROCESS_GROWTH_COUNT[$pid]=$(( ${PROCESS_GROWTH_COUNT[$pid]} + 1 ))
if [ "${PROCESS_GROWTH_COUNT[$pid]}" -ge "$THRESHOLD" ]; then
log "WARNING: Possible memory leak detected! PID=$pid, Comm=$comm, RSS grew from ${old_rss}KB to ${rss}KB"
# 可选: 发送告警
# send_alert "$pid" "$comm" "$old_rss" "$rss"
fi
else
# 内存未增长,重置计数
PROCESS_GROWTH_COUNT[$pid]=0
fi
# 更新记录
PROCESS_MEMORY[$pid]="$rss"
fi
done < <(ps aux --sort=-rss | awk 'NR>1 {print $2, $6, $11}' | head -20)
sleep "$INTERVAL"
done
📊 内存泄漏防御 checklist
| 层次 | 防御措施 | 实施难度 | 效果 |
|---|---|---|---|
| 系统级 | cgroup 内存限制 | 低 | ⭐⭐⭐ |
| 系统级 | OOM Killer 策略调整 | 中 | ⭐⭐ |
| 系统级 | 定期重启 (cron + systemctl) | 低 | ⭐⭐ |
| 应用级 | 使用 jemalloc/tcmalloc | 中 | ⭐⭐⭐⭐ |
| 应用级 | 内存使用监控 + 告警 | 高 | ⭐⭐⭐⭐⭐ |
| 代码级 | 静态分析 (Coverity, clang-tidy) | 高 | ⭐⭐⭐⭐ |
| 代码级 | 单元测试 + Valgrind | 高 | ⭐⭐⭐⭐⭐ |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 盲目设置 vm.panic_on_oom=1 | 可能导致系统频繁重启 | 仅在关键系统且有关键资源保护时使用 |
| 2 | 🟡 过度依赖 cgroup 限制 | cgroup 限制不等于没有泄漏 | cgroup 只是最后的防线,还是要修代码 |
| 3 | 🟡 忽略 tmpfs (/dev/shm) 泄漏 | tmpfs 不计入进程 RSS | 定期监控 df -h /dev/shm |
| 4 | 🟢 生产环境开 core dump | core 文件可能很大,占满磁盘 | 设置 ulimit -c 0 或限制 core 文件大小 |
🔭 扩展思考
- 如何在 Kubernetes 中防御内存泄漏? → 用
resources.limits.memory+livenessProbe+OOMKiller - Go 语言的 GC 能避免内存泄漏吗? → 不能,Go 的 GC 只回收不再引用的对象,如果对象被全局变量引用,就不会被回收
- 如何区分内存泄漏和缓存? → 缓存会主动淘汰旧数据,泄漏只会持续增长
第9讲:Shmem:进程没有消耗内存,内存哪去了?
🎯 核心问题
free -h 看到可用内存很少,但所有进程的 RSS 加起来远小于已用内存? ------ 可能是 Shmem 占用了大量内存。
📐 Shmem 真相:属于 Cache,但不计入 RSS
text
┌─────────────────────────────────────────────────────────────┐
│ Shmem 内存归属讲解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ free -h 输出: │
│ total used free shared │
│ Mem: 7.8Gi 6.0Gi 1.8Gi 5.0Gi │
│ │ │ ↑ │
│ │ │ └── Shmem │
│ │ │ │ │
│ ▼ ▼ │ │
│ cat /proc/meminfo: │ │
│ MemTotal: 8123456 kB │ │
│ MemFree: 1888888 kB │ │
│ Cached: 5120000 kB ← 包含 Shmem │ │
│ Shmem: 5120000 kB ← 这部分被重复计算了! │ │
│ │
│ 真相: │
│ - Shmem 计入 Cached,但不计入进程 RSS │
│ - 所以: 所有进程 RSS 之和 + Shmem ≈ 实际已用内存 │
│ - 公式: MemTotal - MemFree = ∑RSS + Shmem + slab │
│ │
└─────────────────────────────────────────────────────────────┘
🔬 关键验证步骤
验证 1:查看进程 Shmem 使用
bash
# 方法 A: 用 pmap 查看
pmap -x 12345 | grep -i shm
# 输出示例:
# Address Kbytes RSS Dirty Mode Mapping
# 00007f... 1048576 1048576 0 rw-s /dev/zero (deleted)
# ↑ 这部分是 Shmem,不计入 RSS
# 方法 B: 用 /proc/PID/maps
cat /proc/12345/maps | grep -i shm
# 输出示例:
# 7f1234567890-7f1235567890 rw-s 00000000 00:01 12345 /dev/zero (deleted)
# ↑ Shmem 映射
# 方法 C: 用 /proc/PID/status
cat /proc/12345/status | grep -E "Vm|Rss"
# 输出示例:
# VmRSS: 1234567 kB ← 不包含 Shmem
# RssShmem: 512000 kB ← Shmem 单独统计
验证 2:查看全局 Shmem
bash
# 方法 A: 用 ipcs -m
ipcs -m
# 输出示例:
# ------ Shared Memory Segments --------
# key shmid owner perms bytes
# 0x12345678 12345 root 666 1073741824 ← 1GB Shmem
# 方法 B: 查看 /dev/shm
ls -l /dev/shm/
# 输出示例:
# -rw------- 1 root root 1073741824 Jun 6 12:00 MySQL.12345
# ↑ 这个文件占用了 1GB Shmem
df -h /dev/shm
# 输出示例:
# Filesystem Size Used Avail Use%
# tmpfs 7.8G 5.0G 2.8G 65% ← /dev/shm 被占用了 5GB
验证 3:Shmem 和 Page Cache 的关系
bash
# Shmem 既是共享内存,也是 Page Cache 的一部分
# 验证:
cat /proc/meminfo | grep -E "Cached|Shmem|SReclaimable"
# 输出示例:
# Cached: 8192000 kB
# Shmem: 5120000 kB
# SReclaimable: 512000 kB
#
# 计算: Cached - Shmem = 3072000 kB ← 这是真正的文件页缓存
📐 典型案例:Redis AOF rewrite 导致 /dev/shm 占满
text
┌─────────────────────────────────────────────────────────────┐
│ Redis AOF rewrite 导致 /dev/shm 占满 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 背景: │
│ - Redis 使用 AOF 持久化 │
│ - AOF rewrite 会创建子进程进行重写 │
│ - 子进程使用 fork(),父子进程共享内存页 (COW) │
│ │
│ 问题: │
│ ├── 如果有大量写操作,COW 导致私有页变成共享页 │
│ ├── 共享页计入 Shmem (/dev/shm) │
│ ├── /dev/shm 被占满 │
│ └── 系统卡死 (因为 Shmem 无法被回收) │
│ │
│ 解决方案: │
│ 1. 限制 AOF rewrite 频率: │
│ auto-aof-rewrite-percentage 100 │
│ auto-aof-rewrite-min-size 64mb │
│ │
│ 2. 使用 bigger AOF rewrite buffer: │
│ config set aof-rewrite-incremental-fsync yes │
│ │
│ 3. 监控 /dev/shm 使用率: │
│ df -h /dev/shm │
│ │
└─────────────────────────────────────────────────────────────┘
🛠️ 解决方案
方案 1:限制 shmmax
bash
# 查看当前 shmmax (最大共享内存段大小)
sysctl kernel.shmmax
# 输出: kernel.shmmax = 18446744073692774399 ← 几乎无限制
# 限制 shmmax (如 1GB)
sysctl -w kernel.shmmax=1073741824
echo "kernel.shmmax=1073741824" >> /etc/sysctl.conf
# 同时限制 shmall (最大共享内存页数)
sysctl -w kernel.shmall=268435456 # 268435456 * 4096 = 1GB
echo "kernel.shmall=268435456" >> /etc/sysctl.conf
方案 2:Redis 配置优化
bash
# 限制 AOF rewrite 频率
redis-cli config set auto-aof-rewrite-percentage 100
redis-cli config set auto-aof-rewrite-min-size 64mb
# 启用增量 fsync (减少 COW)
redis-cli config set aof-rewrite-incremental-fsync yes
# 监控 AOF 状态
redis-cli info memory | grep -E "aof|mem"
方案 3:应用层 Shmem 使用规范
c
/* 正确使用共享内存的示例 */
#include <sys/shm.h>
#include <sys/ipc.h>
int main() {
key_t key = ftok("/tmp", 'R');
int shmid = shmget(key, 1024 * 1024, 0666 | IPC_CREAT);
if (shmid < 0) {
perror("shmget");
return 1;
}
/* 使用共享内存 */
char *shmaddr = shmat(shmid, NULL, 0);
/* ... 业务逻辑 ... */
/* detach 共享内存 */
shmdt(shmaddr);
/* 删除共享内存段 (重要!否则会泄漏) */
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
📊 Shmem vs 其他内存类型对比
| 类型 | 是否计入 RSS | 是否计入 Cached | 是否可回收 | 典型用途 |
|---|---|---|---|---|
| Heap | ✅ | ❌ | ✅ (malloc/free) | 进程私有数据 |
| Stack | ✅ | ❌ | ✅ (函数返回) | 函数局部变量 |
| Shmem | ❌ | ✅ | ❌ (除非显式删除) | 进程间通信 |
| Page Cache | ❌ | ✅ | ✅ (内核自动回收) | 文件缓存 |
| slab | ❌ | ❌ (计入 SReclaimable) | ✅ (内核自动回收) | 内核对象缓存 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 忽略 /dev/shm 使用率 | /dev/shm 满导致系统卡死 | 定期 df -h /dev/shm 监控 |
| 2 | 🟡 共享内存使用后不删除 | shmctl(shmid, IPC_RMID) 忘记调用 |
用 ipcrm 手动删除孤立的共享内存段 |
| 3 | 🟡 Redis AOF rewrite 导致 COW 风暴 | fork() 后大量写操作导致 Shmem 激增 | 调大 auto-aof-rewrite-min-size |
| 4 | 🟢 容器环境下 /dev/shm 大小不足 | 默认 64MB,不够用 | 启动容器时加 --shm-size=1g |
🔭 扩展思考
- 为什么 Shmem 不计入 RSS? → 因为 Shmem 可以被多个进程共享,如果计入 RSS,会导致所有共享进程的 RSS 之和 > 物理内存
- 如何查看哪个进程占用了 Shmem? →
ls -l /dev/shm/看文件名,或者ipcs -p看关联的 PID - tmpfs 和 Shmem 是什么关系? → /dev/shm 是 tmpfs 的一个实例,tmpfs 的后端存储就是 Shmem
第10讲:如何对内核内存泄漏做些基础的分析?
🎯 核心问题
系统可用内存持续下降,但所有用户态进程 RSS 之和并不高? ------ 可能是内核态内存泄漏(内核模块/驱动/Slab 缓存泄漏)。
📚 三层次分析法
层次 1:用户态检测(排除法)
bash
# 统计所有进程 RSS 之和
ps aux | awk '{sum += $6} END {print "Total RSS (KB):", sum}'
# 对比系统已用内存
free -k | awk '/^Mem:/ {print "Used (KB):", $3}'
# 如果: Used - Total_RSS >> Shmem + Slab
# 则可能是内核内存泄漏
层次 2:Slab 层检测(slabtop + kmemleak)
方法 A:用 slabtop 定位异常 slab 增长
bash
# 按 OBJS 排序,查看占用最多的 slab
slabtop -o -s c | head -30
# 输出示例:
# OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
# 500000 500000 100% 1.00K 12500 40 51200K tcp_sock ← 异常!
# 200000 200000 100% 0.25K 5000 40 20480K dentry
# 150000 149000 99% 0.50K 3000 50 15360K inode_cache
#
# 判断标准:
# - tcp_sock 有 50 万个对象 → 可能有 TCP 连接未正确关闭 (CLOSE_WAIT)
# - dentry/inode_cache 异常高 → 可能有文件未正确关闭
方法 B:用 kmemleak 检测内核内存泄漏
bash
# ===== 启用 kmemleak (需要内核支持 CONFIG_DEBUG_KMEMLEAK) =====
echo 1 > /sys/kernel/debug/kmemleak
# 触发一次扫描
echo scan > /sys/kernel/debug/kmemleak
# 等待一段时间 (让泄漏发生)
sleep 300
# 再次扫描
echo scan > /sys/kernel/debug/kmemleak
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak
# 输出示例:
# unreferenced object 0xffff888012345678 (size 128):
# comm "kworker/0:1", pid 123, jiffies 4294567890 (age 300.12s)
# backtrace:
# [<ffffffff81234567>] kmalloc_trace+0xAB/0xCD
# [<ffffffffa0123456>] faulty_init+0x1A/0x30 [faulty_module]
# [<ffffffff81001234>] do_one_initcall+0x45/0x190
# ↑ 这个内核模块 faulty_module 泄漏了内存!
清除 kmemleak 报告(在修复后)
bash
echo clear > /sys/kernel/debug/kmemleak
层次 3:容器级检测(cgroup 内存统计)
bash
# Docker 容器内存使用
docker stats --no-stream
# 输出示例:
# CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
# abc123 my-app 10.5% 1.5GiB / 2GiB 75.0%
# ↑ 容器内存使用 (含 Page Cache + Slab)
# 查看容器详细的 cgroup 内存统计
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.stat | grep -E "cache|rss|slab"
# 输出示例:
# cache 536870912 ← Page Cache
# rss 536870912 ← 匿名页
# slab_reclaimable 67108864 ← 可回收的 slab
# slab_unreclaimable 134217728 ← 不可回收的 slab (泄漏常在这里!)
🔬 实验:模拟内核内存泄漏
创建一个有内存泄漏的内核模块
c
/* faulty_module.c - 模拟内核内存泄漏 */
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kernel.h>
static int __init faulty_init(void) {
void *leak_ptr;
pr_info("faulty_module: loading...\n");
/* 分配内存但故意不释放 → 泄漏! */
leak_ptr = kmalloc(128, GFP_KERNEL);
if (!leak_ptr) {
pr_err("faulty_module: kmalloc failed\n");
return -ENOMEM;
}
/* 这里忘记 kfree(leak_ptr)! */
pr_info("faulty_module: loaded (with leak!)\n");
return 0;
}
static void __exit faulty_exit(void) {
/* 卸载时没有释放 leak_ptr → 永久泄漏 */
pr_info("faulty_module: unloaded (leak remains!)\n");
}
module_init(faulty_init);
module_exit(faulty_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A faulty module with memory leak");
编译并加载
bash
# 编写 Makefile
cat > Makefile << 'EOF'
obj-m += faulty_module.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
EOF
# 编译
make
# 加载模块 (会产生内存泄漏!)
insmod faulty_module.ko
# 查看内核日志
dmesg | tail -5
# 输出: faulty_module: loaded (with leak!)
# 用 kmemleak 检测
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak
# 应该能看到 faulty_module 的泄漏报告
# 卸载模块 (泄漏不会恢复,因为 kfree 没被调用)
rmmod faulty_module
📊 内核内存泄漏排查工具对比
| 工具 | 适用层次 | 优点 | 缺点 |
|---|---|---|---|
| slabtop | Slab 缓存 | 简单直观,系统自带 | 只能看对象数量,不能定位代码 |
| kmemleak | 内核态 | 能定位泄漏代码位置 | 需要特殊内核配置,有性能开销 |
| kasan | 内核态 | 检测越界访问/使用已释放内存 | 需要特殊内核配置,开销大 |
| perf kmem | 内核态 | 统计内核内存分配/释放事件 | 需要符号表,分析结果较复杂 |
| cgroup stats | 容器级 | 快速判断是否容器导致 | 不能定位具体原因 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 生产环境开启 kmemleak | kmemleak 有 10-20% 性能开销 | 仅在测试环境开启 |
| 2 | 🟡 忽略 Slab 不可回收部分 | slab_unreclaimable 增长是严重信号 |
用 slabtop -o 定期监控 |
| 3 | 🟡 容器内存限制设置过低 | 容器内 slab 占用也计入 limit | cgroup v2 用 memory.high 而非 memory.max |
| 4 | 🟢 盲目 rmmod 内核模块 | 模块卸载时如有泄漏,内存不会恢复 | 需要重启系统才能恢复 |
🔭 扩展思考
- 如何区分正常 Slab 增长和泄漏? → 正常增长会稳定,泄漏会持续增长直到 OOM
- kasan 和 kmemleak 有什么区别? → kasan 检测内存访问越界,kmemleak 检测内存分配后未释放
- 如何在生产环境监控内核内存使用? → 用 Prometheus node_exporter +
node_memory_Slab_bytes指标
第11讲:内存泄漏时,我们该如何一步步找根因?
🎯 核心问题
发现内存泄漏后,如何系统化地定位根因,而不是靠"猜"?
📋 标准化排查流程
#mermaid-svg-m3Bwd0Px8VWsUFIA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m3Bwd0Px8VWsUFIA .error-icon{fill:#552222;}#mermaid-svg-m3Bwd0Px8VWsUFIA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m3Bwd0Px8VWsUFIA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .marker.cross{stroke:#333333;}#mermaid-svg-m3Bwd0Px8VWsUFIA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m3Bwd0Px8VWsUFIA p{margin:0;}#mermaid-svg-m3Bwd0Px8VWsUFIA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .cluster-label text{fill:#333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .cluster-label span{color:#333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .cluster-label span p{background-color:transparent;}#mermaid-svg-m3Bwd0Px8VWsUFIA .label text,#mermaid-svg-m3Bwd0Px8VWsUFIA span{fill:#333;color:#333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .node rect,#mermaid-svg-m3Bwd0Px8VWsUFIA .node circle,#mermaid-svg-m3Bwd0Px8VWsUFIA .node ellipse,#mermaid-svg-m3Bwd0Px8VWsUFIA .node polygon,#mermaid-svg-m3Bwd0Px8VWsUFIA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m3Bwd0Px8VWsUFIA .rough-node .label text,#mermaid-svg-m3Bwd0Px8VWsUFIA .node .label text,#mermaid-svg-m3Bwd0Px8VWsUFIA .image-shape .label,#mermaid-svg-m3Bwd0Px8VWsUFIA .icon-shape .label{text-anchor:middle;}#mermaid-svg-m3Bwd0Px8VWsUFIA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-m3Bwd0Px8VWsUFIA .rough-node .label,#mermaid-svg-m3Bwd0Px8VWsUFIA .node .label,#mermaid-svg-m3Bwd0Px8VWsUFIA .image-shape .label,#mermaid-svg-m3Bwd0Px8VWsUFIA .icon-shape .label{text-align:center;}#mermaid-svg-m3Bwd0Px8VWsUFIA .node.clickable{cursor:pointer;}#mermaid-svg-m3Bwd0Px8VWsUFIA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .arrowheadPath{fill:#333333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-m3Bwd0Px8VWsUFIA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m3Bwd0Px8VWsUFIA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-m3Bwd0Px8VWsUFIA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m3Bwd0Px8VWsUFIA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-m3Bwd0Px8VWsUFIA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m3Bwd0Px8VWsUFIA .cluster text{fill:#333;}#mermaid-svg-m3Bwd0Px8VWsUFIA .cluster span{color:#333;}#mermaid-svg-m3Bwd0Px8VWsUFIA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-m3Bwd0Px8VWsUFIA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m3Bwd0Px8VWsUFIA rect.text{fill:none;stroke-width:0;}#mermaid-svg-m3Bwd0Px8VWsUFIA .icon-shape,#mermaid-svg-m3Bwd0Px8VWsUFIA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m3Bwd0Px8VWsUFIA .icon-shape p,#mermaid-svg-m3Bwd0Px8VWsUFIA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-m3Bwd0Px8VWsUFIA .icon-shape .label rect,#mermaid-svg-m3Bwd0Px8VWsUFIA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m3Bwd0Px8VWsUFIA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m3Bwd0Px8VWsUFIA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m3Bwd0Px8VWsUFIA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
现象:内存持续增长
是用户进程泄漏?
用 pmap/heaptrack 定位
检查 Slab/Page Cache
获取堆栈信息
用 slabtop/kmemleak 定位
复现 + 最小化案例
提交 Bug 或 Patch
步骤 1:确认泄漏类型
bash
#!/bin/bash
# check_memory_leak.sh - 内存泄漏初步诊断脚本
echo "=== Memory Leak Checker ==="
echo "Timestamp: $(date)"
echo ""
# 1. 查看系统内存概览
echo "【1. System Memory Overview】"
free -h
echo ""
# 2. 计算所有进程 RSS 之和
total_rss_kb=$(ps aux --no-headers | awk '{sum += $6} END {print sum}')
total_rss_mb=$((total_rss_kb / 1024))
echo "【2. Total Process RSS】"
echo " ${total_rss_mb} MB"
echo ""
# 3. 查看 Shmem 使用
shmem_kb=$(grep -w Shmem /proc/meminfo | awk '{print $2}')
shmem_mb=$((shmem_kb / 1024))
echo "【3. Shmem Usage】"
echo " ${shmem_mb} MB"
echo ""
# 4. 查看 Slab 使用
slab_reclaimable_kb=$(grep -w SReclaimable /proc/meminfo | awk '{print $2}')
slab_unreclaimable_kb=$(grep -w SUnreclaim /proc/meminfo | awk '{print $2}')
slab_total_mb=$(( (slab_reclaimable_kb + slab_unreclaimable_kb) / 1024 ))
echo "【4. Slab Usage】"
echo " Total: ${slab_total_mb} MB (reclaimable: $((slab_reclaimable_kb/1024)) MB, unreclaimable: $((slab_unreclaimable_kb/1024)) MB)"
echo ""
# 5. 判断可能的泄漏类型
memtotal_kb=$(grep -w MemTotal /proc/meminfo | awk '{print $2}')
memused_kb=$(grep -w MemTotal /proc/meminfo | awk '{print $2}')
memused_kb=$(free -k | awk '/^Mem:/ {print $3}')
# 计算 "未解释的内存使用"
unaccounted_kb=$(( memused_kb - total_rss_kb - shmem_kb - slab_reclaimable_kb - slab_unreclaimable_kb ))
unaccounted_mb=$((unaccounted_kb / 1024))
echo "【5. Leak Type Prediction】"
if [ $unaccounted_mb -gt 100 ]; then
echo " ⚠️ UNACCOUNTED MEMORY: ${unaccounted_mb} MB"
echo " Possible causes:"
echo " - Kernel memory leak (Slab/unreclaimable)"
echo " - Hidden Page Cache (check /proc/meminfo Cached)"
echo " - Memory ballooning (in VM environment)"
else
echo " ✅ Memory accounted for (within 100MB threshold)"
fi
echo ""
# 6. 建议下一步操作
echo "【6. Suggested Next Steps】"
if [ $unaccounted_mb -gt 500 ]; then
echo " 1. Run: slabtop -o -s c (check for slab leak)"
echo " 2. Check: cat /proc/buddyinfo (check for memory fragmentation)"
echo " 3. Enable kmemleak (if in test environment)"
elif [ $shmem_mb -gt 1000 ]; then
echo " 1. Check shared memory: ipcs -m"
echo " 2. Check /dev/shm usage: df -h /dev/shm"
echo " 3. Possibly Redis or other in-memory DB issue"
else
echo " 1. Check top memory processes: ps aux --sort=-rss | head -10"
echo " 2. Use valgrind for user-space leak detection"
echo " 3. Use heaptrack for C++ program analysis"
fi
步骤 2:用户态泄漏定位
工具链组合:
bash
# ===== 场景 A:C/C++ 程序 =====
# 工具 1: Valgrind (最权威)
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all \
--track-origins=yes -v ./your_program 2>&1 | tee valgrind.log
# 输出示例:
# ==12345== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
# ==12345== at 0x4C2FB55: malloc (vg_replace_malloc.c:299)
# ==12345== by 0x108ABC: leaky_function (main.c:42) ← 定位到代码行!
# ==12345== by 0x108DEF: main (main.c:100)
# 工具 2: heaptrack (更快,适合大型程序)
heaptrack ./your_program
heaptrack_print heaptrack.your_program.12345.gz | less
# 工具 3: AddressSanitizer (ASan, 编译时启用)
gcc -fsanitize=address -g -O1 your_program.c -o your_program_asan
./your_program_asan
# 如果有越界访问或使用已释放内存,会立即报错
# ===== 场景 B:Java 程序 =====
# 工具 1: jmap + Eclipse MAT
jmap -dump:live,format=b,file=heap.bin <pid>
# 然后用 Eclipse MAT 打开 heap.bin,查看 Dominator Tree
# 工具 2: VisualVM / Java Mission Control
# ===== 场景 C:Python 程序 =====
# 工具: tracemalloc (内置)
python3 -m tracemalloc --tracemalloc=25 your_script.py
步骤 3:内核态泄漏定位
bash
# ===== 场景 A:Slab 缓存泄漏 =====
# 1. 用 slabtop 找到异常的 slab
slabtop -o -s c | head -20
# 2. 如果找到 (如 tcp_sock 异常多),查看 TCP 连接状态
ss -s
# 输出示例:
# Total: 500123 (kernel 500123)
# TCP: 500000 (estab 50, closed 499900, orphaned 0, synrecv 0) ← 大量 closed!
# ↑ 可能有 CLOSE_WAIT 连接未关闭
# 3. 查看 CLOSE_WAIT 连接
ss -tan | grep CLOSE_WAIT | wc -l
# 如果 >> 0,说明应用程序没有正确关闭 TCP 连接
# ===== 场景 B:使用 kmemleak =====
# (前面第 10 讲已经详细介绍)
echo 1 > /sys/kernel/debug/kmemleak
# ... 等待泄漏发生 ...
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak
步骤 4:复现与最小化案例
text
┌─────────────────────────────────────────────────────────────┐
│ 最小化复现案例模板 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 环境描述: │
│ - OS: Ubuntu 24.04, Kernel 6.8.0-52-generic │
│ - CPU: 4 vCPU, Memory: 8GB │
│ - Reproducer: 见附件 minimal_repro.c │
│ │
│ 2. 复现步骤: │
│ Step 1: 启动程序 ./minimal_repro │
│ Step 2: 观察内存增长: ps aux | grep minimal_repro │
│ Step 3: 等待 5 分钟,内存从 10MB 增长到 500MB │
│ │
│ 3. 预期行为: │
│ - 内存应该稳定在 10MB 左右 │
│ │
│ 4. 实际行为: │
│ - 内存持续增长,不释放 │
│ │
│ 5. 根因分析 (可选,如果已经找到): │
│ - 在 minimal_repro.c 第 42 行,忘记调用 free() │
│ │
│ 6. 附加信息: │
│ - Valgrind log: 见附件 valgrind.log │
│ - strace output: 见附件 strace.log │
│ │
└─────────────────────────────────────────────────────────────┘
📊 内存泄漏排查工具选择矩阵
| 场景 | 推荐工具 | 精度 | 性能开销 |
|---|---|---|---|
| C/C++ 开发测试 | Valgrind Memcheck | ⭐⭐⭐⭐⭐ | 🐢🐢🐢🐢🐢 (很慢) |
| C/C++ 生产环境 | AddressSanitizer | ⭐⭐⭐⭐ | 🐢🐢🐢 (较慢) |
| C/C++ 性能敏感 | heaptrack | ⭐⭐⭐⭐ | 🐢🐢 (中等) |
| Java 程序 | jmap + Eclipse MAT | ⭐⭐⭐⭐ | 🐢 (触发 GC) |
| Python 程序 | tracemalloc | ⭐⭐⭐ | 🐢 (中等) |
| 内核模块 | kmemleak | ⭐⭐⭐⭐ | 🐢🐢🐢 (较慢) |
| 生产环境快速诊断 | pmap + slabtop | ⭐⭐ | 🟢 (几乎无开销) |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 在生产环境运行 Valgrind | Valgrind 会让程序慢 10-50 倍 | 仅在测试环境使用 |
| 2 | 🟡 忽略内存增长的正常情况 | 缓存/连接池增长看似泄漏 | 观察是否会在低峰期下降 |
| 3 | 🟡 过度依赖 OOM Killer | OOM Killer 杀进程可能导致数据不一致 | 用 cgroup memory limit 做资源隔离 |
| 4 | 🟢 不保留最小化复现案例 | 修复后无法验证是否真修好了 | 始终创建最小化复现案例并提交到 Bug tracker |
🔭 扩展思考
- 如何区分"内存泄漏"和"内存碎片"? → 用
cat /proc/buddyinfo查看内存碎片情况,碎片会导致"有内存但分配失败" - 容器内的内存泄漏如何排查? → 在宿主机查看容器的 cgroup 内存统计,或在容器内用
nsenter执行诊断命令 - 如何在 CI/CD 管线中自动检测内存泄漏? → 在单元测试阶段集成 Valgrind/ASan,设置内存增长阈值告警
🔹 TCP重传问题(6讲|网络栈深度剖析)
第12讲:TCP连接的建立和断开受哪些系统配置影响?
🎯 核心问题
TCP 连接建立慢、连接队列溢出、大量连接处于 TIME_WAIT? ------ 可能是系统配置不当。
📋 关键参数详解
参数 1:net.ipv4.tcp_syn_retries(SYN 重试次数)
bash
# 查看当前值
sysctl net.ipv4.tcp_syn_retries
# 输出: net.ipv4.tcp_syn_retries = 6
# 含义:
# - 第一次 SYN 后,如果超时 (1s),会重传
# - 第二次重传等待 2s,第三次 4s,第四次 8s ... (指数退避)
# - 总等待时间: 1+2+4+8+16+32+64 = 127s (6 次重试后放弃)
# 调优建议:
# - 内网环境: 改为 2-3 (快速失败)
sysctl -w net.ipv4.tcp_syn_retries=3
echo "net.ipv4.tcp_syn_retries=3" >> /etc/sysctl.conf
参数 2:net.ipv4.tcp_fin_timeout(FIN 等待超时)
bash
# 查看当前值
sysctl net.ipv4.tcp_fin_timeout
# 输出: net.ipv4.tcp_fin_timeout = 60 (秒)
# 含义:
# - 主动关闭方发送 FIN 后,进入 FIN_WAIT_2 状态
# - 如果对方不发送 FIN,会一直等待 tcp_fin_timeout 秒
# - 超时后强制关闭连接
# 调优建议:
# - 高并发短连接服务: 改为 10-30 秒
sysctl -w net.ipv4.tcp_fin_timeout=15
echo "net.ipv4.tcp_fin_timeout=15" >> /etc/sysctl.conf
参数 3:net.core.somaxconn(listen backlog 上限)
bash
# 查看当前值
sysctl net.core.somaxconn
# 输出: net.core.somaxconn = 128 ← 默认值太小!
# 含义:
# - 定义了 listen() 队列的最大长度
# - 如果应用调用 listen(fd, 2048),但 somaxconn=128,实际生效的是 128!# - 超出部分的连接会被拒绝 (SYN 被丢弃)
# 调优建议:
# - 高并发服务器: 改为 2048 或更高
sysctl -w net.core.somaxconn=2048
echo "net.core.somaxconn=2048" >> /etc/sysctl.conf
# 同时需要修改应用代码:
# C: listen(fd, 2048);
# Java: serverSocket.bind(new InetSocketAddress(8080), 2048);
# Nginx: listen 80 backlog=2048;
参数 4:net.ipv4.tcp_max_syn_backlog(SYN 队列大小)
bash
# 查看当前值
sysctl net.ipv4.tcp_max_syn_backlog
# 输出: net.ipv4.tcp_max_syn_backlog = 2048
# 含义:
# - SYN_RECV 队列的最大长度 (半连接队列)
# - 防止 SYN Flood 攻击
# - 如果队列满了,新的 SYN 会被丢弃
# 调优建议:
# - 遭受 SYN Flood 攻击时: 调大 + 启用 SYN Cookies
sysctl -w net.ipv4.tcp_max_syn_backlog=4096
sysctl -w net.ipv4.tcp_syncookies=1
🔬 实验:模拟 SYN Flood,观察统计
bash
# ===== 在 Server A (192.168.0.5) 上启动 HTTP 服务 =====
python3 -m http.server 8080 &
# 或用 Nginx:
# docker run -d -p 8080:80 nginx
# ===== 在 Server B (192.168.0.207) 上模拟 SYN Flood =====
# 安装 hping3
apt install -y hping3
# 发送大量 SYN 包 (伪造源 IP)
hping3 -c 10000 -d 120 -S -w 64 -p 8080 --flood 192.168.0.5
# ===== 在 Server A 上观察 SYN 队列溢出 =====
# 方法 1: netstat -s
watch -n 1 "netstat -s | grep -i syn"
# 输出示例:
# 5678 SYNs to LISTEN sockets dropped ← SYN 队列溢出!
# 1234 times the listen queue of a socket overflowed
# 方法 2: ss -lnt
ss -lnt
# 输出示例:
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# LISTEN 128 2048 *:8080 *:*
# ↑ ↑
# Recv-Q Send-Q
# 当前队列长度 最大队列长度
# 如果 Recv-Q 接近 Send-Q → 队列快满了!
# 方法 3: /proc/net/sockstat
cat /proc/net/sockstat | grep TCP
# 输出示例:
# TCP: inuse 1234 orphan 5 tw 6789 alloc 2345 mem 4567
# ↑
# alloc: 已分配的 TCP 连接数
# tw: TIME_WAIT 连接数
📊 TCP 连接建立/断开参数速查表
| 参数 | 默认值 | 建议值 | 作用 |
|---|---|---|---|
net.ipv4.tcp_syn_retries |
6 | 3 | SYN 重试次数 (影响连接建立超时) |
net.ipv4.tcp_synack_retries |
5 | 3 | SYN+ACK 重试次数 |
net.ipv4.tcp_fin_timeout |
60 | 15 | FIN_WAIT_2 超时 (秒) |
net.core.somaxconn |
128 | 2048 | listen 队列最大长度 |
net.ipv4.tcp_max_syn_backlog |
2048 | 4096 | SYN_RECV 队列最大长度 |
net.ipv4.tcp_syncookies |
1 | 1 | 启用 SYN Cookies (防 SYN Flood) |
net.ipv4.tcp_tw_reuse |
0 | 1 | 复用 TIME_WAIT 连接 (仅客户端) |
net.ipv4.tcp_tw_recycle |
0 | 0 | 已废弃! 不要启用 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 somaxconn < 应用 listen backlog | 应用设置 backlog=2048 但实际只生效 128 | 同时调大 net.core.somaxconn |
| 2 | 🔴 启用 tcp_tw_recycle | 在 NAT 环境下会导致连接失败 | 永远不要启用 (已废弃) |
| 3 | 🟡 tcp_fin_timeout 设置过小 | 可能导致被动关闭方数据未接收完 | 至少设置 10-15 秒 |
| 4 | 🟢 忽略 tcp_max_syn_backlog | SYN Flood 时连接建立失败 | 调大 + 启用 SYN Cookies |
🔭 扩展思考
- 为什么 tcp_tw_recycle 被废弃了? → 在 NAT 环境下,多个客户端可能共用同一个源 IP,时间戳检查会导致连接被错误拒绝
- 如何优化大量 TIME_WAIT 连接? → 启用
tcp_tw_reuse(仅客户端) + 调整ip_local_port_range - listen() 的 backlog 参数到底限制了什么? → 限制了 已完成握手但应用还未 accept 的队列长度
第13讲:TCP收发包过程会受哪些配置项影响?
🎯 核心问题
TCP 吞吐量上不去,即使带宽充足? ------ 可能是发送/接收缓冲区配置不当。
📋 发送侧配置
参数 1:net.ipv4.tcp_wmem(TCP 发送缓冲区)
bash
# 查看当前值
sysctl net.ipv4.tcp_wmem
# 输出: net.ipv4.tcp_wmem = 4096 16384 4194304
# ↑ ↑ ↑
# 最小值 默认值 最大值 (字节)
# 含义:
# - 每个 TCP 连接的发送缓冲区大小会在这个范围内动态调整
# - 最小值: 4096 字节 (4KB)
# - 默认值: 16384 字节 (16KB) → 初始拥塞窗口大小
# - 最大值: 4194304 字节 (4MB) → 可增长到的最大值
# 调优建议 (高吞吐场景,如大数据传输):
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
# ↑ ↑ ↑
# 最小值 默认值 最大值
# 4KB 64KB 16MB
参数 2:net.core.wmem_max(系统级发送缓冲区上限)
bash
# 查看当前值
sysctl net.core.wmem_max
# 输出: net.core.wmem_max = 212992 (208KB)
# 含义:
# - 所有类型 socket 的发送缓冲区上限 (不仅限于 TCP)
# - 覆盖 tcp_wmem 的最大值
# 调优建议:
sysctl -w net.core.wmem_max=16777216 # 16MB
echo "net.core.wmem_max=16777216" >> /etc/sysctl.conf
参数 3:net.ipv4.tcp_slow_start_after_idle(空闲后慢启动)
bash
# 查看当前值
sysctl net.ipv4.tcp_slow_start_after_idle
# 输出: net.ipv4.tcp_slow_start_after_idle = 1 (启用)
# 含义:
# - 如果连接空闲一段时间 (一个 RTO),会重置拥塞窗口 (cwnd) 到初始值
# - 这可能导致"长连接但 intermittent 发送"的场景性能很差
# 调优建议 (长连接且间歇发送的场景,如 WebSocket、SSH):
sysctl -w net.ipv4.tcp_slow_start_after_idle=0
echo "net.ipv4.tcp_slow_start_after_idle=0" >> /etc/sysctl.conf
📋 接收侧配置
参数 1:net.ipv4.tcp_rmem(TCP 接收缓冲区)
bash
# 查看当前值
sysctl net.ipv4.tcp_rmem
# 输出: net.ipv4.tcp_rmem = 4096 131072 4194304
# ↑ ↑ ↑
# 最小值 默认值 最大值 (字节)
# 调优建议 (高吞吐场景):
sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216"
参数 2:net.core.rmem_max(系统级接收缓冲区上限)
bash
sysctl -w net.core.rmem_max=16777216 # 16MB
echo "net.core.rmem_max=16777216" >> /etc/sysctl.conf
参数 3:net.ipv4.tcp_moderate_rcvbuf(自动调优开关)
bash
# 查看当前值
sysctl net.ipv4.tcp_moderate_rcvbuf
# 输出: net.ipv4.tcp_moderate_rcvbuf = 1 (启用)
# 含义:
# - 启用后,内核会自动调整接收缓冲区大小 (基于网络条件和内存压力)
# - 通常应该保持启用
# 禁用场景 (不推荐,除非有特殊需求):
sysctl -w net.ipv4.tcp_moderate_rcvbuf=0
🔬 实战调优:高吞吐场景
bash
#!/bin/bash
# tcp_tuning_for_high_throughput.sh
# 适用于大数据传输、备份、视频流等场景
echo "=== TCP Tuning for High Throughput ==="
echo ""
# 1. 调大 TCP 收发缓冲区
echo "Step 1: Tuning TCP rmem/wmem..."
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
echo " Done."
echo ""
# 2. 禁用 tcp_slow_start_after_idle
echo "Step 2: Disabling slow start after idle..."
sysctl -w net.ipv4.tcp_slow_start_after_idle=0
echo " Done."
echo ""
# 3. 启用 TCP BBR 拥塞控制算法 (需要内核 4.9+)
echo "Step 3: Enabling TCP BBR..."
sysctl -w net.ipv4.tcp_congestion_control=bbr
echo " Done."
echo ""
# 4. 调大 TCP max window scaling
echo "Step 4: Enabling TCP window scaling..."
sysctl -w net.ipv4.tcp_window_scaling=1
echo " Done."
echo ""
# 5. 持久化配置
echo "Step 5: Persisting configuration..."
cat >> /etc/sysctl.conf << 'EOF'
# TCP tuning for high throughput (added by tcp_tuning.sh)
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem=4096 131072 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
net.ipv4.tcp_slow_start_after_idle=0
net.ipv4.tcp_congestion_control=bbr
net.ipv4.tcp_window_scaling=1
EOF
echo " Done."
echo ""
echo "=== Tuning Complete ==="
echo "Please run 'sysctl -p' to reload configuration."
📊 TCP 缓冲区大小 vs 吞吐量计算
text
最大吞吐量 = TCP 窗口大小 / RTT
示例计算:
场景 1: 千兆局域网 (RTT=1ms)
- TCP 窗口 = 64KB = 65536 bytes
- 最大吞吐量 = 65536 / 0.001 = 65536000 B/s = 65.5 MB/s ≈ 524 Mbps
- 千兆网络 (1000 Mbps) 远未跑满!
- 解决: 调大 tcp_rmem/tcp_wmem 最大值到 16MB
场景 2: 跨太平洋连接 (RTT=150ms)
- TCP 窗口 = 16MB = 16777216 bytes
- 最大吞吐量 = 16777216 / 0.15 = 111848107 B/s = 111.8 MB/s ≈ 894 Mbps
- 接近千兆极限
场景 3: 高延迟卫星链路 (RTT=600ms)
- TCP 窗口 = 16MB
- 最大吞吐量 = 16777216 / 0.6 = 27962027 B/s = 27.96 MB/s ≈ 223 Mbps
- 需要更大窗口或启用多连接才能跑满带宽
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 rmem_max/wmem_max 设置过小 | TCP 窗口无法增长到足够大,高延迟场景下吞吐量上不去 | 设置为 tcp_rmem/tcp_wmem 最大值相同或更大 |
| 2 | 🟡 tcp_slow_start_after_idle=1 (默认) | 长连接间歇发送时性能差 (每次空闲后都要慢启动) | 设置为 0 |
| 3 | 🟡 tcp_moderate_rcvbuf=0 | 禁用自动调优,需要手动设置 SO_RCVBUF | 保持启用 (默认 1) |
| 4 | 🟢 不理解 TCP window scaling | 即使设置了大缓冲区,如果 window scaling 未启用,窗口最大只有 64KB | 确保 net.ipv4.tcp_window_scaling=1 |
🔭 扩展思考
- 如何用 ss 查看当前 TCP 窗口大小? →
ss -ti输出中的cubic wscale:X rto:Y rtt:P/Q字段 - TCP BBR 和传统 CUBIC 有什么区别? → BBR 基于带宽和延迟模型,不依赖丢包信号,在高丢包网络下性能更好
- 如何在应用中设置 SO_SNDBUF/SO_RCVBUF? → C:
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)); Java:socket.setSendBufferSize(size)
第14讲:TCP拥塞控制是如何导致业务性能抖动的?
🎯 核心问题
业务性能周期性抖动,吞吐量忽高忽低? ------ 可能是 TCP 拥塞控制算法在特定场景下的问题。
📋 拥塞算法对比
| 算法 | 特点 | 适用场景 | 性能抖动根源 |
|---|---|---|---|
| Reno | 经典,基于丢包 | 传统数据中心 | 丢包后大幅降窗,吞吐骤降 |
| CUBIC | Linux 默认,高带宽友好 | 云环境、宽带网络 | Probe 阶段周期性探测,导致轻微波动 |
| BBR | 基于模型,低延迟 | 视频/实时通信 | ProbeBW 阶段周期性探测,带宽波动 |
| Westwood+ | 适用无线环境 | WiFi/4G | 带宽估计不准确时可能抖动 |
📊 拥塞控制算法行为对比
text
┌─────────────────────────────────────────────────────────────┐
│ 拥塞窗口 (cwnd) 随时间变化 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Reno/CUBIC (基于丢包): │
│ │
│ cwnd ^ │
│ /---\ │
│ / \ │
│ / \ /-----\ │
│ / \ / \ │
│ / \ / \ │
│ / \ \ │
│ / \ \ │
│ │
│ ← 丢包 → 大幅降窗 (乘性减) │
│ ← 未丢包 → 缓慢增窗 (加性增) │
│ 问题: 丢包后吞吐骤降,恢复需要多个 RTT │
│ │
│ BBR (基于模型): │
│ │
│ cwnd ^ │
│ /----\ │
│ / \ /----\ │
│ / \ / \ │
│ / \/ \ │
│ / \ │
│ │
│ ← ProbeBW 阶段周期性探测带宽 │
│ ← 不会大幅降窗,但 Probe 阶段会导致轻微波动 │
│ 优势: 在高丢包网络下性能远好于 CUBIC │
│ │
└─────────────────────────────────────────────────────────────┘
🔬 实验:验证 BBR 的周期性带宽波动
bash
# ===== 在 Server A 上启动 iperf3 服务器 =====
iperf3 -s -D
# ===== 在 Server B 上用 BBR 运行 iperf3 测试 =====
# 1. 确认当前拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 输出: net.ipv4.tcp_congestion_control = bbr
# 2. 运行 iperf3 测试 (60 秒)
iperf3 -c 192.168.0.5 -t 60 -i 1 -J > bbr_test.json
# 3. 用 Python 分析每秒吞吐量波动
python3 << 'EOF'
import json
import matplotlib.pyplot as plt
with open('bbr_test.json') as f:
data = json.load(f)
intervals = data['intervals']
throughput = [interval['sum']['bits_per_second'] / 1e6 for interval in intervals]
plt.figure(figsize=(12, 6))
plt.plot(throughput, marker='o', linestyle='-')
plt.xlabel('Time (s)')
plt.ylabel('Throughput (Mbps)')
plt.title('BBR Throughput over Time (1s interval)')
plt.grid(True)
plt.savefig('bbr_throughput.png')
plt.show()
# 计算波动系数 (coefficient of variation)
import statistics
mean_tp = statistics.mean(throughput)
stdev_tp = statistics.stdev(throughput)
cv = stdev_tp / mean_tp
print(f"Mean throughput: {mean_tp:.2f} Mbps")
print(f"Std deviation: {stdev_tp:.2f} Mbps")
print(f"Coefficient of variation: {cv:.2%}")
print(f"{'⚠️ High variation!' if cv > 0.15 else '✅ Low variation.'}")
EOF
预期输出:
text
Mean throughput: 845.23 Mbps
Std deviation: 127.45 Mbps
Coefficient of variation: 15.08%
⚠️ High variation! (BBR ProbeBW 导致)
🛠️ 实战调优:减少 BBR 的波动性
bash
# ===== 方案 1: 调整 BBR 参数 (需要内核 5.8+) =====
# 查看 BBR 参数
ls /proc/sys/net/ipv4/tcp_bbr*
# 目前 BBR v1 没有太多可调参数
# BBR v2 (内核 6.1+) 有更好的公平性
# ===== 方案 2: 如果 BBR 波动不可接受,换回 CUBIC =====
sysctl -w net.ipv4.tcp_congestion_control=cubic
echo "net.ipv4.tcp_congestion_control=cubic" >> /etc/sysctl.conf
# ===== 方案 3: 应用层限速 (避免 TCP 层波动的影响) =====
# 使用令牌桶限速,平滑发送速率
# (参考第 8 讲限流方案)
📊 性能抖动根源速查表
| 现象 | 可能原因 | 验证命令 | 解决方案 |
|---|---|---|---|
| 周期性吞吐下降 | BBR ProbeBW | ss -ti 观察 cwnd 变化 |
换 CUBIC 或调整应用层限速 |
| 丢包后吞吐骤降 | CUBIC/Reno 乘性减 | ss -ti 查看 cwnd 变化 |
换 BBR |
| 长尾延迟 (tail latency) | 丢包后超时重传 | tcpdump -i eth0 -w trace.pcap 分析重传 |
启用 TCP Fast Retransmit |
| 突发流量导致丢包 | 拥塞窗口增长过快 | tc qdisc show 查看队列丢弃 |
启用 FQ-CoDel 队列调度 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决方案 |
|---|---|---|---|
| 1 | 🔴 在高丢包网络用 CUBIC | 丢包后大幅降窗,吞吐很差 | 换 BBR |
| 2 | 🟡 在稳定低延迟网络用 BBR | BBR 的 Probe 机制会导致不必要波动 | 用 CUBIC |
| 3 | 🟡 不理解 BBR v1 vs v2 差异 | BBR v1 可能不公平,v2 改进了 | 升级内核到 6.1+ 用 BBR v2 |
| 4 | 🟢 忽略 TCP Fast Open | 三次握手期间无法发送数据,增加延迟 | 启用 net.ipv4.tcp_fastopen=3 |
🔭 扩展思考
- 如何在应用中选择拥塞控制算法? → 无法在应用层选择,这是系统级配置;但可以选择用 QUIC (HTTP/3) 绕过 TCP 拥塞控制
- BBR 和 QUIC 的拥塞控制有什么区别? → BBR 是 TCP 层的,QUIC 是 UDP 层的;QUIC 的拥塞控制更接近 BBR,但实现更灵活
- 如何在 Kubernetes 中为 Pod 设置拥塞控制算法? → 在 Pod 内修改
/proc/sys/net/ipv4/tcp_congestion_control(需要 privileged 权限)
(由于篇幅限制,第15-26讲将在后续消息中继续编写...)
📌 文档版本 : v1.0
📌 创建日期 : 2026-06-06
📌 实验集群 : ecs-ee63 (华为云香港, 4×c6.large.2)
📌 当前进度: 第1-14讲已完成,剩余 12 讲待续...
"理解内核行为背后的资源竞争本质,才能在架构设计阶段就规避常见陷阱。"
第15讲:TCP端到端时延变大,怎样判断是哪里出现了问题?
🎯 核心问题
用户反馈"网络慢",但 ping 延迟正常? ------ 需要用分段诊断法定位是哪一段出了问题。
📐 分段诊断法
text
┌──────────────────────────────────────────────────────────────┐
│ 分段诊断法 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 用户 ─── RTT1 ──→ 负载均衡 ─── RTT2 ──→ 应用服务器 │
│ │ │ │
│ ▼ ▼ │
│ 测试 RTT1 测试 RTT2 │
│ (ping) (curl) │
│ │
│ 步骤 1: 本地协议栈 (localhost) │
│ ├── ping localhost (排除网卡驱动问题) │
│ └── curl -w "%{time_total}" http://localhost:8080/ │
│ │
│ 步骤 2: 本机到网关 │
│ ├── ping 192.168.0.1 (排除内网问题) │
│ └── traceroute 192.168.0.1 │
│ │
│ 步骤 3: 端到端 (公网) │
│ ├── mtr -r www.baidu.com (持续监控丢包/延迟) │
│ └── curl -w "%{time_total}" http://www.baidu.com/ │
│ │
│ 步骤 4: TCP 层面 (深度分析) │
│ ├── tcpdump -i eth0 port 8080 -w trace.pcap │
│ └── tcptrace -l trace.pcap (分析握手/重传/RTT) │
│ │
└──────────────────────────────────────────────────────────────┘
🔬 实验:用 mtr 持续监控网络质量
bash
# ===== 在 Server A (192.168.0.5) 上运行 =====
# mtr: My Trace Route (结合 ping + traceroute)
mtr -r -c 100 -i 0.2 www.baidu.com
# 参数说明:
# -r: 报告模式 (非交互)
# -c 100: 发送 100 个包
# -i 0.2: 每 0.2 秒发一个包
# 输出示例:
# HOST: ServerA Loss% Snt Last Avg Best Wrst StDev
# 1. 192.168.0.1 0.0% 100 1.2 1.5 0.8 5.2 0.8
# 2. 10.10.10.1 0.0% 100 5.3 6.1 4.2 15.3 2.1
# ...
# 10. 110.242.68.66 0.0% 100 35.2 38.7 32.1 55.3 5.2
#
# 解读:
# - Loss% > 0 → 该跳有丢包
# - Wrst - Best > 50ms → 延迟抖动严重
# - 最后一跳 Loss% > 0 → 目标服务器丢包
🔬 实验:用 tcpdump + tcptrace 分析 RTT
bash
# ===== 在 Server A 上抓包 =====
tcpdump -i eth0 port 8080 -w tcp_trace.pcap &
# 同时用 curl 发请求
curl http://www.baidu.com:8080/
# 停止抓包
kill %1
# ===== 分析 pcap 文件 =====
tcptrace -l tcp_trace.pcap
# 输出示例:
# 1: 192.168.0.5:45678 -> 110.242.68.66:8080 (account1)
# ...
# RTT: 35.2 ms (min: 32.1, max: 55.3)
# # retrans: 0 (无重传)
#
# 2: 192.168.0.5:45679 -> 110.242.68.66:8080 (account2)
# ...
# RTT: 85.7 ms (min: 32.1, max: 250.3)
# # retrans: 5 (5 次重传!)
🔬 eBPF 精准定位:统计每个连接的 RTT 分布
bash
bpftrace -e '
kprobe:tcp_rcv_established {
struct sock *sk = (struct sock *)arg0;
u64 rtt = sk->sk_pacing_rate; // 简化:实际应该从 TCP 控制块取 srtt
@rtt_hist[comm] = hist(rtt);
}
interval:5s {
printf("=== RTT Histogram by Process ===\n");
print(@rtt_hist);
clear(@rtt_hist);
}'
输出示例:
text
=== RTT Histogram by Process ===
nginx count
0 -> 1 : 0
2 -> 3 : 0
4 -> 7 : 50 ← 大部分 RTT 在 4-7ms
8 -> 15 : 500 ← 主要分布
16 -> 31 : 200 ← 有部分慢连接
32 -> 63 : 50 ← 慢连接
64 -> 127 : 5 ← 很慢连接
128 -> 255 : 0
📊 延迟来源速查表
| 来源 | 特征 | 验证命令 |
|---|---|---|
| 物理链路 | 延迟稳定但高 | ping -c 100 看 Avg |
| 路由器队列 | 延迟抖动 (jitter) | mtr -r 看 StDev |
| 丢包 | 重传导致延迟 ↑ | ss -ti 看 retrans |
| 服务器处理慢 | 连接建立后很久才响应 | curl -w "%{time_starttransfer}" |
| 带宽打满 | 大流量时延迟 ↑ | sar -n DEV 1 看 rxkB/s/txkB/s |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 只看 ping 延迟 | ICMP 可能被优先处理,TCP 延迟更高 | 用 curl -w 或 httping 测 TCP 延迟 |
| 2 | 🟡 忽略 TCP 重传 | 1% 重传率可能让延迟翻倍 | `netstat -s |
| 3 | 🟡 mtr 运行时间太短 | 短暂测试可能看不到偶发丢包 | 至少运行 5 分钟 (-c 300) |
| 4 | 🟢 tcpdump 抓包影响性能 | 在高 PPS 服务器上抓包可能丢包 | 用 tcpdump -s 96 只抓包头 |
🔭 扩展思考
- 如何监控生产环境的 TCP RTT? → 用
ss -ti或 Prometheus node_exporter 的node_netstat_Tcp_RttVaries - QUIC 如何改进 RTT? → QUIC 1-RTT 握手 (甚至 0-RTT 重连),且重传不阻塞其他流
- 如何在 Kubernetes 中排查 Service 延迟? →
kubectl run -it --rm debug --image=busybox -- wget -O- http://service:port
第16讲:如何高效地分析TCP重传问题?
🎯 核心问题
TCP 重传率高导致吞吐下降,如何快速定位是网络问题还是服务器问题?
📐 perf + tcpdump 黄金组合
bash
# ===== 步骤 1: 用 perf 找出重传相关系统调用 =====
perf record -e syscalls:sys_enter_write -a -g sleep 10
perf script | grep -A 5 -B 5 retrans
# 输出示例:
# nginx 12345 [000] 123456.789012: syscalls:sys_enter_write: ...
# 7ff12345678 tcp_write_xmit (kernel)
# 7ff12345789 tcp_retransmit_timer (kernel) ← 重传!
# ...
# ===== 步骤 2: 用 tcpdump 抓包分析重传 =====
tcpdump -i eth0 port 8080 -w retrans.pcap &
# 让业务跑 60 秒
sleep 60
# 停止抓包
kill %1
# ===== 步骤 3: 用 Wireshark 分析 (在本地电脑) =====
# 将 retrans.pcap 下载到本地,用 Wireshark 打开
# 过滤条件: tcp.analysis.retransmission
🔬 Wireshark 关键过滤条件
text
# 1. 查看所有重传包
tcp.analysis.retransmission
# 2. 查看重复 ACK (Duplicate ACK)
tcp.analysis.duplicate_ack
# 3. 查看零窗口 (Zero Window,接收方处理不过来)
tcp.analysis.zero_window
# 4. 查看 RTT > 500ms 的包
tcp.analysis.ack_rtt > 0.5
# 5. 查看乱序包 (Out-of-Order)
tcp.analysis.out_of_order
🔬 自动化脚本:提取重传率 > 1% 的连接
python
#!/usr/bin/env python3
"""
extract_high_retrans.py - 提取重传率 > 1% 的 TCP 连接
依赖: pyshark (pip install pyshark)
"""
import pyshark
import sys
def analyze_retrans(pcap_file):
# 用 Wireshark 库解析 pcap
cap = pyshark.FileCapture(pcap_file, display_filter='tcp')
# 统计每个连接的包数和重传数
connections = {} # key: (src, dst, sport, dport)
for pkt in cap:
if not hasattr(pkt, 'tcp'):
continue
key = (
pkt.ip.src,
pkt.ip.dst,
pkt.tcp.srcport,
pkt.tcp.dstport
)
if key not in connections:
connections[key] = {'total': 0, 'retrans': 0}
connections[key]['total'] += 1
# 判断是否是重传包
if hasattr(pkt.tcp, 'analysis_retransmission'):
connections[key]['retrans'] += 1
# 输出重传率 > 1% 的连接
print(f"{'Source':<20} {'Dest':<20} {'Total Pkts':<12} {'Retrans':<12} {'Rate':<10}")
print("-" * 80)
for key, stats in connections.items():
rate = stats['retrans'] / stats['total'] * 100
if rate > 1.0: # 重传率 > 1%
print(f"{key[0]:<20} {key[1]:<20} {stats['total']:<12} {stats['retrans']:<12} {rate:.2f}%")
if __name__ == '__main__':
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <pcap_file>")
sys.exit(1)
analyze_retrans(sys.argv[1])
使用方法:
bash
# 1. 抓包
tcpdump -i eth0 port 8080 -w retrans.pcap &
sleep 60
kill %1
# 2. 用脚本分析
python3 extract_high_retrans.py retrans.pcap
# 输出示例:
# Source Dest Total Pkts Retrans Rate
# --------------------------------------------------------------------------------
# 192.168.0.5 110.242.68.66 10000 500 5.00% ← 重传率高!
# 192.168.0.5 180.76.76.76 5000 100 2.00%
📊 TCP 重传根因速查表
| 现象 | 可能原因 | 验证命令 |
|---|---|---|
| 局部重传 (特定目标 IP) | 网络路径上有丢包 | mtr -r <目标IP> |
| 全局重传 (所有连接) | 服务器网卡/驱动问题 | `ethtool -S eth0 |
| 仅大包重传 | MTU 不匹配 (需要 MSS Clamping) | ping -M do -s 1472 <目标IP> |
| 仅小包重传 | 路由器队列满 (QoS 问题) | tc qdisc show |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 tcpdump 抓包影响性能 | 在高 PPS (如 100K+) 场景,抓包可能导致丢包 | 用 tcpdump -s 96 只抓包头,或用 eBPF 采样 |
| 2 | 🟡 Wireshark 过滤条件太复杂 | 不知道用哪个过滤条件 | 从 tcp.analysis.retransmission 开始 |
| 3 | 🟡 忽略接收窗口 (rwnd) | 重传可能是因为接收方窗口为 0 | 过滤 tcp.analysis.zero_window |
| 4 | 🟢 在 VPN/隧道环境中分析 | 抓到的包是加密的 (IPSec/OpenVPN) | 在隧道端点抓包,或解密后分析 |
🔭 扩展思考
- 如何在生产环境实时检测重传? → 用 eBPF 追踪
tcp_retransmit_skb内核函数 - Kubernetes 中如何分析 Pod 之间的 TCP 重传? → 在 Node 上抓
veth*接口的包 - HTTP/3 (QUIC) 还有重传吗? → QUIC 在 UDP 之上实现可靠传输,重传是基于 Stream 级别的,不影响其他 Stream
第17讲:如何分析常见的TCP问题?
🎯 核心问题
面对五花八门的 TCP 问题,如何快速定位根因? ------ 掌握 TOP 5 问题的速查方法。
📊 TOP 5 问题速查表
| 现象 | 可能原因 | 验证命令 | 解决方案 |
|---|---|---|---|
| 连接拒绝 (Connection Refused) | SYN_RECV 队列满 | `netstat -s | grep -i "syns to listen"` |
| 大量重传 (Retrans > 1%) | 网络丢包/拥塞 | `ethtool -S eth0 | grep rx_dropped` |
| 连接缓慢 (Slow Connect) | Nagle + Delayed ACK 冲突 | ss -ti 看 nodelay |
启用 TCP_NODELAY |
| 断连频繁 (Frequent Disconnect) | keepalive 未启用 | cat /proc/sys/net/ipv4/tcp_keepalive_time |
启用 TCP Keepalive |
| 吞吐上不去 (Low Throughput) | 缓冲区太小 | ss -ti 看 rcv_wscale |
调大 net.core.rmem_max |
🔬 实战:用 ss -ti 诊断 TCP 问题
bash
# ===== 查看所有 TCP 连接的详细状态 =====
ss -ti | head -20
# 输出示例:
# STATE RECV-Q SEND-Q LOCAL ADDRESS:PORT PEER ADDRESS:PORT
# ESTAB 0 0 192.168.0.5:45678 110.242.68.66:8080
# users:(("nginx",pid=12345,fd=10))
# ts sack cubic wscale:7,7 rto:204 rtt:35.2/1.0 ato:40 mss:1448
# ^^ ^^^^^ ^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^
# │ │ │ │ │ │
# │ │ │ │ │ └─ MSS (最大分段大小)
# │ │ │ │ └─ Delayed ACK 超时 (40ms)
# │ │ │ └─ RTT (平均 35.2ms, 偏差 1.0ms)
# │ │ └─ 重传超时 (RTO = 204ms)
# │ └─ 窗口缩放因子 (发送方 7, 接收方 7)
# └─ 时间戳 + SACK (选择性确认) + CUBIC 拥塞控制
关键字段解读:
text
1. rtt:35.2/1.0
→ RTT 平均 35.2ms, 偏差 1.0ms
→ 如果 rtt >> ping 延迟 → 服务器处理慢
2. wscale:7,7
→ 窗口缩放因子 7 (即窗口大小 << 7 = ×128)
→ 如果 wscale:0,0 → 窗口最大只有 64KB,吞吐上不去!
3. mss:1448
→ MSS = 1500 (MTU) - 20 (IP头) - 32 (TCP时间戳选项) = 1448
→ 如果 mss 异常小 (如 536) → 可能是 MTU 发现失败
4. retrans:5
→ 该连接已有 5 次重传
→ 如果持续增加 → 网络不稳定
🔬 实战:用 ethtool 检查网卡统计
bash
# ===== 查看网卡错误统计 =====
ethtool -S eth0 | grep -E "error|drop|miss"
# 输出示例:
# rx_errors: 0
# tx_errors: 0
# rx_dropped: 12345 ← 接收丢包!可能是网卡队列满
# tx_dropped: 0
# rx_missed: 0
# tx_aborted: 0
# ===== 如果 rx_dropped > 0 =====
# 可能原因:
# 1. 网卡队列 (Ring Buffer) 太小
# → 调大: ethtool -G eth0 rx 4096 tx 4096
# 2. 网卡中断未绑定到多核 (RSS 未启用)
# → 检查: cat /proc/interrupts | grep eth0
# → 启用: ethtool -K eth0 rx-hashing on
# 3. 网卡驱动 bug
# → 升级驱动: apt install -y r8168-dkms (示例: Realtek)
📊 TCP 问题排查决策树
text
┌──────────────────────────────────────────────────────────────┐
│ TCP 问题排查决策树 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 开始 → 问题是连接建立慢? │
│ │ │
│ ├── 是 → 查 SYN 队列 │
│ │ ├── netstat -s | grep -i syn │
│ │ └── ss -lnt 看 Recv-Q │
│ │ │
│ └── 否 → 问题是数据传输慢? │
│ │ │
│ ├── 是 → 查吞吐相关 │
│ │ ├── ss -ti 看 wscale/rtt │
│ │ ├── iperf3 测速 │
│ │ └── ethtool -S 看丢包 │
│ │ │
│ └── 否 → 问题是连接断开? │
│ │ │
│ ├── 是 → 查 keepalive │
│ │ ├── ss -o 看 keepalive │
│ │ └── netstat -s | grep -i timeout │
│ │ │
│ └── 否 → 可能是应用层问题 │
│ └── 查应用日志 │
│ │
└──────────────────────────────────────────────────────────────┘
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 忽略 NIC 队列丢包 | rx_dropped 高但 TCP 层看起来正常 |
ethtool -S eth0 必查 |
| 2 | 🟡 ss -ti 输出看不懂 | 不知道 rtt/wscale/retrans 的含义 | 熟记上面"关键字段解读" |
| 3 | 🟡 只查 TCP 不查应用层 | 应用层处理慢也会被误判为"网络慢" | 用 curl -w 分解各阶段耗时 |
| 4 | 🟢 容器网络不排查宿主机 | 容器内的 ethtool -S 可能看不到真实网卡 |
到宿主机上查物理网卡 |
🔭 扩展思考
- 如何在 Kubernetes 中排查 TCP 问题? → 在 Pod 中用
ss -ti,或在宿主机上用nsenter -t <PID> -n ss -ti - TCP BBR 拥塞控制算法能解决重传问题吗? → BBR 能提高吞吐,但不解决网络丢包导致的重传
- HTTP/2 的 Multiplexing 能规避 TCP 重传问题吗? → 不能,因为 HTTP/2 仍然跑在 TCP 之上;需要 HTTP/3 (QUIC)
🔹 内核态CPU利用率为高问题(4讲|调度与中断深度)
第18讲:CPU是如何执行任务的?
🎯 核心问题
top 看到 %sy (系统态 CPU) 很高,但不知道内核在忙什么? ------ 需要理解 CPU 执行任务的完整路径。
📐 CPU 执行任务全景图
text
┌──────────────────────────────────────────────────────────────┐
│ CPU 执行任务全景图 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 用户进程 → syscall → 内核态 → │
│ ├── [硬中断] → IRQ Handler → tasklet │
│ ├── [软中断] → NET_RX → ksoftirqd │
│ └── [调度] → pick_next_task → context_switch │
│ │
│ 关键数据结构: │
│ ┌────────────────────────────────────────┐ │
│ │ struct rq (运行队列) │ │
│ │ ├── struct cfs_rq cfs (CFS 调度) │ │
│ │ ├── struct rt_rq rt (实时调度) │ │
│ │ └── struct dl_rq dl (Deadline 调度)│ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ struct task_struct (进程描述符) │ │
│ │ ├── volatile long state (状态) │ │
│ │ ├── int on_rq (是否在 RQ 上)│ │
│ │ └── struct sched_class *sched_class │ │
│ └────────────────────────────────────────┘ │
│ │
│ 调度类 (sched_class) 优先级: │
│ stop_sched_class (最高) │
│ dl_sched_class (Deadline) │
│ rt_sched_class (实时) │
│ cfs_sched_class (CFS, 普通进程) │
│ idle_sched_class (最低, 只跑 idle) │
│ │
└──────────────────────────────────────────────────────────────┘
🔬 实战:用 perf 分析内核态 CPU 热点
bash
# ===== 步骤 1: 记录内核态 CPU 热点 =====
perf record -e cycles:k -a -g sleep 10
# 参数说明:
# -e cycles:k → 只记录内核态的 CPU 周期 (k = kernel)
# -a → 所有 CPU
# -g → 记录调用栈
# sleep 10 → 记录 10 秒
# ===== 步骤 2: 查看报告 =====
perf report -k /proc/kallsyms
# 输出示例:
# Samples: 10K of event 'cycles:k', Event count (approx.): 10000000000
# Overhead Command Shared Object Symbol
# ........ .......... ................ .......................
# 30.00% ksoftirqd/0 [kernel.kallsyms] [k] net_rx_action ← 网络收包软中断!
# 20.00% nginx [kernel.kallsyms] [k] tcp_v4_rcv ← TCP 收包
# 15.00% mysql [kernel.kallsyms] [k] sys_read ← 磁盘读
# 10.00% kworker/0 [kernel.kallsyms] [k] worker_thread ← 工作队列
# ...
#
# 解读:
# - ksoftirqd/0 的 net_rx_action 占 30% → 网络收包是瓶颈
# - 解决方案: 调整网卡 RSS (Receive Side Scaling) 分散到多核
🔬 实战:用 /proc/interrupts 定位中断热点
bash
# ===== 查看中断分布 =====
cat /proc/interrupts
# 输出示例:
# CPU0 CPU1 CPU2 CPU3
# 0: 123456 0 0 0 IO-APIC 2-edge timer
# 1: 1234 0 0 0 IO-APIC 1-edge i8042
# ...
# 24: 500000 0 0 0 PCI-MSI 327680-edge eth0 ← 所有中断都在 CPU0!
# 25: 500000 0 0 0 PCI-MSI 327681-edge eth0-rx-1
# 26: 500000 0 0 0 PCI-MSI 327682-edge eth0-rx-2
# ...
#
# 问题: 所有网卡中断都在 CPU0 → CPU0 软中断 (ksoftirqd/0) 很高
# 解决: 启用 irqbalance 或手动绑定中断到不同 CPU
# ===== 启用 irqbalance =====
systemctl start irqbalance
systemctl enable irqbalance
# ===== 或手动绑定 (高级) =====
# 1. 关闭 irqbalance
systemctl stop irqbalance
# 2. 查看 eth0 的中断号
cat /proc/interrupts | grep eth0
# 3. 绑定到特定 CPU (如 eth0-rx-1 → CPU1, eth0-rx-2 → CPU2)
echo 2 > /proc/irq/25/smp_affinity # CPU1 (二进制 10 = 0x2)
echo 4 > /proc/irq/26/smp_affinity # CPU2 (二进制 100 = 0x4)
📊 CPU 执行任务相关工具速查表
| 工具 | 作用 | 关键输出 |
|---|---|---|
top -H |
查看线程级 CPU 使用 | PID/USER/PR/NI/VIRT/RES/SHR/S/%CPU/%MEM/TIME+/COMMAND |
perf top -k |
实时查看内核态热点 | Samples/Command/Shared Object/Symbol |
cat /proc/interrupts |
查看硬中断分布 | 各 CPU 的中断计数 |
cat /proc/softirqs |
查看软中断分布 | NET_RX/NET_TX/BLOCK/SCHED 等 |
cat /proc/schedstat |
查看调度统计 | 上下文切换次数 / 调度延迟 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 所有中断都在 CPU0 | 单核处理所有网卡中断,成为瓶颈 | 启用 irqbalance 或手动绑定 |
| 2 | 🟡 不理解 CFS 调度 | 以为 nice 值越小优先级越高 (实际是权重) | 用 chrt 设置实时优先级 |
| 3 | 🟡 忽略软中断 (softirq) | %si 高但不知道是哪个 softirq |
cat /proc/softirqs 看哪个 softirq 计数增长快 |
| 4 | 🟢 容器内的 /proc/interrupts 不准确 | 容器看到的是宿主机的全局中断 | 到宿主机上查看 |
🔭 扩展思考
- 如何减少上下文切换? → 用
perf stat -e context-switches评估,减少线程数或用协程 - 为什么 ksoftirqd 不能占用 100% CPU? → 内核有 softirq 时间限制,避免饿死用户进程
- 如何验证是调度问题还是中断问题? →
%si高 = 软中断;%hi高 = 硬中断;%sy高但不伴随%si/%hi= 系统调用
第19讲:业务是否需要使用透明大页:水可载舟,亦可覆舟?
🎯 核心问题
数据库服务器启用 THP (Transparent Huge Pages) 后性能反而下降? ------ THP 不是银弹,需要根据业务场景选择。
📐 THP 正反面
| 方面 | 正面 (✅) | 反面 (❌) |
|---|---|---|
| TLB 命中率 | 减少 TLB miss (大页 = 更少页表项) | 大页分配失败时会同步等待 (khugepaged) |
| 内存访问模式 | 适合大块连续内存访问 (如数据库 buffer pool) | 不适合稀疏内存访问 (如 Redis) |
| 内存碎片 | 减少页表项,降低内存管理开销 | 大页导致外部碎片,小页分配失败 |
| khugepaged | 自动合并普通页为大页 | khugepaged 扫描时会占用 CPU (可配置) |
🔬 诊断命令:查看 THP 状态
bash
# ===== 查看 THP 当前状态 =====
cat /sys/kernel/mm/transparent_hugepage/enabled
# 输出示例:
# always [madvise] never
# ↑ 当前状态
# - always: 总是尝试分配大页
# - madvise: 只有应用程序用 madvise(MADV_HUGEPAGE) 才分配
# - never: 禁用
cat /sys/kernel/mm/transparent_hugepage/defrag
# 输出示例:
# always [madvise] never
# - always: 同步等待大页分配 (会导致 latency spike!)
# - madvise: 只有应用程序建议时才做 defrag
# - never: 不做 defrag
# ===== 查看 THP 使用统计 =====
cat /proc/vmstat | grep thp
# 输出示例:
# thp_fault_alloc 12345 ← 成功分配 THP 次数
# thp_fault_fallback 500 ← 分配失败回退到 4KB 页次数
# thp_collapse_alloc 100 ← khugepaged 合并成功次数
# thp_collapse_alloc_failed 50 ← khugepaged 合并失败次数
#
# 判断:
# - thp_fault_fallback > thp_fault_alloc 的 5% → THP 反而导致性能下降
# - thp_collapse_alloc_failed 高 → 内存碎片严重
🔬 生产建议:不同场景的 THP 配置
bash
# ===== 场景 1: 数据库服务器 (如 MySQL/Oracle) =====
# → 建议: madvise (让数据库自己决定哪些内存用大页)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
# MySQL 配置 (my.cnf):
# [mysqld]
# transparent_hugepage=OFF ← MySQL 官方建议禁用 THP
# ===== 场景 2: 实时系统 (如音视频处理) =====
# → 建议: never (避免大页分配/defrag 导致的延迟抖动)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# ===== 场景 3: 科学计算/大数据 (如 Hadoop/Spark) =====
# → 建议: always (大块连续内存访问,TLB 命中率提升明显)
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag # 避免同步等待
🔬 用 perf 对比启停 THP 的 CPU 消耗
bash
# ===== 测试 1: 启用 THP =====
echo always > /sys/kernel/mm/transparent_hugepage/enabled
# 用 perf 记录 10 秒
perf record -e cycles -a -g sleep 10
# 查看报告
perf report | head -30
# ===== 测试 2: 禁用 THP =====
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 再次用 perf 记录
perf record -e cycles -a -g sleep 10 -o perfdata_nothp
# 对比报告
perf diff perfdata_nothp perfdata_thp
预期结果:
text
# 启用 THP 后:
# - cycle_tlb_propagate -> 减少 (TLB miss 减少)
# - compaction_alloc -> 增加 (内存压缩导致 CPU 开销)
# - khugepaged -> 出现 (内核线程在后台合并页)
📊 THP 相关内核参数速查表
| 参数 | 路径 | 建议值 |
|---|---|---|
| THP 启用状态 | /sys/kernel/mm/transparent_hugepage/enabled |
madvise (默认) 或 never |
| Defrag 策略 | /sys/kernel/mm/transparent_hugepage/defrag |
madvise |
| khugepaged 扫描间隔 | /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan |
4096 (默认) |
| khugepaged 休眠时间 | /sys/kernel/mm/transparent_hugepage/khugepaged/sleep_millisecs |
10000 (10 秒) |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 数据库服务器用 always | THP 导致内存碎片和 latency spike | MySQL/Oracle 官方建议用 never 或 madvise |
| 2 | 🟡 启用 defrag=always | 同步等待大页分配,导致应用线程阻塞 | 用 defrag=madvise |
| 3 | 🟡 忽略 khugepaged CPU 消耗 | khugepaged 扫描时会占用 CPU | 调大 sleep_millisecs (如 30000 = 30秒) |
| 4 | 🟢 虚拟机中启用 THP | 虚拟机的内存是宿主机分配的,THP 效果大打折扣 | 在宿主机启用 THP,虚拟机内用 never |
🔭 扩展思考
- THP 和 Hugetlb 有什么区别? → THP 是透明的 (自动分配),Hugetlb 需要手动保留大页 (
vm.nr_overcommit_hugepages) - 如何在容器中禁用 THP? → 需要宿主机禁用,或用
securityContext.sysctls(Kubernetes 1.23+) - ARM 架构的 THP 和 x86 有区别吗? → ARM 的页表层级不同 (可能 4 级 vs x86 的 5 级),TLB 命中率提升更明显
第20讲:网络吞吐高的业务是否需要开启网卡特性?
🎯 核心问题
10Gbps 网卡线速只有 3Gbps? ------ 可能是网卡特性 (GRO/LRO/TSO/RSS) 未启用。
📐 必开特性详解
特性 1:GRO (Generic Receive Offload)
text
┌──────────────────────────────────────────────────────────────┐
│ GRO: 合并小包为大数据包 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 未启用 GRO: │
│ 应用层发送: "Hello" "World" "!" │
│ TCP 层: 3 个包 (每个 6 字节) │
│ 网卡发送: 3 个包 → 3 次 DMA 中断 │
│ │
│ 启用 GRO: │
│ 应用层发送: "Hello" "World" "!" │
│ TCP 层: 3 个包 │
│ 网卡: 合并为 1 个大数据包 (18 字节) │
│ 网卡发送: 1 个包 → 1 次 DMA 中断 │
│ │
│ 效果: 减少 CPU 中断次数,提高大文件传输吞吐 │
│ │
└──────────────────────────────────────────────────────────────┘
特性 2:TSO (TCP Segmentation Offload)
text
┌──────────────────────────────────────────────────────────────┐
│ TSO: 大包分片卸载到网卡 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 未启用 TSO: │
│ TCP 层: 64KB 数据 │
│ CPU: 分片为 45 个 1448 字节的包 (64KB / 1448 ≈ 45) │
│ 网卡: 发送 45 个包 │
│ │
│ 启用 TSO: │
│ TCP 层: 64KB 数据 │
│ CPU: 不处理 (交给网卡) │
│ 网卡: 硬件分片为 45 个包 │
│ │
│ 效果: 减少 CPU 开销,提高发送吞吐 │
│ │
└──────────────────────────────────────────────────────────────┘
特性 3:RSS (Receive Side Scaling)
text
┌──────────────────────────────────────────────────────────────┐
│ RSS: 多队列负载均衡 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 未启用 RSS (单队列): │
│ 所有网络中断 → CPU0 │
│ CPU0 软中断 (ksoftirqd/0) 100% │
│ 其他 CPU 空闲 │
│ │
│ 启用 RSS (多队列): │
│ 网卡队列 0 → CPU0 │
│ 网卡队列 1 → CPU1 │
│ 网卡队列 2 → CPU2 │
│ ... │
│ 效果: 分散中断到多核,提高 PPS (Packets Per Second) │
│ │
└──────────────────────────────────────────────────────────────┘
🔬 验证命令:查看网卡特性状态
bash
# ===== 查看网卡特性状态 =====
ethtool -k eth0
# 输出示例:
# Offload parameters for eth0:
# tcp-segmentation-offload: on ← TSO 已启用
# udp-fragmentation-offload: off
# generic-receive-offload: on ← GRO 已启用
# large-receive-offload: off ← LRO (旧版 GRO)
# receive-hashing: on ← RSS 已启用
# ...
#
# ===== 如果 TSO/GRO/RSS 未启用 =====
ethtool -K eth0 tso on
ethtool -K eth0 gro on
ethtool -K eth0 receive-hashing on
# ===== 验证多队列是否生效 =====
cat /proc/interrupts | grep eth0
# 应该看到多个 eth0-rx-<queue> 中断,且分散在不同 CPU
🔬 避坑:虚拟化环境的特殊情况
bash
# ===== AWS ENA (Elastic Network Adapter) =====
# ENA 需要配合 ec2-net-utils 调优
# 1. 安装 ec2-net-utils
apt install -y ec2-net-utils
# 2. 启用 ENA 增强联网
modinfo ena | grep ^version
# 3. 验证多队列
ls /sys/class/net/eth0/queues/
# 应该看到 rx-0, rx-1, ..., tx-0, tx-1, ...
# ===== 华为云/vmware 的半虚拟化网卡 =====
# 可能不支持 TSO/GRO,需要改用 virtio-net 的 offload
ethtool -k eth0 | grep tcp-segmentation-offload
# 如果显示 "tcp-segmentation-offload: off [fixed]"
# → [fixed] 表示无法修改 (网卡驱动不支持)
# 解决: 换用支持 offload 的虚拟网卡驱动
📊 网卡特性速查表
| 特性 | 作用 | 适用场景 | 验证命令 |
|---|---|---|---|
| GRO | 合并接收的小包 | 大文件传输 | `ethtool -k eth0 |
| TSO | 发送时分片卸载到网卡 | 大文件传输 | `ethtool -k eth0 |
| RSS | 多队列负载均衡 | 高 PPS 场景 | `cat /proc/interrupts |
| LRO | 旧版 GRO (不建议用) | 旧网卡 | 用 GRO 代替 |
| Jumbo Frame | 支持 > 1500 字节的 MTU | 数据中心内网 | `ip link show eth0 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 虚拟化环境强行启用 TSO | 虚拟网卡驱动不支持,启用后无效果 | 检查 ethtool -k 输出中的 [fixed] 标记 |
| 2 | 🟡 高 PPS 场景未启用 RSS | 单核软中断 100%,其他 CPU 空闲 | ethtool -L eth0 combined 4 (设置 4 个队列) |
| 3 | 🟡 Jumbo Frame 只在一端启用 | MTU 不匹配导致丢包 | 确保交换机和服务器都设置 mtu 9000 |
| 4 | 🟢 忽略网卡队列长度 | 默认 txqueuelen 1000 可能不够 |
ifconfig eth0 txqueuelen 10000 |
🔭 扩展思考
- 如何用 DPDK 绕过内核网络栈? → DPDK 让用户态程序直接操作网卡 (Polling 模式),适合超高 PPS 场景
- AWS ENA vs Azure Accelerated Networking vs 华为云 VIRTIO → 都是 SR-IOV 或半虚拟化网卡,调优方法类似
- Kubernetes 中如何启用网卡 offload? → 需要在宿主机启用,Pod 内继承设置
第21讲:如何分析CPU利用率飙高问题?
🎯 核心问题
CPU 利用率 100%,但 top 看不到是哪个进程? ------ 可能是内核线程 (kworker/ksoftirqd) 或中断在消耗 CPU。
📐 五层定位法
text
┌──────────────────────────────────────────────────────────────┐
│ CPU 飙高五层定位法 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 第 1 层: 进程层 │
│ ├── top -H -p <PID> → 找热点线程 │
│ └── 如果找不到异常进程 → 进入第 2 层 │
│ │
│ 第 2 层: 系统调用层 │
│ ├── perf top -p <PID> → 看 syscall 热点 │
│ └── 如果 syscall 不热 → 进入第 3 层 │
│ │
│ 第 3 层: 内核函数层 │
│ ├── perf record -e cycles -a -g sleep 10 │
│ ├── perf report → 找内核函数热点 │
│ └── 如果是 softirq → 进入第 4 层 │
│ │
│ 第 4 层: 中断层 │
│ ├── cat /proc/interrupts → 定位哪个中断号增长快 │
│ ├── perf stat -e irq:irq_handler_entry → 定位中断处理函数 │
│ └── 如果是 NIC 中断 → 调整 RSS / 启用 irqbalance │
│ │
│ 第 5 层: 软中断层 │
│ ├── cat /proc/net/softnet_stat → 看哪个 CPU 的 softirq │
│ │ 处理慢 │
│ └── 用 eBPF 追踪 ksoftirqd │
│ │
└──────────────────────────────────────────────────────────────┘
🔬 实战:用 eBPF 统计每个内核函数的 CPU 时间
bash
bpftrace -e '
kprobe:do_syscall_64 {
@func[comm] = count();
}
interval:5s {
printf("=== Top 10 Kernel Functions by CPU Time ===\n");
print(@func, 10);
clear(@func);
}'
输出示例:
text
=== Top 10 Kernel Functions by CPU Time ===
nginx 12345
ksoftirqd/0 5000 ← ksoftirqd/0 占用 CPU!
kworker/0:1 3000
...
🔬 实战:定位 ksoftirqd/0 占比高的原因
bash
# ===== 步骤 1: 确认是 softirq 导致的 =====
top
# 输出示例:
# %Cpu0 : 5.0 us, 10.0 sy, 0.0 ni, 80.0 id, 0.0 wa, 0.0 hi, 5.0 si
# ↑ ↑
# 用户态 CPU 软中断 CPU
# - 如果 %si > 10% → 软中断是瓶颈
# ===== 步骤 2: 查看哪个 softirq 类型最热 =====
cat /proc/softirqs
# 输出示例:
# CPU0 CPU1 CPU2 CPU3
# NET_RX: 50000000 50 50 50 ← NET_RX 很高!
# NET_TX: 5000 5000 5000 5000
# BLOCK: 0 0 0 0
# ...
#
# 解读: NET_RX (网络接收) softirq 在 CPU0 上很高 → 网卡中断都绑定在 CPU0
# ===== 步骤 3: 调整网卡中断绑定 =====
# (参考第 18 讲的方法)
📊 CPU 利用率飙高根因速查表
| 现象 | 可能原因 | 验证命令 | 解决方案 |
|---|---|---|---|
%si 高 |
软中断 (如 NET_RX) | cat /proc/softirqs |
调整网卡 RSS / 启用 irqbalance |
%hi 高 |
硬中断 | cat /proc/interrupts |
调整中断绑定 |
%sy 高但不伴 %si/%hi |
系统调用频繁 | perf top -e syscalls:* |
优化应用系统调用次数 |
kworker 高 |
工作队列 (如文件系统事件) | perf report 看 worker_thread |
检查 inotify/fanotify 使用 |
| 所有 CPU 都高但无异常进程 | 可能是 rcu_sched (RCU 回调) |
perf report 看 rcu_sched |
减少内核对象分配 (如 TCP 连接) |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 ksoftirqd 100% 但不知道哪个 softirq | 盲目重启 | cat /proc/softirqs 先定位 |
| 2 | 🟡 忽略 RCU 回调 | rcu_sched 内核线程 CPU 高 |
减少 call_rcu() 调用 (如减少 TCP 连接数) |
| 3 | 🟡 容器内的 top 不准确 | 容器看到的是宿主机 CPU 使用 | 到宿主机用 top 再确认 |
| 4 | 🟢 perf.data 文件太大 | 记录 60 秒可能生成 GB 级文件 | 用 perf record -a -g -F 99 sleep 10 (99Hz 采样) |
🔭 扩展思考
- 如何减少 softirq 对业务的影响? → 用
isolcpus=内核启动参数隔离 CPU,或设置sysctl -w net.core.somaxconn=4096减少软中断次数 - 为什么 kworker 会导致 CPU 飙高? →
inotify监听大量文件,或writeback回写大量脏页 - 如何在 Kubernetes 中限制 Pod 的 CPU 使用? → 用
resources.limits.cpu+cpu manager policy=static
🔹 加餐 & 结束篇(4讲|工具链与总结)
第22讲:我是如何使用tracepoint来分析内核Bug的?
🎯 核心问题
kprobe 经常因为内核版本升级而失效? ------ 用 tracepoint (内核静态追踪点) 更稳定。
📐 tracepoint vs kprobe
text
┌──────────────────────────────────────────────────────────────┐
│ tracepoint vs kprobe │
├──────────────────────────────────────────────────────────────┤
│ │
│ tracepoint: │
│ ├── 内核代码中预先定义的追踪点 │
│ ├── 稳定: 不会随内核版本变化 (ABI 稳定) │
│ ├── 低开销: 用 jump label 实现 (几乎无开销) │
│ └── 查看: perf list | grep tracepoint │
│ │
│ kprobe: │
│ ├── 动态插入到任意内核函数入口/出口 │
│ ├── 灵活: 可以追踪任何内核函数 │
│ ├── 不稳定: 内核函数名变化就会失效 │
│ └── 开销比 tracepoint 高 │
│ │
│ 选择建议: │
│ - 如果能用 tracepoint → 优先用 tracepoint │
│ - 如果 tracepoint 没有覆盖 → 用 kprobe │
│ │
└──────────────────────────────────────────────────────────────┘
🔬 实战案例:定位 ext4 文件系统元数据损坏问题
bash
# ===== 步骤 1: 启用 ext4 tracepoint =====
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_sync_file_enter/enable
# ===== 步骤 2: 抓取日志 =====
cat /sys/kernel/debug/tracing/trace > ext4_trace.log &
# 让业务跑 60 秒
sleep 60
kill %1
# ===== 步骤 3: 分析日志 =====
cat ext4_trace.log | grep -E "ext4_sync_file_enter|ext4_sync_file_exit"
# 输出示例:
# ext4_sync_file_enter: dev 8,2 ino 123456 parent 789 tenacity 1
# ext4_sync_file_exit: dev 8,2 ino 123456 parent 789 ret 0
# ext4_sync_file_enter: dev 8,2 ino 123456 parent 789 tenacity 1
# ext4_sync_file_exit: dev 8,2 ino 123456 parent 789 ret -5 ← 错误!
#
# 解读:
# - ret -5 = -EIO (I/O 错误)
# - 可能是磁盘坏道或文件系统元数据损坏
🔬 推荐工具链
bash
# ===== 工具 1: libbpf + bpftool =====
# 用于开发自定义 eBPF 探针
apt install -y libbpf-dev bpftool
# 查看所有可用的 tracepoint
perf list | grep tracepoint | wc -l
# 输出: 1500+ (现代内核有 1500+ 个 tracepoint)
# ===== 工具 2: perf list =====
# 查看所有可用 tracepoint
perf list | grep tracepoint | grep ext4
# 输出:
# ext4:ext4_sync_file_enter
# ext4:ext4_sync_file_exit
# ext4:ext4_writepages
# ...
# ===== 工具 3: bpftrace =====
# 用高级语言写 eBPF 程序
bpftrace -e 'tracepoint:ext4:ext4_sync_file_enter {@sync[comm] = count();}'
📊 tracepoint 速查表 (常用)
| 子系统 | tracepoint | 用途 |
|---|---|---|
| ext4 | ext4_sync_file_enter |
追踪 fsync() 调用 |
| ext4 | ext4_writepages |
追踪写页 |
| tcp | tcp_send_reset |
追踪 TCP RST 发送 |
| tcp | tcp_retransmit_skb |
追踪 TCP 重传 |
| sched | sched_switch |
追踪进程切换 |
| mm | mm_page_alloc |
追踪页分配 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 盲目用 kprobe | 内核升级后探针失效 | 优先用 tracepoint |
| 2 | 🟡 tracepoint 输出太多 | 全量记录导致磁盘写满 | 用 eBPF map 聚合后再输出 |
| 3 | 🟡 忽略 tracepoint 的 ABI 稳定性 | 以为 tracepoint 参数永远不变 | 查看内核文档 Documentation/trace/events.rst |
| 4 | 🟢 在生产环境开启太多 tracepoint | 开销累积可能影响性能 | 只开启必要的 tracepoint |
🔭 扩展思考
- 如何知道某个内核函数是否有对应的 tracepoint? →
perf list | grep <函数名>或看include/trace/events/*.h - 如何自定义 tracepoint? → 在内核模块中用
TRACE_EVENT()宏定义 - BPF CO-RE (Compile Once -- Run Everywhere) 如何改进 tracepoint 使用? → 用 BTF (BPF Type Format) 自动适配内核结构变化
第23讲:第一次看内核代码,我也很懵逼
🎯 核心问题
面对庞大的 Linux 内核代码 (3000 万行+),如何高效阅读? ------ 从简单模块入手,用工具建立索引。
📐 新手友好路径
text
┌──────────────────────────────────────────────────────────────┐
│ 内核代码阅读路径 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 第 1 阶段: 从简单模块入手 │
│ ├── drivers/char/mem.c (最简单的字符设备驱动) │
│ ├── fs/proc/root.c (proc 文件系统) │
│ └── mm/filemap.c (Page Cache 核心代码) │
│ │
│ 第 2 阶段: 用 cscope/ctags 建立跳转索引 │
│ ├── cscope -R -b -k (递归生成索引) │
│ ├── 在 vim 中用 Ctrl+] 跳转 │
│ └── 在 VSCode 中用 LSP (c/cpp 插件) │
│ │
│ 第 3 阶段: 结合 printk + dmesg 调试 │
│ ├── 在关键函数加 printk() │
│ ├── 重新编译内核模块 │
│ └── dmesg -w 查看输出 │
│ │
│ 第 4 阶段: 用 QEMU + GDB 远程调试 │
│ ├── 启动 QEMU 并等待 GDB 连接 │
│ ├── gdb vmlinux 连接 QEMU │
│ └── 在关键函数设断点 │
│ │
│ 第 5 阶段: 关注 MAINTAINERS 文件 │
│ ├── 找到模块负责人 │
│ ├── 订阅 mailing list │
│ └── 提交 patch 前先读 mailing list 讨论 │
│ │
└──────────────────────────────────────────────────────────────┘
🔬 技巧 1:用 CONFIG_DEBUG_INFO 编译带调试信息的内核
bash
# ===== 步骤 1: 安装编译依赖 =====
apt install -y build-essential libncurses-dev bison flex libssl-dev
# ===== 步骤 2: 下载内核源码 =====
apt source linux-image-$(uname -r)
# ===== 步骤 3: 配置内核 (启用调试信息) =====
cd linux-*/
make menuconfig
# 进入:
# Kernel hacking --->
# Compile-time checks and compiler options --->
# [*] Compile the kernel with debug info
# [*] Generate BTF typeinfo (for BPF)
# 或直接改 .config
scripts/config --enable CONFIG_DEBUG_INFO
scripts/config --enable CONFIG_DEBUG_INFO_BTF
# ===== 步骤 4: 编译内核 (会很耗时!) =====
make -j$(nproc)
🔬 技巧 2:用 QEMU + GDB 远程调试
bash
# ===== 步骤 1: 启动 QEMU 并等待 GDB =====
qemu-system-x86_64 \
-kernel bzImage \
-initrd initramfs.img \
-append "nokaslr" \ # 禁用 KASLR (否则 GDB 断点地址不对)
-s -S # -s: 在端口 1234 等待 GDB; -S: 启动后暂停
# ===== 步骤 2: 在另一个终端启动 GDB =====
gdb vmlinux
(gdb) target remote localhost:1234
(gdb) hbreak start_kernel # 在 start_kernel 设硬件断点
(gdb) continue
# ===== 步骤 3: 调试 =====
(gdb) backtrace # 查看调用栈
(gdb) info locals # 查看局部变量
(gdb) print variable_name # 打印变量值
📊 内核代码阅读工具速查表
| 工具 | 用途 | 优点 |
|---|---|---|
| cscope | 代码跳转 | 轻量,终端可用 |
| ctags | 代码跳转 | 支持多种语言 |
| VSCode + C/C++ 插件 | 图形化代码浏览 | 易用,LSP 支持 |
| elixir.bootlin.com | 在线内核代码浏览 | 无需本地环境 |
| lxr.melnichenko.msu.ru | 在线内核代码交叉引用 | 支持多个内核版本 |
⚠️ 避坑清单
| 序号 | 坑 | 描述 | 解决 |
|---|---|---|---|
| 1 | 🔴 试图一次性读懂所有代码 | 内核代码太庞大,容易放弃 | 从具体子系统入手 (如 mm/ 或 net/) |
| 2 | 🟡 不用调试器 | 只看代码不理解执行流程 | 用 QEMU + GDB 单步跟踪 |
| 3 | 🟡 忽略内核文档 | Documentation/ 目录有详细说明 |
先读文档再读代码 |
| 4 | 🟢 在错误邮件列表问新手问题 | 内核邮件列表只讨论开发问题 | 先到 Stack Overflow 或 LKML archive 搜索 |
🔭 扩展思考
- 如何参与 Linux 内核社区? → 订阅
lkml.org,从简单 bug fix 开始提交 patch - 如何追踪最新内核开发动态? → 关注
lwn.net(Linux Weekly News) - AI 能帮助阅读内核代码吗? → 可以用 LLM 解释函数作用,但寄存器级别的操作 AI 可能理解错误
第24讲:来领奖啦!你填写毕业问卷了吗?
🎯 核心目标
增强社区感与成就感,收集读者反馈为后续专题铺垫。
(本文为互动环节,无技术内容,主要是问卷调查和抽奖活动)
第25讲:毕业问卷获奖用户名单
🎯 核心目标
公布获奖用户,进一步增强社区活跃度。
(本文为名单公布,无技术内容)
第26讲:结课测试|这些Linux内核技术实战技能你都掌握了吗?
🎯 综合挑战题
某数据库节点 CPU 100%,top 显示 ksoftirqd/0 占比高。请设计完整排查与解决路径。
📐 标准答案 (分步得分)
text
┌──────────────────────────────────────────────────────────────┐
│ 综合挑战题标准答案 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 步骤 1: 定位中断来源 │
│ ├── cat /proc/interrupts | grep -E "CPU0|eth0" │
│ ├── 如果发现 eth0 的所有中断都在 CPU0 │
│ └── 结论: 网卡中断未分散到多核 (RSS 未生效) │
│ │
│ 得分点: 正确使用 /proc/interrupts 定位问题 │
│ │
│ 步骤 2: 验证网卡多队列是否启用 │
│ ├── ls /sys/class/net/eth0/queues/ │
│ ├── 如果只有 rx-0/tx-0 → 单队列 │
│ └── 解决: ethtool -L eth0 combined 4 │
│ │
│ 得分点: 知道用 ethtool -L 调整队列数 │
│ │
│ 步骤 3: 调整中断绑定 (更精细的控制) │
│ ├── systemctl stop irqbalance │
│ ├── echo 2 > /proc/irq/<eth0-rx-1>/smp_affinity │
│ ├── echo 4 > /proc/irq/<eth0-rx-2>/smp_affinity │
│ └── 验证: cat /proc/interrupts | grep eth0 │
│ │
│ 得分点: 手动绑定中断到不同 CPU │
│ │
│ 步骤 4: 验证网卡特性是否启用 │
│ ├── ethtool -k eth0 | grep -E "gro|tso|receive" │
│ ├── 如果 GRO/TSO 未启用 → ethtool -K eth0 gro on │
│ └── 验证: perf top 看 ksoftirqd/0 占比是否下降 │
│ │
│ 得分点: 知道 GRO/TSO 能减少软中断次数 │
│ │
│ 步骤 5: 如果是云环境,检查是否支持增强联网 │
│ ├── AWS: ethtool -i eth0 (看是不是 ENA) │
│ ├── 华为云: 检查是否安装 huawei-cloud-utils │
│ └── 解决: 安装对应驱动并启用多队列 │
│ │
│ 得分点: 知道云环境的特殊情况 │
│ │
└──────────────────────────────────────────────────────────────┘
📊 自测题 (选择题)
1. 以下哪个工具可以稳定追踪内核函数,且不受内核版本升级影响?
A. kprobe
B. tracepoint
C. systemtap
D. LTTng
✅ 正确答案:B --- tracepoint 是内核代码中预先定义的追踪点,ABI 稳定
2. THP (Transparent Huge Pages) 对数据库服务的建议配置是?
A. always
B. madvise
C. never
D. 视情况而定
✅ 正确答案:D --- MySQL 官方建议 never,但 PostgreSQL 某些场景可以用 madvise
3. 网卡特性 GRO 的主要作用是?
A. 发送时分片卸载到网卡
B. 合并接收的小包为大数据包
C. 多队列负载均衡
D. 支持 Jumbo Frame
✅ 正确答案:B --- GRO (Generic Receive Offload) 合并小包
📦 博客配套资源包(每讲提供)
| 资源类型 | 示例内容 |
|---|---|
| eBPF 脚本 | pagecache_hit.bpf, tcp_retrans.bpf |
| perf 命令集 | perf-cpu-high.sh, perf-tcp-analysis.sh |
| sysctl 模板 | production-network.conf, db-server-tune.conf |
| 内核模块 demo | hello_kmod.c, shmem_monitor.c |
| 故障复现 Dockerfile | 模拟 Page Cache 飙高 / TCP 重传场景 |
✅ 读者收获:学完本系列后,您将能:
text
✅ 独立诊断 90% 的线上 Linux 性能问题
✅ 用 eBPF/perf 替代"猜谜式"排查
✅ 理解内核行为背后的资源竞争本质
✅ 在架构设计阶段就规避常见内核陷阱
📌 文档版本 : v1.0
📌 创建日期 : 2026-06-06
📌 实验集群 : ecs-ee63 (华为云香港, 4×c6.large.2)
📌 总字数: ~12000 行, ~380KB
"内核不是黑盒 ------ 当你能用 eBPF 看透它的行为时,你就真正掌握了 Linux 的性能之道。"