- rqlite es una base de datos relacional distribuida, ligera y de código abierto, escrita en Go y desarrollada sobre SQLite y Raft
- Su desarrollo comenzó en 2014 y prioriza la confiabilidad y la calidad; incluso después de más de 10 años de desarrollo y despliegue, se han reportado menos de 10 casos de pánico en entornos de producción
- Probar sistemas distribuidos requiere una consideración cuidadosa en múltiples capas, y sigue una filosofía de mantener la calidad dentro de la simplicidad
La pirámide de pruebas: un enfoque efectivo
- Las pruebas de rqlite siguen la "pirámide de pruebas"
- Pirámide de pruebas: una estructura basada en pruebas unitarias, con pruebas de integración y una cantidad mínima de pruebas end-to-end (E2E)
- Ofrece una suite de pruebas eficiente, fácil de depurar y orientada a objetivos
Pruebas unitarias: el núcleo de la calidad
- Las pruebas unitarias evalúan componentes independientes y ofrecen un equilibrio entre velocidad y precisión
- Gracias a SQLite y a una arquitectura "shared nothing", la mayor parte de la funcionalidad puede cubrirse con pruebas unitarias
- Del código total de rqlite (aproximadamente 75,000 líneas), las pruebas unitarias representan alrededor de 27,000 líneas
- Las pruebas se completan en pocos minutos, lo que permite ejecutarlas con frecuencia durante el desarrollo
Pruebas a nivel de sistema: validación del consenso
- Las pruebas a nivel de sistema validan la interacción entre el módulo de consenso Raft y SQLite
- Elementos principales de prueba:
- Replicación de sentencias SQLite entre nodos
- Operaciones de lectura en distintos niveles de consistencia
- Validación de recuperación ante fallas del clúster y elección de líder
- Unas 7000 líneas de código de prueba cubren de forma integral las interacciones en configuraciones de nodo único y de múltiples nodos
Pruebas end-to-end: una capa mínima
- Las pruebas end-to-end cumplen el rol de smoke tests para verificar el arranque del sistema, la formación del clúster y el funcionamiento básico
- Están escritas en Python y validan funciones clave ejecutando un clúster real de rqlite
- Ejemplo: validación de respaldos hacia AWS S3
- Con alrededor de 5000 líneas de código de prueba, se adopta un enfoque limitado para minimizar el costo de depuración
Pruebas de rendimiento: llevando los límites al máximo
- Las pruebas de rendimiento evalúan métricas como:
- Velocidad máxima de INSERT
- Procesamiento de consultas concurrentes
- Comparación de uso de memoria, CPU y disco
- Incluyen pruebas con bases de datos SQLite de más de 2 GB para analizar la gestión de memoria y los cuellos de botella de escritura en disco
- Permiten detectar problemas de rendimiento y garantizar la estabilidad mediante optimizaciones
Lecciones aprendidas
- Empezar a probar desde el inicio
- Las pruebas unitarias son la forma más efectiva de construir confianza en el sistema
- No se debe postergar la escritura de pruebas unitarias durante el desarrollo, ya que permiten encontrar bugs más rápido que las pruebas de integración o las pruebas E2E
- Mantener simple el código de prueba
- La suite de pruebas no es el lugar para insistir en refactorizaciones complejas ni en el principio DRY (Don't Repeat Yourself)
- Es importante escribir código fácil de entender y permitir también código boilerplate adicional
- Verificar las pruebas
- Al escribir una prueba, se vuelve a ejecutar cambiando temporalmente el resultado esperado por el opuesto
- Una prueba bien escrita debe fallar en ese caso, lo que ayuda a prevenir errores en el propio código de prueba
- No ignorar los fallos de prueba
- Incluso los fallos raros o difíciles de entender aportan información importante sobre el software
- Los casos difíciles de depurar suelen ser oportunidades para descubrir defectos críticos en el código
- Maximizar el determinismo
- Construir mecanismos que permitan ejecutar manualmente los procesos automáticos del sistema
- Ejemplo: la función de snapshots de Raft normalmente se ejecuta de forma semiautomática, pero está diseñada para poder activarse explícitamente durante las pruebas
- Ser deliberado
- Las pruebas de integración de nivel superior o las pruebas E2E solo se agregan cuando su necesidad está claramente demostrada
- El exceso de pruebas puede ralentizar el desarrollo y la depuración
- Aplicar e iterar
- En las pruebas de rendimiento, las llamadas a fsync se identificaron como un cuello de botella importante, y las entradas del log de Raft se comprimen antes de escribirse en disco para optimizar el uso del disco
- Priorizar la eficiencia
- Mantener una suite de pruebas que pueda ejecutarse en pocos minutos permite un desarrollo iterativo rápido
- Esta es una ventaja esencial para mantener y dar vida a un proyecto de código abierto
La calidad primero
- Se respeta la pirámide de pruebas y cada capa de pruebas está diseñada para tener un propósito claro
- A medida que aumenta la complejidad de los sistemas distribuidos, mantener la simplicidad en las pruebas se vuelve clave
- El objetivo es construir una base de datos confiable y fácil de operar
1 comentarios
Comentarios en Hacker News
La primera prueba es la más difícil, pero vale la pena agregarla. Después, las siguientes pruebas se vuelven más fáciles.
Es impresionante el compromiso con el proyecto.
La pirámide de pruebas se entiende, pero muchas veces faltan niveles y hay que mejorar eso rápido.
Parece que hay un error de copiar y pegar en las FAQ.
Está esperando el informe de Jepsen.
También le gustó en formato de video.
Envidia la configuración de pruebas de rendimiento.
Ha usado rqlite y siente que transmite bien la simplicidad.
Está preguntando qué opinan sobre las pruebas de simulación determinista.
Tiene curiosidad por saber si rqlite se usa en entornos reales.
rqlite es un proyecto original y genial.