Перейти к содержанию

Когда виновата не файловая система: разбор гонки WAL-reset в SQLite

Опубликовано
  • Админы
16 лет в SQLite жил баг, портивший базу

«База внезапно оказалась битой» — диагноз, который почти всегда вешают на что угодно, кроме самой СУБД. Диск, сетевая файловая система, процесс, убитый по SIGKILL, копирование живого файла обычным cp. Список подозреваемых длинный, и любой из них правдоподобнее, чем баг в SQLite — библиотеке с одним из самых плотных наборов тестов в открытом ПО.

12 августа команда Tailscale опубликовала разбор, из которого следует: иногда виновата всё-таки СУБД. Они полгода ловили повреждения, дошли до платной поддержки самих разработчиков SQLite — и вместе нашли гонку в механизме checkpoint, прожившую в коде не меньше шестнадцати лет.

История интересна не только результатом, но и методом поиска: воспроизвести баг было невозможно, и его пришлось ловить инструментированием. Ниже — хронология и то, что из неё следует для сервера, где на SQLite работает половина сервисов.

Как это выглядело: 19 повреждений за полгода

Симптом был тупой и неприятный: база время от времени оказывалась повреждённой. Не «медленно работает», не «блокировки» — именно порча файла. За шесть месяцев — 19 инцидентов.

Такое обычно списывают на что угодно, кроме самой СУБД: сбойный диск, сетевую файловую систему, убитый по SIGKILL процесс, кривой бэкап поверх живой базы. Список подозреваемых длинный, и каждый из них правдоподобнее, чем «баг в SQLite» — библиотеке с одним из самых плотных наборов тестов в индустрии.

ℹ️ Справка

Что такое WAL и checkpoint в двух абзацах. В режиме журнала предзаписи транзакция не пишет страницы сразу в файл базы. Она дописывает их в отдельный WAL-файл — это быстро и не блокирует читателей.

Рано или поздно накопленное надо перенести в основной файл. Эта операция называется checkpoint. Официальная документация описывает её штатный режим так: «By default, SQLite will automatically checkpoint whenever a COMMIT occurs that causes the WAL file to be 1000 pages or more in size, or when the last database connection on a database file closes».

Когда checkpoint закончился и WAL никто не читает, происходит то самое «сбрасывание в начало»: «the writer will rewind the WAL back to the beginning and start putting new transactions at the beginning of the WAL». Именно этот момент — reset WAL — и оказался местом преступления.

Как искали: шим поверх виртуальной файловой системы

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

SQLite обращается к диску не напрямую, а через слой VFS — абстракцию с подменяемой реализацией. Разработчики SQLite написали шим tmstmpvfs, который встаёт между библиотекой и настоящей файловой системой и протоколирует все дисковые операции с временными метками. Дальше оставалось дождаться очередного инцидента и разобрать журнал.

📌 Заметка

Это, пожалуй, главный переносимый урок истории. Когда баг не воспроизводится, но повторяется, — не пытайтесь поймать его отладчиком. Поставьте инструментирование в самое узкое место интерфейса (у SQLite это VFS) и ждите. Дешевле по усилиям и почти всегда результативнее.

Что нашли: checkpoint, который поверил в несделанную работу

Что делает checkpoint и где возникает гонка
Схема на основе разбора Tailscale и документации sqlite.org/wal.html

Механика в изложении Tailscale: если запись приходит в конкретный момент во время checkpoint, «the checkpointing process gets confused — it thinks some of the pages have been copied from the WAL into the main database file, but they haven't».

Дальше — цепочка, которая и делает баг разрушительным: «Those pages never get written to the database file, and that data is permanently lost. The database file becomes corrupt, because other pages which reference those pages — such as an index — are written to the database».

То есть теряются не просто данные. Теряются страницы, на которые ссылаются другие, успешно записанные страницы. Индекс указывает в пустоту — и файл становится структурно битым, а не просто неполным.

Возраст бага разработчики SQLite оценили минимум в 16 лет. Условия срабатывания — редкие: ручное управление checkpoint в сочетании с агрессивной частотой их запуска. Настолько редкие, что для тестирования пришлось добавлять код, вызывающий гонку намеренно.

Что исправили и когда

Исправление — дополнительная проверка в функции checkpoint, которая замечает, что WAL был сброшен другим потоком. Коммит 7168988acbec2d8d.

Дальше — деталь, из-за которой многие пропустили новость. Смотрим официальный changelog SQLite:

  • 3.51.3, 13 марта 2026 — «Fix the WAL-reset database corruption bug».
  • 3.52.0, 6 марта 2026 — релиз отозван; все запланированные в нём возможности перенесены в 3.53.0.

То есть заплатка вышла ещё в марте, тихо, в патч-релизе ветки 3.51 — а публичный разбор появился только в августе. Пять месяцев баг был исправлен, но почти никто не знал, что именно исправлено.

❗ Главное

Практический вывод не «срочно обновляйтесь», а «проверьте, какая у вас версия». Ключевая цифра — 3.51.3. Всё, что старше, содержит баг.

Сложность в том, что «у вас» — понятие расплывчатое. Системный sqlite3 и SQLite внутри приложения — это, как правило, разные библиотеки: многие программы на Go, Rust и Python несут собственную сборку. Обновление пакета в дистрибутиве не чинит приложение со вкомпилированной старой версией.

Как посмотреть:

# системная библиотека
sqlite3 --version

# версия, которую реально видит конкретная база
sqlite3 /path/to/app.db "SELECT sqlite_version();"

# проверка целостности (на копии, не на живой базе)
sqlite3 /path/to/copy.db "PRAGMA integrity_check;"

Насколько это касается домашнего сервера

Честный ответ: скорее всего, слабо — и вот почему.

Условие срабатывания, названное в разборе, — ручное управление checkpoint плюс высокая частота их запуска. Подавляющее большинство самохостящихся приложений на SQLite ничего такого не делают: они полагаются на автоматический checkpoint по порогу в 1000 страниц. Tailscale же управляли checkpoint явно и запускали их часто — конфигурация нечастая.

⚠️ Осторожно

Из этого не следует, что можно расслабиться. Из этого следует, что если вы видели повреждения базы у себя и списали их на диск — стоит перепроверить версию SQLite в конкретном приложении, прежде чем менять SSD.

И отдельно: копирование живой базы обычным cp — по-прежнему способ получить повреждённый файл собственными руками, независимо от всяких гонок. Штатные варианты — sqlite3 db ".backup out.db" или VACUUM INTO 'out.db'; оба умеют работать с базой, которую в этот момент пишут.

✅ Как правильно

Что имеет смысл сделать за пятнадцать минут:

  1. Собрать список сервисов, которые у вас на SQLite. Обычно это больше, чем кажется: медиасервер, читалка RSS, менеджер паролей, панель мониторинга, история умного дома.
  2. Для каждого — выяснить версию SQLite: SELECT sqlite_version(); через штатную консоль приложения или на копии файла.
  3. Настроить снятие копий штатным способом (.backup / VACUUM INTO), а не cp по расписанию.
  4. Раз в месяц гонять PRAGMA integrity_check; на копии. Это единственный способ узнать о порче раньше, чем приложение упрётся в неё само.

История полезна ещё и как напоминание о масштабе: код, который прогоняют через один из самых плотных наборов тестов в открытом ПО, шестнадцать лет носил в себе гонку, приводящую к потере данных. Не потому что кто-то плохо работал, а потому что окно срабатывания измеряется микросекундами и требует конкретного паттерна использования. «Проверено временем» и «проверено во всех режимах» — не одно и то же.


Источники

💬 Вопрос к сообществу

Вопрос к тем, кто ловил у себя «битую базу» и не докопался до причины: чем в итоге кончилось — заменой диска, переустановкой сервиса, восстановлением из копии? И кто-нибудь вообще проверяет integrity_check регулярно, или все узнают о порче в тот момент, когда приложение перестаёт запускаться?

Featured Replies

No posts to show

Присоединяйтесь к обсуждению

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

Гость
К сожалению, ваш контент содержит запрещённые слова. Пожалуйста, отредактируйте контент, чтобы удалить выделенные ниже слова.
Ответить в этой теме...

Последние посетители 0

  • Ни одного зарегистрированного пользователя не просматривает данную страницу
Яндекс.Метрика

Account

Navigation

Поиск

Поиск

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.