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

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

Конспект в 3 частях

  1. Распределенные Системы. Брендан Бёрнс. Введение
  2. Распределенные Системы. Брендан Бёрнс. Одноузловые паттерны проектирования
  3. Распределенные Системы. Брендан Бёрнс. Паттерны проектирования обслуживающих систем

Одноузловые паттерны проектирования

This is an image

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

Sidecar

This is an image

Sidecar — это одноузловой паттерн, состоящий из двух контейнеров. Первый из них — контейнер приложения. Он содержит основную логику программы. Без этого контейнера приложения бы не существовало. Вдобавок к контейнеру приложения предусмотрен еще «прицепной» (sidecar) контейнер.

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

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

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

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

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

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

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

Исполнение контейнера-прицепа совместно с основным контейнером планируется посредством атомарной группы контейнеров, такой как под (pod) в Kubernetes.

Примеры реализации паттерна Sidecar

В книге Распределенные Системы. Брендан Бёрнс примеры подробно разобраны и описаны готовые решения.

This is an image

Добавление возможности HTTPS-соединения к унаследованному сервису

This is an image

Динамическая конфигурация

This is an image

Простейший PaaS-сервис

This is an image

Модульные контейнеры приложений

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

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

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

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

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

  3. Документирование контейнеров. Предоставить пользователям информацию, как их применять в принципе. Как и в случае с программными библиотеками, ключ к созданию полезной вещи объяснение, как ею пользоваться.

Ambassador

This is an image

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

От паттерна Ambassador двойная польза:

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

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

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

Использование паттерна Ambassador для шардирования сервиса

This is an image

В какой-то момент данных на уровне хранилища (storage layer) становится так много, что они перестают помещаться на одной машине. В таких ситуациях необходимо шардировать уровень хранилища.

Шардинг (шардирование) — разделение уровня хранилища на несколько независимых частей (шардов), каждая из которых размещается на отдельной машине.

This is an image

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

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

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

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

Один из вариантов — включить всю логику шардирования в сам шардированный сервис.

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

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

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

Главный результат применения паттерна Ambassador к шардированным сервисам — разделение обязанностей между контейнером приложения и шардирующим прокси.

Использование паттерна Ambassador для реализации сервиса-посредника

This is an image

Такой процесс называется обнаружением сервисов (service discovery), а система, которая выполняет обнаружение и стыковку, называется сервисом-посредником (service broker).

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

Использование паттерна Ambassador для проведения экспериментов и разделения запросов

This is an image

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

Кроме того, разделение запросов иногда используется для дублирования или деления трафика таким образом, что он распределяется как на рабочую версию ПО, так и на новую, еще не развернутую.

This is an image

Adapter

This is an image

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

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

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

Это позволяет разворачивать единственный инструмент, заточенный под этот конкретный интерфейс.

Мониторинг

This is an image

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

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

Ведение журналов

This is an image

Как и в случае с мониторингом, системы очень неоднородно журналируют данные. Системы могут разделять журналы на различные уровни, например debug, info, warning и error, каждый из которых записывается в отдельный файл. Некоторые просто выводят информацию в потоки stdout или stderr. Это особенно критично в случае контейнеризованных приложений, когда обычно ожидается, что контейнеры выводят информацию в поток stdout, так как именно его содержимое доступно при выполнении команд docker logs или kubectl logs.

Усложняет ситуацию и то, что журналируемая информация в общем случае имеет структурированные элементы, например дату и время записи, но эти сведения сильно различаются для разных реализаций библиотек журналирования (например, для встроенного в Java средства журналирования и пакета glog в Go).

Adapter помогает предоставить модульную, повторно используемую архитектуру для ведения журналов.

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

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

Мониторинг работоспособности сервисов

This is an image

Нетрудно догадаться, как решить эту проблему, — можно использовать контейнер-адаптер. База данных работает в контейнере приложения, который имеет общий с контейнером-адаптером сетевой интерфейс. Контейнер-адаптер — простой контейнер, который содержит только сценарий оболочки, оценивающий работоспособность базы данных. Этот сценарий можно настроить в качестве комплексного средства проверки контейнера СУБД, выполняющего любую диагностику, необходимую нашему приложению. Если контейнер приложения когда-либо не пройдет проверку, он будет автоматически перезапущен.


Источники

  1. Распределенные системы. Паттерны проектирования. Бёрнс Б.
  2. https://azure.microsoft.com/ru-ru/resources/designing-distributed-systems/
  3. https://docs.microsoft.com/ru-ru/azure/architecture/patterns/ambassador
  4. https://matthewpalmer.net/kubernetes-app-developer/articles/multi-container-pod-design-patterns.html