Solaris 动态跟踪指南

第 9 章 聚合

当检测系统以回答有关性能的问题时,可以考虑如何聚合数据以回答特定问题,而不是考虑根据单个探测器收集的数据获得答案。例如,如果要知道某用户 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 不是聚合函数。

许多用于了解数据集的常见函数都是聚合函数。这些函数包括:用于计算集合中元素数目的函数、用于计算集合的最小值和最大值的函数以及用于对集合中的所有元素求和的函数。可通过用于计算集合中元素数目的函数和用于对集中的元素进行求和的函数,来确定集合的运算方法。

但是,一些有用的函数并非聚合函数。这些函数包括:用于计算集合的模(最常见元素)的函数、用于计算集合的中间元素值的函数以及用于计算集合的标准差的函数。

在跟踪数据时对数据应用聚合函数有许多优点:

聚合

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) 的第一个参数,所以下面的示例使用由 execnamearg0 组成的关键字:

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(),则标准化因子将成为最新调用中指定的因子。以下示例列显了一段时间内每秒的速率:


示例 9–1 renormalize.d:重新标准化聚合

#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 章