WEBKT

告别抓包!用eBPF自制网络流量监控神器,性能分析、故障排查一把抓

66 0 0 0

告别抓包!用eBPF自制网络流量监控神器,性能分析、故障排查一把抓

1. 什么是eBPF?为什么选择eBPF?

2. 准备工作

3. 编写eBPF程序:监控TCP连接

4. 扩展功能:流量统计、延迟监控

5. 可视化:将数据展示出来

6. 总结与展望

告别抓包!用eBPF自制网络流量监控神器,性能分析、故障排查一把抓

作为一名SRE,我深知网络性能监控的重要性。传统的网络监控方法,比如tcpdump抓包,虽然功能强大,但往往存在性能瓶颈,尤其是在高流量环境下。有没有一种更高效、更轻量级的方案呢?答案是肯定的,那就是eBPF!

今天,我就手把手教你如何利用eBPF构建一个高性能的网络流量监控工具,让你告别繁琐的抓包分析,轻松掌握网络流量的实时动态,无论是性能分析还是故障排查,都能得心应手。

1. 什么是eBPF?为什么选择eBPF?

eBPF(extended Berkeley Packet Filter)是一种革命性的内核技术,它允许用户在内核中安全地运行自定义代码,而无需修改内核源码或加载内核模块。简单来说,你可以把它想象成一个“内核JavaScript”,通过编写eBPF程序,你可以hook内核的各种事件,比如网络数据包的收发、系统调用等等,从而实现各种强大的功能。

为什么选择eBPF进行网络流量监控?

  • 高性能: eBPF程序在内核中运行,直接访问内核数据,避免了用户态和内核态之间频繁的数据拷贝,性能非常高。
  • 低开销: eBPF程序运行在沙箱环境中,并且经过严格的验证,保证了内核的安全性,同时也降低了系统开销。
  • 灵活性: eBPF允许你自定义监控逻辑,可以根据自己的需求灵活地定制监控指标和分析方法。
  • 实时性: eBPF可以实时地捕获和分析网络数据包,让你能够及时地发现和解决网络问题。

2. 准备工作

在开始之前,你需要准备以下环境:

  • Linux内核: 建议使用4.14及以上版本的内核,以获得更好的eBPF支持。

  • bcc工具: bcc(BPF Compiler Collection)是一套用于创建eBPF程序的工具,它提供了Python和C++接口,可以方便地编写和调试eBPF程序。你可以通过以下命令安装bcc:

    sudo apt-get update
    sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
  • Python: bcc需要Python环境,建议使用Python 3。

3. 编写eBPF程序:监控TCP连接

接下来,我们将编写一个简单的eBPF程序,用于监控TCP连接的建立、关闭和数据传输事件。

from bcc import BPF
# 定义eBPF程序
program = """
#include <uapi/linux/tcp.h>
struct key_t {
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u32 pid;
char comm[TASK_COMM_LEN];
};
BPF_HASH(connect_counter, struct key_t, u64);
BPF_HASH(disconnect_counter, struct key_t, u64);
BPF_HASH(data_counter, struct key_t, u64);
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
struct key_t key = {};
key.saddr = sk->__sk_common.skc_rcv_saddr;
key.daddr = sk->__sk_common.skc_daddr;
key.sport = sk->__sk_common.skc_num;
key.dport = sk->__sk_common.skc_dport;
key.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 zero = 0;
u64 *value = connect_counter.lookup_or_init(&key, &zero);
(*value)++;
return 0;
}
int kprobe__tcp_close(struct pt_regs *ctx, struct sock *sk) {
struct key_t key = {};
key.saddr = sk->__sk_common.skc_rcv_saddr;
key.daddr = sk->__sk_common.skc_daddr;
key.sport = sk->__sk_common.skc_num;
key.dport = sk->__sk_common.skc_dport;
key.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 zero = 0;
u64 *value = disconnect_counter.lookup_or_init(&key, &zero);
(*value)++;
return 0;
}
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
struct key_t key = {};
key.saddr = sk->__sk_common.skc_rcv_saddr;
key.daddr = sk->__sk_common.skc_daddr;
key.sport = sk->__sk_common.skc_num;
key.dport = sk->__sk_common.skc_dport;
key.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 zero = 0;
u64 *value = data_counter.lookup_or_init(&key, &zero);
(*value) += size;
return 0;
}
"""
# 加载eBPF程序
bpf = BPF(text=program)
# 打印头部信息
print("%-16s %-16s %-6s %-6s %-6s %s" % ("SRC ADDR", "DST ADDR", "SPORT", "DPORT", "PID", "COMMAND"))
# 循环打印监控数据
while True:
try:
for table_name in ["connect_counter", "disconnect_counter", "data_counter"]:
table = bpf[table_name]
for k, v in table.items():
src_addr = "%d.%d.%d.%d" % (k.saddr & 0xff, (k.saddr >> 8) & 0xff, (k.saddr >> 16) & 0xff, (k.saddr >> 24) & 0xff)
dst_addr = "%d.%d.%d.%d" % (k.daddr & 0xff, (k.daddr >> 8) & 0xff, (k.daddr >> 16) & 0xff, (k.daddr >> 24) & 0xff)
print("%-16s %-16s %-6d %-6d %-6d %s %s" % (src_addr, dst_addr, k.sport, k.dport, k.pid, k.comm.decode('utf-8', 'replace'), table_name))
table.clear()
time.sleep(1)
except KeyboardInterrupt:
exit()

代码解释:

  1. 引入bcc库: from bcc import BPF 引入bcc库,用于加载和运行eBPF程序。
  2. 定义eBPF程序: 使用字符串定义eBPF程序,程序使用C语言编写,包含以下几个部分:
    • 包含头文件: #include <uapi/linux/tcp.h> 包含TCP协议相关的头文件。
    • 定义Key结构体: struct key_t 定义一个结构体,用于存储连接的五元组信息(源IP、目的IP、源端口、目的端口、PID)以及进程名。
    • 定义BPF_HASH: BPF_HASH 定义三个哈希表,分别用于存储连接建立、连接关闭和数据传输的计数信息。哈希表的Key是key_t结构体,Value是u64类型的计数器。
    • 定义kprobe函数: kprobe__tcp_v4_connectkprobe__tcp_closekprobe__tcp_sendmsg分别定义了三个kprobe函数,用于hook内核的tcp_v4_connecttcp_closetcp_sendmsg函数。当这些函数被调用时,对应的kprobe函数会被执行。
      • tcp_v4_connect:在TCP连接建立时被调用,kprobe函数会将连接的五元组信息存储到connect_counter哈希表中,并将计数器加1。
      • tcp_close:在TCP连接关闭时被调用,kprobe函数会将连接的五元组信息存储到disconnect_counter哈希表中,并将计数器加1。
      • tcp_sendmsg:在TCP发送数据时被调用,kprobe函数会将连接的五元组信息存储到data_counter哈希表中,并将计数器加上发送的数据大小。
  3. 加载eBPF程序: bpf = BPF(text=program) 加载eBPF程序,并将其编译成内核可执行的指令。
  4. 循环打印监控数据: 使用while循环,每隔1秒钟打印一次监控数据。
    • 遍历三个哈希表(connect_counterdisconnect_counterdata_counter),获取连接的五元组信息和计数器值。
    • 将IP地址和端口号转换为可读的字符串。
    • 打印监控数据,包括源IP、目的IP、源端口、目的端口、PID、进程名和事件类型。
    • 清空哈希表,以便下一次循环时获取新的数据。

运行程序:

将以上代码保存为tcp_monitor.py,然后在终端中运行:

sudo python3 tcp_monitor.py

你将会看到类似以下的输出:

SRC ADDR DST ADDR SPORT DPORT PID COMMAND
192.168.1.100 114.114.114.114 54321 53 1234 python3 connect_counter
192.168.1.100 114.114.114.114 54321 53 1234 python3 data_counter
192.168.1.100 114.114.114.114 54321 53 1234 python3 disconnect_counter

这个程序可以实时监控TCP连接的建立、关闭和数据传输事件,并输出连接的五元组信息和进程名。你可以根据自己的需求,修改和扩展这个程序,实现更复杂的监控功能。

4. 扩展功能:流量统计、延迟监控

除了监控TCP连接,eBPF还可以用于实现更高级的网络监控功能,比如流量统计和延迟监控。

4.1 流量统计

你可以使用eBPF来统计每个连接或每个进程的流量,从而分析网络流量的分布情况。

修改上面的代码,添加一个packet_counter哈希表,用于存储每个连接收发的数据包数量。在kprobe__tcp_sendmsg函数中,将数据包数量加1。然后,在循环打印监控数据时,将数据包数量也打印出来。

BPF_HASH(packet_counter, struct key_t, u64);
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
struct key_t key = {};
key.saddr = sk->__sk_common.skc_rcv_saddr;
key.daddr = sk->__sk_common.skc_daddr;
key.sport = sk->__sk_common.skc_num;
key.dport = sk->__sk_common.skc_dport;
key.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 zero = 0;
u64 *value = packet_counter.lookup_or_init(&key, &zero);
(*value)++;
u64 *data_value = data_counter.lookup_or_init(&key, &zero);
(*data_value) += size;
return 0;
}
for table_name in ["connect_counter", "disconnect_counter", "data_counter", "packet_counter"]:
table = bpf[table_name]
for k, v in table.items():
src_addr = "%d.%d.%d.%d" % (k.saddr & 0xff, (k.saddr >> 8) & 0xff, (k.saddr >> 16) & 0xff, (k.saddr >> 24) & 0xff)
dst_addr = "%d.%d.%d.%d" % (k.daddr & 0xff, (k.daddr >> 8) & 0xff, (k.daddr >> 16) & 0xff, (k.daddr >> 24) & 0xff)
print("%-16s %-16s %-6d %-6d %-6d %s %s %d" % (src_addr, dst_addr, k.sport, k.dport, k.pid, k.comm.decode('utf-8', 'replace'), table_name, v.value))
table.clear()

4.2 延迟监控

你可以使用eBPF来测量网络数据包的延迟,从而分析网络性能。

可以使用kprobe__tcp_sendmsgkretprobe__tcp_recvmsg函数来测量数据包的发送和接收时间,然后计算延迟。

BPF_HASH(send_time, struct key_t, u64);
BPF_HASH(latency, struct key_t, u64);
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
struct key_t key = {};
key.saddr = sk->__sk_common.skc_rcv_saddr;
key.daddr = sk->__sk_common.skc_daddr;
key.sport = sk->__sk_common.skc_num;
key.dport = sk->__sk_common.skc_dport;
key.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 now = bpf_ktime_get_ns();
send_time.update(&key, &now);
return 0;
}
int kretprobe__tcp_recvmsg(struct pt_regs *ctx) {
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
struct key_t key = {};
key.saddr = sk->__sk_common.skc_rcv_saddr;
key.daddr = sk->__sk_common.skc_daddr;
key.sport = sk->__sk_common.skc_num;
key.dport = sk->__sk_common.skc_dport;
key.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&key.comm, sizeof(key.comm));
u64 *sent = send_time.lookup(&key);
if (sent) {
u64 now = bpf_ktime_get_ns();
u64 lat = now - *sent;
latency.update(&key, &lat);
send_time.delete(&key);
}
return 0;
}
for table_name in ["latency"]:
table = bpf[table_name]
for k, v in table.items():
src_addr = "%d.%d.%d.%d" % (k.saddr & 0xff, (k.saddr >> 8) & 0xff, (k.saddr >> 16) & 0xff, (k.saddr >> 24) & 0xff)
dst_addr = "%d.%d.%d.%d" % (k.daddr & 0xff, (k.daddr >> 8) & 0xff, (k.daddr >> 16) & 0xff, (k.daddr >> 24) & 0xff)
print("%-16s %-16s %-6d %-6d %-6d %s %s %d ns" % (src_addr, dst_addr, k.sport, k.dport, k.pid, k.comm.decode('utf-8', 'replace'), table_name, v.value))
table.clear()

5. 可视化:将数据展示出来

有了监控数据,如何将它们展示出来呢?你可以使用各种可视化工具,比如Grafana,将eBPF程序收集到的数据以图表的形式展示出来,让你更直观地了解网络性能状况。

  1. 将数据发送到Prometheus: Prometheus是一套开源的监控和报警系统,可以方便地收集和存储监控数据。你可以使用bcc提供的prometheus_output工具,将eBPF程序收集到的数据发送到Prometheus。
  2. 配置Grafana: Grafana是一套开源的数据可视化工具,可以方便地创建各种图表,展示Prometheus中存储的监控数据。你可以在Grafana中配置Prometheus数据源,然后创建各种图表,展示网络流量、延迟等指标。

6. 总结与展望

eBPF为网络监控带来了革命性的改变,它提供了高性能、低开销、灵活和实时的监控能力,让你能够更好地了解和管理网络。

通过本文的介绍,相信你已经掌握了如何使用eBPF构建一个简单的网络流量监控工具。你可以根据自己的需求,扩展和定制这个工具,实现更复杂的监控功能。

未来,eBPF将在网络监控领域发挥更大的作用,它将成为网络工程师和SRE的必备技能。让我们一起拥抱eBPF,打造更智能、更高效的网络监控系统!

一些额外的思考:

  • 安全性: 虽然eBPF程序在沙箱环境中运行,并且经过严格的验证,但仍然存在一定的安全风险。你需要仔细审查eBPF程序的代码,确保其不会对内核造成损害。
  • 性能优化: eBPF程序的性能非常重要,你需要尽可能地优化代码,减少其对内核的影响。可以使用bcc提供的perf工具,分析eBPF程序的性能瓶颈。
  • 可维护性: eBPF程序的代码可维护性也很重要,你需要编写清晰、简洁的代码,并添加适当的注释,方便日后维护和修改。

希望这篇文章能够帮助你入门eBPF网络监控,如果你有任何问题或建议,欢迎在评论区留言。

NetHunter eBPF网络监控流量分析

评论点评

打赏赞助
sponsor

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

分享

QRcode

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