Perf y flamegraphs



El tema de mejorar el rendimiento de los sistemas operativos y encontrar cuellos de botella está ganando una inmensa popularidad. En este artículo, hablaremos sobre una herramienta para encontrar estos lugares utilizando el ejemplo de la pila de bloques en Linux y un caso de resolución de problemas de un host.

Ejemplo 1. Prueba


Nada funciona


Las pruebas en nuestro departamento son sintéticas en el hardware del producto y, más tarde, pruebas de software de aplicación. Recibimos una unidad Intel Optane para realizar pruebas. Ya escribimos sobre probar las unidades Optane en nuestro blog .

El disco se instaló en un servidor estándar construido durante un tiempo relativamente largo en uno de los proyectos en la nube.

Durante la prueba, el disco no se mostró de la mejor manera: durante la prueba con la profundidad de la cola de 1 solicitud por 1 flujo, en bloques de 4Kbytes aproximadamente ~ 70Kiops. Y esto significa que el tiempo de respuesta es enorme: ¡aproximadamente 13 microsegundos por solicitud!

Es extraño, porque la especificación promete "Latencia - Leer 10 µs", y obtuvimos un 30% más, la diferencia es bastante significativa. El disco se reorganizó a otra plataforma, un ensamblaje más "nuevo" utilizado en otro proyecto.

Por que funciona


Es divertido, pero la unidad en la nueva plataforma funcionó como debería. Aumento del rendimiento, disminución de la latencia, CPU por estante, 1 transmisión por solicitud, bloques de 4 Kbytes, ~ 106Kiops a ~ 9 microsegundos por solicitud.

Y luego es hora de comparar la configuración para obtener el rendimiento de las piernas anchas. Después de todo, nos preguntamos por qué. Con perf, puedes:

  • Tome las lecturas del contador de hardware: la cantidad de llamadas de instrucciones, errores de caché, ramas predichas incorrectamente, etc. (Eventos de PMU)
  • Eliminar información de puntos comerciales estáticos, el número de ocurrencias
  • Realizar rastreo dinámico

Para la verificación, usamos muestreo de CPU.

La conclusión es que perf puede compilar todo el seguimiento de la pila de un programa en ejecución. Naturalmente, ejecutar perf introducirá un retraso en la operación de todo el sistema. Pero tenemos la bandera -F # , donde # es la frecuencia de muestreo, medida en Hz.

Es importante comprender que cuanto mayor sea la frecuencia de muestreo, más probabilidades hay de capturar una llamada a una función en particular, pero más frenos trae el generador de perfiles al sistema. Cuanto menor es la frecuencia, mayor es la posibilidad de que no veamos parte de la pila.

Al elegir una frecuencia, debe guiarse por el sentido común y un truco: trate de no establecer una frecuencia uniforme, para no entrar en una situación en la que un trabajo que se ejecuta en un temporizador con esta frecuencia ingresa a las muestras.

Otro punto que es inicialmente engañoso: el software debe compilarse con el indicador -fno-omit-frame-pointer , si esto es posible, por supuesto. De lo contrario, en la traza, en lugar de nombres de funciones, veremos valores sólidos desconocidos . Para algunos programas, los símbolos de depuración vienen como un paquete separado, por ejemplo, someutil-dbg . Se recomienda que los instale antes de ejecutar perf .

Realizamos las siguientes acciones:

  • Tomado de fio de git: //git.kernel.dk/fio.git, etiqueta fio-3.9
  • Se agregó la opción -fno-omit-frame-pointer a CPPFLAGS en Makefile
  • Lanzado make -j8

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

La opción -g es necesaria para capturar la pila de trazas.

Puede ver el resultado con el comando:

 perf report -g fractal 

La opción fractal -g es necesaria para que los porcentajes que reflejan el número de muestras con esta función y se muestren por perf sean relativos a la función de llamada, cuyo número de llamadas se toma como 100%.

Hacia el final de la larga pila de llamadas fio en la plataforma de "nueva construcción", veremos:



Y en la plataforma "old build":



Genial Pero quiero bellos flamegrafs.

Construyendo flamegramas


Para ser bella, hay dos herramientas:

  • Relativamente más flamegraph estático
  • Flamescope , que permite seleccionar un período específico de tiempo de las muestras recolectadas. Esto es muy útil cuando el código de búsqueda carga la CPU con ráfagas cortas.

Estas utilidades aceptan perf script> result como entrada.

Descargue el resultado y envíelo a través de tuberías a svg :

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

Abrir en un navegador y disfrutar de una imagen en la que se puede hacer clic

Puedes usar otro método:

  1. Añadir resultado a flamescope / example /
  2. Ejecute python ./run.py
  3. Pasamos por el navegador hasta el puerto 5000 del host local.

¿Qué vemos al final?


Un buen fio pasa mucho tiempo encuestando :


Un fio malo pasa tiempo en cualquier parte, pero no en encuestas:


A primera vista, parece que el sondeo no funciona en el host anterior, pero en todas partes el núcleo 4.15 es del mismo ensamblaje y el sondeo está habilitado de forma predeterminada en los discos NVMe. Compruebe si el sondeo está habilitado en sysfs :

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

Durante las pruebas, se utilizan llamadas preadv2 con el indicador RWF_HIPRI , una condición necesaria para que el sondeo funcione. Y, si estudia cuidadosamente el gráfico de la llama (o la captura de pantalla anterior de la salida del informe de rendimiento), puede encontrarlo, pero lleva una cantidad muy pequeña de tiempo.

La segunda cosa que es visible es la pila de llamadas diferente para la función submit_bio () y la falta de llamadas io_schedule (). Echemos un vistazo más de cerca a la diferencia dentro de submit_bio ().

Plataforma lenta "construcción anterior":


Plataforma rápida "fresca":


Parece que en una plataforma lenta, la solicitud llega muy lejos al dispositivo, al mismo tiempo que ingresa al programador de Kyber . Puede leer más sobre los programadores de E / S en nuestro artículo .

Una vez que kyber se apagó, la misma prueba de fio mostró una latencia promedio de aproximadamente 10 microsegundos, tal como se indica en la especificación. Genial

Pero, ¿de dónde viene la diferencia en otro microsegundo?

¿Y si un poco más profundo?


Como ya se mencionó, perf le permite recopilar estadísticas de contadores de hardware. Intentemos ver la cantidad de errores de caché e instrucciones 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 





De los resultados se puede ver que una plataforma rápida ejecuta más instrucciones para el ciclo de la CPU y tiene un menor porcentaje de errores de caché durante la ejecución. Por supuesto, no entraremos en detalles sobre el funcionamiento de diferentes plataformas de hardware en el marco de este artículo.

Ejemplo 2. Tienda de comestibles


Algo va mal


En el trabajo de un sistema de almacenamiento distribuido, se observó un aumento en la carga en la CPU en uno de los hosts con un aumento en el tráfico entrante. Los hosts son pares, pares y tienen hardware y software idénticos.

Veamos la carga de la 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 

El problema surgió a las 09:23:46 y vemos que el proceso funcionó en el espacio del núcleo exclusivamente durante todo el segundo. Veamos lo que estaba sucediendo adentro.

¿Por qué tan lento?


En este caso, tomamos muestras de todo el sistema:

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

Aquí se necesita la opción -a para que perf elimine los rastros de todas las CPU.

Abra perf.results con flamescope para rastrear el momento de mayor carga de CPU.

Mapa de calor


Ante nosotros hay un "mapa de calor", cuyos dos ejes (X e Y) representan el tiempo.

En el eje X, el espacio se divide en segundos, y en el eje Y, en segmentos de 20 milisegundos en X segundos. El tiempo corre de abajo hacia arriba y de izquierda a derecha. Los cuadrados más brillantes tienen el mayor número de muestras. Es decir, la CPU en este momento trabajó más activamente.

En realidad, estamos interesados ​​en la mancha roja en el medio. Selecciónelo con el mouse, haga clic y vea qué oculta:


En general, ya es evidente que el problema es la operación lenta tcp_recvmsg y skb_copy_datagram_iovec en él.

Para mayor claridad, compare con muestras de otro host en el que la misma cantidad de tráfico entrante no causa problemas:


Basado en el hecho de que tenemos la misma cantidad de tráfico entrante, plataformas idénticas que han estado funcionando durante mucho tiempo sin parar, podemos suponer que los problemas surgieron del lado del hierro. La función skb_copy_datagram_iovec copia datos de la estructura del núcleo a la estructura en el espacio del usuario para pasar a la aplicación. Probablemente haya problemas con la memoria del host. Al mismo tiempo, no hay errores en los registros.

Reiniciamos la plataforma. Al cargar el BIOS, vemos un mensaje sobre una barra de memoria rota. Reemplazo, el host se inicia y el problema con una CPU sobrecargada ya no se reproduce.

Postdata


Rendimiento del sistema con perf


En términos generales, en un sistema ocupado, ejecutar perf puede introducir un retraso en el procesamiento de solicitudes. El tamaño de estos retrasos también depende de la carga en el servidor.

Intentemos encontrar este retraso:

 ~# /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 

La diferencia no es muy notable, solo alrededor de ~ 8 nanosegundos.

Veamos qué sucede si aumenta la 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] 

Aquí la diferencia ya se está haciendo notable. Se puede decir que el sistema se desaceleró en menos del 1%, pero esencialmente perder alrededor de 7 Kiops en un sistema muy cargado puede provocar problemas.

Está claro que este ejemplo es sintético, sin embargo es muy revelador.

Intentemos ejecutar otra prueba sintética que calcule 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 

Aquí puede ver que incluso el tiempo mínimo de procesamiento aumentó en 270 microsegundos.

En lugar de una conclusión


Perf es una herramienta muy poderosa para analizar el rendimiento del sistema y la depuración. Sin embargo, como con cualquier otra herramienta, debe mantenerse en control y recordar que cualquier sistema cargado bajo estrecha supervisión funciona peor.

Enlaces relacionados:

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


All Articles