Обзор проекта

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

Наш клиент решил закрыть этот разрыв платформой, отвечающей на один вопрос — человек это или машина? — пока визит ещё продолжается. Не ночной batch-процесс и не отчёт: вердикт, возвращаемый в тот же момент, когда загружается страница, чтобы системы самого клиента могли отреагировать немедленно.

Мы восемь месяцев строили этот продукт целиком — от браузерного сборщика сигналов через сервисы скоринга до дата-платформы под ними. Работа шла в течение 2022 года и завершилась в 2023-м.

Под NDA

Проект закрыт соглашением о неразглашении. Клиент, название продукта и пользовательский интерфейс опущены, а архитектура ниже описана на уровне формы, а не деталей реализации.

Задача

Инженерную работу определяли три ограничения, и каждое из них тянуло против остальных.

Всё должно было работать в реальном времени. Вердикт, приходящий после того, как посетитель уже сконвертировался — или уже выжег бюджет, — это журнал аудита, а не защита. Скоринг должен был укладываться внутрь запроса, в бюджет задержки, достаточно малый, чтобы клиент добровольно поставил его в критический путь загрузки собственной страницы.

Всё должно было выдерживать высокую нагрузку. Платформа работала под настоящим high-load: десятки тысяч запросов в минуту по всей клиентской базе, с трафиком, который взлетал без предупреждения — запуск кампании на стороне клиента и атака с другой стороны для планировщика мощностей выглядят одинаково. Пик никогда не был средним, и систему нужно было рассчитывать на пик, оставаясь при этом посильной по стоимости на среднем.

Всё должно было оставаться доказуемым. Каждый вердикт должен был объясняться и допускать повторный разбор постфактум. Клиенты не действуют на основании необъяснённой оценки, а ни логика детекции, ни модели за ней не улучшаются, если под рукой нет сырых сигналов, породивших вчерашние решения. Это означало хранить всё — задача ретеншена и аналитики, стоящая прямо за задачей про задержки.

Реальное время, большой объём и полное хранение — три требования, которым не удовлетворяет ни одна отдельная база данных. Бо́льшая часть архитектуры ниже следует из отказа поступиться хотя бы одним из них.

Сбор сигналов на чистом JavaScript

Клиентская часть платформы — сборщик, который работает внутри страницы клиента и собирает доказательства, на основе которых выносится вердикт: характеристики браузера и окружения, поведение рендеринга и таймингов, паттерны взаимодействия и множество мелких несоответствий, отличающих настоящий браузер, которым управляет человек, от автоматизированного, надевшего чужой user agent.

Он написан на чистом JavaScript — без фреймворка, без рантайма от сборщика, без зависимостей. Это было осознанное ограничение, а не предпочтение:

  • Он работает в чужой странице. Сборщик встроен в тысячи сайтов, которые мы не контролируем, рядом со всем, что эти сайты ещё загружают. Он не может рассчитывать на модульную систему, набор полифилов или конкретный базовый уровень браузера — и не должен конфликтовать с тем, что уже есть на странице.
  • Он должен быть маленьким и быстрым. Всё, что отгружается каждому посетителю каждого клиента, измеряется относительно перформанс-бюджета самого клиента. Рантайм фреймворка оказался бы больше, чем весь сборщик целиком.
  • Он работает во враждебной среде. Объекты измерения активно пытаются его обойти. Сбор сигналов должен быть устойчив к странице, которая может быть инструментирована, пропатчена или эмулирована вокруг него, — а это аргумент в пользу кода без прослоек между ним и браузерными API, которые он читает.

Работа сборщика заканчивается на сборе и отправке сигналов. Все суждения выносятся на сервере, где логика не видна тому, о ком выносится суждение.

Go на горячем пути

Каждый сервис на пути запроса написан на Go. Нагрузка близка к идеальной для языка форме: огромное количество мелких независимых, преимущественно I/O-bound запросов, которые нужно обрабатывать конкурентно с предсказуемой задержкой.

На практике важнее всего оказались два свойства. Горутины сделали конкурентность на уровне запроса достаточно дешёвой, чтобы приём, обогащение и скоринг могли внутренне разветвляться без настройки пула потоков. А рантайм Go дал ту картину хвостовых задержек, на которой строилось продуктовое обещание: в системе реального времени важен не средний ответ, а самые медленные несколько процентов — ровно там, где более тяжёлый рантайм и тратит свой бюджет.

Сам путь намеренно короткий: принять и провалидировать, разрешить контекст, прогнать правила детекции и модели по входящим сигналам, вернуть вердикт. Всё, что не обязано случиться до ответа — сохранение, агрегация, обогащение, обратная связь для моделей, — вытеснено с горячего пути в асинхронный конвейер позади него.

