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

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

В чем проблема

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

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

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

Шаблон Посредник

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

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

Ниже представлена общая схема работы приложения, в соответствии с шаблоном Посредник.

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

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

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

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

Retry-After: <http-date>

Retry-After: <delay-seconds>

Также необходимо продумать процесс развертывания посредника. Учитывая, что данный узел подключается “в разрыв”, необходимо не допустить проблем с доступностью компонентов приложения в целом.

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

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

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

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

В качестве альтернативы Посреднику можно рассмотреть шаблон SideCar, о котором речь пойдет далее.

Шаблон SideCar

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

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

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

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

Основным преимуществом Sidecar является независимость от среды выполнения и языка программирования основного приложения, поэтому не нужно разрабатывать Sidecar для каждого языка. То есть, мы можем написать Sidecar на другом языке и с использованием других фреймворков и сред выполнения и он будет взаимодействовать с основным приложением посредством открытых протоколов, таких как JSON.

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

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

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

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

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

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

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

Паттерн конкурирующих потребителей

Итак, недостатками предыдущих шаблонов в той или иной степени были возможные проблемы с производительностью при обработке большого объема сообщений. Шаблон конкурирующих потребителей (Competing Consumers Pattern) позволяет разрешить нескольким параллельно работающим потребителям обрабатывать сообщения, полученные по тому же каналу обмена сообщениями. С помощью данного шаблона мы можем построить систему, способную обрабатывать несколько сообщений одновременно. Тем самым мы можем оптимизировать пропускную способность, улучшить масштабируемость и доступность, а также сбалансировать нагрузку.

Синхронная обработка каждого запроса неизменно ведет к низкой скорости обработки сообщений системой, особенно в случаях, когда объем сообщений серьезно увеличивается. Обычным методом является передача сообщений приложением через систему обмена сообщениями другого сервиса (consumer), который обрабатывает их асинхронно. Эта стратегия помогает гарантировать, что бизнес-логика в приложении не блокируется во время обработки запросов.

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

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

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

Выглядит это следующим образом:

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

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

Заключение

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

Научиться на практике применять шаблоны проектирования и SOLID в разработке можно на онлайн-курсе от экспертов области. Новый поток стартует 26 июня.

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


  1. LaRN
    21.06.2024 15:16

    Амбасадор похож на фасад. В чем отличие?