Подробные статьи о технологиях, определяющих будущее.

Распределённый SQLite: 5 мифов, проверенных за 12 лет

12 лет распределённого SQLite: Raft, Litestream, Turso, D1. Проверяем пять убеждений о согласованности, репликации и о том, когда выбирать Postgres.

Кольцо из маленьких картотечных шкафов, соединённых нитями света, один из которых светится как лидер
Двенадцать лет распределённого SQLite: много архитектур и одна основная идея, которую снова и снова изобретают заново.

Распределённый SQLite прошёл путь от выходных проектов одного человека до категории продуктов с венчурным финансированием, и большинство инженеров даже не заметили целого десятилетия между этими точками. В сентябре 2014 года Филип О'Тул поставил Raft-лог перед SQLite, реплицировал каждую запись по кластеру и выложил код на GitHub под названием rqlite. Двенадцать лет спустя сложилась целая экосистема — dqlite, Litestream, LiteFS, cr-sqlite, libSQL/Turso, Cloudflare D1 — и большая часть того, что о ней думают, неверна, причём интересными способами.

Я запускал кластерные базы данных в продакшене и разгребал последствия их отказов, так что скажу сразу, чтобы вы не наступали на те же грабли: споры о распределённом SQLite почти никогда не про SQLite. Они про то, какой вопрос вы на самом деле задаёте — что будет, если умрёт машина, что будет, если умрёт машина и я не могу ждать, или что будет, если машины вообще нет. Ниже пять мифов, проверенных на двенадцати годах истории.

Миф 1: распределённый SQLite — это новая идея

Нынешняя волна текстов о реплицированном SQLite начинается примерно в 2021 году, когда Litestream и движение «SQLite достаточно» стали громкими, а Turso и Cloudflare D1 появились с финансированием и анонсами запуска. Так укрепилось убеждение, что кто-то умный недавно заметил: SQLite можно реплицировать. На самом деле первая система Raft поверх SQLite появилась в 2014 году, и это даже не первая репликация SQLite — Expensify делала это внутри компании с протоколом в духе Paxos ещё за годы до выпуска Bedrock в 2016-м, а ActorDB примерно в ту же эпоху дала каждому актору Erlang собственную базу SQLite с репликацией через Raft.

Зачем это важно, помимо справедливости в признании авторства? Потому что дизайн, который пережил двенадцать лет независимых переизобретений, о чём-то говорит. Как минимум восемь разных команд с разными потребностями и разными протоколами посмотрели на проблему и пришли к одной и той же архитектуре: SQLite как движок хранения, репликация снаружи. Никто не нашёл причин от этого отказаться. Когда видишь такой паттерн — сходящиеся повторные реализации на протяжении десятилетия, — перед тобой действительно хорошая идея, а не цикл хайпа. Сравните с категориями, которые вспыхивают и исчезают: эта продолжала накапливать ценность.

Это важно и потому, что старые проектные решения до сих пор несут нагрузку. rqlite в 2014 году выбрал быть CP-системой — записи идут через лидера, их должно подтвердить большинство, а разделённое меньшинство отказывает в записи — и так и не передумал. Этот единственный выбор определил HTTP API, уровни согласованности чтения и поведение снимков состояния. Команды, которые считают согласованность переключателем в конфигурации, который можно решить позже, обычно потом жалеют; разрыв между спецификацией, которую вы подразумевали, и той, которую реализовали, — как раз то место, где плодятся распределённые баги.

Миф 2: Raft поверх SQLite даёт дешёвый распределённый Postgres

Это самый дорогой миф в списке. Питч звучит заманчиво: берём самую распространённую базу данных в мире, добавляем консенсус и избавляемся от операционной тяжести клиент-серверной системы. На деле вы получаете однописательную базу, где каждая запись платит сетевой задержкой до большинства узлов, а SQL-поверхность SQLite — никаких хранимых процедур, скромная система типов, ограниченные примитивы конкурентности — остаётся всё той же поверхностью SQLite.

Честная цена — это путь записи. Локальный коммит SQLite — это пара fsync. Коммит с репликацией через Raft выглядит так: лидер дописывает запись в свой лог, отправляет её фолловерам, ждёт, пока большинство сохранит её на диск, применяет к SQLite и только потом отвечает. В здоровой локальной сети это примерно одна-две миллисекунды вместо десятков микросекунд. Между регионами — скорость света плюс диск. Для нагрузки с преобладанием чтения и умеренным темпом записи — хранилища конфигураций, метаданные service discovery, небольшие control plane — это нормально, и именно под эту нишу и создавали rqlite (его история — кластерная система, которой нужно немного реляционного хранилища и нет хорошего ответа на вопрос, на каком узле его держать). Для горячей OLTP-нагрузки это неправильный инструмент.

