WEBKT

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

182 0 0 0

使用 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网络延迟性能分析

评论点评