TCP跟踪点已经到达Linux!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]

这包括一个多功能的:袜子: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 中添加的,然后在 4.16 中添加了sock:inet_sock_set_state。由于袜子是超集,因此 tcp 在 4.16 中被禁用,将被移除。我们尽量避免像这样更改跟踪点,但在这种情况下,它是短暂的,并且在任何人使用它之前就被删除了。

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

两者都显示相同的输出。供参考:

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

我知道,我知道,我应该把它添加为查找哈希,然后......过了一会儿,这是我刚刚为密件抄送贡献的一个新工具 - 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 地址,并且应使用到目前为止的任何汇总数据,然后删除。
  • tcp:tcp_send_reset:这在有效套接字期间跟踪 RST 发送,以诊断这些类型的问题。
  • tcp:tcp_receive_reset:跟踪 RST 接收。
  • tcp:tcp_probe:用于 TCP 拥塞窗口跟踪,这也允许弃用和删除较旧的 TCP 探测模块。这是由Masami Hiramatsu添加的,并合并到Linux 4.16中。
  • 袜子: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可以用于此目的。

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

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

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