PocketBase и libSQL
PocketBase - это швейцарский нож для ваших пет-проектов. С его помощью можно быстро запустить все - от сайта, до коллайдера. В этой статье расскажу про плюсы/минусы, как использовать и как затащить туда libSQL
Кроме того, надеюсь, что эта статья станет той недостающей документацией по libSQL, которой мне очень не хватало
Возможности PocketBase
PocketBase была создана для помощи в разработке автономных приложений, которые могут работать на одном сервере без необходимости установки чего-либо дополнительно. Основная идея заключается в том, что общие функции, такие как CRUD, аутентификация, загрузка файлов, автоматическое TLS и прочее, обрабатываются “из коробки”, позволяя вам сосредоточиться на пользовательском интерфейсе и фактических бизнес-требованиях вашего приложения.
Все бековое API - это один бинарник, который вы можете залить на любой сервер. Для фронтенда есть готовые SDK на Dart(для Flutter) и JavaScript
Для фронта доступно очень гибкое API. С его помощью можно получать список записей, загружать файлы, манипулировать настройками и кронами, получать сообщения в риалтайме.
Пример того, как выглядит доступ к API PocketBase и получение записей на Dart
1import 'package:pocketbase/pocketbase.dart';
2
3final pb = PocketBase('http://127.0.0.1:8090');
4
5// получаем список записей
6final resultList = await pb.collection('posts').getList(
7 page: 1,
8 perPage: 50,
9 filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
10);
11
12// можно получить список всех записей с помощью getFullList
13final records = await pb.collection('posts').getFullList(sort: '-created');
14
15// или получить только одну запись, которая соответствует фильтру
16final record = await pb.collection('posts').getFirstListItem(
17 'someField="test"',
18 expand: 'relField1,relField2.subRelField',
19);
В PocketBase уже реализован очень удобный админский интерфейс, c помощью которого можно манипулировать коллекциями и записями
Есть интеграция с SMTP, удобное логирование с GUI, настройка бекапов и возможность подключить s3 хранилище. s3 используется как для сохранения бекапов, так и для загрузки файлов в ваших CRUDах
И еще PocketBase поддерживает огромное количество авторизационных провайдеров
Ограничения
PocketBase точно не самый лучший выбор для больших приложений, которым важно резиново скейлиться. В случае с PocketBase скейлиться, как правило, можно только на одном сервере, иначе говоря, поддерживается только вертикальное масштабирование. В большинстве случаев вам может не понадобиться сложная работа по управлению парком машин и сервисов только для запуска вашего бекенда.
PocketBase может стать отличным выбором для небольших и средних приложений - SaaS, серверной части мобильного API, интрасети и т.д. Даже без оптимизации PocketBase может легко обслуживать более 10 000 постоянных подключений в реальном времени на дешевом Hetzner CAX11 VPS за 4 доллара (2 процессора, 4 ГБ оперативной памяти).
Вы можете найти тесты производительности для различных операций чтения и записи в официальном репозитории benchmarks. Всегда есть возможности для улучшений, но текущей производительности уже достаточно для того типа приложений, для которых предназначена PocketBase.
Есть две причины, почему PocketBase сложно масштабировать горизонтально:
- Используется SQLite, а это встроенная база данных. С таким подходом сложно запустить несколько инстансов и пошарить между ними данные
- OAuth2 токены хранятся в кеше инстанса и чтобы все работало как надо, нужно выносить эти ключи в отдельный кеш
Можем ли мы что-то сделать с первым вариантом? Например, можно ли использовать альтернативную базу данных, такую как PostgreSQL? Ответ автора:
Нет, по крайней мере, не из коробки. В PocketBase используется встроенный SQLite (в режиме WAL), и поддержка других баз данных не планируется. Для большинства запросов SQLite (в режиме WAL) превосходит традиционные базы данных, такие как MySQL, MariaDB или PostgreSQL (особенно для операций чтения). Если вам нужна репликация и аварийное восстановление, отличным приложением-компаньоном может стать Litestream.
Тут упоминается Litestream - это отличный инструмент для репликации базы SQLite. И он может решить часть проблем с горизонтальным масштабированием.
Но с версии PocketBase 0.23+ все стало значительно проще. Теперь можно использовать кастомные драйвера при настройке подключения к базе. А это нам дает возможность использовать замечательный проект - libSQL
libSQL
Тут много всего из статьи “libSQL: Diving Into a Database Engineering Epic”. Там я подсмотрел про репликацию и протоколы libSQL
Чтобы разобраться, что такое libSQL, сначала надо хотя бы поверхностно понимать, как работает SQLite. Ниже схема того, как все устроено.
В этой схеме важно, что есть некоторый уровень абстракции - OS Interface который реализует виртуальную файловую систему (VFS). Это имеет значение, потому что можно использовать свои реализации VFS для интеграции дополнительного функционала
libSQL это не только библиотека или SDK. Под libSQL подразумеваются два проекта:
- Форк SQLite - libSQL-core
- Надстройка сервера поверх форка SQLite - sqld
А если есть сервер, то нужно с ним как-то взаимодействовать. LibSQL поддерживает целый набор самых разнообразных протоколов.
Протокол Postgres
Очень удобно, libSQL позволяет взаимодействовать с сервером с помощью инструментов, реализующих протокол передачи данных Postgres.
Это работает почти идеально. Для использования этой возможности необходимо запустить sqld и подключиться с помощью psql (консольного клиента для Postgres).
1$ sqld -d foo.db -p 127.0.0.1:5432 --http-listen-addr=127.0.0.1:8000
2$ psql -q postgres://127.0.0.1
3appinv=>
Утилита psql уверена, что соединяется с Postgres. Для меня это похоже почти на магию.
Конечно, не все прям совсем гладко. Многие инструменты, использующие этот протокол, также пытаются найти внутренние таблицы postgres. Из-за этого, некоторые инструменты и функции работают ограниченно. Но с базовыми операциями нет никаких проблем.
Протокол Hrana
Hrana (от чешского «hrana», что означает «край») — это протокол libSQL для подключения к sqld через веб-сокеты.
TCP-соединения запрещены во многих периферийных средах выполнения, таких как Cloudflare Workers. Но веб-сокеты доступны почти везде. Hrana — это простой протокол, требующий меньше ресурсов, чем Postgres, и работающий поверх функционала веб-сокетов.
Хотя протокол Hrana разрабатывался для использования с sqld, её также можно использовать с SQLite через прокси-сервер. Протокол Hrana поддерживает произвольные строки с необязательными параметрами, что позволяет легко адаптировать его к самым различным диалектам SQL.
Hrana работает поверх протокола веб-сокетов в качестве подпротокола. Это заметно, если поковыряться в заголовках и посмотреть что указано в Sec-WebSocket-Protocol.
Пример запроса
GET /chat HTTP 1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dhGiedSSDkjGSDhugf==
Origin: http://example.com
Sec-WebSocket-Protocol: hrana1
Sec-WebSocket-Version: 13
Пример ответа
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: iwefOIUhUGigUYIGb==
Sec-WebSocket-Protocol: hrana1
Протокол HTTP
Протокол HTTP позволяет нам стримить спецификации Hrana там, где поддержка веб-сокетов отсутствует. В этом случае все работает через запрос/ответ, но используются специальные идентификаторы для реализации некоторого подобия стриминга
sqld группирует запросы с помощью значений-указателей в потоках. Значение-указатель — это идентификатор, отправляемый сервером, который должен быть отправлен обратно клиентом.
Это позволяет очень удобно взаимодействовать по протоколу Hrnana с помощью curl или простых клиентских запросов. Вот пример JSON, который отправляется через curl на сервер.
1$ curl -s -d "{\"statements\": [\"SELECT * from databases;\"] }" \
2 http://127.0.0.1:8000
3[[{"name":"libsql"}]]
Репликация
SQLite может работать в нескольких режимах. Это режим ролбека и режим WAL. Подробнее прочитать можно тут. Режим WAL более современный и он используется в PocketBase. Дальше бы будет рассматривать именно режим WAL.
Чтобы понять, как устроен и используется WAL, неплохо разобраться, как работает виртуальная файловая система (VFS) на уровне ОС. Поскольку SQLite доступен во многих операционных системах, запись файлов в систему осуществляется с помощью вызова уровня VFS. VFS в Windows и Unix включена по умолчанию.
Но VFS можно использовать как прослойку между WAL и любым хранилищем, которое мы захотим использовать, это может быть буквально что угодно.
Виртуализация операций WAL позволяет нам записывать его куда угодно, в том числе и на наши собственные серверы. Более того, кроме указания куда записывать файлы WAL, можно добавить какие-то дополнительные операции и логику при записи этих файлов. Допустим, мы хотим записывать WAL на s3 - без проблем, нет ничего невозможного.
libSQL предоставляет несколько методов для реализации виртуализации записи WAL
1libsql_wal_methods_find
2libsql_wal_methods_register
3libsql_wal_methods_unregister
Таким образом, с помощью VFS мы можем реализовать бездонное хранилище для WAL. В libSQL (sqld) уже реализован способ резервного копирования файлов WAL в s3, которое как раз работает как бесконечное хранилище. Разберемся, как это реализовано.
Шаги для создания bottomless(бездонного) WAL:
- libSQL записывает кадры(фреймы) WAL
- Эти кадры асинхронно реплицируются на s3 в фоновом режиме, партиями
- Когда в libSQL выполняется операция чекпоинт (контрольная точка — это операция, которая уплотняет кадры WAL, удаляя устаревшие), мы также загружаем сжатый снимок основного файла базы данных на s3
- Файлами моментальных снимков можно управлять через bottomless-cli
Разберемся, из чего состоит файл WAL. Это первый шаг, чтоб понять как устроена репликация в libSQL
Файл WAL может содержать 0 или более кадров. Кадр состоит из заголовка и страницы. После 1000 кадров или 10 секунд, в зависимости от настроек, кадры сжимаются и отправляются на s3.
Операция контрольной точки(чекпоинт) — это операция, в ходе которой содержимое файла WAL применяется к базе данных. После операции контрольной точки моментальный снимок(снапшот) базы данных отправляется на s3.
Хранилище в s3 организовано в виде пространств имён, называемых поколениями. Поколение — это период между операциями контрольных точек. Самое последнее поколение находится в верхней части хранилища.
Чтобы применить резервную копию из s3 хранилища, мы указываем снапшот, который нужно использовать, и забираем из хранилища нужные фреймы.
Для применения транзакций libSQL использует специальный кэш транзакций, с помощью которого можно их верифицировать.
При применении транзакций, если они слишком большие, чтобы поместиться в памяти, они сохраняются во временном файле.
libSQL имеет два счетчика, при совпадении которых происходит операция контрольной точки. libSQL использует метод WAL xframes для увеличения счетчика, если транзакция зафиксирована.
libSQL xcheckpoint позволяет придержать создание контрольной точки до завершения задачи.
Подключаем libSQL к PocketBase
Для начала нужно определиться, где будут все наши данные. Можно использовать готовый сервис - turso от самих разработчиков libSQL. Посмотреть, как подключать можно, в этом видосе. Так сложилось, что они не хотят брать мои рубли, а значит будем колхозить свой сервер
Запускаем
Мы пойдем самым трушным путем и поднимем свой sqld. Чтобы все было совсем круто, то можно сделать управление своим сервисом баз данных с помощью PocketBase, но это оставлю на потом.
И тут есть огромная ложка дегтя! Так как ребята пилят свой сервис turso.tech, который они продают за деньги, то они вообще не вкладываются в документацию по опенсорсной части. Мне пришлось по кускам, дискордам и ищью собирать кусочки того, как должен работать сервис, предоставляющий базы данных
Тут есть описание, как нужно запустить докер с нужными параметрами - ссылка, но нам такой вариант не подходит
Нужно запустить базу так, чтобы у нас была возможность создавать множество баз данных на одном сервере sqld
1version: "3"
2services:
3 primary:
4 image: ghcr.io/tursodatabase/libsql-server:latest
5 platform: linux/amd64
6 ports:
7 - 8080:8080
8 - 5001:5001
9 - 8082:8082
10 command: ['sqld', '--admin-listen-addr', '0.0.0.0:8082', '--enable-namespaces', '--disable-default-namespace', '--enable-http-console']
11 environment:
12 - SQLD_NODE=primary
13 - SQLD_HTTP_LISTEN_ADDR=0.0.0.0:8080
14 - SQLD_GRPC_LISTEN_ADDR=0.0.0.0:5001
15 - RUST_LOG=debug
16 - SQLD_HTTP_AUTH=basic:YWRtaW46YWRtaW4=
17 volumes:
18 - ./libsql:/var/lib/sqld
Для начала, тут указывается пачка параметров. Разберемся с ними:
- –admin-listen-addr 0.0.0.0:8082 - нам нужен доступ к админскому API, чтобы иметь возможность создавать новые неймспейсы(базы данных)
- –enable-namespaces - этот параметр включает возможность создания новых неймспейсов
- –enable-http-console - если указать параметр, то по url http://localhost:8080/console будет доступна консоль
Порт 8080
используется по умолчанию для основного взаимодействия с клиентами. Порт 5001
используется для внутреннего взаимодействия между нодами.
Чтобы разобраться, как запустить сервер, мне очень помогло вот это сообщение. К сожалению, в документации почти ничего нет. Пример запуска из чата:
1sqld -d databases \
2 --enable-namespaces \
3 --disable-default-namespace \
4 --enable-http-console \
5 --snapshot-at-shutdown \
6 --http-listen-addr 0.0.0.0:8001 \
7 --admin-listen-addr 0.0.0.0:7001 \
8 --auth-jwt-key-file cert/public_key.pem \
9 --max-active-namespaces 2000
Порт 80802
нужен для доступа к админскому API. Есть очень скудная документация. Совершенно непонятно, что это такое dump_url
, но по кусочкам информации пришел к выводу, что там нужно указывать url, по которому будет доступна база данных.
Последние версии libSQL поддерживают неймспейсы. По своей сути - это отдельные базы данных. На одном сервере sqld теперь можно создавать сколько угодно неймспейсов/баз данных и отдельно их реплицировать.
По итогу, теперь есть все, чтобы создать неймспейс для каждого проекта. Для этого нужно выполнить http-запрос на порт 8082
1POST http://127.0.0.1:8082/v1/namespaces/seliger/create
2Content-Type: application/json
3
4{
5 "dump_url": "http://seliger.local:8080"
6}
seliger - это название нашей новой базы. База данных должна быть доступна по URL http://seliger.local:8080. Это отголоски того, что ребята в первую очередь пилят ентерпрайзную turso.tech. Протокол для работы с сервером sqld ориентируется на заголовок Host, поэтому нам нужно заморочиться с /etc/hosts и указать строчку:
1127.0.0.1 seliger.local
Только после этого можно обращаться к базе данных по url http://seliger.local:8080
Наша база готова к использованию, но чтобы все было совсем топово - нужно настроить репликацию базы в s3 хранилище. Тут описаны подробности, и когда ни-будь я до этого доберусь.
Кстати, есть удобный консольный инструмент для настройки libSQL - libsqltui. С помощью этого инструмента значительно проще использовать admin API. Написан на Go. Рекомендую попробовать
Теперь подключаем libSQL в PocketBase
И так, теперь в PocketBase 0.23+ добавлена поддержка функции DBConnect
в качестве конфигурации приложения для загрузки пользовательских сборок SQLite и драйверов, совместимых со стандартным пакетом Go database/sql
. Функция должна вернуть *dbx.DB
с которым будет работать весь код PocketBase.
Сама функция DBConnect
вызывается дважды. Один раз для основной базы pb_data/data.db
, в которой хранятся все пользовательские коллекции. Второй раз для pb_data/auxiliary.db
, которая используется для записи логов и другой системной информации. Таким образом, мы можем использовать libSQL базу только для основных данных, не трогая логику записи логов.
Ниже привел пример, как можно подключится к серверу libSQL, который мы настроили ранее:
1package main
2
3import (
4 "log"
5
6 "github.com/pocketbase/dbx"
7 "github.com/pocketbase/pocketbase"
8 "github.com/pocketbase/pocketbase/core"
9
10 _ "github.com/tursodatabase/libsql-client-go/libsql"
11)
12
13// register the libsql driver to use the same query builder
14// implementation as the already existing sqlite3 builder
15func init() {
16 dbx.BuilderFuncMap["libsql"] = dbx.BuilderFuncMap["sqlite3"]
17}
18
19func main() {
20 app := pocketbase.NewWithConfig(pocketbase.Config{
21 DBConnect: func(dbPath string) (*dbx.DB, error) {
22 if strings.Contains(dbPath, "data.db") {
23 return dbx.Open("libsql", "http://seliger.local:8080?authToken=YWRtaW46YWRtaW4=")
24 }
25
26 // optionally for the logs (aka. pb_data/auxiliary.db) use the default local filesystem driver
27 return core.DefaultDBConnect(dbPath)
28 },
29 })
30
31 // any custom hooks or plugins...
32
33 if err := app.Start(); err != nil {
34 log.Fatal(err)
35 }
36}
Обратите внимание на authToken - это тот самый токен, указанный при старте контейнера в env SQLD_HTTP_AUTH. Есть несколько способов, как указать токен. Тут для примера указывается basic, что не очень секурно. Нужно авторизироваться по JWT-токену, тут аж целый абзац про авторизацию.
Альтернативы
Конечно, libSQL не первое подобное решение. За последние несколько лет случился бум распределенных SQLite. Например, в документации PocketBase предлагается использовать litestream, как решение для реплицирования базы SQLite на s3.
Какие еще есть селфхостед решения:
- mvsqlite - распределенный MVCC SQLite который работает поверх FoundationDB.
- rqlite - это распределенная реляционная база данных, которая сочетает в себе простоту SQLite с надежностью отказоустойчивостью и высокой доступностью. Она дружелюбна для разработчиков, проста в эксплуатации и спроектирована для обеспечения надежности при минимальной сложности.
Rqlite вполне можно использовать для работы с последними версиями PocketBase. Поддерживается работа через стандартный пакет database/sql
с помощью драйвера. У них значительно лучше устроена документация, но мне на глаза rqlite попался позже, поэтому вернусь к нему в следующий раз
В марте 2024-го вышла статья “Distributed SQLite: Paradigm shift or hype?”. Автор рассуждает о буме систем, построенных на базе SQLite, таких как Cloudflare D1, fly.io + LiteFS и Turso. Самый важный вывод, с которым я полностью согласен, - если есть возможность использовать проверенные решения, такие как PostgreSQL, то используйте их, потому что там уже собаку съели на всех проблемах распределенных систем и они проверены временем. Со всеми новыми модными решениями на базе SQLite придется идти по граблям
Если вы совсем не доверяете всему этому хайпу с SQLite, то посмтрите в сторону PostgresBase. Это форк PocketBase, который работает с PostgreSQL. К сожалению, он не очень регулярно обновляется
Итого
libSQL - то еще поделие без нормальной документации на костылях. Но! С помощью libSQL можно значительно расширить возможности PocketBase и превратить его из карманной админки в инструмент для быстрого запуска довольно серьезных проектов.
Если бы была возможность использовать PocketBase, например, с PostgreSQL, то это все стало бы значительно удобней и проще.
Кроме того, можно попробовать использовать другие решения вместо libSQL, такие как rqlite.
Ссылки
Их тут очень много, но они все очень интересные
- Видео PocketBase Just Got An SQL Upgrade
- Использование libSQL в PocketBase
- Документация по libSQL
- libSQL: Diving Into a Database Engineering Epic
- Внутренние компоненты SQLite: как работает самая используемая база данных в мире
- Get started with libSQL, a next-gen fork of SQLite
- Introducing libsql Migrate
- SQLite Internals: How The World’s Most Used Database Works
- libsqltui Инструмент для управления базами в libSQL
- Distributed SQLite: Paradigm shift or hype? и замечательное обсуждение этой статьи
- Amazingly Simple PocketBase Hosting