惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Engineering at Meta
Engineering at Meta
博客园 - 三生石上(FineUI控件)
The GitHub Blog
The GitHub Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
Schneier on Security
Vercel News
Vercel News
A
Arctic Wolf
G
Google Developers Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
雷峰网
雷峰网
AWS News Blog
AWS News Blog
V
Visual Studio Blog
Cyberwarzone
Cyberwarzone
爱范儿
爱范儿
博客园 - 司徒正美
NISL@THU
NISL@THU
N
News | PayPal Newsroom
N
News and Events Feed by Topic
Scott Helme
Scott Helme
I
InfoQ
Project Zero
Project Zero
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
Tenable Blog
Help Net Security
Help Net Security
D
Docker
IT之家
IT之家
人人都是产品经理
人人都是产品经理
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
PCI Perspectives
PCI Perspectives
aimingoo的专栏
aimingoo的专栏
博客园 - Franky
G
GRAHAM CLULEY
阮一峰的网络日志
阮一峰的网络日志
T
The Exploit Database - CXSecurity.com
Recent Announcements
Recent Announcements
博客园 - 【当耐特】
L
LINUX DO - 热门话题
J
Java Code Geeks
C
Cyber Attacks, Cyber Crime and Cyber Security
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
H
Hacker News: Front Page
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
SecWiki News
SecWiki News
T
Tailwind CSS Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
罗磊的独立博客

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Почему retry и timeout могут ухудшить отказоустойчивость системы
dmikat (Ozon · 2026-04-24 · via Все публикации подряд на Хабре

Retry и timeout кажутся базовыми механизмами отказоустойчивости.
Не прошел запрос — повторим. Ответ не пришел за 500 мс — оборвем. Кажется, что этого достаточно, чтобы система стала надежнее.

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

В этой статье разберем, как retry создает каскадные отказы, почему таймауты могут ухудшить ситуацию и какие механизмы — backoff, jitter и circuit breaker — помогают этого избежать

Зачем нужен retry

Ошибки в распределенных системах неизбежны. Даже если код написан аккуратно, инфраструктура остается средой, где возможны кратковременные сбои: запрос может не дойти из-за сетевой ошибки, сервис может на короткое время оказаться перегружен, DNS — ответить медленнее обычного, а балансировщик — направить трафик на инстанс, который ещё не успел прогреться после старта. Такие проблемы не всегда означают, что операция в принципе невозможна. Часто это просто временное ухудшение состояния системы, которое проходит само через доли секунды или за несколько секунд.

Для таких ситуаций и нужен retry — повторная попытка выполнить запрос после ошибки. Его задача в том, чтобы пережить кратковременный сбой без вмешательства пользователя. Если ошибка действительно носит временный характер, следующая попытка часто проходит успешно. Для пользователя это выглядит как обычный, пусть и чуть более медленный, ответ. В этом и состоит ценность retry: он сглаживает мелкие инфраструктурные провалы и не даёт каждому кратковременному сбою превращаться в видимую проблему.

Простейшая реализация retry может выглядеть так:

var err error
for i := 0; i < 3; i++ {
    err = makeRequest()
    if err == nil {
        return nil
    }
}
return err

Здесь клиент делает до трёх попыток всего, включая первую. Если сервис временно не ответил из-за скачка нагрузки, сетевой задержки или короткого сбоя на стороне зависимости, одна из следующих попыток может пройти.

Именно поэтому retry используется почти везде: в HTTP-клиентах, библиотеках доступа к базам данных, очередях сообщений и SDK облачных сервисов. Например, в AWS SDK for Go v2 сервисные клиенты по умолчанию используют retry.Standard. В документации AWS указано, что стандартное retry-поведение рассчитано на transient errors и при необходимости может быть переопределено.

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

Когда retry усиливает сбой

Проблема retry в том, что он добавляет нагрузку. Пока сервис работает стабильно, это незаметно. Но если он начал деградировать, повторные запросы ухудшают ситуацию.

Представим простой пример. Сервис получает 10 000 запросов в минуту, и из-за перегрузки 10% завершаются ошибкой. Если клиент делает до трех попыток всего, то первая волна дает еще 1 000 повторных запросов. Если часть из них тоже попадает в сбой, появляется следующая волна. Даже в такой упрощенной модели нагрузка вырастает примерно с 10 000 до 11 100 запросов в минуту — и это без учета того, что реальные клиенты могут повторять запросы неравномерно и на разных уровнях системы. Сервис, который уже не справлялся, получает еще больше работы.

Так возникает feedback loop: ошибка → retry → рост нагрузки → новые ошибки. Под нагрузкой такой механизм быстро раскручивает сбой.

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

Поэтому retry нельзя рассматривать как деталь клиентского кода. Он влияет на общую нагрузку и в момент деградации может стать причиной, почему система не восстанавливается, а проваливается дальше.

Как retry создает каскадные отказы

В микросервисах ситуация усложняется. Запрос идет по цепочке: API → сервис A → сервис B → сервис C. Если C начинает тормозить, B решает, что это временный сбой, и делает retry. A видит, что B отвечает долго — тоже retry. Затем API. Один сбой в нижнем сервисе порождает несколько уровней повторных запросов выше.

Так возникает cascading failure — каскадный отказ. Один деградирующий сервис тянет за собой остальные. Они расходуют соединения, потоки и память на повторные попытки достучаться до компонента, который не справляется. Чем глубже цепочка и агрессивнее retry, тем быстрее система усиливает собственную нестабильность.

Поэтому в распределенных системах retry опасен не сам по себе. Ошибка одного сервиса почти никогда не остается локальной. Через цепочки зависимостей она превращается в проблему всей системы.

Проблемы с таймаутами

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

Слишком короткий timeout приводит к ложным ошибкам. Сервис мог бы ответить за 300-400 мс, но клиент завершает запрос через 100 мс и считает его неуспешным. После этого запускается retry, хотя проблема была не в недоступности, а в том, что сервис временно отвечал медленнее. Вместо одного медленного запроса сервис получает несколько.

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

Поэтому timeout нельзя выбирать «на глаз». Один из рабочих подходов — смотреть на реальную latency сервиса, например на p99, и добавлять небольшой буфер. Но это не универсальная формула: итоговое значение зависит от SLA, сетевой среды, поведения клиента и того, что происходит после срабатывания timeout.

Как правильно реализовывать retry

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

Ниже разберем механизмы, которые помогают сделать retry управляемым: exponential backoff, jitter, circuit breaker, выбор ошибок для повторных попыток и идемпотентность. Вместе они определяют, будет retry снижать влияние временных сбоев или сам станет источником новой волны нагрузки.

Exponential backoff

Самая частая ошибка при реализации retry — повторять запросы сразу или через одинаковые интервалы. Если сервис уже начал отвечать медленнее обычного, такие повторы только усиливают нагрузку. Клиент не дает системе времени восстановиться и сам становится частью проблемы.

Именно поэтому retry обычно сочетают с exponential backoff — схемой, в которой после каждой неудачной попытки задержка перед следующим запросом увеличивается. Например, первая повторная попытка делается через 100 мс, следующая — через 200 мс, затем через 400 мс. Идея простая: чем дольше сохраняется сбой, тем осторожнее должен вести себя клиент.

В простом виде это можно описать так:

sleep = min(cap, base * 2^attempt)

Где:

  • base — базовая задержка;

  • attempt — номер повторной попытки;

  • cap — верхняя граница ожидания.

Ограничение сверху здесь важно. Без него задержка быстро становится слишком большой: после нескольких неудачных попыток клиент может начать ждать секунды или даже десятки секунд между вызовами. На практике это уже не всегда полезно — особенно если запрос пользовательский и у операции есть понятный предел по времени ожидания.

Простейшая реализация выглядит так:

var err error
delay := 100 * time.Millisecond
maxDelay := 2 * time.Second

for i := 0; i < 3; i++ {
    err = makeRequest()
    if err == nil {
        return nil
    }

    time.Sleep(delay)

    delay *= 2
    if delay > maxDelay {
        delay = maxDelay
    }
}
return err

Такой подход уже заметно лучше, чем немедленные повторы или фиксированный интервал между попытками. Он снижает плотность запросов и дает деградирующему сервису шанс восстановиться. Если проблема действительно кратковременная — например, короткий всплеск нагрузки или временный сетевой сбой — следующая попытка может пройти успешно без лишнего давления на систему.

Но у exponential backoff есть важное ограничение: он уменьшает частоту повторов, но не убирает синхронность между клиентами. Если они начали retry одновременно, пики нагрузки просто смещаются по времени. Поэтому одного backoff обычно недостаточно — поверх него почти всегда добавляют jitter.

Jitter: почему одного backoff недостаточно

Exponential backoff делает retry осторожнее, но сам по себе не решает главную проблему: клиенты все еще могут повторять запросы слишком синхронно. Так возникает thundering herd — когда тысячи клиентов одновременно обрушивают вторую волну нагрузки на уже деградирующий сервис. Если много экземпляров сервиса получили ошибку примерно в один и тот же момент, они будут увеличивать задержку по одной и той же схеме: например, ждать 100 мс, потом 200 мс, потом 400 мс. Частота повторов снизится, но нагрузка все равно будет приходить неравномерно — не непрерывным потоком, а сериями всплесков.

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

Чтобы уменьшить эту корреляцию, в backoff добавляют jitter — случайную составляющую в задержке. Идея в том, чтобы разные клиенты ждали немного разное время перед повторной попыткой. Тогда запросы распределяются по времени более равномерно, а нагрузка становится менее пикообразной.

Во всех вариантах ниже сначала считается один и тот же базовый backoff-интервал:

temp = min(cap, base * 2^attempt)

Здесь temp — это не итоговое время ожидания, а верхняя граница задержки для текущей попытки. Разница между вариантами jitter не в том, как считается backoff, а в том, как именно из этого интервала выбирается фактический sleep.

Один из простых вариантов выглядит так:

temp = min(cap, base * 2^attempt)
sleep = temp / 2 + random(0, temp / 2)

Такой подход обычно называют Equal Jitter. Он сохраняет часть задержки от backoff и добавляет к ней случайность. Если temp равен 400 мс, то фактическая задержка будет где-то между 200 и 400 мс. Это уже лучше, чем backoff без jitter: клиенты меньше синхронизируются и реже создают одновременные всплески.

Но у Equal Jitter есть ограничение. Все повторы все равно происходят внутри похожего диапазона. Если тысяча клиентов стартовала почти одновременно, они будут ретраить не в одну и ту же миллисекунду, но все равно в одном и том же временном окне. Для умеренной нагрузки этого часто хватает, но при высокой конкуренции пики остаются заметными.

Более агрессивный вариант — Full Jitter:

temp = min(cap, base * 2^attempt)
sleep = random(0, temp)

Здесь случайной становится вся задержка. Если temp равен 400 мс, то фактический sleep может быть любым — от 0 до 400 мс. Клиенты распределяются по времени сильнее, и повторные запросы приходят к сервису более равномерно. Это снижает вероятность того, что система получит новую волну нагрузки сразу после предыдущей неудачи.

Есть и другие варианты, например Decorrelated Jitter, где следующий интервал зависит не только от номера попытки, но и от предыдущего случайного значения. Он тоже хорошо помогает размывать пики. Для практики здесь важны два вывода:

  • backoff без jitter оставляет кластеры запросов;

  • jitter нужен, чтобы эти кластеры размыть.

Это важно не только в теории. Если retry запускают сотни или тысячи клиентов, отсутствие jitter означает, что они будут действовать синхронно: вместе ошибаться, вместе ждать и вместе снова атаковать сервис. Даже если каждый отдельный клиент ведет себя разумно, в сумме система получает резкие всплески нагрузки.

Поэтому jitter — это не косметическое улучшение backoff, а его естественное продолжение. Backoff снижает частоту повторов, а jitter делает их менее синхронными. Вместе они заметно уменьшают давление на деградирующий сервис.

В коде это может выглядеть так:

var err error
baseDelay := 100 * time.Millisecond  
maxDelay := 2 * time.Second

for attempt := 0; attempt < 3; attempt++ {
    err = makeRequest()
    if err == nil {
        return nil
    }

    delay := baseDelay * time.Duration(1<<attempt)
    if delay > maxDelay {
        delay = maxDelay
    }

    jitter := time.Duration(rand.Int63n(int64(delay)))
    time.Sleep(jitter)
}
return err

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

Если в системе мало клиентов и нагрузка невысока, разница между вариантами jitter может быть почти незаметной. Но в нагруженных системах, где одна и та же зависимость вызывается массово, выбор уже становится важен. В таких условиях Full Jitter обычно дает более ровное распределение повторных запросов, чем Equal Jitter.

Circuit breaker

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

Для таких случаев используют circuit breaker. По сути это защитный слой между сервисом и его зависимостью. Он отслеживает неудачные вызовы и, если понимает, что проблема не кратковременная, временно прекращает новые обращения. Идея в том, чтобы fail fast: быстро вернуть ошибку и не тратить ресурсы на заведомо бесполезный вызов.

Обычно у circuit breaker три состояния:

  • Closed — все работает в обычном режиме, запросы проходят к зависимому сервису;

  • Open — обращения временно блокируются, и клиент сразу получает ошибку;

  • Half-open — после паузы breaker пропускает ограниченное число пробных запросов, чтобы проверить, восстановилась ли зависимость.

Логика переходов довольно простая. Пока ошибок мало, breaker остается в состоянии closed. Если число неудачных вызовов, доля ошибок или время ответа выходят за заданный порог, он переходит в open. После этого система перестает посылать новые запросы в проблемную зависимость на некоторое время. Когда таймер истекает, breaker переводится в half-open и позволяет нескольким запросам пройти. Если они успешны, состояние возвращается в closed. Если нет — breaker снова открывается и дает сервису еще время на восстановление.

Важно, что circuit breaker и timeout решают разные задачи. Timeout ограничивает время ожидания одного конкретного вызова. Circuit breaker ограничивает сам поток обращений к уже проблемной зависимости. Эти механизмы обычно работают вместе: timeout не дает одному запросу зависнуть слишком долго, а breaker не позволяет всей системе бесконечно повторять вызовы туда, где уже явно плохо.

На практике breaker может срабатывать по разным сигналам. Самый простой вариант — по числу последовательных ошибок. Но часто смотрят шире: на error rate, на резкий рост latency, на количество таймаутов за интервал или на комбинацию нескольких метрик. Это важно, потому что деградация не всегда выглядит как «сервис упал». Иногда он просто начинает отвечать слишком медленно, и с точки зрения системы это почти так же опасно.

Еще одна важная вещь — наблюдаемость. Срабатывание circuit breaker — всегда нештатная ситуация: либо зависимость действительно деградировала, либо breaker сработал ложно. В обоих случаях это проблема, которую нужно увидеть и исправить. Circuit breaker не должен быть «черным ящиком». Если он открывается, это нужно видеть в метриках и логах: сколько раз он сработал, как долго был открыт, что стало причиной перехода, помог ли half-open вернуть сервис в нормальный режим. Без этого breaker легко превратится в загадочный механизм, который то спасает систему, то неожиданно режет трафик без понятной причины.

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

Какие ошибки вообще стоит ретраить

Одна из самых частых ошибок при работе с retry — включать его на все подряд. Если сервис ответил ошибкой, это еще не значит, что запрос имеет смысл повторять. Retry полезен только тогда, когда есть разумное ожидание, что следующая попытка может пройти успешнее предыдущей. Если причина сбоя не исчезнет сама через короткое время, повтор только создаст лишнюю нагрузку.

Проще всего думать о retry так: он подходит для временных ошибок, а не для логических или постоянных.

В первую очередь под retry обычно подходят сетевые сбои и таймауты. Если соединение оборвалось, DNS временно не ответил, upstream закрыл сокет или клиент не дождался ответа в пределах timeout, это еще не означает, что операция принципиально невозможна. Такие ошибки часто бывают кратковременными, и повторная попытка действительно может пройти успешно.

Из HTTP-ответов чаще всего ретраят ошибки уровня 5xx, но и здесь не все без разбора. Самые типичные кандидаты — это 503 Service Unavailable и 504 Gateway Timeout. В обоих случаях проблема обычно говорит о перегрузке, временной недоступности или сбое в зависимом компоненте. Иногда к этой группе относят и некоторые 502 Bad Gateway, если известно, что ошибка пришла от промежуточного прокси или балансировщика и носит временный характер.

500 Internal Server Error — самый неоднозначный случай. Формально это server-side error, но причина может быть разной: от временного сбоя в зависимости до бага в коде. Если известно, что 500 возникает из-за transient issues (например, race condition или временная недоступность внутренней зависимости) — retry уместен. Если это логическая ошибка в коде — повтор бессмыслен. На практике 500 часто ретраят, но с осторожностью: небольшим числом попыток и с пониманием, что проблема может быть не временной.

Отдельный случай — 429 Too Many Requests. Формально это код из семейства 4xx, но по смыслу он ближе к сигналу “сейчас слишком рано, попробуйте позже”. Такой ответ как раз часто ретраят, но не сразу, а с backoff. Если сервер возвращает Retry-After, клиенту лучше учитывать его, а не выбирать задержку самостоятельно.

А вот большинство остальных 4xx автоматически ретраить не стоит — они указывают не на временную деградацию системы, а на ошибку в запросе, правах доступа или бизнес-логике. Например: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity. Повторный вызов с теми же параметрами, скорее всего, даст тот же результат. В такой ситуации retry не повышает устойчивость, а лишь создает бессмысленный трафик.

Здесь важно учитывать не только HTTP-код, но и контекст. Один и тот же ответ в разных системах может означать разное. Например, 404 для чтения уже удаленного объекта — это нормальный финальный результат, а для недавно созданного ресурса в eventually consistent системе — потенциально временная ситуация. Поэтому retry редко строят только на списке кодов. Обычно смотрят шире: на тип ошибки, на характер зависимости, на идемпотентность операции и на то, есть ли шанс, что состояние изменится само через короткое время.

Полезное практическое правило такое: ретраить стоит только то, что похоже на transient failure — временный сбой сети, недоступность зависимого сервиса, перегрузку или короткий провал инфраструктуры. Все, что выглядит как ошибка данных, прав, валидации или бизнес-правил, лучше сразу считать финальным ответом.

Даже для временных ошибок retry должен быть ограниченным. Если клиент бездумно повторяет любой 503 или timeout, он легко сам усилит деградацию. Поэтому выбор ошибок для retry всегда работает в связке с backoff, jitter, лимитом попыток и пониманием, насколько безопасен повтор для конкретной операции.

В самом грубом приближении картина обычно такая:

  • Чаще всего ретраят: сетевые ошибки, timeout, 429, 503, 504

  • Чаще всего не ретраят: 400, 401, 403, 404, 422 и другие ошибки, связанные с самим запросом

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

Идемпотентность

Даже если ошибка выглядит временной и запрос формально подходит для retry, остается еще один вопрос: безопасно ли вообще повторять эту операцию. Для чтения ответ обычно очевиден — повторный GET редко создает проблемы. Но для операций, которые меняют состояние системы, все сложнее. Если клиент повторно отправит запрос на создание заказа, списание денег или выпуск документа, система может выполнить действие дважды.

Именно здесь появляется тема идемпотентности. В контексте retry она означает простую вещь: повтор одного и того же запроса не должен приводить к дополнительному побочному эффекту. Иначе временный сетевой сбой может превратиться уже не в инфраструктурную, а в бизнес-проблему — например, в двойной платеж или дублирующийся заказ.

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

Самый распространенный способ сделать такие операции безопаснее — использовать Idempotency-Key. Клиент генерирует уникальный ключ для конкретной операции и отправляет его вместе с запросом (обычно как HTTP-заголовок). Если запрос с этим ключом пришел впервые, сервер выполняет бизнес-логику. Если тот же ключ приходит повторно, сервер возвращает результат первой операции, не выполняя ее заново.

Такой подход используют крупные платформы. Например, Stripe API требует Idempotency-Key для всех POST-запросов, чтобы исключить двойные платежи. AWS SDK автоматически генерирует ClientToken для операций вроде EC2 RunInstances, чтобы retry не создал два экземпляра вместо одного. Обе системы хранят ключи с TTL (обычно 24 часа), чтобы покрыть реалистичное окно повторных попыток.

Идемпотентность — это не отдельный слой поверх API, а сквозное свойство системы. Детали реализации сильно зависят от бизнес-логики: как хранить состояние запроса, как защищаться от race conditions, что делать с late-arriving requests. Stripe и AWS решают эти вопросы по-разному. Подробнее об этом можно прочитать в статьях Stripe: Designing robust and predictable APIs with idempotency и AWS Builders' Library: Making retries safe with idempotent APIs.

Здесь же становится понятно, почему retry и идемпотентность так тесно связаны. Retry отвечает на вопрос: когда и как повторять запрос. Идемпотентность отвечает на другой: что произойдет, если этот повтор все-таки дойдет до сервера еще раз. Без первого клиент может не пережить временный сбой. Без второго система может пережить его слишком дорого.

Поэтому для операций, меняющих состояние, хороший retry почти всегда подразумевает идемпотентность. Если система не может безопасно обработать повтор одного и того же запроса, то любой автоматический retry в этой точке нужно включать очень осторожно — либо не включать вовсе.

Почему retry — архитектурная проблема

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

Именно поэтому retry нельзя проектировать в отрыве от архитектуры. Важно не только то, что делает один клиент, но и то, сколько таких клиентов в системе, как они настроены, синхронизируются ли они по таймаутам, какие сервисы стоят ниже по цепочке и насколько безопасно для них увеличение входящего трафика. Один и тот же retry может быть почти незаметным в небольшом внутреннем сервисе и разрушительным в нагруженной микросервисной системе.

Отдельная сложность в том, что retry меняет поведение системы именно в момент деградации. Когда все работает нормально, его влияние может быть почти незаметным. Но как только зависимость начинает отвечать медленнее, количество повторных запросов растет, увеличивается конкуренция за соединения и потоки, начинает плыть latency, а локальная проблема быстро превращается в системную. В этот момент retry уже нельзя рассматривать как удобную опцию клиента — он становится частью общей модели отказа.

Поэтому retry — это часть стратегии отказоустойчивости, а не просто техника повторного вызова. Его нужно рассматривать вместе с timeout, circuit breaker, ограничением нагрузки, идемпотентностью операций и наблюдаемостью. В больших системах к этому иногда добавляют и retry budget — ограничение на общий объем повторных запросов, чтобы они не начинали доминировать над полезным трафиком. Если эти механизмы настроены отдельно друг от друга, система может выглядеть разумно на бумаге, но вести себя плохо под реальной нагрузкой.

На практике это значит, что важно смотреть не только на долю ошибок. Не менее важно понимать, сколько запросов появляются повторно, как меняется latency при включенном retry, растет ли число таймаутов, не возникает ли корреляция между клиентами и не создает ли retry новую волну нагрузки в тот момент, когда зависимость и так не справляется.

Практический чек-лист

Перед тем как включать retry в продакшене, полезно пройтись по нескольким вопросам.

  • Ограничьте число попыток и общее время повторов. Retry без верхней границы легко превращается в источник лишней нагрузки. Обычно хватает двух-трех дополнительных попыток, если ошибка действительно временная.

  • Не ретрайте все подряд. Повторные попытки уместны для сетевых ошибок, timeout, 429, 503, 504 и похожих временных сбоев. Ошибки валидации, прав доступа и бизнес-логики обычно повторять бессмысленно.

  • Используйте exponential backoff и jitter вместе. Один backoff уменьшает частоту повторов, но без jitter клиенты все равно могут создавать синхронные пики нагрузки.

  • Подбирайте timeout по реальному поведению сервиса. Он должен учитывать latency под нагрузкой, SLA и последствия повторной попытки. Слишком короткий timeout создает ложные ошибки, слишком длинный — удерживает ресурсы слишком долго.

  • Добавляйте circuit breaker там, где зависимость может тянуть за собой всю цепочку. Если сервис явно деградирует, системе часто выгоднее быстро вернуть ошибку, чем продолжать тратить ресурсы на почти гарантированно неуспешные вызовы.

  • Проверяйте идемпотентность операций. Для запросов, меняющих состояние, retry безопасен только тогда, когда повтор не создает новых побочных эффектов или защищен идемпотентностью на стороне сервера.

  • Следите за метриками retry отдельно от обычного трафика. Полезно видеть долю повторных запросов, число таймаутов, изменения latency, частоту открытия circuit breaker и поведение зависимостей в момент деградации.

  • Смотрите на retry как на свойство всей системы, а не одного клиента. Если одна и та же зависимость вызывается массово, поведение сотен клиентов важнее, чем логика любого отдельного вызова.

Вывод

Retry и timeout сами по себе не делают систему надежной. Они помогают только тогда, когда встроены в архитектуру осознанно и ограничены понятными правилами. Повторный запрос действительно может сгладить кратковременный сбой, но в момент деградации он так же легко может усилить нагрузку, увеличить latency и ускорить распространение ошибки по цепочке зависимостей.

Поэтому retry нельзя рассматривать как локальную настройку клиента. Это часть общей стратегии отказоустойчивости. Здесь важно все: какие ошибки мы считаем временными, сколько попыток разрешаем, как распределяем их во времени, когда останавливаемся и безопасно ли вообще повторять конкретную операцию.

Хороший retry почти никогда не работает в одиночку. Рядом с ним появляются timeout, backoff, jitter, circuit breaker, ограничения по типам ошибок и идемпотентность для операций, которые меняют состояние. Только в такой связке повторные попытки действительно помогают системе переживать сбои, а не превращаются в источник новой волны нагрузки.

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