Распределенные системы — приложения, состоящие из множества компонентов, работающих на множестве машин. В этой части речь пойдет о паттернах, локализованных в рамках одного узла.
Контейнеры — основной строительный элемент паттернов, но в конечном итоге именно группа контейнеров, локализованная на одной машине, представляет собой базовый элемент паттернов проектирования распределенных систем.
Конспект в 3 частях
- Распределенные Системы. Брендан Бёрнс. Введение
- Распределенные Системы. Брендан Бёрнс. Одноузловые паттерны проектирования
- Распределенные Системы. Брендан Бёрнс. Паттерны проектирования обслуживающих систем
Одноузловые паттерны проектирования
Чтобы обеспечить изоляцию и инкапсуляцию, развертывайте компоненты приложения в отдельном процессе или контейнере.
Sidecar
Sidecar — это одноузловой паттерн, состоящий из двух контейнеров. Первый из них — контейнер приложения. Он содержит основную логику программы. Без этого контейнера приложения бы не существовало. Вдобавок к контейнеру приложения предусмотрен еще «прицепной» (sidecar) контейнер.
Роль прицепа — дополнить и улучшить контейнер приложения, часто таким образом, чтобы приложение не знало о его существовании. В простейшей форме контейнер-прицеп можно использовать, чтобы добавить функциональности контейнеру, который было бы сложно улучшить иным способом.
Контейнер-прицеп и контейнер приложения совместно используют не только процессорные, но и другие ресурсы — части файловой системы, имя хоста, сетевые (и другие) пространства имен.
В рамках паттерна Sidecar контейнер-прицеп дополняет и расширяет контейнер приложения, тем самым добавляя ему функциональности.
Sidecar можно использовать для модернизации унаследованных приложений, если внесение изменений в них обойдется слишком дорого.
Кроме того, этот паттерн можно применять для создания модульных контейнеров-утилит, задающих стандартную реализацию часто используемых функциональных возможностей. Контейнеры утилиты можно задействовать во множестве приложений, повышая согласованность среды и снижая расходы на разработку последующих приложений.
Одно из основных преимуществ применения паттерна Sidecar модульность и повторное использование контейнеров-прицепов.
Для развертывания любого «боевого» приложения, от которого ожидается высокий уровень надежности, требуется функционал, касающийся отладки и управления приложением, например считывание ресурсов, потребляемых приложениями внутри контейнера, по аналогии с утилитой командной строки top.
Исполнение контейнера-прицепа совместно с основным контейнером планируется посредством атомарной группы контейнеров, такой как под (pod) в Kubernetes.
Примеры реализации паттерна Sidecar
В книге Распределенные Системы. Брендан Бёрнс примеры подробно разобраны и описаны готовые решения.
Добавление возможности HTTPS-соединения к унаследованному сервису
Динамическая конфигурация
Простейший PaaS-сервис
Модульные контейнеры приложений
Реализация паттерна Sidecar будет наиболее эффективной, если ее можно будет применять во множестве приложений и во множестве сценариев развертывания. Обеспечивая модульность и возможность повторного использования, реализации этого паттерна позволяют значительно ускорить разработку вашего приложения.
Любой технический выбор подразумевает некоторый компромисс между применением модульных контейнерных паттернов и внесением собственного кода в приложение. Библиотечно-ориентированный подход всегда будет несколько менее адаптирован под особенности вашего приложения. Подобная реализация может оказаться менее эффективной в плане размера или производительности; API могут потребовать некоторой адаптации для использования в вашей среде.
Модульность и возможность повторного применения, как и достижение модульности в разработке высококлассного программного обеспечения, требуют сосредоточенности и дисциплины. В частности, необходимо сконцентрироваться на таких трех составляющих, как:
-
Параметризованные контейнеры. Каждый параметр представляет собой входные данные, которые помогают подстроить обобщенный контейнер под конкретную ситуацию.
-
Определение API всех контейнеров. Думая о модульности и повторном использовании контейнеров, важно понимать, что программный интерфейс (API) контейнера определяется всеми его аспектами взаимодействия с внешней средой. Как и в среде микросервисов, микроконтейнеры рассчитывают на наличие некоторого программного интерфейса, который бы четко разделил основное приложение и контейнер-прицеп. Кроме того, наличие API гарантирует, что все потребители контейнера-прицепа будут работать корректно даже после выхода последующих его версий. В то же время наличие четкого API у контейнера-прицепа позволяет его создателю более эффективно работать, поскольку в этом случае у него есть четкое определение услуг, предоставляемых контейнером (а желательно и юнит-тестов для них).
-
Документирование контейнеров. Предоставить пользователям информацию, как их применять в принципе. Как и в случае с программными библиотеками, ключ к созданию полезной вещи объяснение, как ею пользоваться.
Ambassador
Контейнер-посол выступает посредником во взаимодействии контейнера приложения с внешним миром. Как и в случае с остальными одноузловыми паттернами проектирования, два контейнера составляют симбиотический союз и исполняются совместно на одном компьютере.
От паттерна Ambassador двойная польза:
Во-первых, как и остальные одноузловые паттерны, он позволяет создавать модульные, повторно используемые контейнеры. Разделение ответственности между контейнерами упрощает их разработку и поддержку.
Во-вторых, контейнер-посол можно использовать с различными контейнерами приложений. Такого рода повторное применение ускоряет разработку приложений, поскольку контейнеризованный код можно задействовать в нескольких разных местах.
Вдобавок повышаются качество и согласованность реализации, поскольку код собирается разово и затем используется во многих различных контекстах.
Использование паттерна Ambassador для шардирования сервиса
В какой-то момент данных на уровне хранилища (storage layer) становится так много, что они перестают помещаться на одной машине. В таких ситуациях необходимо шардировать уровень хранилища.
Шардинг (шардирование) — разделение уровня хранилища на несколько независимых частей (шардов), каждая из которых размещается на отдельной машине.
При развертывании шардированного сервиса возникает вопрос о том, как интегрировать его с программным обеспечением клиентского или промежуточного уровня.
Очевидно, должен существовать модуль, который бы переадресовывал конкретный запрос конкретному шарду.
Часто такой шардированный клиент тяжело интегрировать в систему, компоненты которой рассчитывают на подключение к единому хранилищу данных.
К тому же шардированные сервисы препятствуют совместному использованию конфигурации средой разработки (где хранилище состоит, как правило, из одного шарда) и средой эксплуатации (где хранилище часто состоит из множества шардов).
Один из вариантов — включить всю логику шардирования в сам шардированный сервис.
При таком подходе шардированный сервис должен также иметь балансировщик нагрузки с независимой обработкой транзакций, адресующий трафик нужному шарду. Этот балансировщик нагрузки будет, по сути, распределенной реализацией паттерна Ambassador в виде сервиса. Клиентская реализация паттерна Ambassador становится не нужна, но за счет этого усложняется развертывание шардированного сервиса.
Другой вариант — интегрировать одноузловую реализацию паттерна Ambassador на стороне клиента, чтобы она перенаправляла трафик нужному шарду.
При адаптировании существующего приложения к шардированному хранилищу мы создаем контейнер-посол, который содержит всю необходимую логику для переадресации запросов соответствующим шардам хранилища. Таким образом, программное обеспечение клиентского или промежуточного уровней подключается к сервису, который выглядит как единое хранилище, работающее на локальной машине. Но этот сервис на самом деле является шардирующим прокси-контейнером, реализующим паттерн Ambassador. Он принимает запросы от приложения, переадресует их соответствующему шарду хранилища и затем возвращает результат приложению.
Главный результат применения паттерна Ambassador к шардированным сервисам — разделение обязанностей между контейнером приложения и шардирующим прокси.
Использование паттерна Ambassador для реализации сервиса-посредника
Такой процесс называется обнаружением сервисов (service discovery), а система, которая выполняет обнаружение и стыковку, называется сервисом-посредником (service broker).
Приложение просто подключается к экземпляру сервиса (например, MySQL), работающему на локальном компьютере. Обязанность контейнера-посла сервиса-посредника заключается в обследовании окружения и опосредовании подключения к конкретному экземпляру целевого сервиса.
Использование паттерна Ambassador для проведения экспериментов и разделения запросов
В эксплуатируемых системах важно иметь возможность разделения запросов, когда некоторая часть запросов обрабатывается не основным рабочим сервисом, а его альтернативной реализацией. Чаще всего его используют, чтобы проводить эксперименты с новыми или бета версиями сервиса для определения степени надежности и производительности новой версии сервиса по сравнению с существующей.
Кроме того, разделение запросов иногда используется для дублирования или деления трафика таким образом, что он распределяется как на рабочую версию ПО, так и на новую, еще не развернутую.
Adapter
Контейнер-адаптер модифицирует программный интерфейс контейнера приложения таким образом, чтобы он соответствовал некоему заранее определенному интерфейсу, реализация которого ожидается от всех контейнеров приложений.
К примеру, адаптер может обеспечивать реализацию унифицированного интерфейса мониторинга. Или же он может обеспечивать то, что файлы журнала всегда выводятся в stdout, а также требовать соблюдения любых других соглашений.
Разные контейнеры приложений могут предоставлять разные интерфейсы для мониторинга, а контейнер-адаптер подстраивается под гетерогенность среды с целью унификации интерфейса.
Это позволяет разворачивать единственный инструмент, заточенный под этот конкретный интерфейс.
Мониторинг
Хотелось бы иметь унифицированное решение, позволяющее автоматически обнаруживать любые приложения, развернутые в некоторой среде, и наблюдать за их состоянием. Чтобы это стало возможным, каждое приложение должно реализовывать один и тот же интерфейс мониторинга.
Контейнер-адаптер содержит инструменты, преобразующие интерфейс мониторинга, предоставляемого контейнером приложения, в интерфейс, ожидаемый системой мониторинга общего назначения.
Ведение журналов
Как и в случае с мониторингом, системы очень неоднородно журналируют данные. Системы могут разделять журналы на различные уровни, например debug, info, warning и error, каждый из которых записывается в отдельный файл. Некоторые просто выводят информацию в потоки stdout или stderr. Это особенно критично в случае контейнеризованных приложений, когда обычно ожидается, что контейнеры выводят информацию в поток stdout, так как именно его содержимое доступно при выполнении команд docker logs или kubectl logs.
Усложняет ситуацию и то, что журналируемая информация в общем случае имеет структурированные элементы, например дату и время записи, но эти сведения сильно различаются для разных реализаций библиотек журналирования (например, для встроенного в Java средства журналирования и пакета glog в Go).
Adapter помогает предоставить модульную, повторно используемую архитектуру для ведения журналов.
Контейнер приложения может вести журнал в файле, а контейнер-адаптер будет перенаправлять его содержимое в поток stdout. Разные контейнеры приложения могут вести журналы в разных форматах, а контейнер-адаптер может преобразовывать эти данные в общее структурированное представление, которым сможет воспользоваться агрегатор журналов. Адаптер и в данном случае на основе неоднородной среды приложений создает однородную среду общих интерфейсов.
Одна из задач адаптера — привести показатели журнала к стандартному набору событий. Разные приложения имеют разные форматы выходных данных, но, чтобы привести их к однородному формату, можно задействовать стандартный инструмент журналирования, развернутый в виде адаптера.
Мониторинг работоспособности сервисов
Нетрудно догадаться, как решить эту проблему, — можно использовать контейнер-адаптер. База данных работает в контейнере приложения, который имеет общий с контейнером-адаптером сетевой интерфейс. Контейнер-адаптер — простой контейнер, который содержит только сценарий оболочки, оценивающий работоспособность базы данных. Этот сценарий можно настроить в качестве комплексного средства проверки контейнера СУБД, выполняющего любую диагностику, необходимую нашему приложению. Если контейнер приложения когда-либо не пройдет проверку, он будет автоматически перезапущен.
Источники
- Распределенные системы. Паттерны проектирования. Бёрнс Б.
- https://azure.microsoft.com/ru-ru/resources/designing-distributed-systems/
- https://docs.microsoft.com/ru-ru/azure/architecture/patterns/ambassador
- https://matthewpalmer.net/kubernetes-app-developer/articles/multi-container-pod-design-patterns.html