“Si buscas tranquilidad, haz menos.
— Marco Aurelio
La mayoría de las bases de datos que ejecutan software hoy en día funcionan en un arquitectura cliente-servidor. En esta arquitectura, el servidor es el sistema central que gestiona los datos. Procesa solicitudes y envía respuestas a los clientes. Los clientes aquí se refieren a usuarios o aplicaciones que interactúan con la base de datos a través del servidor.
Una forma sencilla de comprender esta arquitectura es utilizar la analogía de las bibliotecas. El servidor es el bibliotecacada dato es un libroy el cliente es un visitante. En este mundo, los visitantes no eligen libros directamente de las estanterías. En cambio, deben acudir al bibliotecario, quien ha organizado meticulosamente su biblioteca para que sea más fácil encontrar un libro. En este mundo, el acceso de un visitante a la biblioteca está mediado completamente por el personal de la biblioteca (del lado del servidor).
Esta es una arquitectura bastante cuidada. Sin embargo, para aplicaciones más pequeñas y livianas, la ingeniería es una exageración. Si sólo tienes unos pocos libros, ¿por qué necesitas construir varios estantes, y mucho menos varias habitaciones? La alternativa a la arquitectura cliente-servidor es la arquitectura de un solo archivo utilizado por el base de datos SQLite.
Para los no iniciados, SQLite es el ideal platónico de las bases de datos. A diferencia de ejecutar un servidor completo para gestionar el acceso a los datos, esta base de datos se aloja íntegramente en un único archivo. Luego, su aplicación puede crear, leer, actualizar y destruir datos simplemente modificando este archivo. Cuando implementa una aplicación web respaldada por una base de datos cliente-servidor, no está implementando un servicio sino dos servicios: uno para su aplicación y otro para su base de datos. Con SQLite, sólo tienes que implementar un único servicio: tu aplicación con el archivo SQLite incluido. Esto significa menos complejidad y menos costo.
Volviendo a nuestra analogía, usar SQLite es como tener un único cuaderno en el que se almacenan todos tus datos. Ni estanterías, ni bibliotecas, ni bibliotecarios. Simplemente abre el libro y agrega, elimina o actualiza tus datos. Quizás puedas ser más sofisticado y agregar un índice al final de tu libro para acelerar la búsqueda. Puedes imaginar cuánto más simple sería esto.
Sin embargo, como dicen en economía: no hay soluciones, sólo hay compensaciones. SQLite no es perfecto y existen razones válidas por las que rara vez se ha utilizado en producción. En este artículo, destacaré algunos de los problemas que han afectado a SQLite y cómo los avances recientes han eliminado estas barreras.
El problema principal en SQLite ha sido tradicionalmente relacionados con la concurrencia. SQLite utiliza un bloqueo de escritura para garantizar que solo se produzca una operación de escritura a la vez. No queremos que las transacciones interfieran entre sí. Si intenta enviar solicitudes de escritura simultáneas, a menudo obtendrá un SQLITE_BUSY
error y una de las transacciones se habrá perdido. En el caso de solicitudes simultáneas, queremos que las transacciones se pongan en cola y funcionen bien entre sí.
Desafortunadamente, el modo de transacción predeterminado en SQLite no facilita esto. Algunos antecedentes importantes: transacción normalmente implica una serie de bases de datos declaracionescomo lecturas y escrituras, que se ejecutan juntas.
-- An example transaction
BEGIN DEFERRED TRANSACTION;
SELECT * FROM inventory WHERE id = 1; -- Statement 1
UPDATE inventory SET stock = stock + 1 WHERE id = 1; -- Statement 2
El modo de transacción predeterminado en SQLite es el modo de transacción diferida. En este modo:
- No se adquiere ningún bloqueo al inicio de la transacción.
- A declaración de solo lectura no activa un bloqueo de escritura; solo requiere un bloqueo de lectura compartido, que permite lecturas simultáneas. Pensar
SELECT
declaraciones. - A escribir declaración requiere un bloqueo de escritura exclusivo, que bloquea todas las demás lecturas y escrituras hasta que se complete la transacción. Pensar
INSERT
,UPDATE
oDELETE
declaraciones.
Como ejemplo, observe las dos transacciones siguientes. Supongamos que se ejecutaran al mismo tiempo:
-- Transaction 1
BEGIN DEFERRED TRANSACTION;
SELECT * FROM inventory WHERE id = 1;
UPDATE inventory SET stock = stock + 1 WHERE id = 1; -- Transcation 2
BEGIN DEFERRED TRANSACTION;
UPDATE inventory SET stock = stock - 1 WHERE id = 1;
-- Example sequence of events:
-- Transaction 1 begins
-- SELECT statement: No lock is acquired yet.
-- Transaction 2 begins
-- Acquires a write lock (UPDATE statement).
-- Transcation 1 continues
-- Tries to acquire a write lock (UPDATE statement).
-- Fails because Transaction 2 already committed and released the lock.
-- SQLite throws SQLITE_BUSY.
-- Transaction 2 commits successfully. Transaction 1 has failed.
En este escenario, porque Transaction 1
estaba en mitad de la transacción cuando el SQLITE_BUSY
Se lanzó una excepción, no se volverá a poner en cola después Transaction 2
ha terminado con el bloqueo de escritura; simplemente será cancelado. SQLite no quiere arriesgarse a obtener resultados inconsistentes si otra transacción modifica datos superpuestos durante la espera de bloqueo, por lo que simplemente le dice a la transacción interrumpida que finalice.
Piénselo de esta manera: imagine que usted y su amigo comparten un cuaderno. Empiezas a leer una historia a medio terminar en el cuaderno y planeas escribir la siguiente parte. Pero antes de que puedas coger el bolígrafo, tu amigo te arrebata el cuaderno. “¡No estabas escribiendo nada de todos modos!”, exclaman. ¿Qué pasa si cambian algo crucial en tu historia? Frustrado e incapaz de continuar, te rindes enfadado y abandonas tu intento de terminar la historia. ¡Resulta que tu amigo no es tan amable como pensabas!
¿Cómo podemos solucionar este problema? ¿Qué pasa si estableces la siguiente regla: cuando uno de ustedes agarra el cuaderno, sin importar si estás leyendo o escribiendo¿esa persona puede usar el cuaderno hasta que termine? ¡Problema resuelto!
Este modo de transacción en SQLite se conoce como inmediato. Ahora, cuando comienza una transacción, independientemente de si está escribiendo o leyendo, reclama el bloqueo de escritura. Si una transacción simultánea intenta reclamar el bloqueo de escritura, ahora se pondrá en cola detrás de la actual en lugar de lanzar el SQLITE_BUSY
.
El uso del modo de transacción inmediata contribuye en gran medida a resolver el problema de concurrencia en SQLite. Para seguir mejorando la concurrencia, también podemos cambiar el modo diario. El valor predeterminado aquí es un diario de reversión. En este paradigma, se copia el contenido original de una página de base de datos. antes modificación. De esta manera, si la transacción falla o si así lo desea, siempre puede volver al diario para restaurar la base de datos a su estado original. Esto es excelente para la reproducibilidad, pero malo para la simultaneidad. Copiar una página completa en una base de datos es lento y bloquea el bloqueo de escritura, lo que retrasa cualquier operación de lectura.
Para solucionar este problema, podemos usar registro de escritura anticipada (WAL). En lugar de escribir los cambios directamente en el archivo de la base de datos principal, los cambios primero se registran en un archivo de registro separado (el “registro de escritura anticipada”) antes de aplicarse a la base de datos a intervalos regulares. Los lectores aún pueden acceder a las operaciones de escritura confirmadas más recientemente, ya que SQLite verifica el archivo WAL además del archivo de base de datos principal durante la lectura. Esto separa las operaciones de escritura y lectura, lo que alivia los problemas de simultaneidad que pueden surgir como resultado del escalado.
Para continuar con nuestra analogía, el registro de escritura anticipada es como tomar una nota adhesiva cada vez que es necesario realizar un cambio en el cuaderno compartido. Si alguien quiere leer una sección del cuaderno, puede comprobar si hay notas adhesivas adjuntas a esa sección para obtener las últimas actualizaciones. Puedes tener muchas personas leyendo el cuaderno simultáneamente al mismo tiempo con este método. Una vez que se empiezan a acumular muchos post-its, puedes editar el cuaderno y tirar los post-its una vez que las ediciones hayan finalizado.
Estas opciones de configuración en SQLite existen desde hace décadas (el registro de escritura anticipada se introdujo en 2010). Teniendo esto en cuenta, ¿por qué SQLite no se ha utilizado en producción durante décadas? Eso nos lleva a nuestro próximo número.
Las unidades de disco duro (HDD) son notoriamente lentas en comparación con las unidades de estado sólido (SSD) en una variedad de operaciones que son importantes para la administración de bases de datos. Por ejemplo, los SSD son aproximadamente 100 veces más rápidos que los HDD en cuanto a latencia (tiempo que lleva una sola operación de E/S). En operaciones de E/S aleatorias por segundo (IOPS), las SSD son entre 50 y 1000 veces más rápidas que las HDD. Los SSD son mucho más rápidos que los HDD debido a la falta de piezas móviles. Los HDD utilizan discos giratorios y piezas móviles para leer y escribir datos, muy parecidos a un antiguo tocadiscos, mientras que los SDD utilizan sólo componentes electrónicos, muy parecidos a una memoria USB gigante.
A pesar de su inferioridad, los discos duros históricamente han dominado el mercado de almacenamiento principalmente debido a su bajo costo. Sin embargo, los SDD se han ido poniendo al día rápidamente. En 2011, los SSD eran aproximadamente 32 veces más caros por GB que los HDD (fuente). Para 2023, la diferencia de precios se redujo, y los SSD ahora son entre 3 y 5 veces más caros por GB en comparación con los HDD (fuente). El año pasado, los precios de los SSD aumentaron debido a los recortes de fabricantes como Samsung y la creciente demanda en los centros de datos. Sin embargo, a largo plazo, podemos esperar que los SSD sigan bajando de precio. Incluso si nunca se alcanza la paridad con los HDD, el bajo precio absoluto es suficiente para garantizar una adopción generalizada. En 2020, los SSD se vendieron más que los HDD, con 333 millones de unidades enviadas en comparación con 260 millones de HDD, lo que marca un punto de inflexión en el mercado del almacenamiento (fuente).
A partir de diciembre de 2024, podrás alquilar una vCPU dedicada con 80 GB de almacenamiento SSD por aproximadamente $16 USD al mes en un servicio como Hetzner. Se pueden conseguir 240 GB por unos 61 dólares. Puede obtener precios aún más económicos con una vCPU compartida. Para muchas aplicaciones más pequeñas este almacenamiento es más que suficiente. El uso de SSD económicos ha eliminado un cuello de botella importante al utilizar SQLite en aplicaciones de producción. Pero todavía queda una cuestión más importante que abordar.
No hace falta decir que tener una copia de seguridad de su base de datos es fundamental en producción. Lo último que quiere cualquier startup es que su base de datos principal se corrompa y se pierdan todos los datos del usuario.
La primera opción para crear una copia de seguridad es la más sencilla. Dado que la base de datos SQLite es solo un archivo, básicamente puede copiar y pegar su base de datos en una carpeta de su computadora o cargarla en un servicio en la nube como los depósitos AWS S3 para mayor confiabilidad. Para bases de datos pequeñas con escrituras poco frecuentes, esta es una gran opción. Como ejemplo simple (tomado del Documentos Litestream), aquí hay un script bash que crea una copia de seguridad:
#!/bin/bash# Ensure script stops when commands fail.
set -e
# Backup our database to the temp directory.
sqlite3 /path/to/db "VACUUM INTO '/path/to/backup'"
# Compress the backup file for more efficient storage
gzip /tmp/db
# Upload backup to S3 using a rolling daily naming scheme.
aws s3 cp /tmp/db.gz s3://mybucket/db-`date +%d`.gz
Algunas notas:
- El
-e
opción enset -e
significa “salir inmediatamente”. Esto garantiza que el script se detendrá si falla algún comando. - SQLite
VACUUM INTO
El comando crea una copia de seguridad compacta de la base de datos SQLite. Reduce la fragmentación en la base de datos y el tamaño del archivo. Piense en ello como una versión limpia y ordenada de su base de datos. Sin embargo, no es necesario utilizarVACUUM INTO
; puedes reemplazarlo con.backup
. Esto copia todo el archivo de base de datos, incluidos todos sus datos y estructura tal cual, en otro archivo. - Las bases de datos SQLite se comprimen bien y el
gzip
El comando facilita esto. - Finalmente, puede cargar la copia del archivo en el proveedor de almacenamiento en la nube de su elección. Aquí estamos subiendo a S3.
Si desea que sus copias de seguridad se ejecuten automáticamente, puede configurar crontab
para ejecutar este trabajo de forma regular. Aquí ejecutamos el script diariamente a medianoche:
# Edit your cron jobs
crontab -e# Add this to the end of the crontab
0 0 * * * /path/to/my_backup_script.sh
Para bases de datos con mucha escritura, donde desea capturar el estado de la base de datos en cualquier momento dado, puede usar Litestream. Esta es una herramienta de código abierto diseñada para proporcionar replicación en tiempo real para bases de datos SQLite mediante la transmisión de cambios a un backend de almacenamiento remoto.
Litestream puede rastrear cambios en el archivo WAL de SQLite. ¿Recuerdas las notas adhesivas? Cada vez que se registra una nueva transacción en el archivo WAL, Litestream puede replicarla de forma incremental en el proveedor de almacenamiento en la nube de su elección. Esto nos permite mantener una copia de seguridad de la base de datos casi en tiempo real sin crear copias completas cada vez.
Para comenzar con Litestream, primero debes instalarlo. En MacOS esto significa usar Homebrew. Entonces, necesitas configurar un litestream.yml
archivo de configuración:
# /etc/litestream.yml
dbs:
- path: /path/to/your.db
replicas:
- type: s3
bucket: your-s3-bucket-name
path: your-database-name
region: your-region
Aquí, transmitiremos transacciones a nuestra base de datos a un depósito S3. Luego podemos ejecutar el siguiente comando para comenzar la replicación:
litestream replicate -config /etc/litestream.yml
En este caso, configuramos cualquier transacción en your.db
para ser replicado en un depósito S3. ¡Eso es todo! Luego podrá restaurar una base de datos SQLite a cualquier estado anterior reproduciendo los cambios de WAL. Como ejemplo, si desea crear una copia de su base de datos llamada restored.db
a partir de una marca de tiempo de las 15:00 UTC del 2024–12–10, puede ejecutar el siguiente comando:
litestream restore -o /path/to/restored.db \
-timestamp "2024-12-10T15:00:00Z" \
s3://your-s3-bucket-name/your-database-name
Para obtener una copia de seguridad de la última versión de su base de datos, simplemente omita el -timestamp flag
.
Te animo a que veas este reciente. charla en Rails World 2024 para ver cómo SQLite se está preparando rápidamente para producción. Han implementado algunos de los cambios que hemos discutido aquí en su adaptador SQLite. También recomiendo leer. Artículo de Stephen Margheim detallando su trabajo en SQLite en Rails si quieres profundizar más. Será mejor que creas que este tipo de mejoras llegarán pronto. Django, Laraveletc.
Las mejoras a SQLite para producción no han terminado. David Heinemeier Hansson, creador de Rails, quiere impulsar SQLite para poder gestionar una empresa SaaS de tamaño mediano. ¡Tiempos emocionantes!