Устойчивость и латентность
Латентность и перцентили, таймауты и распространение дедлайнов, ретраи с backoff и jitter, circuit breaker, bulkhead, backpressure, сброс нагрузки и hedged-запросы.
9 вопросов
JuniorТеорияОчень частоПочему каждому исходящему вызову в Go-сервисе нужен таймаут и как его задать?
Почему каждому исходящему вызову в Go-сервисе нужен таймаут и как его задать?
Без таймаута медленная или зависшая зависимость навсегда занимает goroutine и её соединение, поэтому под нагрузкой эти утечки копятся, пока сервис не исчерпает ресурсы и не упадёт. Каждый вызов ограничивай через ctx, cancel := context.WithTimeout(parent, budget), передавай ctx и следи за ctx.Done() — вызов оборвётся, когда бюджет истечёт.
Типичные ошибки
- ✗Не ставить таймауты на исходящие вызовы, и зависшая зависимость под нагрузкой тихо утекает goroutine и соединениями
- ✗Ставить один глобальный таймаут клиента вместо дедлайна на каждый вызов под бюджет запроса
- ✗Вызвать
context.WithTimeout, но не проверятьctx.Done()и не передаватьctxв сам вызов
Уточняющие вопросы
- →Как передать остаток дедлайна в нижестоящие вызовы по цепочке?
- →Чем
context.WithTimeoutотличается отcontext.WithDeadline?
MiddleТеорияОчень частоКак работает паттерн устойчивости circuit breaker и какую проблему он решает?
Как работает паттерн устойчивости circuit breaker и какую проблему он решает?
Он отслеживает состояния closed -> open -> half-open. Когда доля ошибок переходит порог, он переключается в open и отказывает быстро, не вызывая сломанную зависимость. После паузы он пропускает один пробный запрос (half-open) для проверки восстановления и закрывается при успехе. Это останавливает каскадный сбой и даёт зависимости восстановиться.
Типичные ошибки
- ✗Думают, что брейкер повторяет вызов — он делает обратное: отказывает быстро и полностью пропускает зависимость, пока открыт.
- ✗Забывают про пробный запрос в
half-open, поэтому брейкер не перепроверяет восстановление и остаётся открытым даже после починки. - ✗Ставят порог на абсолютное число ошибок, а не на их долю, и нагруженный сервис срабатывает на шуме при обычном трафике.
Уточняющие вопросы
- →Как настроить порог ошибок и длительность паузы в состоянии
open? - →Зачем сочетать паттерн устойчивости circuit breaker с таймаутами и дедлайнами на вызов?
MiddleКодОчень частоКак написать helper ретраев только для transient-ошибок с exponential backoff, jitter и уважением ctx?
Как написать helper ретраев только для transient-ошибок с exponential backoff, jitter и уважением ctx?
Крутите цикл до фиксированного лимита, вызывая op; при успехе верните nil, повторяйте только при transient-ошибке, иначе её верните. Между попытками спите base*2^n (1s, 2s, 4s) плюс случайный jitter и select по ctx.Done(), чтобы отменённый ctx прервал ожидание.
Типичные ошибки
- ✗Повтор неидемпотентных или нетранзиентных ошибок — дублирование сайд-эффектов или долбёж по постоянному сбою
- ✗Backoff без jitter — все клиенты ретраят синхронно и создают согласованный thundering herd
- ✗Сон голым
time.Sleepбез учётаctx— отменённый вызов досиживает весь backoff
Уточняющие вопросы
- →Зачем jitter вместо фиксированного сдвига между ретраями?
- →Как retry budget предотвращает усиление ретраев по цепочке?
JuniorТеорияЧастоПочему латентность запроса измеряют перцентилями p50/p95/p99, а не средним?
Почему латентность запроса измеряют перцентилями p50/p95/p99, а не средним?
Среднее прячет хвост: низкое среднее уживается с ужасным p99, в который реально попадает много пользователей. Перцентили описывают худший случай, а он доминирует при fan-out — запрос к N сервисам не быстрее самой медленной зависимости.
Типичные ошибки
- ✗Смотреть только на среднюю латентность и считать сервис быстрым, пока жёсткий p99 тихо бьёт по большой доле пользователей.
- ✗Забыть про fan-out: считать запрос быстрым по средней зависимости, хотя его держит самая медленная из них.
Уточняющие вопросы
- →Как fan-out по множеству сервисов усиливает хвост одной зависимости?
- →Как латентность, throughput и concurrency связаны законом массового обслуживания Литтла?
JuniorТеорияЧастоЧто такое load shedding и зачем перегруженный сервис должен его применять?
Что такое load shedding и зачем перегруженный сервис должен его применять?
При перегрузке ты сознательно отбрасываешь низкоприоритетную или лишнюю работу (admission control), чтобы защитить основной путь, и быстро возвращаешь 503/429 вместо медленной смерти. Отсекай рано, до коллапса, чтобы принятые запросы всё же завершались.
Типичные ошибки
- ✗Путают load shedding с backpressure — shedding отбрасывает работу, а backpressure просит upstream замедлиться.
- ✗Отсекают слишком поздно — лишь когда очереди уже забиты и латентность рухнула для всех.
- ✗Отбрасывают запросы молча или с 500 вместо быстрого 503/429, по которому клиент может разумно повторить.
Уточняющие вопросы
- →Как решить, какие запросы отбрасывать в первую очередь при перегрузке?
- →Какой код ответа вернёшь и почему это важно для вызывающего?
MiddleТеорияЧастоКак паттерн изоляции bulkhead и backpressure не дают одной перегруженной зависимости утопить сервис?
Как паттерн изоляции bulkhead и backpressure не дают одной перегруженной зависимости утопить сервис?
Bulkhead изолирует ресурсы по каждой зависимости — отдельные пулы goroutine или соединений, ограниченные очереди — чтобы одна насыщенная не исчерпала ВСЕ. Backpressure сигналит наверх замедлиться через ограниченные channel; полный буфер блокирует или сбрасывает, не растёт.
Типичные ошибки
- ✗Использовать один общий пул goroutine или соединений на все зависимости, и одна медленная морит голодом всех
- ✗Буферизовать без границы под нагрузкой вместо блокировки или сброса — это лишь откладывает крах в OOM
- ✗Путать bulkhead (изоляция) с circuit breaker (быстрый отказ) — они решают разные формы отказа
Уточняющие вопросы
- →Где разместить ограниченную очередь и как подобрать её ёмкость?
- →Как backpressure сочетается с приёмом load-shedding при длительной перегрузке?
MiddleТеорияИногдаКак request coalescing и hedged-запросы по-разному влияют на хвостовую латентность и нагрузку?
Как request coalescing и hedged-запросы по-разному влияют на хвостовую латентность и нагрузку?
Coalescing схлопывает одинаковые конкурентные in-flight запросы в ОДИН вызов (singleflight), гася thundering herd / cache stampede по горячему ключу — режет нагрузку, не латентность одного запроса. Hedged-запросы режут хвост: после ~p95 шлёшь вторую копию на другую реплику, берёшь, что вернулось первым (проигравшего отменяешь), ценой лишней нагрузки — поэтому ограничь долю hedge (~<=5%).
Типичные ошибки
- ✗Считать, что coalescing снижает латентность одного запроса — он лишь режет дублирующую нагрузку; сам in-flight вызов не быстрее.
- ✗Хеджировать всё: неограниченная доля hedge удваивает нагрузку и может добить уже страдающую зависимость вместо спасения хвоста.
- ✗Слать hedge сразу, а не после ~p95 — большинство запросов получают лишнюю вторую копию без выигрыша по хвосту.
Уточняющие вопросы
- →Почему
singleflightкоалесцирует только в одном процессе, а не между инстансами? - →Чем tied-запросы отличаются от обычных hedged-запросов по отмене?
SeniorДизайнИногдаRead-эндпоинт делает fan-out на ~8 downstream-сервисов на запрос; p50 в норме, но p99 неприемлем, при этом менять downstream-сервисы нельзя, нужно уложиться в фиксированный бюджет латентности и не усиливать их нагрузку. Спроектируй, как ты укротишь хвост.
Read-эндпоинт делает fan-out на ~8 downstream-сервисов на запрос; p50 в норме, но p99 неприемлем, при этом менять downstream-сервисы нельзя, нужно уложиться в фиксированный бюджет латентности и не усиливать их нагрузку. Спроектируй, как ты укротишь хвост.
Сначала найди виновника: при fan-out самый медленный из 8 зависимостей задаёт хвост, поэтому меряй p99 по каждой зависимости. Выведи дедлайн на вызов из бюджета запроса и пробрасывай его через context, чтобы ни один хоп его не сбрасывал. Хеджируй медленные реплики после ~p95 с ограниченной долей hedge (например <=5%), схлопывай дубли чтений горячего ключа в один вызов и избегай безлимитных ретраев, усиливающих нагрузку.
Типичные ошибки
- ✗Тюнить агрегатный
p99вместо перцентилей по каждой зависимости, из-за чего настоящий медленный виновник прячется за остальными. - ✗Сбрасывать дедлайн на каждом downstream-хопе, из-за чего суммарное время вылетает за бюджет запроса при fan-out.
- ✗Хеджировать каждый вызов без ограничения доли, что удваивает нагрузку на downstream, которую по условию усиливать нельзя.
Уточняющие вопросы
- →Как подобрать дедлайн на вызов, когда 8 вызовов идут параллельно, а не последовательно?
- →Какой сигнал говорит, что приём срезания хвоста hedged requests помогает, а не просто добавляет напрасную нагрузку?
SeniorДизайнИногдаGo-сервис ловит внезапный всплеск трафика в 10 раз от вирусного события, и одновременно деградирует одна downstream-зависимость; при фиксированной пропускной способности downstream, без времени добавить машины и с требованием сохранить живым основной путь — как ты спроектируешь средства от перегруза, чтобы сервис деградировал мягко и устоял, а не обвалился?
Go-сервис ловит внезапный всплеск трафика в 10 раз от вирусного события, и одновременно деградирует одна downstream-зависимость; при фиксированной пропускной способности downstream, без времени добавить машины и с требованием сохранить живым основной путь — как ты спроектируешь средства от перегруза, чтобы сервис деградировал мягко и устоял, а не обвалился?
Сложи средства от перегруза: ограниченные очереди дают backpressure, load-shedding отбрасывает низкоприоритетную работу с быстрыми 503/429, чтобы принятые запросы дошли, изоляция bulkhead по зависимостям сдерживает деградирующую, а circuit breaker быстро отказывает на ней — и сервис деградирует мягко, а не падает в неограниченные очереди и OOM.
Типичные ошибки
- ✗Дать очередям расти без границ, лишь бы не отвергать запросы — это только откладывает обвал в OOM и портит хвост латентности.
- ✗Агрессивно ретраить деградирующую зависимость, усиливая её нагрузку и превращая частичный сбой в полный каскад.
- ✗Считать сброс нагрузки провалом, а не защитой принятых запросов на основном пути.
Уточняющие вопросы
- →Как ты выберешь, какие запросы сбрасывать первыми при перегрузе?
- →Где задаёшь размер ограниченной очереди и как он ограничивает хвост латентности?