Привет, Хабр. Меня зовут Кирилл Борисов, я SRE в Ситуационном центре. Я часто видел, как неправильное использование паттернов отказоустойчивости архитектуры или их игнорирование приводит к серьёзным последствиям. Поэтому хочу рассказать, как обеспечить надёжность в условиях, когда может упасть любой микросервис.

Так выглядит микросервисная архитектура Uber с обозначением всех взаимосвязей. У каких-то компаний ИТ-системы скромнее, у каких-то — обширнее, но суть в том, что микросервисы не живут в вакууме, они обязательно друг с другом взаимодействуют, обращаются в базы данных, в Kafka и т. д. Всё это нужно отслеживать.

А это график реального сбоя у Amazon, произошедшего в 2021 году. Они накатили обновление (по моему опыту, 90 % проблем — это какой-то апдейт) и увеличили ёмкость одного из ЦОДов. И во внутренней сети случился сбой. Клиенты обращались за network pool-ами, получали отказ и начинали раз за разом повторять обращения. Возник шквал запросов, который положил Amazon, а вместе с ним Slack, Netflix, Epic Games и сервисы многих других компаний. Причина была в том, что в одном из компонентов сработал retry и не было rate limit. Фактически, они сами себя задидосили.

Типичные проблемы

Большие инциденты никогда не происходят из-за огромных проблем, причины всегда мелкие, незначительные. Например, высокая задержка в одном из сервисов. К нему обращаются, сервис не отвечает, переполняется connect-pool, и всё падает. Или, например, сетевые проблемы: отвалился канал в ЦОД, инженер быстро перевоткнул кабель, чтобы никто не заметил. Или возникла частичная деградация сервисов, что привело к снежному кому большого сбоя.

Всевозможные мелкие проблемы приводят к крупным отказам двух типов:

  1. Метастабильное состояние, при котором система не выполняет своей функции после устранения триггера сбоя. Яркий пример — сборщик мусора в Twitter. У них был сервис на Java, в него пришёл трафик на уровне 100 RPS. Затем трафик вырос, и JVM начала чаще запускать сборщик мусора. А у него есть особенность: на доли миллисекунды он останавливает обработку всех запросов, чтобы очистить память. Из-за этого сервис начал отвечать дольше. А поскольку количество сессий сборки мусора увеличилось, стали расти очереди запросов. И даже если бы остановили трафик, накопившиеся запросы пришлось бы ещё долго разгребать. То есть система пришла в метастабильное состояние: функционировала, но новые запросы не обрабатывала, потому что своей очереди ждало множество старых.

  2. Каскадный сбой — это когда один компонент падает и запускает цепную реакцию, парализующую всю систему. Очень распространённое явление. Один из интересных примеров произошел в Microsoft в 2018 году. Отказала система охлаждения в ЦОДе — штатная ситуация: перегрев, серверы автоматически выключились. Но в результате возник сбой в управлении, и выключились вообще все серверы, в том числе управлявшие кластерами. Из-за этого отключился Azure Resource Manager: пропал мониторинг, возможность восстановления и миграции виртуальных машин.

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

Паттерны отказоустойчивости

Повторные запросы (Retry Pattern)

Сеть нестабильна. В сервисах часто встречаются различные ошибки:

  • 110 Connection timeout

  • 100 Network is down  

  • 111 Connection refused

  • 504 gateway timeout

  • 503 service unavailable

  • 500 internal server error

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

Допустим, сервис А обращается к сервису В. Тот отвечает ошибкой: network down, connection refused или чем-то другим. Тогда сервис А шлёт ещё один запрос. В ответ снова ошибка. Делаем ещё один запрос, и, наконец, получаем ответ. То есть этот паттерн подразумевает отправку повторных запросов, пока не получим корректный ответ. Такой подход помогает при коротких сбоях, однако он опасен без jitter и может привести к возникновению лавины запросов.

Как сделать так, чтобы повторы не перегружали систему? Для этого используют exponential backoff: каждый следующий повторный запрос через увеличенный промежуток времени. Например, повторили первый раз, через 20 секунд — второй раз, ещё через 40 секунд — третий раз, и так далее. Это можно сравнить с тем, как вы пытаетесь достучаться до коллеги: сначала спросили, он не отвечает, вы повторили погромче, а через какое-то время крикнули, и тогда он вам ответит.

Второй способ обезопасить паттерн retry: добавить jitter (случайную задержку), особенно в сочетании с exponential backoff. Это позволит ещё больше размыть запросы во времени.

Также можно ограничить общее количество повторных запросов и добавить для них общий таймаут. Если сервис несколько раз не отвечает, то не стоит больше стучаться: у него серьёзный сбой. Как нет смысла 20 раз кричать коллеге, после первого раза проще подойти и похлопать по плечу.

Формула exponential backoff с jitter:

delay = min(base_delay (2^attempt), max_delay) jitter_factor

где:

jitter_factor = random(0.5, 1.5) # полный jitter

или

jitter_factor = 1 + random(-0.1, 0.1) # равный jitter

Типы jitter:

  • Full Jitter: delay = random(0, calculated_delay)

  • Equal Jitter: delay = calculated_delay/2 + random(0, calculated_delay/2)

  • Decorrelated Jitter: delay = random(base_delay, previous_delay * 3)

Не все запросы целесообразно повторять. Повторять можно GET-запросы, они ничего не ломают. А с POST и PUT всё сложнее. В этом случае рекомендуется использовать ключи идемпотентности. Это уникальные идентификаторы, который мы добавляем к запросам. Даже если сервер ответит ошибкой, он может обработать запрос. Например, чтобы не было ситуаций, когда в интернет-магазине кто-то трижды обратился к платёжному сервису и трижды оплатил один и тот же заказ.

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

Мониторинг и метрики отказоустойчивости

Для эффективной работы паттернов отказоустойчивости критически важно отслеживать правильные метрики.

Ключевые метрики для Retry Pattern:

  • Количество повторных попыток по сервисам.

  • Success rate после retry и первой попытки.

  • Время выполнения с учётом retries.

  • Распределение ошибок по типам.

 Чек-лист для Retry Pattern:

  • [ ] Используется экспоненциальный backoff?

  • [ ] Применён jitter для рандомизации?

  • [ ] Ограничено максимальное количество попыток?

  • [ ] Настроены метрики отказов повторных запросов?

Эти меры помогут сгладить пики повторных запросов.

Таймауты (Timeout Pattern)

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

Service A идёт в Service B. Запрос был некорректный и отбился по стандартному таймауту — одной секунде. Мы переделали запрос, отправили снова, и на этот раз быстро получили корректный ответ. Но в сумме набралось 1020 мс. Пример искусственный, но идею, думаю, вы поняли.

Как можно улучшить ситуацию:

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

  • Deadline propagation. Очень удобный инструмент, хотя настраивать его не слишком просто. Идея такая: при обращении к сервисам вы не просто задаёте себе таймаут, но ещё и при его превышении просите сервис больше не обрабатывать ваш запрос. Это полезно в случаях, когда вы не хотите долго ждать ответ и впустую тратить на обработку ресурсы сервиса и оборудования.

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

  • Настроить таймаут по 99 или 95 перцентилю. Мне больше нравится 95, он более щадящий. Это статистическая метрика, отражающая долю запросов, которые обрабатываются быстрее заданного временного порога. Например, быстрее 500 мс. Не нужно использовать среднее значение, потому что вы будете отбрасывать по таймауту многие запросы, которые могли бы обработать.

  • Согласованность таймаутов. Без этого никак. Если в цепочке взаимодействий между сервисами все таймауты будут разные, это может привести к излишнему отбрасыванию запросов. Старайтесь согласовывать таймауты, в идеале — делать их одинаковыми.

Чек-лист для Timeout Pattern:

  • [ ] Таймауты основаны на реальных задержках?

  • [ ] Используются перцентили для настройки?

  • [ ] Таймауты дифференцированы по типу запросов?

  • [ ] Журналируются таймауты и причины?

Автоматический выключатель

Допустим, вы копаетесь в логах и замечаете, что в какие-то моменты сервис работает некорректно. Например, резко возрастает нагрузка на процессор, или взлетает количество операций ввода-вывода, или возникает волна ошибок 500. А вы этому сервису шлёте повторные запросы, которые только ухудшают ситуацию. Сервис цепляет какую-нибудь базу данных, которая живёт в кластере и тянет за собой другую базу, и т. д. Короче, происходит каскадный сбой.

Для решения этой проблемы можно настроить автоматический выключатель (circuit breaker), чтобы спокойно разбираться с возникающими сбоями, не отправляя трафик на захворавший сервис. Например, для этого можно автоматически отключать зависимости, а через некоторое время проверять его состояние: таймаут, /health, мелкие тестовые запросы. Если сервис опять не ответил, сбрасываем таймаут и снова ждём. В Kubernetes для этого служат зонды readiness-liveness.

Также можно применять состояние half-open, оно очень похоже на канареечные релизы: сначала пускаете 5 % трафика, проверяете работу сервиса, потом 10 %, 20 %, 30 %, 100 %.

