WEBKT

非 Kubernetes 环境下 eBPF 网络调试与性能优化实战

90 0 0 0

在云原生架构日益普及的今天,Kubernetes 已经成为容器编排的事实标准。然而,大量的传统应用以及新兴的边缘计算场景仍然运行在非 Kubernetes 的 Linux 环境中。在这些环境中,对系统进行高效的性能分析与故障排查至关重要。eBPF(extended Berkeley Packet Filter)作为一种革命性的内核技术,为我们提供了强大的观测和控制能力,使得在不修改内核代码的前提下,动态地追踪系统行为,诊断性能瓶颈成为可能。

本文将深入探讨 eBPF 在非 Kubernetes Linux 环境下的网络调试和性能优化场景,并具体分析如何利用 eBPF 追踪系统调用、文件 I/O 和进程调度,以解决常见的性能瓶颈问题,例如 TCP 连接建立缓慢或者磁盘 I/O 异常高等情况。

eBPF 简介

eBPF 最初设计用于网络数据包过滤,但现在已经发展成为一个通用的内核虚拟机,允许用户在内核中安全地运行自定义代码。eBPF 程序可以挂载到各种内核事件源(例如系统调用、函数入口/出口、网络事件等),并在事件发生时执行。eBPF 程序通常使用 C 编写,然后使用 LLVM 编译成字节码,最后通过 bpf() 系统调用加载到内核中。内核会验证 eBPF 程序的安全性,确保其不会导致系统崩溃或泄露敏感信息。

eBPF 的核心优势包括:

  • 安全性: 内核验证器确保 eBPF 程序不会导致系统崩溃。
  • 高性能: JIT(Just-In-Time)编译器将 eBPF 字节码编译成机器码,接近原生代码的性能。
  • 灵活性: 可以动态加载和卸载 eBPF 程序,无需重启系统。
  • 可观测性: 能够追踪各种内核事件,提供丰富的系统行为信息。

eBPF 在网络调试中的应用

网络问题是 Linux 系统中常见的性能瓶颈之一。例如,TCP 连接建立缓慢、丢包、延迟高等问题都会影响应用的性能和用户体验。eBPF 提供了强大的网络追踪能力,可以帮助我们诊断和解决这些问题。

追踪 TCP 连接建立过程

TCP 连接建立过程涉及三次握手,任何一个环节出现问题都可能导致连接建立缓慢。我们可以使用 eBPF 追踪 connect()accept() 等系统调用,以及内核中的 TCP 相关函数,例如 tcp_v4_connect()tcp_v4_syn_recv_sock() 等,来分析连接建立的各个阶段的耗时。

以下是一个简单的示例,展示如何使用 bpftrace 工具追踪 connect() 系统调用:

#!/usr/bin/env bpftrace

#include <linux/socket.h>

BEGIN
{
  printf("Tracing connect() syscalls...\n");
}


syscalls:sys_enter_connect
{
  @connects[pid, comm, arg0->sa_family, str(arg0->sa_data)] = count();
}

END
{
  printf("\nConnect Summary:\n");
  print(@connects);
}

这个脚本会追踪所有 connect() 系统调用,并统计每个进程发起的连接数量以及连接的目标地址。通过分析这些数据,我们可以找出连接建立缓慢的进程,并进一步分析其原因。

分析网络延迟

网络延迟是影响应用性能的另一个重要因素。我们可以使用 eBPF 追踪网络数据包的发送和接收过程,计算数据包在网络中的传输时间。例如,我们可以使用 tc (traffic control) 命令将 eBPF 程序附加到网络接口上,然后追踪数据包的 ingress 和 egress 事件。

以下是一个使用 XDP (eXpress Data Path) 的示例,展示如何测量网络延迟:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

struct data_t {
    u64 timestamp;
    u32 src_addr;
    u32 dst_addr;
    u16 src_port;
    u16 dst_port;
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} rb SEC(".maps");


SEC("xdp")
int xdp_prog(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    u64 ts = bpf_ktime_get_ns();

    if (data + sizeof(struct ethhdr) > data_end)
        return XDP_PASS;

    if (bpf_ntohs(eth->h_proto) == ETH_P_IP) {
        struct iphdr *iph = data + sizeof(struct ethhdr);
        if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end)
            return XDP_PASS;

        if (iph->protocol == IPPROTO_TCP) {
            struct tcphdr *tcph = (void*)iph + sizeof(struct iphdr);
            if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct tcphdr) > data_end)
                return XDP_PASS;

            struct data_t pkt = {
                .timestamp = ts,
                .src_addr = iph->saddr,
                .dst_addr = iph->daddr,
                .src_port = bpf_ntohs(tcph->source),
                .dst_port = bpf_ntohs(tcph->dest),
            };

            bpf_ringbuf_output(&rb, &pkt, sizeof(pkt), 0);
        }
    }

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

