Ao instrumentar o sistema para responder a questões relacionadas ao desempenho, é útil considerar como os dados podem ser agregados para responder a uma questão específica em vez de pensar em termos de dados coletados por testes individuais. Por exemplo, se quisesse saber o número de chamadas do sistema por ID de usuário, você não se preocuparia necessariamente com os dados coletados em cada chamada do sistema. Você simplesmente deseja ver uma tabela de IDs de usuário e chamadas do sistema. Historicamente, você responderia a essa questão coletando dados em cada chamada do sistema e pós-processando os dados usando uma ferramenta como a awk(1) ou perl(1). Entretanto, no DTrace a agregação de dados é uma operação de primeira classe. Este capítulo descreve os recursos do DTrace para manipular agregações.
Uma função de agregação tem a seguinte propriedade:
f(f(x0) U f(x 1) U ... U f(xn)) = f(x 0 U x1 U ... U xn)
onde xn é um conjunto de dados arbitrários. Ou seja, ao aplicar uma função de agregação a subconjuntos do todo e depois aplicá-la de novo aos resultados, o resultado é o mesmo que aplicá-la ao próprio todo. Por exemplo, considere uma função SUM que retorna a soma de um determinado conjunto de dados. Se os dados não processados consistirem em {2, 1, 2, 5, 4, 3, 6, 4, 2}, o resultado da aplicação de SUM a todo o conjunto será {29}. De forma similar, o resultado da aplicação de SUM ao subconjunto consistindo nos três primeiros elementos é {5}, o resultado da aplicação de SUM ao conjunto consistindo nos três elementos subseqüentes é {12}, e o resultado da aplicação de SUM aos três elementos restantes também é {12}. SUM é uma função de agregação porque quando aplicada ao conjunto desses resultados, {5, 12, 12}, é produzido o mesmo resultado, {29}, que ao aplicar SUM aos dados originais.
Nem todas as funções são de agregação. Um exemplo de uma função que não é de agregação é a MEDIAN que determina o elemento mediano do conjunto. (O elemento mediano de um conjunto é aquele para o qual existem tantos elementos maiores que ele quanto menores). MEDIAN é derivada classificando-se o conjunto e selecionando-se o elemento do meio. Retornando aos dados não processados originais, se MEDIAN for aplicada ao conjunto consistindo nos três primeiros elementos, o resultado será {2}. (O conjunto classificado é {1, 2, 2}; {2} é o conjunto que consiste no elemento do meio). Da mesma forma, quando MEDIAN é aplicada aos próximos três elementos, o resultado é {4} e quando MEDIAN é aplicada aos três elementos finais, o resultado é {4}. Quando MEDIAN é aplicada a cada um dos subconjuntos, o resultado é o conjunto {2, 4, 4}. Se MEDIAN for aplicada a esse conjunto, o resultado será {4}. Entretanto, a classificação do conjunto original resulta em {1, 2, 2, 2, 3, 4, 4, 5, 6}. Se MEDIAN for aplicada a esse conjunto, o resultado será {3}. Como esses resultados não coincidem, MEDIAN não é uma função de agregação.
Muitas funções comuns para compreensão de um conjunto de dados são funções de agregação. Essas funções incluem a contagem do número de elementos do conjunto, o cálculo do valor mínimo do conjunto, o cálculo do valor máximo do conjunto e a soma de todos os elementos no conjunto. A determinação da média aritmética do conjunto pode ser construída a partir da função para contar o número de elementos do conjunto e a função para somar o número de elementos do conjunto.
Entretanto, muitas funções úteis não são funções de agregação. Essas funções incluem o cálculo do modo (o elemento mais comum) de um conjunto, o valor mediano do conjunto ou o desvio padrão do conjunto.
A aplicação de funções de agregação aos dados quando são rastreados possui algumas vantagens:
O conjunto inteiro de dados não precisa ser armazenado. Sempre que um novo elemento for adicionado ao conjunto, a função de agregação será calculada de acordo com o conjunto que consiste no resultado intermediário atual e no novo elemento. Depois que o novo resultado for calculado, o novo elemento poderá ser descartado. Esse processo reduz a quantidade de armazenamento exigido por um fator do número de pontos de dados, que geralmente é muito grande.
A coleção de dados não induz a problemas de escalabilidade patológicos. As funções de agregação permitem que os resultados intermediários sejam mantidos por CPU em vez de em uma estrutura de dados compartilhada. O DTrace depois aplica a função de agregação ao conjunto que consiste nos resultados intermediários por CPU para produzir o resultado final de todo o sistema.
O DTrace armazena os resultados das funções de agregação em objetos chamados agregações. Os resultados da agregação são indexados usando-se uma tupla de expressões similar às usadas nas matrizes de associação. Em D, a sintaxe de uma agregação é:
@name[ keys ] = aggfunc ( args );
onde nome é o nome da agregação, chaves é uma lista separada por vírgulas de expressões de D, funçagr é uma das funções de agregação do DTrace e args é uma lista de argumentos separados por vírgulas apropriada para a função de agregação. O nome da agregação é um identificador de D que possui como prefixo o caractere especial @. Todas as agregações nomeadas nos programas em D são variáveis globais; não há agregações locais de cláusula ou segmento. Os nomes de agregação são mantidos em um espaço de nome de identificador separado de outras variáveis globais de D. Lembre-se que a e @a não são a mesma variável se você reutilizar nomes. O nome de agregação especial @ pode ser usado para nomear uma agregação anônima em programas em D simples. O compilador de D trata esse nome como um alias do nome de agregação @_.
As funções de agregação de DTrace são mostradas na tabela a seguir. A maioria das funções de agregação utilizam apenas um único argumento que representa os novos dados.
Tabela 9–1 Funções de agregação do DTrace
Nome da função |
Argumentos |
Resultado |
---|---|---|
count |
nenhum |
O número de vezes chamada. |
sum |
expressão escalar |
O valor total das expressões especificadas. |
avg |
expressão escalar |
A média aritmética das expressões especificadas. |
min |
expressão escalar |
O menor valor entre as expressões especificadas. |
max |
expressão escalar |
O maior valor entre as expressões especificadas. |
lquantize |
expressão escalar, limite inferior, limite superior, valor da etapa |
Uma distribuição de freqüência linear, dimensionada pelo intervalo especificado, dos valores das expressões especificadas. Incrementa o valor no depósito mais alto que é menor que a expressão especificada. |
quantize |
expressão escalar |
Uma distribuição de freqüência em potência de dois dos valores das expressões especificadas. Incrementa o valor no depósito mais alto em potência de dois que é menor que a expressão especificada. |
Por exemplo, para contar o número de chamadas do sistema write(2) no sistema, você poderia usar uma seqüência informativa como uma chave e a função de agregação count():
syscall::write:entry { @counts["write system calls"] = count(); }
O comando dtrace imprime resultados de agregação por padrão quando o processo é encerrado, seja como o resultado de uma ação END explícita ou quando o usuário pressiona Control-C. O seguinte exemplo mostra o resultado da execução desse comando, aguardando-se alguns segundos e pressionando-se Control-C:
# dtrace -s writes.d dtrace: script './writes.d' matched 1 probe ^C write system calls 179 # |
Você pode contar chamadas do sistema por nome de processo usando a variável execname como chave de uma agregação:
syscall::write:entry { @counts[execname] = count(); }
O seguinte exemplo mostra o resultado da execução deste comando, aguardando-se alguns segundos e pressionando-se Control-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 # |
Como alternativa, você pode querer examinar em detalhes as gravações organizadas pelo nome do executável e o descritor de arquivos. O descritor de arquivos é o primeiro argumento de write(2), sendo assim, o seguinte exemplo usa uma chave que consiste em execname e arg0:
syscall::write:entry { @counts[execname, arg0] = count(); }
A execução deste comando resulta em uma tabela com o nome do executável e o descritor de arquivos, conforme mostrado no exemplo a seguir:
# 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 # |
O exemplo a seguir exibe o tempo médio gasto na chamada do sistema write, organizado por nome de processo. Este exemplo usa a função de agregação avg(), especificando a expressão para realizar média como o argumento. O exemplo faz a média do tempo gasto na chamada do sistema:
syscall::write:entry { self->ts = timestamp; } syscall::write:return /self->ts/ { @time[execname] = avg(timestamp - self->ts); self->ts = 0; }
O seguinte exemplo mostra o resultado da execução deste comando, aguardando-se alguns segundos e pressionando-se Control-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 # |
A média pode ser útil, mas geralmente não fornece detalhes suficientes para compreender a distribuição dos pontos de dados. Para compreender a distribuição com mais detalhes, use a função de agregação quantize() conforme mostrado no exemplo a seguir:
syscall::write:entry { self->ts = timestamp; } syscall::write:return /self->ts/ { @time[execname] = quantize(timestamp - self->ts); self->ts = 0; }
Como cada linha de resultado torna-se um diagrama de distribuição de freqüência, o resultado deste script é substancialmente maior que os anteriores. O exemplo a seguir mostra uma seleção de resultado de amostra:
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 |
Observe que as linhas da distribuição de freqüência são sempre valores de potência de dois. Cada linha indica a contagem do número de elementos maiores que ou iguais ao valor correspondente, mas menores que o próximo valor maior de linha. Por exemplo, o resultado acima mostra que iropt tinha 4.149 gravações ocorrendo entre 8.192 nanossegundos e 16.383 nanossegundos, inclusive.
Embora quantize() seja útil para obter uma rápida visualização dos dados, talvez você queira examinar uma distribuição em valores lineares. Para exibir uma distribuição de valor linear, use a função de agregação lquantize (). A função lquantize() possui três argumentos além de uma expressão de D. um limite inferior, um limite superior e uma etapa. Por exemplo, se você quisesse visualizar a distribuição de gravações por descritor de arquivo, uma quantização em potência de dois não seria eficiente. Em vez disso, use uma quantização linear com um pequeno intervalo, conforme mostrado no exemplo a seguir:
syscall::write:entry { @fds[execname] = lquantize(arg0, 0, 100, 1); }
A execução deste script por alguns segundos resulta em uma grande quantidade de informação. O exemplo a seguir mostra uma seleção de resultado típico:
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 |
Você também pode usar a função de agregação lquantize() para agregar no momento a partir de um ponto no passado. Essa técnica permite que você observe uma alteração no comportamento ao longo do tempo. O exemplo a seguir exibe a alteração no comportamento da chamada do sistema durante um processo executando o comando 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; }
O script anterior fornece uma melhor avaliação do comportamento da chamada do sistema quando muitos processos date(1) são executados. Para ver esse resultado, execute sh -c 'while true; do date >/dev/null; done' em uma janela, enquanto executa o script de D em outra. O script produz um perfil do comportamento da chamada do sistema do comando 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 |
Este resultado fornece uma breve idéia das diferentes fases do comando date(1)com relação aos serviços solicitados do kernel. Para entender melhor essas fases, talvez você queira compreender quando quais chamadas do sistema estão sendo chamadas. Se esse for o caso, você poderá alterar o script de D para agregar na variável probefunc em vez de uma seqüência de constante.
Por padrão, várias agregações são exibidas na ordem em que são introduzidas no programa em D. Você pode ignorar esse comportamento usando a função printa() para imprimir as agregações. A função printa () também permite que você formate precisamente os dados da agregação usando uma seqüência de formato, conforme descrito no Capítulo 12Formatação de saída.
Se uma agregação não for formatada com uma instrução printa() no seu programa em D, o comando dtrace obterá um instantâneo dos dados da agregação e imprimirá os resultados uma vez, depois que o rastreio tiver sido concluído com o formato de agregação padrão. Se uma determinada agregação for formatada com uma instrução printa(), o comportamento padrão será desativado. Você pode alcançar resultados equivalentes adicionando a instrução printa(@nome da agregação) a uma cláusula do teste dtrace:::END no programa. O formato de resultado padrão das funções de agregação avg(), count(), min(), max() e sum() exibe um valor decimal inteiro correspondente ao valor agregado de cada tupla. O formato de resultado padrão das funções de agregação lquantize() e quantize() exibe uma tabela ASCII dos resultados. As tuplas de agregação são impressas como se trace() tivesse sido aplicada a cada elemento da tupla.
Ao agregar dados durante algum período de tempo, talvez você queira normalizar os dados em relação a algum fator de constante. Essa técnica permite que você compare dados separados com mais facilidade. Por exemplo, ao agregar chamadas do sistema, talvez você queira resultados de chamadas do sistema como uma taxa por segundo em vez de um valor absoluto durante o curso da execução. A ação normalize() do DTrace permite que você normalize os dados dessa forma. Os parâmetros de normalize() são uma agregação e um fator de normalização. O resultado da agregação mostra cada valor dividido pelo fator de normalização.
O exemplo a seguir mostra como agregar dados por chamada do sistema:
#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); }
A execução do script acima por um breve período de tempo gera o seguinte resultado em um computador desktop:
# 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() define o fator de normalização da agregação especificada, mas essa ação não modifica os dados subjacentes. A função denormalize() possui somente uma agregação. Ao adicionar a ação denormalize ao exemplo anterior, são retornadas contagens de chamada do sistema sem processamento e taxas por segundo:
#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); }
A execução do script acima por um breve período de tempo produz um resultado similar ao seguinte exemplo:
# 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 |
As agregações também podem ser renormalizadas. Se normalize() for chamada mais de uma vez para a mesma agregação, o fator de normalização será o fator especificado na chamada mais recente. O exemplo a seguir imprime taxas por segundo ao longo do tempo:
#pragma D option quiet BEGIN { start = timestamp; } syscall:::entry { @func[execname] = count(); } tick-10sec { normalize(@func, (timestamp - start) / 1000000000); printa(@func); }
Ao usar o DTrace para criar scripts de monitoração simples, você pode cancelar periodicamente os valores em uma agregação usando a função clear (). Essa função possui uma agregação como seu único parâmetro. A função clear() cancela somente os valores da agregação; as chaves da agregação são mantidas. Assim, a presença de uma chave em uma agregação que possui um valor associado de zero indica que a chave tinha um valor diferente de zero que foi subseqüentemente definido como zero como parte de clear(). Para descartar os valores de uma agregação e suas chaves, use trunc(). Consulte Truncando agregações para obter detalhes.
O exemplo a seguir adiciona clear() ao Exemplo 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; }
Enquanto o Exemplo 9–1 mostra a taxa de chamada do sistema durante a invocação de dtrace, o exemplo anterior mostra a taxa da chamada do sistema somente para o período de dez segundos mais recente.
Ao olhar os resultados da agregação, você geralmente só se preocupa com os vários resultados superiores. As chaves e os valores associados a qualquer coisa além dos valores mais altos não são interessantes. Talvez você também queira descartar todo o resultado de uma agregação, removendo as chaves e os valores. A função trunc() do DTrace é usada para ambas as situações.
Os parâmetros de trunc() são uma agregação e um valor opcional de truncamento. Sem o valor de truncamento, trunc () descarta ambos os valores de agregação e as chaves de agregação de toda a agregação. Quando um valor de truncamento n está presente, trunc() descarta valores de agregação e chaves exceto para os valores e chaves associados aos valores de n mais altos. Ou seja, trunc(@foo, 10) trunca a agregação chamada foo após os dez valores principais, enquanto trunc(@foo) descarta toda a agregação. Toda a agregação também será descartada se 0 for especificado como o valor de truncamento.
Para ver os valores inferiores de n em vez dos superiores de n, especifique um valor de truncamento negativo para trunc(). Por exemplo, trunc(@foo, -10) trunca a agregação denominada foo depois dos dez valores inferiores.
O exemplo a seguir aumenta o exemplo da chamada do sistema para exibir somente as taxas de chamada do sistema por segundo dos dez principais aplicativos de chamada do sistema em um período de dez segundos:
#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; }
O exemplo a seguir mostra o resultado da execução do script acima em um laptop com pouca carga:
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 |
Como o DTrace armazena em buffer alguns dados de agregação no kernel, pode não haver espaço disponível quando uma nova chave for adicionada a uma agregação. Nesse caso, os dados serão cancelados, um contador será incrementado e o dtrace irá gerar uma mensagem indicando um cancelamento de agregação. Essa situação raramente acontece porque o DTrace mantém o estado de longa duração (que consiste na chave e no resultado intermediário da agregação) no nível do usuário onde o espaço pode crescer dinamicamente. Caso ocorram cancelamentos de agregação, você poderá aumentar o tamanho do buffer da agregação com a opção aggsize para reduzir a probabilidade de cancelamentos. Você também pode usar essa opção para minimizar as marcas de memória do DTrace. Assim como com qualquer opção de tamanho, aggsize pode ser especificada com qualquer sufixo de tamanho. A política de redimensionamento deste buffer é definida pela opção bufresize . Para obter mais detalhes sobre o armazenamento em buffer, consulte o Capítulo 11Buffers e armazenamento em buffer. Para obter mais detalhes sobre as opções, consulte o Capítulo 16Opções e ajustáveis.
Um método alternativo de eliminar cancelamentos de agregação é aumentar a taxa na qual os dados de agregação são consumidos no nível do usuário. O padrão dessa taxa é de uma vez por segundo e pode ser explicitamente ajustado com a opção aggrate . Assim como com qualquer opção de taxa, aggrate pode ser especificada com qualquer sufixo de tempo, mas o padrão é taxa por segundo. Para obter mais detalhes sobre a opção aggsize, consulte o Capítulo 16Opções e ajustáveis.