Каждые 10 минут в русскоязычном эфире Twitch и VK Видео Live в среднем 7 200 каналов. Писать их как есть - больше миллиона строк в сутки. Я храню агрегаты, и за четыре недели вышло 2 млн строк. Расскажу, как это устроено и где я три недели искал проблему не там.

Я Максим, разработчик, вечерами пишу LanolStream - инструменты для стримеров, которые мы делаем вместе со стримершей lanolkid. Стримеры часто спрашивают «во сколько лучше стримить» и «много ли у меня зрителей для моей игры». Чтобы отвечать цифрами, а не ощущениями, нужен весь рынок, а не только наши каналы.

Откуда данные

Twitch: Helix GET /streams?language=ru по 100 эфиров на страницу, с курсором. В вечерний пик в эфире ~12 700 русскоязычных каналов, это ~130 страниц. На всякий случай стоит предел в 150 страниц: если упёрлись, проход помечается capped.

VK Видео Live: каталог онлайн-каналов через API приложения, по 200 на страницу (мы там зарегистрированы как приложение для входа и чат-бота).

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

export function aggregatePass(streams: readonly MarketStream[]): PassRows {
  const byChannel = new Map<string, ChannelSample>();
  for (const s of streams) {
    if (!s.channelId) continue;
    const viewers = Math.max(0, Math.round(Number(s.viewers) || 0));
    const prev = byChannel.get(s.channelId);
    if (prev && prev.viewers >= viewers) continue;
    byChannel.set(s.channelId, { channelId: s.channelId, login: s.login, category: s.category.trim(), viewers });
  }
  // дальше раскладываем каналы по корзинам «размер × категория»
}

Почему не храним сырые замеры

Каждые 10 минут - в среднем ~7 200 эфиров, то есть больше миллиона строк в сутки, а за полгода под 190 миллионов. Для вопросов «когда стримить» и «сколько у похожих каналов» столько не нужно. Храним два агрегата:

? market_slots: день × час × площадка × категория × размер канала. Размер - корзина онлайна (0-4, 5-14, 15-49, 50-149, 150-499, 500+). Внутри - сумма каналов и зрителей по проходам. ? market_channels: неделя × канал. Внутри - число замеров (1 замер = 10 минут эфира), сумма онлайна и пик.

Средние считаются делением на число проходов из market_passes, поэтому пропущенный проход не портит цифры. Слоты живут 42 дня, каналы - 8 недель, чистка раз в сутки в 4 утра. За четыре недели: 1,38 млн строк слотов (460 МБ) и 641 тысяча строк «канал × неделя» (185 МБ).

Два прохода не должны идти вместе

Cron дёргает POST /api/cron/market раз в 10 минут. Если проход затянулся, второй не должен начать писать те же суммы. Вместо локов - аренда в одной строке Postgres:

update market_poll set lease_id = $1, lease_until = now() + interval '9 minutes'
 where id = 1 and (lease_until is null or lease_until < now())
returning id;

Не вернулась строка - кто-то уже работает, выходим. Упал процесс посреди прохода - аренда истечёт сама через 9 минут.

Вставка - пачками по 1000 строк через unnest и on conflict … do update: каждый проход прибавляется к строке своего часа, а у канала за неделю растёт число замеров и обновляется пик.

Грабли

? Категория. Сначала в market_channels при каждом замере писалось category = excluded.category, то есть у канала за неделю оставалась последняя игра. Стримил шесть часов Dota 2 и полчаса Just Chatting под конец - и он «стример Just Chatting». Теперь рядом копится счётчик замеров по каждой категории в jsonb, прямо в том же on conflict:

categories = case when excluded.category = '' then market_channels.categories
  else market_channels.categories || jsonb_build_object(excluded.category,
    coalesce((market_channels.categories ->> excluded.category)::int, 0) + 1) end

Главная игра недели - ключ с наибольшим значением. У старых недель счётчика нет, там честно остаётся «последняя игра».

? VK днём. В 15:00 сборщик видел ~140 каналов VK при 100+ зрителях на канал, а в 22:00 - ~1 500 при 18. У Twitch каналов между 15:00 и 22:00 становится в 1,6 раза больше, у VK - в 11. Три недели я думал, что днём каталог VK отдаёт не всех, и даже написал это в черновике статьи ?

Оказалось, грабли мои. VK выкидывает из страницы скрытые эфиры, поэтому страница посреди выдачи бывает короче 200, а я считал короткую страницу последней. Днём такая попадалась уже на первой, и проход на ней заканчивался. Теперь следующее смещение беру из extra.offset, конец - только is_last или пустая страница, а в журнал на каждый проход пишу размеры страниц, чтобы такое было видно сразу. Данные VK до 19 октября днём недосчитаны, поэтому VK по часам считаю только с этой даты.

После починки в 15:00 сборщик видит ~1 300 каналов VK, в 22:00 ~2 100. В 1,6 раза, ровно как у Twitch. Так что теперь VK по часам тоже считаю, а за неделю каналов VK вышло 12,3 тысячи вместо прежних 9 с небольшим.

Twitch и VK Видео Live по неделям
Twitch и VK Видео Live по неделям

Что получилось увидеть

Доли русскоязычных каналов Twitch по среднему онлайну
Доли русскоязычных каналов Twitch по среднему онлайну

? у 80 % русскоязычных каналов Twitch в среднем меньше пяти зрителей, а топ-1 % собирает 73 % часов просмотра; ? «днём конкуренция меньше» - миф: в 6 утра на канал в эфире ~32 зрителя, в 20 часов ~40; ? на VK Видео Live пик зрителей на канал не вечером, а в 17-18 часов: ~18 против ~11 в 21 час; ? сверка с TwitchTracker сошлась: у них в начале октября ~242,5 тысячи зрителей и ~7 тысяч каналов одновременно, у нас за 5-11 октября 242 тысячи и 6,2 тысячи.

Зрителей на один канал в эфире по часам, Twitch
Зрителей на один канал в эфире по часам, Twitch

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

Страница на каждый канал с 3+ неделями замеров: lanolstream.ru/c/ник. На ней ничего, чего не видно на самой площадке, а свою страницу можно скрыть, войдя своим каналом.

Что бы вы сделали иначе? Особенно интересно, как вы храните временные ряды такого размера без TimescaleDB.

PS: Всё это крутится на одном VPS в Москве рядом с основным приложением ?

Комментарии (0)