SystemdDoctor
мешает работе база данных блокировки

database is locked: служба на SQLite теряет запись

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

Вероятные причины

По порядку: сверху то, что встречается чаще.

  1. Ожидание занятой базы слишком короткое

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

  2. База в откатном режиме вместо режима упреждающей записи

    В откатном режиме читающие и пишущий мешают друг другу. В режиме упреждающей записи чтение и запись идут одновременно, и остаётся единственное ограничение — один пишущий.

  3. Базу держит второй экземпляр службы

    Копия программы, запущенная руками, в контейнере или из старого unit, пишет в тот же файл. Каждая из копий считает себя единственной.

  4. База лежит на сетевом носителе

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

Диагностика

Команды идут в том порядке, в котором их стоит выполнять: каждая следующая проверяет то, что осталось после предыдущей.

Сколько записей потеряно за сутки: это и есть цена беды.

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

Решение

Решение своё для каждой причины. Сначала определите, какая из них ваша, — иначе правки наложатся друг на друга.

1. Ожидание занятой базы слишком короткое
Почему происходит
Встретив чужую запись, 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) до нескольких секунд и сведите запись к одной очереди внутри программы. Повтор запроса снаружи без ожидания только умножает встречи.
2. База в откатном режиме вместо режима упреждающей записи
Почему происходит
В откатном режиме читающие и пишущий мешают друг другу. В режиме упреждающей записи чтение и запись идут одновременно, и остаётся единственное ограничение — один пишущий.
Как проверить
Посмотрите режим журналирования файла базы: он хранится в самом файле.
python3 -c "import sqlite3;c=sqlite3.connect('file:/путь/к/базе.db?mode=ro',uri=True);print(c.execute('pragma journal_mode').fetchone())"
Как исправить
Остановите службу, снимите копию файла и переведите базу в режим упреждающей записи: PRAGMA journal_mode=WAL;. Настройка хранится в файле и переживает перезапуск, менять код для этого не нужно.
3. Базу держит второй экземпляр службы
Почему происходит
Копия программы, запущенная руками, в контейнере или из старого unit, пишет в тот же файл. Каждая из копий считает себя единственной.
Как проверить
Посмотрите, какие процессы держат файл базы открытым.
sudo fuser -v /путь/к/базе.db 2>&1 | tail -5
systemctl list-units --type=service --state=running --no-legend | grep -i имя
Как исправить
Оставьте один экземпляр: лишние копии остановите и уберите из автозапуска. Одновременная работа двух копий с одним файлом рано или поздно кончается не потерянной записью, а повреждённой базой.
4. База лежит на сетевом носителе
Почему происходит
Блокировки на сетевых файловых системах работают не так, как на местных, а режим упреждающей записи там неприменим: он требует общей памяти для всех процессов, а на разных машинах её нет.
Как проверить
Посмотрите, на каком носителе лежит файл.
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',)

Связанные ошибки

Источники

  • SQLite: коды результата, SQLITE_BUSY
    Отказ означает столкновение с другим соединением, обычно из другого процесса: одновременно писать разрешено только одному. Ожидание задаётся busy_timeout.
    документация программы
    сверено 21 сентября 2026
  • SQLite: режим упреждающей записи (WAL)
    Читающие не мешают пишущему и наоборот, но пишущий всё равно один; на сетевой файловой системе режим не работает — нужна общая память; настройка постоянная.
    документация программы
    сверено 21 сентября 2026
  • Проверено на этой машине: systemd 255 (255.4-1ubuntu8.17), Ubuntu 24.04
    Воспроизведено на этой машине: два соединения к одному файлу, первое открыло запись — второе сразу получило «database is locked». У базы панели x-ui на этой машине режим журналирования — откатный («delete»); значение ожидания занятой базы у чужого процесса так не посмотреть, оно задаётся в его собственном соединении.
    собственная проверка, systemd 255
    сверено 21 сентября 2026