《Linux内核技术实战:从Page Cache到CPU调度的深度解构》博客大纲(26讲精编版)

🐧 《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 做深度诊断

🔭 扩展思考

  1. 如何系统学习 Linux 内核? → 从《Linux 内核设计与实现》入手,配合 lxr.melnichenko.msu.ru 在线代码浏览
  2. eBPF 会取代 ftrace 吗? → 不会,ftrace 更低开销,适合生产环境长期开启
  3. 容器化后内核调优还重要吗? → 更重要!容器共享内核,一个容器的错误调优影响所有容器

🔹 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_ratiovm.dirty_background_ratio
4 🟢 容器环境下误读 /proc/meminfo 容器内的 /proc/meminfo 显示宿主机内存 /sys/fs/cgroup/memory/memory.stat

🔭 扩展思考

  1. Page Cache 和 buffer cache 有什么区别? → Linux 2.4+ 已统一,buffer cache 是块设备层的缓存,Page Cache 是页缓存,现在 buffer cache 是 Page Cache 的一部分
  2. 如何强制回收 Page Cache?echo 1 > /proc/sys/vm/drop_caches (仅测试环境!)
  3. 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 链表中的页才能被回收

🔭 扩展思考

  1. 为什么 Page Cache 不回收? → 可能是 mlock() 锁住了页,或者页正在回写 (Writeback)
  2. 如何查看某个文件是否被缓存?vmtouch -v /path/to/filepcstat /path/to/file
  3. 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)

🔭 扩展思考

  1. 为什么 kswapd 不能彻底解决内存压力? → kswapd 是后台线程,回收速度跟不上分配速度时,会触发 Direct Reclaim
  2. 如何监控 Page Cache 回收频率?sar -B 1 查看 pgscank/pgscand
  3. 容器环境下如何限制 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 限制

🔭 扩展思考

  1. 为什么数据库都自己管理缓存 (如 InnoDB Buffer Pool)? → 避免 Page Cache 的二次缓存,减少内存浪费
  2. 如何查看某个进程占用的 Page Cache 大小?cat /proc/PID/smaps | grep -i "^Shared_Clean\|Shared_Dirty"
  3. 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 回收) topwa
3 🟡 忽略容器环境的内存隔离 容器内的 free -h 显示宿主机内存 读 cgroup 内存统计
4 🟢 过度依赖自动化监控 监控面板可能漏掉瞬时的 Page Cache 回收风暴 用 eBPF 做深度追踪

🔭 扩展思考

  1. 如何区分 Page Cache 问题和 Swap 问题? → Swap 问题看 vmstat si/so,Page Cache 问题看 sar -Bpgsteal
  2. 如何在生产环境长期监控 Page Cache 命中率? → 用 bcc-tools/cachestatpcp (Performance Co-Pilot)
  3. 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,但计入系统内存 topRSsh 列或 `cat /proc/PID/status
2 🟡 忽略 slab 缓存增长 slab 缓存泄漏不会体现在进程内存中 slabtop 定期监控
3 🟡 容器环境下误判内存使用 容器内的 slabtop 显示宿主机数据 在宿主机查看
4 🟢 过早优化 未发现真实泄漏就盲目调优 先用 valgrind 确认泄漏点

🔭 扩展思考

  1. 如何区分正常的内存增长和内存泄漏? → 正常增长会稳定在某个值,泄漏会持续增长直到 OOM
  2. 为什么 Java 应用的 RSS 比 Xmx 大很多? → 除了堆内存,还有 metaspace、CodeCache、DirectByteBuffer 等
  3. 如何监控容器内的内存泄漏? → 用 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 文件大小

🔭 扩展思考

  1. 如何在 Kubernetes 中防御内存泄漏? → 用 resources.limits.memory + livenessProbe + OOMKiller
  2. Go 语言的 GC 能避免内存泄漏吗? → 不能,Go 的 GC 只回收不再引用的对象,如果对象被全局变量引用,就不会被回收
  3. 如何区分内存泄漏和缓存? → 缓存会主动淘汰旧数据,泄漏只会持续增长

第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

🔭 扩展思考

  1. 为什么 Shmem 不计入 RSS? → 因为 Shmem 可以被多个进程共享,如果计入 RSS,会导致所有共享进程的 RSS 之和 > 物理内存
  2. 如何查看哪个进程占用了 Shmem?ls -l /dev/shm/ 看文件名,或者 ipcs -p 看关联的 PID
  3. 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 内核模块 模块卸载时如有泄漏,内存不会恢复 需要重启系统才能恢复

🔭 扩展思考

  1. 如何区分正常 Slab 增长和泄漏? → 正常增长会稳定,泄漏会持续增长直到 OOM
  2. kasan 和 kmemleak 有什么区别? → kasan 检测内存访问越界,kmemleak 检测内存分配后未释放
  3. 如何在生产环境监控内核内存使用? → 用 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

🔭 扩展思考

  1. 如何区分"内存泄漏"和"内存碎片"? → 用 cat /proc/buddyinfo 查看内存碎片情况,碎片会导致"有内存但分配失败"
  2. 容器内的内存泄漏如何排查? → 在宿主机查看容器的 cgroup 内存统计,或在容器内用 nsenter 执行诊断命令
  3. 如何在 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

🔭 扩展思考

  1. 为什么 tcp_tw_recycle 被废弃了? → 在 NAT 环境下,多个客户端可能共用同一个源 IP,时间戳检查会导致连接被错误拒绝
  2. 如何优化大量 TIME_WAIT 连接? → 启用 tcp_tw_reuse (仅客户端) + 调整 ip_local_port_range
  3. 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

🔭 扩展思考

  1. 如何用 ss 查看当前 TCP 窗口大小?ss -ti 输出中的 cubic wscale:X rto:Y rtt:P/Q 字段
  2. TCP BBR 和传统 CUBIC 有什么区别? → BBR 基于带宽和延迟模型,不依赖丢包信号,在高丢包网络下性能更好
  3. 如何在应用中设置 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

🔭 扩展思考

  1. 如何在应用中选择拥塞控制算法? → 无法在应用层选择,这是系统级配置;但可以选择用 QUIC (HTTP/3) 绕过 TCP 拥塞控制
  2. BBR 和 QUIC 的拥塞控制有什么区别? → BBR 是 TCP 层的,QUIC 是 UDP 层的;QUIC 的拥塞控制更接近 BBR,但实现更灵活
  3. 如何在 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 -tiretrans
服务器处理慢 连接建立后很久才响应 curl -w "%{time_starttransfer}"
带宽打满 大流量时延迟 ↑ sar -n DEV 1rxkB/s/txkB/s

⚠️ 避坑清单

序号 描述 解决
1 🔴 只看 ping 延迟 ICMP 可能被优先处理,TCP 延迟更高 curl -whttping 测 TCP 延迟
2 🟡 忽略 TCP 重传 1% 重传率可能让延迟翻倍 `netstat -s
3 🟡 mtr 运行时间太短 短暂测试可能看不到偶发丢包 至少运行 5 分钟 (-c 300)
4 🟢 tcpdump 抓包影响性能 在高 PPS 服务器上抓包可能丢包 tcpdump -s 96 只抓包头

🔭 扩展思考

  1. 如何监控生产环境的 TCP RTT? → 用 ss -ti 或 Prometheus node_exporter 的 node_netstat_Tcp_RttVaries
  2. QUIC 如何改进 RTT? → QUIC 1-RTT 握手 (甚至 0-RTT 重连),且重传不阻塞其他流
  3. 如何在 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) 在隧道端点抓包,或解密后分析

🔭 扩展思考

  1. 如何在生产环境实时检测重传? → 用 eBPF 追踪 tcp_retransmit_skb 内核函数
  2. Kubernetes 中如何分析 Pod 之间的 TCP 重传? → 在 Node 上抓 veth* 接口的包
  3. 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 -tinodelay 启用 TCP_NODELAY
断连频繁 (Frequent Disconnect) keepalive 未启用 cat /proc/sys/net/ipv4/tcp_keepalive_time 启用 TCP Keepalive
吞吐上不去 (Low Throughput) 缓冲区太小 ss -tircv_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 可能看不到真实网卡 到宿主机上查物理网卡

