WEBKT

利用 eBPF 精准追踪 TCP 和 DNS 延迟,揪出网络性能瓶颈

175 0 0 0

网络延迟是影响用户体验的关键因素之一。当网站加载缓慢、视频卡顿或者在线游戏延迟过高时,用户往往会感到沮丧。网络工程师和系统管理员需要快速定位并解决这些问题,而 eBPF(extended Berkeley Packet Filter)提供了一种强大的工具,可以深入内核追踪网络数据包,分析延迟的来源。

本文将重点介绍如何使用 eBPF 跟踪 TCP 三次握手和 DNS 查询的延迟,并利用这些数据来诊断常见的网络瓶颈。

什么是 eBPF?

eBPF 最初是 BSD 内核中的一个数据包过滤器,后来被扩展到可以运行用户自定义的沙箱程序,无需修改内核源码即可动态地向内核添加新的功能。eBPF 程序运行在内核态,但通过了严格的验证,保证安全性和稳定性。

eBPF 的强大之处在于其能够监听内核事件,例如网络数据包的收发、系统调用等。通过编写 eBPF 程序,我们可以捕获这些事件,提取关键信息,并进行实时分析。

eBPF 在网络性能分析中的应用

eBPF 提供了多种工具和框架,可以用于网络性能分析,例如:

  • 跟踪网络数据包: 捕获网络数据包,分析其头部信息,例如源 IP 地址、目标 IP 地址、端口号、协议类型等。
  • 测量延迟: 记录数据包到达和离开内核的时间戳,计算延迟。
  • 统计网络流量: 统计特定类型的数据包的数量、字节数等。
  • 监控 TCP 连接: 跟踪 TCP 连接的状态变化,例如建立连接、关闭连接等。

跟踪 TCP 三次握手延迟

TCP 三次握手是建立 TCP 连接的关键步骤。如果三次握手过程出现延迟,会导致连接建立缓慢,影响用户体验。我们可以使用 eBPF 跟踪三次握手过程,分析延迟的来源。

1. 确定跟踪点

我们需要跟踪以下内核函数:

  • tcp_v4_connect:客户端发起 SYN 包时调用。
  • tcp_v4_syn_recv:服务器收到 SYN 包时调用。
  • tcp_v4_建立连接完成的函数:客户端收到 SYN-ACK 包后,发送 ACK 包时调用 (具体函数名取决于内核版本,可以使用 trace 工具查找)。

2. 编写 eBPF 程序

以下是一个简单的 eBPF 程序,用于跟踪 TCP 三次握手延迟:

#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/tcp.h>
#include <linux/tcp.h>

struct data_t {
  u32 pid;
  u32 saddr;
  u32 daddr;
  u16 sport;
  u16 dport;
  u64 ts;
  char state[8];
};

BPF_PERF_OUTPUT(events);

// 记录 SYN_SENT 的时间
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
  struct data_t data = {};
  data.pid = bpf_get_current_pid_tgid();
  data.saddr = sk->__sk_common.skc_rcv_saddr;
  data.daddr = sk->__sk_common.skc_daddr;
  data.sport = sk->__sk_common.skc_num;
  data.dport = sk->__sk_common.skc_dport;
  data.dport = ntohs(data.dport);
  data.ts = bpf_ktime_get_ns();
  strcpy(data.state, "SYN_SENT");
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

