Perf e flamegraphs



O tópico de melhorar o desempenho dos sistemas operacionais e encontrar gargalos está ganhando imensa popularidade. Neste artigo, falaremos sobre uma ferramenta para encontrar esses mesmos lugares usando o exemplo da pilha de blocos no Linux e um caso de solução de problemas em um host.

Exemplo 1. Teste


Nada funciona


Os testes em nosso departamento são sintéticos no hardware do produto e, posteriormente - testes de software de aplicativos. Recebemos uma unidade Intel Optane para teste. Já escrevemos sobre o teste de unidades Optane em nosso blog .

O disco foi instalado em um servidor padrão criado por um tempo relativamente longo em um dos projetos em nuvem.

Durante o teste, o disco não se mostrou da melhor maneira: durante o teste com a profundidade da fila de 1 solicitação por 1 fluxo, em blocos de 4Kbytes sobre ~ 70Kiops. E isso significa que o tempo de resposta é enorme: aproximadamente 13 microssegundos por solicitação!

É estranho, porque a especificação promete "Latência - Leia 10 µs" e temos 30% a mais, a diferença é bastante significativa. O disco foi reorganizado em outra plataforma, uma montagem mais "nova" usada em outro projeto.

Por que isso funciona?


É engraçado, mas a unidade na nova plataforma funcionou como deveria. Desempenho aumentado, latência diminuída, CPU por prateleira, 1 fluxo por solicitação, blocos de 4K bytes, ~ 106Kiops a ~ 9 microssegundos por solicitação.

E então é hora de comparar as configurações para obter desempenho de pernas largas. Afinal, nos perguntamos por que? Com perf, você pode:

  • Faça leituras de contador de hardware: o número de chamadas de instruções, falhas de cache, ramificações previstas incorretamente etc. (Eventos da PMU)
  • Remova as informações dos pontos de negociação estáticos, o número de ocorrências
  • Realizar rastreamento dinâmico

Para verificação, usamos a amostragem da CPU.

A conclusão é que o perf pode compilar todo o rastreamento de pilha de um programa em execução. Naturalmente, a execução do perf apresentará um atraso na operação de todo o sistema. Mas temos a flag -F # , onde # é a frequência de amostragem, medida em Hz.

É importante entender que quanto maior a frequência de amostragem, maior a probabilidade de receber uma chamada para uma função específica, mas mais freios o gerador de perfil acarreta no sistema. Quanto menor a frequência, maior a chance de não vermos parte da pilha.

Ao escolher uma frequência, você precisa ser guiado pelo senso comum e um truque - tente não definir uma frequência uniforme, para não entrar em uma situação em que algum trabalho executado em um timer com essa frequência entre nas amostras.

Outro ponto que é inicialmente enganador - o software deve ser compilado com o sinalizador -fno-omit-frame-pointer , se isso for, é claro, possível. Caso contrário, no rastreamento, em vez de nomes de funções, veremos sólidos valores desconhecidos . Para alguns softwares, os símbolos de depuração vêm como um pacote separado, por exemplo, someutil-dbg . É recomendável que você os instale antes de executar o perf .

Realizamos as seguintes ações:

  • Obtido fio do git: //git.kernel.dk/fio.git, tag fio-3.9
  • Adicionada a opção -fno-omit-frame-pointer ao CPPFLAGS no Makefile
  • Lançado make -j8

perf record -g ~/fio/fio --name=test --rw=randread --bs=4k --ioengine=pvsync2 --filename=/dev/nvme0n1 --direct=1 --hipri --filesize=1G 

A opção -g é necessária para capturar a pilha de rastreios.

Você pode visualizar o resultado pelo comando:

 perf report -g fractal 

A opção -g fractal é necessária para que as porcentagens que refletem o número de amostras com esta função e mostradas por perf sejam relativas à função de chamada, cujo número de chamadas é considerado 100%.

No final da pilha de chamadas longas na plataforma "build nova", veremos:



E na plataforma "build antigo":



Ótimo! Mas quero lindos desenhos de flamingo.

Construção de flamegramas


Para ser bonita, existem duas ferramentas:

  • Flamegraph relativamente mais estático
  • Flamescope , que permite selecionar um período específico de tempo das amostras coletadas. Isso é muito útil quando o código de pesquisa carrega a CPU com rajadas curtas.

Esses utilitários aceitam resultado perf script> como entrada.

Faça o download do resultado e envie-o através de pipes para svg :

 FlameGraph/stackcollapse-perf.pl ./result | FlameGraph/flamegraph.pl > ./result.svg 

Abra em um navegador e desfrute de uma imagem clicável.

Você pode usar outro método:

  1. Adicionar resultado ao flamescope / exemplo /
  2. Execute python ./run.py
  3. Passamos pelo navegador até a porta 5000 do host local

O que vemos no final?


Um bom fio gasta muito tempo na pesquisa :


Um fio ruim passa tempo em qualquer lugar, mas não nas pesquisas:


À primeira vista, parece que a pesquisa não funciona no host antigo, mas em todos os lugares o kernel 4.15 é do mesmo conjunto e a pesquisa é ativada por padrão nos discos NVMe. Verifique se a pesquisa está ativada no sysfs :

 # cat /sys/class/block/nvme0n1/queue/io_poll 1 

Durante os testes, as chamadas preadv2 com o sinalizador RWF_HIPRI são usadas - uma condição necessária para que a pesquisa funcione. E, se você estudar cuidadosamente o gráfico de chama (ou a captura de tela anterior da saída do relatório perf ), poderá encontrá-lo, mas isso leva uma quantidade muito pequena de tempo.

A segunda coisa que é visível é a pilha de chamadas diferente para a função submit_bio () e a falta de chamadas io_schedule (). Vamos dar uma olhada na diferença dentro de submit_bio ().

Plataforma lenta "build antigo":


Plataforma rápida "fresca":


Parece que em uma plataforma lenta, a solicitação percorre um longo caminho até o dispositivo, ao mesmo tempo em que entra no agendador kyber . Você pode ler mais sobre agendadores de E / S em nosso artigo .

Depois que o kyber foi desligado, o mesmo teste de fio mostrou uma latência média de cerca de 10 microssegundos, exatamente como declarado na especificação. Ótimo!

Mas de onde vem a diferença em outro microssegundo?

E se um pouco mais profundo?


Como já mencionado, o perf permite coletar estatísticas de contadores de hardware. Vamos tentar ver o número de falhas de cache e instruções por ciclo:

 perf stat -e cycles,instructions,cache-references,cache-misses,bus-cycles /root/fio/fio --clocksource=cpu --name=test --bs=4k --filename=/dev/nvme0n1p4 --direct=1 --ioengine=pvsync2 --hipri --rw=randread --filesize=4G --loops=10 





Pode-se ver pelos resultados que uma plataforma rápida executa mais instruções para o ciclo da CPU e tem uma porcentagem menor de erros de cache durante a execução. Obviamente, não entraremos em detalhes da operação de diferentes plataformas de hardware na estrutura deste artigo.

Exemplo 2. Mercearia


Algo está errado


No trabalho de um sistema de armazenamento distribuído, foi observado um aumento na carga da CPU em um dos hosts com um aumento no tráfego de entrada. Hosts são pares, pares e têm hardware e software idênticos.

Vejamos a carga da CPU:

 ~# pidstat -p 1441734 1 Linux 3.13.0-96-generic (lol) 10/10/2018 _x86_64_ (24 CPU) 09:23:30 PM UID PID %usr %system %guest %CPU CPU Command 09:23:44 PM 0 1441734 23.00 1.00 0.00 24.00 4 ceph-osd 09:23:45 PM 0 1441734 85.00 34.00 0.00 119.00 4 ceph-osd 09:23:46 PM 0 1441734 0.00 130.00 0.00 130.00 4 ceph-osd 09:23:47 PM 0 1441734 121.00 0.00 0.00 121.00 4 ceph-osd 09:23:48 PM 0 1441734 28.00 82.00 0.00 110.00 4 ceph-osd 09:23:49 PM 0 1441734 4.00 13.00 0.00 17.00 4 ceph-osd 09:23:50 PM 0 1441734 1.00 6.00 0.00 7.00 4 ceph-osd 

O problema surgiu às 09:23:46 e vemos que o processo funcionou no espaço do kernel exclusivamente por todo o segundo. Vamos ver o que estava acontecendo lá dentro.

Por que tão lento?


Nesse caso, coletamos amostras de todo o sistema:

 perf record -a -g -- sleep 22 perf script > perf.results 

A opção -a é necessária aqui para o perf remover traços de todas as CPUs.

Abra perf.results com flamescope para rastrear o momento de aumento da carga da CPU.

Mapa de calor


À nossa frente está um "mapa de calor", cujos dois eixos (X e Y) representam o tempo.

No eixo X, o espaço é dividido em segundos e, no eixo Y, em segmentos de 20 milissegundos em X segundos. O tempo é executado de baixo para cima e da esquerda para a direita. Os quadrados mais brilhantes têm o maior número de amostras. Ou seja, a CPU no momento trabalhava mais ativamente.

Na verdade, estamos interessados ​​na mancha vermelha no meio. Selecione-o com o mouse, clique e veja o que oculta:


Em geral, já é evidente que o problema é a operação lenta tcp_recvmsg e skb_copy_datagram_iovec .

Para maior clareza, compare com amostras de outro host em que a mesma quantidade de tráfego de entrada não causa problemas:


Com base no fato de termos a mesma quantidade de tráfego recebido, plataformas idênticas que funcionam há muito tempo sem parar, podemos assumir que os problemas surgiram do lado do ferro. A função skb_copy_datagram_iovec copia dados da estrutura do kernel para a estrutura no espaço do usuário para transmitir ao aplicativo. Provavelmente há problemas com a memória do host. Ao mesmo tempo, não há erros nos logs.

Nós reiniciamos a plataforma. Ao carregar o BIOS, vemos uma mensagem sobre uma barra de memória quebrada. Substituição, o host inicia e o problema com uma CPU sobrecarregada não é mais reproduzido.

Postscript


Desempenho do sistema com perf


De um modo geral, em um sistema ocupado, a execução do perf pode apresentar um atraso no processamento de solicitações. O tamanho desses atrasos também depende da carga no servidor.

Vamos tentar encontrar esse atraso:

 ~# /root/fio/fio --clocksource=cpu --name=test --bs=4k --filename=/dev/nvme0n1p4 --direct=1 --ioengine=pvsync2 --hipri --rw=randread --filesize=4G --loops=1 test: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=pvsync2, iodepth=1 fio-3.9-dirty Starting 1 process Jobs: 1 (f=1): [r(1)][100.0%][r=413MiB/s][r=106k IOPS][eta 00m:00s] test: (groupid=0, jobs=1): err= 0: pid=109786: Wed Dec 12 17:25:56 2018 read: IOPS=106k, BW=414MiB/s (434MB/s)(4096MiB/9903msec) clat (nsec): min=8161, max=84768, avg=9092.68, stdev=1866.73 lat (nsec): min=8195, max=92651, avg=9127.03, stdev=1867.13 … ~# perf record /root/fio/fio --clocksource=cpu --name=test --bs=4k --filename=/dev/nvme0n1p4 --direct=1 --ioengine=pvsync2 --hipri --rw=randread --filesize=4G --loops=1 test: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=pvsync2, iodepth=1 fio-3.9-dirty Starting 1 process Jobs: 1 (f=1): [r(1)][100.0%][r=413MiB/s][r=106k IOPS][eta 00m:00s] test: (groupid=0, jobs=1): err= 0: pid=109839: Wed Dec 12 17:27:50 2018 read: IOPS=106k, BW=413MiB/s (433MB/s)(4096MiB/9916msec) clat (nsec): min=8259, max=55066, avg=9102.88, stdev=1903.37 lat (nsec): min=8293, max=55096, avg=9135.43, stdev=1904.01 

A diferença não é muito perceptível, apenas cerca de ~ 8 nanossegundos.

Vamos ver o que acontece se você aumentar a carga:

 ~# /root/fio/fio --clocksource=cpu --name=test --numjobs=4 --bs=4k --filename=/dev/nvme0n1p4 --direct=1 --ioengine=pvsync2 --hipri --rw=randread --filesize=4G --loops=1 test: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=pvsync2, iodepth=1 ... fio-3.9-dirty Starting 4 processes Jobs: 4 (f=4): [r(4)][100.0%][r=1608MiB/s][r=412k IOPS][eta 00m:00s] ~# perf record /root/fio/fio --clocksource=cpu --name=test --numjobs=4 --bs=4k --filename=/dev/nvme0n1p4 --direct=1 --ioengine=pvsync2 --hipri --rw=randread --filesize=4G --loops=1 test: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=pvsync2, iodepth=1 ... fio-3.9-dirty Starting 4 processes Jobs: 4 (f=4): [r(4)][100.0%][r=1584MiB/s][r=405k IOPS][eta 00m:00s] 

Aqui a diferença já está se tornando perceptível. Pode-se dizer que o sistema diminuiu a velocidade em menos de 1%, mas essencialmente a perda de 7Kiops em um sistema muito carregado pode levar a problemas.

É claro que este exemplo é sintético, no entanto, é muito revelador.

Vamos tentar executar outro teste sintético que calcula números primos - sysbench :

 ~# sysbench --max-time=10 --test=cpu run --num-threads=10 --cpu-max-prime=100000 ... Test execution summary: total time: 10.0140s total number of events: 3540 total time taken by event execution: 100.1248 per-request statistics: min: 28.26ms avg: 28.28ms max: 28.53ms approx. 95 percentile: 28.31ms Threads fairness: events (avg/stddev): 354.0000/0.00 execution time (avg/stddev): 10.0125/0.00 ~# perf record sysbench --max-time=10 --test=cpu run --num-threads=10 --cpu-max-prime=100000 … Test execution summary: total time: 10.0284s total number of events: 3498 total time taken by event execution: 100.2164 per-request statistics: min: 28.53ms avg: 28.65ms max: 28.89ms approx. 95 percentile: 28.67ms Threads fairness: events (avg/stddev): 349.8000/0.40 execution time (avg/stddev): 10.0216/0.01 

Aqui você pode ver que mesmo o tempo mínimo de processamento aumentou em 270 microssegundos.

Em vez de uma conclusão


O Perf é uma ferramenta muito poderosa para analisar o desempenho e a depuração do sistema. No entanto, como em qualquer outra ferramenta, você precisa manter-se no controle e lembrar que qualquer sistema carregado sob supervisão rigorosa funciona pior.

Links relacionados:

Source: https://habr.com/ru/post/pt437808/


All Articles