WEBKT

使用 eBPF 精准定位网络延迟?这几个技巧你得知道!

31 0 0 0

使用 eBPF 精准定位网络延迟?这几个技巧你得知道!

1. eBPF 基础知识快速回顾

2. 监控网络延迟的关键指标

3. 使用 eBPF 监控 TCP 连接延迟

4. 使用 eBPF 识别网络瓶颈

5. 使用 eBPF 分析应用程序延迟

6. eBPF 工具链推荐

7. eBPF 的局限性

8. 总结

使用 eBPF 精准定位网络延迟?这几个技巧你得知道!

作为一名网络工程师,我经常被问到如何快速定位网络延迟问题。传统的网络监控工具往往只能提供宏观的性能指标,对于复杂网络环境下发生的偶发性延迟,常常束手无策。直到我接触了 eBPF (Extended Berkeley Packet Filter) 技术,才发现它简直是网络性能分析的瑞士军刀!

eBPF 允许我们在内核中安全地运行自定义代码,实时监控和分析网络数据包。这意味着我们可以捕获到传统工具无法触及的底层信息,例如:每个数据包的精确时间戳、内核中的排队延迟、以及特定网络事件发生时的上下文信息。通过巧妙地利用这些信息,我们可以将网络延迟的根源精确地定位到具体的内核函数、网络设备甚至应用程序代码。

今天,我就结合我自己的实践经验,分享一些使用 eBPF 监控和分析网络延迟的实用技巧,希望能帮助你提升网络故障排除效率。

1. eBPF 基础知识快速回顾

如果你还不熟悉 eBPF,不用担心,这里我们快速回顾一下几个关键概念:

  • eBPF 程序: 用 C 编写,然后编译成 BPF 字节码,在内核中运行。它可以在各种事件发生时被触发,例如:接收到网络包、系统调用发生等。
  • BPF Map: eBPF 程序和用户空间程序之间共享数据的桥梁。你可以将 eBPF 程序收集到的数据存储在 BPF Map 中,然后用户空间程序可以读取这些数据进行分析和可视化。
  • Probe (探针): 用于在内核或用户空间的特定位置插入 eBPF 程序。常见的 Probe 类型包括 kprobe (内核探针)、uprobe (用户空间探针) 和 tracepoint (跟踪点)。
  • Tail Call (尾调用): 允许一个 eBPF 程序调用另一个 eBPF 程序,这可以帮助我们构建更复杂的分析逻辑。

简单来说,eBPF 的工作流程就是:定义 eBPF 程序 -> 加载到内核 -> 附加到 Probe -> 收集数据到 BPF Map -> 用户空间程序读取和分析数据。

2. 监控网络延迟的关键指标

在开始编写 eBPF 程序之前,我们需要明确要监控哪些关键指标。以下是一些常用的网络延迟指标:

  • 传输延迟 (Transmission Delay): 数据包从发送端发出到接收端接收所需的时间。可以使用 tcpdumpwireshark 等工具抓包分析,但 eBPF 可以提供更细粒度的信息。
  • 传播延迟 (Propagation Delay): 信号在传输介质中传播所需的时间。这个延迟通常由物理距离和传输介质的特性决定,相对稳定。
  • 处理延迟 (Processing Delay): 路由器或交换机处理数据包所需的时间。eBPF 可以帮助我们分析内核协议栈中的处理延迟。
  • 排队延迟 (Queuing Delay): 数据包在队列中等待处理所需的时间。这是网络拥塞的主要原因之一,eBPF 可以帮助我们识别拥塞点。

通过 eBPF,我们可以分别监控这些延迟,从而更准确地定位延迟的根源。

3. 使用 eBPF 监控 TCP 连接延迟

TCP 连接延迟是衡量网络性能的重要指标。我们可以使用 eBPF 来监控 TCP 连接建立、数据传输和断开过程中的延迟。

3.1 监控 TCP 三次握手延迟

TCP 三次握手是建立 TCP 连接的关键步骤。我们可以使用 kprobe 监控 tcp_v4_connecttcp_rcv_state_process 函数,来测量 SYN、SYN-ACK 和 ACK 包的往返时间 (RTT)。

// eBPF program to monitor TCP handshake latency
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/tcp.h>
struct handshake_data {
u64 ts;
u32 saddr;
u32 daddr;
u16 dport;
};
BPF_HASH(start, struct sock *, struct handshake_data);
BPF_HISTOGRAM(latency);
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
u64 ts = bpf_ktime_get_ns();
struct handshake_data data = {
.ts = ts,
.saddr = sk->__sk_common.skc_rcv_saddr,
.daddr = sk->__sk_common.skc_daddr,
.dport = sk->__sk_common.skc_dport,
};
start.update(&sk, &data);
return 0;
}
int kprobe__tcp_rcv_state_process(struct pt_regs *ctx, struct sock *sk) {
if (sk->sk_state == TCP_ESTABLISHED) {
struct handshake_data *data = start.lookup(&sk);
if (data) {
u64 latency_ns = bpf_ktime_get_ns() - data->ts;
latency.increment(bpf_log2l(latency_ns / 1000)); // us
start.delete(&sk);
}
}
return 0;
}

这段代码的思路是:

  1. tcp_v4_connect 函数入口处,记录当前时间戳、源 IP 地址、目标 IP 地址和目标端口,并以 sock 指针为键存储到 start BPF Map 中。
  2. tcp_rcv_state_process 函数入口处,检查 socket 状态是否变为 TCP_ESTABLISHED,如果是,则从 start BPF Map 中查找对应的记录。
  3. 如果找到记录,则计算握手延迟,并将其存储到 latency BPF Histogram 中。

通过分析 latency BPF Histogram,我们可以了解 TCP 握手延迟的分布情况,从而判断是否存在网络问题。

3.2 监控 TCP 数据传输延迟

除了握手延迟,数据传输延迟也是影响用户体验的关键因素。我们可以使用 kprobe 监控 tcp_sendmsgtcp_cleanup_rbuf 函数,来测量数据包从发送到确认的时间。

// eBPF program to monitor TCP data transfer latency
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/tcp.h>
struct data_send_data {
u64 ts;
u32 saddr;
u32 daddr;
u16 dport;
u32 len; // 发送数据长度
};
BPF_HASH(send_start, struct sock *, struct data_send_data);
BPF_HISTOGRAM(data_latency);
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
u64 ts = bpf_ktime_get_ns();
struct data_send_data data = {
.ts = ts,
.saddr = sk->__sk_common.skc_rcv_saddr,
.daddr = sk->__sk_common.skc_daddr,
.dport = sk->__sk_common.skc_dport,
.len = size,
};
send_start.update(&sk, &data);
return 0;
}
int kprobe__tcp_cleanup_rbuf(struct pt_regs *ctx, struct sock *sk, int copied) {
struct data_send_data *data = send_start.lookup(&sk);
if (data) {
u64 latency_ns = bpf_ktime_get_ns() - data->ts;
data_latency.increment(bpf_log2l(latency_ns / 1000)); // us
send_start.delete(&sk);
}
return 0;
}

这段代码与监控握手延迟的代码类似,主要区别在于:

  1. 监控的函数不同,这里监控的是 tcp_sendmsgtcp_cleanup_rbuf 函数。
  2. 记录了发送数据的长度 len,可以用于分析不同大小的数据包的延迟情况。

3.3 用户空间程序

以上两个 eBPF 程序都需要一个用户空间程序来加载、运行和读取数据。一个简单的 Python 示例:

from bcc import BPF
import time
# Load the eBPF program
b = BPF(src_file="tcp_latency.c") # 替换为你的 eBPF 程序文件名
# Attach the probes
b.attach_kprobe(event="tcp_v4_connect", fn_name="kprobe__tcp_v4_connect")
b.attach_kprobe(event="tcp_rcv_state_process", fn_name="kprobe__tcp_rcv_state_process")
# 或者
b.attach_kprobe(event="tcp_sendmsg", fn_name="kprobe__tcp_sendmsg")
b.attach_kprobe(event="tcp_cleanup_rbuf", fn_name="kprobe__tcp_cleanup_rbuf")
# Print the latency histogram every 5 seconds
try:
while True:
time.sleep(5)
print("\nHandshake Latency (us):")
b["latency"].print_log2_hist() # 或者 b["data_latency"].print_log2_hist()
b["latency"].clear() # 或者 b["data_latency"].clear()
except KeyboardInterrupt:
exit()

这段 Python 代码使用 bcc 库来加载和运行 eBPF 程序,并将探针附加到相应的内核函数。然后,它每 5 秒打印一次延迟直方图,并清除直方图数据。

4. 使用 eBPF 识别网络瓶颈

网络瓶颈是指网络中限制数据传输速率的环节。识别网络瓶颈是解决网络延迟问题的关键。

4.1 监控网络接口的队列长度

当网络接口的队列长度过长时,说明数据包在等待发送,这可能是网络瓶颈的征兆。我们可以使用 eBPF 监控 sch_enqueuesch_dequeue 函数,来测量网络接口的队列长度。

// eBPF program to monitor network interface queue length
#include <uapi/linux/ptrace.h>
#include <net/sch_generic.h>
#include <net/dev.h>
struct queue_data {
u64 enq_ts;
u64 deq_ts;
};
BPF_HASH(queue_times, struct sk_buff *, struct queue_data);
BPF_HISTOGRAM(queue_latency);
int kprobe__sch_enqueue(struct pt_regs *ctx, struct sk_buff *skb, struct Qdisc *q) {
u64 ts = bpf_ktime_get_ns();
struct queue_data data = {.enq_ts = ts};
queue_times.update(&skb, &data);
return 0;
}
int kprobe__sch_dequeue(struct pt_regs *ctx, struct sk_buff *skb, struct Qdisc *q) {
struct queue_data *data = queue_times.lookup(&skb);
if (data) {
u64 latency_ns = bpf_ktime_get_ns() - data->enq_ts;
queue_latency.increment(bpf_log2l(latency_ns / 1000)); // us
queue_times.delete(&skb);
}
return 0;
}

这段代码的思路是:

  1. sch_enqueue 函数入口处,记录数据包入队的时间戳,并以 sk_buff 指针为键存储到 queue_times BPF Map 中。
  2. sch_dequeue 函数入口处,从 queue_times BPF Map 中查找对应的记录。
  3. 如果找到记录,则计算排队延迟,并将其存储到 queue_latency BPF Histogram 中。

通过分析 queue_latency BPF Histogram,我们可以了解网络接口的排队延迟情况,从而判断是否存在拥塞。

4.2 监控 CPU 使用率

CPU 使用率过高也可能导致网络延迟。我们可以使用 eBPF 监控 sched_switch 函数,来测量每个进程的 CPU 使用时间。

// eBPF program to monitor CPU usage per process
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct cpu_data {
u64 ts;
u32 pid;
};
BPF_HASH(cpu_start, u32, struct cpu_data);
BPF_HISTOGRAM(cpu_usage);
int kprobe__sched_switch(struct pt_regs *ctx, bool preempt, struct task_struct *prev, struct task_struct *next) {
u32 prev_pid = prev->pid;
u32 next_pid = next->pid;
// 处理前一个进程
struct cpu_data *prev_data = cpu_start.lookup(&prev_pid);
if (prev_data) {
u64 latency_ns = bpf_ktime_get_ns() - prev_data->ts;
cpu_usage.increment(bpf_log2l(latency_ns / 1000)); // us
cpu_start.delete(&prev_pid);
}
// 记录下一个进程的时间戳
struct cpu_data next_data = {
.ts = bpf_ktime_get_ns(),
.pid = next_pid,
};
cpu_start.update(&next_pid, &next_data);
return 0;
}

这段代码的思路是:

  1. sched_switch 函数入口处,记录前一个进程的 PID 和当前时间戳,并以 PID 为键存储到 cpu_start BPF Map 中。
  2. 计算前一个进程的 CPU 使用时间,并将其存储到 cpu_usage BPF Histogram 中。
  3. 记录下一个进程的 PID 和当前时间戳,并以 PID 为键存储到 cpu_start BPF Map 中。

通过分析 cpu_usage BPF Histogram,我们可以了解每个进程的 CPU 使用情况,从而判断是否存在 CPU 瓶颈。

5. 使用 eBPF 分析应用程序延迟

除了网络和系统层面的延迟,应用程序自身的延迟也可能导致网络性能下降。我们可以使用 uprobe 监控应用程序的关键函数,来测量应用程序的延迟。

5.1 监控 Nginx 请求处理延迟

例如,我们可以使用 uprobe 监控 Nginx 的 ngx_http_process_request 函数,来测量 Nginx 处理 HTTP 请求的延迟。

// eBPF program to monitor Nginx request processing latency
#include <uapi/linux/ptrace.h>
struct request_data {
u64 ts;
u32 pid;
};
BPF_HASH(request_start, u32, struct request_data);
BPF_HISTOGRAM(request_latency);
int uprobe__ngx_http_process_request(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
struct request_data data = {
.ts = ts,
.pid = pid,
};
request_start.update(&pid, &data);
return 0;
}
int uretprobe__ngx_http_process_request(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
struct request_data *data = request_start.lookup(&pid);
if (data) {
u64 latency_ns = bpf_ktime_get_ns() - data->ts;
request_latency.increment(bpf_log2l(latency_ns / 1000)); // us
request_start.delete(&pid);
}
return 0;
}

这段代码的思路是:

  1. ngx_http_process_request 函数入口处,记录当前时间戳和 PID,并以 PID 为键存储到 request_start BPF Map 中。
  2. ngx_http_process_request 函数返回时,从 request_start BPF Map 中查找对应的记录。
  3. 如果找到记录,则计算请求处理延迟,并将其存储到 request_latency BPF Histogram 中。

注意: 使用 uprobe 需要知道目标函数的地址。可以使用 objdump -d /usr/local/nginx/sbin/nginx | grep ngx_http_process_request 命令来查找 ngx_http_process_request 函数的地址。

6. eBPF 工具链推荐

  • bcc (BPF Compiler Collection): 一个 Python 库,用于编写、编译和运行 eBPF 程序。它提供了丰富的 API 和示例,是 eBPF 开发的常用工具。
  • bpftrace: 一种高级的 eBPF 跟踪语言,类似于 awkDTrace。它使用简单的语法,可以快速编写 eBPF 程序进行性能分析。
  • kubectl trace: Kubernetes 集群中运行 bpftrace 程序的工具,方便在容器环境中进行性能分析。
  • ply: 另一个 eBPF 工具,使用 Python 编写,目标是提供更简洁的 eBPF 开发体验。

7. eBPF 的局限性

虽然 eBPF 功能强大,但也存在一些局限性:

  • 内核版本依赖: 不同的内核版本支持的 eBPF 功能可能不同。需要根据目标内核版本选择合适的 eBPF 程序。
  • 安全风险: eBPF 程序在内核中运行,如果编写不当,可能会导致系统崩溃或安全漏洞。需要进行充分的测试和验证。
  • 学习曲线: eBPF 涉及内核编程和网络协议等知识,学习曲线相对陡峭。

8. 总结

eBPF 是一种强大的网络性能分析工具,可以帮助我们深入了解网络延迟的根源。通过监控 TCP 连接延迟、识别网络瓶颈和分析应用程序延迟,我们可以快速定位和解决网络问题,提升用户体验。

希望这篇文章能帮助你入门 eBPF,并将其应用到实际的网络性能分析工作中。掌握 eBPF,你就能像一位经验丰富的医生一样,快速诊断和治疗网络疾病!

网络巡诊狮 eBPF网络延迟性能分析

评论点评

打赏赞助
sponsor

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

分享

QRcode

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