Паттерны Backpressure и Circuit Breaker
С внедрением кеширования приложение ленты новостей успешно растёт и развивается. Пользователям нравится, как быстро они получают свежие новости. Они рассказывают о проекте своим друзьям, и те тоже начинают пользоваться продуктом. Всего за несколько месяцев количество пользователей выросло с сотни до ста тысяч! Теперь бизнес заработает много денег — стейкхолдеры и сотрудники будут довольны.
Всё хорошо, но… спустя несколько дней команда получает срочное сообщение от CEO: серверы упали, инфраструктура не работает, а пользователи больше не получают актуальных новостей. Необходимо срочно разобраться, что произошло.
Метрики нагрузки
Команда садится анализировать логи сервера и довольно быстро понимает главную проблему: нагрузка на приложение выросла в разы.
✏️ Нагрузка — это интенсивность взаимодействия пользователя с приложением. Чем более активны пользователи и чем их больше, тем выше нагрузка. Приложения с высокой нагрузкой называют высоконагруженными (high-load) приложениями.
Но кроме расплывчатого определения, нагрузку можно формально характеризовать конкретными техническими метриками. Самыми важными из них являются:
- DAU (Daily Active Users) — количество пользователей в день. Также часто используют метрику MAU — Monthly Active Users.
- RPS (Requests Per Second) — количество запросов, которые сервер приложения обрабатывает в секунду.
- QPS (Queries Per Second) — количество запросов в секунду к базе данных.
Пиковая нагрузка, как правило, превышает среднюю в два раза. При поддержке высоконагруженных систем ориентироваться следует именно на пиковую нагрузку.
Что произошло с метриками нагрузки?
- DAU вырос до 100 000 активных пользователей, то есть в тысячу раз.
- Количество запросов обычно получают в результате мониторинга метрик. А пока мониторинг не настроен, предположим, что каждый пользователь делал 5 запросов в секунду. В таком случае показатель RPS вырос с 500 до 500 000 запросов в секунду.
- После грамотной настройки кеширования лишь один запрос из 100 пропускал кеш и шёл напрямую в базу данных. Значит, старое значение QPS было 5, а новое — 5 000.
При взгляде на стремительно выросшие метрики становится очевидно, что существующая инфраструктура больше не справляется с нагрузкой. Один сервер на Node.js и единственный инстанс PostgreSQL, пусть и запущенные на мощном многоядерном компьютере, оказались физически не способны обрабатывать столько запросов одновременно.
Влияние высокой нагрузки на опыт пользователей:
- Пользователи могут столкнуться с долгим временем ответа сервера или вовсе с обрывом запроса по тайм-ауту. Никто не хочет ждать загрузки страницы по 5 минут. Это ухудшит их опыт — они могут перейти на использование продукта конкурентов.
- Пользователи могут потерять важные данные. Например, если запрос на запись в базу данных не был правильно обработан.
- Такую систему гораздо сложнее поддерживать: пользователи будут медленнее получать обновления и новые фичи.
Для решения проблемы нужно внедрять паттерны снижения нагрузки.
Паттерны снижения нагрузки
В предыдущих уроках вы уже работали с масштабированием инфраструктуры и приложения. Но часто механизмов кеширования, распределённой обработки и очередей недостаточно, чтобы выдержать поступающую синхронную и асинхронную нагрузку. Может потребоваться ограничить поток данных и предотвратить обслуживания пользователей, чтобы избежать падения всей системы. Для этого вы познакомитесь с паттернами, которые снижают вероятность отказа, — Backpressure и Curicuit Breaker.
Чтобы лучше понять их логику работы, проведём аналогию с механообрабатывающим цехом. Представьте, цех изготавливает детали, которые потом идут на сборку. Какие стратегии могут быть, если цех-производитель выпускает деталей больше, чем сборочный цех может собрать? В таком случае он может:
- замедлять выпуск деталей,
- выбрасывать детали,
- складывать детали на склад,
- создавать новый сборочный цех.
Выбрасывать произведённую продукцию — худший вариант.
Создать новый цех и производить больше продукции — вариант наиболее предпочтительный. Но постройка нового цеха займёт время. Поэтому наилучшим способом будет на время замедлить работу и складывать часть деталей на склад. Параллельно запланировать постройку нового цеха — масштабирование.
Вернёмся в мир IT: такая же ситуация может быть, например, с файлом, который вы хотите читать и записывать одновременно. Чтение происходит быстрее, чем запись (200 Мб/с против 150 Мб/с), поэтому часть данных придётся складывать в буфер — по 50 Мб каждую секунду.
Теперь представьте, что вы делаете это с файлом объёмом 10 ГБ. К тому времени, когда вы полностью прочитаете файл, у вас будет буфер объёмом 2 ГБ, который нужен для завершения записи.
Или один сервер засыпает сообщениями другой быстрее, чем тот может обработать.
На схеме вы можете увидеть, как влияет нагрузка на сервер с течением времени. Запросы, которые не успевает обработать сервер B, — 25 RPS (requests per second), за час превращаются в 90 000 RPH (requests per hour). В результате сервер B может выйти из строя.
Идеальный вариант, когда сервер B контролирует скорость отправки запросов сервером A. Но это не всегда возможно: если сервер A обрабатывает запросы от имени пользователя, вы не всегда можете сообщить пользователю о замедлении или контролировать его. Поэтому полезно иметь буфер на стороне сервера, чтобы лучше распределять нагрузку на потоки и не влиять на других отправителей запросов.
Рассмотрим, как реализуется снижение нагрузки на сервер с помощью паттерна Backpressure.
Паттерн Backpressure
✏️ Паттерн Backpressure — удобный способ ограничить давление или противодействовать потоку данных. Он обеспечивает обратную связь, с помощью которой регулируется скорость работы производителя данных в соответствии со скоростью работы потребителя. Без этой связи быстрый производитель может заполнить буфер потребителя или, если буфер неограничен, исчерпать всю оперативную память.
Чтобы получать представление о работоспособности системы, выявлять узкие места и настраивать механизм обратного давления, важно проводить мониторинг и собирать показатели метрики, в том числе поток данных и уровни давления. На основании полученных данных настраивается работа Backpressure.
Схема работы паттерна состоит из нескольких компонентов. С помощью их комбинации паттерн позволяет системе обрабатывать различные скорости получения и потребления данных. Так система не перегружается, а данные обрабатываются без сбоев и эффективно.
На схеме видно, как возникает дефицит на обработку запросов между сервером-производителем и серверами-потребителями и как с этим работает паттерн Backpressure:
- Производитель данных, или producer, отправляет данные в очередь потребителя (например, Kafka).
- Потребитель данных, или consumer, обрабатывает данные из очереди.
- Канал обратной связи, или feedback channel, обеспечивает коммуникацию между потребителем и производителем. У потребителя появляется возможность давать сигналы обратной связи производителю о способности обрабатывать поступающие данные.
- Механизм регулирования, или flow control mechanism, управляет потоком данных от производителя к потребителю на основе полученной обратной связи. В ответ на сигналы обратного давления производитель корректирует скорость или поведение передачи данных в соответствии с возможностями потребителя. Механизм регулирования потока может включать такие методы, как ограничение скорости, буферизация или динамическая настройка обработки данных.
- Механизм сигнализации, или pressure signal mechanism, — средства, с помощью которых потребитель отправляет сигналы об обратном давлении производителю данных: сообщения, управляющие сообщения, сигнальные флаги или индикаторы доступности ресурсов.
Реализация паттерна Backpressure
🔍 Чтобы лучше понять работу паттерна, вы можете запустить у себя код, который мы разбираем ниже.
Мы будем использовать очередь
queue для буферизации и потоки threading для моделирования производителей и потребителей данных. В этом примере:
- есть общий
buffer(размер 5) для производителя и потребителя; - производитель генерирует случайное число и пытается поместить его в буфер;
- если буфер заполнен, производитель ожидает;
- потребитель пытается использовать данные из буфера;
- если буфер пуст, потребитель ожидает;
- потребитель «спит» случайное время, чтобы имитировать обратное давление, то есть потребитель обрабатывает сообщения медленнее, чем их производит производитель.
PYTHON
Многие инструменты имеют встроенный механизм обратного давления (например, Apache Flink) для работы с потоками. Ряд инструментов можно лишь приблизительно подогнать под ситуацию возросшей нагрузки (например, Kafka) и доработать их логику.
Рассмотрим ещё один паттерн, который поможет справиться с возросшей нагрузкой на сервер.
Паттерн Curcuit Breaker
Этот паттерн более прост в реализации и часто применяется в системах с высокой нагрузкой.
💡 У паттерна Curcuit Breaker две функции: снижение использования ресурсов (CPU, Memory) и предотвращение ошибок в работе приложения.
Представьте, что один из методов API стороннего сервиса, с которым вы работаете, временно недоступен. Это может привести к сбоям в работе вашего приложения — появятся ошибки, которые могут затронуть уже ваших потребителей. Было бы неэффективно продолжать посылать запросы к неработающему API. Так как все запросы в таком случае завершатся ошибкой, лучше использовать временные и вычислительные ресурсы более продуктивно.
Классический пример каскадного сбоя — 12 июля 2013 года, когда произошло временное зависание на сайте РЖД сервиса по продаже билетов. Зависание было связано с техническими проблемами КИС Транскредитбанка, принимающей платежи за билеты.
Если бы был встроен предохранитель, то вместо бесконечных вызовов КИС и «выедания» ресурсов появилось бы сообщение-заглушка и работа сайта не была бы парализована.
Эту проблему решает Curcuit Breaker — автоматический выключатель. Без него приложение будет продолжать попытки отправить запросы к недоступному API, напрасно расходуя ресурсы. Автоматический выключатель предотвращает это, быстро отказывая в выполнении запросов на раннем этапе. Затем он мониторит вызовы к этому внешнему API, и, как только оно снова станет доступным, выключатель снова разрешит вызовы.
На схеме вы видите, как бэкенд-часть приложения обращается к стороннему ресурсу через Third-party API. Curcuit Breaker же может «завернуть» сигнал.
Вспомните, как выбивает пробки и в квартире выключается электричество. Чтобы не случилось возгорания, при перегрузке сети срабатывает автоматический выключатель.
Такой же принцип у паттерна Curcuit Breaker. Выключатель может быть в трёх разных состояниях: открыт, закрыт и наполовину открыт.
Пока выключатель закрыт, запросы к сервису выполняются штатно.
Но если сбои начинают учащаться — превышается порог, состояние переключается на «открыто» и вызовы больше не проходят. В этом случае можно возвращать заглушку.
Например, пользователи активно пытаются купить товары на распродаже в маркетплейсе. В какой-то момент один из сервисов перестаёт отвечать и, чтобы не произошло каскадного сбоя, пользователям начинает выводиться надпись: «Сервис сейчас недоступен, попробуйте через минуту».
Полуоткрытое состояние — это промежуточный этап. Circuit Breaker после определённого времени в открытом состоянии переходит к полуоткрытому состоянию, пропуская тестово несколько запросов, чтобы проверить, нормально ли работает сервис. Если они проходят успешно, Circuit Breaker закрывается, считая, что проблема решена. Но если снова возникают ошибки, он возвращается в открытое состояние, чтобы защитить сервис от перегрузки.
🔷 Применяйте этот паттерн, чтобы предотвратить попытки приложения обратиться к удалённой службе или общему ресурсу, если высока вероятность сбоя операции.
Не рекомендуется использовать Curcuit Breaker для обработки обращений к локальным частным ресурсам в приложении (например, к структуре данных в памяти). В такой среде применение автоматического отключения увеличит нагрузку на систему.
Также не следует применять этот шаблон вместо обработки исключений в бизнес-логике ваших приложений.
Реализация паттерна Curcuit Breaker
🔍 Чтобы лучше понять работу паттерна, вы можете запустить у себя код, который мы разбираем ниже.
Сделаем функцию, которая остановит операцию на X времени, если она завершится неудачей при Y подсчётах.
PYTHON
В случае проверки:
BASH
Мы получим:
PYTHON
Реализация паттерна защищает нас от каскадных сбоев при сбое внешнего сервиса.
Задание 5
Выберите подходящий паттерн под условия задач.
Сервис по бронированию отелей имеет множество интеграций со сторонними ресурсами
Circuit Breaker
Для предотвращения отправки запросов к сторонним API в случае их отказа используется паттерн Circuit Breaker.
Инженеры видеохостинга определили точки отказа, зафиксировали максимальную пропускную способность серверов и дефицит на обработку запросов
Backpressure
В данном случае подойдёт использование паттерна Backpressure. Он поможет настроить буферы, в которые будут складываться запросы, превышающие пропускную способность серверов.
Инженеры агрегатора по бронированию авиабилетов работают над задачей снижения нагрузки на базу данных
Backpressure
Для снижения нагрузки на внутренние ресурсы не рекомендуется использовать паттерн Circuit Breaker: это только увеличит нагрузку на систему. Больше подойдёт Backpressure.
Сервис доставки еды из ресторанов имеет множество интеграций. Необходимо снизить нагрузку на используемые ресурсы
Circuit Breaker
Для этой задачи подойдёт паттерн Circuit Breaker. Он позволит временно приостанавливать отправку запросов к внешним API и таким образом снижать CPU и Memory.
ИТОГИ
Теперь, когда вы знаете о том, как вычислить нагрузку, и знакомы с паттернами её снижения, вернёмся к приложению ленты новостей. Оно столкнулось с тем, что количество пользователей выросло в тысячу раз и такая нагрузка привела к сбоям работы серверов, а запросы стали выполняться очень медленно.
- Чтобы стабилизировать систему, команде нужно провести диагностику системы и выявить «узкие места». Для измерения нагрузки нужно для каждого сервиса собрать метрики производительности: DAU (количество пользователей в день), RPS (количество запросов, которые сервер приложения обрабатывает в секунду) и QPS (количество запросов в секунду к базе данных).
- Следующим шагом можно внедрить в архитектуру паттерн Backpressure. На уровне API нужно настроить лимитирование скорости запросов. Это уменьшит нагрузку на серверы, ограничив количество запросов, поступающих в систему одновременно. А затем настроить приоритеты для обработки запросов. Например, можно в первую очередь обрабатывать запросы на свежие новости, а менее актуальные данные помещать в низкоприоритетную очередь.
- Для ключевых микросервисов можно настроить Circuit Breaker. Это позволит временно отключать или ограничивать взаимодействие с проблемными сервисами, предотвращая дальнейшее распространение сбоев. В случае ошибки Circuit Breaker будет отправлять пользователю сообщение-заглушку, например: «Новостей очень много, чтобы их донести, нам нужно чуть больше времени».
- В перспективе стоит масштабироваться — добавить больше серверов или ресурсов в облаке, чтобы справляться с возросшей нагрузкой.
Что посмотреть про паттерны отказоустойчивости архитектуры?
Yandex for Developers Паттерны отказоустойчивой архитектуры - Сергей Кривощёков
В докладе рассказывается о паттернах отказоустойчивости, которые применяются в сервисах Яндекса.