WAL-режим SQLite может блокировать короткоживущих читателей

· Программы
Хайнек Шлавак столкнулся с ошибкой «database is locked» в почти не пишущей базе SQLite и выяснил, что виноват WAL-режим, где даже чтение подразумевает запись.

cover.svg

Совет «включите WAL, задайте ненулевой busy timeout и оборачивайте пишущие транзакции в BEGIN IMMEDIATE» повторяют так часто, что он звучит как истина в последней инстанции. Разработчик Хайнек Шлавак напоминает: этот рецепт хорош для типового веб-приложения с долгоживущими соединениями — но не для всякой нагрузки.

Его случай оказался нетипичным. База пишется исключительно редко — иногда реже раза в месяц, — зато читается десятки и сотни раз в секунду. Причём читают независимые процессы без пула соединений: открыть базу в режиме SQLITE_OPEN_READONLY, выполнить один SELECT, закрыть. Чтобы редкие писатели всё же не блокировали читателей, автор на всякий случай перевёл базы в WAL — и получил редкие, но постоянные ошибки database is locked.

Чтение, которое пишет

Корень проблемы — в устройстве WAL. Соединения координируются через файл -shm, и открытие или закрытие пустой WAL-базы может на короткое мгновение потребовать эксклюзивной блокировки. Новый читатель, попавший в это окно, получает SQLITE_BUSY — даже если в базу не пишут прямо сейчас и не писали уже несколько дней.

Наглядное доказательство — время модификации файлов. Сама база не менялась с 21 июля, а спутники -wal и -shm были переписаны 24-го — теми самыми «только читающими» процессами. Для системы, которая по сути ничего не пишет, это удивительно много записи.

Ошибку вообще удалось заметить лишь потому, что таймаут по умолчанию в C-библиотеке SQLite равен нулю. Высокоуровневые обёртки обычно задают своё значение: например, модуль sqlite3 в стандартной библиотеке Python по умолчанию ждёт пять секунд. Автор саркастически радуется: сломалось громко — уже хорошо.

Компромиссы до самого дна

Поскольку записи в его сценарии почти нет, Шлавак вернул базы в классический режим DELETE — и ошибка исчезла. Ненулевой busy timeout тоже решает проблему, но с оговоркой: корректно использовать BEGIN IMMEDIATE в SQLAlchemy оказалось нетривиально.

К заметке прилагается воспроизводимый скрипт на чистой стандартной библиотеке Python. Он запускает множество параллельных процессов, каждый в цикле «открыть — SELECT — закрыть», и сравнивает три сценария: WAL без таймаута, WAL с таймаутом в секунду и режим DELETE без таймаута. На MacBook Pro 2023 года с Python 3.14 первый вариант стабильно даёт от одного до десяти сбоев, а два других — ноль.

Мораль проста и знакома: универсальных настроек не бывает, и даже «безопасный по умолчанию» WAL стоит выбирать под конкретный профиль нагрузки.


Источник: Lobsters

Комментарии

Войдите, чтобы комментировать.

  • Пока нет комментариев.