For the complete documentation index, see llms.txt. This page is also available as Markdown.

用 DTrace 探索网络活动

FreeBSD 提供了大量工具和技巧,用于回答关于网络栈活动的问题。systat(1)netstat(1) 这类工具能给出若干起点:例如它们可以按接口或按协议展示包速率和位速率。更高级的工具会利用伯克利包过滤器(Berkeley Packet Filter,BPF)来追踪进出系统的单个数据包;Ports 中的 sysutils/iftop 就用这种技术按四元组展示位速率。老牌的 tcpdump(1) 也通过 BPF 接口实时捕获并记录数据包,从而支持基于事后分析的提问。

FreeBSD 10 起,内核新增了一组 DTrace 探测器,让用户能深入洞察网络栈的内部工作。具体而言,用户现在可以基于 FreeBSD 内核 IP、TCP 和 UDP 层中的数据包发送和接收事件编写脚本,并实时窥探指定连接的内部 TCP 状态。这对任何程序员或系统管理员的工具箱都是一项有力的补充,因为它提供框架来回答关于 FreeBSD IP 栈行为的任意问题;DTrace 不受既有工具输出的局限,使得用户可以编写自己的工具来探索网络活动,无论是为了监控性能指标、定位问题源头,还是单纯为了更深入地了解网络协议。

本文将概述每个新探测器,讲解其用法并给出示例。本文假定读者对 DTrace 有基本了解并熟悉其使用,但本文中的所有示例要么是 dtrace(1) 命令——可直接在 shell 中运行——要么是可执行脚本。这些示例均在 FreeBSD 10 上开发并测试,鼓励有可用测试系统的读者动手运行,以体会 DTrace 的能力。脚本可从 http://people.freebsd.org/~markj/dtrace/network-providers/examples/ 下载。

需要提醒的是,这些探测器的 FreeBSD 实现相对较新,因此在你尝试时自然可能遇到 bug 或难以解释的行为。DTrace 保证脚本不会让系统崩溃或破坏其状态,所以在 FreeBSD 上运行这些示例或任何 DTrace 脚本都没有危险。但是,如果你在使用这些新探测器或 DTrace 时遇到问题,请发送邮件至 freebsd-dtrace@FreeBSD.org 邮件列表报告。不针对 FreeBSD 的 DTrace 相关问题和讨论请发送至 dtracediscuss@lists.dtrace.org 邮件列表;DTrace 的多位原始及现任开发者都订阅了该列表,并乐于回应该列表上的帖子。

FreeBSD 上的 DTrace

熟悉 FreeBSD DTrace 实现的用户可能知道 fbt provider,它用于追踪 FreeBSD 内核中发生的函数调用。该 provider 对熟悉 FreeBSD 内核内部的用户而言极为便利,但有几个缺点:

  • 它的使用要求对内核代码有相当程度的了解,这对希望编写自己脚本的大多数用户来说是一道不低的门槛。

  • fbt 探测器按定义与内核代码紧密耦合;如果脚本所基于的代码发生改动,脚本可能无法运行或行为异常。因此为某个 FreeBSD 版本编写的脚本未必能在另一版本上工作,几乎可以肯定无法在其他操作系统上工作。

  • 单个 fbt 探测器往往不能与逻辑系统事件很好对应。假设你想编写 DTrace 脚本,打印 FreeBSD 交给网卡驱动的每个 IP 数据包的目的地址。事实证明这是一项令人望而生畏的任务:它至少需要在 IPv4 和 IPv6 代码的不同部分埋点四个不同函数,每个函数的调用参数都不同。

新的网络探测器让用户能够编写脚本,并使用不受上述问题困扰的接口来追踪网络相关事件。它们提供了稳定而简洁的窗口来窥探内核活动:要追踪 IP 数据包的目的地址,只需运行:

示例 1:

# dtrace -n 'ip:::send {printf("%s", args[2]->ip_daddr);}'

fbt provider 实现完全相同的功能至少需要写五十行难以理解的 D 代码——这是作者在勉强尝试这道练习后给出的估算。相比之下,这条示例相当直观透明,除了有些神秘的 args[2]。用通俗的话说,它的意思是“每次我们发送 IP 数据包,就打印其目的地址”。在我的笔记本上运行数秒,同时 ping 一个互联网地址,得到如下输出:

# dtrace -n 'ip:::send {printf("%s", args[2]->ip_daddr);}'
dtrace: description 'ip:::send ' matched 1 probe
CPU ID FUNCTION:NAME
0 36564 :send 8.8.178.110
0 36564 :send 8.8.178.110
0 36564 :send 8.8.178.110
0 36564 :send 8.8.178.110

这是 DTrace 的默认输出格式。我们可以在 dtrace(1) 参数中加入 -x quiet,并自行打印换行符(\n),以获得更多控制:

在 D 脚本中,可通过在文件开头加入下面这行达到相同效果:

新的网络探测器

本文讨论的网络探测器起源于 Solaris,在 illumos 和 OS X 中也存在。它们属于新的 iptcpudp DTrace provider;下面的示例 2 命令将在你的系统上列出这些探测器:

示例 2:

可以看出,FreeBSD 现在为 IP、TCP 和 UDP 的数据包发送和接收事件都提供了探测器。IP 发送和接收探测器(即 ip:::sendip:::receive)在 FreeBSD 发送或接收 IPv4 或 IPv6 数据包时触发。类似地,TCP 和 UDP 的发送和接收探测器在内核发送或接收 TCP 或 UDP 数据包时触发。可以看到,tcp provider 还包含对应 TCP 协议事件的额外探测器;目前我们先关注发送和接收探测器,并构建几个示例。

每个网络探测器都接受若干参数,这些参数共同描述了触发该探测器的数据包。参数本身各自是相关信息的集合。例如,ip:::send 的第三个参数(即前面示例中的 args[2])包含 IPv4 与 IPv6 共有的高层 IP 字段:IP 版本(ip_ver)、负载长度(ip_plength)以及源地址和目的地址(ip_saddrip_daddr)。第四个参数包含描述用于传输该数据包的网络接口的信息,第五和第六个参数分别给出该数据包的详细 IPv4 和 IPv6 字段。也就是说,如果该数据包使用 IPv4,args[4] 会暴露其 IPv4 头部字段;而 IPv6 数据包的字段则通过 args[5] 暴露。ip:::send 探测器参数的完整列表与描述见 [1],此处不再赘述;tcpudp provider 的对应页面分别在 [2] 和 [3]。利用发送和接收探测器,我们可以为每个数据包实时打印输出。在负载较重的系统上,这显然不切实际;此时用户会希望使用 DTrace 的数据聚合功能,或加入谓词以有选择地追踪数据包。

下面的脚本在每个 TCP 数据包进出系统时打印其基本信息。注意,被转发的 TCP 数据包不会触发 tcp 探测器,因为 TCP 代码并不会处理它们。

示例 3 用到三个探测器。dtrace:::BEGIN 探测器用于为 tcp 探测器动作的输出打印列头。如列头所示,tcp 探测器为每个 TCP 数据包打印与之关联的四元组,以及 TCP 负载大小和 TCP 标志。一些样本输出展示了经过 NAT 的 SSH、HTTP 和 IMAPS 流量:

示例 3:

tcp 探测器的动作是一次 printf() 调用,用到了 tcp:::sendtcp:::receive 的第三个和第五个参数;这些参数包含相应数据包的 IP 和 TCP 头部,便于取出与该数据包关联的四元组。TCP 负载大小的计算略麻烦:IP 头部中含 IP 负载大小,TCP 头部中含从 TCP 头部起点到 TCP 负载的偏移量,二者之差即为 TCP 负载大小。注意,若出站接口启用了 TSO,DTrace 报告的出站段的 TCP 负载大小可能大于该连接的 MSS。最后,args[4]->tcp_flags 含该段的 TCP 标志,DTrace TCP 库(在 FreeBSD 上位于 /usr/lib/dtrace/tcp.d)为每个 TCP 标志提供了符号名。

示例 4 演示了简单用法,使用线性分布打印每个接口的 IP 负载大小直方图。该脚本运行期间不向终端打印任何内容,而是持续采集数据,并在退出时打印摘要——退出可以由用户按 Ctrl-C 或脚本调用内置的 exit() 函数触发。本例中脚本会一直运行,直到用户结束它:

示例 4:

在只有单一活动接口(本例中为 wlan0)的系统上,样本输出如下。多接口系统会为每个接口各打印一份直方图。

再看一个例子,我们用示例 5 中的 tcp:::state-change 探测器展示 TCP 连接持续时长的分布。该探测器在状态切换发生时给出切换前后的状态,因此我们可以通过记录时间戳来度量时长:

  • 当连接从 CLOSED 转为 SYN-SENT 时;或

  • 当连接从 LISTEN 转为 SYN-RECEIVED 时。

随后我们认为:当连接进入 FIN-WAIT2、CLOSING 或 LAST-ACK 状态时,该连接便结束;此事件发生后,我们记录两个事件之间的时间差。为存储初始连接时间戳,我们使用以 args[1]->cs_cid 为索引的数组,cs_cid 是不透明的整数,唯一标识一条连接。也就是说,我们可以假设多个同时存在的连接不会共享同一连接 ID。完整脚本如下:

示例 5:

tcp:::state-change 探测器中,args[5]->tcps_state 给出切换前的状态,args[3]->tcps_state 给出切换后的状态。注意第二个探测器通过检查 dur[args[1]->cs_cid] 是否非零来确认时间戳是否存在:这能确保我们不会为脚本启动时已经存在的连接记录数据。

