Паттерны надёжности Bulkhead и Transactional outbox
В прошлом уроке вы узнали о паттерне Rate limiter, а сейчас стоит узнать о других способах повысить устойчивость и надёжность системы в условиях высоких нагрузок и распределённых архитектур. Речь пойдёт о паттернах Bulkhead и Transactional Outbox.
Для компаний, работающих с критически важными сервисами, где сбой одного компонента может привести к общему падению системы, полезно освоить паттерн Bulkhead. Он разделяет систему на независимые модули и помогает предотвратить каскадные сбои.
Паттерн Transactional Outbox решает проблему потери данных при взаимодействии между сервисами. Это важно, если компании используют микросервисную архитектуру, где необходимо обеспечить согласованность данных и надёжность обработки событий.
Bulkhead
Bulkhead, как и Rate limiter, представляет собой паттерн надёжности. Он в некотором смысле пришёл в IT из кораблестроения и наследует технику разделения конструкции судов на отсеки, чтобы предотвратить заполнение водой всего судна сразу.
В контексте IT-системы паттерн подразумевает изоляцию различных компонентов системы так, чтобы сбои в одном компоненте не влияли на другие.
Bulkhead помогает решить целый ряд проблем:
Изоляция неисправностей
Это и есть основная задача — повысить надёжность приложения и сделать так, чтобы сбои в определённых компонентах или модулях не распространялись на остальные. Например, если несколько приложений развёрнуты на одной машине с использованием общих ресурсов и в какой-то момент одно приложение «забирает» к себе все ресурсы.
Оптимизация производительности
Изоляция позволяет разработчикам систем распределять ресурсы (такие как ЦП, память и пропускная способность сети) более эффективно. Изолируя ресурсоёмкие задачи или службы, системы избегают конфликтов и обеспечивают стабильную производительность между различными компонентами.
Повышение безопасности
Bulkhead уменьшает поверхность атаки и ограничивает распространение уязвимостей. Изоляция конфиденциальных данных и критических компонентов инфраструктуры помогает снизить риск несанкционированного доступа, утечки данных и т. д.
Гибкость масштабирования
Паттерн косвенно способствует гибкому масштабированию, так как позволяет независимо масштабировать изолированные компоненты.
Принцип работы Bulkhead
Bulkhead может использоваться для изоляции компонентов системы, данных, ресурсов инфраструктуры и сети. Соответственно, этот паттерн применяется и на различных слоях, и в различных участках системы. Конкретная реализация во многом зависит от задач, которые нужно решить, и от того, что именно требуется изолировать и на каком слое.
Рассмотрим ряд распространённых реализаций.
Разделение пулов соединений
Каждому сервису может быть назначен свой пул соединений. Тогда сбои в работе одного сервиса (например, забивания пула подключений из-за долгого времени отклика) не будет воздействовать на подключения к другим сервисам, благодаря изоляции.
Потребитель тоже может разделить ресурсы, чтобы гарантировать, что ресурсы, используемые для вызова одного сервиса, не повлияют на ресурсы, используемые для вызова другого. Такое решение позволяет избежать каскадных сбоев, влияющих на несколько сервисов и их потребителей.
Разделение ресурсов приложения
Если проблема — в чрезмерном потреблении ресурсов одним приложением, которое «сжирает» лишнее и тем самым влияет на работу другого приложения, требуется использовать механизмы его изоляции. Популярным решением является контейнеризация, которая позволяет безопасно запускать множество приложений на одной машине без рисков конкуренции их за чужие ресурсы. Например, с помощью Docker, который вам знаком по прошлым спринтам.
В такой реализации важно учитывать, достаточно ли изначально ресурсов на сервере для суммарного запроса всех приложений, которые требуется развернуть.
Разделение пула тредов
Применение на уровне кода самого приложения особенно актуально для мультипоточных приложений. Такая реализация подразумевает выделение отдельных пулов потоков для различных типов задач или операций. Например, запросы, обращённые к пользователю, могут обрабатываться одним пулом потоков, а фоновые задачи — другим. Таким образом, вы сможете предотвратить конфликт ресурсов между разными процессами и снизить вероятность сбоев. Для такой реализации существует множество фреймворков, например resilience4j и Polly.
Разделение БД
Часто для экономии ресурсов применяется реализация, когда несколько приложений могут использовать разные схемы данных, но в рамках одной инсталляции БД. Если нужно обеспечить повышенный уровень отказоустойчивости каких-то приложений, вы можете создать для них отдельные инсталляции БД.
А ещё приложения с большим объёмом чтения и записи, которые потенциально могут негативно влиять на работу других приложений, направляются на отдельные экземпляры БД. Это помогает избежать узких мест производительности и гарантирует, что сбои или замедления в одной базе данных не повлияют на работу других приложений.
Разделение на уровне брокера сообщений
Сервисы, которые взаимодействуют с помощью асинхронных сообщений, можно изолировать с помощью различных наборов очередей. Каждая очередь тогда имеет выделенный набор экземпляров, обрабатывающих сообщения в очереди. Изоляция может быть и на уровне одной очереди или топика событий. Например, партиции в Kafka.
Другие уровни
Изоляция может выполняться и на уровне сетевого оборудования, балансировщиков и файерволов, очередей сообщений и так далее. Чем выше нагрузка на ваше приложение, тем выше вероятность сбоев, связанных с ней.
Всегда стоит помнить о возможности изоляции различных компонентов приложения, чтобы даже в случае сбоя в одном компоненте, функциональность, за которую отвечают другие компоненты, сохраняла работоспособность.
Сергей
С какими сложностями можно столкнуться?
Сложности внедрения Bulkhead
Внедрение Bulkhead в целом усложняет архитектуру приложения, как и любая декомпозиция. Поэтому не стоит делить компоненты слишком гранулярно, если того не требует ваша задача. Например, если вы хотите распределить потребителей по разделам, вы можете выделить для каждого потребителя отдельный экземпляр приложения или позволить нескольким потребителям использовать вместе один экземпляр.
Выделение ресурсов и управление ими (такими как потоки, память, ЦП, соединения с базой данных) для каждого Bulkhead может быть сложной задачей, особенно в динамических средах. Зачастую это требует применения дополнительных автоматизированных средств (например, Docker).
Ещё нужно учитывать ситуации, при которых может понадобиться перераспределение ресурсов между выделенными компонентами. Например, при росте нагрузки на какую-то конкретную функциональность. Для этого тоже потребуется использование дополнительных средств управления и инструментов мониторинга. Примером такого инструмента может служить VPA, который вы изучали в прошлых уроках.
И в целом некоторые решения могут приводить к снижению производительности. Например, использование многослойной виртуализации может сказываться на производительности приложения.
Теперь вы имеете представление, когда паттерн Bulkhead может сослужить добрую службу, и предупреждены о сложностях его реализации, которые всегда следует учитывать. Пора переходить к паттерну Transactional outbox.
Transactional outbox
Этот шаблон призван решить проблему двойной записи операций, которая возникает в распределённых системах, и особенно актуален для микросервисных архитектур.
Двойная запись операций происходит, когда приложение делает запись в две разные системы. Например, когда микросервису необходимо сохранить данные в базе данных и отправить сообщение или событие через брокер сообщений.
Когда происходит сбой при выполнении одной из этих операций, велики шансы, что возникнет несогласованность данных. Можно попробовать применить Retry, но он не всегда решает проблему (например, в случаях долгой недоступности брокера). Кроме того, ошибка может возникнуть в самом приложении, когда оно неожиданно упало или перезапустилось, допустим, после того, как положило изменения в БД, но до того, как отправило сообщение в очередь.
Все эти ситуации подводят к вопросам. Возможно ли откатить операцию, включая изменения в БД, или завершить операцию частично? Какую операцию поставить первой — запись в БД или в очередь сообщений?
При любой реализации может возникнуть ситуация, когда изменения сохранились только в одной из систем: брокере или БД. От этого и возникает несогласованность.
Если обновление базы данных прошло успешно, но уведомление о событии не было отправлено, потребитель не будет знать об изменении и система может перейти в несогласованное состояние.
Если обновление базы данных прошло неудачно, но уведомление о событии было отправлено, также может возникнуть несогласованное состояние, так как потребитель будет считать, что событие произошло, но в мастер-системе этих данных не будет.
Важно отметить, что проблема в основном применима к операциям, которые предполагают изменение данных. Для операций, в которых осуществляются исключительно запросы данных, применение такого паттерна будет избыточным.
Принцип работы Transactional outbox
Суть реализации заключается в том, что сервис, который должен отправить сообщение или событие, сначала сохраняет его в базе данных как часть транзакции, в рамках которой он создаёт, удаляет или обновляет бизнес-сущность. При этом сохранение этого сообщения происходит в отдельную outbox-таблицу. Из неё отдельный процесс потом их вычитывает и отправляет через брокер сообщений.
Фактически решение состоит в том, чтобы разделить один процесс на два, изолировав операции взаимодействия с внешними системами друг от друга.
Для реализации задействуются следующие компоненты:
Отправитель — сервис, который отправляет сообщение.
База данных — база данных, которая хранит бизнес-сущности и исходящие сообщения.
Таблица исходящих сообщений (Outbox table) — в случае использования реляционной БД исходящие сообщения хранятся в отдельной таблице. В случае использования NoSQL-решения исходящие сообщения могут быть представлены в виде другого объекта, например документа.
💡 Outbox — это API.
Важно помнить, что Outbox-таблица, или таблица исходящих сообщений, несмотря на нестандартную реализацию, является API вашего приложения. Структура таблицы представляет собой контракт события. Поэтому при проектировании с использованием этого паттерна применимы и такие шаблоны, как API First и версионирование.
Передатчик сообщений — в рамках отдельного процесса вычитывает исходящие сообщения и отправляет их в брокер.
Передатчик сообщений
Для реализации передатчика может быть, в свою очередь, использовано два шаблона: Polling publisher и Transaction log tailing.
Polling publisher
Этот шаблон достаточно прост. Он предполагает периодическую вычитку записей из таблицы исходящих сообщений, например по расписанию. Если в таблице были найдены новые сообщения, он отправляет их в брокер.
Далее отправленные сообщения могут быть либо удалены, либо помечены определённым статусом. Второй вариант позволяет в будущем при необходимости повторно отправить все сохранённые сообщения. Это может понадобиться в случае критического сбоя в брокере сообщений или на стороне потребителя, но требует дополнительного дискового пространства. Поэтому удаление можно производить периодически (например, раз в неделю).
Также при реализации может возникнуть вопрос, реализовывать передатчик сообщений в качестве отдельного компонента или как часть отправителя. Оба варианта применяются в реальной разработке. Решение зависит от конкретной ситуации и возможности доработки отправителя, но по умолчанию рекомендуется выбирать вариант реализации внутри отправителя, чтобы не формировать зависимость по модели данных между различными компонентами со всеми вытекающими проблемами.
В случаях если приложение отправителя сложно дорабатывать (вендорское или легаси-решение) или же нет возможности влияния на производительность отправителя (допустим, из-за большого количества исходящих сообщений), стоит рассмотреть вариант реализации передатчика в качестве отдельного компонента.
Transaction log tailing
Альтернативным способом реализации передатчика сообщений является подписка на транзакционный лог БД. Многие БД предлагают специальные механизмы для отслеживания своих логов:
MySQL binlog
Postgres WAL
AWS DynamoDB table streams
Каждое изменение в БД отображается как запись в журнале транзакций. Отдельный компонент подписывается на этот лог транзакций и публикует каждое изменение как сообщение в брокер сообщений. Он отвечает за преобразование записей журнала в сообщение, в рамках чего может приводить к нужному формату данных или обогащать какой-либо метаинформацией.
Основное преимущество такой реализации заключается в том, что передатчик не оказывает никакого воздействия на операционную БД, при этом скорость отправки сообщений, как правило, выше, чем при использовании Polling publisher.
Однако такая реализация является значительно более сложной по сравнению с Polling publisher, так как предполагает использование специфичных для конкретной БД механизмов и необходимость хранения состояния об отправленных сообщениях в отдельном хранилище.
Поэтому на рынке существуют готовые решения класса CDC.
CDC-инструменты
CDC-инструменты предлагают готовые программные решения, которые реализуют паттерн Transaction log tailing. CDC расшифровывается как Change Data Capture, то есть захват изменения событий.
Как правило, они предлагают несколько готовых коннекторов для получения данных из разных БД, а также для выгрузки этих данных в различные брокеры сообщений или другие хранилища. Ещё они могут предлагать открытое API для разработки собственных коннекторов, чтобы настраивать взаимодействие со специфичными БД или брокерами.
Одним из самых популярных решений на рынке является Debezium. Это решение построено на основе Apache Kafka и предоставляет совместимые с Kafka Connect коннекторы, которые отслеживают изменения в различных БД. Например, для подключения к PostgreSQL чаще всего в индустрии используется именно Debezium.
Debezium записывает историю изменений данных в журналы Kafka, откуда их может уже вычитывать потребитель. При этом он способен захватывать и публиковать сообщения с очень маленькой задержкой, измеряемой в миллисекундах, не требуя при этом повышенного использования процессора.
Сергей
О чём ещё нужно знать?
Сложности при реализации паттерна
При реализации паттерна Transactional outbox могут возникнуть сложности вне зависимости от выбранной реализации для передатчика сообщений.
Передатчик сообщений может опубликовать его более одного раза. Например, в нём тоже может произойти сбой после публикации сообщения, но до того, как он фиксирует факт его публикации в БД. После перезапуска передатчик снова отправит сообщение. Для сглаживания рисков задвоения данных рекомендуется применять шаблон идемпотентности сообщений, при котором повторная отправка одного и того же сообщения не изменяет результат.
Необходима база данных. Это может стать проблемой для приложений, которые не используют базу данных, но всё ещё сталкиваются с проблемой двойной записи, например, сразу в несколько брокеров сообщений. Для применения паттерна в любом случае потребуется персистентное хранилище.
Дополнительная сложность. Применение паттерна может добавить сложности, так как требует использования дополнительных компонентов, с которыми, в свою очередь, возникает зависимость модели данных приложения. Доработки могут занимать больше времени.
Пример. В системе «ОнлиСхемы» команда может применить паттерн Transactional outbox совместно с Transactional log tailing для повышения надёжности процесса публикации событий о создании или изменении объектов на доске.
Задание 2
Выберите из списка реализацию паттерна, которая поможет решить описанную проблему.
Приложение предоставляет API. Периодически один из сервисов, который обрабатывает запросы, сталкивается со сбоями и крайне долго обрабатывает запросы. Периодически ресурсов сети не хватает даже для других серверов приложения
Разделение пулов соединений
Каждому сервису можно выделить свой пул соединений. Это значит, что, если один сервис зависнет (предположим, из-за долгого времени отклика), это не повлияет на другие сервисы, поскольку они изолированы друг от друга.
На сервере запущено несколько приложений. Периодически одному из приложений требуется обрабатывать большие реестры с данными. Уже возникали ситуации, при которых падала производительность его «соседей» из-за недостатка ресурсов памяти
Разделение ресурсов приложения
Для решения проблемы чрезмерного потребления ресурсов одним приложением, которое «сжирает» лишнее и тем самым влияет на работу другого приложения, требуется использовать механизмы их изоляции.
Микросервис предоставляет два типа операций: атомарные и пакетные. В рамках пакетных операций иногда приходит до одного миллиона записей, что приводит к конкуренции за ресурсы для выполнения атомарных запросов пользователей и к превышению требуемого времени отклика
Разделение пула тредов
Особенно он актуален для мультипоточных приложений, разделение пула потоков подразумевает выделение отдельных пулов потоков для различных типов задач или операций. Поможет предотвратить конфликт ресурсов между разными процессами и снизить вероятность сбоев.
Система предоставляет асинхронный API, куда различные кредитные конвейеры отправляют запросы на оценку риска заявки по кредиту. В одном из конвейеров решили реализовать формирование предодобренных предложений клиентам, для этого они отбирают все записи клиентов, которым потенциально готовы предложить кредит, и отправляют их также на оценку рисков. Из-за того что одномоментно в очередь попадает сразу множество сообщений, они откладывают обработку атомарных запросов от других конвейеров
Разделение на уровне брокера сообщений
Сервисы, которые взаимодействуют с помощью асинхронных сообщений, могут быть изолированы с помощью различных наборов очередей, чтобы запросы в этих очередях не конфликтовали.
Задание 4
Сопоставьте шаблоны паттернов Polling publisher и Transaction log tailing с их характеристиками.
Polling publisher
После отправки у строк в Outbox-таблице может быть обновлён статус
После отправки у строк в Outbox-таблице может быть обновлён статус
После отправки сообщения могут быть удалены из Outbox-таблицы
После отправки сообщения могут быть удалены из Outbox-таблицы
Предполагает периодическую вычитку записей из таблицы исходящих сообщений
Предполагает периодическую вычитку записей из таблицы исходящих сообщений
Transaction log tailing
Для реализации могут быть использованы CDC-инструменты
Для реализации могут быть использованы CDC-инструменты
Отслеживает записи в журнале транзакций БД
Отслеживает записи в журнале транзакций БД
Правильных ответов: 5, неправильных ответов: 0.
Polling publisher, как правило, — компонент, который по расписанию запрашивает записи из Outbox-таблицы, на их основании формирует исходящее сообщение и кладёт его в брокер. После отправки записи из таблицы либо удаляются, либо проставляется новый статус, например «Отправлено».
Transaction log tailing, как правило, использует API-БД для отслеживания новых записей в транзакционном логе. На рынке существует ряд готовых решений класса CDC, которые предоставляют реализацию этого паттерна «из коробки».
Паттерн Bulkhead используется для изоляции различных компонентов системы, таких как сервисы, ресурсы, данные и инфраструктурные элементы, чтобы предотвращать каскадные сбои и обеспечивать отказоустойчивость системы. Bulkhead предполагает разделение ресурсов (пулы соединений, тредов, базы данных, контейнеры) таким образом, чтобы сбои в одном компоненте не влияли на другие.
Конкретная реализация Bulkhead может применяться на разных уровнях системы. Например, разделение пула соединений для разных сервисов или создание отдельных инсталляций баз данных для приложений с высокой нагрузкой, чтобы изолировать их от других сервисов.
Transactional Outbox — паттерн, который устраняет проблему двойной записи в распределённых системах. Он обеспечивает надёжную передачу событий между сервисами, записывая изменения данных и события в одну транзакцию. После успешного завершения транзакции события помещаются в специальную Outbox-таблицу и могут быть асинхронно отправлены другим сервисам, обеспечивая согласованность операций.
Вместе с Transactional Outbox для отслеживания изменений в базе данных и передачи событий в реальном времени часто используются инструменты CDC. Они автоматизируют процесс считывания данных из таблицы Outbox и отправки их в другие системы, повышают производительность и надёжность взаимодействия между микросервисами.