当检测系统以回答有关性能的问题时,可以考虑如何聚合数据以回答特定问题,而不是考虑根据单个探测器收集的数据获得答案。例如,如果要知道某用户 ID 进行的系统调用的数目,您不必关心每次系统调用收集的数据,只需要查看包含用户 ID 和系统调用的表即可。以前,您需要通过在每次系统调用时收集数据,并使用工具(如 awk(1) 或 perl(1))对数据进行后期处理来回答此问题。但是在 DTrace 中,聚合数据非常容易。本章介绍用于处理聚合的 DTrace 工具。
聚合函数具有以下属性:
f(f(x0) U f(x 1) U ... U f(xn)) = f(x 0 U x1 U ... U xn)
其中,xn 是一个包含任意数据的数据集。即,对整个数据集合的子集应用聚合函数,然后再次对结果应用该聚合函数,得出的结果与对整个数据集合本身应用该函数相同。例如,考虑生成给定数据集之和的 SUM 函数。如果原始数据由 {2, 1, 2, 5, 4, 3, 6, 4, 2} 组成,则对整个集合应用 SUM 的结果将是 {29}。同样,对由前三个元素组成的子集应用 SUM 的结果将是 {5},对由接下来的三个元素组成的子集应用 SUM 的结果将是 {12},而对余下三个元素应用 SUM 的结果也是 {12}。SUM 是一个聚合函数,因为将其应用于这些结果的集合 {5, 12, 12} 与将 SUM 应用于原始数据生成的结果相同,即 {29}。
并非所有函数都为聚合函数。用于确定集合的中间元素的 MEDIAN 函数就是一个非聚合函数。(中间元素的定义是:集合中大于它的元素数目与小于它的元素数目相同。)MEDIAN 是通过对集合进行排序,然后选择中间元素获取的。现在返回到最初的原始数据,如果对由前三个元素组成的集合应用 MEDIAN,则结果为 {2}。(经过排序的集合为 {1, 2, 2};{2} 是由中间元素组成的集合。)同样,对接下来的三个元素应用 MEDIAN 将生成 {4},而对最后三个元素应用 MEDIAN 将生成 {4}。因此,对每个子集应用 MEDIAN 将生成集合 {2, 4, 4}。对此集合应用 MEDIAN 将生成结果 {4}。但是,对原始集合进行排序将生成 {1, 2, 2, 2, 3, 4, 4, 5, 6}。对此集合应用 MEDIAN 将生成 {3}。因为这些结果不匹配,所以 MEDIAN 不是聚合函数。
许多用于了解数据集的常见函数都是聚合函数。这些函数包括:用于计算集合中元素数目的函数、用于计算集合的最小值和最大值的函数以及用于对集合中的所有元素求和的函数。可通过用于计算集合中元素数目的函数和用于对集中的元素进行求和的函数,来确定集合的运算方法。
但是,一些有用的函数并非聚合函数。这些函数包括:用于计算集合的模(最常见元素)的函数、用于计算集合的中间元素值的函数以及用于计算集合的标准差的函数。
在跟踪数据时对数据应用聚合函数有许多优点:
无需存储整个数据集。每次将新元素添加到集合中时,只要该集合由当前中间结果和新元素组成,就将计算聚合函数。在计算新结果后,新元素可能会被废弃。此过程将成倍降低数据点需要的存储空间(通常非常大)。
数据收集不会导致异常可伸缩性问题。聚合函数允许在每个 CPU 中保存中间结果,而不是将中间结果保存在共享数据结构中。然后,DTrace 将对由每个 CPU 中的中间结果组成的集合应用聚合函数,以生成最终系统范围的结果。
DTrace 将聚合函数的结果存储在称为聚合的对象中。聚合结果使用类似于关联数组所用的表达式元组来建立索引。在 D 中,聚合的语法如下所示:
@name[ keys ] = aggfunc ( args );
其中,name 是聚合的名称,keys 是用逗号分隔的 D 表达式列表,aggfunc 是其中一个 DTrace 聚合函数,而 args 是用逗号分隔的适用于聚合函数的参数列表。聚合 name 是使用特殊字符 @ 作为前缀的 D 标识符。D 程序中指定的所有聚合都为全局变量;没有线程局部聚合或子句局部聚合。聚合名称保存在与其他 D 全局变量不同的标识符名称空间中。请记住,如果重用名称,则 a 和 @a 不是同一变量。特殊聚合名称 @ 可用于在简单 D 程序中命名匿名聚合。D 编译器将此名称视为聚合名称 @_ 的别名。
下表中显示了 DTrace 聚合函数。大多数聚合函数仅接受表示新数据的单个参数。
表 9–1 DTrace 聚合函数
函数名 |
参数 |
结果 |
---|---|---|
count |
无 |
调用次数。 |
sum |
标量表达式 |
所指定表达式的总计值。 |
avg |
标量表达式 |
所指定表达式的算术平均值。 |
min |
标量表达式 |
所指定表达式中的最小值。 |
max |
标量表达式 |
所指定表达式中的最大值。 |
lquantize |
标量表达式、下限、上限、步长值 |
所指定表达式的值的线性频数分布(按指定范围确定大小)。递增最高存储桶中小于指定表达式的值。 |
quantize |
标量表达式 |
所指定表达式的值的二次方频数分布。递增最高二次方存储桶中小于所指定表达式的值。 |
例如,要计算系统中 write(2) 系统调用数,可以使用 count() 聚合函数,并以提示性字符串作为关键字:
syscall::write:entry { @counts["write system calls"] = count(); }
缺省情况下,当因为显式 END 操作或用户按 Ctrl-C 组合键而使进程终止时,dtrace 命令将列显聚合结果。以下示例输出显示了运行此命令、等待几秒钟,然后按 Ctrl-C 组合键的结果:
# dtrace -s writes.d dtrace: script './writes.d' matched 1 probe ^C write system calls 179 # |
可以通过将 execname 变量用作聚合的关键字来按每个进程名称计算系统调用:
syscall::write:entry { @counts[execname] = count(); }
以下示例输出显示了运行此命令、等待几秒钟,然后按 Ctrl-C 组合键的结果:
# dtrace -s writesbycmd.d dtrace: script './writesbycmd.d' matched 1 probe ^C dtrace 1 cat 4 sed 9 head 9 grep 14 find 15 tail 25 mountd 28 expr 72 sh 291 tee 814 def.dir.flp 1996 make.bin 2010 # |
或者,您可能想要进一步查看按可执行文件的名称和文件描述符组织的输出内容。文件描述符是 write(2) 的第一个参数,所以下面的示例使用由 execname 和 arg0 组成的关键字:
syscall::write:entry { @counts[execname, arg0] = count(); }
运行此命令将会生成包含可执行文件的名称和文件描述符的表,如下例所示:
# dtrace -s writesbycmdfd.d dtrace: script './writesbycmdfd.d' matched 1 probe ^C cat 1 58 sed 1 60 grep 1 89 tee 1 156 tee 3 156 make.bin 5 164 acomp 1 263 macrogen 4 286 cg 1 397 acomp 3 736 make.bin 1 880 iropt 4 1731 # |
以下示例显示按进程名组织的用于写入的系统调用中花费的平均时间。此示例使用 avg() 聚合函数,并将用于求平均值的表达式指定为参数。该示例对系统调用中花费的挂钟时间求平均值。
syscall::write:entry { self->ts = timestamp; } syscall::write:return /self->ts/ { @time[execname] = avg(timestamp - self->ts); self->ts = 0; }
以下示例输出显示了运行此命令、等待几秒钟,然后按 Ctrl-C 组合键的结果:
# dtrace -s writetime.d dtrace: script './writetime.d' matched 2 probes ^C iropt 31315 acomp 37037 make.bin 63736 tee 68702 date 84020 sh 91632 dtrace 159200 ctfmerge 321560 install 343300 mcs 394400 get 413695 ctfconvert 594400 bringover 1332465 tail 1335260 # |
平均值非常有用,但通常不能提供足够的详细信息来帮助您了解数据点的分布。要更详细地了解分布情况,请使用 quantize() 聚合函数,如下例所示:
syscall::write:entry { self->ts = timestamp; } syscall::write:return /self->ts/ { @time[execname] = quantize(timestamp - self->ts); self->ts = 0; }
因为每一行输出都会产生频数分布图,所以此脚本的输出实际上比之前的输出要长一些。以下示例显示了选择的样本输出:
lint value ------------- Distribution ------------- count 8192 | 0 16384 | 2 32768 | 0 65536 |@@@@@@@@@@@@@@@@@@@ 74 131072 |@@@@@@@@@@@@@@@ 59 262144 |@@@ 14 524288 | 0 acomp value ------------- Distribution ------------- count 4096 | 0 8192 |@@@@@@@@@@@@ 840 16384 |@@@@@@@@@@@ 750 32768 |@@ 165 65536 |@@@@@@ 460 131072 |@@@@@@ 446 262144 | 16 524288 | 0 1048576 | 1 2097152 | 0 iropt value ------------- Distribution ------------- count 4096 | 0 8192 |@@@@@@@@@@@@@@@@@@@@@@@ 4149 16384 |@@@@@@@@@@ 1798 32768 |@ 332 65536 |@ 325 131072 |@@ 431 262144 | 3 524288 | 2 1048576 | 1 2097152 | 0 |
请注意,频数分布的行数始终是二次方值。每一行表示大于或等于对应值,但小于下一个更大行值的元素数目。例如,以上输出说明 iropt 在 8,192 纳秒和 16,383 纳秒之间(含 8,192 纳秒和 16,383 纳秒)进行了 4149 次写入操作。
虽然 quantize() 可用于快速了解数据,但您可能想要检查线性值的分布情况。要显示线性值的分布,请使用 lquantize() 聚合函数。除 D 表达式外,lquantize() 函数还接受三个参数:下限、上限和步长。例如,如果想要按文件描述符查看写入分布,则使用二次方量化可能不再有效。应改为使用范围较小的线性量化,如下例所示:
syscall::write:entry { @fds[execname] = lquantize(arg0, 0, 100, 1); }
运行此脚本几秒钟后将会生成大量信息。以下示例显示了一组典型输出:
mountd value ------------- Distribution ------------- count 11 | 0 12 |@ 4 13 | 0 14 |@@@@@@@@@@@@@@@@@@@@@@@@@ 70 15 | 0 16 |@@@@@@@@@@@@ 34 17 | 0 xemacs-20.4 value ------------- Distribution ------------- count 6 | 0 7 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 521 8 | 0 9 | 1 10 | 0 make.bin value ------------- Distribution ------------- count 0 | 0 1 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3596 2 | 0 3 | 0 4 | 42 5 | 50 6 | 0 acomp value ------------- Distribution ------------- count 0 | 0 1 |@@@@@ 1156 2 | 0 3 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 6635 4 |@ 297 5 | 0 iropt value ------------- Distribution ------------- count 2 | 0 3 | 299 4 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 20144 5 | 0 |
还可使用 lquantize() 聚合函数来聚合自过去某个时间点以来的时间。通过此方法,可以观察一段时间内行为的变化。以下示例显示了执行 date(1) 命令的进程的生命周期中系统调用行为的变化:
syscall::exec:return, syscall::exece:return /execname == "date"/ { self->start = vtimestamp; } syscall:::entry /self->start/ { /* * We linearly quantize on the current virtual time minus our * process's start time. We divide by 1000 to yield microseconds * rather than nanoseconds. The range runs from 0 to 10 milliseconds * in steps of 100 microseconds; we expect that no date(1) process * will take longer than 10 milliseconds to complete. */ @a["system calls over time"] = lquantize((vtimestamp - self->start) / 1000, 0, 10000, 100); } syscall::rexit:entry /self->start/ { self->start = 0; }
执行许多 date(1) 进程时,上面的脚本有助于更好地了解系统调用行为。要查看此结果,请在一个窗口中运行 sh -c 'while true; do date >/dev/null; done',同时在另一个窗口中执行 D 脚本。该脚本会生成 date(1) 命令的系统调用行为的配置文件:
# dtrace -s dateprof.d dtrace: script './dateprof.d' matched 218 probes ^C system calls over time value ------------- Distribution ------------- count < 0 | 0 0 |@@ 20530 100 |@@@@@@ 48814 200 |@@@ 28119 300 |@ 14646 400 |@@@@@ 41237 500 | 1259 600 | 218 700 | 116 800 |@ 12783 900 |@@@ 28133 1000 | 7897 1100 |@ 14065 1200 |@@@ 27549 1300 |@@@ 25715 1400 |@@@@ 35011 1500 |@@ 16734 1600 | 498 1700 | 256 1800 | 369 1900 | 404 2000 | 320 2100 | 555 2200 | 54 2300 | 17 2400 | 5 2500 | 1 2600 | 7 2700 | 0 |
通过此输出,可以大致了解与内核的必需服务相关的 date(1) 命令的不同阶段。为了更好地了解这些阶段,您可能需要了解何时进行哪些系统调用。如果这样,可以更改 D 脚本来聚合变量 probefunc 而不是聚合常量字符串。
缺省情况下,多个聚合按照它们在 D 程序中的引入顺序显示。可使用 printa() 函数列显聚合来覆盖此行为。printa() 函数还允许您使用格式字符串精确地设置聚合数据的格式,如第 12 章中所述。
如果在 D 程序中未使用 printa() 语句设置聚合的格式,则 dtrace 命令将对聚合数据进行快照,并在跟踪完成后使用缺省聚合格式立即列显结果。如果使用 printa() 语句设置指定聚合的格式,则将禁用缺省行为。通过将语句 printa(@ aggregation-name) 添加到程序的 dtrace:::END 探测器子句中,可获取相同的结果。avg()、count()、min()、max() 和 sum() 聚合函数的缺省输出格式显示与每个元组的聚合值对应的十进制整数值。lquantize() 和 quantize() 聚合函数的缺省输出格式将显示结果的 ASCII 表。列显聚合元组时,就像已经对每个元组元素应用了 trace() 一样。
在聚合数据一段时间后,您可能需要将与某个常数因子有关的数据标准化。通过此方法,可以更容易地比较不相交的数据。例如,在聚合系统调用时,您可能需要按每秒的速率而不是基于运行过程的绝对值来输出系统调用。DTrace normalize() 操作使您可以按此方式将数据标准化。normalize() 的参数包括聚合和标准化因子。聚合的输出显示除以标准化因子之后的每个值。
以下示例显示如何按系统调用聚合数据:
#pragma D option quiet BEGIN { /* * Get the start time, in nanoseconds. */ start = timestamp; } syscall:::entry { @func[execname] = count(); } END { /* * Normalize the aggregation based on the number of seconds we have * been running. (There are 1,000,000,000 nanoseconds in one second.) */ normalize(@func, (timestamp - start) / 1000000000); }
运行以上脚本一小段时间将会在台式计算机上生成以下输出:
# dtrace -s ./normalize.d ^C syslogd 0 rpc.rusersd 0 utmpd 0 xbiff 0 in.routed 1 sendmail 2 echo 2 FvwmAuto 2 stty 2 cut 2 init 2 pt_chmod 3 picld 3 utmp_update 3 httpd 4 xclock 5 basename 6 tput 6 sh 7 tr 7 arch 9 expr 10 uname 11 mibiisa 15 dirname 18 dtrace 40 ksh 48 java 58 xterm 100 nscd 120 fvwm2 154 prstat 180 perfbar 188 Xsun 1309 .netscape.bin 3005 |
normalize() 对指定的聚合设置标准化因子,但此操作不会修改基础数据。denormalize() 仅接受聚合。将取消标准化操作添加到前面的示例中会同时返回原始系统调用的计数和每秒的速率:
#pragma D option quiet BEGIN { start = timestamp; } syscall:::entry { @func[execname] = count(); } END { this->seconds = (timestamp - start) / 1000000000; printf("Ran for %d seconds.\n", this->seconds); printf("Per-second rate:\n"); normalize(@func, this->seconds); printa(@func); printf("\nRaw counts:\n"); denormalize(@func); printa(@func); }
运行以上脚本一小段时间将生成与以下示例类似的输出:
# dtrace -s ./denorm.d ^C Ran for 14 seconds. Per-second rate: syslogd 0 in.routed 0 xbiff 1 sendmail 2 elm 2 picld 3 httpd 4 xclock 6 FvwmAuto 7 mibiisa 22 dtrace 42 java 55 xterm 75 adeptedit 118 nscd 127 prstat 179 perfbar 184 fvwm2 296 Xsun 829 Raw counts: syslogd 1 in.routed 4 xbiff 21 sendmail 30 elm 36 picld 43 httpd 56 xclock 91 FvwmAuto 104 mibiisa 314 dtrace 592 java 774 xterm 1062 adeptedit 1665 nscd 1781 prstat 2506 perfbar 2581 fvwm2 4156 Xsun 11616 |
也可以重新标准化聚合。如果对同一聚合多次调用 normalize(),则标准化因子将成为最新调用中指定的因子。以下示例列显了一段时间内每秒的速率:
#pragma D option quiet BEGIN { start = timestamp; } syscall:::entry { @func[execname] = count(); } tick-10sec { normalize(@func, (timestamp - start) / 1000000000); printa(@func); }
使用 DTrace 生成简单监视脚本时,可使用 clear() 函数定期清除聚合中的值。此函数仅接受聚合作为其参数。clear() 函数仅清除聚合的值;聚合的关键字将保留。因此,如果聚合中的某个关键字的关联值为零,则表示该关键字具有非零值,但后来作为 clear() 的一部分被设置为零。要同时废弃聚合的值和关键字,请使用 trunc()。有关详细信息,请参见截断聚合。
以下示例将 clear() 添加到示例 9–1 中:
#pragma D option quiet BEGIN { last = timestamp; } syscall:::entry { @func[execname] = count(); } tick-10sec { normalize(@func, (timestamp - last) / 1000000000); printa(@func); clear(@func); last = timestamp; }
虽然示例 9–1 显示了 dtrace 调用的生命周期中系统调用的速率,但上面的示例仅显示了最近 10 秒内的系统调用速率。
查看聚合结果时,您通常只需关心最前面的几个结果。无需关注与最高值之外的任何其他对象关联的关键字和值。您可能还希望废弃整个聚合结果,从而删除关键字和值。DTrace trunc() 函数适用于这两种情况。
trunc() 的参数包括聚合和可选截断值。如果没有截断值,trunc() 将同时废弃整个聚合的聚合值和聚合关键字。如果存在截断值 n,则 trunc() 将废弃聚合值和聚合关键字,但与最高 n 个值关联的值和关键字除外。即,trunc(@foo, 10)将截断前 10 个值之后的名为 foo 的聚合,其中 trunc(@foo) 将废弃整个聚合。如果将 0 指定为截断值,将会废弃整个聚合。
要查看后 n 个值(而不是前 n 个值),请为 trunc() 指定负的截断值。例如,trunc(@foo, -10) 将截断后 10 个值之前的名为 foo 的聚合。
以下示例增加了系统调用示例,以便仅显示 10 秒内最前面 10 个系统调用应用程序的每秒系统调用速率。
#pragma D option quiet BEGIN { last = timestamp; } syscall:::entry { @func[execname] = count(); } tick-10sec { trunc(@func, 10); normalize(@func, (timestamp - last) / 1000000000); printa(@func); clear(@func); last = timestamp; }
以下示例显示在轻负荷膝上型计算机上运行以上脚本的输出:
FvwmAuto 7 telnet 13 ping 14 dtrace 27 xclock 34 MozillaFirebird- 63 xterm 133 fvwm2 146 acroread 168 Xsun 616 telnet 4 FvwmAuto 5 ping 14 dtrace 27 xclock 35 fvwm2 69 xterm 70 acroread 164 MozillaFirebird- 491 Xsun 1287 |
因为 DTrace 将一些聚合数据缓存在内核中,所以向聚合中添加新关键字时可能会出现空间不足的情况。在此情况下,数据将被删除并且计数器会递增,而 dtrace 将生成一条消息,指示进行了聚合删除操作。因为 DTrace 在用户级(可以动态增加空间)保持长期运行状态(由聚合的关键字和中间结果组成),所以这种情况很少发生。如果在极少数情况下发生了聚合删除,则可以使用 aggsize 选项增加聚合缓冲区大小,以减少发生删除的可能性。也可以使用此选项将 DTrace 的内存使用量减至最少。与任何大小选项一样,可以使用任何大小后缀指定 aggsize。此缓冲区的调整大小策略由 bufresize 选项指示。有关缓冲的更多详细信息,请参见第 11 章。有关选项的更多详细信息,请参见第 16 章。
避免聚合删除的另一个方法是提高在用户级使用聚合数据的速率。此速率缺省为每秒一次,并且可使用 aggrate 选项显式调整。与任何速率选项一样,可以使用任何时间后缀指定 aggrate,但缺省为每秒的速率。有关 aggsize 选项的更多详细信息,请参见第 16 章。