database is locked: служба на SQLite теряет запись
SQLite допускает только одного пишущего одновременно, и отказ «database is locked» означает встречу с другим соединением — как правило, из другого процесса. Служба обычно не падает: она пишет предупреждение и теряет одну порцию данных, поэтому беда живёт в журнале месяцами. Виноваты в ней не столько сами одновременные записи, сколько сочетание короткого ожидания занятой базы с откатным режимом журналирования, при котором читающие мешают пишущему.
Вероятные причины
По порядку: сверху то, что встречается чаще.
-
Ожидание занятой базы слишком короткое
Встретив чужую запись, SQLite не встаёт в очередь: он ждёт заданное время и возвращает отказ. Ноль или доли секунды означают отказ при первом же совпадении фоновой задачи и обращения из веб-части.
-
База в откатном режиме вместо режима упреждающей записи
В откатном режиме читающие и пишущий мешают друг другу. В режиме упреждающей записи чтение и запись идут одновременно, и остаётся единственное ограничение — один пишущий.
-
Базу держит второй экземпляр службы
Копия программы, запущенная руками, в контейнере или из старого unit, пишет в тот же файл. Каждая из копий считает себя единственной.
-
База лежит на сетевом носителе
Блокировки на сетевых файловых системах работают не так, как на местных, а режим упреждающей записи там неприменим: он требует общей памяти для всех процессов, а на разных машинах её нет.
Диагностика
Команды идут в том порядке, в котором их стоит выполнять: каждая следующая проверяет то, что осталось после предыдущей.
Сколько записей потеряно за сутки: это и есть цена беды.
journalctl -u ИМЯ-СЛУЖБЫ --since "-1 day" --no-pager | grep -c "database is locked"Режим журналирования базы. Открытие только для чтения ничего в файле не меняет.
python3 -c "import sqlite3;c=sqlite3.connect('file:/путь/к/базе.db?mode=ro',uri=True);print(c.execute('pragma journal_mode').fetchone())"Кто держит файл открытым прямо сейчас — так находится лишний экземпляр службы.
sudo fuser -v /путь/к/базе.dbФайловая система под базой: сетевая означает, что блокировкам верить нельзя.
findmnt -T /путь/к/базе.dbРешение
Решение своё для каждой причины. Сначала определите, какая из них ваша, — иначе правки наложатся друг на друга.
- Почему происходит
- Встретив чужую запись, SQLite не встаёт в очередь: он ждёт заданное время и возвращает отказ. Ноль или доли секунды означают отказ при первом же совпадении фоновой задачи и обращения из веб-части.
- Как проверить
-
Посмотрите, как часто служба жалуется и совпадают ли жалобы по времени с её же расписанием.
journalctl -u ИМЯ-СЛУЖБЫ --since "-1 day" --no-pager | grep -c "database is locked" journalctl -u ИМЯ-СЛУЖБЫ --since "-1 day" --no-pager | grep "database is locked" | awk '{print $3}' | cut -c1-5 | sort | uniq -c | tail -5
- Как исправить
- Поднимите в настройках приложения ожидание занятой базы (busy_timeout) до нескольких секунд и сведите запись к одной очереди внутри программы. Повтор запроса снаружи без ожидания только умножает встречи.
- Почему происходит
- В откатном режиме читающие и пишущий мешают друг другу. В режиме упреждающей записи чтение и запись идут одновременно, и остаётся единственное ограничение — один пишущий.
- Как проверить
-
Посмотрите режим журналирования файла базы: он хранится в самом файле.
python3 -c "import sqlite3;c=sqlite3.connect('file:/путь/к/базе.db?mode=ro',uri=True);print(c.execute('pragma journal_mode').fetchone())"
- Как исправить
-
Остановите службу, снимите копию файла и переведите базу в режим упреждающей записи:
PRAGMA journal_mode=WAL;. Настройка хранится в файле и переживает перезапуск, менять код для этого не нужно.
- Почему происходит
- Копия программы, запущенная руками, в контейнере или из старого unit, пишет в тот же файл. Каждая из копий считает себя единственной.
- Как проверить
-
Посмотрите, какие процессы держат файл базы открытым.
sudo fuser -v /путь/к/базе.db 2>&1 | tail -5 systemctl list-units --type=service --state=running --no-legend | grep -i имя
- Как исправить
- Оставьте один экземпляр: лишние копии остановите и уберите из автозапуска. Одновременная работа двух копий с одним файлом рано или поздно кончается не потерянной записью, а повреждённой базой.
- Почему происходит
- Блокировки на сетевых файловых системах работают не так, как на местных, а режим упреждающей записи там неприменим: он требует общей памяти для всех процессов, а на разных машинах её нет.
- Как проверить
-
Посмотрите, на каком носителе лежит файл.
findmnt -T /путь/к/базе.db
- Как исправить
- Перенесите базу на местный диск, а сетевой ресурс оставьте под копии. Ни ожидание, ни смена режима журналирования сетевую блокировку не исправят.
Пример вывода
Фоновая задача и обращение из веб-части встретились на одной базе. Пример показательный: он собран на тестовой машине специально для этой страницы, а не взят из чужого журнала.
x-ui[1243]: WARNING - add inbound traffic failed: database is locked
x-ui[1243]: WARNING - node heartbeat: load nodes failed: database is locked
x-ui[1243]: WARNING - node traffic sync: load nodes failed: database is locked
$ python3 -c "import sqlite3;c=sqlite3.connect('file:/etc/x-ui/x-ui.db?mode=ro',uri=True);print(c.execute('pragma journal_mode').fetchone())"
('delete',)
Связанные ошибки
- Приложение не стартует: блокировка миграций базы Служба зависает или падает на применении миграций: осталась блокировка после прерванного запуска.
- Резервное копирование не идёт: хранилище копий заблокировано Задача копирования падает на блокировке: прошлый запуск был прерван и не снял её.
- Prometheus: каталог данных занят другим экземпляром Служба не запускается: блокировка каталога данных после аварийного завершения или второй экземпляр.
- Apache: Invalid command — не включён модуль Apache не запускается: директива требует модуль, который не подключён. Как найти нужный модуль и включить.
- Apache: could not bind to address Apache не занимает порт: конфликт с nginx, порт занят, нет прав на привилегированный порт.
- Apache: конфликт модулей многопроцессности и PHP Apache не запускается после включения модуля PHP: несовместимость с выбранным модулем многопроцессности.
- Apache: правила .htaccess не действуют Файл .htaccess игнорируется: запрещён параметром AllowOverride, нет нужного модуля, файл не читается.
- BIND: порт 53 занят systemd-resolved named не может занять порт 53: его держит локальный разрешатель имён. Как развести их по адресам.
Источники
-
SQLite: коды результата, SQLITE_BUSY
Отказ означает столкновение с другим соединением, обычно из другого процесса: одновременно писать разрешено только одному. Ожидание задаётся busy_timeout. -
SQLite: режим упреждающей записи (WAL)
Читающие не мешают пишущему и наоборот, но пишущий всё равно один; на сетевой файловой системе режим не работает — нужна общая память; настройка постоянная. -
Проверено на этой машине: systemd 255 (255.4-1ubuntu8.17), Ubuntu 24.04
Воспроизведено на этой машине: два соединения к одному файлу, первое открыло запись — второе сразу получило «database is locked». У базы панели x-ui на этой машине режим журналирования — откатный («delete»); значение ожидания занятой базы у чужого процесса так не посмотреть, оно задаётся в его собственном соединении.