// 记录 SYN_RECV 的时间
int kprobe__tcp_v4_syn_recv(struct pt_regs *ctx, struct sock *sk, struct sk_buff *skb, const struct tcp_request_sock *req) {
  struct data_t data = {};
  data.pid = bpf_get_current_pid_tgid();
  data.saddr = sk->__sk_common.skc_rcv_saddr;
  data.daddr = sk->__sk_common.skc_daddr;
  data.sport = sk->__sk_common.skc_num;
  data.dport = sk->__sk_common.skc_dport;
  data.dport = ntohs(data.dport);
  data.ts = bpf_ktime_get_ns();
  strcpy(data.state, "SYN_RECV");
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

// 记录 ESTABLISHED 的时间 (需要根据内核版本调整函数名)
int kprobe__tcp_建立连接完成的函数(struct pt_regs *ctx, struct sock *sk) {
  struct data_t data = {};
  data.pid = bpf_get_current_pid_tgid();
  data.saddr = sk->__sk_common.skc_rcv_saddr;
  data.daddr = sk->__sk_common.skc_daddr;
  data.sport = sk->__sk_common.skc_num;
  data.dport = sk->__sk_common.skc_dport;
  data.dport = ntohs(data.dport);
  data.ts = bpf_ktime_get_ns();
  strcpy(data.state, "ESTABLISHED");
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

代码解释:

  • #include:包含必要的头文件,例如 ptrace.hsock.htcp.h,这些头文件定义了内核数据结构和函数。
  • struct data_t:定义了一个数据结构,用于存储我们需要的信息,例如 PID、源 IP 地址、目标 IP 地址、端口号、时间戳和连接状态。
  • BPF_PERF_OUTPUT(events):定义了一个 perf 事件输出,用于将数据从内核空间传递到用户空间。
  • kprobe__tcp_v4_connectkprobe__tcp_v4_syn_recvkprobe__tcp_建立连接完成的函数:定义了三个 kprobe 函数,分别用于跟踪 tcp_v4_connecttcp_v4_syn_recvtcp_建立连接完成的函数 函数的调用。在这些函数中,我们提取相关信息,并将其存储到 data_t 结构体中,然后通过 events.perf_submit 将数据发送到用户空间。

编译和加载 eBPF 程序:

可以使用 bcclibbpf 等工具来编译和加载 eBPF 程序。

3. 分析数据

在用户空间,我们可以编写程序来读取 perf 事件,并分析 TCP 三次握手的延迟。例如,我们可以计算 SYN_SENT 到 SYN_RECV 的时间差,以及 SYN_RECV 到 ESTABLISHED 的时间差,从而确定延迟的来源。

跟踪 DNS 查询延迟

DNS 查询是将域名解析为 IP 地址的过程。如果 DNS 查询延迟过高,会导致网站加载缓慢。我们可以使用 eBPF 跟踪 DNS 查询过程,分析延迟的来源。

1. 确定跟踪点

我们需要跟踪以下内核函数:

  • dns_lookup:发起 DNS 查询时调用。
  • dns_query_recv:收到 DNS 响应时调用。

2. 编写 eBPF 程序

以下是一个简单的 eBPF 程序,用于跟踪 DNS 查询延迟:

#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/dns.h>

struct data_t {
  u32 pid;
  u32 saddr;
  u32 daddr;
  u16 sport;
  u16 dport;
  u64 ts_start;
  u64 ts_end;
  char qname[64];
};

BPF_HASH(start, u32, struct data_t);
BPF_PERF_OUTPUT(events);

// 记录 DNS 查询开始的时间
int kprobe__dns_lookup(struct pt_regs *ctx, struct sock *sk, struct sk_buff *skb, const unsigned char *name, int len) {
  u32 pid = bpf_get_current_pid_tgid();
  struct data_t data = {};
  data.pid = pid;
  data.saddr = sk->__sk_common.skc_rcv_saddr;
  data.daddr = sk->__sk_common.skc_daddr;
  data.sport = sk->__sk_common.skc_num;
  data.dport = sk->__sk_common.skc_dport;
  data.dport = ntohs(data.dport);
  data.ts_start = bpf_ktime_get_ns();
  bpf_probe_read_str(data.qname, sizeof(data.qname), name);
  start.update(&pid, &data);
  return 0;
}

// 记录 DNS 查询结束的时间
int kprobe__dns_query_recv(struct pt_regs *ctx, struct sock *sk, struct sk_buff *skb) {
  u32 pid = bpf_get_current_pid_tgid();
  struct data_t *data = start.lookup(&pid);
  if (data) {
    data->ts_end = bpf_ktime_get_ns();
    events.perf_submit(ctx, data, sizeof(struct data_t));
    start.delete(&pid);
  }
  return 0;
}

代码解释:

  • BPF_HASH(start, u32, struct data_t):定义了一个哈希表,用于存储 DNS 查询的开始时间。
  • kprobe__dns_lookup:记录 DNS 查询开始的时间,并将数据存储到哈希表中。
  • kprobe__dns_query_recv:记录 DNS 查询结束的时间,从哈希表中读取开始时间,计算延迟,并将数据发送到用户空间。

3. 分析数据

在用户空间,我们可以编写程序来读取 perf 事件,并分析 DNS 查询的延迟。例如,我们可以统计不同域名的查询延迟,找出延迟较高的域名。

案例分析

以下是一些使用 eBPF 分析网络延迟的案例:

  • 案例 1:TCP 三次握手延迟过高
    • 使用 eBPF 跟踪 TCP 三次握手过程,发现 SYN_SENT 到 SYN_RECV 的时间差较长。
    • 进一步分析,发现服务器的 SYN backlog 队列已满,导致 SYN 包被丢弃。
    • 解决方法:调整服务器的 SYN backlog 队列大小。
  • 案例 2:DNS 查询延迟过高
    • 使用 eBPF 跟踪 DNS 查询过程,发现特定域名的查询延迟较高。
    • 进一步分析,发现 DNS 服务器的负载过高,导致查询响应缓慢。
    • 解决方法:增加 DNS 服务器的资源,或者使用 CDN 等技术来缓存 DNS 记录。

总结

eBPF 是一种强大的网络性能分析工具,可以深入内核追踪网络数据包,分析延迟的来源。通过使用 eBPF,我们可以快速定位网络瓶颈,并采取相应的措施来优化网络性能。本文介绍了如何使用 eBPF 跟踪 TCP 三次握手和 DNS 查询的延迟,并提供了一些案例分析,希望能帮助读者更好地理解和应用 eBPF 技术。

注意: 上述代码只是示例,实际应用中需要根据具体情况进行调整。同时,eBPF 程序的编写需要一定的内核知识和编程经验。建议参考相关的文档和教程,例如:

希望这篇文章能够帮助你利用 eBPF 来诊断和解决网络性能问题。

网络侦探 eBPF网络延迟TCPDNS

评论点评