A/B-тесты на уровне Nginx: рабочие приёмы из практики
Nginx умеет распределять запросы между вариантами без изменений в приложении. Разбираем split_clients, закрепление пользователя, маршрутизацию, кеширование и ошибки, из-за которых тест даёт ложные результаты.

Если распределять трафик между версиями страницы только в приложении, решение быстро упирается в задержки, дублирование логики и сложную откатку. Nginx 1.25.x и 1.26.x позволяют вынести край эксперимента на reverse proxy: выбрать вариант, передать его приложению и закрепить пользователя cookie. Но простого деления 50/50 недостаточно — без контроля кеша, ботов и повторных запросов A/B-тест легко превращается в набор несопоставимых данных.
Что именно должен делать Nginx
На границе системы прокси решает четыре задачи:
- выбирает вариант A или B;
- сохраняет выбор между запросами;
- передаёт вариант в приложение или направляет запрос в отдельный backend;
- добавляет признак варианта в логи и метрики.
Сам Nginx не считает конверсии и не определяет статистическую значимость. Он только формирует стабильную выборку. События покупки, регистрации или клика должны отправляться в аналитическую систему вместе с тем же идентификатором варианта.
Главный риск A/B-теста на прокси — не ошибиться в синтаксисе Nginx, а потерять стабильность выборки: один пользователь не должен видеть A утром и B вечером без понятной причины.
Сноска. Sticky assignment — закрепление пользователя за вариантом. В этой статье оно реализуется через cookie, а не через состояние внутри Nginx: конфигурация остаётся статeless, и выбор не теряется при перезапуске воркеров.
Три способа выбрать вариант
1. Хеширование признаков через split_clients
Модуль ngx_http_split_clients_module распределяет значение по диапазонам на основе хеша. Это не генератор случайных чисел на каждый запрос: одинаковая входная строка обычно приводит к одному и тому же варианту. Для первого эксперимента можно использовать IP и User-Agent:
split_clients "$remote_addr$http_user_agent" $hash_variant {
50% a;
* b;
}
Переменная $hash_variant получит значение a примерно для половины хеш-пространства, остальным запросам достанется b. Процент — это доля хеш-пространства, а не гарантия ровно половины реальных пользователей.
Подход не требует cookie, но у него есть цена. Пользователь может сменить сеть, получить другой IP или открыть сайт в другом браузере. Общий компьютер также будет считаться одним набором признаков. Для публичного сайта это приемлемый старт, но не лучший способ измерять поведение авторизованных клиентов.
2. Cookie поверх первоначального распределения
Для длительного теста лучше сначала уважать существующую cookie, а при её отсутствии использовать хеш. Директива map выбирает значение без цепочки из большого числа условных блоков:
split_clients "$remote_addr$http_user_agent" $hash_variant {
50% a;
* b;
}
map $cookie_ab_variant $ab_variant {
a a;
b b;
default $hash_variant;
}
add_header Set-Cookie "ab_variant=$ab_variant; Path=/; Max-Age=2592000; SameSite=Lax" always;
Такой фрагмент нужно применять только к страницам эксперимента, иначе cookie будет записываться на изображения, стили и API-ответы. Директива add_header добавляет заголовок к ответам, а параметр always распространяет его и на часть ответов с ошибками. Если приложение само выставляет Set-Cookie, проверьте, не появятся ли два конфликтующих заголовка.
Cookie с вариантом не должна считаться доказательством личности пользователя. Клиент может удалить или изменить её. Для авторизованных систем лучше использовать внутренний стабильный идентификатор и согласовать его передачу с командой приложения.
3. Маршрутизация в разные upstream-группы
Иногда вариант отличается не шаблоном, а целой версией приложения. Тогда Nginx может отправить запрос в разные upstream-группы:
upstream app_a {
server 127.0.0.1:8081;
}
upstream app_b {
server 127.0.0.1:8082;
}
map $ab_variant $experiment_backend {
a app_a;
b app_b;
}
location /experiment/ {
proxy_set_header X-AB-Variant $ab_variant;
proxy_set_header Host $host;
proxy_pass http://$experiment_backend;
}
Имена app_a и app_b здесь совпадают с группами upstream. При таком варианте проверяйте поведение proxy_pass с переменной в вашей сборке и не смешивайте его с добавлением URI в директиве: правила изменения пути отличаются от статического proxy_pass. Если различается только контент, чаще безопаснее отправить один и тот же backend заголовок X-AB-Variant и принять решение в приложении.
Как собрать конфигурацию без скрытых переключений
Удобная схема состоит из отдельных слоёв: выбор, исключения, передача и наблюдаемость. Не зашивайте процент, cookie и адреса backend в несколько разных location: при откате легко оставить старую часть логики.
map $http_user_agent $is_excluded_client {
default 0;
~*bot 1;
~*spider 1;
}
map "$is_excluded_client:$cookie_ab_variant" $ab_variant {
~^1: control;
~^0:a a;
~^0:b b;
default control;
}
log_format experiment '$request_id $remote_addr $status '
'variant=$ab_variant uri=$request_uri';
server {
access_log /var/log/nginx/experiment.log experiment;
location /landing/ {
proxy_set_header X-AB-Variant $ab_variant;
proxy_set_header X-Request-ID $request_id;
proxy_pass http://application;
}
}
Это пример каркаса для уже определённых переменных и upstream-группы. В реальной конфигурации добавьте отдельный механизм первичного выбора для пользователей без cookie. Важная деталь: регулярное выражение в map должно быть проверено на тестовых User-Agent, а список исключений — согласован с аналитикой. Не исключайте всех клиентов с необычным User-Agent автоматически: так можно потерять часть мобильного трафика.
Сравнение подходов
| Подход | Стабильность | Цена | Когда выбирать |
|---|---|---|---|
| Хеш IP и User-Agent | Средняя | Нет cookie, но вариант меняется при смене признаков | Короткий тест на публичных страницах |
| Хеш плюс cookie | Высокая для одного браузера | Нужно следить за Set-Cookie и очисткой cookie | Большинство UI-экспериментов |
| Разные upstream-группы | Зависит от выбранного признака | Две версии приложения, отдельные релизы и мониторинг | Тестирование backend или инфраструктуры |
| Решение в приложении | Высокая при стабильном ID | Дополнительная логика и задержка на уровне приложения | Авторизованные пользователи и сложные правила сегментации |
Кеширование: место, где ломается половина тестов
Если Nginx или CDN закешировал HTML только по URL, первый попавший в кеш вариант начнут получать остальные пользователи. Cookie и заголовок в этом случае не помогут: запрос до приложения вообще не дойдёт.
Для страниц эксперимента есть три практических варианта:
- временно отключить кеширование HTML на время проверки механики;
- включить вариант в ключ кеша, если выбранная CDN поддерживает такую настройку;
- отдавать общий HTML, а различия загружать после запроса к приложению.
Если используете встроенный proxy cache, ключ должен учитывать вариант. Синтаксис зависит от уже принятой схемы кеширования, поэтому не копируйте универсальную строку поверх рабочей конфигурации. Проверка проста: очистите кеш, отправьте два запроса с разными cookie и сравните не только заголовок варианта, но и тело ответа.
Инвалидация кеша — отдельная тема. Здесь важно только одно: до запуска теста точно определить, какой слой может вернуть старый HTML.
Что проверить до запуска
- Распределение. Отправьте несколько сотен запросов с разными тестовыми cookie и посчитайте варианты в access log. Небольшое отклонение от 50/50 нормально, но перекос нужно объяснить.
- Закрепление. Повторите запрос с cookie
ab_variant=a, затем сb. Приложение и заголовок ответа должны показывать соответствующий вариант. - Кеш. Сравните тело ответа и заголовки для двух cookie после прогрева кеша. Они не должны случайно совпадать из-за общего ключа.
- Откат. Проверьте, что удаление блока маршрутизации возвращает всех пользователей на контрольную версию без изменения DNS.
- Логи. Убедитесь, что в каждой записи есть вариант, HTTP-статус, URI и идентификатор запроса. Без этого ошибки приложения трудно отделить от ошибок распределения.
Что ломается на практике
Один пользователь видит два варианта
Чаще всего причина в отсутствии cookie, очистке браузером, разных доменах или в том, что часть страниц обслуживается другим server-блоком. Проверьте область действия cookie, протокол, поддомен и фактический ответ через инструменты разработчика.
Варианты распределяются неравномерно
Проверьте, что вы считаете пользователей, а не запросы. Страница с десятью ресурсами создаёт больше записей, чем страница с двумя. Для первичного анализа фильтруйте только нужный URI и учитывайте cookie или стабильный идентификатор.
Приложение не видит заголовок
Проверьте, что proxy_set_header X-AB-Variant $ab_variant; находится в том location, который реально обрабатывает запрос. Заголовок также может перезаписываться вложенной конфигурацией. Смотрите запрос между Nginx и backend, а не только заголовки в браузере.
Конверсия резко выросла после включения теста
Это может быть не эффект варианта, а потеря части событий, кеширование одного HTML или исключение мобильных клиентов. Сопоставьте количество назначений, просмотров и конверсий по каждому варианту. Если воронка не сходится, эксперимент нельзя интерпретировать.
Когда Nginx лучше не использовать
Прокси подходит, когда правило простое и нужно быстро разделить трафик. Он становится неудобным, если сегмент зависит от десятков признаков, требуется постепенное включение для отдельных аккаунтов или выбор должен синхронно применяться к нескольким сервисам. В таких случаях назначайте вариант в приложении или отдельной системе фичефлагов, а Nginx оставляйте для передачи и логирования признака.
Не используйте IP как идентификатор в тестах, где важна точная пользовательская аналитика. За одним адресом могут находиться офис, мобильный оператор или NAT-сеть, а один человек может менять адрес несколько раз за день.
Что делать дальше
Начните с одного URL и двух вариантов, включите подробный access log и прогоните тестовую выборку с фиксированными cookie. Затем проверьте кеш, цепочку прокси и доставку события конверсии. В продакшене смотрите не только на итоговую конверсию, но и на долю запросов без варианта, ошибки 4xx/5xx, задержку ответа и расхождение между назначениями и аналитическими событиями.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Официальная документация Nginx: split_clients, Официальная документация Nginx: map
Частые вопросы
Можно ли сделать A/B-тест в Nginx без изменений приложения?
Почему нельзя просто делить трафик по $request_id?
Как убедиться, что кеш не смешивает варианты?
Можно ли использовать IP для длительного эксперимента?
Что делать, если часть запросов не получает вариант?
Работают ли эти директивы в любой версии Nginx?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.