使用 BPF 记录 TCP 重传和丢包记录

2021-03-07 22:51:07 浏览数 (1)

背景

在云函数的日常运营中,经常有用户提出要求协助排查网络问题。一般的手段就是使用 tcpdump 抓包,但是部署抓包往往是在问题发生之后,而且抓包后复现的时机也不确定,往往费时费力。本文讲述使用 BPF 记录 TCP 的重传和丢包记录,作为定位网络问题的一种辅助手段。

在 BPF 出现之前

BPF 出现之前,在 Linux 上我们也是可以解决这个问题的,只不过比较繁琐,需要对内核、调试器、编译器等许多基础知识有较深理解,参见这里。

TCP 重传为例,我们使用 perf 工具来查找跟踪的点位:

代码语言:txt复制
$ sudo perf list 'tcp:*'

List of pre-defined events (to be used in -e):

  tcp:tcp_destroy_sock                               [Tracepoint event]
  tcp:tcp_probe                                      [Tracepoint event]
  tcp:tcp_rcv_space_adjust                           [Tracepoint event]
  tcp:tcp_receive_reset                              [Tracepoint event]
  tcp:tcp_retransmit_skb                             [Tracepoint event]
  tcp:tcp_retransmit_synack                          [Tracepoint event]
  tcp:tcp_send_reset                                 [Tracepoint event]


Metric Groups:

对于 TCP 重传,显然 tcp_retransmit_skb 是一个合适的跟踪点位。让我们来看看这个函数在内核代码中的签名:

代码语言:txt复制
int tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs);

struct sock 的定义在这里,它包含了我们所需要的信息。这是一个庞大且复杂的结构体,而且对于 kprobe 来说,我们只能使用寄存器以及偏移来输出其值。

要知道这个结构体中每个字段的真实偏移,我们需要内核的符号表,使用 GDB 来确定其值:

代码语言:txt复制
$ sudo apt install linux-image-unsigned-5.8.0-37-generic-dbgsym
$ gdb /usr/lib/debug/boot/vmlinux-5.8.0-37-generic
(gdb) ptype struct sock
...
(gdb) print (int)&((struct sock*)0)->__sk_common.skc_dport
$1 = 12

除此之外,我们还需要了解 X86_64 的调用约定,诸如函数调用的第一个参数使用 di 寄存器传递等。

有了这些背景知识后,我们可以使用 kprobe 来达成这一目标,记录 TCP 重传的例子如下:

代码语言:txt复制
$ echo 'p:kprobes/tcp_retransmit tcp_retransmit_skb port= 12(%di):u16 dst= 0(%di):u32 state= 18(%di):u8' >> /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/tcp_retransmit/enable

这个例子不仅晦涩难懂,而且不易开发及调试,好在现在我们有了 BPF

BPF 横空出世

BPF 是一项革命性技术,它能在内核中运行沙箱程序, 而无需修改内核源码或者加载内核模块。

对于上面的例子,一个等价的 BPF 程序如下:

代码语言:txt复制
#include <uapi/linux/ptrace.h>
#include <net/sock.h>

int log_tcp_retransmit(struct pt_regs *ctx, struct sock *sk) {
    u16 port = sk->__sk_common.skc_dport;
    u32 daddr = sk->__sk_common.skc_daddr;
    u8 state = sk->__sk_common.skc_state;
    bpf_trace_printk("tcp_retransmit port=%d dst=%d state=%dn", port, daddr, state);
    return 0;
}

使用 C 编写,不需要理解 ABI 等细节,而且方便调试。你可以在这里找到完整的代码。对于 TCP 重传,也是一样的道理。

重传的日志记录在 /sys/kernel/debug/tracing/trace,下面是一些真实的记录:

代码语言:txt复制
$ sudo cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 103/103   #P:1
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
...
          <idle>-0       [000] ..s. 2621728.977959: 0: tcp_retransmit port=37897 dst=831376843 state=1
           <...>-3202753 [000] ..s. 2622104.077378: 0: tcp_retransmit port=37897 dst=831376843 state=1
...

转换一下格式,可以看到重传的目的地址及端口等信息:

代码语言:txt复制
2621728.977959 2021-03-05 19:48:18.959403       tcp_retransmit  203.205.141.49           2452   TCP_ESTABLISHED
2622104.077378 2021-03-05 19:54:34.058822       tcp_retransmit  203.205.141.49           2452   TCP_ESTABLISHED

结论

本文讲述使用 BPF 带来的可观测性能力,获取 TCP 的重传及丢包记录,作为辅助定位网络问题的手段。与传统的 kprobe 方式相比, BPF 带来的可编程性极大地提升了开发效率,既没有增加系统的复杂度,也不会牺牲执行效率和安全性。

0 人点赞