Архитектурное проектирование приложений в Kubernetes

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

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

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

Проектирование масштабируемых приложений

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

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

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

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

12 факторов

Популярная методология, которая может помочь вам сосредоточиться на наиболее важных характеристиках – это философия «Двенадцать факторов». Она создана, чтобы помочь разработчикам и группам понять основные качества, которыми пользуются веб-сервисы, предназначенные для работы в облаке, и описать принципы, применимые к программному обеспечению, которое будет жить в кластерной среде, такой как Kubernetes. Монолитные приложения тоже могут извлечь выгоду из этих рекомендаций, но особенно хорошо они работают для архитектуры микросервисов.

Краткое содержание этих факторов:

  1. Кодовая база: Поддерживайте одну кодовую базу, отслеживаемую в системе контроля версий. Кодовая база диктует, что нужно развернуть.
  2. Зависимости: Явно объявляйте и изолируйте зависимости (храните их в коде или указывайте версии в формате, который может установить менеджер пакетов).
  3. Конфигурация: Отделите параметры конфигурации от приложения и определите их в среде выполнения.
  4. Сторонние службы (Backing Services): Считайте сторонние службы  подключаемыми ресурсами. Локальные и удаленные службы абстрагируются как доступные для сети ресурсы с данными о соединении, заданными в конфигурации.
  5. Сборка, релиз, выполнение: Строго разделяйте стадии сборки и выполнения. Этап сборки создает артефакт развертывания из исходного кода, этап релиза объединяет артефакт и конфигурацию, а этап выполнения выполняет релиз.
  6. Процессы: Запускайте приложение как один или несколько процессов, не сохраняющих внутреннее состояние (stateless).
  7. Привязка портов: Экспортируйте сервисы через привязку портов. Приложения должны привязываться к порту и прослушивать соединения. Перенаправление запросов следует обрабатывать извне.
  8. Параллелизм: Масштабируйте приложение с помощью процессов. Одновременное выполнение нескольких копий приложения, возможно, на нескольких серверах, позволяет масштабировать, не корректируя код приложения.
  9. Утилизируемость (Disposability): Процессы должны быть в состоянии быстро запускаться и прекращать работу без серьезных побочных эффектов.
  10. Паритет разработки/работы приложения: Среда разработки, промежуточного развёртывания (staging) и рабочего развёртывания (production) должны быть максимально похожими.
  11. Журналирование: Приложения должны передавать журналы на стандартный вывод, чтобы внешние службы могли решить, как лучше их обрабатывать.
  12. Задачи администрирования: Выполняйте задачи администрирования/управления с помощью разовых процессов.

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

Контейнеризация компонентов приложения

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

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

Оптимизация контейнеров

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

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

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

Обычно при этом рекомендуется создавать производственные образы на основе минимального родительского образа. Если вы хотите полностью избежать излишеств готовых образов дистрибутивов (например, образ ubuntu:16.04 включает почти полную среду Ubuntu 16.04), вы можете создавать свои образы с нуля – на основе минимального образа Docker scratch. Тем не менее, образ scratch не обеспечивает доступ ко многим основным инструментам и часто нарушает предположения о среде, которую поддерживает какое-либо программное обеспечение. В качестве альтернативы часто используется alpine – образ Alpine Linux, который завоевал популярность благодаря надежной минимальной базовой среде, обеспечивающей полнофункциональный дистрибутив Linux.

Для интерпретируемых языков, таких как Python или Ruby, парадигма слегка меняется, поскольку здесь нет стадии компиляции, и интерпретатор должен быть доступен для запуска кода в процессе производства. Однако поскольку здесь также нужно использовать минимальные образы, на Docker Hub доступно много разных оптимизированных образов на основе Alpine Linux. Преимущества использования таких образов для интерпретируемых языков аналогичны преимуществам для компилируемых языков: Kubernetes может быстро извлечь все необходимые образы контейнеров на новые ноды, чтобы начать работу.

Определение сферы применения контейнеров и подов

Хотя для работы в кластере Kubernetes приложения должны быть контейнерами, наименьшей единицей абстракции, которой Kubernetes может управлять напрямую, являются поды. Под представляет собой объект Kubernetes, состоящий из одного или нескольких тесно связанных контейнеров. Контейнеры в подах имеют единый жизненный цикл и управляются как единое целое. Например, контейнеры всегда планируются на одной ноде, запускаются или останавливаются вместе и используют одни ресурсы (файловые системы и IP-пространство).

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

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

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

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

Улучшение функциональности подов

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

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

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

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

Извлечение конфигураций в ConfigMaps и Secrets

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

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

Secrets – это аналогичный тип объекта Kubernetes, используемый для безопасного хранения конфиденциальных данных и выборочного доступа к портам и другим компонентам. Secrets — это удобный способ передачи конфиденциального материала в приложения без сохранения их в виде обычного текста в легкодоступных местах в вашей обычной конфигурации. Функционально они работают так же, как и ConfigMaps, поэтому приложения могут использовать данные из ConfigMaps и Secrets с помощью одних и тех же механизмов.

ConfigMaps и Secrets помогают избежать помещения конфигурации непосредственно в определениях объектов Kubernetes. Вы можете указать ключ конфигурации вместо самого значения, это позволяет обновлять конфигурацию «на ходу», изменяя ConfigMap или Secret. Также это дает возможность изменять активное поведение runtime подов и других объектов Kubernetes, не изменяя определения ресурсов Kubernetes.

Готовность и живучесть приложения

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

Liveness определяет живучесть и работоспособность приложения внутри контейнера. Kubernetes может периодически запускать команды внутри контейнера, чтобы проверять поведение базового приложения, или может отправлять сетевые запросы HTTP или TCP в указанное место, чтобы определить, доступен ли этот процесс и как он отвечает. Если проверка не удалась, Kubernetes перезапускает контейнер, чтобы попытаться восстановить функциональность внутри контейнера.

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

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

Использование развертываний для управления масштабируемостью и доступностью

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

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

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

Создание сервисов и правил для управления доступом к уровням приложений

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

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

Доступ к внутренним сервисам

Чтобы эффективно использовать сервисы, сначала необходимо определить предполагаемых потребителей для каждой группы подов. Если сервис будет использоваться только другими приложениями, развернутыми в кластере Kubernetes, тип сервиса clusterIP позволит подключаться к набору подов, используя стабильный IP-адрес, который маршрутизируется только внутри кластера. Любой объект, развернутый в кластере, сможет связываться с группой реплицированных подов, отправляя трафик непосредственно на IP-адрес сервиса. Это самый простой тип сервиса, который хорошо работает для внутренних уровней приложения.

DNS-аддон позволяет Kubernetes предоставлять сервисам DNS-имена. Это позволяет подам и другим объектам связываться с сервисами по имени вместо IP-адресов. Этот механизм существенно не влияет на использование сервиса, но идентификаторы на основе имени могут упростить подключение компонентов или определение взаимодействий, если адрес не известен заранее.

Открытие публичных сервисов

Если интерфейс должен быть общедоступным, лучшим вариантом является, как правило, тип сервиса load balancer. Он использует API конкретного облачного провайдера для предоставления балансировки нагрузки, что обслуживает трафик на поды через общедоступный IP-адрес. Это позволяет маршрутизировать внешние запросы на поды сервиса, предлагая контролируемый сетевой канал для внутренней сети кластера.

Поскольку тип сервиса балансировки нагрузки создает балансировщик для каждого сервиса, открывать сервисы Kubernetes по этому методу потенциально может стать затратно. Чтобы упростить это, можно использовать объекты ingress Kubernetes для описания способов маршрутизации различных типов запросов к различным сервисам на основе заранее определенного набора правил. Например, запросы на «example.com» могут отправляться на сервис A, а запросы для «8host.com» могут быть перенаправлены на сервис B. Объекты Ingress позволяют логически маршрутизировать смешанный поток запросов к их целевым сервисам.

Правила Ingress должны интерпретироваться входным контроллером (ingress controller) — обычно это какой-то балансировщик нагрузки типа Nginx; он развертывается внутри кластера в виде пода, который реализует правила доступа и пересылает трафик на сервисы Kubernetes. В настоящее время тип объекта ingress находится в бета-версии, но есть несколько рабочих реализаций, которые можно использовать для минимизации количества внешних балансировщиков нагрузки.

Использование декларативного синтаксиса для управления состоянием Kubernetes

Kubernetes очень гибок в определении и контроле ресурсов, развернутых в кластере. Используя такие инструменты, как kubectl, вы можете явно определить специальные объекты, которые нужно сразу развернуть в вашем кластере. Это может быть полезно для быстрого развертывания ресурсов при изучении Kubernetes, но у такого подхода есть недостатки, из-за которых его нежелательно использовать для долгосрочного управления производством.

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

К счастью, Kubernetes предоставляет альтернативный декларативный синтаксис, который позволяет полностью определять ресурсы в текстовых файлах, а затем использовать kubectl для применения или изменения конфигурации. Хранение этих файлов конфигурации в репозитории управления версиями – простой способ отслеживания изменений и интеграции. Управление файлами также упрощает адаптацию существующих шаблонов к новым ресурсам путем копирования и редактирования существующих определений. Сохранение определений объектов Kubernetes в версированных каталогах позволяет сохранять снапшоты состояний кластера. Это очень важно во время операций восстановления, миграции или отслеживания основной причины появления в системе непредвиденных изменений.

Заключение

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

Tags: