Как пересчитывать и хранить агрегации для текущих периодов (час, день) для актуальных данных
Что проверяют
Умеете ли вы отделить закрытые периоды, которые считаются один раз и больше не меняются, от текущего часа или дня, который растёт прямо сейчас. Следом обычно спрашивают, что делать с опоздавшими событиями и как не посчитать одно событие дважды.
Как отвечать
Закрытые часы и дни храните готовыми строками в таблице агрегатов. Текущий период либо досчитывается инкрементально, либо собирается на чтении из сырых данных за последний час.
- бакет: ключ агрегата это начало периода плюс измерения, например
(hour, user_id). Час в Go получают черезt.Truncate(time.Hour), но для дня так нельзя:Truncateсчитает от нулевого времени и игнорирует часовой пояс, начало дня по Москве собирают черезtime.Date(y, m, d, 0, 0, 0, 0, loc); - буфер в памяти: воркер копит счётчики в
mapпод мьютексом (или шардирует по ключу) и раз в несколько секунд сбрасывает пачкой; - upsert: в PostgreSQL
INSERT ... ON CONFLICT (bucket, key) DO UPDATE SET cnt = agg.cnt + EXCLUDED.cnt, так параллельные воркеры не перетирают друг друга; - готовые механизмы: continuous aggregates в TimescaleDB,
SummingMergeTreeилиAggregatingMergeTreeс materialized view в ClickHouse,HINCRBYс TTL в Redis для горячего окна; - роллап: день собирается из 24 часовых строк, а не заново из сырых событий.
Отдельно скажите про неаддитивные метрики. Суммы и количества складываются, среднее храните парой sum и count. Уникальных пользователей и перцентили сложением не получить, для них HyperLogLog, t-digest или пересчёт из сырых данных.
Где спотыкаются
Пересчитывают весь день из сырых событий на каждый запрос. На малом объёме это работает, на миллионах строк превращается в полный скан каждые несколько секунд.
Забывают про опоздавшие события и повторную доставку из Kafka. Нужен водяной знак (сколько ждать перед закрытием периода) и идемпотентность, например сохранение обработанного offset в той же транзакции, что и счётчик.
Считают день через Truncate(24 * time.Hour) и получают сдвиг на три часа для московских пользователей.