http://www.brendangregg.com/blog/2016-10-15/linux-bcc-tcptop.html

@无牙

译注:本文主要介绍作者基于eBPF写的tcptop工具。

我最近用eBPF写了tcptop,这个工具可以汇总当前系统活动的tcp会话:

tcptop

Tracing… Output every 1 secs. Hit Ctrl-C to end

19:46:24 loadavg: 1.86 2.67 2.91 3/362 16681

PID COMM LADDR RADDR RX_KB TX_KB 16648 16648 100.66.3.172:22 100.127.69.165:6684 1 0 16647 sshd 100.66.3.172:22 100.127.69.165:6684 0 2149 14374 sshd 100.66.3.172:22 100.127.69.165:25219 0 0 14458 sshd 100.66.3.172:22 100.127.69.165:7165 0 0

PID COMM LADDR6 RADDR6 RX_KB TX_KB 16681 sshd fe80::8a3:9dff:fed5:6b19:22 fe80::8a3:9dff:fed5:6b19:16606 1 1 16679 ssh fe80::8a3:9dff:fed5:6b19:16606 fe80::8a3:9dff:fed5:6b19:22 1 1 16680 sshd fe80::8a3:9dff:fed5:6b19:22 fe80::8a3:9dff:fed5:6b19:16606 0 0

输出的结果第一行是系统的一些汇总信息,之后是一组组的IPv4和IPv6流量的信息。tcptop和很多其他基于Linux 4.x的BPF工具一起放在开源的bcc项目中.

这些流量信息对于性能分析和问题排查比较有用:这个服务器目前正在跟那些人通信,跟每个人收发了多少数据。你也可能使用这个工具发现一些你的程序本来可以避免的流量,来提升系统的整体性能。

当前的版本包含如下选项:

tcptop -h

usage: tcptop [-h] [-C] [-S] [-p PID] [interval] [count]

Summarize TCP send/recv throughput by host

positional arguments: interval output interval, in seconds (default 1) count number of outputs

optional arguments:
-h, --help         show this help message and exit
-C, --noclear      don't clear the screen
-S, --nosummary    skip system summary line
-p PID, --pid PID  trace this PID only

examples:
./tcptop           # trace TCP send/recv by host
./tcptop -C        # don't clear the screen
./tcptop -p 181    # only trace PID 181

我个人很喜欢用-C选项,因为这个会翻滚的打印输出信息而不会清空屏幕输出。这样我可以检查不同时间的输出,或者拷贝感兴趣的输出给别人。我倾向于把-C选项当成默认选项,但是默认我还是保留了大家期待的原来William LeFebvre写的top的那种清空屏幕的方式。

其他选项后面可能会增加。当前版本除非加-C选项,否则不会根据屏幕的大小截断,尽管它应该这样。
[编辑] 开销:

当前tcptop通过在内核tcp这层中增加增加汇总链接数据的trace逻辑,然后用户态bcc程序定期被唤醒并抓取这些汇总的数据,并展示出来。

当你往网络的收发路径上增加指令时,必须要格外小心:这里的执行频率可能会超过一百万次每秒。bcc/BFP比其他tracing方案开销更低的原因有:

    内核到用户态只传输数据的汇总信息,而不是dump每个数据包;
        内核到用户态的传输并不是很频繁的:每一个时间间隔一次;
	    因为TCP的缓存机制,追踪TCP这层比追踪数据这层的频率更低(我见过3倍的差距,当然如果是巨型帧的话差别会很小);
	        BPF的指令采用即时编译(JIT)的方式; 

		(采用BFP)Tracing的开销跟TCP事件的发生的频率有关(其实就是程序里面被trace的TCP函数:tcp_sendmsg()/tcp_recvmsg()或者tcp_cleanup_rbuf())。由于TCP的缓存机制,这个的调用频率会比数据包级别的要低。你可以使用funccount工具(在bcc里面有)来追踪这些函数调用的频率。

		我在一些生产环境的服务器上取样做了一些测试,发现这些TCP trace点的执行频率大概在4K到15K每秒的样子。tcptop占用的CPU的开销大概总共在一个核的0.5%到2.0%之间。或许你的服务器上的压力更大,这样,tcptop的开销也会越大。我见过的生产环境中最极端的例子是一个代码仓库服务器,大概网络带宽压力在5Gbps的样子,TCP trace点的执行频率大约在300K每秒。我估算了一下这种场景下,tcptop大概会要占用一个核的40%的CPU,或者相当于一个32核的机器的总CPU的1.3%。不算太大,但也不并不是可以忽略的级别。

		我在想,tcptop的开销能否降得更低?或许我们可以给某些结构体增加一些计数器,例如struct sock/struct tcp,BPF的开发者Alexei Starovoitov建议我去看看新扩展的tcp_info结构体。
		[编辑] tcp_info和RFC-4989

		下面是include/uapi/linux/tcp.h中定义的最新的tcp_info:

		struct tcp_info { 

			__u8    tcpi_state;

			[...] 

				__u64   tcpi_bytes_acked;    /* RFC4898 tcpEStatsAppHCThruOctetsAcked */
			__u64   tcpi_bytes_received; /* RFC4898 tcpEStatsAppHCThruOctetsReceived */
			__u32   tcpi_segs_out;       /* RFC4898 tcpEStatsPerfSegsOut */
			__u32   tcpi_segs_in;        /* RFC4898 tcpEStatsPerfSegsIn */

			__u32   tcpi_notsent_bytes;
			__u32   tcpi_min_rtt;
			__u32   tcpi_data_segs_in;  /* RFC4898 tcpEStatsDataSegsIn */
			__u32   tcpi_data_segs_out; /* RFC4898 tcpEStatsDataSegsOut */

			__u64   tcpi_delivery_rate;

		};

