MAATRIX / Блог / Очередь вместо отказа: как заставить клиента подождать спокойно

Очередь вместо отказа: как заставить клиента подождать спокойно

MAATRIX

Сервер не справляется с наплывом запросов, и у вас есть ровно два способа сообщить об этом пользователю: молча захлопнуть дверь («502 Bad Gateway», зависшая вкладка, ничего не происходит) или сказать честно — «сейчас людей больше, чем мы можем обслужить одновременно, вот ваше место в очереди, вот сколько ждать». Разница между этими двумя сценариями не в мощности инфраструктуры, а в одном архитектурном решении: превратить перегрузку из отказа в управляемое ожидание. Дальше — как это устроено технически и почему пользователи на самом деле не против подождать, если видят, что происходит.

Почему отказ хуже, чем ожидание

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

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

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

Три места, где можно поставить очередь

Прежде чем писать код, нужно решить, на каком уровне стека вы ставите людей в очередь — это решение определяет всё остальное.

УровеньЧто делаетПлюсыМинусы
Edge / CDNСтатическая страница ожидания отдаётся с CDN, ещё до вашего сервераНе грузит бэкенд вообще, держит любой наплывНужна интеграция с CDN-провайдером или готовый сервис (Cloudflare Waiting Room и аналоги)
Балансировщик / реверс-проксиNginx/HAProxy отдаёт очередь на уровне маршрутизации запросовКонтроль остаётся у вас, не нужен внешний сервисЛогика очереди пишется руками, балансировщик — не для хранения состояния позиций
ПриложениеБэкенд сам решает, пускать запрос в обработку сразу или поставить в очередьМаксимальная гибкость, доступ к бизнес-логике (кто VIP, какой тип запроса приоритетнее)Сам факт похода в приложение уже создаёт нагрузку, которую вы пытаетесь избежать

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Виртуальная очередь: механика на Redis

Ядро виртуальной очереди — отсортированное множество, где каждому клиенту при входе присваивается позиция, а бэкенд регулирует скорость, с которой людей пускают дальше. На Redis это делается через ZADD и время как score:

# клиент встал в очередь — текущее время как score
ZADD site_queue 1756992400 client:a91f3c

# узнать позицию клиента (0 = первый в очереди)
ZRANK site_queue client:a91f3c

# узнать общий размер очереди
ZCARD site_queue

# впустить следующую пачку — например, 100 человек
ZRANGE site_queue 0 99
ZREM site_queue <ID впущенных>

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

SET queue_token:a91f3c client_session_id NX EX 1800

NX не даст создать второй активный токен для того же клиента, EX 1800 подчистит мусор от тех, кто закрыл вкладку и не вернулся, без отдельного сборщика.

Скорость впуска — не константа, а функция от реальной пропускной способности бэкенда. Простой вариант — впускать фиксированную пачку раз в N секунд через cron-задачу или воркер:

import time
import redis

r = redis.Redis()
BATCH_SIZE = 50
INTERVAL_SEC = 3

while True:
    batch = r.zrange("site_queue", 0, BATCH_SIZE - 1)
    if batch:
        r.zrem("site_queue", *batch)
        for client_id in batch:
            r.set(f"admitted:{client_id.decode()}", "1", ex=600)
    time.sleep(INTERVAL_SEC)

Более точный вариант — впускать не по таймеру, а по факту освобождения ёмкости: воркер следит за активной нагрузкой (число открытых соединений к БД, длина очереди задач, latency ключевого эндпоинта) и впускает новую пачку только когда метрика в пределах нормы. Это медленнее реагирует на резкое падение нагрузки, зато не впускает людей быстрее, чем бэкенд реально успевает их обслуживать — а именно это и есть цель очереди, а не сам факт её существования.

Graceful queueing вместо HTTP 503

Классический способ сказать «сервер перегружен» — HTTP 503 с заголовком Retry-After. Формально это правильно: код статуса говорит клиенту (и поисковым роботам), что это временная проблема, а не постоянная ошибка.

HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: text/html

Проблема в том, что 503 — это разговор сервера с браузером или ботом, а не с человеком. Обычный пользователь не увидит заголовок Retry-After, он увидит текст ошибки или белый экран и либо уйдёт, либо начнёт жать обновление вручную — то есть ретраи всё равно происходят, просто хаотично и без контроля с вашей стороны.

Graceful queueing — это тот же самый смысл («подождите, вас обслужат»), но выраженный как полноценный пользовательский опыт, а не код ответа:

  1. Вместо 503 отдаётся 200 OK со страницей ожидания — полноценным HTML с позицией, оценкой времени и логикой автообновления.
  2. Клиент сам не ретраит запрос вручную — этим занимается JS на странице ожидания, по контролируемому расписанию, а не в ответ на панику пользователя.
  3. Когда подходит очередь, сервер либо редиректит на реальную страницу, либо возвращает признак «впущен» в ответ на опрос, и клиент сам переходит дальше.

Стоит отличать это от backlog-очереди на уровне TCP-сокета: та живёт в ядре ОС и определяет, сколько непринятых accept()-соединений может накопиться, прежде чем новым клиентам придёт connection refused, — это защита сервера на уровне сети, работающая ещё до того, как запрос попал в приложение. Виртуальная очередь — решение уровня приложения, управляющее уже принятыми соединениями. Правильный backlog не даст серверу упасть от шторма TCP-соединений, виртуальная очередь не даст захлебнуться бизнес-логике — это дополняющие друг друга, а не взаимозаменяемые механизмы.

UX очереди: что показывать человеку

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

Обязательные элементы страницы ожидания:

  • Позиция в очереди, а не абстрактный спиннер. «Вы 214-й» ощутимо конкретнее, чем «пожалуйста, подождите».
  • Оценка времени ожидания, посчитанная от текущей скорости впуска, а не взятая с потолка. Если впускается 50 человек раз в 3 секунды, а вы 214-й, оценка — это простая арифметика (214 / 50 * 3 ≈ 13 секунд), которую стоит явно округлять в большую сторону, а не выдавать ложную точность.
  • Обновление позиции без перезагрузки страницы — через поллинг, SSE (Server-Sent Events) или WebSocket. Выбор зависит от масштаба и того, что уже есть в стеке:
СпособНагрузка на сервер при большой очередиСложность реализацииКогда уместен
Поллинг (fetch раз в N секунд)Растёт линейно с числом ожидающихМинимальнаяНебольшая и средняя очередь, простой стек
SSEНиже, чем у поллинга — одно долгоживущее соединение на клиента, сервер сам шлёт обновленияСредняя, нужен сервер с поддержкой долгих соединенийСредняя-крупная очередь, обновления в одну сторону (сервер → клиент)
WebSocketСопоставима с SSE, но дороже в поддержке ради однонаправленной задачиВыше — нужен полный дуплекс, хотя тут он не нуженОправдан, только если та же инфраструктура уже используется для другого функционала

Для чистой задачи «показать позицию в очереди» WebSocket почти всегда избыточен по сравнению с SSE — обновления идут только от сервера к клиенту, и полный дуплекс тут не нужен. Поллинг с интервалом 3-5 секунд и небольшим случайным джиттером (чтобы тысячи клиентов не били сервер синхронными залпами) — рабочее решение для подавляющего большинства сценариев, и его проще всего отладить и поддерживать.

  • Визуальный прогресс-бар, даже условный. Даже если он не отражает буквально долю пройденного пути (реальная скорость впуска неравномерна), заполняющаяся полоса снижает субъективное ощущение бесконечного ожидания сильнее, чем меняющееся число само по себе — это вопрос убирания неопределённости, а не украшательства.
  • Явное «не закрывайте вкладку» и что произойдёт, если закроют. Если место в очереди теряется при закрытии вкладки — скажите это прямо, а не оставляйте пользователя гадать.

Чего не стоит делать в очереди ожидания

Несколько граблей, которые обесценивают всю затею с прогрессом и доверием:

  • Фейковый прогресс, не связанный с реальной позицией. Прогресс-бар, который ползёт с заданной скоростью независимо от фактического продвижения в очереди, работает ровно до первого случая, когда реальность разошлась с картинкой, — дальше пользователь перестаёт верить интерфейсу вообще.
  • Оценка времени без права на ошибку. Если написано «осталось 2 минуты», а прошло 10, лучше показывать диапазон или явно писать «оценка ориентировочная» — короткая оговорка снижает раздражение сильнее, чем точная цифра, которая не подтвердилась.
  • Опрос позиции без бэкоффа и джиттера. Если тысячи клиентов синхронно опрашивают сервер каждые ровно 3 секунды, вы получаете периодические микро-пики нагрузки строго по расписанию. Случайное отклонение (например, 3000 + random(0, 1000) мс) размазывает нагрузку по времени почти бесплатно.
  • Очередь без пути для VIP и приоритетных случаев, если они есть в бизнес-логике (уже оплаченный заказ, повторный запрос после сбоя оплаты). Жёсткая FIFO-очередь без исключений иногда создаёт больше проблем, чем решает.
  • Молчание при сбое самой очереди. Если Redis, на котором держится очередь, недоступен, страница ожидания не должна превращаться в белый экран — нужен явный fallback-режим (например, временный более консервативный rate limiting без персональных позиций), а не тихий отказ там, где вы старались уйти именно от отказов.

Когда очередь не подходит и что делать вместо неё

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

Очередь также плохо сочетается с ожиданием в реальном времени, где сама суть сервиса — низкая задержка (торговые терминалы, игровые матчи с реальными соперниками): там правильнее ограничивать доступ через rate limiting или отказ с понятной причиной, чем ставить человека в очередь на действие, которое обесценивается задержкой. Разница между этими подходами разобрана отдельно в материале о том, как работает rate limiting: рейт-лимит режет избыточные запросы сразу, очередь — откладывает их обслуживание на потом. Путать их — типичная ошибка при проектировании защиты от перегрузки.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Сколько человек можно держать в очереди одновременно на Redis, не упираясь в производительность?

ZSET на Redis по своей структуре справляется с сотнями тысяч элементов без проблем — узким местом обычно становится не структура данных, а частота опросов позиции клиентами. Если очередь большая, важнее оптимизировать интервал поллинга и использовать SSE, чем беспокоиться о ёмкости ZSET.

Что делать, если пользователь закрыл вкладку и вернулся через час — он теряет место в очереди?

Зависит от TTL токена. Если токен привязан к клиенту через cookie с сохранённым ID, при повторном заходе можно восстановить прежнюю позицию, пока TTL не истёк. Разумный TTL должен быть заметно больше реалистичного времени ожидания, но не бесконечным, иначе очередь копит мёртвые записи.

Нужна ли очередь, если пиковая нагрузка предсказуема и инфраструктуру можно заранее увеличить под неё?

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

Чем виртуальная очередь отличается от готовых сервисов вроде Cloudflare Waiting Room?

Готовый сервис снимает разработку и обычно работает на уровне edge — плюс для разовых пиковых событий. Минус — ещё одна внешняя зависимость в критичный момент и меньше контроля над логикой (приоритеты, кастомная оценка времени). Для регулярных пиков собственная реализация обычно окупается быстрее за счёт полного контроля над деталями.

Как тестировать очередь до того, как на неё придёт реальный пик?

Нагрузочным тестом со ступенчато растущим числом клиентов, где важна не только RPS, а то, действительно ли позиция двигается предсказуемо и оценка времени остаётся близкой к реальности. Отдельно стоит намеренно уронить Redis или воркер впуска во время теста и убедиться, что fallback-режим действительно срабатывает, а не превращается в те же 502 и 503, от которых вы пытались уйти.

Помогает ли очередь против ботов и скриптов, которые пытаются обойти лимиты?

Сама по себе — нет: без привязки токена к сессии и без ограничения на число активных токенов с одного клиента ничего не мешает боту встать в очередь десятками параллельных «личностей». Это разные задачи, которые решаются вместе, но не одним и тем же механизмом.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →