WEBKT

云平台工程师如何用好eBPF?容器CPU监控实战指南

61 0 0 0

1. eBPF:内核观测的瑞士军刀

2. 准备工作:搭建eBPF开发环境

3. 实战:监控容器CPU占用率

4. 进阶:过滤特定容器的进程

5. 更多优化:使用perf event进行更精确的采样

6. 总结与展望

作为一名云平台工程师,你是否曾为容器的CPU使用率监控而头疼?传统的监控方式往往粒度粗,难以定位到具体的进程,更别提进行精细化的资源隔离和性能优化了。别担心,eBPF(Extended Berkeley Packet Filter)技术为你提供了一种强大的解决方案。本文将深入探讨如何利用eBPF监控容器中各个进程的CPU占用情况,从而实现更高效的资源管理和性能优化。我们将会从eBPF的基础概念入手,一步步地引导你完成一个实战项目,让你能够真正掌握这项技术。

1. eBPF:内核观测的瑞士军刀

在深入实战之前,我们需要先了解一下eBPF是什么,以及它为什么如此强大。

1.1 什么是eBPF?

eBPF最初是为网络数据包过滤而设计的,但现在已经发展成为一个通用的内核观测和可编程框架。它可以让你在内核中安全地运行自定义代码,而无需修改内核源代码或加载内核模块。这极大地降低了风险,并提高了灵活性。

1.2 eBPF的优势

  • 安全性: eBPF程序在运行前会经过内核验证器的严格检查,确保不会崩溃或影响系统稳定性。
  • 高性能: eBPF程序通常使用JIT(Just-In-Time)编译,可以获得接近原生代码的性能。
  • 灵活性: 你可以使用C等高级语言编写eBPF程序,然后编译成字节码在内核中运行。
  • 可观测性: eBPF可以访问内核中的各种事件和数据,为你提供深入的系统洞察。

1.3 eBPF的应用场景

eBPF的应用场景非常广泛,包括:

  • 网络性能监控和优化: 例如,跟踪TCP连接延迟、丢包率等。
  • 安全分析: 例如,检测恶意软件、入侵行为等。
  • 性能分析: 例如,监控CPU、内存、磁盘I/O等。
  • 容器监控: 例如,监控容器的资源使用情况、网络流量等。

2. 准备工作:搭建eBPF开发环境

在开始编写eBPF程序之前,我们需要先搭建一个合适的开发环境。这里我们推荐使用Ubuntu系统,因为它对eBPF的支持比较完善。

2.1 安装必要的工具

首先,我们需要安装一些必要的工具,包括:

  • Linux内核头文件: 用于编译eBPF程序。
  • LLVM: 用于将C代码编译成eBPF字节码。
  • Clang: C语言编译器,与LLVM配合使用。
  • bpftool: 用于加载、卸载和管理eBPF程序。
  • libbpf: eBPF的C库,提供了一些辅助函数。

在Ubuntu上,可以使用以下命令安装这些工具:

sudo apt update
sudo apt install linux-headers-$(uname -r) clang llvm libelf-dev bpftool

2.2 安装Python库(可选)

虽然eBPF程序可以使用C语言编写,但通常我们会使用Python库来简化程序的开发和调试。这里我们推荐使用bccbpftrace这两个库。

  • bcc(BPF Compiler Collection): 提供了一组高级的工具和库,可以让你更方便地编写和调试eBPF程序。
  • bpftrace: 一种高级的eBPF跟踪语言,可以让你用简单的脚本来分析系统性能。

可以使用以下命令安装这些库:

sudo apt install python3-pip
pip3 install bcc bpftrace

3. 实战:监控容器CPU占用率

现在,我们终于可以开始编写eBPF程序来监控容器的CPU占用率了。我们将使用C语言编写eBPF程序,并使用Python脚本来加载和显示结果。

3.1 编写eBPF程序(C语言)

首先,创建一个名为cpu_monitor.c的文件,并添加以下代码:

#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct key_t {
u32 pid;
u32 tid;
char comm[TASK_COMM_LEN];
};
BPF_HASH(counts, struct key_t, u64);
int kprobe__finish_task_switch(struct pt_regs *ctx, struct task_struct *prev) {
struct task_struct *curr = (struct task_struct *)ctx->rax;
struct key_t key = {.pid = curr->pid, .tid = curr->tgid};
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 zero = 0;
u64 *val = counts.lookup_or_init(&key, &zero);
if (val) {
(*val) += 1;
}
return 0;
}

这段代码的作用是:

  • 定义了一个key_t结构体,用于存储进程的PID、TID和进程名。
  • 定义了一个counts哈希表,用于存储每个进程的CPU占用计数。
  • 使用kprobe__finish_task_switch函数,在每次进程切换时,增加对应进程的CPU占用计数。

3.2 编写Python脚本

接下来,创建一个名为cpu_monitor.py的文件,并添加以下代码:

#!/usr/bin/env python3
from bcc import BPF
import time
# 加载eBPF程序
b = BPF(src_file="cpu_monitor.c")
# 打印哈希表数据
while True:
time.sleep(2)
print("\n{:<6} {:<6} {:<16} {:<8}".format("PID", "TID", "COMM", "COUNT"))
for k, v in sorted(b["counts"].items(), key=lambda counts: counts[1].value):
print("{:<6} {:<6} {:<16} {:<8}".format(k.pid, k.tid, k.comm.decode('utf-8', 'replace'), v.value))
b["counts"].clear()

这段代码的作用是:

  • 使用bcc库加载cpu_monitor.c文件,编译并加载eBPF程序到内核。
  • 每隔2秒,打印一次哈希表中的数据,显示每个进程的PID、TID、进程名和CPU占用计数。
  • 清空哈希表,以便下次统计。

3.3 运行程序

现在,你可以运行cpu_monitor.py脚本来监控容器的CPU占用率了。首先,确保你有足够的权限运行eBPF程序(通常需要root权限):

sudo python3 cpu_monitor.py

运行后,你会看到类似以下的输出:

PID TID COMM COUNT
1 1 init 1234
2 2 kthreadd 5678
... ... ... ...
1234 1234 my_container_app 9012

4. 进阶:过滤特定容器的进程

上面的程序会监控所有进程的CPU占用率,但有时我们只想监控特定容器的进程。这时,我们可以使用cgroup来过滤进程。

4.1 什么是cgroup?

cgroup(Control Group)是Linux内核提供的一种资源隔离机制,可以限制、控制和隔离进程组(即cgroup)的资源使用。Docker等容器技术广泛使用cgroup来实现容器的资源隔离。

4.2 获取容器的cgroup路径

要过滤特定容器的进程,首先需要获取该容器的cgroup路径。可以使用以下命令获取Docker容器的cgroup路径:

docker inspect <container_id> | grep Cgroup

例如,如果你的容器ID是1234567890ab,那么运行以上命令可能会得到类似以下的输出:

"Cgroup": "/docker/1234567890ab",
"CgroupParent": "/docker",

4.3 修改eBPF程序

接下来,修改cpu_monitor.c文件,添加cgroup过滤功能:

#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct key_t {
u32 pid;
u32 tid;
char comm[TASK_COMM_LEN];
};
BPF_HASH(counts, struct key_t, u64);
// 定义cgroup路径
const char *target_cgroup = "/docker/1234567890ab";
int kprobe__finish_task_switch(struct pt_regs *ctx, struct task_struct *prev) {
struct task_struct *curr = (struct task_struct *)ctx->rax;
struct key_t key = {.pid = curr->pid, .tid = curr->tgid};
bpf_get_current_comm(&key.comm, sizeof(key.comm));
// 获取进程的cgroup ID
u32 cgroup_id = bpf_get_current_cgroup_id();
// 获取目标cgroup的ID
u32 target_cgroup_id = bpf_get_cgroup_id(target_cgroup);
// 如果进程不在目标cgroup中,则忽略
if (cgroup_id != target_cgroup_id) {
return 0;
}
u64 zero = 0;
u64 *val = counts.lookup_or_init(&key, &zero);
if (val) {
(*val) += 1;
}
return 0;
}

这段代码的关键是:

  • 定义了一个target_cgroup变量,用于存储目标容器的cgroup路径。
  • 使用bpf_get_current_cgroup_id函数获取当前进程的cgroup ID。
  • 使用bpf_get_cgroup_id函数获取目标cgroup的ID。
  • 如果进程的cgroup ID与目标cgroup的ID不匹配,则忽略该进程。

