rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Hay muchas limitaciones, pero también hay muchos casos de uso para rqlite en lugar de SQlite.
En un proyecto estoy utilizando una base de datos SQLite . Los datos no son críticos, se pueden recargar en cualquier momento. Aún así, no quiero que parte de la aplicación deje de responder cuando la base de datos SQLite no esté disponible temporalmente.
Buscaba una base de datos rápida, más o menos tolerante a fallos, y también distributed, para poder replicar algunos módulos de lectura. Al buscar en internet surgieron unas cuantas soluciones y rqlite me pareció una buena opción.
En este post pongo en marcha un cluster rqlite con tres nodos usando Docker-Compose y luego accedo a los nodos con una aplicación Python .
Como siempre estoy ejecutando Ubuntu 22.04.
Limitaciones de rqlite
Empiezo primero con las limitaciones porque puede que no se adapten a tu caso. Aquí están:
- No soporta transacciones.
- Existe un pequeño riesgo de pérdida de datos en el caso de que el nodo se bloquee antes de que los datos en cola se mantengan.
- Velocidad. No esperes los mismos tiempos que al acceder directamente a una base de datos SQLite . No sólo existe la sobrecarga de red, sino que la latencia de escritura es mucho mayor que la de SQLite debido al algoritmo de consenso Raft . El uso de escrituras masivas mejora drásticamente el rendimiento.
- Funciones no deterministas y otras, véase el documento rqlite 'Developer Guide'.
Hay más información en el documento 'Comparing Litestream, rqlite, and dqlite', véanse los enlaces más abajo.
Los archivos docker-compose.yml y '.env
Hay muchas formas de configurar un clúster rqlite . Aquí usamos el rqlite Docker image para crear un cluster de tres nodos rqlite siguiendo las instrucciones para 'Automatic Bootstrapping', ver enlaces más abajo.
No exponemos los puertos al sistema anfitrión, sino que hacemos que los nodos (sólo) estén disponibles en una red Docker . El nombre de host es utilizado por los nodos rqlite para el descubrimiento y otras aplicaciones utilizan los nombres de host para acceder al clúster de nodos. Y usamos Volumes porque queremos preservar los datos almacenados en el Raft incluso si el cluster se cae durante un corto periodo de tiempo.
Importante: Después de cambiar el archivo docker-compose.yml y/o '.env', elimine los datos de los directorios montados (./data/rqlite-node-1, ./data/rqlite-node-2, ./data/rqlite-node-3). Si no lo hace, puede obtener todo tipo de comportamientos extraños.
El archivo docker-compose.yml:
# docker-compose.yml
version: '3.7'
services:
rqlite-node-1:
image: rqlite/rqlite:7.21.4
hostname: ${RQLITE_NODE_1_HOSTNAME}
volumes:
- ./data/rqlite-node-1:/rqlite/file/data
command:
- rqlited
- -node-id
- "${RQLITE_NODE_1_NODE_ID}"
- -http-addr
- "${RQLITE_NODE_1_HTTP_ADDR}"
- -raft-addr
- "${RQLITE_NODE_1_RAFT_ADDR}"
- -http-adv-addr
- "${RQLITE_NODE_1_HTTP_ADV_ADDR}"
- -raft-adv-addr
- "${RQLITE_NODE_1_RAFT_ADV_ADDR}"
# join
- -bootstrap-expect
- "3"
- -join
- "${RQLITE_NODE_1_JOIN_ADDR},${RQLITE_NODE_2_JOIN_ADDR},${RQLITE_NODE_3_JOIN_ADDR}"
- /rqlite/file/data
networks:
- rqlite-cluster-network
rqlite-node-2:
image: rqlite/rqlite:7.21.4
hostname: ${RQLITE_NODE_2_HOSTNAME}
volumes:
- ./data/rqlite-node-2:/rqlite/file/data
command:
- rqlited
- -node-id
- "${RQLITE_NODE_2_NODE_ID}"
- -http-addr
- "${RQLITE_NODE_2_HTTP_ADDR}"
- -raft-addr
- "${RQLITE_NODE_2_RAFT_ADDR}"
- -http-adv-addr
- "${RQLITE_NODE_2_HTTP_ADV_ADDR}"
- -raft-adv-addr
- "${RQLITE_NODE_2_RAFT_ADV_ADDR}"
# join
- -bootstrap-expect
- "3"
- -join
- "${RQLITE_NODE_1_JOIN_ADDR},${RQLITE_NODE_2_JOIN_ADDR},${RQLITE_NODE_3_JOIN_ADDR}"
- /rqlite/file/data
networks:
- rqlite-cluster-network
rqlite-node-3:
image: rqlite/rqlite:7.21.4
hostname: ${RQLITE_NODE_3_HOSTNAME}
volumes:
- ./data/rqlite-node-3:/rqlite/file/data
command:
- rqlited
- -node-id
- "${RQLITE_NODE_3_NODE_ID}"
- -http-addr
- "${RQLITE_NODE_3_HTTP_ADDR}"
- -raft-addr
- "${RQLITE_NODE_3_RAFT_ADDR}"
- -http-adv-addr
- "${RQLITE_NODE_3_HTTP_ADV_ADDR}"
- -raft-adv-addr
- "${RQLITE_NODE_3_RAFT_ADV_ADDR}"
# join
- -bootstrap-expect
- "3"
- -join
- "${RQLITE_NODE_1_JOIN_ADDR},${RQLITE_NODE_2_JOIN_ADDR},${RQLITE_NODE_3_JOIN_ADDR}"
- /rqlite/file/data
networks:
- rqlite-cluster-network
networks:
rqlite-cluster-network:
external: true
name: rqlite-cluster-network
The '.env'-file:
# .env
COMPOSE_PROJECT_NAME=rqlite-cluster
# RQLITE_NODE_1
RQLITE_NODE_1_HOSTNAME=rqlite-node-1
RQLITE_NODE_1_NODE_ID=rqlite-node-1
RQLITE_NODE_1_DATA_DIR=/rqlite/file/data
RQLITE_NODE_1_HTTP_ADDR=rqlite-node-1:4001
RQLITE_NODE_1_RAFT_ADDR=rqlite-node-1:4002
RQLITE_NODE_1_HTTP_ADV_ADDR=rqlite-node-1:4001
RQLITE_NODE_1_RAFT_ADV_ADDR=rqlite-node-1:4002
# join
RQLITE_NODE_1_JOIN_ADDR=rqlite-node-1:4001
# RQLITE_NODE_2
RQLITE_NODE_2_HOSTNAME=rqlite-node-2
RQLITE_NODE_2_NODE_ID=rqlite-node-2
RQLITE_NODE_2_DATA_DIR=/rqlite/file/data
RQLITE_NODE_2_HTTP_ADDR=rqlite-node-2:4001
RQLITE_NODE_2_RAFT_ADDR=rqlite-node-2:4002
RQLITE_NODE_2_HTTP_ADV_ADDR=rqlite-node-2:4001
RQLITE_NODE_2_RAFT_ADV_ADDR=rqlite-node-2:4002
# join
RQLITE_NODE_2_JOIN_ADDR=rqlite-node-2:4001
# RQLITE_NODE_3
RQLITE_NODE_3_HOSTNAME=rqlite-node-3
RQLITE_NODE_3_NODE_ID=rqlite-node-3
RQLITE_NODE_3_DATA_DIR=/rqlite/file/data
RQLITE_NODE_3_HTTP_ADDR=rqlite-node-3:4001
RQLITE_NODE_3_RAFT_ADDR=rqlite-node-3:4002
RQLITE_NODE_3_HTTP_ADV_ADDR=rqlite-node-3:4001
RQLITE_NODE_3_RAFT_ADV_ADDR=rqlite-node-3:4002
# join
RQLITE_NODE_3_JOIN_ADDR=rqlite-node-3:4001
Ahora crea la red Docker :
> docker network create rqlite-cluster-network
Y arranca el cluster:
> docker-compose up
Los mensajes en el terminal muestran los nodos rqlite en contacto entre sí. ¿Está realmente en marcha el clúster? Para comprobarlo, abre otro terminal y entra en uno de los contenedores rqlite :
> docker exec -it rqlite-cluster_rqlite-node-3_1 sh
A continuación, conéctate a uno de los nodos:
# rqlite -H rqlite-node-1
Resultado:
Welcome to the rqlite CLI. Enter ".help" for usage hints.
Version v7.21.4, commit 971921f1352bdc73e4e66a1ec43be8c1028ff18b, branch master
Connected to rqlited version v7.21.4
rqlite-node-1:4001>
Emita el comando '.nodes'. Resultado:
rqlite-node-2:
leader: false
time: 0.001115574
api_addr: http://rqlite-node-2:4001
addr: rqlite-node-2:4002
reachable: true
rqlite-node-3:
leader: false
time: 0.001581149
api_addr: http://rqlite-node-3:4001
addr: rqlite-node-3:4002
reachable: true
rqlite-node-1:
time: 0.000009044
api_addr: http://rqlite-node-1:4001
addr: rqlite-node-1:4002
reachable: true
leader: true
¡Ya está, el cluster está en marcha!
Ahora, probemos un comando SQL desde otro contenedor conectado a la red del cluster, aquí usamos la imagen 'nicolaka/netshoot' :
> docker run -it --network rqlite-cluster-network nicolaka/netshoot bash
Ejecuta el comando para crear una tabla:
# curl -XPOST 'rqlite-node-2:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
"CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT, age INTEGER)"
]'
Resultado:
{
"results": [
{
"time": 0.000179355
}
],
"time": 0.018545186
}
Repetimos el comando y el resultado es:
{
"results": [
{
"error": "table foo already exists"
}
],
"time": 0.017034644
}
Genial, nuestro cluster rqlite está en marcha.
Acceder al cluster rqlite con Python
El proyecto rqlite también tiene varios clientes, incluido pyrqlite, un cliente para Python. Usamos el ejemplo pyrqlite en la página rqlite Github . Hacemos dos cambios:
- El 'host'.
- Eliminamos la tabla si ya existe.
En el sistema anfitrión, creamos un subdirectorio 'code' y añadimos el siguiente archivo:
# rqlite_test.py
import pyrqlite.dbapi2 as dbapi2
# Connect to the database
connection = dbapi2.connect(
host='rqlite-node-2',
port=4001,
)
try:
with connection.cursor() as cursor:
cursor.execute('DROP TABLE IF EXISTS foo')
cursor.execute('CREATE TABLE foo (id integer not null primary key, name text)')
cursor.executemany('INSERT INTO foo(name) VALUES(?)', seq_of_parameters=(('a',), ('b',)))
with connection.cursor() as cursor:
# Read a single record with qmark parameter style
sql = "SELECT `id`, `name` FROM `foo` WHERE `name`=?"
cursor.execute(sql, ('a',))
result = cursor.fetchone()
print(result)
# Read a single record with named parameter style
sql = "SELECT `id`, `name` FROM `foo` WHERE `name`=:name"
cursor.execute(sql, {'name': 'b'})
result = cursor.fetchone()
print(result)
finally:
connection.close()
Iniciamos y entramos en un contenedor Python , conectado al 'rqlite-cluster-network', y montamos nuestro código en el sistema anfitrión en '/code' dentro del contenedor:
> docker run -it --net rqlite-cluster-network -v ${PWD}/code:/code python:3.11.5-slim-bullseye bash
Dentro del contenedor, instala pyrqlite:
# pip install pyrqlite
Dentro del contenedor, cambia al directorio '/code' y ejecuta el script:
# python rqlite_test.py
Resultado:
OrderedDict([('id', 1), ('name', 'a')])
OrderedDict([('id', 2), ('name', 'b')])
¡Funciona!
Resumen
Aunque parece que es muy fácil utilizar rqlite en lugar de SQLite, ¡no lo es! Debes determinar (leer, leer, leer) si rqlite cumple tus requisitos, lo cual no es fácil porque las diferencias y limitaciones se mencionan en varias páginas de la documentación.
Crear un cluster rqlite no es difícil si se utiliza Docker, o Docker Swarm. También existe una guía para Kubernetes. El clúster rqlite nos proporciona una base de datos dist distribuida, más o menos tolerante a fallos, SQLite .
Para obtener tolerancia a fallos a nivel de aplicación, debemos añadir una lista de nodos rqlite (hosts) a nuestra aplicación y añadir algo de código para cambiar a otro nodo rqlite , cuando un nodo rqlite no esté disponible. En mi caso, rqlite es la solución perfecta.
Enlaces / créditos
Comparing Litestream, rqlite, and dqlite
https://gcore.com/learning/comparing-litestream-rqlite-dqlite
rqlite
https://rqlite.io
rqlite - Automatic clustering: Automatic Bootstrapping
https://rqlite.io/docs/clustering/automatic-clustering
rqlite - Developer Guide
https://rqlite.io/docs/api
rqlite/rqlite Docker image
https://hub.docker.com/r/rqlite/rqlite
Leer más
rqlite
Deje un comentario
Comente de forma anónima o inicie sesión para comentar.
Comentarios (1)
Deje una respuesta.
Responda de forma anónima o inicie sesión para responder.
I'm the creator of rqlite -- nice article. I'm glad you find the software useful.
Philip
(https://www.philipotoole.com)
Recientes
- Uso de Ingress para acceder a RabbitMQ en un clúster Microk8s
- Galería de vídeo simple con Flask, Jinja, Bootstrap y JQuery
- Programación básica de trabajos con APScheduler
- Un conmutador de base de datos con HAProxy y el HAProxy Runtime API
- Docker Swarm rolling updates
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
Más vistos
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados