InquirerMMDA: Light to moderate traffic Friday morning ahead of Undas breakCNN TürkTestlerde Ne Kadar Başarılı?PunchI remain dominant APC figure in Imo — OkorochaInquirer Entertainment‘Star Trek,’ ‘X-Men’ star Patrick Stewart dead at 86The Jerusalem PostBus driver killed, dozens wounded in collision with tractor near Netivot한겨레정부, ‘국민 배우’ 고 최불암에 최고 등급 금관문화훈장 추서ZDF heuteAktuelle Pressemitteilungen des ZDFCapital FMOver 300 households displaced by floods in Tana RiverComplete SportsNeville Rips Glazer Family, INEOS Over 14 Years Of Failure At Man UnitedThe Sydney Morning HeraldInside the cyber bootcamp helping stop the radicalisation of boysWirtualna PolskaKto będzie prezesem CPK? Trump grzmi przed wyborami. Tragedia w Kapadocji [SKRÓT PORANKA]The South AfricanWhy Kaizer Chiefs will beat Pirates in Saturday’s Soweto Derby
The Daily Newsstand · Free, Always
Friday, October 30, 2026

Сколько миллисекунд без читателей нужно SQLite, чтобы файл -wal перестал расти

Translate

В режиме WAL база SQLite пишет изменения сначала в файл -wal, а потом переносит их в основной файл и начинает -wal с начала. Документация предупреждает: если читатели перекрываются и хотя бы один активен всегда, -wal растёт без предела, и нужны промежутки без читателей. Какой длины, там не сказано. В обсуждениях это лечат sleep() на 16 мс и больше. Я замерил в контейнере на SQLite 3.46.1. На моём стенде хватало общей паузы в единицы миллисекунд: 2 мс при 50 коммитах/с, 5 мс при 200 и 1000. Неожиданно для меня при быстрой записи паузы понадобилось больше, чем при медленной. Без неё три читателя по 50 мс, сменяющие друг друга, раздули -wal так же, как один на 20 секунд: 19,7 МиБ, и ни разу SQLite не начал файл заново. Случайные запросы по 5 мс почти не мешали, пока хотя бы 13% времени не было ни одного читателя. Ниже замеры, догадка о том, откуда берутся эти миллисекунды, и признак, по которому проблему видно раньше, чем кончится диск.

Что уже известно

Документация SQLite описывает механизм в разделе про слишком большие WAL-файлы. Если читателей много, их транзакции перекрываются и хотя бы один читатель активен всегда, checkpoint не завершится и WAL «будет расти без предела». Лечение там названо так: нужны «reader gaps», промежутки без читателей, или ручной checkpoint в режиме RESTART/TRUNCATE. Сколько должен длиться промежуток, там не сказано.

Почти тот же эксперимент этой осенью опубликовал автор поста «Why your SQLite WAL file never shrinks» на dev.to. На macOS с писателем 2000 коммитов/с три читателя по секунде со сдвигом дали рост 11,9 МиБ/с. TRUNCATE раз в 5 с в двух прогонах из трёх удержал файл до 60 МиБ ценой остановки писателя до 1,8 с. Его вывод для практики: держать чтения короткими и следить за разницей log − checkpointed. Промежуток без читателей он сравнивал в двух положениях: «около 20% времени» и «никогда». Его самые короткие чтения длились 0,4 с.

На форуме SQLite (2020) WAL в 2 ГБ при базе в 6 МБ перестал расти после «occasional sleep» в потоках, на Hacker News (2024) спасались sleep() от 16 мс на всех запросах. Нужную паузу там не измеряли.

Стенд

Все проверки идут в одноразовом контейнере python:3.15-slim с ограничением в 2 CPU: Python 3.15.0, SQLite 3.46.1, страница 4096 байт, автоматический checkpoint по умолчанию (1000 страниц), synchronous=NORMAL, если не сказано иное. В пробе (первая таблица) писатель вставляет в таблицу на 2000 строк по одной строке с 500 байтами данных за коммит. Во всех остальных проверках таблица на 20 000 строк, а писатель с заданным темпом делает BEGIN IMMEDIATE и пять UPDATE случайных строк, это около пяти кадров WAL на коммит. Возврат -wal к началу (дальше «перемотка») я считаю по полю checkpoint sequence в заголовке файла, байты 12–16: SQLite увеличивает его при каждой перемотке.

