В конце прошлого года компания Red Hat опубликовала доклад с описанием принципов, которым должны соответствовать контейнеризированные приложения, стремящиеся к тому, чтобы стать органичной частью «облачного» мира: «Следование этим принципам обеспечит готовность приложений к автоматизируемости на таких платформах для облачных приложений, как Kubernetes», — считают в Red Hat. И мы, изучив этот документ, с их выводами согласны, а посему решили поделиться ими с русскоязычным ИТ-сообществом.
Обратите внимание, что эта статья является не дословным переводом оригинального документа (PDF), подготовленного Bilgin Ibryam — архитектором из Red Hat, активным участником нескольких проектов Apache и автором книг «Camel Design Patterns» и «Kubernetes Patterns», — а представляет основные его тезисы в довольно свободном изложении.
Как правило, с облачными (cloud native) приложениями можно предвидеть отказы, а их функционирование и масштабирование возможно даже тогда, когда нижележащая инфраструктура испытывает проблемы. Чтобы это стало возможным, платформы, предназначенные для запуска таких приложений, накладывают определённые обязательства и ограничения на запускаемые в них приложения. Если кратко, то приложение недостаточно просто поместить в контейнер и запустить — возможность его эффективной оркестровки в платформах вроде Kubernetes требует дополнительных усилий. Каковы же они?
Предлагаемые здесь идеи созданы под вдохновением различных других работ (например,
The Twelve-Factor App), затрагивающих многие области: от управления исходным кодом до моделей масштабируемости приложений. Однако область применения рассматриваемых здесь принципов ограничена проектированием контейнеризированных приложений, основанных на микросервисах, для cloud native-платформ вроде Kubernetes.
В описании всех принципов в качестве основного примитива используется образ контейнера, а в качестве целевого окружения для его запуска — платформа оркестровки контейнеров. Следование этим принципам призвано гарантировать, что (подготовленные в соответствии с ними) контейнеры получат полноценную поддержку в большинстве движков оркестровки, т.е. будут обслуживаться планировщиком, масштабироваться и мониториться автоматизированно. Принципы перечислены в случайном (а не приоритетном) порядке.
Во многих смыслах SCP аналогичен принципу единственной ответственности (Single Responsibility Principle, SRP) в SOLID, говорящему о том, что каждый класс должен иметь одну ответственность. Стоящая за SRP мотивация — должна быть лишь одна причина, которая может привести к изменению класса.
Слово «concern» (переводится как «озабоченность», «беспокойство», «интерес», «задача») подчёркивает, что озабоченность — более высокий уровень абстракции, чем ответственность, который лучше описывает спектр задач контейнера (по сравнению с классом). Если главной мотивацией для SRP является единственная причина для изменения, то для SCP — возможность повторного использования образа контейнера и его заменяемость. Если вы создаёте контейнер, отвечающий за одну задачу, и он полностью её решает, вырастает вероятность повторного использования этого образа в других обстоятельствах.
В общем, принцип SCP гласит, что каждый контейнер должен решать единственную проблему и делать это хорошо (на ум сразу приходит классика из философии UNIX — DOTADIW, «Do one thing and do it well» — прим. перев.). Если же микросервису необходимо отвечать за множество проблем, можно использовать такие паттерны, как sidecar- и init-контейнеры, для объединения множества контейнеров в единую развёртываемую площадку (под), где каждый контейнер будет по-прежнему заниматься единственной задачей.
Контейнеры — унифицированный способ упаковывать и запускать приложения, превращающий их в «чёрный ящик». Однако любой контейнер должен предоставлять программные интерфейсы приложения (API) для окружения, в котором он исполняется, делая возможным мониторинг состояния и поведения контейнера. Это необходимое условие для возможности автоматизации обновлений контейнера и сопровождения его жизненного цикла.
С практической точки зрения, контейнеризированное приложение должно предоставлять хотя бы (как минимум!) API для различных проверок его состояния: liveness (работоспособность) и readiness (готовность к обслуживанию запросов). Ещё лучше, если предлагаются и другие способы отслеживать состояние приложения — в частности, логирование важных событий в STDERR и STDOUT для их последующей агрегации утилитами вроде Fluentd и Logstash, интеграции с инструментами для сборки метрик: OpenTracing, Prometheus и др.
Вывод таков: обращайтесь со своим приложением как с чёрным ящиком, но реализуйте все необходимые API, помогающие платформе следить за приложением и управлять им настолько хорошо, насколько это возможно.
Если HOP говорит о предоставлении API, из которых сможет «читать» платформа, то LCP — это обратная сторона: у вашего приложения должна быть возможность узнавать о событиях из платформы. И даже более того: не только узнавать о них, но и реагировать на них — отсюда происходит и название этого принципа («conformance» переводится как «соответствовать», «согласоваться», «подчиняться правилам»).
У управляющей платформы может быть множество событий, которые помогут в управлении жизненным циклом контейнера, но некоторые из них важнее других. Например, для корректного завершения работы процесса приложению необходимо получить сообщение с соответствующим сигналом (SIGTERM) во избежание срочного прекращения работы через SIGKILL. Бывают и другие значимые события — например, PostStart и PreStop, которые необходимы для «прогрева» приложения в начале его работы или, наоборот, освобождения ресурсов при завершении.
В контейнеризированных приложениях закладывается неизменность (immutability): их собирают один раз, после чего они запускаются без изменений в разных окружениях. Это подразумевает использование внешних инструментов для хранения используемых в их работе данных, а также создание/применение различных конфигураций для разных окружений. Любое изменение в контейнеризированном приложении должно приводить к сборке нового образа контейнера, которые будет использоваться во всех окружениях. Этот же принцип, известный как immutable infrastructure, используется для управления серверной инфраструктурой.
Одна из главных причин перехода на контейнеризированные приложения — контейнеры должны быть настолько недолговечными, насколько это возможно, и готовыми к замене другим контейнером в любой момент времени. Причин заменить контейнер может быть много: проверка состояния, обратное масштабирование (scale down) приложения, миграция на другой хост, нехватка ресурсов…
Поэтому контейнеризированным приложениям необходимо поддерживать своё состояние распределённым и избыточным. Кроме того, приложение должно быстро стартовать и останавливаться и даже быть готовым к внезапному (и полному) аппаратному сбою. Другая полезная практика в реализации этого принципа — создание маленьких контейнеров, т.к. контейнеры автоматически запускаются на разных хостах, и их меньший размер ускорит время запуска (поскольку предварительно их нужно физически скопировать на хостовую систему).
Контейнер должен содержать всё необходимое на момент сборки, полагаясь лишь на наличие ядра Linux (все дополнительные библиотеки «появляются» в момент сборки). Помимо библиотек это означает также необходимость содержать исполняемые среды языков программирования, платформу приложений (если используется) и любые другие зависимости для запуска контейнеризированного приложения. Единственное исключение здесь составляют конфигурации, которые будут разными в разных окружениях и должны предоставляться во время запуска (пример —
Некоторые приложения состоят из множества контейнеризированных компонентов. Например, контейнеризированное веб-приложение может требовать контейнера с базой данных. Этот принцип не предлагает объединять контейнеры: просто у контейнера с базой данных должно быть всё необходимое для её работы, а контейнера с веб-приложением — для работы веб-приложения (веб-сервер и т.д.).
Принцип S-CP рассматривает контейнеры с перспективы времени сборки и результирующего бинарника с его содержимым, однако контейнер — это не одномерный чёрный ящик, лежащий на диске. Другие «измерения» контейнера появляются при его запуске — это «измерения» потребления памяти, процессора и других ресурсов.
Любой контейнер должен объявлять свои требования к ресурсам и передавать эту информацию платформе, поскольку его запросы на CPU, память, сеть, диск влияют на то, как платформа выполняет планирование, автомасштабирование, управление ресурсами, обеспечивает общий уровень SLA для контейнера. Кроме того, важно, чтобы приложение умещалось в выделенные ей ресурсы. В случае нехватки ресурсов платформа с меньшей вероятностью будет останавливать или мигрировать такие контейнеры.
В дополнение к этим принципам предлагаются менее фундаментальные, но всё же тоже зачастую полезные практики, относящиеся к контейнерам:
Ссылки на дополнительные ресурсы о паттернах и лучших практиках по теме:
Про некоторые из этих принципов — в частности, про Image Immutability Principle (IIP), который мы назвали как «One image to rule them all», и Self-Containment Principle (S-CP) — рассказывалось в нашем докладе «Лучшие практики CI/CD с Kubernetes и GitLab» (по ссылке — текстовая выжимка и полное видео).
Читайте также в нашем блоге:
Обратите внимание, что эта статья является не дословным переводом оригинального документа (PDF), подготовленного Bilgin Ibryam — архитектором из Red Hat, активным участником нескольких проектов Apache и автором книг «Camel Design Patterns» и «Kubernetes Patterns», — а представляет основные его тезисы в довольно свободном изложении.
Как правило, с облачными (cloud native) приложениями можно предвидеть отказы, а их функционирование и масштабирование возможно даже тогда, когда нижележащая инфраструктура испытывает проблемы. Чтобы это стало возможным, платформы, предназначенные для запуска таких приложений, накладывают определённые обязательства и ограничения на запускаемые в них приложения. Если кратко, то приложение недостаточно просто поместить в контейнер и запустить — возможность его эффективной оркестровки в платформах вроде Kubernetes требует дополнительных усилий. Каковы же они?
Подход Red Hat к приложениям cloud native
Предлагаемые здесь идеи созданы под вдохновением различных других работ (например,
The Twelve-Factor App), затрагивающих многие области: от управления исходным кодом до моделей масштабируемости приложений. Однако область применения рассматриваемых здесь принципов ограничена проектированием контейнеризированных приложений, основанных на микросервисах, для cloud native-платформ вроде Kubernetes.
В описании всех принципов в качестве основного примитива используется образ контейнера, а в качестве целевого окружения для его запуска — платформа оркестровки контейнеров. Следование этим принципам призвано гарантировать, что (подготовленные в соответствии с ними) контейнеры получат полноценную поддержку в большинстве движков оркестровки, т.е. будут обслуживаться планировщиком, масштабироваться и мониториться автоматизированно. Принципы перечислены в случайном (а не приоритетном) порядке.
1. Single Concern Principle (SCP)
Во многих смыслах SCP аналогичен принципу единственной ответственности (Single Responsibility Principle, SRP) в SOLID, говорящему о том, что каждый класс должен иметь одну ответственность. Стоящая за SRP мотивация — должна быть лишь одна причина, которая может привести к изменению класса.
Слово «concern» (переводится как «озабоченность», «беспокойство», «интерес», «задача») подчёркивает, что озабоченность — более высокий уровень абстракции, чем ответственность, который лучше описывает спектр задач контейнера (по сравнению с классом). Если главной мотивацией для SRP является единственная причина для изменения, то для SCP — возможность повторного использования образа контейнера и его заменяемость. Если вы создаёте контейнер, отвечающий за одну задачу, и он полностью её решает, вырастает вероятность повторного использования этого образа в других обстоятельствах.
В общем, принцип SCP гласит, что каждый контейнер должен решать единственную проблему и делать это хорошо (на ум сразу приходит классика из философии UNIX — DOTADIW, «Do one thing and do it well» — прим. перев.). Если же микросервису необходимо отвечать за множество проблем, можно использовать такие паттерны, как sidecar- и init-контейнеры, для объединения множества контейнеров в единую развёртываемую площадку (под), где каждый контейнер будет по-прежнему заниматься единственной задачей.
2. High Observability Principle (HOP)
Контейнеры — унифицированный способ упаковывать и запускать приложения, превращающий их в «чёрный ящик». Однако любой контейнер должен предоставлять программные интерфейсы приложения (API) для окружения, в котором он исполняется, делая возможным мониторинг состояния и поведения контейнера. Это необходимое условие для возможности автоматизации обновлений контейнера и сопровождения его жизненного цикла.
С практической точки зрения, контейнеризированное приложение должно предоставлять хотя бы (как минимум!) API для различных проверок его состояния: liveness (работоспособность) и readiness (готовность к обслуживанию запросов). Ещё лучше, если предлагаются и другие способы отслеживать состояние приложения — в частности, логирование важных событий в STDERR и STDOUT для их последующей агрегации утилитами вроде Fluentd и Logstash, интеграции с инструментами для сборки метрик: OpenTracing, Prometheus и др.
Вывод таков: обращайтесь со своим приложением как с чёрным ящиком, но реализуйте все необходимые API, помогающие платформе следить за приложением и управлять им настолько хорошо, насколько это возможно.
3. Life-cycle Conformance Principle (LCP)
Если HOP говорит о предоставлении API, из которых сможет «читать» платформа, то LCP — это обратная сторона: у вашего приложения должна быть возможность узнавать о событиях из платформы. И даже более того: не только узнавать о них, но и реагировать на них — отсюда происходит и название этого принципа («conformance» переводится как «соответствовать», «согласоваться», «подчиняться правилам»).
У управляющей платформы может быть множество событий, которые помогут в управлении жизненным циклом контейнера, но некоторые из них важнее других. Например, для корректного завершения работы процесса приложению необходимо получить сообщение с соответствующим сигналом (SIGTERM) во избежание срочного прекращения работы через SIGKILL. Бывают и другие значимые события — например, PostStart и PreStop, которые необходимы для «прогрева» приложения в начале его работы или, наоборот, освобождения ресурсов при завершении.
4. Image Immutability Principle (IIP)
В контейнеризированных приложениях закладывается неизменность (immutability): их собирают один раз, после чего они запускаются без изменений в разных окружениях. Это подразумевает использование внешних инструментов для хранения используемых в их работе данных, а также создание/применение различных конфигураций для разных окружений. Любое изменение в контейнеризированном приложении должно приводить к сборке нового образа контейнера, которые будет использоваться во всех окружениях. Этот же принцип, известный как immutable infrastructure, используется для управления серверной инфраструктурой.
5. Process Disposability Principle (PDP)
Одна из главных причин перехода на контейнеризированные приложения — контейнеры должны быть настолько недолговечными, насколько это возможно, и готовыми к замене другим контейнером в любой момент времени. Причин заменить контейнер может быть много: проверка состояния, обратное масштабирование (scale down) приложения, миграция на другой хост, нехватка ресурсов…
Поэтому контейнеризированным приложениям необходимо поддерживать своё состояние распределённым и избыточным. Кроме того, приложение должно быстро стартовать и останавливаться и даже быть готовым к внезапному (и полному) аппаратному сбою. Другая полезная практика в реализации этого принципа — создание маленьких контейнеров, т.к. контейнеры автоматически запускаются на разных хостах, и их меньший размер ускорит время запуска (поскольку предварительно их нужно физически скопировать на хостовую систему).
6. Self-Containment Principle (S-CP)
Контейнер должен содержать всё необходимое на момент сборки, полагаясь лишь на наличие ядра Linux (все дополнительные библиотеки «появляются» в момент сборки). Помимо библиотек это означает также необходимость содержать исполняемые среды языков программирования, платформу приложений (если используется) и любые другие зависимости для запуска контейнеризированного приложения. Единственное исключение здесь составляют конфигурации, которые будут разными в разных окружениях и должны предоставляться во время запуска (пример —
ConfigMap
в Kubernetes).Некоторые приложения состоят из множества контейнеризированных компонентов. Например, контейнеризированное веб-приложение может требовать контейнера с базой данных. Этот принцип не предлагает объединять контейнеры: просто у контейнера с базой данных должно быть всё необходимое для её работы, а контейнера с веб-приложением — для работы веб-приложения (веб-сервер и т.д.).
7. Runtime Confinement Principle (RCP)
Принцип S-CP рассматривает контейнеры с перспективы времени сборки и результирующего бинарника с его содержимым, однако контейнер — это не одномерный чёрный ящик, лежащий на диске. Другие «измерения» контейнера появляются при его запуске — это «измерения» потребления памяти, процессора и других ресурсов.
Любой контейнер должен объявлять свои требования к ресурсам и передавать эту информацию платформе, поскольку его запросы на CPU, память, сеть, диск влияют на то, как платформа выполняет планирование, автомасштабирование, управление ресурсами, обеспечивает общий уровень SLA для контейнера. Кроме того, важно, чтобы приложение умещалось в выделенные ей ресурсы. В случае нехватки ресурсов платформа с меньшей вероятностью будет останавливать или мигрировать такие контейнеры.
Другие рекомендации
В дополнение к этим принципам предлагаются менее фундаментальные, но всё же тоже зачастую полезные практики, относящиеся к контейнерам:
- Стремитесь к маленьким образам. Удаляйте временные файлы и избегайте установки ненужных пакетов. Это сокращает не только размер контейнера, но и время сборки, а также время передачи данных по сети при копировании образов.
- Поддерживайте любые UID. Избегайте использования команды sudo или требования конкретного пользователя/UID для запуска контейнера.
- Отмечайте важные порты. Их обозначение с помощью команды
EXPOSE
упрощает использование образов и для людей, и для ПО. - Используйте тома для постоянных данных (таких, что должны быть сохранены после уничтожения контейнера).
- Определяйте метаданные в образах — с помощью тегов, лейблов, аннотаций. Это упрощает их дальнейшее использование разработчиками.
- Синхронизируйте хост и образ. Некоторым контейнеризированным приложениям может требоваться синхронизация с хостом для определённых атрибутов (например, времени и идентификатора машины).
Ссылки на дополнительные ресурсы о паттернах и лучших практиках по теме:
- Container Patterns (Matthias Luebken);
- Best practices for writing Dockerfiles (Docker);
- Container Best Practices (Project Atomic);
- OpenShift Enterprise 3.0 Creating Images Guidelines (Red Hat);
- Design patterns for container-based distributed systems (Brendan Burns, David Oppenheimer);
- Kubernetes Patterns (Bilgin Ibryam, Roland Hu?);
- The Twelve-Factor App (Adam Wiggins).
P.S. от переводчика
Про некоторые из этих принципов — в частности, про Image Immutability Principle (IIP), который мы назвали как «One image to rule them all», и Self-Containment Principle (S-CP) — рассказывалось в нашем докладе «Лучшие практики CI/CD с Kubernetes и GitLab» (по ссылке — текстовая выжимка и полное видео).
Читайте также в нашем блоге:
- «Смерть микросервисного безумия в 2018 году»;
- «Путеводитель CNCF по решениям Open Source (и не только) для cloud native»;
- «Сколько разработчиков думают, что Continuous Integration не нужна?»;
- «Наш опыт с Kubernetes в небольших проектах» (видео доклада, включающего в себя знакомство с техническим устройством Kubernetes).
Комментарии (5)
gitKroz
14.04.2018 07:47Стремитесь к маленьким образам. Удаляйте временные файлы и избегайте установки ненужных пакетов.
С одной стороны да. С другой стороны это доставляет много хлопот при отладке. Благо хоть vim обычно устанавливают, а вот tcpdump, curl, snmpwalk и т. п. часто не хватает.
К вопросу временной установки нужных утилит на время отладки с последующим передеплоем по завершению относятся неоднозначно.
Кто как решает данный вопрос?VolCh
14.04.2018 09:41Временной установкой обычно решаю. С пакетами для отладки или ручного мониторинга сложный вопрос в целом — никогда не знаешь, что понадобится тому, кто залезет в образ и залезет ли вообще кто-то хоть раз. Иногда, для каких-то исключительных случаев — включение в образ, как правило временное.
VolCh
Вот с позиции разработчика, который не сильно разбирается в администрировании, но которому часто приходится создавать свои и запускать их и чужие образы, две самые большие проблемы заключались в согласовании uid и передаче секретов.
samizdam
UID хостового пользователя в моей практике приходится использовать при запуске команд, в основном сборки приложения и разных кодогенераторов, dev tools. Чтобы артефакты, которые оказываются в файловой системе хоста и внутри проекта принадлежали пользователю (разработчику).
В runtime внутри контейнера с приложением как правило используется root, либо служебный пользователь приложения, как он определён вендором образа.
VolCh
Ещё при отладке приложения, когда монтируешь некоторые или все каталоги проекта в контейнер и окахывается, что каталог cache или log от рута или вообще 14345 юзера. Ещё когда шаришь данные между контейнерами или котнейнером и хостом (например для бэкапа) через тома, а вендоры образа разных пользователей ставят. В общем разные кейсы есть, когда крайне желательно использовать
--user
как при разработке и тестировании, так и при эксплуатации. Но далеко не все даже именитые вендоры такую возможность поддерживают. Ну и в своих образах обычно после пары попыток отказываешься от идеи uid/gid agnostic образов — всё от рута или системных пользователей типа www-data.Есть ещё мэппинг, но очень неюзабельный