WEBKT

巧用eBPF:无需修改内核,精准监控进程网络连接延迟

25 0 0 0

为什么选择eBPF?

监控网络连接延迟的原理

实践:使用 eBPF 监控进程网络连接延迟

1. 准备工作

2. 编写 eBPF 程序 (Python)

3. 运行 eBPF 程序

4. 查看结果

改进和扩展

总结

在现代微服务架构和云原生环境中,监控应用程序的网络性能至关重要。网络延迟是影响用户体验的关键因素之一。本文将介绍如何使用eBPF(extended Berkeley Packet Filter)技术,在不修改内核代码的前提下,精准监控特定进程的网络连接延迟,具体来说,就是从进程发起连接到连接建立成功的时间间隔。

为什么选择eBPF?

eBPF 是一种革命性的技术,它允许用户在内核空间安全地运行自定义代码,而无需修改内核源代码或加载内核模块。这为网络监控、安全分析、性能调优等领域带来了前所未有的灵活性和效率。

相比传统的网络监控方法,eBPF 具有以下优势:

  • 高性能: eBPF 程序运行在内核空间,可以高效地捕获和处理网络数据包,避免了用户态和内核态之间频繁的数据拷贝。
  • 安全性: eBPF 程序在运行前会经过验证器的严格检查,确保其不会导致系统崩溃或安全漏洞。
  • 灵活性: eBPF 允许用户自定义监控逻辑,可以根据实际需求灵活地选择要监控的网络事件和数据。
  • 非侵入性: 无需修改内核代码,避免了潜在的兼容性问题和维护成本。

监控网络连接延迟的原理

要监控进程的网络连接延迟,我们需要在以下两个关键时刻进行时间戳记录:

  1. 连接发起时: 当进程调用 connect() 系统调用发起连接时,记录当前时间戳。
  2. 连接建立成功时: 当 TCP 三次握手完成,连接建立成功时,记录当前时间戳。

然后,通过计算两个时间戳之间的差值,即可得到连接建立的延迟。

eBPF 提供了强大的跟踪能力,可以通过以下两种方式来捕获上述事件:

  • kprobes: kprobes 允许我们在内核函数的入口和出口处插入探针,执行自定义代码。我们可以使用 kprobes 分别在 sys_connect 函数的入口和 tcp_v4_connect (或 tcp_v6_connect,取决于IP协议版本) 的返回处记录时间戳。
  • tracepoints: tracepoints 是内核中预先定义的跟踪点,可以在特定事件发生时触发。我们可以使用 tcp:tcp_connect tracepoint 来捕获 TCP 连接建立成功的事件。

实践:使用 eBPF 监控进程网络连接延迟

下面是一个使用 kprobesBPF_HASH map 来监控进程网络连接延迟的示例代码。这个例子使用 kprobes 捕获 sys_connect 入口和 tcp_v4_connect 的出口,然后计算延迟。为了区分不同的连接,使用一个 BPF_HASH map 将 socket file descriptor 和连接发起时间关联起来。

1. 准备工作

  • 安装 bcc: bcc (BPF Compiler Collection) 是一个用于创建 eBPF 程序的工具包,提供了 Python 和 Lua 接口,简化了 eBPF 程序的开发和部署。

    # Debian/Ubuntu
    sudo apt-get update
    sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
    # CentOS/Fedora
    sudo yum install bpfcc-tools kernel-devel-$(uname -r)
  • 安装 libbpf: 一些更新的eBPF功能可能需要libbpf。

    # Debian/Ubuntu (可能需要从 backports 安装)
    sudo apt-get install libbpf-dev
    # CentOS/Fedora
    sudo yum install libbpf-devel
  • 确定目标进程的 PID: 使用 ps 命令或 top 命令找到要监控的进程的 PID。

2. 编写 eBPF 程序 (Python)

#!/usr/bin/env python
from bcc import BPF
import argparse
import time
# 定义参数解析
parser = argparse.ArgumentParser(description="Monitor network connection latency for a specific process.")
parser.add_argument("-p", "--pid", type=int, required=True, help="The PID of the process to monitor.")
args = parser.parse_args()
pid = args.pid
# eBPF 程序的 C 代码
program = """
#include <uapi/linux/ptrace.h>
#include <linux/socket.h>
#include <net/sock.h>
#include <linux/in.h>
struct connect_key {
int pid;
int fd;
};
BPF_HASH(connect_start, struct connect_key, u64);
// kprobe: sys_connect 入口
int kprobe__sys_connect(struct pt_regs *ctx, int fd, struct sockaddr *uservaddr, int addrlen) {
struct connect_key key = {.pid = pid, .fd = fd};
u64 ts = bpf_ktime_get_ns();
// 过滤进程 PID
if (pid != PID) {
return 0;
}
connect_start.update(&key, &ts);
return 0;
}
// kretprobe: tcp_v4_connect 出口 (IPv4)
int kretprobe__tcp_v4_connect(struct pt_regs *ctx) {
int ret = PT_REGS_RC(ctx);
struct sock *sk = (struct sock *)PT_REGS_PAR1(ctx);
struct connect_key key = {.pid = pid, .fd = sk->sk_socket->file->f_fd};
u64 *start_ts, delta;
// 过滤进程 PID
if (pid != PID) {
return 0;
}
start_ts = connect_start.lookup(&key);
if (start_ts == NULL) {
return 0; // 没有找到开始时间,说明 connect 调用失败或被取消
}
delta = bpf_ktime_get_ns() - *start_ts;
bpf_trace_printk("PID %d, FD %d, Latency = %llu ns, Return = %d\n", pid, sk->sk_socket->file->f_fd, delta, ret);
connect_start.delete(&key);
return 0;
}
// kretprobe: tcp_v6_connect 出口 (IPv6)
int kretprobe__tcp_v6_connect(struct pt_regs *ctx) {
int ret = PT_REGS_RC(ctx);
struct sock *sk = (struct sock *)PT_REGS_PAR1(ctx);
struct connect_key key = {.pid = pid, .fd = sk->sk_socket->file->f_fd};
u64 *start_ts, delta;
// 过滤进程 PID
if (pid != PID) {
return 0;
}
start_ts = connect_start.lookup(&key);
if (start_ts == NULL) {
return 0; // 没有找到开始时间,说明 connect 调用失败或被取消
}
delta = bpf_ktime_get_ns() - *start_ts;
bpf_trace_printk("PID %d, FD %d, Latency = %llu ns, Return = %d\n", pid, sk->sk_socket->file->f_fd, delta, ret);
connect_start.delete(&key);
return 0;
}
""
# 替换 PID
program = program.replace("PID", str(pid))
# 加载 eBPF 程序
bpf = BPF(text=program)
# 附加 kprobes
bpf.attach_kprobe(event="sys_connect", fn_name="kprobe__sys_connect")
bpf.attach_kretprobe(event="tcp_v4_connect", fn_name="kretprobe__tcp_v4_connect")
bpf.attach_kretprobe(event="tcp_v6_connect", fn_name="kretprobe__tcp_v6_connect")
# 打印输出
print("Monitoring network connection latency for PID %d...\n" % pid)
# 循环读取输出
try:
while True:
time.sleep(0.1)
for line in bpf.trace_fields():
print(line)
except KeyboardInterrupt:
pass

代码解释:

  • BPF_HASH(connect_start, struct connect_key, u64) 定义一个 BPF_HASH map,用于存储连接发起的时间戳。Key 为 connect_key 结构体,包含 PID 和文件描述符 FD,Value 为时间戳 (u64)。
  • kprobe__sys_connect sys_connect 函数的入口探针。它会创建一个 connect_key,并以当前时间戳作为值,存储到 connect_start map 中。
  • kretprobe__tcp_v4_connectkretprobe__tcp_v6_connect tcp_v4_connecttcp_v6_connect 函数的出口探针。这两个函数分别处理 IPv4 和 IPv6 的连接。探针首先根据 socket 获取文件描述符,构造 connect_key,然后在 connect_start map 中查找对应的时间戳。如果找到,则计算延迟,并使用 bpf_trace_printk 打印输出。最后,从 map 中删除该条目。
  • bpf_trace_printk 一个特殊的 eBPF 函数,用于将数据输出到内核跟踪缓冲区。可以使用 cat /sys/kernel/debug/tracing/trace_pipe 命令查看输出。

3. 运行 eBPF 程序

保存上述代码为 conn_latency.py,然后运行:

sudo python conn_latency.py -p <PID>

<PID> 替换为实际要监控的进程的 PID。

4. 查看结果

程序运行后,会将监控结果输出到控制台。例如:

Monitoring network connection latency for PID 12345...
(12345, 3, 123456789, 0)

这些数字的含义是: PID, 文件描述符 FD, 延迟 (纳秒), 返回值. 你也可以通过 cat /sys/kernel/debug/tracing/trace_pipe 查看更原始的输出。

改进和扩展

  • 使用 tracepoints 可以尝试使用 tcp:tcp_connect tracepoint 替代 kretprobe__tcp_v4_connectkretprobe__tcp_v6_connect,简化代码。
  • 聚合数据: 可以将延迟数据聚合到 BPF_HISTOGRAM map 中,生成延迟分布图。
  • 用户态程序: 可以编写用户态程序,从内核跟踪缓冲区读取数据,并进行更复杂的分析和可视化。
  • 错误处理: 在 eBPF 程序中添加错误处理逻辑,例如检查 connect 系统调用的返回值,判断连接是否建立成功。
  • 更精确的时间戳: 使用 bpf_ktime_get_boot_ns() 可以获得更精确的时间戳,不受系统时间调整的影响。
  • 支持 UDP: 目前的代码只支持 TCP 连接。可以添加对 UDP 连接的支持,监控 UDP 包的延迟。

总结

本文介绍了如何使用 eBPF 技术,在不修改内核代码的前提下,监控进程的网络连接延迟。通过使用 kprobes 或 tracepoints 捕获网络事件,并使用 BPF map 存储和处理数据,我们可以实现对网络性能的精细化监控。eBPF 的强大功能为网络监控、安全分析和性能调优提供了无限可能。希望本文能够帮助读者理解 eBPF 的基本原理和应用,并将其应用到实际场景中。

eBPF探索者 eBPF网络监控性能分析

评论点评

打赏赞助
sponsor

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

分享

QRcode

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