Files
healthlog/docs/backlog/cena-chitayushchego-marshruta.md
T

8.5 KiB
Raw Blame History

Цена первого читающего маршрута: память, WAL и повторный опрос

Приоритет: высокий

Решение принято 2026-08-02: вариант (г) плюс (в), именно в таком порядке. Разбирается без владельца: контракт хранения не меняется, данные не трогаются, а обе части — общепринятая практика, а не собственный дизайн. Чекпойнт по таймеру закрывает единственное проявление, которое ломает приём (диск), и стоит одной горутины; ETag по PRAGMA data_version — один запрос к базе, снимает и повтор, и большую часть читающих транзакций, не заводя кеша ответа.

Вариант (а) — предел и дедлайн маршрута — не отвергнут, а отложен до Read API точек: там предел размера ответа всё равно проектируется, и делать его дважды не нужно. Вариант (б) не берём, пока счётчик не заговорит. Вариант (д) — последним, если (в) окажется мало.

Задача берётся перед 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.