99 lines
8.5 KiB
Markdown
99 lines
8.5 KiB
Markdown
# Цена первого читающего маршрута: память, WAL и повторный опрос
|
||
|
||
**Приоритет:** высокий
|
||
|
||
**Решение принято 2026-08-02: вариант (г) плюс (в), именно в таком порядке.**
|
||
Разбирается без владельца: контракт хранения не меняется, данные не трогаются,
|
||
а обе части — общепринятая практика, а не собственный дизайн. Чекпойнт по
|
||
таймеру закрывает единственное проявление, которое ломает приём (диск), и стоит
|
||
одной горутины; `ETag` по `PRAGMA data_version` — один запрос к базе, снимает и
|
||
повтор, и большую часть читающих транзакций, не заводя кеша ответа.
|
||
|
||
Вариант (а) — предел и дедлайн маршрута — **не отвергнут, а отложен** до
|
||
[Read API точек](read-api-tochki.md): там предел размера ответа всё равно
|
||
проектируется, и делать его дважды не нужно. Вариант (б) не берём, пока
|
||
счётчик не заговорит. Вариант (д) — последним, если (в) окажется мало.
|
||
|
||
Задача берётся **перед** Read API: тот строится поверх этой машинерии.
|
||
Ниже — исходная постановка блокера, она же ТЗ.
|
||
|
||
## Что решить
|
||
|
||
Чем ограничить стоимость маршрута чтения, у которого нет ни предела ответа, ни
|
||
собственного дедлайна, ни условного запроса. Вопрос поднялся на каталоге
|
||
(`GET /api/v1/metrics`, change `2026-08-02-katalog-i-rod-agregacii`), но
|
||
принадлежит не ему: тот же ответ понадобится Read API точек и MCP, и решать его
|
||
трижды нельзя.
|
||
|
||
Три измеренных проявления одной причины.
|
||
|
||
**Память.** Снимок каталога держит разжатые точки окна по всем метрикам сразу,
|
||
хотя измерение идёт по одной метрике. Замер враждебного прохода ревью: 20 метрик
|
||
× 8 часов × 5000 точек — 693 мс и +153 МиБ живой кучи на один запрос.
|
||
Предварительный отбор по учётным колонкам (сделан) снял разжатие заведомо
|
||
непригодных часов, но множители «метрики × окно × точки × одновременные запросы»
|
||
остались без потолка. Приём живёт в том же процессе и уже даёт пик 768 МиБ на
|
||
теле 40 МиБ; OOM убивает приём, а доставка, не попавшая в архив, телефоном не
|
||
переприсылается.
|
||
|
||
**WAL.** Замер эксплуатационного прохода на копии с драйвером и PRAGMA проекта:
|
||
непрерывная запись плюс четыре читающих транзакции внахлёст дают рост `-wal`
|
||
около 7 МБ/с без верхней границы (40 МБ за пять секунд), тогда как тот же
|
||
писатель без читателей стабилизируется на 4 МБ. Пассивный чекпойнт SQLite не
|
||
продвигается дальше снимка самого старого активного читателя, и ошибки при этом
|
||
нет — виден только растущий файл. `PRAGMA wal_checkpoint` в проекте не
|
||
вызывается нигде.
|
||
|
||
**Повторный опрос.** Спека каталога требует побайтового совпадения двух ответов
|
||
на неизменившейся витрине — то есть ресурс по построению пригоден для условного
|
||
запроса, а `ETag`/`304` не выставляется. Потребителей трое (агент-медик, трекер,
|
||
игра), и самый частый их запрос — повтор неизменившегося.
|
||
|
||
## Варианты и цена
|
||
|
||
**а. Предел и дедлайн у маршрута.** Потолок числа метрик и точек в одном ответе,
|
||
собственный `context.WithTimeout`, честный отказ при превышении. Цена: клиент
|
||
обязан уметь читать частичный каталог, то есть появляется пагинация — контракт
|
||
чтения усложняется на первой же ручке.
|
||
|
||
**б. Измерение потоком по метрике внутри той же транзакции.** Точки метрики
|
||
освобождаются сразу после вердикта; требование «один снимок» не нарушается. Цена:
|
||
хранилище перестаёт возвращать снимок значением и начинает отдавать его
|
||
последовательно (итератор или колбэк) — то есть меняется форма границы
|
||
`store`/`catalog`, ради случая, которого живой поток пока не производит.
|
||
|
||
**в. Условный запрос: `ETag` по `PRAGMA data_version`.** Снимает и стоимость
|
||
повтора, и большую часть читающих транзакций разом: клиент с непротухшим `ETag`
|
||
получает `304`, и снимок не открывается вовсе. Цена: один лишний запрос к базе на
|
||
каждый вызов и обещание клиенту, что версия витрины меняется не чаще, чем данные.
|
||
|
||
**г. Периодический `wal_checkpoint(PASSIVE)` по таймеру рядом с воркером.**
|
||
Лечит только WAL, зато дёшево и без изменения контракта. Память и повтор
|
||
остаются.
|
||
|
||
**д. Кеш ответа на короткий TTL.** Закрывает всё сразу, но заводит третье
|
||
представление того же факта, и его инвалидация становится новым местом, где можно
|
||
ошибиться молча. Дизайн каталога отверг кеш именно поэтому.
|
||
|
||
## Что заблокировано
|
||
|
||
Ничего сегодня: на живом корпусе каталог собирается за 45 мс, потребителей у него
|
||
пока нет, а маршрут живёт в доверенной сети. Блокировано будущее — Read API
|
||
точек, где объёмы на порядок больше, и выкладка наружу, где опрос станет
|
||
непрерывным.
|
||
|
||
## Рекомендация
|
||
|
||
**г + в, именно в таком порядке.** Чекпойнт по таймеру закрывает единственное
|
||
проявление, которое ломает приём (диск), и стоит одной горутины без изменения
|
||
контракта. `ETag` по `data_version` — один запрос к базе, снимает и повтор, и
|
||
большую часть читающих транзакций, и делает это без кеша ответа.
|
||
|
||
Вариант «а» откладывать до Read API точек: там предел размера ответа всё равно
|
||
проектируется (`read-api-tochki.md`), и делать его дважды не нужно. Вариант «б»
|
||
не брать, пока счётчик не заговорит: он меняет форму границы ради случая,
|
||
которого поток не производит. Вариант «д» — последним, если «в» окажется мало.
|
||
|
||
Связано: `docs/architecture.md` → «Измерение рода агрегации», `read-api-tochki.md`,
|
||
`stats-nablyudaemost.md`.
|