En busca de un SQLite más rápido
(avi.im)- En entornos serverless y edge, ejecutar varias instancias de SQLite juntas hace que la espera de I/O síncrona aumente la latencia de cola; investigadores de Helsinki y Cambridge probaron una forma de reducirla con I/O asíncrona y almacenamiento desagregado
- io_uring de Linux permite que la aplicación siga haciendo otras tareas mientras hay solicitudes de I/O en curso mediante una cola de envío y una cola de finalización, lo que sirve como base para reducir el bloqueo de hilos
- En SQLite, si durante la ejecución de
sqlite3_step()una página del B-Tree necesaria no está en caché, se lee el disco con I/O síncrona comoread()de POSIX, por lo que el hilo se detiene hasta que termina la I/O - En lugar de cambiar solo llamadas POSIX, los investigadores adaptaron la VM y el BTree al modelo de ejecución asíncrona en Limbo, un proyecto de reescritura basado en Rust
- En los benchmarks, la latencia de cola p999 se redujo hasta 100 veces, pero p90 y p99 fueron casi iguales a SQLite, y la evaluación con múltiples readers/writers queda como trabajo futuro
Investigación para hacer SQLite más rápido
- Investigadores de la University of Helsinki y de Cambridge abordan la aplicación de I/O asíncrona y almacenamiento desagregado a SQLite en “Serverless Runtime / Database Co-Design With Asynchronous I/O”
- Este paper sirve de base para Limbo, un proyecto de reescritura de SQLite en Rust
- Como es un paper de workshop, es breve y está enfocado en serverless y edge computing
- La idea central es que, aunque SQLite ya es rápido, la latencia de cola en entornos multitenant puede reducirse aún más si se cambia el modelo de ejecución
Cómo io_uring reduce la espera de I/O
- io_uring del kernel de Linux ofrece una interfaz de I/O asíncrona
- Su nombre viene del ring buffer compartido entre espacio de usuario y espacio del kernel, lo que reduce la sobrecarga de copiar buffers entre ambos espacios
- La aplicación puede enviar solicitudes de I/O y seguir haciendo otras tareas hasta que el OS notifique su finalización
- El flujo de funcionamiento es el siguiente
- La llamada al sistema
io_uring_setup()configura dos regiones de memoria: la cola de envío y la cola de finalización - La aplicación coloca solicitudes de I/O en la cola de envío y usa
io_uring_enter()para avisarle al OS que empiece a procesarlas - A diferencia de
read()ywrite(), no bloquea el hilo y devuelve el control al espacio de usuario - La aplicación puede hacer otro trabajo y luego consultar periódicamente la cola de finalización para confirmar que la I/O terminó
- La llamada al sistema
El cuello de botella de I/O síncrona en la ejecución de consultas de SQLite
- Una aplicación SQLite abre el archivo de base de datos con
sqlite3_open(), proceso durante el cual se invoca I/O de bajo nivel del OS, comoopende POSIX sqlite3_prepare()convierte sentencias SQL comoSELECTeINSERTen una secuencia de instrucciones de bytecodesqlite3_step()ejecuta las instrucciones de bytecode hasta producir una fila para la consulta o hasta que termine la ejecución- Si hay una fila para leer, devuelve
SQLITE_ROW - Si la sentencia termina, devuelve
SQLITE_DONE
- Si hay una fila para leer, devuelve
- Durante la ejecución se invoca el pager del backend y se recorre el B-Tree que representa tablas y filas
- Si una página necesaria del B-Tree no está en la caché de páginas de SQLite, ocurre un acceso a disco
- SQLite lee el contenido de la página del disco a memoria con I/O síncrona como
readde POSIX - Durante ese tiempo,
sqlite3_step()bloquea el hilo del kernel - Para hacer trabajo concurrente mientras se espera la I/O, la aplicación necesita usar más hilos
- SQLite lee el contenido de la página del disco a memoria con I/O síncrona como
Por qué se quiere embebeder SQL en serverless y edge
- Si el cómputo serverless se ejecuta en el edge y la base de datos está en la nube, aparece el costo de ida y vuelta de red entre la función serverless y la nube
- Una opción es colocar también los datos en el edge, pero se propone como mejor enfoque embebeder la base de datos dentro del runtime de edge
- Cloudflare Workers ya logra algo de este estilo, pero expone una interfaz KV
- KV no encaja bien en todos los dominios de problema
- Mapear datos tabulares al modelo KV empeora la experiencia de desarrollo
- También introduce costo de serialización y deserialización
- SQL puede ser una mejor opción, y SQLite, al ser una base de datos embebida, puede incluirse directamente en el runtime serverless
Por qué no es fácil cambiar SQLite simplemente a io_uring
- SQLite usa I/O síncrona basada en
read()ywrite()tradicionales de POSIX - Aunque en aplicaciones pequeñas esto quizá no sea un gran problema, puede convertirse en un cuello de botella al ejecutar cientos de bases de datos SQLite en un mismo servidor
- En entornos donde hay que maximizar la utilización de recursos del servidor, la I/O síncrona actúa como una limitación
- SQLite también tiene problemas de concurrencia y multitenancy
- Como la I/O es síncrona y bloqueante, las aplicaciones en la misma máquina compiten por los recursos
- Como resultado, aumenta la latencia
- No es fácil reemplazar las llamadas de I/O de POSIX por io_uring de forma directa
- Las aplicaciones que usan I/O bloqueante deben rediseñarse para ajustarse al modelo de I/O asíncrona de io_uring
- La librería SQLite debe poder devolver el control a la aplicación mientras la I/O sigue en curso
- En vez de cambiar solo algunas llamadas de SQLite, los investigadores eligieron reescribir SQLite en Rust y usar io_uring
El modelo de ejecución asíncrona de Limbo
- Limbo es un proyecto que reescribe SQLite en Rust, y modifica los componentes VM y BTree para soportar I/O asíncrona
- Las instrucciones de bytecode síncronas se reemplazan por sus equivalentes asíncronos
- Por ejemplo, la instrucción
Nextavanza el cursor y, si hace falta, trae la página siguiente- En la versión síncrona anterior, si ocurría I/O de disco, se bloqueaba hasta leer la página y devolverla al llamador
- En la versión asíncrona,
NextAsyncse envía y retorna de inmediato - El llamador luego puede bloquearse o realizar otras tareas
- La I/O asíncrona elimina bloqueos y mejora la concurrencia
- Para aumentar aún más la utilización de recursos, también se propone almacenamiento desagregado, separando el motor de consultas y el motor de almacenamiento
- Como referencia relacionada, también se enlaza Disaggregated Storage - a brief introduction
Resultados de benchmarks y preguntas pendientes
- Los benchmarks simulan un runtime serverless multitenant
- Cada tenant tiene su propia base de datos embebida
- La cantidad de tenants varía de 1 a 100 en incrementos de 10
- En SQLite se usa un hilo separado por tenant, y las consultas se ejecutan en cada hilo para medir el desempeño
- La consulta ejecutada es
SELECT * FROM users LIMIT 100, repetida 1000 veces - Limbo realiza el mismo experimento, pero usando corrutinas de Rust
- Como resultado, la latencia de cola en p999 se reduce hasta 100 veces
- La latencia de consultas de SQLite no se degradó gradualmente con el aumento del número de hilos
- El trabajo sigue en curso y el paper deja varias preguntas abiertas
- En Future Work se contemplan benchmarks adicionales con múltiples readers y writers
- Las ventajas se notan sobre todo después de p999
- El rendimiento en p90 y p99 es casi igual al de SQLite
- El código de Limbo está disponible como open source
- Limbo es actualmente un proyecto oficial de Turso, y también se publicó un texto de presentación
Aún no hay comentarios.