到目前为止的示例或许已让你相信,网络 provider 是构建网络工具的良好基础——无论这些工具是用于调查某个特定问题,还是用于追踪现有监控工具难以获取的数据。不过,前面看到的大多数示例原则上都可以用基于 BPF 拦截和检查数据包的自定义程序重新实现。但 TCP 探测器让我们能窥见相应 TCP 连接的内部状态,这是 BPF 无法做到的。这让我们可以,例如,在连接发生时监控其 TCP 状态转换:

示例 6

示例 6 的脚本在以连接 ID(args[1]->cs_cid)为索引的 last 数组中记录每条连接上一次状态切换的时间戳。每次发生状态转换时,打印与该连接关联的四元组、自上次状态转换以来经过的时间,以及转换本身(例如“state-established -> state-close-wait”)。

tcp_state_string 数组定义在 /usr/lib/dtrace/tcp.d 中,为每个 TCP 状态提供字符串表示。状态的符号名也可用,例如 TCPS_TIME_WAITTCPS_LAST_ACK

注意,当 TCP 连接结束时——即进入 FIN-WAIT-2、CLOSING 或 LAST-ACK 状态——我们会把 last 数组中的对应条目置零。此时内核中的连接状态即将被拆除,因此若该 CID 被未来某条连接复用,该数组条目将不再有效。

高级技巧

前面提到过,可以按进程追踪网络活动。未来有望通过探测器参数直接取得关联进程的 PID,但目前由于相关数据在内核中的组织方式尚无法做到。不过,借助 fbt provider,仍可以把 PID 或进程名与 TCP 和 UDP 探测器中通过 args[1]->cs_cid 取得的连接 ID 关联起来。完成此事的 D 代码有些晦涩,但已包含在下面的示例中,可在其他脚本中复用。

下面的示例 7 脚本每两秒打印一次按进程汇总的 TCP 活动,报告 TCP 上发送和接收的总字节数,并按进程和四元组细分:

示例 7:

该脚本由若干片段构成。dtrace:::BEGIN 探测器用于初始化一对全局变量,分别用于统计当前时间窗口内发送和接收的 TCP 负载字节数;这两个变量由 profile:::tick-2sec 探测器每两秒重置一次,该探测器还每两秒向终端打印一次汇总数据。四个 fbt 探测器用于填充和清理 procs 数组,该数组把连接 ID 映射到进程名(例如 firefoxsshd)。具体而言,fbt::tcp_usr_attach 探测器在创建新的 TCP 套接字时触发;fbt::sosend_genericfbt::soreceive 探测器在进程通过 TCP 发送或接收数据时触发;fbt::in_pcbdetach 探测器在 TCP 连接关闭时触发。

脚本余下部分执行实际的按进程统计。每当 TCP 栈发送或接收对应 procs 数组中某条目的数据包时,inout 全局变量便相应递增,bytes 数组也相应更新。该数组以进程名和连接四元组为索引,其内容在 profile:::tick-2sec 探测器中打印并清空。

这使得我们可以深入到特定进程的用量层面,而这一任务在没有 DTrace 的情况下相当困难。当然,示例 7 的脚本可以改造为执行不同类型的按进程或按用户统计。例如,可以保留运行累计值,而不是每两秒清空统计;当进程退出时(由 proc:::exit 探测器指示),可将其 TCP 总用量保存以备后续分析。此外,稍作改动,示例 7 也可改为追踪 UDP 而非 TCP,如示例 8 所示。

示例 8:

作为最后一个示例,我们演示如何用 DTrace 深入 TCP 协议的一些较为高级的方面。这里我们主要使用每个 TCP 探测器的第四个参数(args[3])。该参数包含描述连接内部状态的信息,对于研究仅凭单个数据包头部难以观察的现象很有用。

下面我们用 TCP provider 检测乱序段的到达;具体而言,该脚本检查入站的数据包,其序列号与下一个预期序列号(通过 args[3]->tcps_rnxt 取得)不符的情况。如果 FreeBSD 看到这样的段,且重组队列有空间,便将其加入队列;否则将其丢弃。乱序段可能是丢包或多路径的结果;一般而言它们会拖累吞吐量,若在一条连接中占比偏高,应当排查。

示例 9 的脚本按远端主机地址统计乱序段,并计算总字节数和包数以便对比:

示例 9:

延伸阅读

本文旨在介绍 FreeBSD 10 中可用的网络 provider,并让读者体会这些 provider 适合解决的问题类型。完整探测器集的参考资料见 [1-3],Brendan Gregg 与 Jim Mauro 合著的 DTrace 一书 [4] 也有介绍。


Mark Johnston 是软件工程师,居住在安大略省滑铁卢。他于 2013 年在滑铁卢大学取得数学学士学位,自 2010 年起成为 FreeBSD 用户。他对操作系统开发的各个方面都感兴趣,尤其关注调试与性能分析工具。获得 commit 权限后,他的主要工作重心是改进 FreeBSD 的 DTrace 实现。可通过电子邮件 markj@FreeBSD.org 与他联系。

最后更新于