TCP Tracepoints

Linux 中的 TCP 跟踪点已经到来!Linux 4.15 添加了其中的五个,而 4.16(尚未完全发布)又添加了两个(tcp:tcp_probe和sock:inet_sock_set_state - 一个可用于 TCP 分析的套接字跟踪点)。我们现在有

# perf list 'tcp:*' 'sock:inet*'

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

  tcp:tcp_destroy_sock                               [Tracepoint event]
  tcp:tcp_probe                                      [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]

  sock:inet_sock_set_state                           [Tracepoint event]

这包括一个通用的:sock:inet_sock_set_state。它可用于跟踪内核何时更改 TCP 会话的状态,例如从 TCP_SYN_SENT 更改为 TCP_ESTABLISHED。一个使用示例是我在开源bcc集合中的tcplife工具:

# tcplife
PID   COMM       LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS
22597 recordProg 127.0.0.1       46644 127.0.0.1       28527     0     0 0.23
3277  redis-serv 127.0.0.1       28527 127.0.0.1       46644     0     0 0.28
22598 curl       100.66.3.172    61620 52.205.89.26    80        0     1 91.79
22604 curl       100.66.3.172    44400 52.204.43.121   80        0     1 121.38
22624 recordProg 127.0.0.1       46648 127.0.0.1       28527     0     0 0.22
3277  redis-serv 127.0.0.1       28527 127.0.0.1       46648     0     0 0.27
[...]

我在这个跟踪点存在之前写了 tcplife,所以我不得不使用 tcp_set_state() 内核函数的 kprobes(内核动态跟踪)。这行得通,但它依赖于可能从一个内核版本更改为下一个内核版本的各种内核实现细节。为了保持 tcplife 正常工作,每次内核更改时都需要包含不同的代码,这将变得难以维护和增强。想象一下需要在几个不同的内核版本上测试更改,因为 tcplife 对每个版本都有特殊的代码!

跟踪点被认为是一种“稳定的 API”,因此它们的详细信息不应从一个内核版本更改为下一个内核版本,从而使使用它们的程序更易于维护。我故意说“不应该”,因为我认为这些是“尽力而为”,而不是“一成不变”。如果它们被认为是一成不变的,那么说服内核维护者接受新的跟踪点将更加困难(有充分的理由)。例如:tcp:tcp_set_state是在 4.15 中添加的,然后sock:inet_sock_set_state是在 4.16 中添加的。由于 sock one 是超集,tcp one 在 4.16 中被禁用并将被删除。我们尽量避免像这样更改跟踪点,但在这种情况下,它是短暂的,在任何人使用它之前就被删除了。

tcplife 无论如何都不是使用跟踪点的好例子,因为它在三个地方(tx 和 rx 字节,以及跟踪点上的尽力而为的进程上下文)超出了跟踪点 API,因此它可能仍需要一些维护。但它比 kprobes 版本有了很大的改进,其他工具只能坚持使用 tracepoints API。

显示sock:inet_sock_set_state的另一种方法是使用 Sasha Goldshtein 的 bcc 跟踪工具将其与 tcp_set_state() 上的 kprobes 进行比较。第一个命令使用 kprobes,第二个命令使用跟踪点:


# trace -t -I net/sock.h 'p::tcp_set_state(struct sock *sk) "%llx: %d -> %d", sk, sk->sk_state, arg2'
TIME     PID     TID     COMM            FUNC             -
2.583525 17320   17320   curl            tcp_set_state    ffff9fd7db588000: 7 -> 2
2.584449 0       0       swapper/5       tcp_set_state    ffff9fd7db588000: 2 -> 1
2.623158 17320   17320   curl            tcp_set_state    ffff9fd7db588000: 1 -> 4
2.623540 0       0       swapper/5       tcp_set_state    ffff9fd7db588000: 4 -> 5
2.623552 0       0       swapper/5       tcp_set_state    ffff9fd7db588000: 5 -> 7
^C
# trace -t 't:sock:inet_sock_set_state "%llx: %d -> %d", args->skaddr, args->oldstate, args->newstate'
TIME     PID     TID     COMM            FUNC             -
1.690191 17308   17308   curl            inet_sock_set_state ffff9fd7db589800: 7 -> 2
1.690798 0       0       swapper/24      inet_sock_set_state ffff9fd7db589800: 2 -> 1
1.727750 17308   17308   curl            inet_sock_set_state ffff9fd7db589800: 1 -> 4
1.728041 0       0       swapper/24      inet_sock_set_state ffff9fd7db589800: 4 -> 5
1.728063 0       0       swapper/24      inet_sock_set_state ffff9fd7db589800: 5 -> 7
^C

