- находки 48 и 49: единицы не менялись ни разу; настоящих столкновений 0.65%, несравнимых наборов полей нет, тай-брейк берёт меньшее в 96% случаев - edinicy-metriki-v-razreze закрыт: гипотеза не подтвердилась замером - тай-брейк отложен до каталога рода агрегации
6.0 KiB
Разнести ответ приёма и свёртку доставки
Приоритет: высокий
Была блокером, вынутым ревью кода задачи razbor-metrik-v-obekty (профиль
deep, находка №4 триажа, severity major). Решение принято — ниже задача.
Что не так сегодня
Свёртка выполняется синхронно внутри обработчика запроса, поэтому время ответа равно времени свёртки.
WriteTimeout в Go ставится в readRequest — до чтения тела и до вызова
обработчика (net/http/server.go:993-997, прочитано в исходниках). Значит
30 секунд по умолчанию это бюджет на всё сразу: дочитать до 64 МиБ по
мобильной сети, сделать fsync архива, вставить доставку и свернуть.
Воспроизведено минимальной программой: сервер с WriteTimeout=200ms,
обработчик спит 500 мс.
handler: WriteHeader(200), body Write err=<nil>
client: elapsed=501ms err=EOF
Сервер считает, что отдал 200 — ошибки записи не видно, ответ ушёл в буфер и
сбрасывается позже. Клиент получил обрыв. Код обработчика этого не видит, а
accessLog честно запишет status_code=200: единственный сегодняшний канал
наблюдаемости в этом сценарии врёт.
Стоимость свёртки измерена до перехода на одну транзакцию на доставку:
| тело | объектов | свёртка |
|---|---|---|
| 80 КиБ | 1001 | 815 мс |
| 323 КиБ | 4001 | 3.07 с |
| 1302 КиБ | 16001 | 11.07 с |
Одна транзакция на доставку убрала около 0.7 мс на объект (прогон живого архива ускорился с 64 до 52 секунд), но порядок величины остался: широкая доставка по-прежнему измеряется секундами.
Бьёт это по широким проходам — Today, Previous 7 Days, ручной
экспорт, — то есть ровно по тем, ради которых заведён инвариант «дыры
закрываются сами».
Что решено
Вариант (а): отвечать 200 сразу после архивации и учёта; свёртка —
воркером в порядке журнала, с подбором pending при старте.
Почему он, а не альтернативы:
- Поднять
write_timeoutдо согласованного сfoldTimeout— дёшево, но худший случай (64 МиБ) всё равно минуты, и молчаниеaccessLogостаётся. Это лечит симптом. - Оставить как есть — широкие проходы продолжают рваться.
Вариант (а) решает причину и попутно снимает две смежные дыры: параллельные
доставки одной автоматизации перестают гонять наследование слоя (сейчас вторая
может не найти слоя первой и уйти в failed), и доставка, застрявшая в
pending из-за сбоя записи, наконец кем-то подбирается.
Что делать
- Воркер свёртки: одна горутина, очередь идентификаторов доставок, обработка
строго в порядке журнала (
received_at,id) — от этого зависит наследование слоя и воспроизводимость. - Приём отвечает
200после архивации и вставки доставки; свёртку ставит в очередь. Очередь переполнена — доставка остаётсяpending, это не отказ. - Подбор
pendingпри старте, тем же путём. Это половинаreindex, поэтому код должен быть общим с ним, а не соседним. - Остановка сервиса дожидается текущей доставки: свёртка — одна транзакция, рвать её нечем, но очередь надо дренировать осознанно.
- Метка «доставка ждала свёртки дольше N» — в наблюдаемость, чтобы отставание воркера было видно до того, как оно станет отставанием на сутки.
- Тесты: порядок журнала соблюдается при конкурентных доставках;
pendingподбирается при старте; отмена контекста не оставляет половинчатого состояния;task verify:archiveдаёт то же состояние.
Что стоит без решения
Ничего: свёртка работает, просто рискует не уложиться в таймаут на самых широких доставках. Данные при этом не теряются — тело ложится в архив до свёртки.
Связано
- reindex-iz-arhiva — подбор
pendingэто её половина; делать одним кодом. - stats-nablyudaemost — метка «ответ не уложился в таймаут» и отставание воркера должны попасть туда.