SQLite хороша тем, что в ней нет сервера. Есть один файл, есть приложение, которое его открывает, и на этом инфраструктура заканчивается. Для небольшого сервиса, внутренней админки, бота или API с умеренной записью это честное и удобное решение. Но у простоты есть обратная сторона: вся история работы приложения лежит на одном диске одной виртуальной машины. Диск потерян, образ повреждён, администратор удалил не тот каталог, и «простая локальная база» превращается в пустое место.

Litestream закрывает именно эту дыру и не берётся за большее. Он работает рядом с SQLite, непрерывно отправляет изменения базы в совместимое объектное хранилище и позволяет потом восстановить файл. Руководство Litestream: Getting started показывает это на примере S3-совместимого MinIO. Всё остальное, то есть как далеко отстаёт копия, что именно вы получите при восстановлении и чем эта схема не является, придётся выяснять самим. Этим мы и займёмся.

Четыре предмета, которые легко перепутать

Разговоры о резервировании SQLite часто путаются из-за слова «база». Одним словом называют сразу четыре разных вещи, и у каждой своя судьба. Полезно развести их заранее.

Из чего состоит схема
ПредметГде живётЧто с ним происходит
SQLite-файлДиск VPS, где работает приложениеПринимает записи от приложения, это единственный «живой» экземпляр
Поток репликации LitestreamПроцесс на том же сервереСледит за изменениями файла и отправляет их наружу
Внешний объектОбъектное хранилище, совместимое с S3Хранит накопленные данные для восстановления; сам по себе не отвечает на SQL-запросы
Восстановленный экземплярНовый файл, где угодноПоявляется только после отдельной операции restore и далее живёт как обычная SQLite-база

Ключевое слово в последней строке: «отдельной». В руководстве replicate и restore показаны как разные операции. Replicate работает постоянно в фоне и следит за живой базой. Restore запускается руками или скриптом, когда нужно получить файл обратно. Между ними нет автоматического моста: объект в хранилище не превращается в рабочую базу сам, а работающая база не подменяется восстановленной сама.

Из этого вытекает неочевидная вещь. Хранилище с вашими изменениями не является второй базой. Вы не можете направить на него приложение, подключиться к нему клиентом SQL или прочитать «на всякий случай» последнюю строку таблицы. Прочитать данные можно только из файла, который получился после восстановления. Пока такого файла нет, у вас не резервная копия, которую вы проверили, а надежда на неё.

Что значит «непрерывно», если запись всё равно идёт через сеть

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

Цена за это простая: между последней записью в локальный файл и тем моментом, когда её можно восстановить из хранилища, существует интервал. Он может быть малым, но он не равен нулю. Если сервер исчезнет внутри этого интервала, самые свежие записи не доедут. Обещать «нулевую потерю» для такой схемы нельзя, и честнее обсуждать не обещание, а число: сколько секунд отставания вы наблюдаете у своей конкретной системы и устраивает ли вас такой хвост.

От чего зависит отставание? Назовём факторы, которые вы реально можете увидеть и изменить:

  • интенсивность записи: чем больше изменений приложение создаёт в единицу времени, тем больше нужно отправить;
  • пропускная способность и задержка канала от VPS до хранилища: хранилище в соседней стране ведёт себя иначе, чем хранилище рядом;
  • состояние самого процесса репликации: остановленный или упавший процесс означает, что отставание растёт с каждой новой транзакцией;
  • доступность хранилища и правильность прав: если запись в объект отклоняется, данные копятся локально, а наружу не уходят.

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

Как увидеть отставание своими глазами

Самый честный способ измерить отставание — завести в базе метку времени, которую можно сравнить с восстановленной копией. Это не особенность Litestream, а обычная логика наблюдения: вы пишете известное значение и смотрите, когда оно появится с другой стороны.

Добавьте в базу крохотную служебную таблицу с одной строкой:

CREATE TABLE IF NOT EXISTS heartbeat (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  beat_at TEXT NOT NULL
);

И пусть небольшой скрипт раз в несколько секунд обновляет эту строку текущим временем в UTC:

