Desde el principio, sabía que la API Bencher Perf
iba a ser uno de los puntos finales más exigentes en términos de rendimiento.
Creo que la razón principal por la que muchas personas han tenido que reinventar la rueda del seguimiento de benchmarks
es que las herramientas existentes no manejan la alta dimensionalidad requerida.
Por “alta dimensionalidad”, me refiero a la capacidad de rastrear el rendimiento a lo largo del tiempo y a través de múltiples dimensiones:
Branches, Testbeds, Benchmarks y Medidas.
Esta capacidad para cortar y dividir en cinco dimensiones diferentes conduce a un modelo muy complejo.
Debido a esta complejidad inherente y la naturaleza de los datos,
consideré usar una base de datos de series temporales para Bencher.
Al final, sin embargo, opté por usar SQLite en su lugar.
Pensé que era mejor hacer cosas que no escalan
en vez de pasar el tiempo extra aprendiendo una arquitectura de base de datos completamente nueva que podría o no ser de ayuda.
Con el tiempo, las demandas en la API de Bencher Perf también han aumentado.
Originalmente, tenías que seleccionar todas las dimensiones que querías trazar manualmente.
Esto creó mucha fricción para los usuarios para llegar a una trama útil.
Para resolver esto, añadí una lista de los Informes más recientes a las Páginas de Perf,
y por defecto, se seleccionaba y trazaba el Informe más reciente.
Esto significa que si había 112 benchmarks en el Informe más reciente, entonces todos los 112 serían trazados.
El modelo también se volvió aún más complicado con la capacidad de rastrear y visualizar Límites de Umbrales.
Con esto en mente, hice algunas mejoras relacionadas con el rendimiento.
Dado que el Gráfico de Perf necesita el Informe más reciente para comenzar a trazar,
refactoricé la API de Informes para obtener los datos de resultados de un Informe en una sola llamada a la base de datos en lugar de iterar.
El intervalo de tiempo para la consulta del Informe predeterminado se estableció en cuatro semanas, en lugar de ser ilimitado.
También limité drásticamente el alcance de todos los manejadores de base de datos, reduciendo la contención de bloqueos.
Para ayudar a comunicar a los usuarios, añadí un indicador de estado cargando tanto para el Gráfico de Perf como para las pestañas de dimensión.
Con todo eso bajo mi cinturón, sabía que realmente necesitaba profundizar aquí
y ponerme mis pantalones de ingeniero de rendimiento.
Nunca había perfilado una base de datos SQLite antes,
y honestamente, realmente nunca había perfilado ninguna base de datos antes.
Ahora espera un minuto, podrías estar pensando.
Mi perfil de LinkedIn dice que fui “Administrador de Bases de Datos” por casi dos años.
¿Y nunca perfilé una base de datos‽
Sí. Supongo que esa es una historia para otro momento.
De ORM a Consulta SQL
El primer obstáculo con el que me encontré fue obtener la consulta SQL de mi código Rust.
Utilizo Diesel como el mapeador objeto-relacional (ORM) para Bencher.
Diesel crea consultas parametrizadas.
Envía la consulta SQL y sus parámetros vinculados por separado a la base de datos.
Es decir, la sustitución la realiza la base de datos.
Por lo tanto, Diesel no puede proporcionar una consulta completa al usuario.
El mejor método que encontré fue usar la función diesel::debug_query para obtener la consulta parametrizada:
Y luego limpiar y parametrizar manualmente la consulta en SQL válido:
Si conoces una mejor manera, ¡por favor házmelo saber!
Esta es la forma que el mantenedor del proyecto sugirió sin embargo,
así que simplemente seguí con ello.
Ahora que tenía una consulta SQL, finalmente estaba listo para… leer muchísima documentación.
Planificador de Consultas SQLite
El sitio web de SQLite tiene una excelente documentación para su Planificador de Consultas.
Explica exactamente cómo SQLite ejecuta tu consulta SQL,
y te enseña qué índices son útiles y en qué operaciones debes prestar atención, como los escaneos completos de tablas.
Para ver cómo el Planificador de Consultas ejecutaría mi consulta Perf,
necesitaba agregar una nueva herramienta a mi cinturón de herramientas: EXPLAIN QUERY PLAN
Puedes prefijar tu consulta SQL con EXPLAIN QUERY PLAN
o ejecutar el comando de punto .eqp on antes de tu consulta.
De cualquier manera, obtuve un resultado que se ve así:
¡Vaya!
Hay mucho aquí.
Pero las tres grandes cosas que me llamaron la atención fueron:
SQLite está creando una vista materializada al vuelo que escanea la tabla boundarycompleta
Luego, SQLite está escaneando la tabla metriccompleta
SQLite está creando dos índices al vuelo
¿Y qué tan grandes son las tablas metric y boundary?
Bueno, resulta que son las dos tablas más grandes,
ya que es donde se almacenan todos los Métricos y Límites.
Dado que este era mi primer rodeo de ajuste de rendimiento de SQLite,
quería consultar a un experto antes de hacer cualquier cambio.
Experto en SQLite
SQLite tiene un modo “experto” experimental que puede habilitarse con el comando .expert.
Sugiere índices para consultas, así que decidí probarlo.
Esto es lo que sugirió:
¡Definitivamente es una mejora!
Eliminó el escaneo de la tabla metric y ambos índices creados al vuelo.
Honestamente, yo no habría logrado obtener los primeros dos índices por mi cuenta.
¡Gracias, Experto en SQLite!
Ahora lo único que queda por eliminar es esa maldita vista materializada creada al vuelo.
Vista Materializada
Cuando agregué la capacidad de rastrear y visualizar los Límites de Umbral el año pasado,
tuve que tomar una decisión en el modelo de la base de datos.
Existe una relación de 1-a-0/1 entre una Métrica y su Límite correspondiente.
Es decir, una Métrica puede relacionarse con cero o un Límite, y un Límite solo puede relacionarse con una Métrica.
Por lo tanto, podría haber simplemente expandido la tabla metric para incluir todos los datos de boundary con cada campo relacionado a boundary siendo nulable.
O podría crear una tabla boundary separada con una clave foránea UNIQUE a la tabla metric.
Para mí, la última opción se sintió mucho más limpia, y pensé que siempre podría ocuparme de cualquier implicación de rendimiento más tarde.
Estas fueron las consultas efectivas usadas para crear las tablas metric y boundary:
Y resulta que “más tarde” había llegado.
Intenté simplemente agregar un índice para boundary(metric_id) pero eso no ayudó.
Creo que la razón tiene que ver con el hecho de que la consulta Perf se origina en la tabla metric
y porque esa relación es 0/1 o dicho de otro modo, nulable, tiene que ser escaneada (O(n))
y no puede ser buscada (O(log(n))).
Esto me dejó con una opción clara.
Necesitaba crear una vista materializada que aplanara la relación metric y boundary
para evitar que SQLite tenga que crear una vista materializada al vuelo.
Esta es la consulta que usé para crear la nueva vista materializada metric_boundary:
Con esta solución, estoy intercambiando espacio por rendimiento en tiempo de ejecución.
¿Cuánto espacio?
Sorprendentemente, solo alrededor de un 4% de aumento, a pesar de que esta vista es para las dos tablas más grandes en la base de datos.
Lo mejor de todo, es que me permite tener todo lo que quiero en mi código fuente.
Crear una vista materializada con Diesel fue sorprendentemente fácil.
Solo tuve que usar las mismas macros que Diesel utiliza cuando genero mi esquema normal.
Dicho esto, aprendí a apreciar mucho más a Diesel a lo largo de esta experiencia.
Consulta Error Adicional para todos los detalles jugosos.
Conclusión
Con los tres nuevos índices y una vista materializada añadidos, esto es lo que ahora muestra el Planificador de Consultas:
¡Mira todas esas búsquedas SEARCH hermosas, todas con índices existentes! 🥲
Y después de desplegar mis cambios en producción:
Ahora era el momento de la prueba final.
¿Qué tan rápido carga esa página de Perf de Rustls?
Aquí incluso te daré una etiqueta de anclaje. Haz clic en ella y luego actualiza la página.
Bencher es un conjunto de herramientas de benchmarking continuo.
¿Alguna vez has tenido un impacto de regresión de rendimiento en tus usuarios?
Bencher podría haber evitado que eso sucediera.
Bencher te permite detectar y prevenir las regresiones de rendimiento antes de que lleguen a producción.
Ejecutar: Ejecute sus benchmarks localmente o en CI usando sus herramientas de benchmarking favoritas. La CLI bencher simplemente envuelve su arnés de benchmarks existente y almacena sus resultados.
Seguir: Sigue los resultados de tus benchmarks con el tiempo. Monitoriza, realiza consultas y representa gráficamente los resultados utilizando la consola web de Bencher basándose en la rama de origen, el banco de pruebas y la medida.
Capturar: Captura las regresiones de rendimiento en CI. Bencher utiliza analíticas de vanguardia y personalizables para detectar regresiones de rendimiento antes de que lleguen a producción.
Por las mismas razones que las pruebas unitarias se ejecutan en CI para prevenir regresiones funcionales, los benchmarks deberían ejecutarse en CI con Bencher para prevenir regresiones de rendimiento. ¡Los errores de rendimiento son errores!
Ya estoy utilizando Bencher con Bencher,
pero todos los adaptadores de arneses de referencia existentes son para arneses de micro-referencia.
La mayoría de los arneses HTTP son realmente arneses de pruebas de carga,
y las pruebas de carga son diferentes de las pruebas de rendimiento.
Además, no estoy buscando expandir Bencher a pruebas de carga en el corto plazo.
Ese es un caso de uso muy diferente que requeriría consideraciones de diseño muy distintas,
como esa base de datos de series temporales, por ejemplo.
Incluso si tuviera pruebas de carga en lugar,
realmente necesitaría estar ejecutándolas contra una extracción reciente de datos de producción para que esto se hubiera detectado.
Las diferencias de rendimiento para estos cambios fueron insignificantes con mi base de datos de prueba.
Haz clic para ver los resultados de la referencia de la base de datos de prueba
Antes:
Después de índices y vista materializada:
Todo esto me lleva a creer que debería crear un micro-referencia
que se ejecute contra el punto final de la API de Perf y utilizar los resultados con Bencher.
Esto requerirá una base de datos de prueba considerable
para asegurarse de que este tipo de regresiones de rendimiento se detecten en CI.
He creado un problema de seguimiento para este trabajo, si te gustaría seguir el progreso.
Todo esto me ha hecho pensar:
¿Y si pudieras hacer [pruebas de instantáneas][snapshot testing] del plan de consulta de tu base de datos SQL?
Es decir, podrías comparar tus planes de consulta de base de datos SQL actuales versus candidatos.
Las pruebas de plan de consulta SQL serían algo así como referencias basadas en el conteo de instrucciones para bases de datos.
El plan de consulta ayuda a indicar que puede haber un problema con el rendimiento en tiempo de ejecución,
sin tener que referenciar realmente la consulta de la base de datos.
También he creado un problema de seguimiento para esto.
¡Por favor, siéntete libre de agregar un comentario con tus pensamientos o cualquier trabajo previo que conozcas!
Bono por Error
Originalmente tenía un error en mi código de vista materializada.
Así se veía la consulta SQL:
¿Ves el problema? No. ¡Yo tampoco!
El problema está justo aquí:
De hecho, debería ser:
Estaba intentando ser demasiado inteligente,
y en mi esquema de vista materializada de Diesel había permitido esta unión:
Asumí que esta macro de alguna manera era lo suficientemente inteligente
para relacionar el alert.boundary_id con el metric_boundary.boundary_id.
Pero lamentablemente, no fue así.
Parece que simplemente eligió la primera columna de metric_boundary (metric_id) para relacionarla con alert.
Una vez que descubrí el error, fue fácil de arreglar.
Solo tuve que usar una unión explícita en la consulta de Perf:
🐰 ¡Eso es todo, amigos!
🤖 Este documento fue generado automáticamente por OpenAI GPT-4. Puede que no sea exacto y contenga errores. Si encuentra algún error, abra un problema en GitHub.