От одной базы к дата-платформе

Платформа стартовала на PostgreSQL, и на старте это было правильным решением. Одна база хранила события, аккаунты, конфигурацию и агрегаты; один язык запросов отвечал на любой вопрос; весь продукт можно было держать в голове целиком, пока он ещё нащупывал свою форму.

Столкновения с продакшен-объёмом это не пережило, и причину стоит сформулировать точно: дело было не в том, что «PostgreSQL слишком медленный», а в том, что от одного хранилища требовали быть четырьмя разными вещами одновременно. Высокочастотный append-only приёмник событий, хранилище быстрых выборок на пути запроса, аналитическое хранилище для агрегатов по миллиардам строк и поисковый индекс для разбора отдельных сессий — это четыре нагрузки с четырьмя противоречащими друг другу наборами требований. Тюнинг под любую из них ухудшал остальные.

Поэтому мы их разделили и перевели платформу в Google Cloud:

Redis — состояние горячего пути. Redis держит всё, к чему решение о скоринге должно обратиться за единицы миллисекунд: свежее состояние по посетителю и сессии, счётчики частоты и лимитов, разрешённую конфигурацию и справки по репутации. Именно это делает вердикт внутри запроса возможным в принципе, и именно это забирает на себя объём чтений, который иначе лёг бы на основную базу.

— шов между синхронным и асинхронным. Каждый оценённый запрос публикуется как событие, а ответ возвращается немедленно; всё, что дальше, читает уже оттуда. Это та развязка, которая позволяет пути запроса оставаться коротким, а остальной платформе — масштабироваться, передеплоиваться или временно замедляться так, чтобы эндпоинт скоринга этого не заметил. И это же означает, что всплеск трафика попадает в очередь, а не в базу данных.

Объектное хранилище — сырые события, навсегда. Каждый пакет сигналов архивируется в S3-совместимое в исходном виде. Это самое дешёвое надёжное место для данных, будущее применение которых ещё неизвестно, и именно поэтому логику детекции можно ретроспективно прогонять по реальному историческому трафику, а не по его сводке.

— аналитический слой. Агрегации по всей истории событий, отчётность по качеству трафика, когортный и кампейн-анализ, а также исследовательская работа за новыми эвристиками детекции — всё это выполняется здесь, в хранилище, построенном под сканы такого размера, и полностью вне операционного пути.

Elasticsearch — расследования. Когда клиент оспаривает вердикт или аналитик идёт по следу нового паттерна, вопрос звучит как «покажи мне вот эти сессии с шестью фильтрами» — это интерактивный поиск, а не агрегация. Это обслуживает , поэтому исследовательские запросы никогда не касаются систем, обслуживающих трафик.

PostgreSQL остался, но с гораздо более узкой зоной ответственности: аккаунты, конфигурация клиентов и те реляционные данные, которым действительно нужны транзакции и ограничения целостности. Именно снятие нагрузок, для которых он никогда не был предназначен, снова сделало его подходящим инструментом.

Kubernetes в Google Cloud

Платформа работает как контейнеризованные сервисы в кластере Kubernetes в Google Cloud. С таким профилем трафика альтернатива всерьёз и не рассматривалась: нагрузка, меняющаяся на порядок за минуты, требует инфраструктуры, которая добавляет мощность сама, и сервисов, каждый из которых дёшево реплицировать.

Здесь окупается разделение на небольшие сервисы на Go. У приёма, скоринга и асинхронных консьюмеров совсем разные кривые масштабирования, и как отдельные деплойменты каждый из них масштабируется по своему сигналу: слой приёма — по частоте запросов, консьюмеры — по глубине очереди. Rolling-деплои позволяли обновлять эндпоинт скоринга в рабочее время без окна обслуживания, что для сервиса, стоящего в пути загрузки страницы клиента, было жёстким требованием, а не удобством.

Результаты

Клиент завершил проект с продакшен-платформой, которая оценивает живой трафик в реальном времени, выдерживает десятки тысяч запросов в минуту с запасом на всплески в несколько раз выше и сохраняет каждый сырой сигнал, который когда-либо видела, — так что улучшение детекции, сделанное сегодня, проверяется на годах реального трафика, а не на синтетическом бенчмарке.

Не менее важно, что архитектура разделяет ответственность по тем линиям, которые действительно значимы для бизнеса: горячий путь остаётся маленьким, быстрым и скучным, а анализ, расследования и работа с моделями происходят за очередью, где они могут расти сколь угодно, никогда не угрожая времени ответа, которое видит клиент.

Именно такие системы мы берём целиком: высоконагруженные сервисы на Go, событийная дата-платформа и инфраструктура Kubernetes, на которой всё это работает. Если у вас есть задача такой формы — расскажите нам о ней.