深入 Linux 性能调试 ------ BPF 与 BCC 工具实战指南
副标题 :别再只盯着 top 了------用 eBPF 这把「瑞士军刀」,透视你的 Linux 系统
目标读者 :Linux 运维工程师 / 后端开发 / 系统程序员 / 性能调优爱好者
实验环境:ecs-2aa6-0001,Ubuntu 24.04.4 LTS,内核 6.8.0-106-generic,BCC 0.29.1
目录
- [引言:为什么你需要 BPF?](#引言:为什么你需要 BPF?)
- [第一章:环境准备与基石------BCC 工具集](#第一章:环境准备与基石——BCC 工具集)
- 1.1 BCC 是什么?
- 1.2 动手安装 BCC
- 1.3 BCC 核心工具速览
- [第二章:CPU 性能瓶颈排查实战](#第二章:CPU 性能瓶颈排查实战)
- 2.1 案例:定位高 CPU 消耗的函数(
profile+ 火焰图) - 2.2 案例:追踪新进程的启动(
execsnoop)
- [第三章:文件系统与磁盘 I/O 调试](#第三章:文件系统与磁盘 I/O 调试)
- 3.1 案例:谁在疯狂读写文件?(
opensnoop/filetop) - 3.2 案例:深入磁盘 I/O 延迟分析(
biolatency/biosnoop)
- 4.1 案例:监控实时网络流量(
tcptop) - 4.2 案例:追踪连接的建立与关闭(
tcpconnect/tcpaccept)
- [第五章:进阶探索------BPF 与应用程序、安全](#第五章:进阶探索——BPF 与应用程序、安全)
- 结语:从工具使用者到问题终结者
引言:为什么你需要 BPF?
一个真实的生产故障场景
凌晨 2:15,告警短信把你从睡梦中炸醒:「核心业务服务器 CPU 使用率飙升至 98%,响应超时率 40%!」
你火速登录服务器,熟练地敲下 top:
%Cpu(s): 97.8 us, 1.2 sy, 0.0 id, 0.0 wa, ...
PID USER %CPU COMMAND
12345 app 98.7 java
看到了------Java 进程占满 CPU。但问题来了:JVM 里有上百个线程,成百上千个方法,到底是哪个方法 在烧 CPU?top 只能告诉你「哪个进程」,无法告诉你「进程里的哪个函数」。
你转而用 perf top,确实能看到函数名,但采样结果转瞬即逝,既不能回放也无法深入分析。你试了 strace,结果系统调用日志像洪水一样涌来,真正的热点反而淹没其中------而且 strace 会让被追踪的进程性能下降 30%~50%,生产环境根本不敢用。
这就是传统工具的局限:
| 工具 | 能做什么 | 不能做什么 |
|---|---|---|
top / htop |
进程级别 CPU/内存概览 | 无法看到进程内部的函数调用 |
strace |
追踪系统调用 | 开销巨大(~30%+),不能用于生产 |
perf |
硬件性能计数器采样 | 后处理复杂,不能做动态过滤 |
vmstat / iostat |
系统级 I/O 统计 | 无法归因到具体进程/文件 |
tcpdump |
抓包分析 | 无法关联到进程/连接状态 |
BPF 的革命性
eBPF(extended Berkeley Packet Filter) 改变了这一切。它允许你在 Linux 内核中运行沙箱化的程序,无需修改内核源码、无需重启、开销极低(通常 1%~3%)。
┌──────────────────────────────────────────────────────────┐
│ 传统可观测性工具栈 │
│ │
│ top / ps ──→ 只看到进程级别 │
│ strace ──→ 开销 30%+,生产环境不敢用 │
│ perf ──→ 数据量大,难做动态过滤 │
│ tcpdump ──→ 抓包可以,但不知道是哪个进程发出的 │
└──────────────────────────────────────────────────────────┘
↓ BPF 革命
┌──────────────────────────────────────────────────────────┐
│ eBPF 可观测性 │
│ │
│ • 内核级探针(kprobe / tracepoint) → 零侵入追踪 │
│ • 动态插桩(USDT) → 应用层埋点 │
│ • 安全沙箱(Verifier) → 内核不会崩溃 │
│ • < 3% 性能开销 → 生产环境可用 │
│ • 即时加载,无需重启 → 在线诊断 │
└──────────────────────────────────────────────────────────┘
一句话总结:BPF 让你能在内核中安全地执行自定义代码,观测系统行为的方方面面------从 CPU 函数级热点,到磁盘 I/O 微秒级延迟,再到网络连接生命周期------而不需要修改一行内核代码。
本文目标
带你从零开始,在真实服务器上搭建 BCC 环境,通过 6 个可复现的实战案例,掌握 BPF/BCC 的核心调试技能。每一节都包含真实的命令输出和解读,你可以边读边在自己服务器上操作。
第一章:环境准备与基石------BCC 工具集
1.1 BCC 是什么?
BCC(BPF Compiler Collection) 是 iovisor 社区开发的一套工具集,让你可以用 Python / Lua 编写 BPF 程序的前端,用 C 语言编写运行在内核中的 BPF 后端。
┌───────────────────────────────────────────┐
│ BCC 工作架构 │
│ │
│ Python 前端 ──→ LLVM/Clang 编译 ──→ BPF 字节码 │
│ ↕ │
│ BPF Maps / Perf Buffer ── 内核态 ←──→ 用户态 │
│ │
│ /usr/sbin/*-bpfcc ←── 90+ 预构建工具 │
└───────────────────────────────────────────┘
BCC 自带 90+ 个生产就绪的工具 ,覆盖了 CPU、内存、磁盘 I/O、网络、安全等几乎所有系统领域。你不需要会写 BPF 程序就能使用它们------就像你用 top 不需要懂 Linux 调度器一样。
1.2 动手安装 BCC
实验环境 :我们的实操基于华为云 ecs-2aa6-0001 节点:
bash
# 系统信息
root@build-01:~# uname -r
6.8.0-106-generic
root@build-01:~# cat /etc/os-release | head -2
PRETTY_NAME="Ubuntu 24.04.4 LTS"
VERSION_ID="24.04"
关键要求 :Linux 内核版本 ≥ 4.9(推荐 5.x+ 以获得完整功能)。内核版本通过
uname -r查看。Ubuntu 24.04 默认内核 6.8 完全满足。
安装步骤(Ubuntu / Debian):
bash
# 一步到位安装
apt-get update
apt-get install -y bpfcc-tools python3-bpfcc bpftool linux-headers-$(uname -r)
# 安装火焰图生成工具(可选,用于 CPU 可视化分析)
git clone --depth=1 https://github.com/brendangregg/FlameGraph.git /opt/FlameGraph
在实验服务器上验证安装状态:
bash
root@build-01:~# dpkg -l | grep -i bpf
ii bpfcc-tools 0.29.1+ds-1ubuntu7 all tools for BPF Compiler Collection (BCC)
ii python3-bpfcc 0.29.1+ds-1ubuntu7 all Python 3 wrappers for BPF Compiler Collection
CentOS / RHEL 用户:
bash
yum install -y bcc-tools python3-bcc
验证安装------运行几个简单命令确认一切正常:
bash
# 测试:追踪 exec() 系统调用(相当于实时看到所有新进程)
/usr/sbin/execsnoop-bpfcc
# 正常输出应显示 "Tracing exec() syscalls..."
# 测试:查看块设备 I/O 延迟分布
/usr/sbin/biolatency-bpfcc -m 1 1
# 正常输出应显示毫秒级 I/O 直方图
⚠️ 踩坑提示 :Ubuntu 24.04 中 BCC 工具安装在
/usr/sbin/下,命令名带-bpfcc后缀(如execsnoop-bpfcc)。部分文档中写的execsnoop在 Ubuntu 上会报command not found。
1.3 BCC 核心工具速览
BCC 提供 90+ 工具,按领域分类,以下是你最可能用到的 15 个:
| 分类 | 工具 | 作用 | 一句话说明 |
|---|---|---|---|
| CPU | profile |
CPU 函数级采样 | 「哪个函数在烧 CPU?」 |
execsnoop |
追踪新进程启动 | 「谁在偷偷跑命令?」 | |
runqlat |
调度器排队延迟 | 「CPU 够不够用?」 | |
cpudist |
CPU on-CPU 时间分布 | ||
| 内存 | memleak |
内存泄露检测 | 「哪个 alloc 没 free?」 |
cachestat |
页缓存命中率 | 「内存够不够做缓存?」 | |
| 磁盘 I/O | biolatency |
磁盘 I/O 延迟直方图 | 「磁盘慢在哪一段?」 |
biosnoop |
逐 I/O 请求追踪 | 「每个 I/O 的详细信息」 | |
biotop |
按进程排 I/O 占用 | 「谁在刷盘?」 | |
opensnoop |
文件打开追踪 | 「哪个进程打开了哪个文件?」 | |
filetop |
文件级 I/O 排序 | 「哪个文件最「热」?」 | |
| 网络 | tcptop |
TCP 流量进程排行 | 「谁在吃带宽?」 |
tcpconnect |
TCP 主动连接追踪 | 「连接都去了哪里?」 | |
tcpaccept |
TCP 被动连接追踪 | 「谁在连我的端口?」 | |
tcplife |
TCP 连接生命周期 | 「连接存活了多久?」 |
下文将围绕其中最核心的工具展开实战。
第二章:CPU 性能瓶颈排查实战
2.1 案例:定位高 CPU 消耗的函数(profile)
问题描述
你的服务器上某个进程 CPU 使用率跑到 100%,top 能告诉你「是哪个进程」,但无法告诉你「进程里哪个函数在消耗 CPU」。火焰图是可视化 CPU 时间分布的终极武器。
工具介绍:profile-bpfcc
profile 是一个基于采样的 CPU 剖析器(profiler),它周期性地采集每个 CPU 上正在执行的内核栈和用户栈,统计每个函数调用栈出现的次数。采样频率越高、时间越长,结果越精确。
工作原理:
每 10ms 发送一个定时中断
↓
内核记录当前 CPU 的 PC(程序计数器)
↓
回溯调用栈(frame pointer unwinding)
↓
聚合统计:哪个函数 → 哪个调用路径 → 多少次
实操步骤
Step 1:准备 CPU 密集型测试程序
为了复现真实场景,我们先写一个带明显热点的 C 程序:
c
// cpu_burner.c - 模拟 CPU 密集型负载
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <unistd.h>
// 质数判定 ------ 这是「热点函数」
int is_prime(long n) {
if (n < 2) return 0;
for (long i = 2; i * i <= n; i++)
if (n % i == 0) return 0;
return 1;
}
int main(int argc, char *argv[]) {
int mode = argc > 1 ? atoi(argv[1]) : 0;
printf("[cpu_burner] PID=%d, mode=%d\n", getpid(), mode);
long count = 0;
for (long n = 2; ; n++) {
if (mode == 0) {
if (is_prime(n)) count++;
if (n > 500000) {
printf("[burner] found %ld primes\n", count);
n = 2; count = 0;
}
} else {
volatile double x = sin(n * 0.001) * cos(n * 0.0001);
if (n % 1000000 == 0)
printf("[burner] tick n=%ld\n", n);
}
}
return 0;
}
重要 :编译时使用
-O0 -g -fno-omit-frame-pointer,避免编译器内联优化导致函数调用栈丢失:
bash
gcc -O0 -g -fno-omit-frame-pointer -o /tmp/cpu_burner /tmp/cpu_burner.c -lm
Step 2:启动 CPU 负载
bash
# 启动两个 CPU 密集型进程作为追踪目标
/tmp/cpu_burner 0 > /dev/null 2>&1 &
/tmp/cpu_burner 1 > /dev/null 2>&1 &
Step 3:运行 profile 采样并生成火焰图
bash
# profile 采样 8 秒,频率 99Hz,折叠格式输出
# -d 8 : 采样 8 秒
# -F 99 : 每秒 99 次采样(99Hz)
# -f : 输出折叠格式(一行一个调用栈,末尾为采样次数)
/usr/sbin/profile-bpfcc -d 8 -F 99 -f > /tmp/profile.txt
# 生成火焰图 SVG
/opt/FlameGraph/flamegraph.pl \
--title="CPU Flame Graph - cpu_burner" \
--countname=samples \
/tmp/profile.txt > /tmp/flamegraph.svg
结果解读
原始折叠格式输出(部分):
cpu_burner_dbg;_start;__libc_start_main;[unknown];main;is_prime 521
cpu_burner_dbg;_start;__libc_start_main;[unknown];main;[unknown] 137
cpu_burner_dbg;_start;__libc_start_main;[unknown];main 123
cpu_burner_dbg;_start;__libc_start_main;[unknown];main;[unknown] 65
格式说明 :每一行
调用栈 采样次数,函数名以;分隔,从左到右是从进程到叶节点(最底层被调用函数)。
关键发现:
| 调用栈路径 | 采样次数 | 占比 | 含义 |
|---|---|---|---|
main → is_prime |
521 | ~55% | 🔥 绝对热点------质数判定函数 |
main → [unknown] |
137 | ~15% | 在 main 内部其他操作(循环、printf) |
main |
123 | ~13% | main 函数入口开销 |
unknown 出现是因为部分 libc 内部函数符号未解析。生产环境中使用
-g编译 + 不 strip 符号表可以消除。
火焰图解读方法:
┌──────────────────────────────────────────────────────────┐
│ << 火焰图分析口诀 >> │
│ │
│ 1. X 轴宽度 = CPU 时间占比 --- 越宽越热 │
│ 2. Y 轴高度 = 调用栈深度 --- 向上是 call,向下是 return │
│ 3. 顶部"平原" = 热点函数 --- 横跨最宽的就是元凶 │
│ 4. 颜色 = 代码类型(用户态橙色 / 内核态红色) │
└──────────────────────────────────────────────────────────┘
在我们的实验中,is_prime 出现在调用栈最右侧(叶节点),并且行尾计数高达 521,是绝对的热点函数。优化方向非常明确:优化 is_prime 函数的算法(如使用 Miller-Rabin 素性检测替代试除法)。
💡 生产实战技巧 :不用自己写测试程序。线上 Java 应用 CPU 飙高时,直接
profile-bpfcc -p <Java_PID> -d 30 -f | flamegraph.pl > cpu.svg,然后看最宽的「平原」是哪个方法,配合perf-map-agent解析 JIT 编译的符号。
2.2 案例:追踪新进程的启动(execsnoop)
问题描述
服务器突然 CPU 飙升,top 只看到一堆短命进程------你甚至来不及看它们的命令行参数它们就消失了。这通常是某个定时任务或监控脚本频繁 fork 导致的问题。你需要实时看到系统中每一个新进程的诞生。
工具介绍:execsnoop-bpfcc
execsnoop 通过挂载(hook)内核中的 execve 系统调用追踪点,捕获每一次进程执行的完整信息。
execsnoop 挂载点:
内核 tracepoint: syscalls:sys_enter_execve
↓
捕获: PID, PPID, UID, 命令行参数, 返回值
↓
输出: 实时流,一行一个进程
实操步骤
bash
# 在新终端运行 execsnoop(带时间戳)
/usr/sbin/execsnoop-bpfcc -T
在另一个终端触发一些进程:
bash
for i in 1 2 3; do cat /etc/hostname > /dev/null; done
whoami > /dev/null
结果解读
真实输出:
21:40:02 TIME PCOMM PID PPID RET ARGS
cat 14713 14709 0 /bin/cat /etc/hostname
21:40:02 sleep 14714 14709 0 /usr/bin/sleep 0.1
21:40:02 cat 14715 14709 0 /bin/cat /etc/hostname
21:40:02 sleep 14716 14709 0 /usr/bin/sleep 0.1
21:40:02 cat 14717 14709 0 /bin/cat /etc/hostname
21:40:02 sleep 14718 14709 0 /usr/bin/sleep 0.1
21:40:02 whoami 14719 14709 0 /usr/bin/whoami
字段说明:
| 字段 | 含义 | 本例说明 |
|---|---|---|
TIME |
时间戳(需 -T) |
精确到秒 |
PCOMM |
父进程名 | 我们的 for 循环由 bash 触发 |
PID |
新进程 ID | 每次 exec 都是新 PID |
PPID |
父进程 ID | 全部是 14709(同一个 bash) |
RET |
execve 返回值 |
0 = 成功,非 0 = 失败 |
ARGS |
完整命令行 | 包含所有参数 |
关键发现:
- 所有进程的
PPID都是 14709,说明它们由同一个父进程(我们的 shell)启动 RET全是 0,进程启动全部成功- 可以清晰看到
sleep、cat、whoami的完整命令行
💡 生产实战技巧 :怀疑服务器有挖矿病毒时,运行
execsnoop-bpfcc -T并 grep 可疑的进程名。也可以加上-x参数追踪失败的 exec,用于排查「command not found」类错误。
第三章:文件系统与磁盘 I/O 调试
3.1 案例:谁在疯狂读写文件?(opensnoop)
问题描述
磁盘 I/O 飙升,iostat 显示 util% 接近 100%,但不知道是哪个进程在操作哪些文件。你需要实时看到每一次 open() 系统调用的详情。
工具介绍:opensnoop-bpfcc
opensnoop 挂载点:
内核 tracepoint: syscalls:sys_enter_openat
↓
捕获: PID, 进程名, FD, 返回值, 文件路径
↓
输出: 实时流,一行一次文件打开操作
实操步骤与真实输出
我们开启 opensnoop 后,让它运行几秒钟:
bash
/usr/sbin/opensnoop-bpfcc -T
真实输出(截取):
0.000000000 TIME(s) PID COMM FD ERR PATH
15145 sshd 5 0 /var/log/btmp
0.000124000 296 systemd-journal 34 0 /proc/15145/comm
0.000159000 296 systemd-journal 34 0 /proc/15145/cmdline
0.001620 15151 sshd 3 0 /dev/null
0.001621 15151 sshd -1 2 /proc/sys/crypto/fips_enabled
0.001620 15151 sshd 3 0 /usr/lib/ssl/openssl.cnf
0.001623 15151 sshd 4 0 /etc/gai.conf
0.001623 15151 sshd 4 0 /etc/nsswitch.conf
0.001623 15151 sshd 4 0 /etc/passwd
字段说明:
| 字段 | 含义 | 重点关注 |
|---|---|---|
PID |
进程 ID | 哪个进程在执行 open |
COMM |
进程名 | sshd / systemd-journal |
FD |
文件描述符 | -1 表示打开失败 |
ERR |
错误码 | 非 0 = 打开失败(2 = ENOENT 文件不存在) |
PATH |
文件路径 | 🔥 这就是你要找的「元凶文件」 |
关键发现:
sshd(PID 15151) 在打开/proc/sys/crypto/fips_enabled时返回ERR=2(文件不存在),这可能是一个配置问题systemd-journal频繁读取/proc/<pid>/comm等文件,这是正常的日志记录行为sshd读取了一系列动态库(libcrypto.so.3、libpam.so.0等),这是每个 SSH 会话的标准加载流程
💡 生产实战技巧 :
opensnoop-bpfcc -T | grep -v '^296'过滤掉 systemd-journal 的噪音(PID 296)。加上-x参数只显示失败的 open,用于排查文件缺失导致的程序异常。
3.2 案例:深入磁盘 I/O 延迟分析(biolatency / biosnoop)
问题描述
应用日志无异常,但用户反馈响应慢。iostat 只能告诉你磁盘忙不忙,但无法告诉你延迟分布------是每次都慢还是偶尔卡一下?偶尔卡一下的时候是谁在写什么?
工具介绍
-
biolatency:以直方图(histogram)形式展示块设备 I/O 请求的延迟分布。x 轴是延迟区间,y 轴是落在该区间的 I/O 次数。 -
biosnoop:逐行输出每一个 I/O 请求的详细信息------时间、进程名、扇区号、传输字节数、延迟。biolatency 工作原理:
挂载块层 tracepoint: block:block_io_start / block:block_io_done
↓
计算每个 I/O: LATENCY = io_done_time - io_start_time
↓
按延迟区间分组统计 → 直方图输出
实操步骤
Step 1:制造磁盘 I/O 负载
bash
# 50,000 次 4KB 随机写,使用 O_DIRECT 绕过页缓存
dd if=/dev/zero of=/tmp/iotest bs=4K count=50000 oflag=direct conv=fdatasync &
# 同步写(不带 O_DIRECT,经过页缓存)
dd if=/dev/zero of=/tmp/iotest2 bs=4K count=50000 conv=fdatasync &
Step 2:运行 biolatency 查看延迟分布
bash
# -m: 毫秒级直方图; 1: 每秒输出一次; 5: 共输出 5 次
/usr/sbin/biolatency-bpfcc -m 1 5
真实输出:
Tracing block device I/O... Hit Ctrl-C to end.
msecs : count distribution
0 -> 1 : 2599 |****************************************|
2 -> 3 : 2 | |
4 -> 7 : 0 | |
8 -> 15 : 1 | |
msecs : count distribution
0 -> 1 : 2516 |****************************************|
2 -> 3 : 1 | |
4 -> 7 : 1 | |
msecs : count distribution
0 -> 1 : 2651 |****************************************|
2 -> 3 : 2 | |
4 -> 7 : 1 | |
msecs : count distribution
0 -> 1 : 2572 |****************************************|
2 -> 3 : 2 | |
4 -> 7 : 1 | |
msecs : count distribution
0 -> 1 : 2531 |****************************************|
2 -> 3 : 1 | |
4 -> 7 : 2 | |
Step 3:运行 biosnoop 逐 I/O 追踪
bash
# 实时追踪每一个块设备 I/O 请求
/usr/sbin/biosnoop-bpfcc
真实输出:
TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)
0.000000 dd 16297 vda W 15807256 4096 0.30
0.000425 dd 16297 vda W 15807264 4096 0.40
0.001298 dd 16297 vda W 15807272 4096 0.33
0.001772 dd 16297 vda W 15807280 4096 0.46
0.002094 dd 16297 vda W 15807288 4096 0.31
0.002491 dd 16297 vda W 15807296 4096 0.39
0.002822 dd 16297 vda W 15807304 4096 0.32
0.003203 dd 16297 vda W 15807312 4096 0.37
0.003533 dd 16297 vda W 15807320 4096 0.32
0.003971 dd 16297 vda W 15807328 4096 0.43
0.005355 dd 16297 vda W 15807352 4096 0.55
0.006462 dd 16297 vda W 15807360 4096 1.10 ← 延迟峰值
0.006798 dd 16297 vda W 15807368 4096 0.32
结果解读
biolatency 分析:
每秒钟约 2500~2600 次 I/O 操作
├── 99.9% 的 I/O 在 0~1ms 完成 ← 绝大多数极快
├── ~0.1% 在 2~3ms ← 偶尔轻微延迟
└── 极少在 4~15ms ← 偶发性的较大延迟
延迟分布是长尾形状(绝大部分 < 1ms),属于正常云盘表现。如果 4~7ms 区间的 count 突然增大,说明存储后端出现了性能抖动。
biosnoop 分析:
| 字段 | 本次值 | 含义 |
|---|---|---|
COMM |
dd |
执行 I/O 的进程名 |
DISK |
vda |
块设备名(Virtio 虚拟磁盘) |
T |
W |
操作类型(W=写, R=读) |
SECTOR |
15807xxx | 写入的数据扇区号 |
BYTES |
4096 | 每次写入 4KB(与 dd 的 bs=4K 一致) |
LAT(ms) |
0.29 ~ 1.10 | 单次 I/O 延迟(毫秒) |
注意到
SECTOR地址基本连续(15807256 → 15807264 → ...),说明是顺序写 。如果扇区号跳变剧烈,则是随机写------此时延迟通常会显著升高。
💡 生产实战技巧 :biolatency加上-Q参数可以区分设备时间和排队时间,帮助判断延迟是来自存储硬件还是内核 I/O 调度器排队。biosnoop加上-P参数可以自动标注 I/O 是顺序还是随机。
第四章:网络性能问题诊断
4.1 案例:监控实时网络流量(tcptop)
问题描述
服务器出口带宽突然跑满,iftop 能看到对端 IP,但不知道是哪个进程发的流量。你需要一个「TCP 流量版的 top」------按进程显示实时 TCP 吞吐量。
工具介绍:tcptop-bpfcc
tcptop 通过挂载内核中 TCP 发送/接收的 probe 点,按进程和连接聚合实时流量。它就像 top 一样,但显示的不是 CPU 利用率,而是每秒 TCP 吞吐量(KB/s)。
⚠️
tcptop需要终端支持,在无 TERM 环境(如 CI/CD)可能报 TERM 相关错误。可以通过TERM=xterm设置环境变量或使用非交互式的tcpconnect/tcpaccept替代。
4.2 案例:追踪连接的建立与关闭(tcpconnect / tcpaccept)
问题描述
运维或安全场景中,你需要知道:
- 主动连接(connect):服务器上哪些进程在向外部发起 TCP 连接?连到哪里?
- 被动连接(accept):哪些外部 IP 在连接你服务器上的端口?
这在排查「连接泄漏」「异常外连」「连接风暴」时至关重要。
工具介绍
-
tcpconnect:追踪内核tcp_v4_connect/tcp_v6_connect,捕获每一次主动连接。 -
tcpaccept:追踪内核inet_csk_accept,捕获每一次被动接受连接。tcpconnect 挂载点: kprobe:tcp_v4_connect
tcpaccept 挂载点: kretprobe:inet_csk_accept
实操步骤
先启动一个 HTTP 服务作为被追踪目标:
python
# 启动一个简单 HTTP 服务(端口 9999)
import http.server, threading
class H(http.server.SimpleHTTPRequestHandler):
def log_message(self, *a): pass
server = http.server.HTTPServer(('', 9999), H)
t = threading.Thread(target=server.serve_forever, daemon=True); t.start()
print("HTTP server on :9999")
运行 tcpconnect(主动连接侧------curl 发起连接):
bash
# -t 添加时间戳
/usr/sbin/tcpconnect-bpfcc -t
在另一个终端用 curl 发起请求:
bash
for i in $(seq 5); do curl -s -o /dev/null http://localhost:9999/etc/hostname; done
真实输出:
Tracing connect ... Hit Ctrl-C to end
TIME(s) PID COMM IP SADDR DADDR DPORT
16647 curl 4 127.0.0.1 127.0.0.1 9999
-0.000 16647 curl 6 ::1 ::1 9999
运行 tcpaccept(被动接受侧------HTTP 服务接受连接):
bash
/usr/sbin/tcpaccept-bpfcc
真实输出:
PID COMM IP RADDR RPORT LADDR LPORT
16757 python3 4 127.0.0.1 37830 127.0.0.1 9999
结果解读
tcpconnect 输出解读:
PID=16647 COMM=curl ← curl 进程发起的连接
IP=4 SADDR=127.0.0.1 DADDR=127.0.0.1 DPORT=9999 ← IPv4 本地回环
IP=6 SADDR=::1 DADDR=::1 DPORT=9999 ← IPv6 本地回环
curl 默认优先尝试 IPv6(::1),失败后回退到 IPv4(127.0.0.1)。两次 connect 是 curl 的正常行为(Happy Eyeballs)。
tcpaccept 输出解读:
PID=16757 COMM=python3 ← 我们的 HTTP 服务进程
IP=4 RADDR=127.0.0.1 LPORT=9999 ← 接受来自 localhost 的连接
RPORT=37830 ← 客户端的临时端口
字段速查:
| tcpconnect | tcpaccept | 含义 |
|---|---|---|
| PID | PID | 发起/接受连接的进程 ID |
| COMM | COMM | 进程名 |
| IP | IP | 4=IPv4, 6=IPv6 |
| SADDR | RADDR | 源地址 / 远程地址 |
| DADDR | LADDR | 目的地址 / 本地地址 |
| DPORT | LPORT | 目的端口 / 本地端口 |
| --- | RPORT | 远程(客户端)端口 |
💡 生产实战技巧:
- 排查外连异常 :
tcpconnect-bpfcc -t | grep -v '127.0.0.1'过滤掉本地回环,只看真正的外连。- 排查连接泄漏 :
tcpconnect-bpfcc -P 3306只看连接到 MySQL 的进程。如果短连接频繁创建不释放,这里会看到大量 connect。- 排查安全事件 :
tcpaccept-bpfcc能看到谁在连你的 SSH 端口(22),配合防火墙做安全审计。
第五章:进阶探索------BPF 与应用程序、安全
5.1 在应用中集成 BPF(USDT)简介
前文我们使用 BCC 工具观测的是内核行为 ------系统调用、TCP 连接、磁盘 I/O。但有时候,问题的根源不在内核,而在应用层的某个函数。
USDT(User Statically Defined Tracing) 允许开发者在自己的 C/C++ 代码中插入静态探针(probe),然后通过 BPF 动态追踪这些探针,无需重启应用。
工作原理:
C/C++ 代码中插入 DTRACE_PROBE 宏
↓
编译后生成 ELF 中的 .note.stapsdt 段
↓
BCC / bpftrace 通过 uprobe 读取这些探针地址
↓
运行时动态附加 BPF 程序到探针位置
↓
当程序执行到探针处 → 触发 BPF 程序 → 记录日志/计数/延时
一个简单的示例:
c
#include <sys/sdt.h> // systemtap SDK
void do_work(int id) {
DTRACE_PROBE2(myapp, work_start, id, 0); // 探针:工作开始
// ... 实际业务逻辑 ...
DTRACE_PROBE2(myapp, work_done, id, result); // 探针:工作结束
}
bash
# 追踪应用程序中的自定义探针
bpftrace -e 'usdt:/path/to/myapp:myapp:work_start { printf("work_start id=%d\n", arg0); }'
主流项目中使用 USDT 的案例:MySQL(查询执行耗时)、PostgreSQL、Node.js(GC 事件)、Python(函数调用)。掌握 USDT 意味着你可以为任何 C/C++ 应用添加可观测性。
5.2 BPF 与系统安全初探
BPF 在安全领域同样大放异彩。几个典型应用:
| 安全场景 | BPF 能力 | 代表工具 |
|---|---|---|
| 运行时安全监控 | hook 关键系统调用,检测异常行为 | Falco(CNCF 项目) |
| 系统调用过滤 | 限制进程能调用的 syscall 白名单 | seccomp-bpf |
| 网络包过滤 | 在内核态过滤/丢弃恶意流量 | Cilium(eBPF-based CNI) |
| 文件完整性监控 | 追踪所有文件修改操作 | opensnoop 常驻 |
| 容器逃逸检测 | 监控 mount namespace 变化 | Tracee(Aqua Security) |
┌─────────────────────────────────────────────────┐
│ BPF 安全可观测性架构 │
│ │
│ 用户空间 │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Falco │ │ Cilium │ │ Tracee │ │
│ │ (规则引擎) │ │ (网络策略) │ │ (行为分析) │ │
│ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
├───────┼─────────────┼──────────────┼──────────────┤
│ 内核空间 BPF Programs (sandboxed) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ sys_enter_* XDP/tc hook kprobe:mount │
│ (系统调用拦截) (网络包拦截) (ns 变更检测) │
└─────────────────────────────────────────────────┘
以 Falco 为例:它内置了 80+ 条安全规则(如「检测非官方源安装软件包」「检测容器内执行 shell」),每条规则背后都是一个 BPF 程序在内核态实时检测。相比传统的审计日志方案,BPF 方案开销低数个数量级。
结语:从工具使用者到问题终结者
回顾:BPF/BCC 带来的范式转变
在 BPF 之前,Linux 性能调试像是在黑暗中摸索------你能摸到墙,但看不到房间的全貌。BPF 给了你一束光:
| 维度 | 传统方式 | BPF 方式 |
|---|---|---|
| CPU 热点 | perf top(不可回放) |
profile + 火焰图(精确到函数) |
| 进程追踪 | ps + 日志(离散的) |
execsnoop(实时全量) |
| 磁盘 I/O | iostat(设备级、无进程归因) |
biolatency + biosnoop(逐 I/O、逐进程) |
| 网络连接 | netstat(快照)+ tcpdump(无法归因进程) |
tcpconnect / tcpaccept(实时、按进程) |
| 性能开销 | strace ~30% |
BPF ~1-3% |
| 安全审计 | 审计日志(事后分析) | 实时内核态检测 |
下一步:持续关注 BPF 生态
BPF 技术栈正在快速发展,推荐关注:
BPF 技术栈全景
├── BCC (Python/Lua 前端) ← 本文工具
│ └── 90+ 预构建工具,即装即用
├── bpftrace (类 awk 脚本) ← 快速原型
│ └── # bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
├── libbpf (C 语言原生库) ← 生产级
│ └── CO-RE(一次编译,到处运行)
├── Cilium / Falco / Pixie ← 应用层
│ └── CNI / 安全 / K8s 可观测性
└── eBPF for Windows ← 跨平台
└── Microsoft 正在推进 Windows eBPF
行动号召
「下次再遇到棘手的性能问题,别再只盯着 top 了------试试 BPF 吧!」
记住这 6 个工具,它们是你排查问题的第一梯队:
| 问题类型 | 首选工具 | 一句话命令 |
|---|---|---|
| CPU 高 | profile-bpfcc -d 30 -f |
生成火焰图找热点 |
| 异常进程 | execsnoop-bpfcc -T |
实时看谁在启动进程 |
| 文件被删 | opensnoop-bpfcc -T |
追踪文件操作 |
| 磁盘慢了 | biolatency-bpfcc -m 1 10 |
延迟直方图 |
| 带宽跑满 | tcptop-bpfcc |
TCP 流量 top |
| 连接异常 | tcpconnect-bpfcc -t + tcpaccept-bpfcc |
连接全生命周期 |
附录:速查表 & 参考资源
BCC 常用参数速查
| 参数 | 适用工具 | 含义 |
|---|---|---|
-T |
execsnoop, opensnoop, tcpconnect | 添加时间戳 |
-t |
tcpconnect, tcpaccept | 另一种时间戳格式 |
-d N |
profile | 运行 N 秒后退出 |
-F N |
profile | 采样频率(Hz) |
-f |
profile | 折叠格式输出(火焰图用) |
-p PID |
profile, opensnoop | 只追踪特定进程 |
-m |
biolatency | 毫秒级直方图 |
-Q |
biolatency, biosnoop | 包含 OS 排队时间 |
-P |
tcpconnect | 按端口过滤 |
学习资源
| 资源 | 链接 | 说明 |
|---|---|---|
| BCC 官方仓库 | github.com/iovisor/bcc | 源码 + 工具文档 |
| BCC 教程 | github.com/iovisor/bcc/blob/master/docs/tutorial.md | 官方入门教程 |
| bpftrace 教程 | github.com/bpftrace/bpftrace | 类 awk 的 BPF 脚本 |
| Brendan Gregg 博客 | brendangregg.com | BPF 性能分析权威 |
| FlameGraph 仓库 | github.com/brendangregg/FlameGraph | 火焰图生成工具 |
| eBPF 官方文档 | ebpf.io | eBPF 入门和项目目录 |
| Linux Kernel BPF 文档 | kernel.org/doc/html/latest/bpf | 内核级文档 |
本文所有命令均在以下环境中实测通过:华为云 ecs-2aa6-0001 (2vCPU/4GiB), Ubuntu 24.04.4 LTS, Linux 6.8.0-106-generic, BCC 0.29.1。