INSERT INTO heartbeat (id, beat_at)
VALUES (1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
ON CONFLICT(id) DO UPDATE SET beat_at = excluded.beat_at;

Запускать его можно простым циклом оболочки с паузой в десять секунд или задачей планировщика, а в проде такой пульс может выполнять само приложение. Важно не то, чем именно вы пишете, а то, что запись идёт через тот же файл, который охраняет Litestream, и что вы знаете интервал между ударами пульса.

Теперь разберём расчёт на условном примере. Допустим, пульс бьётся каждые 10 секунд. Вы восстанавливаете копию в отдельный файл и читаете из неё beat_at: там 14:03:20. А в живой базе на момент, когда вы одновременно запросили её значение, было 14:03:31. Разница равна 11 секундам. Это и есть наблюдаемое отставание копии, огрублённое шагом пульса: реальное значение лежит где-то между 11 и 21 секундой минус погрешность вашего же запроса. Чтобы получить число точнее, уменьшайте шаг пульса, но помните, что вы при этом увеличиваете нагрузку на запись и ради наблюдения создаёте дополнительные изменения.

Одно измерение мало что скажет. Отставание бывает разным: тише ночью, больше в час пик, заметно длиннее в момент, когда приложение выполняет пакетное обновление тысяч строк. Поэтому сделайте несколько замеров в разные часы и запишите максимум, который видели, а не среднее. Для вопроса «сколько работы я могу потерять» важен худший случай, а не типичный.

Есть и обратный взгляд на ту же проверку. Пульс отвечает на вопрос «какой момент я могу восстановить». Это, пожалуй, самая точная формулировка того, что даёт резервирование: не «всё, что было», а «состояние на такой-то момент в недавнем прошлом». Когда вы говорите о требованиях к потере данных с коллегой или заказчиком, оперируйте именно этим моментом, а не словом «бэкап».

Какие права нужны процессу и зачем не давать лишнего

Хранилище надо подключить, а значит, процессу репликации нужны ключи. Здесь проще всего пойти по пути «выдам полный доступ к бакету, чтобы работало». Разумнее задать вопрос: какие действия процесс должен выполнять?

  • Записывать объекты. Это основная работа: отправлять во внешнее хранилище накопленные изменения.
  • Читать и перечислять объекты. Без этого невозможно восстановление, а также невозможно понять, что уже лежит в хранилище. Для самой репликации часто нужен и просмотр содержимого своего префикса.
  • Удалять объекты. Вот тут нужно остановиться. Если ваша политика хранения предполагает, что процесс сам убирает старые данные, право удаления ему понадобится. Если же вы чистите хранилище иначе, этого права у процесса может не быть, и его отсутствие стоит проверить на практике.

Теперь последствия лишнего доступа. Ключи лежат на том же VPS, где работает приложение. Это значит, что любой, кто получил контроль над сервером через уязвимость в приложении, украденный SSH-ключ или ошибку в конфигурации, получает и ключи от хранилища. Если эти ключи позволяют удалять и перезаписывать всё в бакете, то один инцидент уничтожает сразу и живую базу, и её копию. Резервная копия, которую можно стереть с того же сервера, который она должна защищать, защищает только от поломки диска, но не от злого умысла или собственной ошибки скрипта.

Поэтому полезно разделять доступы по ролям. Процесс репликации живёт с минимальным набором действий над своим префиксом, а не над всем аккаунтом. Для восстановления, которое вы запускаете с другой машины или с вашего рабочего места, достаточно прав на чтение и перечисление. Такие ключи не должны храниться на рабочем сервере. Если их украдут, злоумышленник сможет прочитать данные, но не уничтожит копию. Это уже вопрос конфиденциальности, а не потери.

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

Где должно лежать хранилище, чтобы оно действительно что-то спасало

Руководство показывает MinIO, потому что его удобно запустить за минуту и посмотреть, как всё работает. Но в нём есть важная оговорка: демонстрационный MinIO без постоянного тома хранит данные только пока существует контейнер. Остановили и удалили контейнер, а вместе с ним исчезли и «резервные копии». Для боевой схемы нужно постоянное хранилище, то есть том, который переживает пересоздание контейнера.

Эту логику можно продолжить. Если вы подняли MinIO на том же VPS, на том же диске, где живёт приложение, то при потере диска вы теряете и базу, и её копию. Формально резервирование есть, но от главного риска, потери единственного диска, оно не спасает. Хранилище должно жить отдельно от сервера приложения: у другого провайдера, в другой стойке или хотя бы на другой машине с собственным диском. Подойдёт и управляемое S3-совместимое хранилище. Важно, чтобы выход из строя одной машины не уносил обе копии.

Отдельный вопрос — расстояние. Объект, лежащий далеко от VPS, дольше доезжает и сильнее зависит от качества канала. Это отражается на отставании, которое вы только что научились измерять. Измерьте его для выбранного места, а не для воображаемого.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Восстановление: сначала в новый файл, рядом, а не поверх

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

Почему это важно именно для SQLite? Потому что база здесь — это просто файл, и случайно перезаписать его проще простого. Команда, которой вы хотели «проверить», может превратиться в команду, которой вы «заменили». Если восстановление пишет результат в заведомо пустое место, например в отдельный каталог проверок, то худший исход — неудачная попытка, а не уничтоженная рабочая база.

Разумный порядок действий такой:

  1. Подготовьте пустой каталог для результата, лучше вне каталога приложения.
  2. Убедитесь, что приложение и процесс репликации продолжают работать как раньше: восстановление не требует их останавливать, потому что оно только читает из хранилища.
  3. Выполните операцию restore, указав выходной файл в этом новом каталоге. Конкретные ключи и параметры возьмите из актуальной справки по вашей версии Litestream: названия опций лучше сверять с той версией, которая у вас установлена, а не с памятью.
  4. Дождитесь завершения и убедитесь, что файл появился и его размер выглядит правдоподобно.

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

Что значит «восстановилось»: проверяем чтением, а не размером файла

Файл появился — это ещё не успех. Размер говорит только о том, что что-то записалось на диск. Успех — это когда из файла можно прочитать данные, которые нужны приложению. Проверка делается в несколько слоёв, от простого к предметному.

Целостность самого файла. SQLite умеет проверять свою структуру:

sqlite3 /srv/restore-check/app.db "PRAGMA integrity_check;"

Ожидаемый ответ — одна строка ok. Любой другой вывод означает, что файл повреждён или восстановление прошло неправильно.

Пульс. Прочитайте служебную строку и вычислите отставание, как мы делали выше:

sqlite3 /srv/restore-check/app.db "SELECT beat_at FROM heartbeat;"

Предметные данные. Целостный файл может быть целостно устаревшим или целостно неполным. Поэтому выполните запросы, которые отражают смысл приложения. Сколько строк в главной таблице заказов? Какая самая поздняя дата создания записи? Находится ли там пользователь, зарегистрированный последним перед проверкой? Сравните ответы с живой базой. Они не обязаны совпасть до последней строки (это и есть отставание), но расхождение должно укладываться в то, что вы измерили пульсом. Если в восстановленной базе не хватает записей за час, а пульс показывает 12 секунд, значит, что-то идёт не так, и причину нужно искать до аварии, а не после.

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

Можно оформить критерии приёмки таблицей и прикладывать её к каждой проверке:

Критерии приёмки восстановления
ПроверкаЧто считается успехом
integrity_checkЕдинственная строка ok
ПульсОтставание не больше значения, которое вы приняли как допустимое
Количество строк в ключевых таблицахРасхождение с живой базой объясняется отставанием
Запуск тестового экземпляра приложенияОсновной сценарий проходит без ошибок схемы

Проверку нужно повторять, а не делать один раз при внедрении. Изменились ключи, поменялась схема базы, вырос объём, обновилась версия, и всё это способно незаметно сломать цепочку. Регулярное восстановление в отдельный каталог — самый дешёвый способ узнавать о таких поломках без ущерба.

Резервная копия, второй писатель и переключение — три разные задачи

Чтобы ожидания не разошлись с возможностями, разведём три сюжета, которые в разговорах часто сливаются в один.

Резервирование. Это то, что делает Litestream: сохранять изменения единственной живой базы во внешнем месте. Писатель один, файл один, копия нужна на случай потери. Задача хорошо определена, и для маленькой системы её решение не требует больше ничего.

Несколько одновременно пишущих серверов. SQLite — это файл, который открывает приложение на конкретной машине. Если вы запустите два экземпляра приложения на двух серверах, у каждого будет собственный файл. Litestream не склеивает их в общую базу и не разрешает конфликты записи. Если направить обоих на один и тот же путь хранилища, вы получите не мультиписатель, а испорченную историю. Нужна общая запись с нескольких узлов — это признак того, что задача перерасла SQLite. Тогда стоит смотреть на серверную СУБД, и по этому поводу у нас есть отдельный разбор: SQLite перестал справляться: как переехать на взрослую базу.

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

Из этого следует спокойный вывод: для небольшой системы с одним писателем не нужно строить платформу высокой доступности. Достаточно отдельного хранилища, измеренного отставания и отработанной процедуры восстановления. Если простой на время ручного восстановления вас устраивает, добавлять сложность незачем. Если нет, значит, требования к доступности выше, чем способна дать схема «один файл плюс копия», и это повод пересмотреть выбор базы, а не уговаривать Litestream делать то, для чего он не предназначен.

Какой VPS нужен такой схеме

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

У VPS MAATRIX диски на NVMe, процессоры AMD EPYC и root-доступ, а значит, можно самостоятельно поставить Litestream, настроить процесс как службу и держать каталог для проверок. В линейке есть тарифы Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe), Uab (2 vCPU, 4 ГБ, 80 ГБ), Heka (4 vCPU, 8 ГБ, 160 ГБ), Maat (8 vCPU, 16 ГБ, 240 ГБ) и Djed (16 vCPU, 32 ГБ, 320 ГБ). Эти цифры описывают ресурсы, а не ёмкость: сколько записей в секунду выдержит ваше приложение и как быстро вырастет база, покажут только собственные замеры. Локации в публичном предложении указаны такие: UK, US, RU и FR. Доступность конкретного образа и операционной системы уточняется в форме заказа.

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

Репетиция, после которой всё становится на места

Теперь соберём всё в короткий сценарий, который можно прогнать в тихий час. Он занимает минуты, а ясности даёт больше, чем любые рассуждения.

Сначала убедитесь, что пульс бьётся: значение beat_at в живой базе обновляется. Затем в отдельный каталог восстановите копию из хранилища. Проверьте PRAGMA integrity_check, прочитайте пульс из копии и сравните его со временем в живой базе. Запишите отставание. Пересчитайте число строк в нескольких ключевых таблицах и сопоставьте с живой базой. Поднимите на копии тестовый экземпляр приложения и пройдите основной сценарий. Остановите его и очистите каталог проверок.

Если на каком-то шаге что-то не сошлось, это ценная находка, потому что вы нашли проблему при спокойной репетиции, а не посреди аварии. Чаще всего всплывают три вещи: у ключей на чтение нет нужных прав, процесс репликации давно остановлен, а никто не заметил, или восстановленная схема не подходит новой версии приложения.

Финальная сцена: два файла рядом

Теперь представим, что настоящая авария случилась. Диск исходного VPS недоступен, но вы не в панике: у вас есть хранилище, отработанная процедура и знание, какое отставание считать нормальным. На новой машине вы восстанавливаете базу в отдельный каталог, проверяете целостность, читаете пульс, видите отставание в пределах привычных секунд и убеждаетесь, что последние известные заказы на месте.

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

А исходная база, если до неё удаётся добраться, например диск оказался доступен для чтения, никуда не исчезает. Вы переименовываете её, скажем, в app.db.old, убираете в каталог для разбора и оставляете нетронутой. Рядом лежат два файла: тот, с которым приложение работает сейчас, и тот, где остались последние секунды до аварии. Их можно сравнить запросом, найти записи, не доехавшие до хранилища, и при необходимости перенести их руками. В этой паре видна вся суть схемы: Litestream не обещает, что ничего не потеряется, но делает потерю маленькой, измеримой и поправимой.