Без паузы 50 мс и 20 секунд дают одно и то же

Писатель 200 коммитов/с, 20 секунд на сценарий, два повтора с одинаковым итогом.

читатели

чтений

перемоток

-wal в конце

нет

0

5

3,9 МиБ

один на все 20 с

1

0

19,7 МиБ

три по 1 с со сдвигом, без паузы

60

0

19,7 МиБ

три по 50 мс со сдвигом, без паузы

1169

0

19,7 МиБ

три по 1 с вместе, пауза 0,2 с

51

4

4,7 МиБ

Во всех трёх сценариях без паузы файл рос на 2 МиБ каждые 2 секунды, длина отдельного чтения ничего не меняла. Документация подтвердилась буквально.

Сколько нужно паузы

Следующий стенд: два читателя по 100 мс со сдвигом 50 мс, то есть 150 мс база занята, затем пауза G, когда не читает никто. Паузу я мерил по факту, медиана отличалась от заданной не больше чем на 0,1 мс. 12 секунд на точку. Раз в три секунды стороннее соединение снимало счётчики через PRAGMA wal_checkpoint(PASSIVE), а это тоже checkpoint.

темп записи

без читателей

G = 0

1 мс

2 мс

5 мс

10–50 мс

50/с

3; 3,9

0; 11,8

0; 11,8

2; 5,2

2; 4,5

2–3; 3,9–4,1

200/с

11; 3,9

0; 47,2

0; 47,2

0; 47,2

11; 4,5

10–11; 3,9–4,7

1000/с

59; 4,0

0; 235,9

0; 235,9

0; 235,9

33; 12,2

35–59; 6,3–7,2

В ячейке: перемоток за 12 с; наибольший -wal, МиБ.

Из проверенных значений срабатывали уже паузы в единицы миллисекунд, это не «20% времени». Шаг сетки грубый, и каждая точка — один прогон: в следующей серии при 200 коммитах/с хватило и 3 мс. Так что цифры в таблице — исходы отдельных прогонов на моём стенде; точную границу они не задают. Сдвигается она в неожиданную сторону. Я ждал, что пауза должна быть не короче интервала между коммитами, тогда при 1000 коммитах в секунду хватило бы миллисекунды. Вышло наоборот: при 50 коммитах/с хватило 2 мс, при 1000/с 2 мс уже не хватило.

Откуда могут браться эти миллисекунды

Чтобы понять, от чего зависит нужная пауза, я заставил писателя после каждого коммита записывать время начала, конца и номер перемотки из заголовка -wal, и сопоставил это с паузами. Из 266 перемоток 265 случились сразу после коммита, который целиком, вместе со своим checkpoint, поместился в паузу. Коммиты, которые начались в паузе, но закончились уже при новом читателе, я только подсчитал, с перемотками их не сопоставлял. Единственное исключение из 266 я не разобрал.

Похоже, в паузу должен влезть коммит с догоняющим checkpoint. Фазы checkpoint изнутри SQLite я не замерял, это вывод из совпадения по времени, зато он объясняет, почему быстрой записи понадобилась пауза шире. Длительность такого коммита зависит от fsync:

режим

200 коммитов/с

1000 коммитов/с

synchronous=NORMAL

1–2 мс: 0 перемоток, 31 МиБ; 3 мс: 5 перемоток, 6,8 МиБ

1–3 мс: 0, 157 МиБ; 4 мс: 24 перемотки, 10 МиБ

synchronous=OFF

1 мс: 5 перемоток, 6,7 МиБ

1 мс: 0; 2 мс: 26 перемоток, 6,9 МиБ

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

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

А если читатели случайные, как веб-запросы

