Гайды

Резервное копирование базы SQLite без порчи данных

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

Есть один способ делать резервные копии 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 попытается применить журнал к не тому файлу.

#Чем резервная копия отличается от экспорта данных?

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

#Нужно ли что-то менять в этой схеме при переезде на другую машину?

Переезд — отдельная процедура с приёмкой, а не просто восстановление копии: помимо базы едут вложения, настройки и пути. Порядок разобран в гайде про перенос экземпляра на другой диск или машину.

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт