巧用 eBPF!容器 CPU 和内存占用率监控,告别盲人摸象
为什么选择 eBPF?
eBPF 监控容器资源的基本原理
实战:使用 eBPF 监控容器 CPU 使用率
扩展:监控容器内存占用
总结
作为一名资深开发者,我深知容器化技术在现代应用中的重要性。但容器内部的资源使用情况,就像一个黑盒子,让人难以捉摸。如何才能穿透这层迷雾,清晰地了解每个进程的 CPU 和内存消耗呢?今天,我就来分享一种高效、强大的方法:使用 eBPF (Extended Berkeley Packet Filter) 来监控容器内部的资源使用情况。
为什么选择 eBPF?
你可能会问,已经有很多工具可以监控容器资源了,比如 docker stats
、kubectl top
等,为什么还要费力气使用 eBPF 呢?原因很简单:
- 性能开销低:eBPF 程序运行在内核态,避免了用户态和内核态之间的频繁切换,极大地降低了性能开销。相比于传统的监控方法,eBPF 对系统性能的影响几乎可以忽略不计。
- 高度灵活性:eBPF 允许你自定义监控逻辑,可以根据实际需求收集各种指标。无论是 CPU 使用率、内存占用、网络流量,还是自定义的应用程序指标,eBPF 都能轻松应对。
- 深入内核:eBPF 可以直接访问内核数据,获取最原始、最准确的系统信息。这使得 eBPF 能够提供更深入的性能分析和故障诊断。
- 安全性:eBPF 程序在运行前会经过严格的验证,确保其不会对系统造成损害。同时,eBPF 还提供了完善的权限控制机制,防止恶意程序滥用。
eBPF 监控容器资源的基本原理
eBPF 的核心思想是在内核中注入自定义的代码,这些代码可以被安全地执行,并且可以访问内核数据。要使用 eBPF 监控容器资源,我们需要做以下几件事:
- 确定监控目标:我们需要明确要监控哪些指标,比如 CPU 使用率、内存占用等。
- 选择合适的 eBPF 钩子点:eBPF 允许我们将代码注入到内核的各个关键位置,比如函数入口、函数返回、系统调用等。我们需要选择合适的钩子点,以便能够获取到我们需要的指标数据。
- 编写 eBPF 程序:使用 C 语言编写 eBPF 程序,程序的主要功能是从内核中获取指标数据,并将其存储到 eBPF 的数据结构中。
- 加载和运行 eBPF 程序:使用 eBPF 提供的工具,将编写好的 eBPF 程序加载到内核中并运行。
- 从 eBPF 数据结构中读取数据:编写用户态程序,从 eBPF 的数据结构中读取指标数据,并将其展示出来。
实战:使用 eBPF 监控容器 CPU 使用率
接下来,我们通过一个实际的例子来演示如何使用 eBPF 监控容器的 CPU 使用率。我们将使用 perf_events
钩子点来监控进程的 CPU 时间。
1. 编写 eBPF 程序 (cpu_usage.c)
#include <uapi/linux/ptrace.h> #include <linux/sched.h> struct key_t { u32 pid; u32 tid; char comm[TASK_COMM_LEN]; }; BPF_HASH(start, struct key_t, u64); BPF_HASH(counts, struct key_t, u64); // 记录进程/线程开始运行的时间 int sched_process_exec(struct pt_regs *ctx, struct task_struct *tsk) { struct key_t key = {.pid = tsk->tgid, .tid = tsk->pid}; bpf_get_current_comm(&key.comm, sizeof(key.comm)); u64 ts = bpf_ktime_get_ns(); start.update(&key, &ts); return 0; } // 记录进程/线程切换出去的时间,并计算 CPU 使用时间 int sched_process_switch(struct pt_regs *ctx, struct task_struct *prev) { struct key_t key = {.pid = prev->tgid, .tid = prev->pid}; bpf_get_current_comm(&key.comm, sizeof(key.comm)); // 获取进程/线程开始运行的时间 u64 *tsp = start.lookup(&key); if (tsp == NULL) { return 0; // 没有找到开始时间,说明进程/线程可能已经结束 } // 计算 CPU 使用时间 u64 delta = bpf_ktime_get_ns() - *tsp; // 更新 CPU 使用计数 u64 value = 0; u64 *countsp = counts.lookup_or_init(&key, &value); (*countsp) += delta; // 删除开始时间 start.delete(&key); return 0; }
这段代码定义了两个 eBPF 哈希表:start
用于存储进程/线程开始运行的时间,counts
用于存储进程/线程的 CPU 使用时间。sched_process_exec
函数在进程/线程开始运行时被调用,记录开始时间;sched_process_switch
函数在进程/线程切换出去时被调用,计算 CPU 使用时间并更新计数。
2. 编译 eBPF 程序
使用 clang 编译器将 C 代码编译成 eBPF 字节码:
clang -O2 -target bpf -c cpu_usage.c -o cpu_usage.o
3. 编写用户态程序 (cpu_usage.py)
#!/usr/bin/env python3 from bcc import BPF import time # 加载 eBPF 程序 b = BPF(src_file="cpu_usage.c") # 附加 kprobe 到内核函数 b.attach_kprobe(event="sched_process_exec", fn_name="sched_process_exec") b.attach_kprobe(event="sched_process_switch", fn_name="sched_process_switch") # 打印表头 print("%-16s %-6s %-6s %s" % ("COMM", "PID", "TID", "CPU (ms)")) # 循环读取数据 while True: time.sleep(1) for k, v in sorted(b["counts"].items(), key=lambda counts: counts[1].value): print("%-16s %-6d %-6d %.2f" % (k.comm.decode('utf-8', 'replace'), k.pid, k.tid, v.value / 1000000)) b["counts"].clear()
这段 Python 代码使用 bcc
库来加载和运行 eBPF 程序。它首先加载编译好的 eBPF 字节码,然后将 sched_process_exec
和 sched_process_switch
函数分别附加到内核的 sched_process_exec
和 sched_process_switch
事件上。最后,它循环读取 counts
哈希表中的数据,并打印进程/线程的 CPU 使用率。
4. 运行程序
首先,确保你已经安装了 bcc
库:
sudo apt-get update sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r) sudo pip3 install bcc
然后,运行 Python 脚本:
sudo python3 cpu_usage.py
你将会看到类似下面的输出:
COMM PID TID CPU (ms) sleep 2245 2245 0.01 python3 2244 2244 0.02 ... (输出省略) ...
这表示每个进程/线程的 CPU 使用时间(单位:毫秒)。
5. 容器环境下的应用
上述代码可以直接在宿主机上运行,监控所有进程的 CPU 使用率。要监控容器内部的进程,我们需要做一些调整:
找到容器的 PID namespace:每个容器都有自己的 PID namespace,我们需要找到目标容器的 PID namespace。可以使用
docker inspect
命令来获取容器的 PID namespace。在容器的 PID namespace 中运行 eBPF 程序:可以使用
nsenter
命令在容器的 PID namespace 中运行 eBPF 程序。例如:sudo nsenter -t <容器PID> -p python3 cpu_usage.py
其中,
<容器PID>
是容器的 PID。
这样,eBPF 程序就能监控容器内部的进程的 CPU 使用率了。
扩展:监控容器内存占用
除了 CPU 使用率,我们还可以使用 eBPF 监控容器的内存占用。与监控 CPU 使用率类似,我们需要选择合适的 eBPF 钩子点,并编写 eBPF 程序来获取内存占用信息。一个常用的方法是使用 kmem_cache_alloc
和 kmem_cache_free
钩子点来跟踪内核内存的分配和释放。
1. 编写 eBPF 程序 (memory_usage.c)
#include <uapi/linux/ptrace.h> #include <linux/mm.h> #include <linux/sched.h> struct key_t { u32 pid; u32 tid; char comm[TASK_COMM_LEN]; }; BPF_HASH(allocs, struct key_t, u64); BPF_HASH(frees, struct key_t, u64); // 记录内存分配 int kalloc(struct pt_regs *ctx, struct kmem_cache *cachep, gfp_t flags) { struct key_t key = {.pid = bpf_get_current_pid_tgid() >> 32, .tid = bpf_get_current_pid_tgid()}; bpf_get_current_comm(&key.comm, sizeof(key.comm)); u64 size = cachep->size; u64 value = 0; u64 *valp = allocs.lookup_or_init(&key, &value); (*valp) += size; return 0; } // 记录内存释放 int kfree(struct pt_regs *ctx, struct kmem_cache *cachep, void *objp) { struct key_t key = {.pid = bpf_get_current_pid_tgid() >> 32, .tid = bpf_get_current_pid_tgid()}; bpf_get_current_comm(&key.comm, sizeof(key.comm)); u64 size = cachep->size; u64 value = 0; u64 *valp = frees.lookup_or_init(&key, &value); (*valp) += size; return 0; }
这段代码定义了两个 eBPF 哈希表:allocs
用于存储进程/线程分配的内存大小,frees
用于存储进程/线程释放的内存大小。kalloc
函数在内存分配时被调用,记录分配的内存大小;kfree
函数在内存释放时被调用,记录释放的内存大小。
2. 编译 eBPF 程序
clang -O2 -target bpf -c memory_usage.c -o memory_usage.o
3. 编写用户态程序 (memory_usage.py)
#!/usr/bin/env python3 from bcc import BPF import time # 加载 eBPF 程序 b = BPF(src_file="memory_usage.c") # 附加 kprobe 到内核函数 b.attach_kprobe(event="kmem_cache_alloc", fn_name="kalloc") b.attach_kprobe(event="kmem_cache_free", fn_name="kfree") # 打印表头 print("%-16s %-6s %-6s %10s %10s %10s" % ("COMM", "PID", "TID", "ALLOC (KB)", "FREE (KB)", "NET (KB)")) # 循环读取数据 while True: time.sleep(1) for k in sorted(set(b["allocs"].keys()) | set(b["frees"].keys()), key=lambda key: key.pid): alloc = b["allocs"].get(k, 0).value free = b["frees"].get(k, 0).value net = alloc - free print("%-16s %-6d %-6d %10d %10d %10d" % (k.comm.decode('utf-8', 'replace'), k.pid, k.tid, alloc // 1024, free // 1024, net // 1024)) b["allocs"].clear() b["frees"].clear()
这段 Python 代码与监控 CPU 使用率的代码类似,只是钩子点和数据结构不同。它使用 kmem_cache_alloc
和 kmem_cache_free
钩子点来跟踪内存的分配和释放,并打印进程/线程的内存分配、释放和净占用情况。
4. 运行程序
sudo python3 memory_usage.py
你将会看到类似下面的输出:
COMM PID TID ALLOC (KB) FREE (KB) NET (KB) sleep 2245 2245 4 0 4 python3 2244 2244 128 64 64 ... (输出省略) ...
这表示每个进程/线程的内存分配、释放和净占用情况(单位:KB)。
5. 容器环境下的应用
与监控 CPU 使用率类似,要监控容器内部的进程的内存占用,我们需要在容器的 PID namespace 中运行 eBPF 程序。
总结
eBPF 是一种强大的工具,可以用于监控容器内部的资源使用情况。通过自定义 eBPF 程序,我们可以收集各种指标,并进行深入的性能分析和故障诊断。希望这篇文章能够帮助你更好地了解和使用 eBPF,从而更好地管理你的容器化应用。
当然,eBPF 的学习曲线比较陡峭,需要一定的内核知识和编程经验。但只要你愿意投入时间和精力,相信你一定能够掌握这项强大的技术,并将其应用到实际工作中。
最后,给大家一些学习 eBPF 的建议:
- 从简单的例子开始:不要一开始就尝试编写复杂的 eBPF 程序,可以从简单的例子入手,比如打印 hello world、监控函数调用等。
- 阅读官方文档和示例代码:eBPF 官方文档和示例代码是学习 eBPF 的重要资源,可以帮助你了解 eBPF 的基本概念和使用方法。
- 参考开源项目:有很多优秀的开源项目使用了 eBPF 技术,可以参考这些项目的代码,学习 eBPF 的实际应用。
- 多实践:学习 eBPF 最好的方法就是多实践,通过编写和运行 eBPF 程序,加深对 eBPF 的理解。
祝你学习顺利!