Чек-лист для Circuit Breaker:

  • [ ] Есть ли fallback? Всегда возвращайте клиенту не ошибку, а понятное сообщение.

  • [ ] Применяется Half-Open state?

  • [ ] Поддерживается прогрессивное восстановление?

  • [ ] Есть оповещения на срабатывания выключателя?

Ограничение количества запросов (Rate Limiting)

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

Для защиты от подобных ситуаций применяют ограничение частоты запросов (rate limit). Очень эффективный инструмент: просто отрезают какое-то количество трафика.

Устанавливайте ограничения на клиентах и серверах. Например, сервис А говорит сервису Б, что будет обращаться к нему не чаще 100 раз в секунду, и сервис Б говорит сервису А, что будет принимать запросы не чаще 100 раз в секунду.

Устанавливайте burst limit. Это механизм постепенного отбрасывания трафика выше какого-то предела, для сглаживания всплесков.

Алгоритмы Rate Limiting:

  1. Token Bucket. Самый популярный. Ведро постоянно пополняется токенами с заданной скоростью. Запрос проходит, если есть токен.

  2. Leaky Bucket. Запросы поступают в ведро и вытекают с постоянной скоростью. Если ведро переполнено, запросы отбрасываются.

  3. Fixed Window. Подсчёт запросов в фиксированных временных окнах (например, 100 запросов в минуту).

  4. Sliding Window Log. Точное отслеживание времени каждого запроса в скользящем окне.

  5. *Sliding Window Counter. Компромисс между точностью и производительностью.

Сравнение алгоритмов:

Token Bucket:    [+] Поддержка burst  [-] Сложность реализации  

Fixed Window:    [+] Простота         [-] Эффект границы окна

Sliding Window:  [+] Точность         [-] Потребление памяти

Используйте разные ограничения для разных клиентов.

Чек-лист для Rate Limiting:

  • [ ] Есть ли лимиты на внешнем периметре (API-шлюзы, Ingress)?

  • [ ] Есть ли отдельный лимит для фоновых задач и скриптов?

  • [ ] Есть ли мониторинг rate-limiting событий (графики, оповещения)?

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

Продвинутые техники отказоустойчивости

Bulkhead Pattern (Паттерн Переборок)

Bulkhead pattern — это изоляция ресурсов для предотвращения каскадных сбоев. Название пришло из судостроения: корпус корабля делят переборками на отсеки, чтобы затопление одного отсека не потопило весь корабль.

Принцип работы

Основная идея: разделить системные ресурсы на независимые пулы:

  • Thread pools: отдельные пулы потоков для разных операций.

  • Connection pools: изолированные пулы подключений к БД.

  • Memory allocation: выделенная память для критичных компонентов.

  • CPU resources: распределение процессорного времени.

Типы изоляции

1. По критичности:

Критичные операции:  [ThreadPool-Critical: 50 threads]

Обычные операции:    [ThreadPool-Normal: 30 threads]  

Фоновые задачи:      [ThreadPool-Background: 10 threads]

2. По клиентам:

VIP клиенты:         [ConnectionPool-VIP: 20 connections]

Обычные клиенты:     [ConnectionPool-Regular: 50 connections]

Внутренние сервисы:  [ConnectionPool-Internal: 10 connections]

3. Географическая изоляция:

US-East region:      [Resources-US: CPU, Memory, Network]

EU-West region:      [Resources-EU: CPU, Memory, Network]

Asia-Pacific:        [Resources-APAC: CPU, Memory, Network]

Лучшие практики Bulkhead

  • Правильное разделение: группируйте операции по критичности, а не по функциональности.

  • Мониторинг: отслеживайте загрузку каждого пула отдельно.

  • Настройка размеров: размеры пулов должны соответствовать нагрузке.

  • Fallback стратегии: предусмотрите, что делать при переполнении пулов.

  • Тестирование изоляции: регулярно проверяйте, что перегрузка одного пула не влияет на другие.

Чек-лист для Bulkhead Pattern:

  • [ ] Определены критичные и некритичные операции.

  • [ ] Настроена изоляция пулов соединений/потоков.

  • [ ] Реализован мониторинг для каждого пула.

  • [ ] Настроены оповещения на переполнение пулов.

  • [ ] Изоляция протестирована под нагрузкой.

Adaptive Timeout (Адаптивные таймауты)

Adaptive Timeout — это динамическое изменение значений таймаутов на основе текущей производительности системы. Вместо использования статических значений, таймауты адаптируются к реальным условиям.

Проблема статических таймаутов

Статические таймауты имеют недостатки:

  • Слишком короткие → много ложных срабатываний.

  • Слишком длинные → медленная реакция на сбои.

  • Не учитывают текущее состояние системы.

  • Одинаковые значения для разного времени суток.

Алгоритм адаптивного таймаута

Базовая формула:

adaptive_timeout = percentile(recent_response_times, P) * multiplier + buffer

где:

  • P — 95 или 99 перцентиль;

  • multiplier = 1,2-2,0 (запас на вариативность);

  • buffer — минимальный буфер времени;

Алгоритм экспоненциального сглаживания:

new_timeout = α current_response_time + (1-α) previous_timeout

где α = 0,1-0,3 (коэффициент сглаживания)

Лучшие практики Adaptive Timeout

  1. Достаточно данных: не обновляйте таймаут на основе < 10-20 измерений.

  2. Сглаживание: используйте экспоненциальное сглаживание для стабильности.

  3. Границы: всегда устанавливайте min/max значения.

  4. Разные сервисы: у каждого сервиса свой адаптивный таймаут.

  5. Мониторинг: отслеживайте изменения таймаутов и их эффективность.

Чек-лист для Adaptive Timeout:

  • [ ] Настроено измерение времени ответов для каждого сервиса.

  • [ ] Установлены разумные min/max границы таймаутов.

  • [ ] Реализовано экспоненциальное сглаживание.

  • [ ] Добавлен мониторинг изменений таймаутов.

  • [ ] Протестирована работа при различных нагрузках.

Backpressure Handling (Обработка обратного давления)

Backpressure — это механизм контроля нагрузки, когда медленный получатель сигнализирует отправителю о необходимости снизить скорость передачи данных. Это критично для предотвращения переполнения буферов и сбоев системы.

Проблема без Backpressure

Что происходит при отсутствии контроля:

  • Быстрый producer → медленный consumer.

  • Накопление в очередях и буферах.

  • Потребление памяти растёт вплоть до Out Of Memory.

  • Увеличение задержки обработки.

  • Потеря данных при переполнении.

Стратегии Backpressure

1. Push-based (проталкивание):

Producer → [Queue] → Consumer

           ↑ очередь растёт

Проблема: consumer не может контролировать скорость.

2. Pull-based (вытягивание):

Producer ← [Queue] ← Consumer запрашивает данные

           ↑ контролируемый размер

Решение: consumer запрашивает только то, что может обработать.

3. Adaptive (адаптивная)

Producer ⇄ [Queue] ⇄ Consumer

           ↑ динамический размер

Комбинация: автоматическое замедление producer-а.

Лучшие практики Backpressure

  1. Выбор стратегии: block для критичных данных, drop для метрик и логов.

  2. Мониторинг: отслеживайте rejected- и dropped-элементы.

  3. Graceful degradation: возвращайте клиентам понятные ошибки.

  4. Адаптивность: динамически изменяйте стратегию по нагрузке.

  5. Circuit breaking: комбинируйте с circuit breaker для защиты.

 Чек-лист для Backpressure Handling:

  • [ ] Определена стратегия обработки перегрузки (Block/Drop/Adaptive).

  • [ ] Реализован мониторинг размеров очередей и отклоненных элементов.

  • [ ] Настроены meaningful HTTP status codes (503, 429).

  • [ ] Добавлены заголовки Retry-After для клиентов.

  • [ ] Протестирована работа под высокой нагрузкой.

Чек-лист готовности к production:

  • [ ] Все критичные интеграции имеют таймауты.

  • [ ] Настроены retry для идемпотентных операций.

  • [ ] Внедрены circuit breaker для внешних API.

  • [ ] Работает rate limiting на входе.

  • [ ] Настроены оповещения на ключевые метрики.

  • [ ] Проведено chaos testing.

  • [ ] Команда обучена работе с новыми инструментами.

  • [ ] Есть runbook для типовых инцидентов.

Резюме

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

Каждый инструмент — потенциальный источник ошибок, если неправильно настроен. Можно слать повторные запросы самим себе, можно выставить такие таймауты, что ответа не дождёшься или отбросишь множество полезных запросов, и т. д.

Но главное — все эти паттерны нужно не просто внедрить, а проверить в боевых условиях через chaos engineering. Только в искусственно созданных сбоях вы поймёте, как поведёт себя система: выдержит ли rate limiting, правильно ли сработает circuit breaker, не обрушит ли retry ваш сервис, не окажутся ли таймауты слишком жёсткими или мягкими.

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

Что бы вы ни создавали, спрашивайте себя: «Что будет, если всё пойдёт не так?», и проверяйте это не на клиентах, а заранее, в контролируемом эксперименте.

Комментарии (0)