Перейти к содержимому
bacebu4
Назад

Как проектировать дедлайны и таймауты

Read in English

Требование: клиент ждёт ответа не дольше 200 мс.

Фиксированный таймаут на каждом вызове зависимости не выполняет это требование, потому что фиксированный таймаут ограничивает только один вызов. Решение: вычислять таймаут каждого вызова в момент вызова, из времени, которое осталось до одного дедлайна, общего для всего запроса.

Пример

У нас три зависимости:

Зависимостьp50p99.9Обязательна
A, профиль клиента10 мс40 мсДа
B, цены30 мс90 мсДа
C, рекомендации25 мс120 мсНет, есть фолбэк

Наш сервис вызывает A, затем B, затем C, потому что каждому вызову нужен результат предыдущего. Наша собственная работа занимает 20 мс.

Почему фиксированные таймауты не работают

Разделим 200 мс заранее, чтобы фиксированные таймауты в сумме давали ровно 200 мс:

Такое разделение ломается в двух случаях:

Фиксированное разделение: запрос падает, хотя осталось ещё 90 мс

Один дедлайн на запрос

У обеих проблем одна причина: каждый таймаут зафиксирован до начала запроса. Решение: вычислять таймаут каждого вызова в момент его начала, из оставшегося времени. Чтобы знать оставшееся время, наш сервис должен знать, когда клиент перестанет ждать. Этот момент времени и есть дедлайн.

Таймаут вызова из оставшегося времени

Вызывающий сервис вычисляет таймаут каждого вызова из времени, которое осталось в момент вызова:

таймаут вызова=min⁡(дедлайн−сейчас−резерв, потолок зависимости)\text{таймаут вызова} = \min(\text{дедлайн} - \text{сейчас} - \text{резерв},\ \text{потолок зависимости})

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

Потолок зависимости

Потолок зависимости — это самое большое время, которое мы ждём одну зависимость, даже если времени осталось больше. Выбирайте его из допустимой доли ложных таймаутов, то есть таймаутов на вызовах, которые завершились бы успешно. Amazon выбирает долю, например 0,1%, и берёт соответствующий перцентиль задержки, p99.9.

В нашем примере потолки такие:

Минимальный таймаут

Если таймаут вызова слишком мал, чтобы зависимость успела ответить, вызов, скорее всего, упадёт по таймауту. Задайте каждой зависимости минимальный таймаут, например её задержку p50. Если таймаут вызова меньше минимального таймаута, не делайте вызов. Используйте фолбэк или верните ошибку, если зависимость обязательна.

Формула для цепочки вызовов

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

Возьмём цепочку из раздела «Почему фиксированные таймауты не работают» с теми же задержками.

Те же задержки: таймаут от дедлайна позволяет B успеть

Потолки A, B и C в сумме дают 220 мс, и ещё 20 мс нужны на собственную работу. Фиксированное разделение не может дать такие таймауты, потому что его доли в сумме должны быть не больше 200 мс. Формула может использовать такие потолки, потому что вызов получает весь свой потолок, только если осталось достаточно времени. Когда ранние вызовы медленные, поздние вызовы получают таймауты короче.

Например, если A отвечает за 48 мс, а B за 108 мс, то C начинается в 156 мс и получает min(200 − 156 − 20, 60) = 24 мс. Это меньше минимального таймаута C в 25 мс, поэтому наш сервис пропускает C и отвечает клиенту в 176 мс.

Медленные A и B: с вызовом C ответ пришёл бы после дедлайна, поэтому C пропущен

Ретраи и хеджирование в пределах дедлайна

Ретраи. Перед каждым ретраем заново вычисляйте таймаут вызова и сравнивайте его с минимальным таймаутом. Ни один ретрай не выполняется после дедлайна.

Хеджированные запросы. При медленном вызове ретрай начинается только после истечения таймаута вызова, а к этому моменту времени остаётся мало. Хеджированный запрос (hedged request) не ждёт таймаута. Если исходный вызов ждёт дольше, чем задержка p95 зависимости, отправьте вторую копию с таймаутом, вычисленным из оставшегося времени. Используйте тот ответ, который придёт первым, и отмените другой вызов. Это добавляет примерно 5% запросов (см. The Tail at Scale).

Медленный вызов B: ретрай и хеджированный запрос


Поделиться: