Привет, я Денис, технический эксперт в Cloud.ru. В какой-то момент своей карьеры мне пришлось разобрать много подобных инцидентов и написать по ним RCA. Разбирая очередной анализ, я увидел повторяющийся паттерн: девятки теряются не в одном месте, а понемногу на каждом уровне: в сети, базе данных, логике приложения и тд. В итоге из красивых 99.99% SLA в облаке получается реальная доступность системы, которая уже не выглядит так хорошо и зачастую не вписывается в error budget. 

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

Что такое SLA и как оно считается

SLA (Service Level Agreement) — это договоренность между поставщиком и клиентом об уровне доступности сервиса. Проще говоря, сколько времени сервис должен работать без сбоев и какие условия считаются «недоступностью».

Доступность считается по простой формуле: 
uptime / total time

Например:

  • 99% — это ~3.65 дня простоя в год;

  • 99,5% — ~4ч 23м простоя в год;

  • 99.9% — ~8.7 часов в год;

  • 99.99% — ~52 минуты в год.

На бумаге разница между 99.9% и 99.99% выглядит небольшой, всего одна девятка. Но в реальности — это 8 часов против 52 минут простоя в год. И эти дополнительные 7 часов — это обычно именно те моменты, когда приходят пользователи, запускаются рекламные кампании или проводятся платежи, т.е. моменты наивысшей нагрузки.

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

Почему SLA облачного сервиса ≠ SLA вашего приложения

Основная проблема — композиция и зависимости.

Если ваша система состоит из 10 кубиков, то есть сервисов с SLA 99.95%, итоговая доступность системы будет ниже, потому что вероятность отказа накапливается. Это не интуитивно и кажется, что если все сервисы по отдельности надежны, то и система должна быть надежной — на практике это не так.

Простой пример.

Дано: 10 зависимых сервисов, каждый по 99.95%.

Итоговая доступность вашей системы уже заметно ниже из-за перемножения вероятностей:

(0,9995)^10 ~ 0,995012 (или 99,5%), т.е. уже не 52 минуты, а 4 с лишним часа. Для систем уровня business и mission critical это недопустимо.

И это только математика, в реальной жизни все хуже.

Добавим к этому еще несколько моментов, которые расходуют бюджет на ошибки:

  • каскадные отказы: перегрузка и падение БД → упало API → упал фронт;

  • разные методы расчета SLA: провайдер считает доступность виртуальных машин, а вы — доступность API;

  • человеческий фактор: деплой конфигурации с ошибкой.

Типичный сценарий из практики:

  1. База перегружена и начинает отвечать медленно.

  2. Приложение не имеет тайм-аутов.

  3. Очереди начинают копиться.

  4. Падает пул соединений.

  5. Умирает весь сервис. 

При этом ни один компонент формально не упал. В итоге получается, что SLA вашего приложения почти всегда ниже SLA любого отдельного облачного компонента. 

Как построить отказоустойчивую архитектуру

Базовые принципы

Первое, что я усвоил на практике — без базовых принципов никакие технологии не спасают. Можно использовать Kubernetes, Kafka или любые другие managed-сервисы и все равно ловить даунтаймы.

Что это за принципы:

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

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

Изоляция. Падение одного компонента не должно валить все.

Graceful degradation. Лучше частичная работа, чем полный отказ. Нет доступа к базе – отдай кеш, нет рекомендаций — покажи базовый список и т.д.

Пользователь чаще готов терпеть деградацию, чем полный отказ.

Сетевой уровень: используйте балансировщики нагрузки

Без балансировщика отказоустойчивость невозможна. Он распределяет трафик между инстансами целевой группы и включает механизм проверки их работоспособности (health check). Как только нода перестает отдавать HTTP-код 200 на HC-запрос, балансировщик исключает эту ноду из балансировки.

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

Нужен баланс. Для себя я выработал правило: для нового сервиса я выставляю параметры так, чтобы отказ фиксировался за десятки секунд, а не минуты. Потом при необходимости корректирую.

Уровень вычислений: Kubernetes

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

Deployment — поддерживает заданное количество реплик приложения и обеспечивает контролируемое обновление версий.

Horizontal Pod Autoscaler (HPA) — автоматически изменяет количество подов в зависимости от нагрузки.

Pod Disruption Budget (PDB) — ограничивает число подов, которые можно одновременно добровольно вывести из работы, и тем самым помогает сохранить доступность приложения во время обслуживания кластера.

Pod anti-affinity — позволяет распределять поды по разным нодам.

Topology Spread Constraints — помогает равномерно распределять поды между зонами доступности и другими доменами размещения.

Применяя эти механизмы вместе, вы значительно снижаете риск отказа микросервисов и, как следствие, недоступности системы.

Уровень данных: PostgreSQL

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

У меня есть две рекомендации на этот счет.

Всегда используйте отказоустойчивую топологию кластера для критичных систем. Минимальная топология, подходящая для критичных систем — мультизональная Primary/Standby на две ноды. Но я бы рекомендовал использовать мультизональную трехузловую Primary + 2 Standby. Сейчас объясню, почему.

Помните историю про ведра и воду? Так вот, если запись и чтение информации разделены, т. е. запись идет в мастер, а чтение из реплики, то отказ реплики приведет к тому, что вся читающая нагрузка ляжет на оставшийся мастер. А если средняя нагрузка в момент отказа >50% на каждую ноду? Получим почти гарантированную деградацию и отказ мастера с дальнейшим даунтаймом всей системы. 
 
В случае с двумя репликами при отказе одной из них читающая нагрузка переедет на вторую реплику и кластер останется в рабочем штатном состоянии. 

Также полезно будет начать мониторить ситуации, когда средневзвешенная нагрузка на CPU любой из нод в течение 5 минут >80%. Если таких ситуаций становится все больше — пора запланировать скейлинг флэйвора. Важный момент: скейлить нужно в момент наименьшей нагрузки, так как изменение флэйвора ведет к поочередному перезапуску всех нод кластера.

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

Уровень обмена сообщениями: Kafka как буфер между сервисами

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

Как и любой критичный компонент, Kafka требует отдельной настройки отказоустойчивости. Для продакшен-систем обычно разворачивают кластер как минимум из трех брокеров, задают фактор репликации топиков больше 1 (для критичных топиков, как правило, 3), настраивают default.replication.factor, а также min.insync.replicas. Чтобы подтверждение записи действительно означало сохранение сообщения на достаточном числе реплик, продюсеры должны использовать настройку acks=all.

Кроме того, важно учитывать семантику доставки сообщений. Потребители должны быть готовы к повторной доставке и повторной обработке сообщений, а критичные операции по возможности быть идемпотентными. На стороне продюсеров также обычно включают идемпотентность (enable.idempotence=true), чтобы снизить риск дублей при ретраях.

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

Уровень приложения

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

Минимальный набор:

  • retries с exponential backoff. Временные ошибки нужно обрабатывать повторными попытками, но с растущей задержкой и ограничением по числу повторов, чтобы не усиливать деградацию зависимого сервиса;

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

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

  • идемпотентность. Повторное выполнение одной и той же операции не должно приводить к повторному побочному эффекту. Это критично при retries, повторной доставке сообщений и failover-сценариях.

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

Наблюдаемость

Когда все уровни системы настроены, настало время подумать о ее observability. Да, да, тот самый царь-даш с пресловутыми SLO, SLI, error rate, error budget и прочими SRE-прелестями.  

Расскажу, как я обычно проектирую подобные дашборды.

Делаю дашборд многоуровневым. 

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

Второй уровень — состояние ключевых компонентов системы: Kafka, PostgreSQL, compute, балансировщиков и других базовых зависимостей. Этот слой тоже не должен быть перегружен деталями: достаточно одной-двух метрик на компонент, которые показывают, способен ли он обрабатывать нагрузку. Например, доступность записи и чтения для managed PostgreSQL или количество нод в состоянии NotConnected для managed Kubernetes.

Третий уровень — детальные технические метрики по конкретному компоненту или сервису. Сюда уже относятся ресурсы и производительность: CPU, RAM, диски, сеть, latency, очереди, соединения и другие показатели, которые помогают локализовать причину деградации.

Такой подход обычно заметно ускоряет troubleshooting, особенно если система состоит из десятка и более компонентов. Сначала становится понятно, что проблема действительно есть, затем — где именно она возникла, и только после этого имеет смысл переходить к логам, трассировкам и поиску root cause.

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

Когда дашборд здоровья системы построен, можно переходить к следующему шагу – проактивному мониторингу. Ведь давайте честно, никто не любит подскакивать ночами и чинить упавшую систему. Гораздо лучше настроить алертинг так, чтобы он начал отлавливать «желтое состояние» — это такое состояние, которое говорит, что тренд на деградацию уже начался, но сама эта деградация еще не наступила. А значит, есть время в спокойном режиме покопать причины. Это, я считаю, высший пилотаж. И волки сыты система в порядке, и ваша менталка тоже :)

Выводы

И вот мы плавно подошли к выводам. Еще раз пройдемся по основным тезисам:

  • SLA облака — это потолок, а не гарантия вашего SLA;

  • реальную доступность определяет архитектура и логика приклада, а не цифры в SLA провайдера;

  • отказоустойчивость — это не разовая задача, а постоянный процесс улучшения, тестирования и пересмотра архитектуры.

Всем highload!