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

rqlite: альтернатива dist с высокой степенью готовности и SQLite

Есть много ограничений, но есть и много вариантов использования rqlite вместо SQlite.

17 октября 2023
В
post main image
https://unsplash.com/@_christianlambert

В одном из проектов я использую базу данных SQLite . Данные не являются критическими, их можно перезагрузить в любое время. Тем не менее, я не хочу, чтобы часть приложения перестала реагировать на временную недоступность базы данных SQLite .

Я искал быструю, более или менее отказоустойчивую базу данных, а также distributed, чтобы можно было реплицировать некоторые модули чтения. При поиске в Интернете было найдено несколько решений, и rqlite показался мне хорошим выбором.

В этом посте я создаю кластер rqlite с тремя узлами с помощью Docker-Compose, а затем обращаюсь к узлам с помощью приложения Python .

Как обычно, я использую Ubuntu 22.04.

Ограничения rqlite

Начну с ограничений, так как они могут не подойти в вашем случае. Вот они:

  • Транзакции не поддерживаются.
  • Существует небольшой риск потери данных в случае сбоя узла до того, как данные из очереди будут сохранены.
  • Скорость. Не ожидайте такого же времени, как при прямом доступе к базе данных SQLite . Мало того, что существуют сетевые накладные расходы, так еще и задержки при записи значительно выше, чем у SQLite , что обусловлено алгоритмом консенсуса Raft . Использование массовых записей значительно повышает производительность.
  • Недетерминированные функции и другие, см. документ rqlite 'Developer Guide'.

Более подробная информация содержится в документе 'Comparing Litestream, rqlite, and dqlite', см. ссылки ниже.

Файлы docker-compose.yml и '.env'

Существует множество способов настройки кластера rqlite . Здесь мы используем rqlite Docker image для создания кластера из трех узлов rqlite , следуя инструкциям по 'Automatic Bootstrapping', см. ссылки ниже.

Мы не открываем порты хост-системе, а делаем узлы (только) доступными в сети Docker . Очень важным является задание имени хоста для узлов! Имя хоста используется узлами rqlite для обнаружения, а другие приложения используют имена хостов для доступа к кластеру узлов. А мы используем Volumes , поскольку хотим сохранить данные, хранящиеся в Raft , даже если кластер на короткое время выйдет из строя.

Важно: После изменения файла docker-compose.yml и/или '.env' удалите данные из смонтированных каталогов (./data/rqlite-node-1, ./data/rqlite-node-2, ./data/rqlite-node-3). Если этого не сделать, то можно получить разного рода странное поведение!

Файл 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

Теперь создайте сеть Docker :

> docker network create rqlite-cluster-network

И запустите кластер:

> docker-compose up

Сообщения в терминале показывают, что узлы rqlite связываются друг с другом. Действительно ли кластер запущен? Чтобы проверить это, откройте другой терминал и войдите в один из контейнеров rqlite :

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

Затем подключитесь к одному из узлов:

# rqlite -H rqlite-node-1

Результат:

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>

Выполните команду '.nodes'. Результат:

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

Вот и все, кластер работает!

Теперь попробуем выполнить команду SQL из другого контейнера, подключенного к кластерной сети, здесь мы используем образ 'nicolaka/netshoot' :

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

Выполните команду создания таблицы:

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

Результат:

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

Повторяем команду и получаем результат:

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

Отлично, наш кластер rqlite запущен и работает.

Доступ к кластеру rqlite с помощью Python

Проект rqlite также имеет несколько клиентов, в том числе pyrqlite, клиент для Python. Мы используем пример pyrqlite на странице rqlite Github . Мы вносим два изменения:

  • 'host'.
  • Мы удаляем таблицу, если она уже существует.

На хост-системе создаем подкаталог 'code' и добавляем в него следующий файл:

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

Запускаем и вводим контейнер Python , подключенный к 'rqlite-cluster-network', и монтируем наш код на хост-системе по адресу '/code' внутри контейнера:

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

Внутри контейнера установите pyrqlite:

# pip install pyrqlite

Внутри контейнера перейдите в каталог '/code' и запустите скрипт:

# python rqlite_test.py

Результат:

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

Работает!

Резюме

Хотя кажется, что использовать rqlite вместо SQLite очень просто, это не так! Вы должны определить (читать, читать, читать), соответствует ли rqlite вашим требованиям, а это не так просто, поскольку различия и ограничения указаны на нескольких страницах документации.

Создание кластера rqlite не представляет сложности при использовании Docker или Docker Swarm. Существует также руководство для Kubernetes. Кластер rqlite дает нам dist распределенную, более или менее отказоустойчивую базу данных SQLite .

Чтобы получить отказоустойчивость на уровне приложения, мы должны добавить список узлов (хостов) rqlite в наше приложение и добавить код для переключения на другой узел rqlite , когда узел rqlite недоступен. В любом случае, для моего случая rqlite является идеальным решением!

Ссылки / кредиты

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

Подробнее

rqlite

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии (1)

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.

avatar

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