🔭 扩展思考

  1. 如何在 Kubernetes 中排查 TCP 问题? → 在 Pod 中用 ss -ti,或在宿主机上用 nsenter -t <PID> -n ss -ti
  2. TCP BBR 拥塞控制算法能解决重传问题吗? → BBR 能提高吞吐,但不解决网络丢包导致的重传
  3. 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 不准确 容器看到的是宿主机的全局中断 到宿主机上查看

🔭 扩展思考

  1. 如何减少上下文切换? → 用 perf stat -e context-switches 评估,减少线程数或用协程
  2. 为什么 ksoftirqd 不能占用 100% CPU? → 内核有 softirq 时间限制,避免饿死用户进程
  3. 如何验证是调度问题还是中断问题?%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 官方建议用 nevermadvise
2 🟡 启用 defrag=always 同步等待大页分配,导致应用线程阻塞 defrag=madvise
3 🟡 忽略 khugepaged CPU 消耗 khugepaged 扫描时会占用 CPU 调大 sleep_millisecs (如 30000 = 30秒)
4 🟢 虚拟机中启用 THP 虚拟机的内存是宿主机分配的,THP 效果大打折扣 在宿主机启用 THP,虚拟机内用 never

🔭 扩展思考

  1. THP 和 Hugetlb 有什么区别? → THP 是透明的 (自动分配),Hugetlb 需要手动保留大页 (vm.nr_overcommit_hugepages)
  2. 如何在容器中禁用 THP? → 需要宿主机禁用,或用 securityContext.sysctls (Kubernetes 1.23+)
  3. 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

🔭 扩展思考

  1. 如何用 DPDK 绕过内核网络栈? → DPDK 让用户态程序直接操作网卡 (Polling 模式),适合超高 PPS 场景
  2. AWS ENA vs Azure Accelerated Networking vs 华为云 VIRTIO → 都是 SR-IOV 或半虚拟化网卡,调优方法类似
  3. 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 reportworker_thread 检查 inotify/fanotify 使用
所有 CPU 都高但无异常进程 可能是 rcu_sched (RCU 回调) perf reportrcu_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 采样)

🔭 扩展思考

  1. 如何减少 softirq 对业务的影响? → 用 isolcpus= 内核启动参数隔离 CPU,或设置 sysctl -w net.core.somaxconn=4096 减少软中断次数
  2. 为什么 kworker 会导致 CPU 飙高?inotify 监听大量文件,或 writeback 回写大量脏页
  3. 如何在 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

🔭 扩展思考

  1. 如何知道某个内核函数是否有对应的 tracepoint?perf list | grep <函数名> 或看 include/trace/events/*.h
  2. 如何自定义 tracepoint? → 在内核模块中用 TRACE_EVENT() 宏定义
  3. 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 搜索

🔭 扩展思考

  1. 如何参与 Linux 内核社区? → 订阅 lkml.org,从简单 bug fix 开始提交 patch
  2. 如何追踪最新内核开发动态? → 关注 lwn.net (Linux Weekly News)
  3. 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 的性能之道。"


相关推荐
知无不研2 小时前
对套接字的深入理解
linux·服务器·网络·c++·socket·网络套接字
wuminyu4 小时前
Java锁机制之Java对象重量级锁源码剖析
java·linux·c语言·jvm·c++
deadbird5 小时前
Xbox 无线适配器 Linux 设置指南
linux
wait a minutes5 小时前
Ubuntu 升级后 NVIDIA 驱动修复指南
linux·运维·ubuntu
bush46 小时前
嵌入式linux学习记录十二,mmap
java·linux·学习
似水এ᭄往昔7 小时前
【Linux系统编程】--进程概念
linux·运维·服务器
Dxy12393102167 小时前
Linux 如何关闭关不掉的进程
linux·运维·chrome
小徐敲java7 小时前
Linux读取串口实时数据
linux·运维·服务器
keyipatience8 小时前
25.Linux静态动态库全解析
linux·运维·服务器