这个程序会将每个 TCP 数据包的时间戳、源地址、目标地址、源端口和目标端口记录到一个 ring buffer 中。我们可以使用用户态程序从 ring buffer 中读取数据,并计算数据包的延迟。通过分析这些数据,我们可以找出延迟较高的网络连接,并进一步分析其原因,例如网络拥塞、路由问题等。

eBPF 在性能优化中的应用

除了网络调试,eBPF 还可以用于性能优化。例如,我们可以使用 eBPF 追踪文件 I/O 和进程调度,找出性能瓶颈,并采取相应的优化措施。

追踪文件 I/O

磁盘 I/O 是影响应用性能的另一个重要因素。例如,频繁的小文件读写、大量的磁盘同步操作等都会导致 I/O 性能下降。我们可以使用 eBPF 追踪 read()write()open()close() 等系统调用,以及内核中的文件系统相关函数,例如 vfs_read()vfs_write() 等,来分析文件 I/O 的各个环节的耗时。

以下是一个简单的示例,展示如何使用 perf 工具追踪 read() 系统调用:

perf probe -x /lib64/libc.so.6 read
perf record -e probe//lib64/libc.so.6:read -ag -- sleep 10
perf report

这个命令会追踪 read() 系统调用,并记录其调用栈。通过分析 perf report 的输出,我们可以找出调用 read() 系统调用最多的函数,并进一步分析其原因,例如是否可以减少文件读取次数、是否可以使用缓存等。

分析进程调度

进程调度是操作系统的重要功能之一。不合理的进程调度策略会导致某些进程长时间等待,从而影响应用的性能。我们可以使用 eBPF 追踪进程的调度事件,例如 sched_switchsched_wakeup 等,来分析进程的调度情况。

以下是一个简单的示例,展示如何使用 bcc 工具追踪 sched_switch 事件:

from bcc import BPF

# 编写 eBPF 程序
program = """
#include <uapi/linux/sched.h>

struct data_t {
    u32 pid;
    u64 ts;
    char comm[TASK_COMM_LEN];
};

BPF_PERF_OUTPUT(events);

int kprobe__sched_switch(struct pt_regs *ctx, struct task_struct *prev) {
    struct data_t data = {};
    data.pid = prev->pid;
    data.ts = bpf_ktime_get_ns();
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}
"""

# 创建 BPF 对象
bpf = BPF(text=program)

# 加载 eBPF 程序
bpf.attach_kprobe(event="sched_switch", fn_name="kprobe__sched_switch")

# 定义回调函数
def print_event(cpu, data, size):
    event = bpf["events"].event(data)
    print(f"{event.pid} {event.comm.decode('utf-8', 'replace')} {event.ts}")

# 注册回调函数
bpf["events"].open_perf_buffer(print_event)

# 循环读取事件
while True:
    try:
        bpf.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

这个程序会追踪 sched_switch 事件,并记录每个进程的 PID、时间戳和进程名。通过分析这些数据,我们可以找出长时间等待的进程,并进一步分析其原因,例如是否与其他进程存在资源竞争、是否需要调整进程优先级等。

总结

eBPF 是一种强大的内核技术,为 Linux 系统的网络调试和性能优化提供了新的可能性。在非 Kubernetes 环境下,我们可以利用 eBPF 追踪系统调用、文件 I/O 和进程调度,找出性能瓶颈,并采取相应的优化措施。虽然 eBPF 的学习曲线较陡峭,但是掌握 eBPF 技术对于提升 Linux 系统的性能和稳定性至关重要。 通过本文的介绍,希望能够帮助读者更好地理解和应用 eBPF 技术,解决实际工作中的性能问题。

请注意,上述示例代码仅供参考,实际应用中需要根据具体情况进行调整和优化。同时,eBPF 的使用需要一定的内核知识和编程经验,建议读者在学习和使用 eBPF 时,参考相关的文档和教程。

性能猎人 eBPFLinux性能优化

评论点评