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

rqlite : une alternative à haute disponibilité et dist distribuée SQLite

Il y a de nombreuses limitations, mais il y a aussi de nombreux cas d'utilisation de rqlite au lieu de SQlite.

17 octobre 2023
Dans
post main image
https://unsplash.com/@_christianlambert

Dans un projet, j'utilise une base de données SQLite . Les données ne sont pas critiques, elles peuvent être rechargées à tout moment. Néanmoins, je ne veux pas qu'une partie de l'application ne réponde plus lorsque la base de données SQLite est temporairement indisponible.

Je cherchais une base de données rapide, plus ou moins tolérante aux pannes, et également dist distribuée, afin de pouvoir répliquer certains modules de lecture. Quelques solutions sont apparues lors de mes recherches sur internet et rqlite m'a semblé être un bon choix.

Dans ce billet, je crée un cluster rqlite avec trois noeuds en utilisant Docker-Compose et j'accède ensuite aux noeuds avec une application Python .

Comme toujours, j'utilise Ubuntu 22.04.

Limitations de rqlite

Je commence par les limitations car elles peuvent ne pas correspondre à votre cas. Les voici :

  • Les transactions ne sont pas prises en charge.
  • Il y a un petit risque de perte de données dans le cas où le nœud tombe en panne avant que les données en file d'attente ne soient persistées.
  • Vitesse. Ne vous attendez pas à obtenir les mêmes temps que lorsque vous accédez directement à une base de données SQLite . Il y a non seulement la surcharge du réseau, mais aussi une latence d'écriture beaucoup plus élevée que celle de SQLite causée par l'algorithme de consensus Raft . L'utilisation d'écritures en bloc améliore considérablement les performances.
  • Fonctions non déterministes et autres, voir le document rqlite 'Developer Guide'.

De plus amples informations sont disponibles dans le document 'Comparing Litestream, rqlite, and dqlite', voir les liens ci-dessous.

Les fichiers docker-compose.yml et '.env'.

Il y a plusieurs façons de mettre en place un cluster rqlite . Ici, nous utilisons le rqlite Docker image pour créer un cluster de trois nœuds rqlite en suivant les instructions pour 'Automatic Bootstrapping', voir les liens ci-dessous.

Nous n'exposons pas les ports au système hôte, mais rendons les nœuds (uniquement) disponibles sur un réseau Docker . Le nom d'hôte est utilisé par les nœuds rqlite pour la découverte et d'autres applications utilisent les noms d'hôte pour accéder à la grappe de nœuds. Nous utilisons Volumes parce que nous voulons préserver les données stockées dans Raft même si la grappe s'arrête pendant une courte période.

Important : Après avoir modifié le fichier docker-compose.yml et/ou '.env', supprimez les données des répertoires montés (./data/rqlite-node-1, ./data/rqlite-node-2, ./data/rqlite-node-3). Si vous ne le faites pas, vous risquez d'obtenir toutes sortes de comportements bizarres !

Le fichier 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

Maintenant, créez le réseau Docker :

> docker network create rqlite-cluster-network

Et démarrez le cluster :

> docker-compose up

Les messages dans le terminal montrent que les nœuds rqlite sont en contact les uns avec les autres. La grappe est-elle vraiment en place ? Pour le vérifier, ouvrez un autre terminal et entrez dans l'un des conteneurs rqlite :

> docker exec -it rqlite-cluster_rqlite-node-3_1 sh

Connectez-vous ensuite à l'un des nœuds :

# rqlite -H rqlite-node-1

Résultat :

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>

Lancez la commande '.nodes'. Résultat :

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

Voilà, le cluster est en place !

Essayons maintenant une commande SQL depuis un autre conteneur connecté au réseau du cluster, ici nous utilisons l'image 'nicolaka/netshoot' :

> docker run -it --network rqlite-cluster-network nicolaka/netshoot bash

Lancez la commande de création d'une table :

# 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)"
]'

Résultat :

{
    "results": [
        {
            "time": 0.000179355
        }
    ],
    "time": 0.018545186
}

Répétez la commande et le résultat est le suivant :

{
    "results": [
        {
            "error": "table foo already exists"
        }
    ],
    "time": 0.017034644
}

Super, notre cluster rqlite est opérationnel.

Accéder au cluster rqlite avec Python

Le projet rqlite possède également plusieurs clients, dont pyrqlite, un client pour Python. Nous utilisons l'exemple pyrqlite sur la page rqlite Github . Nous apportons deux modifications :

  • L'"hôte".
  • Nous supprimons la table si elle existe déjà.

Sur le système hôte, créez un sous-répertoire 'code' et ajoutez le fichier suivant :

# 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()

Démarrer et entrer dans un conteneur Python , connecté au 'rqlite-cluster-network', et monter notre code sur le système hôte à '/code' à l'intérieur du conteneur :

> docker run -it --net rqlite-cluster-network -v ${PWD}/code:/code python:3.11.5-slim-bullseye bash

Dans le conteneur, installer pyrqlite :

# pip install pyrqlite

À l'intérieur du conteneur, accédez au répertoire '/code' et exécutez le script :

# python rqlite_test.py

Résultat :

OrderedDict([('id', 1), ('name', 'a')])
OrderedDict([('id', 2), ('name', 'b')])

Le script fonctionne !

Résumé

Bien qu'il semble très facile d'utiliser rqlite au lieu de SQLite, ce n'est pas le cas ! Vous devez déterminer (lire, lire, lire) si rqlite répond à vos besoins, ce qui n'est pas facile car les différences et les limitations sont mentionnées sur plusieurs pages de la documentation.

La création d'un cluster rqlite n'est pas difficile si l'on utilise Docker ou Docker Swarm. Il existe également un guide pour Kubernetes. Le cluster rqlite nous donne une base de données dist distribuée, plus ou moins tolérante aux fautes, SQLite .

Pour obtenir la tolérance aux pannes au niveau de l'application, nous devons ajouter une liste de nœuds rqlite (hôtes) à notre application et ajouter du code pour basculer vers un autre nœud rqlite , lorsqu'un nœud rqlite n'est pas disponible. Quoi qu'il en soit, pour mon cas, rqlite est une solution parfaite !

Liens / crédits

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

En savoir plus...

rqlite

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires (1)

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.

avatar

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