Требование: клиент ждёт ответа не дольше 200 мс.
Фиксированный таймаут на каждом вызове зависимости не выполняет это требование, потому что фиксированный таймаут ограничивает только один вызов. Решение: вычислять таймаут каждого вызова в момент вызова, из времени, которое осталось до одного дедлайна, общего для всего запроса.
Пример
У нас три зависимости:
| Зависимость | p50 | p99.9 | Обязательна |
|---|---|---|---|
A, профиль клиента | 10 мс | 40 мс | Да |
B, цены | 30 мс | 90 мс | Да |
C, рекомендации | 25 мс | 120 мс | Нет, есть фолбэк |
Наш сервис вызывает A, затем B, затем C, потому что каждому вызову нужен результат предыдущего. Наша собственная работа занимает 20 мс.
Почему фиксированные таймауты не работают
Разделим 200 мс заранее, чтобы фиксированные таймауты в сумме давали ровно 200 мс:
Aполучает 40 мс.Bполучает 100 мс.Cполучает 40 мс.- Собственная работа получает 20 мс.
Такое разделение ломается в двух случаях:
- Вызов не может использовать время, которое не использовал предыдущий вызов.
Aотвечает за 10 мс, аBнужно 105 мс, на 5 мс больше его таймаута.Bпадает по таймауту, и запрос завершается ошибкой. В этот момент из 200 мс осталось ещё 90 мс, аCи нашей собственной работе нужно из них не больше 60 мс.
- Ретрай не помещается. Ретраю
Bнужны ещё 100 мс, которых в разделении нет.
Один дедлайн на запрос
У обеих проблем одна причина: каждый таймаут зафиксирован до начала запроса. Решение: вычислять таймаут каждого вызова в момент его начала, из оставшегося времени. Чтобы знать оставшееся время, наш сервис должен знать, когда клиент перестанет ждать. Этот момент времени и есть дедлайн.
- Первый сервис, который получает запрос клиента, задаёт дедлайн один раз. Дедлайн равен времени прихода запроса плюс 200 мс.
- Вызывающий сервис задаёт таймаут каждого вызова. Вызываемый сервис получает его как свой собственный дедлайн и вычисляет из него свои таймауты вызовов.
Таймаут вызова из оставшегося времени
Вызывающий сервис вычисляет таймаут каждого вызова из времени, которое осталось в момент вызова:
Резерв — это время, которое нужно нашему сервису после ответа на вызов: объединить данные, сериализовать ответ и передать его вызывающему.
Потолок зависимости
Потолок зависимости — это самое большое время, которое мы ждём одну зависимость, даже если времени осталось больше. Выбирайте его из допустимой доли ложных таймаутов, то есть таймаутов на вызовах, которые завершились бы успешно. Amazon выбирает долю, например 0,1%, и берёт соответствующий перцентиль задержки, p99.9.
В нашем примере потолки такие:
Aполучает 50 мс: его p99.9 в 40 мс плюс небольшой запас.Bполучает 110 мс: его p99.9 в 90 мс плюс небольшой запас.Cполучает 60 мс, меньше его p99.9 в 120 мс, потому что уCесть фолбэк. Мы допускаем больше ложных таймаутов наC, чтобы страница загружалась быстрее.
Минимальный таймаут
Если таймаут вызова слишком мал, чтобы зависимость успела ответить, вызов, скорее всего, упадёт по таймауту. Задайте каждой зависимости минимальный таймаут, например её задержку p50. Если таймаут вызова меньше минимального таймаута, не делайте вызов. Используйте фолбэк или верните ошибку, если зависимость обязательна.
Формула для цепочки вызовов
Каждому вызову нужен результат предыдущего, поэтому резерв включает ещё и минимальные таймауты (в нашем примере p50) всех обязательных вызовов, которые ещё впереди. Не резервируйте время для необязательных вызовов.
Возьмём цепочку из раздела «Почему фиксированные таймауты не работают» с теми же задержками.
Aначинается в 0 мс. Резерв — 20 мс (собственная работа) плюс минимальный таймаутBв 30 мс, всего 50 мс.Aполучает min(200 − 0 − 50, 50) = 50 мс и отвечает за 10 мс.Bначинается в 10 мс.Cнеобязателен, поэтому резерв всего 20 мс.Bполучает min(200 − 10 − 20, 110) = 110 мс и отвечает за 105 мс. При фиксированном разделенииBздесь упал бы по таймауту.Cначинается в 115 мс и получает min(200 − 115 − 20, 60) = 60 мс. Он отвечает за 25 мс, и наш сервис отвечает клиенту в 160 мс.
Потолки A, B и C в сумме дают 220 мс, и ещё 20 мс нужны на собственную работу. Фиксированное разделение не может дать такие таймауты, потому что его доли в сумме должны быть не больше 200 мс. Формула может использовать такие потолки, потому что вызов получает весь свой потолок, только если осталось достаточно времени. Когда ранние вызовы медленные, поздние вызовы получают таймауты короче.
Например, если A отвечает за 48 мс, а B за 108 мс, то C начинается в 156 мс и получает min(200 − 156 − 20, 60) = 24 мс. Это меньше минимального таймаута C в 25 мс, поэтому наш сервис пропускает C и отвечает клиенту в 176 мс.
Ретраи и хеджирование в пределах дедлайна
Ретраи. Перед каждым ретраем заново вычисляйте таймаут вызова и сравнивайте его с минимальным таймаутом. Ни один ретрай не выполняется после дедлайна.
Хеджированные запросы. При медленном вызове ретрай начинается только после истечения таймаута вызова, а к этому моменту времени остаётся мало. Хеджированный запрос (hedged request) не ждёт таймаута. Если исходный вызов ждёт дольше, чем задержка p95 зависимости, отправьте вторую копию с таймаутом, вычисленным из оставшегося времени. Используйте тот ответ, который придёт первым, и отмените другой вызов. Это добавляет примерно 5% запросов (см. The Tail at Scale).