5 принципов здравого смысла для создания cloud-native apps
«Облачно-ориентированные» (cloud native) или просто «облачные» приложения создаются специально для работы в облачных инфраструктурах. Обычно они строятся как набор слабо связанных микросервисов, упакованных в контейнеры, которые, в свою очередь управляются облачной платформой. Такие приложения по умолчанию готовы к сбоям, а значит надежно работают и масштабируются даже при серьезных отказах инфраструктурного уровня. Обратная сторона медали — наборы ограничений (контракты), которые облачная платформа накладывает на контейнерные приложения, чтобы иметь возможность управлять ими в автоматическом режиме.
Прекрасно осознавая необходимость и важность перехода на облачные приложения, многие организации все еще не знают, с чего начать. В этом посте мы рассмотрим ряд принципов, соблюдение которых при разработке контейнерных приложений позволит реализовать потенциал облачных платформ и добиться надежной работы и масштабирования приложений даже при серьезных отказах на уровне ИТ-инфраструктуры. Конечная цель изложенных здесь принципов — научиться создавать приложения, которые могут автоматически управляться облачными платформами, такими как Kubernetes.
Принципы проектирования программного обеспечения
В мире программирования под принципами понимаются довольно общие правила, которые необходимо соблюдать при разработке ПО. Их можно применять при работе с любым языком программирования. У каждого принципа есть свои цели, инструментом достижения которых обычно служат шаблоны и практики. Также существует ряд фундаментальных принципов создания качественного софта, из которых вытекают все остальные. Приведем примеры фундаментальных принципов:
- KISS (Keep it simple, stupid) — не усложнять;
- DRY (Don«t repeat yourself) — не повторяться;
- YAGNI (You aren«t gonna need it) — не создавать то, в чем нет непосредственной нужды;
- SoC (Separation of concerns) — разделять ответственности.
Как видно, эти принципы не задают никаких конкретных правил, а относятся к разряду так называемых соображений здравого смысла на основе практического опыта, которые разделяют многие разработчики и на которые они же регулярно ссылаются.
Кроме того, существует SOLID — набор из первых пяти принципов объектно-ориентированного программирования и проектирования, сформулированных Робертом Мартином. SOLID включает в себя обобщенные и открытые для трактовки взаимодополняющие принципы, которые — если применять их в комплексе — помогают создавать более качественные программные системы и лучше поддерживать их в долгосрочной перспективе.
Принципы SOLID относятся к сфере ООП и формулируются на языке таких понятий и концепций, как классы, интерфейсы и наследование. По аналогии, для облачных приложений тоже можно сформулировать принципы разработки, только базовым элементом здесь будет не класс, а контейнер. Следуя этим принципам, можно создавать контейнерные приложения, которые лучше отвечают целям и задачам облачных платформ вроде Kubernetes.
Облачно-ориентированные контейнеры: подход Red Hat
Сегодня в контейнеры можно относительно легко упаковать практически любое приложение. Но для того, чтобы приложения эффективно автоматизировались и оркестрировались в рамках облачной платформы типа Kubernetes, требуется приложить дополнительные усилия.
Основой для изложенных ниже идей послужила методология The Twelve-Factor App и множество других работ по различным аспектам создания веб-приложений, от управления исходным кодом до моделей масштабирования. Описываемые принципы относятся только к разработке контейнерных приложений, которые построены на основе микросервисов и предназначены для облачных платформ, таких как Kubernetes. Базовым элементом в наших рассуждениях служит образ контейнера, а под целевой средой выполнения контейнеров понимается платформа оркестрации контейнеров. Цель предлагаемых принципов состоит в том, чтобы создавать контейнеры, для которых на большинстве платформ оркестрации можно автоматизировать задачи диспетчеризации (scheduling — выбор хоста для запуска экземпляра контейнера), масштабирования и мониторинга. Принципы излагаются в произвольном порядке.
Принцип единственной задачи (Single Concern Principle, SCP)
Этот принцип во многом похож на принцип единственной ответственности (Single Responsibility Principle, SRP), который входит в набор SOLID и гласит, что каждый объект должен иметь одну обязанность, и эта обязанность должна быть полностью инкапсулирована в класс. Суть SRP в том, что каждая обязанность — это причина для изменений, а класс должен иметь одну и только одну причину для изменения.
В SCP вместо слова «ответственность» (responsibility) мы используем слово «задача» (concern), чтобы указать на более высокий уровень абстракции и более широкое назначение контейнера по сравнению с ООП-классом. И если цель SRP — иметь только одну причину для изменений, то за SCP стоит желание расширить возможности повторного использования и замены контейнеров. Следуя SRP и создавая контейнер, который решает одну единственную задачу и делает это функционально законченным образом, вы повышаете шансы на повторное использование образа этого контейнера в различных контекстах приложения.
Принцип SCP гласит, что каждый контейнер должен решать одну единственную задачу и делать это хорошо. Причем, SCP в мире контейнеров достигается проще, чем SRP в мире ООП, поскольку контейнеры обычно выполняют один единственный процесс, и большую часть времени этот процесс решает одну единственную задачу.
Если какой-то контейнерный микросервис должен решать сразу несколько задач, то его можно разбить на однозадачные контейнеры и объединить их в рамках одного pod-а (единицы развертывания контейнерной платформы) с помощью шаблонов sidecar и init-контейнеров. Кроме того, SCP облегчает замену старого контейнера (например, веб-сервера или брокера сообщений) на новый, который решает ту же задачу, но имеет расширенную функциональность или лучше масштабируется.
Принцип удобства мониторинга (High Observability Principle, HOP)
При использовании контейнеров в качестве унифицированного способа упаковки и запуска приложений сами приложения рассматриваются как «черный ящик». Однако, если это облачные контейнеры, то они должны предоставлять среде выполнения специальные API-интерфейсы, чтобы контролировать исправность контейнеров и при необходимости принимать соответствующим меры. Без этого не получится унифицировать автоматизацию обновления контейнеров и управления их жизненным циклом, что, в свою очередь, ухудшит устойчивость и удобство использования программной системы.
На практике контейнерное приложение должно, как минимум, иметь API для различных типов проверок исправности: тестов на активность (liveness) и тестов на готовность (readiness). Если приложение претендует на большее, то должно предоставлять и другие средства контроля своего состояния. Например, регистрацию важных событий через STDERR и STDOUT для агрегирования журналов с помощью Fluentd, Logstash и других подобных инструментов. А также интеграцию с библиотеками трассировки и сбора метрик, такими как OpenTracing, Prometheus и т. п.
В общем, приложение можно по-прежнему рассматривать как «черный ящик», но при этом его надо снабдить всеми API, которые необходимы платформе для того, чтобы мониторить и управлять им наилучшим образом.
Принцип подстраивания к жизненному циклу (Life-cycle Conformance Principle, LCP)
LCP — это антитеза HOP. Если HOP гласит, что контейнер должен предоставлять платформе API-интерфейсы для чтения, то LCP требует от приложения способности воспринимать информацию от платформы. Причем, контейнер должен не только получать события, но и подстраиваться, иначе говоря, реагировать на них. Отсюда и название принципа, который можно рассматривать как требование предоставлять платформе API-интерфейсы для записи.
У платформ есть разные типы событий, помогающих управлять жизненным циклом контейнера. Но решать, какие из них воспринимать и как реагировать, должно само приложение.
Понятно, что одни события важнее других. Например, если приложение плохо переносит аварийное завершение работы, то оно обязано принимать сообщения signal: terminate (SIGTERM) и как можно быстрее инициировать свою процедуру завершения, чтобы успеть до поступления signal: kill (SIGKILL), который идет после SIGTERM.
Кроме того, для жизненного цикла приложения могут быть важны такие события, как PostStart и PreStop. Например, после запуска приложению может требоваться определенное время на «прогрев», прежде чем оно сможет отвечать на запросы. Или приложение должно каким-то особым образом высвобождать ресурсы при завершении работы.
Принцип неизменности контейнерного образа (Image Immutability Principle, IIP)
Общепринято, что контейнерные приложения должны оставаться неизменными после сборки, даже если запускаются в разных средах. Отсюда вытекает необходимость экстернализировать хранение данных на этапе выполнения (иначе говоря, использовать для этого внешние средства), а также полагаться на внешние, настроенные под конкретную среду выполнения, конфигурации, вместо того чтобы модифицировать или создавать уникальные контейнеры для каждой среды. После любых изменений в приложении контейнерный образ должен собираться заново и развертываться во всех используемых средах. Кстати, при управлении ИТ-системами используется схожий принцип, известный как принцип неизменности серверов и инфраструктуры.
Цель IIP — предотвратить создание отдельных контейнерных образов для разных сред выполнения и использовать везде один и тот же образ вместе с соответствующей конфигурацией для конкретной среды. Следование этому принципу позволяет реализовать такие важные с точки зрения автоматизации облачных систем практики, как откат (roll-back) и накат (roll-forward) обновлений приложения.
Принцип одноразовости процессов (Process Disposability Principle, PDP)
Одной из важнейших характеристик контейнера является его эфемерность: экземпляр контейнера легко создается и легко уничтожается, поэтому его в любой момент можно легко заменить на другой экземпляр. Причин для такой замены может быть масса: провал теста на исправность, масштабирование приложения, перенос на другой хост, исчерпание ресурсов платформы или другие ситуации.
Как следствие, контейнерные приложения должны сохранять свое состояние с помощью каких-то внешних средств, либо использовать для этого внутренние распределенные схемы с избыточностью. Кроме того, приложение должно быстро запускаться и быстро завершать работу, а также быть готовыми к внезапному фатальному отказу оборудования.
Одна из практик, помогающая реализовать этот принцип, заключается в том, чтобы создавать контейнеры небольшого размера. Облачные среды могут автоматически выбирать хост для запуска экземпляра контейнера, поэтому чем меньше размер контейнера, тем быстрее он запустится — он просто быстрее скопируется на целевой хост по сети.
Принцип самодостаточности (Self-containment Principle, S-CP)
Согласно этому принципу, на этапе сборки в состав контейнера включаются все необходимые компоненты. Контейнер должен строиться в расчете на то, что в системе есть только чистое ядро Linux, поэтому все необходимые дополнительные библиотеки надо размещать в самом контейнере. Там же должны располагаться такие вещи, как среда выполнения для соответствующего языка программирования, платформа приложений (при необходимости) и прочие зависимости, которые потребуются во время работы контейнерного приложения.
Исключения делаются лишь для конфигураций, которые варьируются от среды к среде, и должны предоставляться на этапе выполнения, например, через Kubernetes ConfigMap.
Приложение может включать в себя несколько контейнеризованных компонентов, например, отдельный контейнер СУБД в составе контейнерного веб-приложения. Согласно принципу S-CP, эти контейнеры надо не объединять в один, а сделать так, чтобы контейнер СУБД содержал в себе все необходимое для работы базы данных, а контейнер веб-приложения — все необходимое для работы веб-приложения, тот же веб-сервер. В результате, во время выполнения контейнер веб-приложения будет зависеть от контейнера СУБД и обращаться к нему по мере надобности.
Принцип ограничения на этапе выполнения (Runtime Confinement Principle, RCP)
Принцип S-CP определяет, как должен собираться контейнер и что должен содержать двоичный файл образа. Но контейнер — это не просто «черный ящик», у которого есть только одна характеристика — размер файла. Во время выполнения контейнер обретает и другие измерения: объем используемой памяти, процессорное время и другие системные ресурсы.
И здесь пригодится принцип RCP, согласно которому контейнер должен декапировать свои требования к системным ресурсам и передавать их платформе. Имея ресурсные профили каждого контейнера (сколько ему нужно ресурсов ЦП, памяти, сети и дисковой системы), платформа может оптимальным образом выполнять диспетчеризицию и автомасштабирование, управлять ИТ-мощностями и поддерживать SLA-уровни для контейнеров.
Помимо удовлетворения требований к ресурсам контейнера, приложению также важно не выходить за обозначенные им самим рамки. В противном случае, при возникновении дефицита ресурсов платформа с большей вероятностью включит его в список приложений, которые надо прервать или мигрировать.
Говоря об ориентированности на облако, мы прежде всего имеем в виду способ работы.
Выше мы сформулировали ряд общих принципов, которые задают методологический фундамент для построения качественных контейнерных приложений для облачных сред.
Отметим, что помимо этих общих принципов вам также понадобятся дополнительные продвинутые методы и техники работы с контейнерами. Кроме того, у нас есть несколько коротких рекомендаций, которые имеют более конкретный характер и должны применяться (или не применяться) в зависимости от ситуации:
- Старайтесь уменьшать размер образов: удаляйте временные файлы и не ставьте ненужные пакеты — чем меньше размер контейнера, тем быстрее он собирается и копируется на целевой хост по сети.
- Ориентируйтесь на произвольные User-ID: не используйте команду sudo или какие-то особенных userid для запуска своих контейнеров.
- Маркируйте важные порты: номера портов можно задавать и во время выполнения, но лучше указать их с помощью команды EXPOSE — другим людям и программам будет проще использовать ваши образы.
- Храните постоянные данные на томах: данные, которые должны остаться после уничтожения контейнера, следует записывать на тома.
- Прописывайте метаданые образа: теги, метки и аннотации облегчают использование образов — другие разработчики будут вам благодарны.
- Синхронизируйте хост и образы: для некоторых контейнерных приложений требуется синхронизации контейнера с хостом по определенным атрибутам, таким как время или идентификатор машины.
- В заключение делимся шаблонами и лучшими практиками, которые помогут эффективнее реализовать перечисленные выше принципы:
www.slideshare.net/luebken/container-patterns
docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices
docs.projectatomic.io/container-best-practices
docs.openshift.com/enterprise/3.0/creating_images/guidelines.html
www.usenix.org/system/files/conference/hotcloud16/hotcloud16_burns.pdf
leanpub.com/k8spatterns
12factor.net
Вебинар по новой версии OpenShift Container Platform — 4
11 июня в 11.00
Что вы узнаете:
- Immutable Red Hat Enterprise Linux CoreOS
- OpenShift service mesh
- Operator framework
- Knative framework