А дальше идёт операционная реальность, которую никто не пишет в README: выборы лидера — это режим отказа, которым вы теперь владеете. Split-brain предотвращается голосованием большинства, да, но это значит, что трёхузловой кластер, потерявший два узла, отказывает во всех записях. Вы променяли «база недоступна, потому что сервер умер» на «база недоступна, потому что умер кворум», и второе труднее объяснить дежурному инженеру в 3 часа ночи. Если вы не думали о тестировании выбранной системы в духе Jepsen, стоит подумать: один комментатор на Hacker News задал острый вопрос, тестировали ли rqlite в Jepsen, потому что у непроверенных систем обычно прячутся сюрпризы. Тем временем опытный инженер в той же дискуссии отметил, что с современными инструментами запустить настоящий Postgres стало достаточно просто, и «просто используй сервер баз данных» снова легитимный вариант по умолчанию. Он прав.

Весы, на одной чаше простой блок, на другой — клубок цепей и шестерёнок
Консенсус даёт отказоустойчивость, а платите вы за каждую запись.

Миф 3: репликация означает консенсус

Если из двенадцатилетней истории и можно вынести одну мысль, то вот она: «реплицированный SQLite» — не один дизайн, а три угла пространства решений, и появлялись они в показательном порядке. Путать их — значит оказаться с кластером Raft для решения задачи резервного копирования.

  • Консенсус (CP). rqlite, dqlite. Один лидер, сильная согласованность, настоящий кластер. Отвечает на вопрос: «что если машина умрёт и я не могу ждать восстановления?» Цена: задержка записи и операции кворума.
  • Стриминг (один писатель). Litestream, позже LiteFS. Никакого консенсуса: WAL SQLite стримится в объектное хранилище, обеспечивая почти непрерывное аварийное восстановление для одного узла. Отвечает на вопрос: «что если машина умрёт?» Цена: небольшое окно возможной потери данных и время восстановления в секундах или минутах.
  • Слияние (AP). cr-sqlite и движение local-first. CRDT как расширение SQLite, множество писателей расходятся и сливаются без лидера. Отвечает на вопрос: «что если центральной машины нет вообще?» Цена: eventual consistency и семантика слияния, которую нужно проектировать заранее.

Аргументы Бена Джонсона в пользу Litestream заслуживают уважения, потому что в большинстве случаев он был прав: приложения работают на одной машине, и им нужен способ восстановиться после её гибели, а не кластер. Первый вопрос — «что если машина умрёт» — встречается гораздо чаще второго — «и я не могу ждать восстановления». Я видел, как команды разворачивали трёхузловой кластер Raft для защиты базы, у которой требование RTO звучало как «когда-нибудь до обеда». Это не инженерия надёжности, это карго-культ. И, красиво замыкая круг, стриминг WAL в Litestream в итоге вдохновил то, как сам rqlite работает с WAL в версии 8: ветка стриминга подпитала ветку консенсуса.

Поэтому, прежде чем выбирать инструмент, запишите вопрос. Буквально. «Когда узел X умирает, записи должны возобновиться в течение N секунд, и мы принимаем потерю последних M секунд зафиксированных данных». Если M — несколько секунд, а N — минута, Litestream в S3 — ваш ответ, и он никогда не разбудит вас из-за кворума. Если M равно нулю, а N — секунды, вы на территории консенсуса и будете платить за это при каждой записи. Это та же дисциплина, что и любая модель угроз: назовите отказ до того, как купите защиту, — так же, как подходите к системам, устойчивым к катастрофам, заранее решая, от какой именно катастрофы вы защищаетесь.

Миф 4: управляемый «SQLite на периферии» убирает компромиссы

Деньги в этой категории ушли платформам: Turso привлекла венчурное финансирование и была куплена Supabase, а Cloudflare выпустила D1 внутри своей edge-сети. Питч в том, что кто-то другой теперь берёт на себя сложные распределённые части. Иногда это правда. Иногда это маркетинг, и наружу вылезает неприятный секрет: один разработчик Cloudflare публично признал, что D1 — по сути обёртка над одиночным Durable Object, который отдаёт свою базу SQLite, и что часто лучше использовать хранилище Durable Objects напрямую. Это не распределённая база в смысле консенсуса; это один файл SQLite с платформой вокруг. Пользователи, которые держат D1 в продакшене, сообщают о соответствующей надёжности.

Всё это не значит, что управляемые варианты плохи. Это значит, что компромиссы никуда не делись: они переехали за API, где их не видно. Когда вы запускаете rqlite сами, вы точно знаете, что значит CP: три узла, один лидер, подтверждения от большинства, разделённое меньшинство отказывает в записи. Когда вы покупаете «SQLite на периферии», нужно задать те же вопросы чёрному ящику: где писатель, что происходит при разделении сети, каково реальное окно долговечности? Если ответ поставщика расплывчатый, на разборе инцидента вам будет нечего сказать.

Репликация как сервис не отменяет вопроса о согласованности. Она просто переносит ответ в чужую инфраструктуру, которую вы не можете проверить.

Историческую закономерность полезно усвоить: открытые системы (rqlite, dqlite, Litestream) восемь лет развивали идею, прежде чем её кто-то продал, а деньги потекли к тем, кто превратил идею в сервис с ценником. Это обычная судьба инфраструктуры. Покупка часто правильное решение, но покупайте с открытыми глазами, понимая, какой из трёх углов вы на самом деле получаете.

Снежный шар с тёплой избушкой внутри, стоящий в туманном поле серверных стоек
Управляемый SQLite на периферии: уютная одиночная база, завёрнутая в платформу, внутрь которой вы не можете заглянуть.

Миф 5: сильная согласованность — бесплатное обновление по сравнению с бэкапами

Выбор часто подают как бэкапы (слабые, дешёвые) против консенсуса (сильный, дорогой, но строго лучше). Это неправда. Сильная согласованность через Raft — это другой продукт, а не лучшая версия того же продукта, и она накладывает постоянные налоги, которых бэкапы не знают.

Налог первый: пропускная способность. Один практик, использовавший оба варианта, отметил, что rqlite даёт гарантии «хранится в более чем одном месте», но работает заметно медленнее одноузловой базы — это и есть раунд консенсуса, и он никуда не девается. Налог второй: тонкая семантика. Если реплицировать, передавая SQL-выражения, вы наследуете проблемы детерминизма: функции вроде random() или datetime('now') могут расходиться между узлами, если слой их не перепишет, поэтому некоторые реализации поддерживают только подмножество SQL SQLite. Если реплицировать на уровне WAL, этого можно избежать, но вы привязываетесь к формату хранения SQLite на диске и раскладке страниц. Безупречного места для слоя не существует, везде компромисс. Налог третий: распределённые транзакции действительно сложны. Один инженер описал, как проект эволюционировал от варианта, где каждый оператор транзакции был отдельной командой Raft (почти правильно, но медленно), к варианту, где инициирующий узел выполняет всю транзакцию локально, откатывает её и отправляет одну команду коммита со всем пакетом. Такие уроки, добытые кровью, вы усваиваете только при эксплуатации этих систем.

Вот минимальная иллюстрация разницы в пути записи через HTTP API rqlite:

# Local SQLite: one process, one fsync, microseconds.
sqlite3 app.db "INSERT INTO kv VALUES ('flag', 'on')"
# rqlite: this HTTP call hides a Raft round trip —
# leader log append, replication to a majority, then apply.
curl -XPOST 'localhost:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["INSERT INTO kv VALUES (?, ?)", "flag", "on"]'
# The guarantee changed: the write now survives the loss
# of any single node. So did the latency budget.

И обратите внимание, чего вы не получили: масштабирования чтения. Raft реплицирует лог, чтобы фолловеры могли перехватить лидерство; он не делает так, чтобы фолловеры по умолчанию отдавали строго согласованные чтения (rqlite предлагает уровни согласованности чтения именно потому, что это компромисс). Если вы пришли за «распределённостью» в надежде на большую пропускную способность чтения, вам нужны были реплики для чтения, а не консенсус.

Чему учат двенадцать лет Raft поверх SQLite

История этой категории необычно наглядно показывает, как зреют инфраструктурные идеи. Сначала кто-то строит принципиальную вещь под конкретную задачу (rqlite, из реальной проблемы кластеризации на реальной работе). Затем другие переписывают её под свои ограничения упаковки: dqlite от Canonical — это библиотека, которую встраивают, потому что Canonical нужна база данных внутри её продуктов, а rqlite — это сервер, который запускают рядом с чьим угодно продуктом. Потом кто-то доказывает, что типичный случай меньше, чем предполагает принципиальное решение (Litestream), оказывается прав, и вся область перекалибруется. Наконец платформы оборачивают всё это и продают. Если вы достаточно долго работаете в инфраструктуре, этот фильм вы увидите снова: LLM-агенты сейчас проходят те же уроки распределённых систем.

Моя прямолинейная рекомендация после всего этого: начните с одиночного SQLite плюс стриминг WAL в объектное хранилище. Удивительно, как далеко это позволяет уехать, и ваша история отказа — восстановление из бэкапа, а не постмортем о консенсусе. Переходите на систему на основе Raft, только когда можете в одном предложении сформулировать требование к доступности, которое стриминг не выполняет, и принимайте счёт за задержку записи и операции кворума, который за этим придёт. К Postgres тянитесь в момент, когда понадобится его SQL-поверхность или пропускная способность нескольких писателей. А если ничего из этого не подходит, в мире встраиваемых баз данных есть и другие варианты, от встраиваемых графовых баз данных до ниши local-first CRDT.

Итог развенчания мифов: распределённый SQLite не новый, не дешёвый Postgres, не один дизайн, не делается безопасным обёрткой от платформы и не является бесплатным обновлением по сравнению с бэкапами. Это двенадцатилетний, хорошо изученный класс решений с тремя углами, каждый из которых правилен для своего вопроса. Обжигаются те инженеры, которые так и не записали вопрос.