这些计数器是Eric Dumazet(Google)在2015年给RFC4898新增的,今年MArtin KaFai Lau(Facebook)又加了一些。

最有意思的是tcpi_bytes_acked 和tcpi_bytes_received,在Linux 4.1中引入。它们可以通过’ss -nti’打印出来:

ss -nti

State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 0 100.66.3.172:22 10.16.213.254:55277 cubic wscale:5,9 rto:264 rtt:63.487/0.067 ato:48 mss:1448 cwnd:58 bytes_acked:175897 bytes_received:15933 segs_out:618 segs_in:917 send 10.6Mbps lastsnd:176 lastrcv:196 lastack:112 pacing_rate 21.2Mbps rcv_rtt:64 rcv_space:28960 ESTAB 0 0 100.66.3.172:22 10.16.213.254:52066 cubic wscale:5,9 rto:264 rtt:63.509/0.064 ato:40 mss:1448 cwnd:54 bytes_acked:47461 bytes_received:28861 segs_out:746 segs_in:1285 send 9.8Mbps lastsnd:10732 lastrcv:10732 lastack:10668 pacing_rate 19.7Mbps rcv_rtt:76 rcv_space:28960 […]

FRC4898中的定义:

tcpEStatsAppHCThruOctetsAcked OBJECT-TYPE SYNTAX ZeroBasedCounter64 UNITS “octets” MAX-ACCESS read-only STATUS current DESCRIPTION “The number of octets for which cumulative acknowledgments have been received, on systems that can receive more than 10 million bits per second. Note that this will be the sum of changes in tcpEStatsAppSndUna.” ::= { tcpEStatsAppEntry 5 }

tcpEStatsAppHCThruOctetsReceived OBJECT-TYPE SYNTAX ZeroBasedCounter64 UNITS “octets” MAX-ACCESS read-only STATUS current DESCRIPTION “The number of octets for which cumulative acknowledgments have been sent, on systems that can transmit more than 10 million bits per second. Note that this will be the sum of changes in tcpEStatsAppRcvNxt.” ;::= { tcpEStatsAppEntry 8 }

这些计数器其实就是收发的字节数。我们是不是也可以像’ss -nti’一样直接在tcptop里面计数器?这样的话就可以避免在收发路径上增加trace指令,从而降低开销。

但是去轮询tcp_info结构体中的数据至少有两个问题:

    第一个问题是短连接和不完整的会话:就像top会漏掉在连续两次刷新时间间隔内开始并结束的进程一样,轮询的模式可能会漏掉短的tcp连接的信息。不幸的是,tcp_info在tcp连接进入TIME_WAIT之前就被释放了(可能是为了减小收到DoS攻击时的开销),因此,应用可能会有大量的tcp短连接被polling模型给漏掉。一个解决方法是在tcptop中缓存上一次轮询时的tcp_info的会话状态,并且在TCP关闭的路径上用BPF增加一些指令,包括去抓取RFC-4898中定义的那些counter。这样短生命周期的会话就可以被抓出来,那些在两次轮询间隔之间的短连接可以被计算进去。
    第二个问题是轮询tcp_info的开销:“ss -nti”在某些有15K个会话的服务器上,会用到100ms的CPU时间。这个可能可以降低一些,但是仍然可能在某些负载的服务器上,轮询tcp_info再加上缓存tcp_info的会话以及执行TCP关闭时的tracing指令,开销仍然可能超过仅仅是对TCP send/receive的tracing. 

    由于其他一些方案面临的问题,因此,当前的基于在TCP send/receive路径上的tracing的方案,可能并不会被替代。这个我仍然在调研。
    [编辑] 老版tcptop

    几年前基于我之前在tcpsnoop上的工作,我写了tcptop的原始版本,当时用的是Dtrace。通过当时写tcptop我意识到要尽可能的减少动态probes,由于TCP/IP协议栈会随着内核版本的变化而改变,用的probe越多,你的程序就越脆弱。最好是用静态tracepoint,因为这些一般不会变。

    Linux的TCP send & receive路径需要静态tracepoint,有了这个之后,像tcptop这样的工具,就不容易因为内核的改变打破。当然,这是另一个话题了。

    最后,谢谢Coburn(Netfix)的建议,让我基于BPF重写tcptop。