El análisis de rendimiento y ajuste es una poderosa herramienta para verificar la conformidad del rendimiento para los clientes.
El análisis de rendimiento se puede utilizar para identificar cuellos de botella en el programa, aplicando un enfoque científico al verificar experimentos de ajuste. Este artículo define un enfoque general para el análisis de rendimiento y ajuste usando como ejemplo un servidor web en Go.
Go es especialmente adecuado aquí, ya que cuenta con herramientas de perfilado pprof en la biblioteca estándar.

Estrategia
Creamos una lista resumida para nuestro análisis estructural. Intentaremos utilizar algunos datos para tomar decisiones en lugar de hacer cambios basados en la intuición o suposiciones. Para ello, procederemos de la siguiente manera:
- Definimos los límites de optimización (requisitos);
- Calculamos la carga transaccional del sistema;
- Realizamos la prueba (creamos datos);
- Observamos;
- Analizamos: ¿se cumplen todos los requisitos?
- Ajustamos de manera científica, formulamos una hipótesis;
- Realizamos un experimento para verificar esta hipótesis.

Arquitectura de un servidor HTTP simple
Para este artículo, utilizaremos un pequeño servidor HTTP en Golang. Todo el código de este artículo se puede encontrar .
La aplicación analizada es un servidor HTTP que consulta Postgresql en cada solicitud. Además, se utilizan Prometheus, node_exporter y Grafana para recopilar y visualizar métricas de la aplicación y del sistema.

Para simplificar, asumimos que para el escalado horizontal (y simplificando cálculos) cada servicio y base de datos se despliegan juntos:

Definimos los objetivos
En este paso, nos definimos un objetivo. ¿Qué estamos tratando de analizar? ¿Cómo sabremos cuándo es momento de finalizar? En este artículo, asumiremos que tenemos clientes y que nuestro servicio manejará 10,000 solicitudes por segundo.
En se examinan en detalle las formas de selección y modelado. Procedamos de la misma manera, construyendo modelos:
- Latencia: el 99% de las solicitudes deben completarse en menos de 60 ms;
- Costo: el servicio debe consumir la menor cantidad de dinero que consideremos razonablemente posible. Para ello, maximizamos el rendimiento;
- Planificación de capacidades: se necesita comprender y documentar cuántas instancias de la aplicación deberán iniciarse, incluyendo la función general de escalabilidad, así como cuántas instancias serán necesarias para satisfacer los requisitos iniciales de carga y garantizar. .
La latencia puede requerir optimización adicional al análisis, pero el ancho de banda ciertamente debe ser analizado. En el proceso SRE, el requisito de latencia proviene del cliente y/o del negocio, representado por el propietario del producto. Y nuestro servicio cumplirá con este compromiso desde el principio sin ningún ajuste.
Configuración del entorno de prueba
Con el entorno de prueba, podremos aplicar una carga medida a nuestro sistema. Se generarán datos de rendimiento del servicio web para el análisis.
Carga transaccional
Este entorno utiliza para crear una frecuencia de solicitudes HTTP ajustable, hasta que se detenga:
$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta reportMonitoreo
Durante la ejecución, se aplicará la carga transaccional. Además de las métricas de la aplicación (número de solicitudes, latencias de respuesta) y del sistema operativo (memoria, CPU, IOPS), se ejecutará la profilación de la aplicación para entender dónde están los problemas y cómo se consume el tiempo del procesador.
Profilación
La profilación es un tipo de medida que permite ver a dónde va el tiempo de CPU durante la ejecución de la aplicación. Permite determinar dónde y cuánto tiempo de CPU se está utilizando:

Estos datos se pueden utilizar durante el análisis para obtener información sobre el tiempo de CPU malgastado y el trabajo inútil realizado. Go (pprof) puede generar perfiles y visualizarlos en forma de flame graph, utilizando un conjunto estándar de herramientas. Hablaré sobre su uso y la guía de configuración más adelante en el artículo.
Ejecución, monitoreo, análisis.
Realizaremos un experimento. Vamos a ejecutar, observar y analizar hasta que el rendimiento nos satisfaga. Elegiremos aleatoriamente una baja carga para aplicarla en la obtención de resultados de las primeras observaciones. En cada paso posterior, aumentaremos la carga con un coeficiente de escala elegido con cierto rango. Cada ejecución de la prueba de carga se realiza ajustando la cantidad de solicitudes: make load-test LOAD_TEST_RATE=X.
50 solicitudes por segundo

Presta atención a los dos gráficos superiores. El superior izquierdo muestra que nuestra aplicación maneja 50 solicitudes por segundo (según su perspectiva), y el superior derecho — la duración de cada solicitud. Ambos parámetros nos ayudan a ver y analizar: si estamos dentro de nuestros límites de rendimiento o no. La línea roja en el gráfico Latencia de Solicitud HTTP muestra un SLO de 60 ms. En la línea se observa que estamos muy por debajo de nuestro tiempo máximo de respuesta.
Veamos desde el punto de vista del costo:
10000 solicitudes por segundo / 50 solicitudes por servidor = 200 servidores + 1
Todavía podemos mejorar este indicador.
500 solicitudes por segundo
Cosas más interesantes comienzan a suceder cuando la carga es de 500 solicitudes por segundo:

De nuevo en el gráfico superior izquierdo, se puede ver que la aplicación registra una carga normal. Si no es así, hay un problema con el servidor en el que se ejecuta la aplicación. El gráfico de latencia de respuesta en la parte superior derecha muestra que 500 solicitudes por segundo resultaron en una latencia de respuesta de 25-40 ms. El percentil 99 todavía se mantiene muy bien dentro del SLO de 60 ms mencionado arriba.
Desde el punto de vista del costo:
10000 solicitudes por segundo / 500 solicitudes por servidor = 20 servidores + 1
Todavía se puede mejorar.
1000 solicitudes por segundo

¡Gran inicio! La aplicación muestra que procesó 1000 solicitudes por segundo, sin embargo, el límite de latencia fue superado desde el punto de vista del SLO. Esto se puede ver en la línea p99 en el gráfico superior derecho. A pesar de que la línea p100 está mucho más alta, las latencias reales superan el máximo de 60 ms. Vamos a profundizar en el perfilado para averiguar qué está haciendo realmente la aplicación.
Profilación
Para el perfilado, establecemos la carga en 1000 solicitudes por segundo, luego usamos pprof para capturar datos, para descubrir dónde la aplicación está gastando tiempo de CPU. Esto se puede hacer activando el endpoint HTTP pprof, después de lo cual podemos guardar los resultados bajo carga utilizando curl:
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profLos resultados pueden mostrarse así:
$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof
El gráfico muestra dónde y cuánto tiempo de CPU está utilizando la aplicación. Según la descripción de :
En el eje X tenemos el llenado del perfil de pila, ordenado alfabéticamente (esto no es tiempo), el eje Y muestra la profundidad de la pila, contando desde cero en [top]. Cada rectángulo representa un marco de pila. Cuanto más ancho es el marco, más frecuentemente aparece en las pilas. Lo que está arriba consume CPU, y lo que está abajo son los elementos secundarios. Los colores, por lo general, no significan nada y se eligen al azar para diferenciar los cuadros.
Análisis — hipótesis
Para la configuración, nos concentraremos en tratar de encontrar gastos inútiles de tiempo de CPU. Buscaremos las principales fuentes de estos gastos inútiles y las eliminaremos. Bueno, considerando que el perfilado revela con bastante precisión dónde la aplicación gasta su tiempo de CPU, probablemente tendremos que hacerlo varias veces, además de necesitar modificar el código fuente de la aplicación, reiniciar las pruebas y observar que el rendimiento se aproxima al deseado.
Siguiendo las recomendaciones de Brendan Gregg, leeremos el gráfico de arriba hacia abajo. Cada línea representa un marco de pila (llamada a función). La primera línea es el punto de entrada del programa, el padre de todas las demás llamadas (en otras palabras, todas las otras llamadas tendrán esta en su pila). La siguiente línea ya es diferente:
![]()
Si pasas el cursor sobre el nombre de la función en el gráfico, se mostrará el tiempo total que estuvo en la pila durante la depuración. La función HTTPServe estuvo allí el 65% del tiempo, otras funciones de runtime, runtime.mcall, mstart y gc, ocuparon el resto del tiempo. Un dato interesante: el 5% del tiempo total se gastó en solicitudes DNS:

Las direcciones que busca el programa pertenecen a Postgresql. Hacemos clic en FindByAge:

Curiosamente, el programa muestra que en principio hay tres fuentes principales que añaden latencia: abrir y cerrar conexiones, solicitar datos y conectarse a la base de datos. El gráfico muestra que las solicitudes DNS, abrir y cerrar conexiones ocupan alrededor del 13% de todo el tiempo de ejecución.
Hipótesis: La reutilización de conexiones mediante un pool debería reducir el tiempo de una sola solicitud HTTP, permitiendo mayor ancho de banda y menores latencias..
Configuración de la aplicación — experimento.
Actualizamos el código fuente, intentamos eliminar la conexión a Postgresql en cada solicitud. Primera opción — uso a nivel de aplicación. En este experimento, vamos un pool de conexiones usando el controlador SQL para Go:
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
if err != nil {
return nil, err
}Ejecución, monitoreo, análisis.
Después de reiniciar la prueba con 1000 solicitudes por segundo, se puede ver que el p99 de las latencias llegó a la normalidad con un SLO de 60 ms.
¿Qué pasa con el costo?
10000 solicitudes por segundo / a 1000 solicitudes por servidor = 10 servidores + 1
¡Hagamos aún mejor!
2000 solicitudes por segundo.

El duplicar la carga muestra lo mismo; el gráfico en la esquina superior izquierda demuestra que la aplicación logra procesar 2000 solicitudes por segundo, con un p100 por debajo de 60 ms, y el p99 cumple con el SLO.
Desde el punto de vista del costo:
10000 solicitudes por segundo / a 2000 solicitudes por servidor = 5 servidores + 1
3000 solicitudes por segundo.

Aquí la aplicación puede manejar 3000 solicitudes con una latencia p99 menor a 60 ms. El SLO no se incumple, y el costo se acepta así:
10000 solicitudes por segundo / a 3000 solicitudes por servidor = 4 servidores + 1 (el autor redondeó hacia arriba, nota del traductor)
Intentemos otra ronda de análisis.
Análisis — hipótesis
Recopilamos y mostramos los resultados de la depuración de la aplicación a 3000 solicitudes por segundo:

Todavía se gasta un 6% del tiempo en establecer conexiones. La configuración del pool mejoró el rendimiento, pero aún se observa que la aplicación continúa trabajando en la creación de nuevas conexiones con la base de datos.
Hipótesis: Las conexiones, a pesar de la existencia del pool, aún se están desechando y limpiando, por lo que la aplicación necesita reinstalarlas. Establecer el número de conexiones en espera al tamaño del pool debería ayudar con la latencia al minimizar el tiempo que la aplicación pasa creando una conexión..
Configuración de la aplicación — experimento.
Intentamos establecer igual al tamaño del pool (también descrito ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Ejecución, monitoreo, análisis.
3000 solicitudes por segundo.

p99 menor a 60 ms con un p100 significativamente menor.

La verificación del flame graph muestra que establecer conexiones ya no es notable. Verificamos más detalladamente. pg(*conn).query — tampoco notamos el establecimiento de la conexión aquí.

Conclusión
El análisis del rendimiento es crítico para entender si se satisfacen las expectativas del cliente y los requisitos no funcionales. El análisis, mediante la comparación de observaciones con las expectativas de los clientes, puede ayudar a determinar lo que es aceptable y lo que no. Go proporciona herramientas eficaces integradas en la biblioteca estándar que facilitan la realización del análisis de manera sencilla y accesible.
Fuente: habr.com
