Когда к нам пришла команда AdPulse, у них была амбициозная идея: создать DSP-платформу (Demand-Side Platform) нового поколения для programmatic-рекламы. Платформа, которая позволит рекламодателям закупать трафик через RTB-аукционы с прозрачной аналитикой в реальном времени. Не очередной интерфейс над Google Ads, а полноценная инфраструктура для работы с рекламными биржами напрямую.
Основная сложность: RTB (Real-Time Bidding) — это мир, где каждая миллисекунда на счету. Биддер должен принять решение об участии в аукционе, рассчитать ставку и ответить за менее чем 20 миллисекунд. При потоке в миллионы запросов в секунду. Это не типичный стартап с формой регистрации и дашбордом — это инженерный вызов, который требует нестандартных решений.
Вызов: что такое RTB и почему это сложно
RTB — протокол аукциона в реальном времени. Когда пользователь загружает веб-страницу с рекламным слотом, рекламная биржа (SSP) отправляет запрос на аукцион всем подключённым DSP-платформам. У каждой DSP есть 50-100 миллисекунд на ответ — из которых 30-80ms занимает сеть. На обработку остаётся 10-20ms.
За эти 20 миллисекунд биддер должен:
- Распарсить OpenRTB-запрос (данные о пользователе, сайте, слоте)
- Проверить frequency capping (не показывали ли рекламу этому пользователю слишком часто)
- Применить таргетинг (география, устройство, интересы)
- Рассчитать оптимальную ставку на основе бюджета и исторических данных
- Сформировать и отправить ответ
Всё это — при нагрузке 2,000,000+ запросов в секунду. Если биддер не успевает ответить за отведённое время — ставка не учитывается, и деньги рекламодателя не расходуются эффективно.
В RTB нет «медленного, но стабильного». Либо ты быстрый, либо тебя нет в аукционе.
Исследование и проектирование: недели 1-2
Первые две недели мы полностью посвятили исследованию домена RTB и проектированию архитектуры. Мы изучили спецификации OpenRTB 2.6, проанализировали архитектуры существующих DSP (AppNexus, The Trade Desk, MediaMath) и определили ключевые требования:
- Latency биддера: p99 < 20ms
- Пропускная способность: 2M+ QPS (queries per second)
- Frequency capping: проверка за <1ms
- Аналитика: обновление дашборда каждые 5 секунд
- Бюджетный контроль: точность расхода бюджета до 0.1%
Эти требования определили выбор стека. Ни один из традиционных стартап-стеков (Node.js + PostgreSQL, Python + Django) не способен обеспечить такую производительность без серьёзного overengineering.
Архитектура: Go + Kafka + ClickHouse + Redis
Мы разделили систему на четыре основных компонента:
1. Биддер (Go)
Ядро системы. Написан на Go для минимальной latency. Каждый инстанс обрабатывает 50-100K QPS. Горизонтальное масштабирование: добавляем инстансы по мере роста трафика.
Ключевые оптимизации:
- Пулинг объектов:
sync.Poolдля переиспользования буферов и структур, чтобы снизить нагрузку на GC - Zero-copy parsing: кастомный парсер OpenRTB без лишних аллокаций
- Connection pooling: предварительно установленные соединения с Redis для frequency capping
- Batch processing: отправка событий в Kafka батчами по 1000 штук с интервалом 100ms
// Упрощённая структура обработки bid-запроса
func (s *Server) handleBidRequest(w http.ResponseWriter, r *http.Request) {
// Парсинг за ~1ms
req := s.pool.Get().(*BidRequest)
defer s.pool.Put(req)
req.Parse(r.Body)
// Frequency capping: Redis lookup за ~0.5ms
if s.freqCapper.IsLimited(req.UserID, req.CampaignID) {
w.WriteHeader(204) // No bid
return
}
// Таргетинг + расчёт ставки: ~2ms
bid := s.bidCalculator.Calculate(req)
if bid == nil {
w.WriteHeader(204)
return
}
// Ответ за ~0.5ms
resp := s.formatResponse(bid)
w.Header().Set("Content-Type", "application/json")
w.Write(resp)
// Асинхронная отправка события в Kafka
s.eventProducer.Send(bid.Event())
}
2. Потоковая обработка (Kafka)
Каждое событие (bid, win, impression, click) отправляется в Kafka. Это обеспечивает:
- Надёжность: события не теряются даже при сбое Consumer
- Масштабируемость: Kafka горизонтально масштабируется до миллионов сообщений в секунду
- Развязку: биддер не зависит от скорости записи в аналитику
Мы используем 3 топика: bids (все ставки), wins (выигранные аукционы), events (показы, клики, конверсии). Consumer-группы читают из этих топиков и записывают в ClickHouse.
3. Аналитика (ClickHouse)
ClickHouse — колоночная OLAP-база, оптимизированная для аналитических запросов. Мы храним в ней все события с детализацией до отдельного показа.
Структура таблицы событий:
CREATE TABLE events (
event_time DateTime,
event_type Enum8('bid'=1, 'win'=2, 'impression'=3, 'click'=4),
campaign_id UInt32,
creative_id UInt32,
user_id String,
geo_country LowCardinality(String),
device_type Enum8('desktop'=1, 'mobile'=2, 'tablet'=3),
bid_price Float32,
win_price Float32,
domain String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (campaign_id, event_time);
Запрос «покажи расход по кампаниям за последний час» выполняется за 50-200ms даже при 500M+ строк в таблице. На PostgreSQL это заняло бы минуты.
4. Frequency Capping (Redis)
Для frequency capping мы используем Redis с TTL-ключами. Схема простая, но эффективная:
// Ключ: fc:{user_id}:{campaign_id}
// Значение: количество показов
// TTL: 24 часа (или пользовательский период)
INCR fc:user123:campaign456
EXPIRE fc:user123:campaign456 86400
// При каждом запросе биддер проверяет:
GET fc:user123:campaign456
// Если > лимита → пропускаем аукцион
Redis-кластер из 3 нод обрабатывает 1M+ операций в секунду с latency <1ms. Этого достаточно для всех проверок частотности.
Дашборд: React + real-time
Фронтенд дашборда написан на React с Next.js. Ключевая особенность — real-time обновление метрик через Server-Sent Events (SSE). Бэкенд дашборда (Node.js) каждые 5 секунд делает запрос к ClickHouse и отправляет обновлённые данные на клиент.
Основные виджеты:
- Spend в реальном времени: сколько потрачено за сегодня / неделю / месяц
- Win Rate: процент выигранных аукционов
- CPM / CPC / CPA: стоимость за 1000 показов, клик, конверсию
- Geography heatmap: распределение показов по странам
- Frequency distribution: сколько раз пользователи видели рекламу
- Bid landscape: распределение ставок и win-цен
Результаты
За 8 недель мы создали рабочую DSP-платформу со следующими показателями:
| Метрика | Цель | Результат |
|---|---|---|
| Latency биддера (p99) | <20ms | 12ms |
| Пропускная способность | 2M QPS | 2.4M QPS |
| Frequency capping latency | <1ms | 0.3ms |
| Дашборд latency | <200ms | 80ms |
| Бюджетная точность | 99.9% | 99.95% |
| Время разработки | 10 недель | 8 недель |
Уроки, которые мы извлекли
1. Профилирование — с первого дня
Мы использовали pprof (встроенный профайлер Go) с самого начала. Это позволило обнаружить и устранить узкие места до того, как они стали проблемой. Главная находка: парсинг JSON через encoding/json тратил 40% CPU. Переход на easyjson снизил это до 8%.
2. Батчинг решает
Индивидуальная отправка событий в Kafka давала 5K msg/s. Переход на батч по 1000 сообщений увеличил пропускную способность до 500K msg/s. Тот же принцип — батчить записи в ClickHouse.
3. Не оптимизируйте рано, но проектируйте для оптимизации
Мы начали с простого монолитного биддера. Когда нагрузка выросла, мы без проблем выделили frequency capping в отдельный сервис и добавили инстансы биддера — потому что архитектура это позволяла.
4. Мониторинг — это не опция
Prometheus + Grafana с первого дня. Дашборды для latency, throughput, error rate, memory usage. Алерт при p99 > 15ms — это ранний сигнал, что система перегружена. Без мониторинга мы бы узнали о проблеме только от клиентов.
5. Load testing — до продакшна
Мы написали кастомный load generator на Go, который имитирует реальный RTB-трафик: разные размеры запросов, географии, устройства. Это позволило найти проблемы, которые не проявлялись при синтетических нагрузках.
Инфраструктура и деплой
Платформа развёрнута на dedicated-серверах (Hetzner) — для RTB облачные провайдеры слишком дороги из-за объёмов трафика. Конфигурация:
- 3 сервера биддера: 32 cores, 64GB RAM каждый
- 3 ноды Kafka: 16 cores, 32GB RAM, NVMe SSD
- 2 ноды ClickHouse: 32 cores, 128GB RAM, NVMe SSD
- 3 ноды Redis: 8 cores, 32GB RAM
- 1 сервер дашборда: Next.js + Node.js API
CI/CD через GitHub Actions: push в main → тесты → сборка Docker-образа → деплой на staging → smoke tests → деплой в production с blue-green переключением.
Подробнее о проекте — на странице кейса AdPulse. Если вам нужна подобная система — мы в Complex Solutions специализируемся именно на таких проектах.