WEBKT

巧用 eBPF!容器 CPU 和内存占用率监控,告别盲人摸象

28 0 0 0

为什么选择 eBPF?

eBPF 监控容器资源的基本原理

实战:使用 eBPF 监控容器 CPU 使用率

扩展:监控容器内存占用

总结

作为一名资深开发者,我深知容器化技术在现代应用中的重要性。但容器内部的资源使用情况,就像一个黑盒子,让人难以捉摸。如何才能穿透这层迷雾,清晰地了解每个进程的 CPU 和内存消耗呢?今天,我就来分享一种高效、强大的方法:使用 eBPF (Extended Berkeley Packet Filter) 来监控容器内部的资源使用情况。

为什么选择 eBPF?

你可能会问,已经有很多工具可以监控容器资源了,比如 docker statskubectl top 等,为什么还要费力气使用 eBPF 呢?原因很简单:

  • 性能开销低:eBPF 程序运行在内核态,避免了用户态和内核态之间的频繁切换,极大地降低了性能开销。相比于传统的监控方法,eBPF 对系统性能的影响几乎可以忽略不计。
  • 高度灵活性:eBPF 允许你自定义监控逻辑,可以根据实际需求收集各种指标。无论是 CPU 使用率、内存占用、网络流量,还是自定义的应用程序指标,eBPF 都能轻松应对。
  • 深入内核:eBPF 可以直接访问内核数据,获取最原始、最准确的系统信息。这使得 eBPF 能够提供更深入的性能分析和故障诊断。
  • 安全性:eBPF 程序在运行前会经过严格的验证,确保其不会对系统造成损害。同时,eBPF 还提供了完善的权限控制机制,防止恶意程序滥用。

eBPF 监控容器资源的基本原理

eBPF 的核心思想是在内核中注入自定义的代码,这些代码可以被安全地执行,并且可以访问内核数据。要使用 eBPF 监控容器资源,我们需要做以下几件事:

  1. 确定监控目标:我们需要明确要监控哪些指标,比如 CPU 使用率、内存占用等。
  2. 选择合适的 eBPF 钩子点:eBPF 允许我们将代码注入到内核的各个关键位置,比如函数入口、函数返回、系统调用等。我们需要选择合适的钩子点,以便能够获取到我们需要的指标数据。
  3. 编写 eBPF 程序:使用 C 语言编写 eBPF 程序,程序的主要功能是从内核中获取指标数据,并将其存储到 eBPF 的数据结构中。
  4. 加载和运行 eBPF 程序:使用 eBPF 提供的工具,将编写好的 eBPF 程序加载到内核中并运行。
  5. 从 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_execsched_process_switch 函数分别附加到内核的 sched_process_execsched_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_allockmem_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_allockmem_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 的理解。

祝你学习顺利!

容器极客 eBPF容器监控性能优化

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9612