Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Hedged requests: снижение хвостовых задержек

Hedged requests: как убрать хвост p99 дублирующим запросом

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

Прямой ответ

Hedged request — спекулятивный повтор того же запроса к другой реплике, если первый не ответил за заданное время. Ответ берётся от того, кто ответил раньше; второй запрос отменяется. Приём снижает хвостовую задержку, не меняя основную нагрузку заметно.

Почему это работает: у каждого запроса есть небольшая вероятность попасть на медленную реплику — сборка мусора, фоновая задача, contention на диске. Медианная задержка остаётся низкой, а p99 растёт в разы. Если вероятность медленного ответа 5 %, то при двух попытках вероятность, что обе окажутся медленными, — 0,25 %.

Схема работы:

async def hedged_get(fetch, delay: float, replicas: list[str]):
    first = asyncio.create_task(fetch(replicas[0]))
    done, pending = await asyncio.wait({first}, timeout=delay)

    if done:                                    # первый успел
        return first.result()

    second = asyncio.create_task(fetch(replicas[1]))
    done, pending = await asyncio.wait(
        {first, second}, return_when=asyncio.FIRST_COMPLETED
    )
    for task in pending:
        task.cancel()                           # второй результат не нужен
    return done.pop().result()

Ключевой параметр — задержка старта второго запроса. Практическое правило из работы The Tail at Scale: отправлять дубль после того, как истёк 95-й процентиль обычной задержки. Тогда дублируются только те запросы, которые с высокой вероятностью окажутся медленными.

Если медиана составляет 20 миллисекунд, а p95 — 80, задержка старта ставится около 80 миллисекунд. При таких настройках доля дублирующих запросов составляет около 5 %, а p99 сокращается в несколько раз.

Ограничение, которое нельзя нарушать: hedged requests применимы только к идемпотентным операциям. Два одновременных списания — это двойное списание. Чтение подходит, запись — нет; о классе идемпотентных методов в RFC 9110.

Практика: настройка и границы применения

1. Измеряйте p95 до настройки. Задержка старта, выбранная на глаз, либо не даёт эффекта, либо удваивает нагрузку. Нужны метрики распределения задержки; о гистограммах — в статье про процентили.

2. Ограничивайте долю дублей. Если дублируется больше 5–10 % запросов, задержка старта выбрана слишком малой, и система начинает обслуживать сама себя.

3. Проверяйте запас ёмкости. Дополнительные 5 % нагрузки требуют запаса. На системе, работающей на пределе, приём усиливает перегрузку вместо снижения задержки; о расчёте ёмкости — в статье про capacity planning.

4. Отменяйте проигравший запрос. Если второй ответ не отменяется, ресурсы расходуются впустую, а при записи в кэш возможны гонки.

5. Не применяйте к операциям с побочными эффектами. Чтение из базы, получение курса, проверка статуса — подходят. Создание платежа, отправка письма, списание — нет.

6. Различайте hedged и retry. Повтор выполняется после ошибки или таймаута, то есть когда первый запрос уже не используется. Hedged выполняется параллельно с первым, пока тот ещё может ответить.

7. Следите за эффектом на репликах. Если медленная реплика получает больше запросов из-за дублей, её загрузка растёт, и она деградирует сильнее. Нужно сочетание с исключением сбойных узлов.

Рекомендации:

  1. Начинайте с одного уровня цепочки. Применение на каждом уровне умножает нагрузку; достаточно одного места, обычно ближе к хранилищу.
  2. Сочетайте с исключением медленных реплик. Автоматическое выведение деградировавшего узла из балансировки решает часть проблемы без дублей; о паттернах — в статье про circuit breaker.
  3. Измеряйте эффект до и после. Метрика p99 до включения и после — единственный способ подтвердить пользу.
  4. Не применяйте при высокой загрузке. Приём рассчитан на систему с запасом; в перегрузке он ухудшает положение.
  5. Учитывайте стоимость отмены. Если отменить запрос нельзя, дубль удваивает работу полностью, и выгода снижается.

Практический сценарий: p99 поиска превышал медиану в двадцать раз

Контекст: поисковый запрос в медиане занимал 25 миллисекунд, но p99 достигал 500. Пользователи периодически видели заметную задержку.

  1. Диагностика. Распределение задержки показало длинный хвост: около 4 % запросов занимали больше 200 миллисекунд. Медленные ответы распределялись по разным узлам и совпадали по времени со сборкой мусора и фоновым слиянием индексов.
  2. Оценка запаса. Загрузка поискового кластера составляла около 60 %, поэтому дополнительные 5 % нагрузки были допустимы.
  3. Действия. (а) Ввели hedged request с задержкой старта 90 миллисекунд — на уровне измеренного p95; (б) ограничили долю дублей в 5 % и добавили метрику на её контроль; (в) реализовали отмену проигравшего запроса; (г) оставили приём только на уровне чтения индекса, не распространяя на всю цепочку; (д) добавили исключение узлов с устойчиво высокой задержкой из балансировки.
  4. Результат. p99 сократился с 500 до 140 миллисекунд при росте общего числа запросов на 4,3 %. Медиана не изменилась.

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

FAQ

Можно ли применять hedged requests к записи? Нет, если операция не идемпотентна. Два параллельных запроса на запись могут выполниться оба. Для записи применяют повтор после явной ошибки, а не спекулятивный дубль.

Чем это отличается от обычного повтора? Повтор начинается после отказа или таймаута, когда первый запрос уже бесполезен. Hedged request идёт параллельно, пока первый ещё выполняется, поэтому выигрывает время, а не только надёжность.

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

Что делать, если ёмкости нет? Сначала увеличить запас или исключить медленные реплики из ротации. Hedged requests на загруженной системе усиливают перегрузку и дают обратный эффект.

Источники: Google Research (The Tail at Scale), RFC 9110, Prometheus docs — ссылки в шапке.