Both are showing the same output. For reference:

1: TCP_ESTABLISHED
2: TCP_SYN_SENT
3: TCP_SYN_RECV
4: TCP_FIN_WAIT1
5: TCP_FIN_WAIT2
6: TCP_TIME_WAIT
7: TCP_CLOSE
8: TCP_CLOSE_WAIT

我知道,我知道,我应该将其添加为查找哈希,然后……稍后,这是我刚刚为 bcc 贡献的一个新工具——tcpstate——它可以进行翻译,并显示每个状态的持续时间:

# tcpstates
SKADDR           C-PID C-COMM     LADDR           LPORT RADDR         RPORT OLDSTATE    -> NEWSTATE    MS
ffff9fd7e8192000 22384 curl       100.66.100.185  0     52.33.159.26  80    CLOSE       -> SYN_SENT    0.000
ffff9fd7e8192000 0     swapper/5  100.66.100.185  63446 52.33.159.26  80    SYN_SENT    -> ESTABLISHED 1.373
ffff9fd7e8192000 22384 curl       100.66.100.185  63446 52.33.159.26  80    ESTABLISHED -> FIN_WAIT1   176.042
ffff9fd7e8192000 0     swapper/5  100.66.100.185  63446 52.33.159.26  80    FIN_WAIT1   -> FIN_WAIT2   0.536
ffff9fd7e8192000 0     swapper/5  100.66.100.185  63446 52.33.159.26  80    FIN_WAIT2   -> CLOSE       0.006
^C

我在 Linux 4.16 上演示了这一点,在 Yafang Shao 编写了一个增强来显示所有状态转换之后,而不仅仅是内核实现的状态转换。这是它在 4.15 上的样子:

# trace -I net/sock.h -t 'p::tcp_set_state(struct sock *sk) "%llx: %d -> %d", sk, sk->sk_state, arg2'
TIME     PID    TID    COMM         FUNC             -
3.275865 29039  29039  curl         tcp_set_state    ffff8803a8213800: 7 -> 2
3.277447 0      0      swapper/1    tcp_set_state    ffff8803a8213800: 2 -> 1
3.786203 29039  29039  curl         tcp_set_state    ffff8803a8213800: 1 -> 8
3.794016 29039  29039  curl         tcp_set_state    ffff8803a8213800: 8 -> 7
^C
# trace -t 't:tcp:tcp_set_state "%llx: %d -> %d", args->skaddr, args->oldstate, args->newstate'
TIME     PID    TID    COMM         FUNC             -
2.031911 29042  29042  curl         tcp_set_state    ffff8803a8213000: 7 -> 2
2.035019 0      0      swapper/1    tcp_set_state    ffff8803a8213000: 2 -> 1
2.746864 29042  29042  curl         tcp_set_state    ffff8803a8213000: 1 -> 8
2.754193 29042  29042  curl         tcp_set_state    ffff8803a8213000: 8 -> 7

回到 4.16,这是通过 bcc 的 tplist 工具获得的带有参数的当前跟踪点列表:

# tplist -v 'tcp:*'
tcp:tcp_retransmit_skb
    const void * skbaddr;
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_send_reset
    const void * skbaddr;
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_receive_reset
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_destroy_sock
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_retransmit_synack
    const void * skaddr;
    const void * req;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_probe
    __u8 saddr[sizeof(struct sockaddr_in6)];
    __u8 daddr[sizeof(struct sockaddr_in6)];
    __u16 sport;
    __u16 dport;
    __u32 mark;
    __u16 length;
    __u32 snd_nxt;
    __u32 snd_una;
    __u32 snd_cwnd;
    __u32 ssthresh;
    __u32 snd_wnd;
    __u32 srtt;
    __u32 rcv_wnd;
# tplist -v sock:inet_sock_set_state
sock:inet_sock_set_state
    const void * skaddr;
    int oldstate;
    int newstate;
    __u16 sport;
    __u16 dport;
    __u16 family;
    __u8 protocol;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];

添加的第一个 TCP 跟踪点是Cong Wang (Twitter) 的tcp:tcp_retransmit_skb。他引用了我来自perf-tools的基于 kprobe 的tcpretrans工具作为消费者示例。Song Liu (Facebook) 添加了另外五个跟踪点,包括tcp:tcp_set_state现在是sock:inet_sock_set_state。感谢 Cong 和 Song,以及 David S. Miller(网络维护者)接受这些并就正在进行的 tcp 跟踪点工作提供反馈。

在开发过程中,我与 Song(和 Alexei Starovoitov)讨论了最近添加的内容,因此我已经了解了这些内容存在的原因和用途。当前 TCP 跟踪点的一些粗略说明:

  • tcp:tcp_retransmit_skb :跟踪重传。有助于理解网络问题,包括拥塞。将由我的 tcpretrans 工具而不是 kprobes 使用。
  • tcp:tcp_retransmit_synack :跟踪 SYN/ACK 重传。我想这可以用于一种 DoS 检测(SYN 泛洪触发 SYN/ACK,然后重新传输)。这与 tcp:tcp_retransmit_skb 是分开的,因为此事件没有 skb。
  • tcp:tcp_destroy_sock :任何在内存中汇总 TCP 会话详细信息的程序都需要,该会话将由 sock 地址作为键控。这个探针可以用来知道会话已经结束,因此 sock 地址即将被重用,任何到目前为止的汇总数据都应该被消费然后删除。
  • tcp:tcp_send_reset :这会在有效套接字期间跟踪 RST 发送,以诊断这些类型的问题。
  • tcp:tcp_receive_reset :跟踪 RST 接收。
  • tcp:tcp_probe :用于 TCP 拥塞窗口跟踪,它还允许弃用和删除旧的 TCP 探测模块。这是Masami Hiramatsu 添加的,并在 Linux 4.16 中合并。
  • sock:inet_sock_set_state :可用于许多事情。tcplife 工具就是其中之一,而且我的 tcpconnect 和 tcpaccept bcc 工具也可以转换为使用这个跟踪点。我们可以添加单独的tcp:tcp_connect和tcp:tcp_accept跟踪点(或tcp:tcp_active_open和tcp:tcp_passive_open),但是sock:inet_sock_set_state可以用于此。

使用这些跟踪点比数据包捕获方法 (libpcap) 更受欢迎,因为跟踪点的开销应该更低,并且可以公开有用的内核状态,而不是在线上。

我可以想象这些 TCP 跟踪点会有多么有用,因为我多年前设计并使用了类似的跟踪点:我在CEC2006上演示的DTrace TCP 提供程序。我最初将 TCP 状态更改拆分为每个状态的探针,但是当它被合并时,它变成了单个tcp:::state-change探针,就像我们现在在 Linux 中通过 sock 跟踪点所做的那样。

下一步是什么?tcp:tcp_send和tcp:tcp_receive的跟踪点可能很方便,但必须特别注意它们可以添加的微小开销(启用和特别禁用),因为发送/接收可能是非常频繁的活动。更多错误条件的跟踪点也很有用,例如连接拒绝路径,这可能有助于分析 DoS 攻击。

如果您对添加 TCP 跟踪点感兴趣,我建议您编写一个 kprobe 解决方案作为概念证明开始,并获得一些生产经验。这就是我之前的 kprobe 工具所扮演的角色。kprobe 解决方案将显示跟踪点是否有用,如果有用,则有助于证明将其包含在 Linux 内核维护者中。

转载翻译自: https://www.brendangregg.com