注意: 你需要将target_cgroup变量的值替换为你实际的容器cgroup路径。

4.4 重新编译和运行程序

保存修改后的cpu_monitor.c文件,并重新运行cpu_monitor.py脚本。现在,你应该只会看到目标容器中的进程的CPU占用率了。

5. 更多优化:使用perf event进行更精确的采样

上面的程序使用kprobe__finish_task_switch函数来统计CPU占用率,但这只是一个近似值。如果需要更精确的采样,可以使用perf event。

5.1 什么是perf event?

perf event是Linux内核提供的一种性能分析工具,可以用于测量各种硬件和软件事件,例如CPU周期、指令数、缓存命中率等。eBPF可以与perf event结合使用,实现更精确的性能分析。

5.2 修改eBPF程序

修改cpu_monitor.c文件,使用perf event来统计CPU占用率:

#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct key_t {
u32 pid;
u32 tid;
char comm[TASK_COMM_LEN];
};
BPF_HASH(counts, struct key_t, u64);
// 定义perf event
BPF_PERF_EVENT(cpu_cycles, struct pt_regs, BPF_SAMPLE_CPU) {
struct task_struct *curr = (struct task_struct *)bpf_get_current_task();
struct key_t key = {.pid = curr->pid, .tid = curr->tgid};
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 zero = 0;
u64 *val = counts.lookup_or_init(&key, &zero);
if (val) {
(*val) += 1;
}
return 0;
}

这段代码的关键是:

  • 使用BPF_PERF_EVENT宏定义了一个名为cpu_cycles的perf event,用于测量CPU周期。
  • cpu_cycles事件处理函数中,获取当前进程的PID、TID和进程名,并增加对应进程的CPU占用计数。

5.3 修改Python脚本

修改cpu_monitor.py脚本,配置perf event:

#!/usr/bin/env python3
from bcc import BPF
import time
# 加载eBPF程序
b = BPF(src_file="cpu_monitor.c")
# attach perf event
b["cpu_cycles"].enable()
# 打印哈希表数据
while True:
time.sleep(2)
print("\n{:<6} {:<6} {:<16} {:<8}".format("PID", "TID", "COMM", "COUNT"))
for k, v in sorted(b["counts"].items(), key=lambda counts: counts[1].value):
print("{:<6} {:<6} {:<16} {:<8}".format(k.pid, k.tid, k.comm.decode('utf-8', 'replace'), v.value))
b["counts"].clear()

这段代码的关键是:

  • 使用b["cpu_cycles"].enable()函数启用cpu_cycles perf event。

5.4 重新编译和运行程序

保存修改后的cpu_monitor.ccpu_monitor.py文件,并重新运行cpu_monitor.py脚本。现在,你应该可以获得更精确的CPU占用率数据了。

6. 总结与展望

通过本文的学习,你已经掌握了如何使用eBPF监控容器的CPU占用率。eBPF的强大之处在于它的灵活性和可扩展性。你可以根据自己的需求,编写自定义的eBPF程序,监控各种系统指标,并进行精细化的资源管理和性能优化。

eBPF的未来充满希望。随着技术的不断发展,eBPF将在云计算、容器化、安全分析等领域发挥越来越重要的作用。希望本文能够帮助你入门eBPF,并在未来的工作中充分利用这项强大的技术。

一些额外的思考:

  • 动态调整资源限制: 基于eBPF监控数据,可以实现自动化的资源限制调整,例如,当容器的CPU占用率超过阈值时,自动增加CPU配额。
  • 性能瓶颈分析: 结合其他eBPF工具,可以深入分析容器的性能瓶颈,例如,找出占用CPU时间最多的函数或代码段。
  • 安全监控: 利用eBPF检测容器中的异常行为,例如,未授权的网络连接、恶意文件访问等。

希望这些思考能够帮助你更好地理解eBPF的应用前景,并激发你更多的创新灵感。

容器观测者 eBPF容器监控CPU占用率

评论点评

打赏赞助
sponsor

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

分享

QRcode

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