angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

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.

17 octubre 2023
En
post main image
https://unsplash.com/@_christianlambert

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.

avatar

I'm the creator of rqlite -- nice article. I'm glad you find the software useful.
Philip
(https://www.philipotoole.com)