Как бороться с хвостовыми задержками в распределённой системе?
- Что такое hedged requests?
- Как снизить p99 при fan-out на много серверов?
- Откуда берутся медленные запросы, если все серверы здоровы?
Хвосты неизбежны: сборка мусора, фоновые задачи, соседи по машине, переключения. При fan-out они становятся нормой. Главные приёмы — не ждать медленного: дублирующие запросы, умная балансировка, частичные ответы.
- Hedged request: нет ответа за p95 — отправить копию на другую реплику, взять первый ответ
- Балансировка по нагрузке (least requests, power of two choices) вместо round robin
- Разделять тяжёлые и лёгкие запросы, ограничивать очереди
А ещё Частичный ответ лучше позднего: поиск без одного шарда, лента без одного блока
Подробно
Откуда хвосты даже на здоровых машинах:
- паузы сборщика мусора и JIT-компиляция;
- фоновые задачи: compaction в LSM, VACUUM, бэкапы, ротация логов;
- общие ресурсы: соседние контейнеры, throttling CPU в Kubernetes, шумные соседи по диску и сети;
- очереди: временный всплеск на одном экземпляре;
- потери пакетов и повторная передача TCP (200 мс на RTO);
- холодные кэши после перезапуска.
Устранить всё нельзя, поэтому строят системы, терпимые к хвостам (идея статьи Google «The Tail at Scale»).
Приёмы:
- hedged requests — отправить запрос одной реплике; если ответа нет дольше p95, отправить копию другой и взять тот, что придёт первым. Добавляет ~5% нагрузки, а хвост срезает в разы. Только для идемпотентных чтений;
- tied requests — отправить сразу двум, и когда один начал обработку, он отменяет копию у другого;
- балансировка с учётом нагрузки — round robin продолжает слать на тормозящий экземпляр; least outstanding requests, power of two choices и выбрасывание медленных (outlier detection) уводят трафик;
- разные очереди и пулы для тяжёлых и лёгких запросов, чтобы отчёт на 10 секунд не стоял перед запросом на 5 мс;
- частичные ответы — агрегатор с дедлайном отдаёт то, что успело прийти: поисковая выдача без одного шарда, лента без рекомендаций;
- меньше fan-out — меньше шардов на запрос, предагрегация, кэш на уровне агрегатора;
- дедлайны вместо таймаутов — передавать оставшееся время по цепочке, чтобы не делать работу, результат которой уже не нужен.
Измерять — по гистограммам на стороне клиента, отдельно для каждой зависимости; смотреть не только p99, но и p99.9 — при большом fan-out именно он определяет p99 агрегатора.
Пример
async def hedged(get, replicas, hedge_after=0.05):
first = asyncio.create_task(get(replicas[0]))
done, _ = await asyncio.wait({first}, timeout=hedge_after) # ждём ~p95
if done:
return first.result()
second = asyncio.create_task(get(replicas[1])) # дублируем
done, pending = await asyncio.wait({first, second}, return_when=asyncio.FIRST_COMPLETED)
for t in pending: t.cancel()
return done.pop().result()Что хотят услышать
- Источники хвостов на здоровых машинах.
- Hedged requests и почему они дёшевы.
- Балансировка с учётом нагрузки и изоляция тяжёлых запросов.
- Частичные ответы и дедлайны.
Частые ошибки
- Дублировать неидемпотентные запросы.
- Пытаться «победить» хвост только тюнингом GC.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗Хвосты можно полностью устранить
- ✓Hedged requests и балансировка по нагрузке
- ✗При fan-out хвосты становятся реже
- ✗Достаточно увеличить таймауты
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.