Есть один способ делать резервные копии SQLite, который выглядит очевидным, применяется чаще всех и почти всегда неправилен: скопировать файл базы обычным копированием, пока приложение работает.
Так делать нельзя, и коварство в том, что результат выглядит рабочим. Копия открывается, запросы к ней выполняются, размер похож на правду. Проблема обнаруживается в день восстановления: часть транзакций отсутствует, часть страниц относится к разным моментам времени, а индексы ссылаются на строки, которых в копии нет. Это не «повреждённый файл», это согласованный по форме и несогласованный по содержанию снимок.
#Что понадобится
- Утилита
sqlite3или доступ к базе из кода на любом языке. - Место для копии на другом устройстве. Копия рядом с оригиналом защищает только от вашей ошибки, но не от смерти диска.
- Пятнадцать минут на первую проверку восстановления. Без неё вся работа не имеет смысла.
Резервное копирование — та часть эксплуатации, которую ставят последней и вспоминают первой; в общем маршруте о том, как поднять свой ИИ-сервис, она относится к шестому этапу вместе с мониторингом.
#Шаг 1. Понять, из чего состоит база
В режиме WAL база — это не один файл, а три:
| Файл | Что в нём |
|---|---|
persona.db |
основные данные |
persona.db-wal |
транзакции, ещё не перенесённые в основной файл |
persona.db-shm |
разделяемая память для координации соединений |
Отсюда и берётся главная ошибка. Скопировав только persona.db, вы получаете состояние на момент последней контрольной точки, а не текущее: всё, что накопилось в журнале, отсутствует. Скопировав все три обычным копированием, вы получаете три файла, снятых в разные моменты, — что зачастую хуже, чем один.
Проверить, в каком режиме работает ваша база, и разобраться с блокировками поможет разбор WAL и конкурентного доступа в SQLite.
#Шаг 2. Выбрать корректный способ
Их три, и они не равнозначны.
#Способ А. Команда .backup в утилите
sqlite3 /var/lib/persona/persona.db ".backup '/backup/persona-$(date +%F).db'"
Использует встроенный механизм резервного копирования: копия делается постранично, с учётом идущих транзакций, и получается согласованной без остановки приложения. Самый короткий корректный вариант.
#Способ Б. VACUUM INTO
sqlite3 /var/lib/persona/persona.db "VACUUM INTO '/backup/persona-$(date +%F).db'"
Даёт согласованную копию и заодно уплотняет её: без свободных страниц, с пересобранными индексами. Копия выходит меньше оригинала — иногда заметно. Плата: операция дороже по процессору и диску, и на большой базе занимает ощутимо больше времени, чем способ А.
#Способ В. Из кода: контрольная точка плюс механизм копирования
Так это устроено у нас:
source = sqlite3.connect(str(src))
source.execute("PRAGMA wal_checkpoint(FULL)") # перенести журнал в основной файл
source.commit()
source.close()
source = sqlite3.connect(str(src))
target = sqlite3.connect(str(dst))
source.backup(target) # согласованный снимок
target.close(); source.close()
Два шага здесь не дублируют друг друга. Контрольная точка сокращает то, что придётся тащить, и оставляет журнал в понятном состоянии; механизм копирования обеспечивает согласованность, даже если основной процесс продолжает писать.
Чего делать не нужно: cp, copy, robocopy, синхронизация папки в облако. Все они копируют байты без всякого понятия о транзакциях.
#Шаг 3. Не забыть про всё, что не база
База — это только структурированная часть. Рядом обычно лежит то, без чего восстановление даст пустой интерфейс:
- миниатюры и вложения на диске;
- каталог данных приложения с настройками и сессиями;
- файл
.env— и вот здесь стоп.
Секреты в резервной копии — отдельная тема, и не в том смысле, что их надо копировать бережно, а в том, что копия становится хранилищем ключей. У Persona ключ участника лежит в таблице настроек открытым текстом, значит любая копия базы — это копия всех ключей всех, кто на инстансе. То же касается полного экспорта: он содержит ту же таблицу и не редактируется. Такой файл нельзя прикладывать к обращению в поддержку и нельзя класть в общедоступное облако. Что и как редактировать при передаче данных наружу, разобрано в статье про резервные копии и приватность.
Практическое следствие: копия либо шифруется, либо лежит там, куда есть доступ только у вас. Если шифруете — мастер-пароль хранится не рядом с копией и не в репозитории.
#Шаг 4. Автоматизировать и следить, что оно работает
#!/usr/bin/env sh
set -eu
DB=/var/lib/persona/persona.db
DST=/backup/persona-$(date +%F-%H%M).db
sqlite3 "$DB" ".backup '$DST'"
sqlite3 "$DST" "PRAGMA integrity_check;" | grep -qx ok || { echo "БИТАЯ КОПИЯ: $DST" >&2; exit 1; }
gzip -9 "$DST"
# хранить 14 последних
ls -1t /backup/persona-*.db.gz | tail -n +15 | xargs -r rm --
Три детали, которые отличают этот скрипт от типового.
Во-первых, проверка целостности сразу после снятия. Копия, о которой вы узнаёте, что она битая, через месяц, — хуже отсутствия копии, потому что вы на неё рассчитывали.
Во-вторых, ненулевой код возврата. Скрипт, который молча завершается при ошибке, создаёт ровно ту иллюзию, ради борьбы с которой всё и делается.
В-третьих, свежесть копии должна попасть в мониторинг. Резервное копирование ломается тихо: место кончилось, права поменялись, путь переехал. Проверка «последняя копия не старше суток» стоит одну строку и ловит все три случая; куда её встроить — в статье про мониторинг личного сервиса.
#Проверка результата
Копия не считается копией, пока из неё не восстановились. Полная приёмка — четыре шага.
# 1. распаковать в отдельный каталог, не в рабочий
mkdir -p /tmp/restore && gunzip -c /backup/persona-2026-08-26-0300.db.gz > /tmp/restore/persona.db
# 2. целостность
sqlite3 /tmp/restore/persona.db "PRAGMA integrity_check;" # ждём ok
# 3. содержательная проверка: данные на месте и свежие
sqlite3 /tmp/restore/persona.db "SELECT COUNT(*) FROM messages;"
sqlite3 /tmp/restore/persona.db "SELECT MAX(created_at) FROM messages;"
# 4. приложение реально стартует на этой базе
PERSONA_DB_PATH=/tmp/restore/persona.db .venv/bin/python -m app.cli doctor
Четвёртый шаг — самый важный и самый пропускаемый. Целостность на уровне страниц не отвечает на вопрос, поднимется ли на этой базе приложение: версия схемы может не совпасть с версией кода. Это же упражнение — обязательная часть регламента применения схемы, разобранного в статье про миграции базы данных без потери данных.
Критерий приёмки: максимальная дата в шаге 3 отличается от времени снятия копии на минуты, а не на дни. Расхождение в дни означает, что вы копируете основной файл без журнала.
#Что может пойти не так
Копия сделана, а данных за последние часы нет. Копировали persona.db обычным копированием, журнал остался за бортом. Переходите на способ А или В.
integrity_check возвращает список ошибок. Копия негодная. Не пытайтесь её чинить: возьмите предыдущую и разберитесь, почему сломалась эта. Частая причина — копирование во время работы обычными средствами.
Копия занимает столько же, сколько база, и место кончилось. Сжатие плюс VACUUM INTO заметно уменьшают объём. Но сначала посмотрите, что именно занимает место: у систем с записью экрана основной вес обычно не в базе, а во вложениях, и копировать их по тому же расписанию не обязательно.
Восстановились, а приложение не стартует. Версия схемы в копии старше кода. Правильный порядок: восстановить копию, запустить ту версию приложения, при которой она снималась, дать ей применить схему, и только потом обновляться.
Копии есть, а восстановиться некуда. Копия на том же диске защищает от вашей ошибки и не защищает от смерти диска. Минимум — второе физическое устройство.
#Частые вопросы
#Можно ли делать копию, не останавливая приложение?
Да, и именно для этого существуют встроенный механизм копирования и VACUUM INTO. Останавливать приложение нужно только для полного посекторного копирования всех трёх файлов, а это как раз тот способ, которым лучше не пользоваться.
#Как часто снимать копии?
От того, сколько данных вы готовы потерять. Для личного сервиса разумная отправная точка — ежедневная копия с хранением двух недель, плюс копия перед каждым обновлением, меняющим схему. Копия перед обновлением важнее ежедневной: именно она понадобится.
#Достаточно ли синхронизации каталога в облако?
Нет, и это самый опасный из «почти правильных» вариантов. Синхронизация копирует файлы по мере изменения, то есть по определению в разные моменты времени. Кладите в облако готовый архив копии, снятой корректно, а не рабочий каталог базы.
#Что делать со старым файлом -wal после восстановления?
Ничего: корректно снятая копия его не требует. Если рядом с восстановленной базой лежит чужой -wal от другой базы — удалите его, иначе SQLite попытается применить журнал к не тому файлу.
#Чем резервная копия отличается от экспорта данных?
Копия предназначена для восстановления того же приложения и содержит всё, включая служебное и секретное. Экспорт предназначен для переноса содержимого — он читаемый, выборочный и обычно очищенный. Про второй сценарий — в статье про экспорт и перенос памяти ИИ.
#Нужно ли что-то менять в этой схеме при переезде на другую машину?
Переезд — отдельная процедура с приёмкой, а не просто восстановление копии: помимо базы едут вложения, настройки и пути. Порядок разобран в гайде про перенос экземпляра на другой диск или машину.