Расписанное перекрытие бывает у фоновых заданий и опросов со сдвигом, а в обычном сервисе запросы приходят случайно. Я запускал читателей с пуассоновскими приходами (BEGIN; SELECT; работа d мс; COMMIT) при писателе 200 коммитов/с и мерил по среднему числу одновременных читателей L = частота × длительность. Без читателей при этом темпе -wal держится около 4 МиБ (первая строка таблицы выше).

L

доля времени без читателей

длина чтения

перемоток

наибольший -wal

0,5

60%

5 мс

13

4,0–4,2 МиБ

1

32–40%

20 / 5 / 2 / 1 / 0,5 мс

13

4,0–4,3 МиБ

2

13%

5 мс

12–13

4,8 МиБ

3

3,4–5,3%

10 / 5 / 2 / 1 мс

10–12

4,8–9,5 МиБ

4

1,2–2,5%

10 / 5 / 2 мс

5–9

7,1–17,3 МиБ

Два повтора по 14 секунд, в строке разброс по повторам и длинам чтения. Перемотки продолжались во всех сериях, тогда как при расписанном перекрытии без паузы их не было ни одной. При L = 1 картина не отличается от работы без читателей, даже при 2000 чтений по 0,5 мс в секунду, когда пауз длиннее 4 мс остаётся меньше одной в секунду. При L = 4 перемоток остаётся 5–9 вместо 13, а файл вырастает до 7–17 МиБ. Остановился бы рост на долгом прогоне, по этим данным не сказать: я сохранял только наибольший и конечный размер.

Короткие частые чтения при той же нагрузке не хуже длинных, скорее лучше: при L = 3 чтения по 1–2 мс дали 4,8–5,7 МиБ, по 5 мс — 6,5–9,5. Это согласуется с объяснением выше: короткий читатель оставляет маленький догон, и checkpoint, видимо, успевает за более короткую паузу. Длительность checkpoint в этой серии я не мерил.

Как заметить, пока не кончился диск

Совет из поста на dev.to, следить за log − checkpointed, у меня starvation не показал. При паузе 0 и 200 коммитах/с сторонний PRAGMA wal_checkpoint(PASSIVE) раз в три секунды отвечал так:

(0, 3008, 2948)  (0, 6007, 5942)  (0, 9005, 8936)  (0, 12005, 11931)

Разница — 60–75 кадров, файл при этом 47 МиБ. Перенос почти успевает, не получается только перемотка. У автора поста разница доходила до 2751 кадра, потому что его читатели держали снимок по секунде. С короткими читателями она маленькая.

Работает другой признак: сам log. При паузе 50 мс тот же запрос возвращал 994, 988, 987, то есть log держался чуть ниже порога автоматического checkpoint в 1000 кадров. При starvation он растёт без остановки. Второй признак ещё проще: checkpoint sequence в заголовке -wal (четыре байта со смещения 12, big-endian) перестаёт меняться.

Что из этого следует

Если -wal растёт, посмотрите на расписание чтений. Пул воркеров, которые опрашивают базу со сдвигом, и задания по таймеру друг за другом без зазора не оставляют ни одного промежутка без читателей. Пока чтения так и перекрываются, укорачивание каждого из них не поможет; помогает оно, только если между ними появляются общие паузы. На моём стенде хватало паузы в 5 мс при темпе до 1000 коммитов/с и synchronous=NORMAL. Если паузу устроить нельзя, остаётся периодический wal_checkpoint(TRUNCATE) с заданным busy_timeout: без него вызов при активном читателе сразу вернёт busy. Цену этого для писателя измерил автор поста на dev.to, и у него один вызов из нескольких всё равно вернулся busy.

Со случайными запросами перемотки шли при любой проверенной нагрузке. Чтения по сотне миллисекунд и дольше в этой серии я не проверял, там картина может быть хуже.

Границы проверки: один хост Linux, контейнер с 2 CPU, SQLite 3.46.1 из Debian-образа, Python-потоки (GIL добавляет свои задержки), один писатель с маленькими транзакциями. На другом диске и при больших коммитах нужные паузы будут другими: они зависят от времени коммита с checkpoint. Что длинная пауза нужна на перенос накопившихся за чтение кадров, я вывел из совпадения цифр; объём и время этого переноса отдельно не измерял.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.