Паттерны kubernetes
Паттерны kubernetes
Паттерны kubernetes
2020
Научный редактор М. Малявин
Переводчик А. Макарова
Литературный редактор А. Руденко
Художники В. Мостипан, А. Шляго (Шантурова)
Корректоры С. Беляева, Н. Викторова
Верстка Л. Егорова
Билджин Ибрам, Роланд Хасс
Паттерны Kubernetes: Шаблоны разработки собственных
облачных приложений. — СПб.: Питер, 2020.
ISBN 978-5-4461-1443-6
© ООО Издательство "Питер", 2020
Все права защищены. Никакая часть данной книги не может
быть воспроизведена в какой бы то ни было форме без
письменного разрешения владельцев авторских прав.
Предисловие
Начиная работу над фреймворком Kubernetes почти пять лет
назад, мы — Крейг, Джо и я — понимали, что он способен
изменить мир разработки и доставки программного
обеспечения. Но мы не думали, что это изменение произойдет
так быстро. В настоящее время Kubernetes служит основой для
создания переносимых и надежных систем для основных
общедоступных и частных облаков, а также
невиртуализированных окружений. Несмотря на широкую
распространенность фреймворка Kubernetes, благодаря
которой кластер в облаке можно развернуть менее чем за пять
минут, многие недостаточно четко представляют, что делать
дальше, после создания этого кластера. Мы добились
значительных успехов в практической реализации самого
Kubernetes, но это только часть решения. Это фундамент, на
котором создаются приложения. Он предлагает обширную
библиотеку инструментов для их создания, но почти не дает
советов и рекомендаций архитекторам или разработчикам
приложений, как можно объединить различные части этого
фундамента, чтобы получить законченную надежную систему,
соответствующую целям и потребностям.
Представление о том, что же дальше делать с кластером
Kubernetes, можно получить из прошлого опыта работы с
аналогичными системами или прибегнуть к методу проб и
ошибок, но такой путь обходится слишком дорого с точки
зрения времени и качества систем, предлагаемых нашим
конечным пользователям. Для тех, кто решит предоставлять
критически важные услуги на основе таких систем, как
Kubernetes, обретение опыта методом проб и ошибок займет
слишком много времени и приведет к очень серьезным
проблемам, связанным с простоями и сбоями.
Вот почему книга Билджина и Роланда имеет особую
ценность. «Паттерны Kubernetes» знакомят вас с опытом,
который мы вложили в API и инструменты, составляющие
Kubernetes. Фреймворк Kubernetes является воплощением
опыта, накопленного сообществом разработчиков,
занимающихся созданием высоконадежных распределенных
систем в разных окружениях. Каждый объект и каждая
возможность, добавленные в Kubernetes, — это
основополагающий инструмент, разработанный и созданный
специально для удовлетворения конкретной потребности. В
этой книге рассказывается, как использовать идеи, заложенные
в Kubernetes, для решения практических задач и построения
своей системы.
Работая над Kubernetes, мы всегда говорили, что наша
главная цель — максимально упростить разработку
распределенных систем, и именно такие книги наглядно
показывают, насколько мы преуспели в этом. Билджин и
Роланд отобрали основные инструменты разработчика
Kubernetes и разбили их на группы, упростив их изучение и
применение. К концу этой книги вы будете знать не только о
компонентах, доступных вам в Kubernetes, но и о том, «как» и
«зачем» строить системы с использованием этих компонентов.
Брендан Бернс (Brendan Burns), разработчик Kubernetes
Вступление
В последние годы с развитием микросервисов и контейнеров
способы проектирования, разработки и эксплуатации ПО
значительно изменились. Современные приложения
оптимизируются в целях масштабируемости, эластичности,
отказоустойчивости и быстрого изменения. Для соответствия
новым принципам эти современные архитектуры требуют
другого набора паттернов и практик. Цель этой книги —
помочь разработчикам создавать облачные приложения с
использованием Kubernetes в качестве платформы времени
выполнения. Для начала кратко познакомимся с двумя
основными составляющими этой книги: фреймворком
Kubernetes и паттернами проектирования.
Kubernetes
Kubernetes — это платформа для управления контейнерами.
Зарождение Kubernetes произошло где-то в центрах обработки
данных компании Google, где появилась внутренняя
платформа управления контейнерами Borg
(https://research.google.com/pubs/pub43438.html). Платформа
Borg много лет использовалась в Google для запуска
приложений. В 2014 году Google решил передать свой опыт
работы с Borg новому проекту с открытым исходным кодом
под названием Kubernetes (в переводе с греческого «кормчий»,
«рулевой»), а в 2015 году он стал первым проектом,
переданным в дар недавно основанному фонду Cloud Native
Computing Foundation (CNCF).
С самого начала проект Kubernetes приобрел целое
сообщество пользователей, и число участников росло
невероятно быстрыми темпами. В настоящее время Kubernetes
считается одним из самых активных проектов на GitHub.
Можно даже утверждать, что на момент написания этой книги
Kubernetes был наиболее часто используемой и
многофункциональной платформой управления контейнерами.
Kubernetes также формирует основу других платформ,
построенных поверх него. Наиболее известной из таких систем
вида «платформа как услуга» (Platform-as-a-Service) является
Red Hat OpenShift, которая добавляет в Kubernetes различные
дополнительные возможности, в том числе способы создания
приложений на этой платформе. Это только часть причин, по
которым мы выбрали Kubernetes в качестве эталонной
платформы для описания паттернов использования облачных
систем в этой книге.
Эта книга предполагает наличие у читателя некоторых
базовых знаний о Kubernetes. В главе 1 мы перечислим
основные понятия Kubernetes и заложим основу для
обсуждения паттернов в следующих главах.
Паттерны проектирования
Понятие паттернов, или шаблонов, проектирования появилось
в 1970-х годах в области архитектуры. Кристофер Александер
(Christopher Alexander), архитектор и системный теоретик, и
его команда опубликовали в 1977 году новаторский труд «A
Pattern Language»1 (Oxford University Press), в котором
описываются архитектурные шаблоны создания городов,
зданий и других строительных проектов. Некоторое время
спустя эта идея была принята недавно сформировавшейся
индустрией программного обеспечения. Самая известная
книга в этой области — «Приемы объектно-ориентированного
проектирования. Паттерны проектирования» Эриха Гаммы,
Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса —
«Банды четырех» (Addison-Wesley). Когда мы говорим об
известных паттернах «Одиночка» (Singleton), «Фабрика»
(Factories) или «Делегирование» (Delegation), то используем
названия, данные в этой книге. С тех пор было написано много
других замечательных книг о паттернах для различных
областей с разной степенью подробностей, таких как
«Enterprise Integration Patterns»2 Грегора Хопа (Gregor Hohpe) и
Бобби Вульфа (Bobby Woolf ) или «Patterns of Enterprise
Application Architecture»3 Мартина Фаулера (Martin Fowler).
Проще говоря, паттерн описывает повторимое решение
задачи4. Паттерн отличается от рецепта тем, что вместо
пошаговых инструкций для решения задачи он определяет
план решения целого класса подобных задач. Например,
паттерн Александера «Пивная» описывает, как следует строить
питейные заведения, чтобы они стали местами, где
«незнакомцы и друзья становятся собутыльниками», а не
«пристанищами для одиночек». Все заведения, построенные по
этому шаблону, выглядят по-разному, но имеют общие черты,
такие как открытые ниши для групп от четырех до восьми
человек и общий зал, где сотни людей могут вместе выпивать,
веселиться, слушать музыку или делать что-то еще.
Однако паттерны не просто предоставляют решения. Они
также формируют язык. Уникальные названия паттернов
образуют компактный язык, в основе которого лежат
существительные, и каждый паттерн имеет уникальное
название. Когда люди упоминают эти названия в разговорах
между собой, они автоматически вызывают у них похожие
ментальные представления. Например, когда мы говорим о
столе, любой, кто слышит нас, предполагает, что мы говорим о
деревянной столешнице на четырех ножках, на которую можно
класть разные вещи. То же происходит в программной
инженерии, когда мы говорим о «фабрике». В контексте
объектно-ориентированного программирования мы
немедленно связываем с термином «фабрика» некий объект,
который производит другие объекты. Поскольку мы уже знаем
решение, лежащее в основе паттерна, то можем перейти к
решению еще не решенных проблем.
Есть и другие характеристики языка паттернов. Паттерны
взаимосвязаны между собой и могут перекрываться, поэтому
вместе охватывают большую часть пространства задач. Кроме
того, как отмечается в книге «Язык паттернов», паттерны
имеют разные уровни детализации и области действия. Более
общие паттерны охватывают более широкий спектр задач и
предлагают приблизительные рекомендации, касающиеся их
решения. Специализированные паттерны дают очень
конкретное решение, но применяются не так широко. Эта
книга содержит все виды паттернов, и многие паттерны
ссылаются на другие паттерны или даже могут включать
другие паттерны как часть решения.
Другая особенность паттернов заключается в том, что они
следуют жесткому формату. Однако каждый автор определяет
свой формат, и, к сожалению, нет единого стандарта, который
определял бы, как должны излагаться паттерны. Мартин
Фаулер дает превосходный обзор форматов, используемых для
языков паттернов, в своей статье «Writing Software Patterns»
(http://bit.ly/2HIuUdJ).
Структура книги
Мы выбрали простой паттерн структуры книги. Мы не
придерживаемся какого-либо конкретного языка и для
описания каждого паттерна используем следующую структуру:
Название
Каждый паттерн имеет название, которое также является
названием главы. Названия образуют основу языка
паттернов.
Задача
В этом разделе дается широкий контекст и подробное
описание пространства паттерна.
Решение
Этот раздел рассказывает, как паттерн решает проблему
способом, характерным для Kubernetes. Этот раздел также
содержит ссылки на другие паттерны, которые либо
связаны, либо являются частью данного паттерна.
Пояснение
Обзор достоинств и недостатков решения в данном
контексте.
Дополнительная информация
Этот заключительный раздел содержит источники
дополнительной информации, касающейся паттерна.
Мы организовали паттерны в этой книге следующим
образом:
Что вы узнаете
Вы сделаете массу открытий. Некоторые паттерны могут
выглядеть как выдержки из руководства по Kubernetes, но при
ближайшем рассмотрении вы увидите, что паттерны
представлены с концептуальной точки зрения, чего не хватает
в других книгах, посвященных этой теме. Другие описываются
иначе, с подробными рекомендациями для каждой конкретной
задачи, как, например, в части IV Конфигурационные паттерны.
Независимо от степени подробности описания паттерна, вы
узнаете все, что Kubernetes предлагает для каждого
конкретного паттерна, со множеством иллюстративных
примеров. Все эти примеры были протестированы, и мы
расскажем, как получить их исходный код в разделе
«Использование примеров кода».
Прежде чем начать погружение, кратко перечислим, чем не
является эта книга:
Типографские соглашения
Как уже упоминалось, паттерны образуют простой,
взаимосвязанный язык. Чтобы подчеркнуть эту взаимосвязь,
названия паттернов записываются курсивом (например, Sidecar
(Прицеп)). Когда паттерн получает название по базовому
понятию Kubernetes (например, Init Container
(Инициализирующий контейнер) или Controller (Контроллер)),
мы используем такой способ оформления только для прямых
ссылок на сам паттерн. Там, где это имеет смысл, мы также
даем ссылки на главы с описаниями паттернов для упрощения
навигации.
Мы также используем следующие соглашения:
Благодарности
Создание этой книги было долгим путешествием,
продолжавшимся два года, и мы хотим поблагодарить всех
наших рецензентов, помогавшим нам не сбиться с пути.
Особую благодарность хотим выразить Паоло Антинори (Paolo
Antinori) и Андреа Тарокки (Andrea Tarocchi), помогавшим нам
в этом путешествии. Большое спасибо также Марко Лукше
(Marko Luk a), Брендону Филипсу (Brandon Philips), Майклу
Хуттерманну (Michael H ttermann), Брайану Грейсли (Brian
Gracely), Эндрю Блоку (Andrew Block), Иржи Кремсеру (Jiri
Kremser), Тобиасу Шнеку (Tobias Schneck) и Рику Вагнеру (Rick
Wagner), которые поддержали нас своим опытом и советами.
Наконец, но не в последнюю очередь, большое спасибо нашим
редакторам Вирджинии Уилсон (Virginia Wilson), Джону
Девинсу (John Devins), Кэтрин Тозер (Katherine Tozer),
Кристине Эдвардс (Christina Edwards) и всем замечательным
сотрудникам O’Reilly за то, что помогли довести эту книгу до
финала.
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу
[email protected] (издательство «Питер», компьютерная
редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете
подробную информацию о наших книгах.
5 Лукша Марко. Kubernetes в действии. М.: ДМК Пресс, 2018. — Примеч. пер.
Глава 1. Введение
В этой вводной главе мы подготовим основу для остальной
части книги и обсудим важные понятия Kubernetes,
используемые в проектировании и реализации облачных
приложений на основе контейнеров. Понимание этих новых
абстракций, а также связанных с ними принципов и паттернов
из этой книги является ключом к созданию распределенных
приложений, автоматизируемых облачными платформами.
Эта глава не является обязательным условием для
понимания паттернов, описываемых далее. Читатели,
знакомые с понятиями Kubernetes, могут пропустить ее и сразу
перейти к интересующей их категории.
Распределенные примитивы
Чтобы объяснить, что подразумевается под новыми
абстракциями и примитивами, мы сравним их с хорошо
известным объектно-ориентированным программированием
(ООП), например, на языке Java. Во вселенной ООП
используются такие понятия, как класс, объект, пакет,
наследование, инкапсуляция и полиморфизм. Среда
выполнения Java предоставляет конкретные функции и
гарантии управления жизненным циклом наших объектов и
приложения в целом.
Язык Java и виртуальная машина Java (Java Virtual Machine,
JVM) предоставляют локальные, внутрипроцессные
строительные блоки для создания приложений. Kubernetes
добавляет в эту привычную картину совершенно новое
измерение, предлагая новый набор распределенных
примитивов и среды выполнения для создания
распределенных систем, разбросанных по нескольким узлам и
процессам. Используя Kubernetes для реализации всего
поведения приложения, мы больше не полагаемся только на
локальные примитивы.
Timer,
Периодическое выполнение Планировщик заданий
ScheduledExecutorService
Распределенный
Понятие Локальный примитив
примитив
Контроллер набора
Фоновое выполнение Фоновые потоки выполнения
демонов (DaemonSet)
Контейнеры
Контейнеры — это строительные блоки для создания облачных
приложений на основе Kubernetes. Проводя аналогию с ООП и
Java, образы контейнеров можно сравнить с классами, а
контейнеры — с объектами. По аналогии с классами, которые
можно расширять (наследовать) и таким способом изменять их
поведение, мы можем создавать образы контейнеров, которые
расширяют (наследуют) другие образы контейнеров, и таким
способом изменять поведение. По аналогии с объектами,
которые можно объединять и использовать их возможности,
мы можем объединять контейнеры, помещая их в поды (Pod),
и использовать результаты их взаимодействий.
Продолжая сравнение, можно сказать, что Kubernetes
напоминает виртуальную машину Java, но разбросанную по
нескольким хостам и отвечающую за запуск контейнеров и
управление ими.
Init-контейнеры можно сравнить с конструкторами
объектов; контроллеры DaemonSet похожи на потоки
выполнения, действующие в фоновом режиме (как, например,
сборщик мусора в Java). Поды можно считать аналогами
контекста инверсии управления (Inversion of Control, IoC),
используемого, например, в Spring Framework, где несколько
объектов имеют общий управляемый жизненный цикл и могут
напрямую обращаться друг к другу.
Параллели можно было бы проводить и дальше, но не
глубоко. Однако следует отметить, что контейнеры играют
основополагающую роль в Kubernetes, а создание модульных,
многоразовых, специализированных образов контейнеров
является основой успеха любого проекта и экосистемы
контейнеров в целом. Но что еще можно сказать о контейнерах
и их назначении в контексте распределенного приложения,
помимо перечисления технических характеристик образов
контейнеров, которые обеспечивают упаковку и изоляцию?
Вот несколько основных особенностей контейнеров:
• Образ контейнера — это функциональная единица,
решающая одну определенную задачу.
Службы
Группы контейнеров, или поды, — это эфемерные
образования, они могут появляться и исчезать в любое время
по разным причинам: например, в ходе масштабирования
вверх или вниз, в случае неудачи при проверке
работоспособности контейнеров и при миграции узлов. IP-
адрес группы становится известен только после того, как она
будет запланирована и запущена на узле. Группу можно
повторно запланировать для запуска на другом узле, если узел,
на котором она выполнялась, прекратил работу. Все это
означает, что сетевой адрес группы контейнеров может
меняться в течение жизни приложения, и потому необходим
какой-то другой примитив для обнаружения и балансировки
нагрузки.
Роль этого примитива играют службы (Services) Kubernetes.
Служба — это еще одна простая, но мощная абстракция
Kubernetes, которая присваивает имени службы постоянные IP-
адрес и номер порта. То есть служба — это именованная точка
входа для доступа к приложению. В наиболее
распространенном сценарии служба играет роль точки входа
для набора групп контейнеров, но это не всегда так. Служба —
это универсальный примитив и может также служить точкой
входа для доступа к функциональным возможностям за
пределами кластера Kubernetes. Соответственно, примитив
службы можно использовать для обнаружения служб и
распределения нагрузки и для замены реализации и
масштабирования без влияния на потребителей службы.
Подробнее о службах мы поговорим в главе 12 «Обнаружение
служб».
Метки
Как было показано выше, на этапе сборки аналогом
микросервиса является контейнер, а на этапе выполнения —
группа контейнеров. А что можно считать аналогом
приложения, состоящего из нескольких микросервисов?
Kubernetes предлагает еще два примитива, помогающих
провести аналогию с понятием приложения: метки и
пространства имен. Мы подробно рассмотрим пространства
имен в разделе «Пространства имен» ниже.
До появления микросервисов понятию приложения
соответствовала одна единица развертывания с единой схемой
управления версиями и циклом выпуска новых версий.
Приложение помещалось в один файл .war, .ear или в каком-то
другом формате. Но затем приложения были разделены на
микросервисы, которые разрабатываются, выпускаются,
запускаются, перезапускаются и масштабируются независимо
друг от друга. С появлением микросервисов понятие
приложения стало более размытым — больше нет ключевых
артефактов или действий, которые должны выполняться на
уровне приложения. Однако если понадобится указать, что
некоторые независимые службы принадлежат одному
приложению, можно использовать метки. Давайте представим,
что одно монолитное приложение мы разделили на три
микросервиса, а другое — на два.
В этом случае мы получаем пять определений групп
контейнеров (и, может быть, много экземпляров этих групп),
которые не зависят друг от друга с точки зрения разработки и
времени выполнения. Тем не менее нам все еще может
потребоваться указать, что первые три пода представляют
одно приложение, а два других — другое приложение. Поды
могут быть независимыми и представлять определенную
ценность для бизнеса по отдельности, а могут зависеть друг от
друга. Например, один под может содержать контейнеры,
отвечающие за интерфейс с пользователем, а два других — за
реализацию основной функциональности. Если какой-то из
подов прекратит работать, приложение окажется бесполезным
с точки зрения бизнеса. Использование меток дает
возможность определять набор подов и управлять им как
одной логической единицей. На рис. 1.3 показано, как можно
использовать метки для группировки частей распределенного
приложения в конкретные подсистемы.
Рис. 1.3. Метки используются для идентификации подов как принадлежащих одному
приложению
Аннотации
Другой примитив — аннотации — очень похож на метки.
Подобно меткам, аннотации организованы в виде
ассоциативного массива, но предназначены для определения
метаданных, которые используются компьютером, а не
человеком.
Информация в аннотациях не предназначена для
использования в запросах и сопоставления объектов.
Аннотации предназначены для присоединения
дополнительных метаданных к объектам, созданным разными
инструментами и библиотеками. Например, аннотации можно
использовать для добавления номера версии и сборки,
информации об образе, временных меток, имен веток в
репозитории Git, номеров запросов на включение, хешей
образов изображений, адресов в реестре, имен авторов,
сведений об инструментах и многого другого. То есть метки
используются главным образом для поиска и выполнения
действий с соответствующими ресурсами, а аннотации — для
прикрепления метаданных, которые могут использоваться
компьютером.
Пространства имен
Другой примитив, который также может помочь в управлении
группой ресурсов, — пространство имен Kubernetes. Как уже
отмечалось выше, пространство имен может показаться
похожим на метку, но в действительности это совсем другой
примитив со своими характеристиками и назначением.
Пространства имен Kubernetes позволяют разделить кластер
Kubernetes (который обычно распределяется по нескольким
хостам) на логические пулы ресурсов. Пространства имен
определяют области для ресурсов Kubernetes и предоставляют
механизм для авторизации и применения других политик к
сегменту кластера. Чаще всего пространства имен
используются для организации разных программных
окружений, таких как окружения для разработки,
тестирования, интеграционного тестирования или
промышленной эксплуатации. Пространства имен также
можно использовать для организации многопользовательских
архитектур (multitenancy), а также для изоляции рабочих групп,
проектов и даже конкретных приложений. Но, в конечном
счете, для полной изоляции некоторых окружений пространств
имен недостаточно, поэтому создание отдельных кластеров
является обычным явлением. Как правило, создается один
непромышленный кластер Kubernetes, используемый,
например, для разработки, тестирования и интеграционного
тестирования, и другой — промышленный кластер Kubernetes
для опытной и промышленной эксплуатации.
Давайте перечислим некоторые особенности пространств
имен и посмотрим, как они могут помочь нам в разных
сценариях:
Пояснение
Мы кратко рассмотрели лишь часть основных понятий
Kubernetes, которые используются в этой книге. Примитивов,
которыми разработчики пользуются каждый день, намного
больше. Например, создавая контейнерную службу, вы можете
задействовать множество объектов Kubernetes, которые
позволяют воспользоваться всеми преимуществами
фреймворка. Имейте в виду, что это только объекты,
используемые разработчиками приложений для интеграции
контейнерной службы в Kubernetes. Есть и другие понятия,
используемые администраторами, чтобы позволить
разработчикам эффективно управлять платформой. На рис. 1.4
представлены основные ресурсы Kubernetes, которые могут
пригодиться разработчикам.
Рис. 1.4. Понятия Kubernetes для разработчиков
Задача
Kubernetes может управлять приложениями, написанными на
разных языках программирования, если эти приложения
можно запускать в контейнере. Однако разные языки имеют
разные требования к ресурсам. Как правило, программы,
написанные на компилируемых языках, работают быстрее и
часто требуют меньше памяти по сравнению с динамически
компилируемыми программами или программами,
выполняющимися под управлением интерпретатора.
Учитывая, что многие современные языки программирования
из одной и той же категории имеют схожие требования к
ресурсам, с точки зрения потребления ресурсов более важными
аспектами являются предметная область, бизнес-логика
приложения и фактические детали реализации.
Трудно предсказать количество ресурсов, которое может
понадобиться контейнеру для оптимального
функционирования, и именно разработчик знает ожидаемый
объем ресурсов, необходимый для реализации службы
(выявленный в ходе тестирования). Некоторые службы имеют
постоянный профиль использования процессора и памяти, а
некоторые — переменчивый. Некоторые службы нуждаются в
долговременном хранилище для хранения данных; некоторые
устаревшие службы требуют доступа к фиксированным портам
в хост-системе для корректной работы. Описание всех этих
характеристик приложений и передача их управляющей
платформе – фундаментальное условие для нормального
функционирования облачных приложений.
Помимо требований к ресурсам, среда времени выполнения
приложений также зависит от возможностей платформы, таких
как хранение данных или конфигурация приложения.
Решение
Знать требования контейнера к окружению времени
выполнения важно по двум основным причинам. Во-первых,
зная все зависимости времени выполнения и потребности в
ресурсах, Kubernetes сможет принимать разумные решения о
том, где в кластере разместить контейнер, чтобы максимально
эффективно использовать оборудование. В окружении с
общими ресурсами, используемыми большим числом
процессов с разным приоритетом, знание требований каждого
процесса является залогом их успешного сосуществования.
Однако эффективное размещение — это только одна сторона
медали.
Вторая причина важности профилей ресурсов контейнеров
— это планирование вычислительных мощностей. Исходя из
потребности в каждой конкретной службе и общего количества
служб, можно оценить необходимые вычислительные
мощности для разных окружений и определить экономически
наиболее эффективные профили хостов для удовлетворения
потребностей всего кластера. Профили ресурсов служб и
планирование вычислительной мощности вместе являются
залогом долгого и успешного управления кластером.
Прежде чем углубиться в профили ресурсов, посмотрим, как
объявлять зависимости времени выполнения.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
volumeMounts:
- mountPath: "/logs"
name: log-volume
volumes:
- name: log-volume
persistentVolumeClaim:
claimName: random-generator-log
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: PATTERN
valueFrom:
configMapKeyRef:
name: random-generator-config
key: pattern
Профили ресурсов
Объявить зависимость контейнера от карт конфигураций,
секретов и томов несложно. Но чтобы выяснить требования
контейнера к ресурсам, необходимо поразмышлять и
поэкспериментировать. Вычислительные ресурсы в контексте
Kubernetes — это все, что можно запросить, получить и
использовать из контейнера. Ресурсы делятся на две группы:
сжимаемые (могут регулироваться, например процессорное
время или пропускная способность сети) и несжимаемые
(нерегулируемые, например объем памяти).
Важно различать сжимаемые и несжимаемые ресурсы. Если
контейнеры потребляют слишком много сжимаемых ресурсов,
таких как процессор, их аппетиты урезаются, но если они
используют слишком много несжимаемых ресурсов (например,
памяти), они останавливаются (потому что нет другого способа
попросить приложение освободить выделенную память).
Исходя из характера приложения и особенностей его
реализации, нужно указать минимально необходимые объемы
ресурсов (так называемые запросы) и максимально возможные
— до которых потребление может вырасти (лимиты).
Определение контейнера может задавать необходимую долю
процессорного времени и объем памяти в форме запроса и
лимита. В общих чертах идея запросов/лимитов напоминает
мягкие/жесткие лимиты. Например, аналогичным образом мы
определяем размер кучи для приложения Java с помощью
параметров командной строки -Xms и -Xmx.
При размещении подов на узлах планировщик
ориентируется на величину запросов (а не лимитов). Планируя
каждый конкретный под, планировщик рассматривает только
узлы, имеющие достаточный объем ресурсов для размещения
этого пода и всех его контейнеров, складывая запрашиваемые
объемы ресурсов. В этом смысле поле запросов requests в
определении контейнера влияет на возможность планирования
пода. В листинге 2.3 показано, как такие ограничения
определяются для подов.
Листинг 2.3. Ограничения ресурсов
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 200m
memory: 200Mi
Без гарантий
Под, не определяющий запросы и лимиты для своих
контейнеров, считается самым низкоприоритетным и
уничтожается первым, когда на узле, где он располагается,
заканчиваются несжимаемые ресурсы.
С переменным качеством
Под, определяющий неравные значения в запросах и
лимитах (когда лимиты превышают запросы),
гарантированно получает минимальный объем ресурсов, но
при наличии свободных ресурсов может получить их
больше, вплоть до объявленного предела. Когда узел
испытывает нехватку ресурсов, эти поды могут
останавливаться, если не останется подов, обслуживаемых
без гарантий качества.
Гарантированный
Под, определяющий равные значения в запросах и лимитах,
получает наивысший приоритет и гарантированно будут
останавливаться, только если на узле не останется подов,
обслуживаемых без гарантий или с переменным качеством.
Как видите, характеристики потребления ресурсов
контейнерами, которые вы объявляете или опускаете,
напрямую влияют на качество обслуживания и определяют
относительную важность пода в случае истощения ресурса.
Определяйте требования подов к ресурсам с учетом этого.
Приоритет пода
Мы выяснили, как объявление потребностей в ресурсах влияет
на качество обслуживания и на очередность, в которой
Kubelet11 останавливает контейнеры в случае нехватки
ресурсов. На момент написания этих строк уже велись работы
по реализации еще одной возможности, имеющей отношение к
качеству обслуживания и позволяющей определить приоритет
пода и возможность его вытеснения. Настройка приоритета
дает возможность указать важность пода относительно других
подов и повлиять на порядок, в котором они будут
планироваться. Давайте посмотрим на эти настройки,
показанные в листинге 2.4.
Листинг 2.4. Приоритет пода
apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
name: high-priority
value: 1000
globalDefault: false
description: Это класс подов с очень высоким
приоритетом
---
apiVersion: v1
kind: Pod
metadata:
name: random-generator
labels:
env: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
priorityClassName: high-priority
Ресурсы проекта
Kubernetes — это платформа самообслуживания, которая
позволяет разработчикам запускать приложения в
предопределенных изолированных окружениях. Однако для
нормальной работы многопользовательской платформе
необходимы также определенные границы и средства
управления, чтобы отдельные пользователи не смогли
захватить все ресурсы платформы. Одним из таких
инструментов является квотирование ресурсов ResourceQuota.
С его помощью можно ограничить совокупное потребление
ресурсов в пространстве имен. С помощью квот ResourceQuota
администраторы кластера могут ограничить общую сумму
используемых вычислительных ресурсов (процессор, память) и
хранилища. С их помощью также можно ограничить общее
количество объектов (карт конфигураций, секретов, подов и
служб), создаваемых в пространстве имен.
Еще одним полезным инструментом в этой области
является диапазон ограничений LimitRange, который
позволяет установить границы использования ресурсов
каждого типа. Помимо минимальных и максимальных
значений и значений по умолчанию, этот механизм также
позволяет контролировать отношение между запросами и
лимитами, которое также известно как уровень перерасхода. В
табл. 2.1 показан пример, как можно выбирать возможные
значения для запросов и лимитов.
Пояснение
Контейнеры удобно использовать не только для изоляции
процессов, но и как формат упаковки. С соответствующими
профилями ресурсов они также помогают успешно
планировать вычислительные мощности. Проведите несколько
начальных экспериментов, чтобы выяснить потребности в
ресурсах для каждого контейнера, и используйте эту
информацию как основу для будущего планирования и
прогнозирования.
Однако, что более важно, профили ресурсов дают
приложениям возможность сообщить платформе Kubernetes
информацию, необходимую для планирования и управления
решениями. Если приложение не определяет никаких запросов
или лимитов, тогда Kubernetes будет интерпретировать его
контейнеры как черные ящики, которые можно останавливать
при заполнении кластера. Поэтому для каждого приложения
важно продумать и определить требования к ресурсам.
Теперь, узнав, как определять размеры приложений,
перейдем к главе 3 «Декларативное развертывание», где
познакомимся с некоторыми стратегиями установки и
обновления приложений в Kubernetes.
Дополнительная информация
• Квоты ресурсов
(http://kubernetes.io/docs/admin/resourcequota/).
Задача
Мы можем настроить изолированные окружения в виде
пространств имен и размещать службы в этих окружениях с
минимальным участием человека благодаря услугам
планировщика. Но с ростом числа микросервисов постоянное
обновление и замена их новыми версиями становятся все
более трудоемкими.
Обновление службы до следующей версии включает такие
действия, как запуск новой версии пода, безопасная остановка
старой версии пода, ожидание и проверка успешности запуска,
а иногда, в случае неудачи, откат до предыдущей версии. Эти
действия выполняются либо ценой простоя, когда не работает
ни одна из версий службы, либо без простоев, но за счет
увеличения использования ресурсов двумя версиями службы,
действующими одновременно в период обновления.
Выполнение этих шагов вручную может привести к
человеческим ошибкам и потребовать значительных усилий
для правильного выполнения сценариев, что быстро
превращает процесс выпуска новой версии в узкое место.
Решение
К счастью, Kubernetes предлагает средства автоматизации этой
деятельности. Используя понятие развертывания, можно
описать, как должно обновляться приложение, применяя
разные стратегии и настраивая разные аспекты процесса
обновления. Если за один цикл выпуска (который, в
зависимости от команды и проекта, может длиться от
нескольких минут до нескольких месяцев) развертывание
каждого экземпляра микросервиса выполняется несколько раз,
тогда эта автоматизация, предлагаемая Kubernetes, поможет
сэкономить немало усилий.
В главе 2 мы увидели, что для эффективной работы
планировщику необходимы: достаточный объем ресурсов на
хост-системе, соответствующие политики размещения и
контейнеры с правильно определенными профилями ресурсов.
Аналогично, чтобы механизм развертывания успешно
справился со своей работой, контейнеры должны быть
сформированы для выполнения в облачном окружении. В
основе механизма развертывания, как нетрудно догадаться,
лежит способность запускать и останавливать наборы подов.
Чтобы этот механизм работал должным образом, сами
контейнеры обычно должны принимать и обрабатывать
события жизненного цикла (такие, как SIGTERM; см. главу 5
«Управляемый жизненный цикл»), а также предоставлять
конечные точки для проверки их работоспособности, как
описано в главе 4 «Проверка работоспособности».
Если контейнеры в точности выполняют эти два
требования, платформа сможет чисто закрыть старые
контейнеры и заменить их, запустив обновленные экземпляры.
В таком случае все остальные аспекты процесса обновления
можно определить декларативным способом и выполнить как
одно атомарное действие с предопределенными шагами и
ожидаемым результатом. Давайте рассмотрим варианты
обновления контейнеров.
Непрерывное развертывание
Декларативный способ обновления приложений в Kubernetes
производится с использованием понятия развертывания.
Внутренне механизм развертывания создает набор реплик
ReplicaSet, который поддерживает селекторы меток. Кроме
того, абстракция развертывания позволяет определить
поведение процесса обновления с использованием таких
стратегий, как RollingUpdate (непрерывное обновление,
используется по умолчанию) и Recreate (воссоздание). В
листинге 3.1 показаны важные детали настройки для стратегии
непрерывного обновления.
Листинг 3.1. Непрерывное развертывание обновления
apiVersion: apps/v1
kind: Deployment
metadata:
name: random-generator
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-
generator:1.0
name: random-generator
readinessProbe:
exec:
command: [ "stat", "/random-
generator-ready" ]
Рис. 3.2. Фиксированное развертывание с использованием стратегии Recreate
Канареечное развертывание
Канареечное развертывание (Canary deployment) — это способ
постепенного развертывания новой версии приложения в
рабочем окружении путем замены старых экземпляров
новыми с помощью небольших блоков. Этот метод снижает
риски, связанные с внедрением новой версии, открывая доступ
к новым версиям только некоторым пользователям. Если
новая версия хорошо зарекомендовала себя при обслуживании
небольшой выборки пользователей, производится замена всех
старых экземпляров. На рис. 3.4 показано, как выполняется
канареечное развертывание.
Пояснение
Объект развертывания Deployment наглядно показывает, как
Kubernetes превращает утомительный процесс ручного
обновления приложений в декларативное действие, которое
можно повторять и автоматизировать. Стратегии
развертывания, поддерживаемые изначально (непрерывного
обновления и воссоздания), управляют заменой старых
контейнеров новыми, а стратегии выпуска (сине-зеленая и
канареечная) определяют, как открывается доступ
потребителям к новой версии. Последние две стратегии
выпуска основаны на решении о переходе, принимаемом
человеком и, как следствие, автоматизированы не полностью и
требуют взаимодействия с человеком. На рис. 3.5 показано,
как меняется количество экземпляров новой и старой версий
во время переходов при использовании разных стратегий
развертывания и выпуска.
Дополнительная информация
Задача
Kubernetes регулярно проверяет состояние процесса
контейнера и перезапускает его при обнаружении проблемы.
Однако из практики мы знаем, что простой проверки
состояния процесса недостаточно для определения
работоспособности приложения. Часто бывает так, что
приложение зависает, а его процесс все еще работает.
Например, приложение на Java может столкнуться с ошибкой
OutOfMemoryError (нехватка памяти), но при этом процесс JVM
будет продолжать выполняться. Также приложение может
зависнуть, попав в бесконечный цикл или в состояние
взаимоблокировки или когда система начинает активно
использовать файл подкачки из-за нехватки памяти. Для
выявления подобных ситуаций фреймворку Kubernetes нужен
надежный способ проверки работоспособности приложений —
не для того, чтобы понять внутреннюю работу приложения, а
чтобы убедиться, что приложение работает как ожидается и
способно обслуживать потребителей.
Решение
Индустрия программного обеспечения признает
невозможность писать код без ошибок. Более того, в
распределенных приложениях вероятность сбоев возрастает. В
результате основное внимание сместилось с обнаружения и
устранения неисправностей на обнаружение сбоев и
восстановление работоспособности после них. Обнаружение
сбоя — сложная задача. Она не имеет универсального решения,
пригодного для всех приложений, потому что разные
приложения имеют разное понятие сбоя. Кроме того, разные
виды сбоев требуют разных корректирующих воздействий.
Временные сбои могут устраняться самим приложением при
наличии достаточного времени, а при некоторых других сбоях
может потребоваться перезапустить приложение. Давайте
посмотрим, какие проверки использует Kubernetes для
обнаружения и исправления сбоев.
Проверка работоспособности
Если ваше приложение попадает в ситуацию
взаимоблокировки, процесс не останавливается сам по себе и
продолжает действовать. Чтобы обнаружить подобные
проблемы и любые другие нарушения в работе бизнес-логики
приложения, Kubernetes использует проверки
работоспособности — регулярные проверки, выполняемые
агентом Kubelet, который запрашивает у контейнера
подтверждение его работоспособности. Важно, чтобы проверка
работоспособности выполнялась извне, а не внутри самого
приложения, потому что некоторые сбои могут помешать
сторожевому коду в приложении сообщить об ошибке. В
отношении корректирующих воздействий проверка
работоспособности аналогична проверке наличия процесса,
поэтому при обнаружении сбоя контейнер перезапускается.
Однако эти проверки обеспечивают большую гибкость в
выборе методов проверки приложения, а именно:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-liveness-check
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: DELAY_STARTUP
value: "20"
ports:
- containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
apiVersion: v1
kind: Pod
metadata:
name: pod-with-readiness-check
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
readinessProbe:
exec:
command: [ "stat", "/var/run/random-
generator-ready" ]
Пояснение
Для полной автоматизации облачные приложения должны
быть максимально открыты и предоставлять управляющей
платформе средства чтения и интерпретации информации о
работоспособности приложения и, при необходимости, для
выполнения корректирующих воздействий. Проверка
работоспособности играет важную роль в автоматизации таких
действий, как развертывание, автоматическое восстановление,
масштабирование и др. Однако существуют и другие средства,
с помощью которых приложение может сообщить о себе
больше информации.
Очевидный и старый метод решения этой задачи —
журналирование. Желательно, чтобы контейнеры фиксировали
в журналах все значимые события, связанные с ошибками, и
хранили эти журналы в централизованном месте для
дальнейшего анализа. Журналы, как правило, не используются
для автоматизации действий, их задача — оповещение и
помощь в дальнейшем расследовании. Более полезным
аспектом журналов является возможность анализа причин
сбоев и обнаружение незаметных ошибок.
Помимо вывода информации в стандартные потоки, также
рекомендуется записывать причины завершения контейнера в
/dev/termination-log. Это место, куда контейнер может записать
свои последние слова, прежде чем окончательно исчезнуть. На
рис. 4.1 показаны возможные варианты взаимодействия
контейнера с платформой времени выполнения.
Рис. 4.1. Варианты наблюдения за контейнером
Дополнительная информация
Задача
В главе 4 «Проверка работоспособности» мы узнали, почему
контейнеры должны предоставлять программный интерфейс
для различных проверок работоспособности. Программный
интерфейс проверки работоспособности — это комплекс
конечных точек, доступных только для чтения, которые
платформа постоянно опрашивает, чтобы получить
представление о приложении. Это — механизм, используемый
платформой для извлечения информации из приложения.
Кроме мониторинга состояния контейнера, платформа
иногда может выдавать команды, ожидая определенной
реакции от приложения. Опираясь на внутренние правила и
внешние факторы, облачная платформа может в любой
момент принять решение о запуске или остановке
управляемых ею приложений. Но только само контейнерное
приложение может определить, на какие события и как оно
будет реагировать. Фактически приложение предлагает
программный интерфейс, который платформа использует для
связи и отправки команд приложению. Кроме того,
приложения могут получать дополнительные выгоды от
управления жизненным циклом извне или игнорировать
управляющие воздействия, если эта услуга им не нужна.
Решение
Выше мы видели, что простая проверка статуса процесса —
недостаточно хороший показатель работоспособности
приложения. Вот почему существуют разные API для
мониторинга работоспособности контейнера. Аналогично,
использования одной только модели процесса для запуска и
остановки приложения недостаточно. Часто приложения
требуют более тонких воздействий и механизмов управления
жизненным циклом. Некоторым приложениям нужна помощь
для разогрева, а некоторым требуется выполнить точную и
четкую процедуру завершения. Для этих и других случаев
платформа генерирует некоторые события, как показано на
рис. 5.1, которые контейнер может принимать и обрабатывать,
если это необходимо.
Сигнал SIGTERM
Всякий раз, когда фреймворк Kubernetes решает остановить
контейнер, например, останавливая под, которому тот
принадлежит, или перед повторным запуском с целью
устранения неисправности, выявленной при проверке
работоспособности, контейнер получает сигнал SIGTERM.
SIGTERM — это вежливое предложение контейнеру
завершиться самому, прежде чем Kubernetes отправит более
резкий «окрик» — сигнал SIGKILL. Получив сигнал SIGTERM,
приложение должно завершиться как можно быстрее. Одни
приложения могут быстро остановиться в ответ на этот сигнал,
другим приложениям может потребоваться завершить уже
запущенные запросы, закрыть соединения и очистить
временные файлы, что может занять немного больше времени.
В любом случае реакция на SIGTERM обозначает подходящий
момент для аккуратного завершения контейнера.
Сигнал SIGKILL
Если процесс контейнера не остановился после сигнала
SIGTERM, он принудительно завершается следующим сигналом
SIGKILL. Kubernetes не посылает сигнал SIGKILL немедленно, а
ожидает 30 секунд по умолчанию после отправки сигнала
SIGTERM. Этот период можно настроить отдельно для каждого
пода, определив параметр
.spec.terminationGracePeriodSeconds, но соблюдение
этой настройки не гарантируется, потому что ее можно
переопределить в командах Kubernetes. Поэтому разработчики
должны стремиться проектировать и реализовать
контейнерные приложения так, чтобы они быстро запускались
и завершались.
apiVersion: v1
kind: Pod
metadata:
name: post-start-hook
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
lifecycle:
postStart:
exec:
command:
- sh
- -c
- sleep 30 && echo "Wake up!" >
/tmp/postStart_done
Команда postStart в этом примере ждет 30 секунд.
sleep здесь просто имитирует продолжительный процесс
инициализации приложения. Кроме того, она использует файл
для синхронизации с основным приложением, которое
запускается параллельно.
Команда postStart выполняется после создания
контейнера, параллельно с процессом самого контейнера. Даже
при том, что логику инициализации и разогрева приложения
часто можно реализовать часть процедуры запуска контейнера,
обработчик postStart все еще может пригодиться в
некоторых ситуациях. По своему характеру postStart
является блокирующей операцией, и контейнер остается в
состоянии Waiting (пауза) до завершения обработчика
postStart, что, в свою очередь, заставляет всю группу
контейнеров (под) оставаться в состоянии Pending (ожидание).
Эту особенность postStart можно использовать, чтобы
отложить инициализацию контейнера и дать время для
инициализации основного процесса контейнера.
Другое применение postStart — предотвращение запуска
контейнера при несоблюдении некоторых предварительных
условий. Например, если обработчик postStart сообщит об
ошибке, вернув ненулевой код завершения, фреймворк
Kubernetes уничтожит основной процесс контейнера.
Механизмы вызова точек входа postStart и preStop
напоминают вызов точек входа определения
работоспособности, как описывалось в главе 4 «Проверка
работоспособности», и поддерживают следующие типы
обработчиков:
exec
Выполняет команду непосредственно в контейнере.
httpGet
Выполняет HTTP-запрос GET, посылая его в порт, открытый
одним из контейнеров в поде.
Будьте осторожны, закладывая критически важную логику в
обработчик postStart, потому что нет никаких гарантий
относительно порядка его выполнения. Поскольку обработчик
выполняется параллельно с процессом контейнера, есть
вероятность, что он выполнится до запуска контейнера. Кроме
того, обработчик должен соответствовать семантике
выполнения «не менее одного раза», то есть он должен
позаботиться о попытках повторного запуска. Еще один аспект,
о котором следует помнить, — платформа не выполняет
повторных попыток, если HTTP-запрос не достиг обработчика.
apiVersion: v1
kind: Pod
metadata:
name: pre-stop-hook
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
lifecycle:
preStop:
httpGet:
port: 8080
path: /shutdown
Пояснение
Одним из основных преимуществ облачной платформы
является возможность надежного и предсказуемого
выполнения и масштабирования приложений в потенциально
ненадежной облачной инфраструктуре. Эти платформы
предлагают набор ограничений и контрактов для приложений,
работающих под их управлением. В интересах приложения
следовать этим контрактам, чтобы воспользоваться всеми
возможностями, предлагаемыми облачной платформой.
Обработка этих событий гарантирует правильный запуск и
завершение приложения с минимальным воздействием на
потребляющие их службы. На данный момент это означает, что
контейнеры должны действовать подобно хорошо
спроектированному процессу POSIX. В будущем могут
появиться другие события, подсказывающие приложению,
когда оно будет масштабироваться, или предлагающие
освободить ресурсы, чтобы предотвратить преждевременное
завершение. Важно понимать, что жизненный цикл
приложения больше не контролируется человеком, а
полностью автоматизируется платформой.
Помимо управления жизненным циклом приложения,
другой большой обязанностью платформ управления, таких
как Kubernetes, является распределение контейнеров по
массиву узлов. Паттерн Automated Placement (Автоматическое
размещение) определяет приемы влияния на решения по
планированию извне.
Дополнительная информация
• Defer-контейнеры (http://bit.ly/2TegEM7).
Глава 6. Автоматическое размещение
Автоматическое размещение является основной функцией
планировщика Kubernetes, который распределяет новые поды
между узлами в соответствии с требованиями контейнеров к
ресурсам и с соблюдением правил планирования. Этот паттерн
описывает принципы алгоритма планирования в Kubernetes и
способы влияния на решения о размещении.
Задача
Типичная система на основе микросервисов состоит из
десятков или даже сотен изолированных процессов.
Контейнеры и поды служат хорошими абстракциями для
упаковки и развертывания, но не решают проблему
размещения этих процессов на подходящих узлах. При
большом и постоянно растущем количестве микросервисов
назначение и размещение их по отдельности начинает
вызывать неуправляемое нарастание сложностей.
Контейнеры имеют зависимости друг от друга, зависимости
от узлов и потребности в ресурсах, и все эти параметры
меняются со временем. Ресурсы, доступные в кластере, также
меняются со временем из-за сокращения или расширения
кластера или из-за того, что часть ресурсов уже занята
размещенными контейнерами. Порядок размещения
контейнеров также влияет на доступность, производительность
и емкость распределенных систем. Все это делает
планирование контейнеров движущейся целью, в которую
приходится стрелять на ходу.
Решение
В Kubernetes размещение подов на узлах осуществляется
планировщиком. Это область, имеющая массу настроек, на
момент написания этих строк все еще продолжала быстро
развиваться и изменяться. В этой главе мы рассмотрим
основные механизмы управления планированием, движущие
силы, влияющие на размещение, а также причины и
последствия выбора того или иного варианта. Планировщик
Kubernetes является мощным инструментом, позволяющим
экономить время. Он играет фундаментальную роль в
платформе Kubernetes, но, как и другие компоненты Kubernetes
(API Server, Kubelet), его можно использовать изолированно
или вообще не использовать.
На самом верхнем уровне планировщик Kubernetes
извлекает определение каждого вновь созданного пода,
используя API Server, и связывает его с определенным узлом.
Он отыскивает подходящий узел (если таковой имеется) для
каждого пода, будь то первоначальное размещение
приложения, масштабирование вверх или перемещение с
вышедшего из строя узла на работоспособный узел. При этом
учитываются зависимости времени выполнения, требования к
ресурсам и высокой доступности, используются приемы
горизонтального распределения подов и размещения подов
поблизости друг от друга для уменьшения задержек при
взаимодействиях. Однако чтобы планировщик правильно
выполнял свою работу и допускал возможность
декларативного размещения, ему нужны информация о
емкости узлов и контейнеры с объявленными профилями
ресурсов и действующими политиками. Давайте рассмотрим
каждое из требований подробнее.
Доступные ресурсы на узле
Прежде всего, кластер Kubernetes должен иметь узлы с
объемом ресурсов, достаточным для запуска новых подов.
Каждый узел имеет определенную емкость для запуска подов, и
планировщик гарантирует, что сумма ресурсов,
запрашиваемых подом, не превысит доступную емкость узла.
Емкость узла, выделенного только для нужд Kubernetes,
рассчитывается по формуле в листинге 6.1.
Листинг 6.1. Емкость узла
Политики размещения
Последний элемент — наличие правильных политик
фильтрации и приоритетов для конкретных потребностей
приложений. Планировщик имеет набор предикатов и политик
приоритетов по умолчанию, который подходит для
большинства случаев. Его можно переопределить и запустить
планировщик с другим набором политик, как показано в
листинге 6.2.
{
"kind" : "Policy",
"apiVersion" : "v1",
"predicates" : [
{"name" : "PodFitsHostPorts"},
{"name" : "PodFitsResources"},
{"name" : "NoDiskConflict"},
{"name" : "NoVolumeZoneConflict"},
{"name" : "MatchNodeSelector"},
{"name" : "HostName"}
],
"priorities" : [
{"name" : "LeastRequestedPriority",
"weight" : 2},
{"name" : "BalancedResourceAllocation",
"weight" : 1},
{"name" : "ServiceSpreadingPriority",
"weight" : 2},
{"name" : "EqualPriority", "weight" :
1}
]
}
Процесс планирования
Поды назначаются узлам, обладающим определенной
емкостью, в соответствии с политиками размещения. Для
полноты обсуждения на рис. 6.1 в общих чертах показано, как
все необходимые элементы собираются вместе и какие
основные этапы преодолевает под в процессе планирования.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
nodeSelector:
disktype: ssd
Набор меток, которыми должен быть отмечен узел,
пригодный для выполнения этого пода.
Помимо определения дополнительных меток узлов, можно
использовать некоторые из меток которыми отмечен каждый
узел по умолчанию. Например, каждый узел имеет уникальную
метку kubernetes.io/hostname, которую можно
использовать для размещения подов на узлах, исходя из имени
хоста. Также можно использовать другие метки по умолчанию,
которые определяют операционную систему, аппаратную
архитектуру и типы экземпляров.
Близость узлов
Kubernetes поддерживает еще много весьма гибких способов
настройки процесса планирования. Один такой способ —
определение степени близости узлов, который является более
общей формой способа на основе селектора узла, описанного
выше, и позволяет задать правила, обязательные или
предпочтительные. Обязательные правила должны
выполняться всегда, чтобы узел мог быть выбран для запуска
пода, тогда как предпочтительные правила подразумевают
степень предпочтения, увеличивая вес соответствующих узлов,
но не являются обязательными. Кроме того, поддержка
понятия близости узлов позволяет выразить весьма широкий
спектр ограничений, добавляя такие операторы, как In, NotIn,
Exists, DoesNotExist, Gt и Lt. В листинге 6.4 показано, как
определяется близость узлов.
Листинг 6.4. Определение пода с описанием близости узлов
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExec
ution:
nodeSelectorTerms:
-
matchExpressions:
- key: numberCores
operator: Gt
values: [ "3" ]
preferredDuringSchedulingIgnoredDuringExe
cution:
- weight: 1
preference:
matchFields:
- key: metadata.name
operator: NotIn
values: [ "master" ]
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExec
ution:
-
labelSelector:
matchLabels:
confidential: high
topologyKey: security-
zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExe
cution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
confidential: none
topologyKey: kubernetes.io/hostname
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
Непригодность и допустимость
Еще одна возможность, помогающая управлять выбором узлов
для выполнения пода, основана на понятиях непригодности и
допустимости. Оценка близости узлов позволяет подам
выбирать наиболее подходящие узлы, а непригодность и
допустимость имеют противоположное назначение. Они
позволяют узлам определять, какие поды должны или не
должны планироваться на них. Непригодность — это
характеристика узла, и если она определена, эта
характеристика не позволяет планировать поды для
выполнения на узле, если они непригодны для этого. В этом
смысле непригодность и допустимость можно рассматривать
как условие включения, позволяющее планировать на узлах,
которые по умолчанию недоступны для планирования, тогда
как правила близости являются условием исключения, явно
определяющим, на каких узлах может выполняться под, и
исключающим все невыбранные узлы.
Непригодность добавляется в узел с помощью kubectl:
kubectl taint nodes master
noderole.kubernetes.io/master="true":NoSchedule,
что оказывает эффект, представленный в листинге 6.6.
Соответствующая допустимость добавляется в определение
пода, как показано в листинге 6.7. Обратите внимание, что
параметры key и effect в разделе taints в листинге 6.6 и в
разделе tolerations в листинге 6.7 имеют одни и те же
значения.
Листинг 6.6. Непригодность узла
apiVersion: v1
kind: Node
metadata:
name: master
spec:
taints:
- effect: NoSchedule
key: node-role.kubernetes.io/master
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
tolerations:
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
Рис. 6.2. Процессы, запланированные на узлах, и неизрасходованные ресурсы
RemoveDuplicates
Стратегия удаления дубликатов гарантирует выполнение на
одном узле только одного пода, связанного с набором
реплик ReplicaSet или развертыванием Deployment. Если
обнаружится большее число подов, лишние поды будут
вытеснены. Эта стратегия может пригодиться в сценариях,
когда узел вышел из строя и управляющие контроллеры
запустили новые поды на других, исправных узлах. Когда
неисправный узел восстановится и вернется в кластер,
количество запущенных модулей окажется больше
желаемого, и тогда перепланировщик сможет помочь
вернуть их число к нужному количеству реплик. Удаление
дубликатов на узлах также может помочь равномерно
распределить поды между несколькими узлами, если
политики планирования и топология кластера изменятся
после первоначального размещения.
LowNodeUtilization
Эта стратегия выявляет узлы с низким коэффициентом
использования ресурсов и переносит на них поды с других,
перегруженных узлов, в надежде добиться более удачного
распределения и равномерного использования ресурсов. Под
узлами с недоиспользованными ресурсами подразумеваются
узлы, на которых количество ядер, объем памяти или число
подов меньше настроенных пороговых значений
thresholds. Аналогично, под перегруженными узлами
подразумеваются узлы, на которых эти значения
превышают настроенные значения targetThresholds.
Любой узел, использование ресурсов которого находится
между этими значениями, считается оптимально
используемым и не подвергается действию этой стратегии.
RemovePodsViolatingInterPodAntiAffinity
Эта стратегия исключает блоки, нарушающие правила
удаленности между подами, что может произойти при
добавлении правил определения удаленности уже после
размещения подов на узлах.
RemovePodsViolatingNodeAffinity
Эта стратегия предназначена для вытеснения подов,
нарушающих правила близости.
Независимо от используемой политики, перепланировщик
старается избежать вытеснения:
Пояснение
Размещение — это область, в которую вмешательство
нежелательно. Если вы будете следовать рекомендациям из
главы 2 «Предсказуемые требования» и объявите все
потребности контейнера в ресурсах, планировщик выполнит
свою работу и поместит под на наиболее подходящий узел.
Однако если этого недостаточно, у вас в запасе есть несколько
способов помочь планировщику прийти к желаемой топологии
развертывания. В заключение перечислим подходы к
управлению планированием (имейте в виду, что на момент
написания этой книги данный список менялся с каждой новой
версией Kubernetes), в порядке возрастания их сложности:
nodeName
Простейшая форма связывания пода с узлом. В идеале это
поле должно заполняться планировщиком, который
руководствуется политиками, а не вручную. Связывание
пода с конкретным узлом значительно сужает область для
планирования этого пода. Это возвращает нас к эпохе,
предшествовавшей Kubernetes, когда мы явно указывали
узлы для запуска наших приложений.
nodeSelector
Массив пар ключ/значение. Чтобы под можно было
запустить на узле, указанные пары ключ/значение должны
присутствовать на нем в виде меток. Селектор узла — один
из самых простых механизмов управления выбором
планировщика, требующий всего лишь добавить несколько
значимых меток в определение пода и узла (что приходится
делать в любом случае).
Близость узла
Это правило позволяет выразить зависимость пода от узлов.
Например, учесть наличие оборудования, местоположение и
т.д.
Непригодность и допустимость
Непригодность и допустимость позволяют узлу
контролировать, какие поды могут или не могут
планироваться на них, например, чтобы выделить узел для
группы подов или даже вытеснить поды во время
выполнения. Другое преимущество настроек непригодности
и допустимости состоит в том, что при расширении кластера
Kubernetes путем добавления новых узлов с новыми
метками вам не придется добавлять новые метки во все
поды — это нужно будет сделать только для подов, которые
должны быть размещены на новых узлах.
Нестандартный планировщик
Если ни один из предыдущих подходов не является
достаточно хорошим или ваши требования к планированию
слишком сложны, вы можете реализовать свой
планировщик. Такой планировщик может работать вместо
стандартного планировщика Kubernetes или одновременно с
ним. Можно использовать гибридный подход, когда
запускается процесс «расширения планировщика», к
которому обращается стандартный планировщик Kubernetes
на последнем этапе перед принятием решения о
планировании. При таком подходе не требуется
реализовывать полный планировщик и достаточно лишь
предоставить HTTP API для фильтрации и определения
приоритетов узлов. Преимущество создания своего
планировщика состоит в том, что при этом можно
учитывать факторы, внешние по отношению к кластеру
Kubernetes, такие как стоимость оборудования, сетевые
задержки и оптимизация использования ресурсов при
распределении подов между узлами. Также есть
возможность задействовать несколько своих
планировщиков вместе с планировщиком по умолчанию и
выбирать, какой планировщик использовать для каждого
пода. Каждый планировщик может иметь свой набор
политик для своего подмножества подов.
Как видите, есть много способов управлять размещением
подов, и выбор правильного подхода или комбинирование
нескольких подходов могут оказаться сложной задачей.
Основная мысль этой главы: определите и объявите профили
ресурсов контейнера, снабдите поды и узлы соответствующими
метками, наконец, старайтесь поменьше вмешиваться в работу
планировщика Kubernetes.
Дополнительная информация
Задача
Основным примитивом для запуска контейнеров и управления
ими является под (Pod) — группа контейнеров. Существуют
разные способы создания подов с различными
характеристиками:
Простой под
Позволяет вручную создать под для запуска контейнеров.
Однако когда узел, на котором выполняется такой под,
выходит из строя, под не перезапускается. Запускать поды
таким способом не рекомендуется, за исключением случаев
разработки или тестирования. Этот механизм также
известен под названиями неуправляемые, или голые, поды.
Решение
Задание Job в Kubernetes напоминает набор реплик ReplicaSet
— оно точно так же создает один или несколько подов и
обеспечивает их выполнение. Однако, в отличие от набора
реплик, после успешного завершения ожидаемого количества
подов задание считается выполненным и дополнительные
поды не запускаются. В листинге 7.1 показано, как выглядит
определение задания.
Листинг 7.1. Определение задания Job
apiVersion: batch/v1
kind: Job
metadata:
name: random-generator
spec:
completions: 5
parallelism: 2
template:
metadata:
name: random-generator
spec:
restartPolicy: OnFailure
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
command: [ "java", "-cp", "/",
"RandomRunner",
"/numbers.txt", "10000" ]
.spec.completions
Определяет, сколько раз следует запустить под, чтобы
задание считалось выполненным.
.spec.parallelism
Определяет, сколько реплик пода может выполняться
параллельно. Большое число в этом параметре не
гарантирует высокого уровня параллелизма, и на самом
деле количество одновременно выполняемых подов может
быть меньше (а в некоторых тупиковых ситуациях больше)
желаемого (например, из-за особенностей регулирования,
ограниченности ресурсов, при приближении к числу
.spec.completions и по другим причинам). Запись
значения 0 в этот параметр эффективно приостанавливает
задание.
На рис. 7.1 показано, как действует паттерн Batch Job
(Пакетное задание), настройки которого приводились в
листинге 7.1, со счетчиком выполнений, равным пяти, и
уровнем параллелизма, равным двум.
Исходя из значений этих двух параметров, можно выделить
следующие типы заданий:
Рабочая очередь
Опустив параметр .spec.completions и присвоив
параметру .spec.parallelism целое число больше
единицы, вы получите рабочую очередь для параллельных
заданий. Задание типа «рабочая очередь» считается
выполненным, если все поды завершились и хотя бы один
из них завершился с признаком успеха. В этом сценарии
требуется, чтобы поды координировали свою работу друг с
другом и определяли, над чем каждый будет работать.
Например, когда в очереди имеется фиксированное, но
неизвестное количество элементов для обработки,
параллельно выполняющиеся поды могут выбирать их один
за другим и обрабатывать. Первый под, обнаруживший, что
очередь опустела, и завершившийся с признаком успеха,
указывает на выполнение задания. Контроллер Job также
ожидает завершения всех других подов. Поскольку один под
может обработать несколько элементов, этот тип заданий
является отличным выбором, когда накладные расходы на
запуск одного пода для обработки одного элемента
оказываются неоправданно высокими.
Для обработки неограниченного потока элементов лучше
использовать другие контроллеры управления подами, такие
как набор реплик ReplicaSet.
Пояснение
Абстракция задания Job — простой, но важный примитив, на
котором основываются другие примитивы, такие как
планировщик заданий CronJob. Задания помогают превратить
изолированные единицы работы в надежную и
масштабируемую единицу выполнения. Однако задание никак
не определяет, как отдельные элементы для обработки должны
отображаться в задания или поды. Это вы должны сделать
сами, приняв во внимание плюсы и минусы каждого варианта:
Дополнительная информация
Задача
В мире распределенных систем и микросервисов наблюдается
явное стремление к организации взаимодействий и обмену
событиями между приложениями в режиме реального времени
с использованием HTTP и упрощенных механизмов обмена
сообщениями. Однако, независимо от последних тенденций в
разработке программного обеспечения, планирование заданий
имеет долгую историю и продолжает оставаться актуальным.
Паттерн Periodic Job (Периодическое задание) обычно
используется для автоматизации обслуживания системы и
решения административных задач. Он также применяется в
бизнес-приложениях, требующих периодического выполнения
определенных задач. Типичными примерами могут служить
интеграция между системами посредством передачи файлов,
интеграция приложений посредством периодического опроса
баз данных, отправка электронных писем с новостями, а также
архивация и удаление старых файлов.
Традиционно для периодического выполнения заданий с
целью обслуживания системы используется
специализированное программное обеспечение —
планировщик Cron. Однако специализированное программное
обеспечение может оказаться избыточным для простых
случаев использования, к тому же задания Cron, выполняемые
на единственном сервере, трудно поддерживать, так как они
представляют собой единую точку отказа. Вот почему очень
часто разработчики стремятся реализовать решения, которые
способны осуществлять планирование и выполнять
необходимую бизнес-логику. Например, в мире Java
выполнение заданий с привязкой ко времени можно
организовать с использованием библиотек, таких как Quartz и
Spring Batch, или пользовательских реализаций на основе
класса ScheduledThreadPoolExecutor. Но, как и в случае с
Cron, основная сложность этого подхода состоит в том, чтобы
сделать механизм планирования устойчивым и
высокодоступным, а это влечет значительное потребление
ресурсов. Кроме того, при таком подходе планировщик
заданий является частью приложения, и чтобы обеспечить
высокую доступность планировщика, нужно обеспечить
высокую доступность всего приложения. Обычно ради этого
приходится запускать несколько экземпляров приложения, но
так, чтобы активно занимался планированием только один
экземпляр, а это требует реализации алгоритма выбора лидера
и решения других проблем, характерных для распределенных
систем.
В конце концов, простая служба, которая должна
скопировать несколько файлов один раз в день, может
потребовать нескольких узлов, распределенного механизма
выбора лидера и многого другого. Реализация контроллера
CronJob в Kubernetes решает все эти проблемы и позволяет
планировать ресурсы Job с помощью хорошо известного
формата описания заданий, используемого планировщиком
Cron. Это дает возможность разработчикам сосредоточиться
только на реализации выполняемой работы, а не на аспектах
планирования с привязкой ко времени.
Решение
В главе 7 «Пакетное задание» мы познакомились с
возможностями и вариантами использования поддержки
заданий Job в Kubernetes. Все, о чем рассказывалось там,
относится и к этой главе, потому что примитив CronJob
основан на Job. Экземпляр CronJob напоминает строку в файле
crontab в Unix (таблица заданий планировщика cron) и
управляет аспектами выполнения задания, связанными со
временем. Он позволяет периодически выполнять задание в
определенные моменты времени. Образец определения такого
периодического задания приводится в листинге 8.1.
Листинг 8.1. Ресурс CronJob
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: random-generator
spec:
# Через каждые три минуты
schedule: "*/3 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- image: k8spatterns/random-
generator:1.0
name: random-generator
command: [ "java", "-cp", "/",
"RandomRunner",
"/numbers.txt", "10000"
]
restartPolicy: OnFailure
.spec.schedule
Строка в формате crontab, определяющая планирование
задания Job с привязкой ко времени (например, "0 * * * *"
требует запускать задание в начале каждого часа).
.spec.startingDeadlineSeconds
Крайний срок (в секундах) запуска задания, если было
пропущено запланированное время. Некоторые задания
допустимо выполнять только в течение определенного
отрезка времени и бессмысленно запускать позднее.
Например, если задание не было запущено в нужное время
из-за недостатка вычислительных ресурсов или отсутствия
других условий, иногда лучше пропустить выполнение,
потому что данные, которые должно обработать это
задание, уже устарели.
.spec.concurrencyPolicy
Определяет порядок управления одновременным
выполнением заданий, созданных одним и тем же CronJob.
Значение по умолчанию Allow (разрешить) позволяет
запускать новые экземпляры заданий, даже если
предыдущие задания еще не завершились. Если такое
поведение нежелательно, можно указать в этом параметре
значение Forbid (запретить), чтобы пропустить запуск
нового экземпляра задания, или значение Replace
(заменить), чтобы остановить прежний экземпляр задания и
запустить новый.
.spec.suspend
Этот параметр приостанавливает запуск новых экземпляров
задания, но не влияет на уже запущенные.
.spec.successfulJobsHistoryLimit и
.spec.failedJobsHistoryLimit
Эти поля определяют, сколько заданий, выполнившихся
успешно и завершившихся с ошибкой, следует сохранить
для исследования.
CronJob — это узкоспециализированный примитив и
применяется, только когда единица работы имеет временное
измерение. Даже при том, что CronJob не является
примитивом общего назначения, он служит отличным
примером того, как механизмы Kubernetes основываются друг
на друге и поддерживают сценарии использования, не
связанные с облачными вычислениями.
Пояснение
Как видите, CronJob — это довольно простой примитив,
добавляющий кластерное Cron-подобное поведение к
существующему механизму заданий Job. Но когда в сочетании с
другими примитивами, такими как Pod, средствами изоляции
ресурсов контейнера и другими особенностями Kubernetes,
например, описанными в главе 6 «Автоматическое
размещение» или в главе 4 «Проверка работоспособности», он
превращается в очень мощную систему планирования заданий.
Это позволяет разработчикам сосредоточиться исключительно
на предметной области и заняться реализацией контейнерного
приложения, отвечающей только за выполнение бизнес-
логики. Планирование осуществляется за рамками приложения
и является частью платформы со всеми ее дополнительными
преимуществами, такими как высокая доступность,
отказоустойчивость, емкость и размещение подов на основе
политик. Конечно, по аналогии с реализацией зданий Job, при
реализации контейнера для выполнения под управлением
CronJob следует учитывать все тупиковые ситуации: повторные
запуски, пропуск запусков, параллельные запуски или
принудительная остановка.
Дополнительная информация
Задача
Понятие демона (фонового процесса) в программных системах
существует на многих уровнях. На уровне операционной
системы демон — это долго выполняющаяся,
самовосстанавливающаяся компьютерная программа, которая
запускается как фоновый процесс. В Unix имена демонов
обычно заканчиваются на «d», например httpd, named и sshd.
В других операционных системах используются
альтернативные термины, такие как служебные задачи и
фантомные задания.
Независимо от названия, эти программы объединяет то,
что они выполняются как процессы, обычно не
взаимодействуют с монитором, клавиатурой и мышью и
запускаются во время загрузки системы. Аналогичное понятие
существует и на уровне приложений. Например, в JVM потоки-
демоны работают в фоновом режиме и предоставляют
вспомогательные услуги пользовательским потокам
выполнения. Эти потоки-демоны имеют низкий приоритет,
работают в фоновом режиме, не влияют на жизненный цикл
приложения и выполняют такие задачи, как сборка мусора или
финализация.
В Kubernetes тоже есть похожее понятие — набор демонов
DaemonSet. Как мы знаем, Kubernetes является распределенной
платформой, разбросанной по нескольким узлам, основная
задача которой — управление подами приложений, поэтому
DaemonSet представлен подами, выполняющимися в фоновом
режиме на узлах кластера и предоставляющими некоторые
услуги остальной части кластера.
Решение
Набор реплик ReplicaSet и его предшественник
ReplicationController — это управляющие структуры,
отвечающие за выполнение определенного количества подов.
Эти контроллеры постоянно проверяют список запущенных
подов и следят за тем, чтобы фактическое количество
выполняющихся подов всегда соответствовало желаемому. В
этом смысле набор демонов DaemonSet действует аналогично и
следит, чтобы всегда выполнялось определенное количество
подов. Разница лишь в том, что первые два, ReplicaSet и
ReplicationController, поддерживают выполнение
определенного количества подов, руководствуясь обычными
требованиями к высокой доступности и уровню нагрузки для
приложений, независимо от количества узлов.
DaemonSet, напротив, решая, сколько экземпляров подов
запустить, не учитывает уровень нагрузки. Его главная задача
— поддерживать выполнение одного пода на каждом узле или
на определенных узлах. Давайте посмотрим, как определяется
DaemonSet (листинг 9.1).
Листинг 9.1. Ресурс DaemonSet
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: random-refresher
spec:
selector:
matchLabels:
app: random-refresher
template:
metadata:
labels:
app: random-refresher
spec:
nodeSelector:
feature: hw-rng
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
command:
- sh
- -c
- >-
"while true; do
java -cp / RandomRunner
/host_dev/random 100000;
sleep 30; done"
volumeMounts:
- mountPath: /host_dev
name: devices
volumes:
- name: devices
hostPath:
path: /dev
Использовать только узлы с меткой feature и значением
hw-rng.
Наборы демонов DaemonSet часто монтируют разделы
файловой системы узла для решения задач обслуживания.
hostPath для прямого доступа к каталогам узла.
При таком поведении основными кандидатами для
включения в набор демонов DaemonSet обычно являются
процессы, связанные с поддержкой инфраструктуры, такие как
сборщики журналов, экспортеры метрик и даже
маршрутизаторы kube-proxy, которые выполняют операции на
уровне кластера. Существует много различий в том, как
DaemonSet и ReplicaSet управляют подами, но основные из них
следующие:
Через DNS
Создать автономную (headless) службу Service с тем же
селектором пода, как в DaemonSet, и использовать ее для
извлечения нескольких записей A из DNS, содержащих IP-
адреса и порты всех подов.
статические поды
Другой способ запуска контейнеров подобно тому, как это
делает DaemonSet, — использовать статические поды. Kubelet,
помимо взаимодействия с Kubernetes API Server и получения
определений подов, может также получать объявления
ресурсов из локального каталога. Подами, которые определены
таким способом, управляет только Kubelet, и они выполняются
только на одном узле. Программный интерфейс служб не
наблюдает за этими подами, они не управляются никакими
контроллерами и их работоспособность не проверяется. За
такими подами наблюдает только клиент Kubelet, который
перезапускает их, когда они завершаются. Аналогично, Kubelet
периодически сканирует указанный в настройках каталог,
обнаруживает изменения в определениях подов и добавляет
или удаляет поды при необходимости.
Пояснение
В этой книге мы описываем особенности и паттерны
Kubernetes, которые используются в основном
разработчиками, а не администраторами. DaemonSet находится
где-то посередине, больше тяготея к набору инструментов
администратора, но мы включили его в обсуждение, потому
что он также часто используется разработчиками приложений.
Контроллеры DaemonSet и CronJob также являются
прекрасными примерами, как Kubernetes превращает понятия,
характерные для одного узла, такие как Crontab и демоны, в
кластерные примитивы для управления распределенными
системами. Все это новые распределенные понятия, которые
разработчики тоже должны знать.
Дополнительная информация
Задача
Одна из основных возможностей фреймворка Kubernetes —
простота и прозрачность масштабирования приложений. Поды
могут масштабироваться императивно, командой kubectl
scale, или декларативно, путем определения такого
контроллера, как ReplicaSet, и даже динамически, основываясь
на нагрузке на приложение, как описано в главе 24 «Эластичное
масштабирование». Запуская несколько экземпляров одной и
той же службы (не контроллера службы Service, а компонента
распределенного приложения, представленного подом),
система может увеличивать пропускную способность и
доступность. Доступность увеличивается, потому что в случае
выхода из строя одного экземпляра службы диспетчер сможет
переадресовать запросы другим, исправным экземплярам. В
Kubernetes множественные экземпляры являются репликами
пода, а ресурс Service отвечает за диспетчеризацию запросов.
Однако иногда желательно, чтобы действовал только один
экземпляр службы. Например, если служба выполняет
некоторую периодическую задачу, тогда при одновременном
выполнении нескольких экземпляров каждый будет запускать
задачу через запланированные интервалы, что приведет к
дублированию, а не к запуску только одной задачи, как
ожидалось. Другой пример: служба, которая выполняет опрос
определенных ресурсов (файловой системы или базы данных),
и требуется, чтобы такой опрос и обработку результатов
осуществлял только один экземпляр и, возможно, даже один
поток. Третий случай имеет место, когда речь заходит об
однопоточном потребителе, извлекающем сообщения из
брокера сообщений, который также является службой-
одиночкой.
Во всех этих и подобных им ситуациях нужно иметь
возможность ограничивать количество активных экземпляров
службы в каждый момент времени (обычно требуется только
один), независимо от того, сколько экземпляров было
запущено и продолжает работать.
Решение
Запуск нескольких реплик одного модуля создает топологию
активный-активный, в которой все экземпляры службы
активны. Но нам нужна топология активный-пассивный (или
ведущий-ведомый), в которой активен только один экземпляр, а
все остальные являются пассивными. Блокировку приложения
можно организовать на двух уровнях: извне и изнутри.
Рис. 10.1. Механизм блокировки приложения извне
Рис. 10.2. Механизм блокировки приложения изнутри
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: random-generator-pdb
spec:
selector:
matchLabels:
app: random-generator
minAvailable: 2
Пояснение
Когда требуются строгие гарантии единственности экземпляра,
их нельзя получить, полагаясь на ReplicaSet и механизмы
блокировки вне приложения. Назначение контроллера
ReplicaSet в Kubernetes — гарантировать доступность подов, а
не поддерживать семантику «не более одного». Как следствие,
существует много сценариев нарушения требования
единственности (например, когда узел, на котором запущен
под-одиночка, отключается от остальной части кластера и
отключенный экземпляр пода заменяется новым), когда в
течение короткого периода времени одновременно действуют
два пода. Если это неприемлемо, используйте контроллеры
StatefulSet или рассмотрите варианты приобретения
блокировок в приложении, которые предоставляют больший
контроль над процессом выбора лидера с более строгими
гарантиями. Подход на основе блокировок также поможет
предотвратить случайное масштабирование подов при
изменении значения параметра replicas.
Иногда требуется, чтобы только один компонент
контейнерного приложения действовал в одиночку. Например,
контейнерное приложение, реализующее конечную точку
HTTP, можно безопасно масштабировать до нескольких
экземпляров, но в нем имеется компонент опроса, который
должен выполняться в одиночку. Использование подхода с
блокировкой вне приложения не может использоваться в такой
ситуации, так как помешает масштабированию всей службы.
Мы должны либо выделить компонент-одиночку в отдельную
единицу развертывания (что хорошо в теории, но не всегда
практично из-за больших накладных расходов), либо
использовать механизм блокировки внутри приложения и
блокировать только этот компонент. Это позволит прозрачно
масштабировать все приложение и конечные точки HTTP, а для
других частей использовать поведение активный-пассивный.
Дополнительная информация
Задача
К настоящему моменту мы познакомились со многими
примитивами Kubernetes для создания распределенных
приложений: контейнерами с поддержкой проверки
работоспособности и ограничения ресурсов, группами
контейнеров (подами), механизмом динамического
размещения подов в пределах кластера, пакетными заданиями,
инструментами планирования заданий, подами-одиночками и
многими другими. Все эти примитивы характеризует одна
общая черта: они рассматривают управляемое приложение как
приложение без сохранения состояния, сконструированное из
идентичных взаимозаменяемых контейнеров и
соответствующее принципам методологии «Двенадцать
факторов».
Наличие платформы, решающей задачи размещения,
отказоустойчивости и масштабировании приложений без
состояния, дает значительное преимущество, однако не менее
важной является поддержка приложений с состоянием, каждый
экземпляр которых уникален и имеет свои характеристики.
В реальном мире за каждой масштабируемой службой без
состояния стоит служба с состоянием, обычно в форме
некоторого хранилища данных. На ранних этапах развития
Kubernetes, когда еще отсутствовала поддержка приложений с
состоянием, эта проблема решалась путем размещения
приложений без состояния в Kubernetes, а компонентов с
состоянием — вне кластера, в общедоступном облаке или на
локальном аппаратном обеспечении, управляемом с помощью
традиционных, не облачных механизмов. Учитывая, что
каждое предприятие имеет множество приложений с
состоянием (устаревших и современных), отсутствие их
поддержки считалось существенным ограничением Kubernetes,
известной как универсальная облачная платформа.
Но какие типичные требования предъявляются
приложениями с состоянием? Мы можем развернуть
приложение с состоянием, такое как Apache ZooKeeper,
MongoDB, Redis или MySQL, используя: развертывание
Deployment, которое создает набор реплик ReplicaSet с
параметром replicas = 1, чтобы обеспечить надежность
приложения; службу Service для поддержки обнаружения его
конечной точки; и PersistentVolumeClaim с PersistentVolume в
роли хранилища для состояния.
В общем и целом это верно для приложения с состоянием,
действующего в единственном экземпляре, но не совсем,
потому что ReplicaSet не гарантирует семантику «не больше
одного экземпляра», и количество реплик может
увеличиваться на короткие промежутки времени. Это может
иметь катастрофические последствия и приводить к потере
данных. Кроме того, серьезные проблемы могут возникать в
случае с распределенными службами с состоянием, состоящим
из нескольких экземпляров. Приложение с состоянием,
включающее несколько кластерных служб, требует от базовой
инфраструктуры разносторонних гарантий. Рассмотрим
некоторые из наиболее распространенных требований,
предъявляемых распределенными приложениями с
сохранением состояния.
Хранилище
Мы можем увеличить количество реплик в ReplicaSet и
получить распределенное приложение с состоянием. Но как
определить требования к хранилищу в таком случае? Обычно
для распределенного приложения с состоянием, такого как
упомянутое выше, требуется выделенное постоянное
хранилище для каждого экземпляра. Набор реплик ReplicaSet с
параметром replicas = 3 и определением
PersistentVolumeClaim (PVC) приведет к тому, что все три пода
будут подключены к одному и тому же постоянному тому
PersistentVolume (PV). ReplicaSet и PVC гарантируют лишь
запуск требуемого числа экземпляров и подключение
хранилища к узлам, где действуют эти экземпляры, но само
хранилище не является выделенным, а совместно используется
всеми экземплярами пода.
Решить проблему общего хранилища для всех экземпляров
можно, реализовав механизм деления хранилища на сегменты
и бесконфликтного их использования внутри самого
приложения. Однако в этом случае создается единая точка
отказа с единственным хранилищем. Кроме того, такой подход
подвержен ошибкам из-за изменения количества подов в
процессе масштабирования и может вызвать серьезные
сложности в реализации предотвращения повреждения или
потери данных во время масштабирования.
Другое решение — создание отдельного набора реплик
ReplicaSet (с replicas = 1) для каждого экземпляра
распределенного приложения с состоянием. В этом сценарии
каждый набор ReplicaSet получает свой запрос постоянного
тома PVC и выделенное хранилище. Недостаток этого подхода
— необходимость большого объема ручного труда: для
масштабирования придется создать новый набор определений
ReplicaSet, PVC или Service. В этом подходе не хватает всего
одной абстракции, управляющей всеми экземплярами
приложения с состоянием как единым целым.
Идентичность
Как следует из предыдущих требований, кластерные
приложения с состоянием сильно зависят от наличия
постоянных сетевых координат и выделенного
долговременного хранилища для каждого экземпляра, потому
что каждый экземпляр приложения с состоянием уникален и
имеет свою идентичность, основными компонентами которой
являются долговременное хранилище и сетевые координаты. К
этому списку также можно добавить идентификатор/имя
экземпляра (для некоторых приложений с состоянием
требуются уникальные постоянные имена), которое в
Kubernetes будет именем пода. Поды, созданные с помощью
ReplicaSet, получают произвольные имена и не сохраняют их
после перезапуска.
Упорядоченность
Кроме уникальной и долговременной идентификации,
экземпляры кластерных приложений с состоянием имеют
фиксированное положение в их коллекциях. Обычно это
положение влияет на последовательность масштабирования
экземпляров вверх и вниз, но также может использоваться для
распределения данных, блокировки или выбора ведущего
экземпляра.
Другие требования
Постоянное и долговременное хранилище, постоянство
сетевых координат и упорядоченность — все это типичные
требования, предъявляемые кластерными приложениями с
состоянием. Но управление приложениями с состоянием
предъявляет также множество других специфических
требований, которые меняются от случая к случаю. Например,
некоторые приложения поддерживают понятие кворума и
требуют постоянного присутствия некоторого минимального
количества экземпляров; некоторые из них требуют
последовательного развертывания экземпляров, тогда как
другие поддерживают параллельное развертывание; некоторые
допускают наличие дубликатов экземпляров, а некоторые нет.
Предусмотреть все эти уникальные требования и предоставить
универсальный механизм просто невозможно, поэтому
Kubernetes также позволяет создавать определения
нестандартных ресурсов и операторов для управления
приложениями с состоянием. Об операторах мы поговорим в
главе 23.
Выше были перечислены некоторые типичные проблемы
управления распределенными приложениями с состоянием и
ряд не самых лучших вариантов их решения. А теперь давайте
рассмотрим механизм, имеющийся в Kubernetes, помогающий
удовлетворить эти требования, — примитив StatefulSet.
Решение
Чтобы объяснить, что предлагает StatefulSet для управления
приложениями с состоянием, мы периодически будем
сравнивать его с уже знакомым примитивом ReplicaSet,
который в Kubernetes используется для выполнения
приложений без состояния. Можно провести такую аналогию:
примитив StatefulSet предназначен для управления
домашними любимцами, а ReplicaSet — для управления
домашним скотом. Домашние любимцы и скот — известная
(хотя и неоднозначная) аналогия в мире DevOps: идентичные и
взаимозаменяемые серверы называются скотом, а
незаменимые уникальные серверы, требующие
индивидуального ухода, называются домашними любимцами.
Аналогично, примитив StatefulSet (первоначально основанный
на этой аналогии и названный набором любимцев PetSet)
предназначен для управления уникальными модулями, тогда
как ReplicaSet предназначен для управления идентичными
взаимозаменяемыми подами.
Давайте рассмотрим, как работают StatefulSet и как они
удовлетворяют потребности приложений с состоянием. В
листинге 11.1 приводится определение
нашей службы
генератора случайных чисел в форме StatefulSet.15
Листинг 11.1. Служба Service для доступа к StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: rg
spec:
serviceName: random-generator
replicas: 2
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: logs
mountPath: /logs
volumeClaimTemplates:
- metadata:
name: logs
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Mi
Хранилище
Большинство приложений с состоянием хранят некоторую
информацию и поэтому требуют выделенного постоянного
хранилища для каждого экземпляра. Настройка и связывание
постоянного хранилища с подом в Kubernetes производится
посредством PV и PVC. Для создания PVC вместе с подом
StatefulSet использует элемент volumeClaimTemplates. Это
дополнительное свойство является одним из основных
отличий StatefulSet от примитива ReplicaSet, который имеет
элемент persistentVolumeClaim.
Вместо ссылки на предопределенный PVC StatefulSet
динамически создает PVC с помощью volumeClaimTemplates
во время создания пода. Благодаря этому каждый под получает
свой выделенный PVC во время создания, а также во время
масштабирования вверх, при изменении счетчика replicas в
StatefulSet.
Как вы, наверное, заметили, мы говорили, что PVC
создаются и связываются с подами, но мы ничего не сказали о
PV. Дело в том, что примитив StatefulSet никак не управляет
PV. Хранилище для подов должно быть заранее выделено
администратором или провайдером PV на основе
запрошенного класса хранения и готово для использования
подами с состоянием.
Обратите внимание на асимметричное поведение:
масштабирование StatefulSet вверх (увеличение счетчика
replicas) создает новые поды и связанные с ними PVC. При
масштабировании вниз поды удаляются, но PVC (и PV) никуда
не исчезают, то есть постоянные тома PV не утилизируются и
не удаляются, и Kubernetes не может освободить хранилище.
Такое поведение основано на предположении, что хранилища
для приложений с состоянием имеют важнейшее значение и
периодическое масштабирование вниз не должно приводить к
потере данных. Если вы уверены, что приложение с состоянием
было остановлено специально и скопировало/передало свои
данные другим экземплярам, то можете удалить PVC вручную,
что позднее позволит повторно использовать
соответствующий постоянный том PV.
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
clusterIP: None
selector:
app: random-generator
ports:
- name: http
port: 8080
Рис. 11.1. Распределенное приложение с состоянием в Kubernetes
Идентичность
Идентичность — фундамент, на котором основываются все
остальные гарантии контроллера StatefulSet. Опираясь на имя
StatefulSet, можно получить имя пода и его идентичность.
Используя эту идентичность, можно определить имена PVC,
обращаться к конкретными подам через автономные службы
Service и т.д. Идентичность любого пода известна еще до его
создания, и это знание можно использовать в самом
приложении.
Упорядоченность
Распределенное приложение с состоянием по определению
состоит из нескольких уникальных и невзаимозаменяемых
экземпляров. Кроме их уникальности, экземпляры также могут
быть связаны друг с другом порядком их создания, в
соответствии с требованием упорядоченности.
С точки зрения StatefulSet упорядоченность важна только
для масштабирования. Поды имеют имена, включающие
порядковый индекс в конце (начиная с 0), и порядок их
создания определяет порядок, в котором они масштабируются
вверх и вниз (при масштабировании вниз поды
останавливаются в обратном порядке следования их индексов,
от n — 1 до 0).
Когда определяется ReplicaSet с несколькими репликами,
поды планируются и запускаются все вместе, не дожидаясь
успешного запуска предыдущих подов (под успешным
запуском подразумевается переход в состояние готовности, как
описывается в главе 4 «Проверка работоспособности»).
Порядок запуска подов и их переход в состояние готовности не
гарантируется. То же происходит, когда происходит
масштабирование ReplicaSet вниз (изменением счетчика
replicas или удалением набора). Все поды, принадлежащие
ReplicaSet, начинают останавливаться одновременно без
какого-либо порядка. Такое поведение обеспечивает быструю
остановку, но может оказаться нежелательным для
приложений с состоянием, особенно если имеют место
фрагментирование и распределение данных между
экземплярами.
Чтобы обеспечить надлежащую синхронизацию данных в
процессе масштабирования вверх или вниз, StatefulSet по
умолчанию выполняет запуск и остановку подов
последовательно. Это означает, что поды запускаются, начиная
с первого (с индексом 0), и каждый последующий запускается
только после успешного запуска предыдущего.
Масштабирование вниз выполняется в обратном порядке:
сначала останавливается под с самым большим индексом, и
только после успешной остановки начинается остановка пода
со следующим индексом в порядке убывания. Так
продолжается, пока не остановится под с индексом 0.
Другие особенности
StatefulSet обладает еще целым рядом характеристик, которые
можно настраивать в соответствии с потребностями
приложений с состоянием. Каждое приложение с состоянием
является уникальным и требует внимательного исследования
перед попыткой вписать его в модель StatefulSet. Давайте
рассмотрим еще несколько возможностей Kubernetes, которые
могут пригодиться при работе с приложениями с состоянием.
Раздельное обновление
Мы описали выше гарантии упорядочения при
масштабировании подов в StatefulSet. Для обновления
запущенных приложений с состоянием (например,
изменением параметра .spec.template), StatefulSet
поддерживает поэтапное (такое, как канареечное)
развертывание и гарантирует выполнение определенного
количества экземпляров, пока обновления будут
применяться к остальной части экземпляров.
Организовать раздельное обновление экземпляров при
использовании стратегии по умолчанию поэтапного
обновления можно, указав число в параметре
.spec.updateStrategy.rollingUpdate.partition.
Этот параметр (со значением по умолчанию 0) определяет
порядковый номер, по которому следует разделить
StatefulSet для обновления. Если параметр указан, обновятся
все поды с индексами большими или равными значению
partition, а остальные — нет. Это относится даже к
остановленным подам; Kubernetes воссоздаст их в
предыдущей версии. Это позволяет обновить сначала одну
часть кластерного приложения с состоянием (например,
чтобы гарантировать наличие кворума), а затем развернуть
изменения в остальной части кластера, записав в параметр
partition значение 0.
Параллельное развертывание
Когда параметру .spec.podManagementPolicy
присваивается значение Parallel, контроллер StatefulSet
запускает или завершает работу всех подов одновременно,
не дожидаясь готовности или полной остановки
предыдущего пода, прежде чем перейти к следующему. Если
приложение не требует последовательной обработки, эта
настройка может ускорить запуск или остановку.
Пояснение
В этой главе мы рассмотрели некоторые стандартные
требования и проблемы управления распределенными
приложениями с состоянием. Мы выяснили, что организовать
выполнение единственного экземпляра приложения с
состоянием относительно просто, но обслуживание
распределенного состояния — это сложная и многоплановая
задача. Обычно мы связываем понятие «состояние» с понятием
«хранилище», однако здесь мы увидели, что состояние может
иметь несколько аспектов и разные приложения с состоянием
могут требовать разных гарантий. В этом отношении
контроллер StatefulSet является отличным средством для
реализации распределенных приложений с состоянием. Он
учитывает необходимость постоянного хранилища, работы в
Сети (через сервисы), поддержки идентичности,
упорядоченности и некоторых других аспектов. Он предлагает
хороший набор строительных блоков для автоматического
управления приложениями с состоянием и превращает их в
полноправных граждан облачного мира.
Контроллеры StatefulSet дают хорошее начало, но мир
приложений с состоянием уникален и сложен. Кроме
приложений с состоянием, разработанных для выполнения в
облаке, которые прекрасно вписываются в StatefulSet,
существует масса устаревших приложений, разработанных для
выполнения в обычном, не облачном, окружении и
предъявляющих еще более широкие требования. К счастью,
фреймворку Kubernetes есть чем ответить на это. В сообществе
Kubernetes давно поняли, что вместо моделирования
различных рабочих нагрузок с помощью ресурсов Kubernetes и
реализации их поведения с использованием универсальных
контроллеров предпочтительнее позволить пользователям
реализовать свои контроллеры и даже дать им возможность
моделировать ресурсы приложений с помощью своих
определений ресурсов и поведения с помощью операторов.
В главах 22 и 23 вы познакомитесь с соответствующими
паттернами Controller (контроллер) и Operator (Оператор),
которые с успехом можно использовать для управления
сложными приложениями с состоянием в облачных
окружениях.
Дополнительная информация
Задача
Приложения, развернутые в Kubernetes, редко существуют
сами по себе и обычно взаимодействуют с другими службами
внутри кластера или с системами вне кластера.
Взаимодействие может быть инициировано изнутри или извне.
Взаимодействия, инициируемые изнутри, обычно
выполняются методом опроса: приложение, сразу после
запуска или позднее, подключается к другой системе и
начинает посылать и получать данные. Типичным примером
может служить приложение, запущенное в поде, которое
устанавливает соединение с файловым сервером и начинает
использовать файлы на нем, или подключается к брокеру
сообщений и начинает получать либо отправлять сообщения,
или соединяется с реляционной базой данных либо
хранилищем пар ключ/значение и начинает читать или писать
данные.
Важная отличительная особенность здесь заключается в
том, что приложение, запущенное в поде, решает в какой-то
момент открыть исходящее соединение с другим подом или
внешней системой и начинает обмен данными. В этом
сценарии взаимодействия инициируются изнутри, и для
приложения не нужны никакие дополнительные настройки в
Kubernetes.
Этот подход часто используется для реализации паттернов,
описанных в главе 7 «Пакетное задание» или в главе 8
«Периодическое задание». Кроме того, к другим системам
иногда активно подключаются поды, действующие под
управлением DaemonSet или ReplicaSet. Однако в Kubernetes
наиболее распространен вариант, когда в кластере имеются
службы, ожидающие соединений, чаще всего в форме
входящих HTTP-соединений, от других подов в кластере или
внешних систем. В этих случаях потребителям услуг необходим
механизм для обнаружения подов, которые динамически
размещаются планировщиком на узлах и иногда
масштабируются вверх и вниз.
Это было бы серьезной проблемой, если бы нам пришлось
самим отслеживать, регистрировать и искать конечные точки
динамических подов в Kubernetes. Вот почему Kubernetes
реализует паттерн Service Discovery (Обнаружение служб) с
помощью различных механизмов, которые мы рассмотрим в
этой главе.
Решение
В эпоху до появления Kubernetes наиболее распространенным
механизмом обнаружения служб было обнаружение на стороне
клиента. Когда потребителю требовалось обратиться к службе,
которая может масштабироваться до нескольких экземпляров,
потребитель должен был иметь агента обнаружения,
способного отыскать в реестре экземпляры службы и выбрать
один из них. Это решение может быть реализовано, например,
с помощью встроенного агента (такого, как клиент ZooKeeper,
клиент Consul или Ribbon) или с помощью другого процесса,
такого как Prana, предназначенного для поиска службы в
реестре, как показано на рис. 12.1.
В эпоху после появления Kubernetes многие функции
распределенных систем, такие как размещение, проверка и
восстановление работоспособности, изоляция ресурсов, а
также обнаружение служб и балансировка
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
protocol: TCP
RANDOM_GENERATOR_SERVICE_HOST=10.109.72.32
RANDOM_GENERATOR_SERVICE_PORT=8080
Несколько портов
В одном определении службы можно зарезервировать
несколько портов, исходных и целевых. Например, если под
поддерживает возможность соединения по протоколу HTTP
через порт 8080 и по протоколу HTTPS через порт 8443, нет
необходимости определять две службы, потому что одна
служба Service может связать оба порта с портами 80 и 443.
Близость сеансов
Когда появляется новый запрос, Service выбирает случайный
под. Это поведение можно изменить, определив параметр
sessionAffinity: ClientIP, который привяжет все
запросы, исходящие с одного и того же IP-адреса, к
определенному поду. Не забывайте, что службы Service в
Kubernetes выполняют балансировку нагрузки на
транспортном уровне L4 и не могут исследовать сетевые
пакеты и выполнять балансировку нагрузки на уровне
приложений, например на основе cookie.
Проверка готовности
В главе 4 «Проверка работоспособности» вы узнали, как
определить параметр readinessProbe для контейнера.
Если для пода определены проверки готовности и они
терпят неудачу, под удаляется из списка конечных точек
службы Service, даже если он соответствует селектору.
Виртуальный IP-адрес
При создании служба Service с типом type: ClusterIP
получает постоянный виртуальный IP-адрес. Однако этот
IP-адрес не соответствует ни одному сетевому интерфейсу и
не существует в реальности. Он обслуживается
компонентом kube-proxy, действующим на каждом узле,
который получает информацию о новой службе и обновляет
настройки iptables на своем узле, добавляя правила, которые
перехватывают сетевые пакеты, направляющиеся на этот
виртуальный IP-адрес, и заменяют его IP-адресом
выбранного пода. В настройки iptables не добавляются
правила для ICMP — только для протокола, указанного в
определении службы, такого как TCP или UDP. Как
следствие, нет возможности использовать ping для
проверки IP-адреса службы, потому что эта команда
использует протокол ICMP. Зато есть возможность получить
доступ к службе через TCP (например, послать HTTP-запрос).
Выбор ClusterIP
В определении службы Service можно явно указать IP-адрес в
поле .spec.clusterIP. Это должен быть действительный
IP-адрес из предопределенного диапазона. Хотя это и не
рекомендуется, но этот параметр может пригодиться при
работе с устаревшими приложениями, настроенными на
использование определенного IP-адреса, или при наличии
существующей записи DNS, которую желательно
использовать повторно.
Службы Service с типом type: ClusterIP доступны только
внутри кластера Kubernetes, используются для обнаружения
подов, соответствующих селекторам, и применяются наиболее
часто. Далее мы рассмотрим другие типы служб, которые
позволяют обнаруживать конечные точки, указанные вручную.
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ClusterIP
ports:
- protocol: TCP
port: 80
Далее, в листинге 12.4 определяется ресурс конечных точек
с именем, совпадающим с именем службы Service, который
определяет целевые IP-адреса и порты.
Листинг 12.4. Конечные точки для внешней службы
apiVersion: v1
kind: Endpoints
metadata:
name: external-service
subsets:
- addresses:
- ip: 1.1.1.1
- ip: 2.2.2.2
ports:
- port: 8080
apiVersion: v1
kind: Service
metadata:
name: database-service
spec:
type: ExternalName
externalName: my.database.example.com
ports:
- port: 80
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: NodePort
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
nodePort: 30036
protocol: TCP
Номер порта
Вместо выбора конкретного порта в виде параметра
nodePort: 30036 можно позволить Kubernetes
самостоятельно выбрать порт из его диапазона.
Правила брандмауэра
Так как этот метод основан на открытии порта на всех узлах,
в брандмауэр необходимо добавить дополнительные
правила, разрешающие внешним клиентам подключаться к
этому порту.
Выбор узла
Внешний клиент может открыть соединение с любым узлом
в кластере. Но если узел окажется недоступным, клиентское
приложение должно само установить соединение с другим
работоспособном узлом. С этой целью может быть полезно
установить балансировщик нагрузки перед узлами, который
выбирает исправные узлы и обрабатывает отказы.
Выбор пода
Когда клиент открывает соединение через порт узла, он
перенаправляется к случайно выбранному поду, который
может находиться на том же узле, где было открыто
соединение, или на другом узле.
Рис. 12.5. Обнаружение служб по порту узла
Исходящие адреса
Существуют еще некоторые особенности, связанные с
исходящими адресами пакетов, отправляемых службам
Service разных типов. В частности, службы типа NodePort
получают пакеты с исходящими адресами клиентов,
прошедшими процедуру преобразования сетевых адресов
(NAT), то есть исходящие IP-адреса клиентов в сетевых
пакетах заменяются внутренними адресами узла. Например,
когда клиентское приложение отправляет пакет на узел 1,
исходящий адрес в нем меняется на адрес узла, адрес
назначения меняется на адрес пода, после чего пакет
пересылается на узел 2, где находится сам под. Когда под
получает сетевой пакет, исходящий адрес в нем будет
отличаться от адреса оригинального клиента и совпадать с
адресом узла 1. Чтобы этого не происходило, можно
определить параметр externalTrafficPolicy: Local,
как описано выше, и передавать трафик только подам,
находящимся на узле 1.
Другой способ организовать возможность обнаружения
служб для внешних клиентов — настроить балансировку
нагрузки. Вы уже видели, как служба Service с типом type:
NodePort строится поверх обычной службы с типом type:
ClusterIP, дополнительно открывая порт на каждом узле.
Ограничением этого подхода является необходимость иметь
балансировщик нагрузки для клиентских приложений,
выбирающий работоспособный узел. Службы с типом
LoadBalancer устраняют это ограничение.
Кроме создания обычных служб Service и открытия портов
на каждом узле с помощью служб с типом type: NodePort,
можно также экспортировать службу вовне с использованием
балансировщика нагрузки облачного провайдера. На рис. 12.6
показана такая конфигурация: собственный балансировщик
нагрузки играет роль шлюза в кластер Kubernetes.
Рис. 12.6. Обнаружение служб посредством балансировщика нагрузки
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: LoadBalancer
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
protocol: TCP
status:
loadBalancer:
ingress:
- ip: 146.148.47.155
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: random-generator
spec:
backend:
serviceName: random-generator
servicePort: 8080
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: random-generator
annotations:
nginx.ingress.kubernetes.io/rewrite-target:
/
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: random-generator
servicePort: 8080
- path: /cluster-status
backend:
serviceName: cluster-status
servicePort: 80
маршруты в OpenShift
Red Hat OpenShift — популярный корпоративный дистрибутив
Kubernetes. Помимо полной совместимости с Kubernetes,
OpenShift предлагает ряд дополнительных возможностей. Одна
из них — маршруты Route, очень похожие на Ingress. Они
настолько похожи, что почти неразличимы. Следует отметить,
что появление поддержки маршрутов Route предшествовало
введению объекта Ingress в Kubernetes, поэтому маршруты
Route можно смело считать прямыми предшественниками
Ingress.
Пояснение
В этой главе мы рассмотрели основные механизмы,
используемые при реализации паттерна Service Discovery
(Обнаружение служб) в Kubernetes. Обнаружение динамических
подов внутри кластера всегда производится через ресурс
Service, хотя разные варианты могут приводить к разным
реализациям. Абстракция Service — это высокоуровневый
облачный способ настройки низкоуровневых деталей, таких
как виртуальные IP-адреса, правила iptables, записи DNS или
переменные окружения. Обнаружение служб извне кластера
основывается на абстракции Service и направлено на
предоставление услуг внешнему миру. Тип NodePort
позволяет определить простую службу, доступную извне,
однако для настройки служб с высокой доступностью требуется
интеграция с поставщиком инфраструктуры платформы.
В табл. 12.1 перечислены разные способы реализации
паттерна Service Discovery (Обнаружение служб) в Kubernetes. В
ней дается краткая информация о различных механизмах
обнаружения служб, описанных в этой главе, организованных в
порядке возрастания сложности. Мы надеемся, что она
поможет вам мысленно построить модель и лучше понять ее.
Предпочтительный
NodePort type: NodePort Внешний вариант для трафика,
отличного от HTTP
LoadBalancer type: LoadBalancer Внешний Требует поддержки со
стороны облачной
инфраструктуры
Интеллектуальный
Ingress kind: Ingress Внешний механизм маршрутизации
на основе L7/HTTP
Дополнительная информация
• Ingress (https://kubernetes.io/docs/concepts/services-
networking/ingress/).
Задача
Большинство облачных приложений не имеют состояния и
отличительной идентичности. Однако иногда даже этим
приложениям бывает необходима информация о себе и об
окружении, в котором они выполняются. Это может быть
информация, известная только во время выполнения, такая
как имя пода, IP-адрес и имя хоста, на котором размещено
приложение. Или другая статическая информация,
определяемая на уровне пода, такая как конкретные запросы и
лимиты ресурсов, или динамическая, такая как аннотации и
метки, которые могут изменяться пользователем во время
выполнения.
Например, в зависимости от ресурсов, доступных
контейнеру, приложение может настраивать размер пула
потоков выполнения, изменять алгоритм сбора мусора или
распределение памяти. Имя пода и имя хоста могут
пригодиться для журналирования или отправки метрик на
центральный сервер. Также вам может понадобиться отыскать
другие поды в том же пространстве имен с определенной
меткой и объединить их в кластерное приложение. Для этих и
других случаев Kubernetes предоставляет Downward API.
Решение
Описанные требования и решение, представленное ниже,
относятся не только к контейнерам, но и к любому
динамическому окружению, где метаданные ресурсов могут
изменяться. Например, AWS предлагает службы Instance
Metadata и User Data, которые можно запросить у любого
экземпляра EC2. Аналогично, AWS ECS предоставляет API, с
помощью которого контейнеры могут запрашивать и получать
информацию о кластере.
В Kubernetes используется еще более элегантный и простой
подход. Downward API позволяет передавать метаданные о
поде в контейнеры и в кластер через переменные окружения и
файлы. Это те же механизмы, которые мы использовали для
передачи данных, связанных с приложением, из ConfigMap и
Secret. Но в этом случае данные создаются не нами. Мы просто
указываем ключи, которые нас интересуют, а Kubernetes
присваивает им значения динамически. На рис. 13.1
представлена общая схема, как Downward API внедряет в поды
информацию о ресурсах и о среде времени выполнения.
Рис. 13.1. Механизмы интроспекции приложений
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: MEMORY_LIMIT
valueFrom:
resourceFieldRef:
container: random-generator
resource: limits.memory
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
volumeMounts:
- name: pod-info
mountPath: /pod-info
volumes:
- name: pod-info
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: annotations
fieldRef:
fieldPath: metadata.annotations
Пояснение
Во многих случаях приложению необходимо иметь
информацию о себе и об окружении, в котором оно работает.
Kubernetes предлагает ненавязчивые механизмы для
интроспекции и внедрения метаданных. Один из недостатков
Downward API — ограниченное количество ключей, на которые
можно ссылаться. Если приложению нужно больше данных,
особенно о других ресурсах или метаданных, связанных с
кластером, оно должно запрашивать их через API Server.
Этот метод используется многими приложениями, которые
запрашивают API Server с целью обнаружения других подов в
том же пространстве имен, которые имеют определенные
метки или аннотации. Затем приложение может сформировать
кластер с обнаруженными подами и, например,
синхронизировать состояние. Он также используется
приложениями мониторинга для поиска интересующих их
подов и управления ими.
Существует множество клиентских библиотек на разных
языках программирования, предназначенных для
взаимодействия с Kubernetes API Server и получения
информации о самом приложении, которую нельзя получить с
помощью Downward API.
Дополнительная информация
Задача
Задача инициализации широко распространена во многих
языках программирования. В некоторых случаях она является
частью языка, а в некоторых для обозначения конструкций
инициализации используются соглашения об именах и
паттерны. Например, в Java, чтобы создать экземпляр объекта,
который требует дополнительной настройки, используется
конструктор (или, в некоторых необычных ситуациях,
статические блоки). Конструкторы гарантированно
запускаются средой выполнения на этапе создания экземпляра
объекта и только один раз (это только пример; мы не будем
вдаваться в подробности, характерные для разных языков и
ситуаций). Кроме того, конструктор можно использовать для
проверки предварительных условий, например наличия
обязательных параметров. Также конструкторы могут
использоваться для инициализации полей экземпляра
значениями входных аргументов или значениями по
умолчанию.
Контейнеры инициализации (или Init-контейнеры)
представляют аналогичную абстракцию, но на уровне подов.
То есть если в поде имеется один или несколько контейнеров,
которые образуют основное приложение, эти контейнеры
могут иметь предварительные условия, которые должны
удовлетворяться перед их запуском. К числу таких условий
можно отнести, например, настройку специальных разрешений
для файловой системы, схемы базы данных или начальных
данных приложения. Также логика инициализации может
потребовать использовать инструменты и библиотеки, которые
нельзя включить в образ приложения. По соображениям
безопасности образ приложения может не иметь разрешений
для выполнения действий по инициализации. Как вариант, вы
можете отложить запуск приложения, пока не будут
удовлетворены внешние зависимости. Во всех таких случаях
Kubernetes предлагает использовать init-контейнеры, чтобы
отделить действия, связанные с инициализацией, от основных
обязанностей приложения.
Решение
Паттерн Init Containers (Init-контейнеры) в Kubernetes является
частью определения пода и делит все контейнеры в поде на две
группы: собственно init-контейнеры и контейнеры
приложения. Все init-контейнеры выполняются
последовательно, друг за другом, и все они должны
завершиться с признаком успеха, прежде чем Kubernetes
приступит к запуску контейнеров приложения. В этом смысле
init-контейнеры похожи на конструкторы классов в Java,
которые помогают инициализировать объекты. Контейнеры
приложения, напротив, запускаются параллельно, в
произвольном порядке. Описанный процесс показан на рис.
14.1.
Как правило, init-контейнеры имеют небольшой размер,
быстро запускаются и быстро завершаются с признаком успеха,
кроме случаев, когда они используются для задержки запуска
подов, пока не будут удовлетворены все необходимые
зависимости. В этом последнем случае они могут не
завершаться довольно долго. Если init-контейнер завершится с
признаком ошибки, будет произведен перезапуск всего пода
(если только он не отмечен меткой RestartNever), в
результате чего все init-контейнеры будут запущены снова.
Поэтому для предотвращения побочных эффектов желательно
создавать идемпотентные init-контейнеры.
apiVersion: v1
kind: Pod
metadata:
name: www
labels:
app: www
spec:
initContainers:
- name: download
image: axeclbr/git
command:
- git
- clone
- https://github.com/mdn/beginner-html-
site-scripted
- /var/lib/data
volumeMounts:
- mountPath: /var/lib/data
name: source
containers:
- name: run
image: docker.io/centos/httpd
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/www/html
name: source
volumes:
- emptyDir: {}
name: source
command:
- /bin/sh
- "-c"
- "sleep 3600"
Контроллеры доступа
Веб-обработчики доступа
Инициализаторы
Пояснение
Итак, зачем разделять контейнеры в подах на две группы?
Почему бы просто не использовать обычный контейнер
приложения для инициализации пода, если это необходимо?
Дело в том, что эти две категории контейнеров имеют разные
жизненные циклы, цели и иногда даже авторов.
Init-контейнеры запускаются перед контейнерами
приложений, и, что особенно важно, init-контейнеры
запускаются последовательно и только после успешного
завершения текущего init-контейнера. То есть на каждом этапе
процедуры инициализации можно быть уверенными, что
предыдущий этап успешно завершен и можно смело
переходить к следующему этапу. Контейнеры приложений,
напротив, запускаются параллельно и не дают гарантий,
которые дают init-контейнеры. Благодаря этим различиям
можно создавать контейнеры, ориентированные на
инициализацию или решение прикладных задач, и
организовывать их в поды.
Дополнительная информация
• Init-контейнеры (http://bit.ly/2TR7OsD).
Задача
Контейнеры — популярная технология упаковки, которая
позволяет разработчикам и системным администраторам
создавать, доставлять и запускать приложения
унифицированным способом. Контейнер представляет
естественную границу функциональной единицы со своей
средой времени выполнения, циклом выпуска, API и
коллективом разработчиков, которому он принадлежит.
Типичный контейнер действует подобно процессу в Linux —
решает одну проблему, и делает это хорошо, — и создается в
предположении возможности замены и повторного
использования. Последнее особенно важно, поскольку
позволяет быстрее создавать приложения с использованием
существующих специализированных контейнеров.
В настоящее время, чтобы послать HTTP-запрос, не нужно
писать клиентскую библиотеку, достаточно использовать уже
существующую. Аналогично, для обслуживания веб-сайта не
нужно создавать контейнер с веб-сервером, достаточно
использовать уже существующий. Этот подход позволяет
разработчикам не изобретать колесо и создать экосистему с
меньшим количеством контейнеров лучшего качества для
обслуживания. Однако чтобы иметь возможность использовать
узкоспециализированные многоразовые контейнеры,
необходимы способы расширения их возможностей и средства
для организации взаимодействий между ними. Паттерн Sidecar
(Прицеп) описывает как раз такой способ организации
взаимодействий, когда один контейнер расширяет
возможности другого, уже существующего контейнера.
Решение
В главе 1 мы видели, как поды позволяют объединить
несколько контейнеров в один блок. За кулисами, во время
выполнения, под также является контейнером, но запускается
как приостановленный (буквально с помощью команды pause)
процесс перед всеми остальными контейнерами в поде. Он не
делает ничего, кроме того, что хранит все пространств имен
Linux, которые контейнеры приложений используют для
взаимодействия на протяжении жизненного цикла пода. Кроме
этой детали реализации, интерес представляют также все
характеристики, которые предоставляет абстракция пода.
Под является настолько фундаментальным примитивом,
что присутствует во многих облачных платформах, пусть под
разными именами, но всегда со схожими возможностями. Под,
как единица развертывания, накладывает определенные
ограничения времени выполнения на принадлежащие ему
контейнеры. Например, все контейнеры разворачиваются на
одном узле и имеют общий жизненный цикл. Кроме того, под
позволяет своим контейнерам использовать общие тома и
обмениваться данными через локальную сеть или с
применением средств межпроцессных взаимодействий хоста.
Именно поэтому пользователи объединяют контейнеры в
поды. Паттерн Sidecar (Прицеп), который иногда называют
также Sidekick (Компаньон), описывает сценарий добавления
контейнера в под для расширения возможностей другого
контейнера.
Типичным примером, демонстрирующим этот паттерн,
может служить HTTP-сервер и механизм синхронизации с
репозиторием Git. Контейнер HTTP-сервера решает задачи,
связанные с обслуживанием файлов через HTTP и не знает, как
и откуда эти файлы поступают. Точно так же единственной
целью контейнера, осуществляющего синхронизацию с Git,
является синхронизация данных в локальной файловой
системе с данными на сервере Git. Ему неважно, что
происходит с файлами после синхронизации, его единственная
задача — синхронизировать содержимое локальной папки с
содержимым на удаленном сервере Git. В листинге 15.1
приводится определение пода с этими двумя контейнерами,
настроенными на использование тома для обмена файлами.
Листинг 15.1. Реализация паттерна Sidecar (Прицеп)
apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
containers:
- name: app
image: docker.io/centos/httpd
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/www/html
name: git
- name: poll
image: axeclbr/git
volumeMounts:
- mountPath: /var/lib/data
name: git
env:
- name: GIT_REPO
value: https://github.com/mdn/beginner-
html-site-scripted
command:
- "sh"
- "-c"
- "git clone $(GIT_REPO) . && watch -n 600
git pull"
workingDir: /var/lib/data
volumes:
- emptyDir: {}
name: git
Задача
Контейнеры позволяют унифицировать упаковку и запуск
приложений, написанных на разных языках и с
использованием разных библиотек. В настоящее время многие
команды используют разные технологии и создают
распределенные системы, состоящие из гетерогенных
(разнородных) компонентов. Эта разнородность может вызвать
трудности, когда все компоненты должны обрабатываться
другими системами единообразным способом. Паттерн Adapter
(Адаптер) предлагает решение, помогающее скрыть сложность
системы и предоставить унифицированный доступ к ней.
Решение
Этот паттерн лучше проиллюстрировать на примере.
Основным условием успешной эксплуатации распределенных
систем является всеобъемлющий мониторинг и возможность
отправки оповещений. Кроме того, если распределенная
система состоит из нескольких служб, для их мониторинга
можно использовать внешний инструмент, извлекающий и
сохраняющий метрики каждой службы.
Однако службы, написанные на разных языках, могут иметь
разные возможности и поставлять метрики в разных форматах,
отличных от ожидаемого инструментом мониторинга. Такое
разнообразие создает проблему для мониторинга разнородных
приложений с использованием единого решения, которое
ожидает поддержки единого формата всей системой. Паттерн
Adapter (Адаптер) позволяет организовать унифицированный
интерфейс мониторинга, экспортируя метрики из различных
контейнеров приложений в один стандартный формат и
протокол. На рис. 16.1 изображен контейнер-адаптер, который
преобразует информацию с метриками, хранящуюся локально,
во внешний формат, который понимает сервер мониторинга.
apiVersion: apps/v1
kind: Deployment
metadata:
name: random-generator
spec:
replicas: 1
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-
generator:1.0
name: random-generator
env:
- name:
LOG_FILE
value: /logs/random.log
ports:
- containerPort: 8080
protocol: TCP
volumeMounts:
- mountPath: /logs
name: log-volume
# ---------------------------------------
-----
- image: k8spatterns/random-generator-
exporter
name: prometheus-adapter
env:
- name:
LOG_FILE
value: /logs/random.log
ports:
- containerPort: 9889
protocol: TCP
volumeMounts:
- mountPath: /logs
name: log-volume
volumes:
- name: log-
volume
emptyDir: {}
Пояснение
Паттерн Adapter (Адаптер) — это специализированный вариант
паттерна Sidecar (Прицеп), описанного в главе 15. Он действует
как обратный прокси для гетерогенной системы, скрывая ее
сложность за унифицированным интерфейсом. Использование
отдельного названия, отличного от названия более общего
паттерна Sidecar (Прицеп), позволяет более точно обозначить
назначение этого паттерна.
В следующей главе вы познакомитесь с еще одним
специализированным вариантом паттерна Sidecar (Прицеп):
паттерн Ambassador (Посредник), который действует как
прокси, для доступа к службам во внешнем мире.
Дополнительная информация
Задача
Контейнерные службы существуют не в вакууме и часто
обращаются к другим службам, надежный доступ к которым
может быть затруднен. Сложность доступа к другим службам
может быть обусловлена динамическими и изменяющимися
адресами, необходимостью балансировки нагрузки между
кластерными экземплярами служб, использованием
ненадежного протокола или сложных форматов данных. В
идеале контейнеры должны быть узкоспециализированными и
допускающими возможность многократного использования.
Но если у нас есть контейнер, который реализует некоторую
важную функцию и особым образом использует внешнюю
службу, он будет нести больше одной ответственности.
Для доступа к внешней службе может потребоваться
использовать специальную библиотеку обнаружения служб,
которую нежелательно помещать в контейнер. Или может
понадобиться заменить одни службы другими и использовать
другие библиотеки и методы обнаружения служб.
Абстрагирование и изоляция логики доступа к другим службам
во внешнем мире как раз и являются целью паттерна
Ambassador (Посредник).
Решение
Продемонстрируем использование паттерна на примере
организации кэша для приложения. Для доступа к локальному
кэшу в окружении для разработки может быть достаточно
определить некоторые настройки в конфигурации, но в
промышленном окружении может понадобиться настроить
клиент, подключающийся к разным сегментам кэша. Другой
пример — поиск службы в реестре для ее обнаружения на
стороне клиента. Еще один пример — взаимодействие с
внешней службой по ненадежному протоколу, такому как
HTTP, из-за чего для защиты приложения приходится
использовать логику обработки обрывов связи, настраивать
тайм-ауты, выполнять повторные попытки и многое другое.
Во всех этих случаях можно использовать контейнер-
посредник, скрывающий сложность доступа к внешним
службам и обеспечивающий упрощенное их представление для
основного контейнера приложения через локальное сетевое
соединение. На рис. 17.1 и 17.2 показано, как контейнер-
посредник помогает отделить прикладную логику от логики
доступа к хранилищу пар ключ/значение, принимая запросы
через локальный порт. На рис. 17.1 видно, как можно
делегировать доступ к данным, хранящимся в удаленном
распределенном хранилище, таком как Etcd.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
labels:
app: random-generator
spec:
containers:
- image: k8spatterns/random-
generator:1.0
name: main
env:
- name:
LOG_URL
value: http://localhost:9009
ports:
- containerPort: 8080
protocol: TCP
- image: k8spatterns/random-generator-log-
ambassador
name: ambassador
Главный контейнер приложения содержит REST-службу,
которая генерирует случайные числа.
Адрес URL для подключения к контейнеру-посреднику
через локальное соединение.
Контейнер-посредник, выполняющийся параллельно и
прослушивающий порт 9009 (который не экспортируется за
пределы пода).
Пояснение
Фактически паттерн Ambassador (Посредник) — это все тот же
паттерн Sidecar (Прицеп). Основное отличие между ними
заключается в том, что Посредник не добавляет новых
возможностей в основное приложение, а просто действует
подобно интеллектуальному прокси-серверу, открывающему
выход во внешний мир, из-за чего этот паттерн иногда
называют Proxy (Представитель).
Дополнительная информация
Задача
Всякое нетривиальное приложение требует определения
параметров для доступа к источникам данных и внешним
службам или для тонкой настройки поведения в
промышленном окружении. Еще до появления манифеста
«Двенадцать факторов» мы знали, что хранить конфигурацию
внутри приложения неправильно. Конфигурацию следует
вынести за рамки приложения, чтобы ее можно было изменить
даже после его сборки. Это еще больше увеличивает ценность
контейнерных приложений, которые позволяют использовать
неизменяемые артефакты и способствуют этому. Но как это
сделать в контейнерном мире?
Решение
Для хранения конфигураций приложений манифест
«Двенадцать факторов» рекомендует использовать переменные
окружения. Этот подход прост и может использоваться в
любых окружениях и на любых платформах. Любая
операционная система позволяет определять переменные
окружения и передавать их в приложениях, и каждый язык
программирования поддерживает простые средства доступа к
этим переменным. Можно смело утверждать, что переменные
окружения — это универсальный механизм. Обычно при
использовании переменных окружения во время сборки
определяются их значения по умолчанию, а во время
выполнения они изменяются. Давайте рассмотрим некоторые
конкретные примеры, как это реализовать в Docker и
Kubernetes.
В образах Docker переменные окружения можно определить
прямо в файлах Dockerfile, с помощью директивы ENV, по одной
переменной в строке, как показано в листинге 18.1.
Листинг 18.1. Файл Dockerfile с определениями переменных окружения
FROM openjdk:11
ENV PATTERN "EnvVar Configuration"
ENV LOG_FILE "/tmp/random.log"
ENV SEED "1349093094"
# Альтернативный вариант:
ENV PATTERN="EnvVar Configuration"
LOG_FILE=/tmp/random.log SEED=1349093094
...
о значениях по умолчанию
Значения по умолчанию упрощают жизнь, потому что
избавляют от бремени выбора значения для параметра
конфигурации, о существовании которого вы можете даже не
подозревать. Они также играют важную роль в парадигме
преобладания соглашений перед настройками. Однако не всегда
желательно определять значения по умолчанию. А иногда
такой подход может даже быть антипаттерном.
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: LOG_FILE
value: /tmp/random.log
- name: PATTERN
valueFrom:
configMapKeyRef:
name: random-generator-config
key: pattern
- name: SEED
valueFrom:
secretKeyRef:
name: random-generator-secret
key: seed
Фактическое значение для переменной окружения.
Значение извлекается из карты конфигураций ConfigMap.
Имя карты конфигураций ConfigMap.
Ключ внутри карты конфигураций ConfigMap для поиска
значения для переменной окружения.
Значение извлекается из Secret (поиск выполняется так
же, как при использовании карты конфигураций ConfigMap).
При таком подходе можно определять не только
фактические значения переменных окружения (как для
LOG_FILE), но и ссылаться на ресурсы Secret (предназначенные
для хранения конфиденциальных данных) и ConfigMap (если
настройки можно хранить в открытом виде). Преимущество
использования ресурсов Secret и ConfigMap заключается в том,
что они позволяют управлять переменными окружения
независимо от определений подов. Подробнее о Secret и
ConfigMap, а также об их плюсах и минусах рассказывается в
главе 19 «Конфигурация в ресурсах».
В предыдущем примере значение для переменной SEED
извлекается из ресурса Secret. В общем и целом это верное
решение, но важно отметить, что переменные окружения не
являются безопасными хранилищами. Конфиденциальная
информация, помещенная в переменные окружения,
становится доступной для чтения и может даже просочиться в
журналы.
Пояснение
Переменные окружения просты в использовании и хорошо
знакомы. Идея их применения хорошо отображается на
контейнеры и поддерживается всеми платформами времени
выполнения. Но переменные окружения небезопасны и
хороши только для небольшого числа параметров. Но когда
параметров слишком много, управление переменными
окружения превращается в утомительную задачу.
В таких случаях многие используют дополнительный
уровень косвенности и помещают свои конфигурации в
различные файлы, по одному для каждого окружения, а затем
используют единственную переменную для выбора одного из
этих файлов. Этот подход, например, используется в профилях
Spring Boot. Поскольку файлы профилей обычно хранятся в
самом приложении внутри контейнера, они оказываются тесно
связанными с приложением. Это часто приводит к тому, что
конфигурации для разработки и эксплуатации включаются в
один образ Docker с приложением, что требует перестройки
образа при любом изменении в любом окружении. Все это
лишь доказывает, что переменные окружения подходят только
для небольших наборов конфигураций.
Паттерны Configuration Resource (Конфигурация в ресурсах),
Immutable Configuration (Неизменяемая конфигурация) и
Configuration Template (Макет конфигурации), описанные в
следующих главах, являются более удачными альтернативами,
когда возникает потребность в более сложных конфигурациях.
Переменные окружения — это универсальный механизм, и
их можно определять на разных уровнях. Это может приводить
к фрагментированию настроек и затруднять выявление
источника значения для заданной переменной. Из-за
отсутствия централизованного места, где определяются все
переменные окружения, часто бывает трудно отладить
проблемы, связанные с ошибками в конфигурации.
Еще один недостаток переменных окружения состоит в том,
что определить их можно только перед запуском приложения и
нельзя изменить позже. С одной стороны, невозможность
оперативного изменения конфигурации во время выполнения
приложения можно считать недостатком. Однако многие видят
в этом преимущество, потому что это способствует
неизменности конфигурации. В данном случае неизменность
подразумевает необходимость остановки действующего
контейнера и запуска новой копии с измененной
конфигурацией, например, с помощью стратегии
развертывания, такой как непрерывное развертывание
обновлений. При таком подходе вы всегда будете иметь
приложение с четко определенным состоянием конфигурации.
Переменные окружения просты в использовании, но в
основном могут применяться только для хранения простых
конфигураций и имеют ограничения при наличии более
сложных требований. Паттерны, описываемые далее,
показывают, как преодолеть эти ограничения.
Дополнительная информация
Задача
Один из существенных недостатков паттерна EnvVar
Configuration (Конфигурация в переменных окружения) состоит
в том, что он подходит для случаев с небольшим количеством
параметров и простых конфигураций. Другой недостаток в
том, что переменные окружения могут определяться в разных
местах, из-за чего часто бывает сложно найти, где
определяется та или иная переменная. И даже отыскав ее,
нельзя быть полностью уверенным, что она не
переопределяется где-то еще. Например, переменные,
определяемые в образе Docker, можно переопределить во
время выполнения в ресурсе Deployment.
Часто бывает удобнее хранить все конфигурационные
данные в одном месте, а не во множестве файлов с
определениями ресурсов. Также нет смысла помещать
содержимое всего конфигурационного файла в переменную
окружения. В таких случаях некоторая дополнительная
косвенность может дать большую гибкость, что и предлагает
паттерн Configuration Resource (Конфигурация в ресурсах).
Решение
Фреймворк Kubernetes предлагает специализированные
ресурсы для хранения конфигураций, более гибкие, чем
простые переменные окружения — объекты ConfigMap и Secret
для данных общего назначения и конфиденциальных данных
соответственно.
Оба ресурса поддерживают одинаковые способы
использования, потому что обеспечивают хранение и
управление парами ключ/значение. Все приемы, которые далее
описываются применительно к ресурсам ConfigMap, в
большинстве случаев можно использовать при работе с
ресурсами Secret. Кроме кодирования в формат Base64, эти два
ресурса не имеют технических различий.
После создания карты конфигурации ConfigMap и
сохранения в ней данных есть два способа использования
ключей из ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: random-generator-config
data:
PATTERN: Configuration Resource
application.properties: |
# Конфигурация генератора случайных чисел
log.file=/tmp/generator.log
server.port=7070
EXTRA_OPTIONS: "high-secure,native"
SEED: "432576345"
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- env:
- name: PATTERN
valueFrom:
configMapKeyRef:
name: random-generator-config
key: PATTERN
....
Если в ConfigMap присутствует большое число записей,
которые должны быть доступны в виде переменных
окружения, использование специального синтаксиса может
сэкономить немало времени. Вместо объявления каждой
записи по отдельности, как показано в предыдущем примере в
разделе env:, можно определить раздел envFrom:, который
отобразит все записи из ConfigMap с ключами, которые можно
использовать как допустимые имена переменных окружения.
Также есть возможность добавить определенный префикс, как
показано в листинге 19.4.
Листинг 19.4. Установка переменных окружения из значений в ConfigMap
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
envFrom:
- configMapRef:
name: random-generator-config
prefix: CONFIG_
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
volumeMounts:
- name: config-volume
mountPath: /config
volumes:
- name: config-volume
configMap:
name: random-generator-config
Пояснение
Ресурсы ConfigMap и Secret позволяют хранить
конфигурационную информацию в специализированных
объектах ресурсов, которые легко управляются с помощью
Kubernetes API. Самое большое преимущество ConfigMap и
Secret в том, что они отделяют определение конфигурационных
данных от их использования. Такое разделение позволяет
управлять объектами, используя конфигурации независимо от
конфигураций.
Еще одно преимущество в том, что ConfigMap и Secret
являются неотъемлемой частью платформы. Они не требуют
никаких нестандартных конструкций, подобных тем, что
описаны в главе 20 «Неизменяемая конфигурация».
Тем не менее паттерн Configuration Resource (Конфигурация
в ресурсах) тоже имеет свои ограничения: ресурсы Secret не
могут иметь размер больше 1 Мбайт, то есть они не могут
хранить произвольные объемы данных и не годятся для
хранения прикладных данных, не имеющих отношения к
конфигурации. Ресурсы Secret могут хранить двоичные данные,
но из-за того что данные хранятся в формате Base64, чистый их
объем не может превышать 700 Кбайт.
Кроме того, многие действующие кластеры Kubernetes
устанавливают свои квоты на число ресурсов ConfigMap,
которые можно использовать в пространстве имен или в
проекте, поэтому ConfigMap тоже не является универсальным
решением.
В следующих двух главах мы познакомимся с приемами
хранения очень больших объемов конфигурационных данных,
основанных на паттернах Immutable Configuration
(Неизменяемая конфигурация) и Configuration Template (Макет
конфигурации).
Дополнительная информация
Задача
Как рассказывалось в главе 18 «Конфигурация в переменных
окружения», переменные окружения предлагают самый
простой способ настройки контейнерных приложений. Но
несмотря на простоту и универсальность, управлять
переменными окружения становится крайне сложно, как
только их число превысит определенный порог.
Эту сложность отчасти можно преодолеть с помощью
Configuration Resources (Конфигурация в ресурсах). Однако эти
паттерны не обеспечивают неизменности самих
конфигурационных данных. Под неизменностью здесь
подразумевается невозможность изменить конфигурацию
после запуска приложения, которая гарантирует, что
конфигурационные данные всегда находятся в четко
определенном состоянии. Кроме того, неизменяемую
конфигурацию можно хранить в системе управления версиями
и следовать за процессом контроля изменений.
Решение
Для решения упомянутых проблем все конфигурационные
данные для конкретного окружения можно поместить в один
пассивный образ и распространять его как обычный образ
контейнера. Во время выполнения приложение и образ с
данными связываются друг с другом, и приложение получает
возможность извлекать конфигурацию из этого образа. Такой
подход позволяет легко создавать различные образы с
конфигурационными данными для различных окружений. Эти
образы объединяют всю информацию о конфигурации для
конкретных окружений и могут храниться в системе
управления версиями, как и любые другие образы
контейнеров.
Создаются такие образы данных легко и просто, так как
являются обычными образами контейнеров, которые хранят
только данные. Сложность заключается лишь в том, чтобы
связать контейнеры во время запуска. Для этого можно
использовать различные подходы, в зависимости от
платформы.
Тома Docker
Прежде чем рассматривать решение для Kubernetes, отступим
на шаг назад и рассмотрим случай с обычными контейнерами
Docker. В Docker контейнер может экспортировать том с
данными. С помощью директивы VOLUME в Dockerfile можно
указать каталог, который позже можно сделать общим. Во
время запуска содержимое этого каталога в контейнере
копируется в общий каталог. Как показано на рис. 20.1, такое
связывание томов
FROM scratch
ADD app-dev.properties
/config/app.properties
VOLUME
/config
FROM busybox
ADD dev.properties /config-src/demo.properties
ENTRYPOINT [ "sh", "-c", "cp /config-src/* $1",
"--" ]
initContainers:
- image: k8spatterns/config-dev:1
name: init
args:
- "/config"
volumeMounts:
- mountPath: "/config"
name: config-directory
containers:
- image: k8spatterns/demo:1
name: demo
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath: "/config"
name: config-directory
volumes:
- name: config-directory
emptyDir: {}
Паттерны OpenShift
Паттерны — это обычные описания ресурсов, которые
допускают параметризацию. Как показано в листинге 20.7, в
качестве такого параметра можно использовать образ
конфигурации.
Листинг 20.7. Паттерн OpenShift, параметризуемый образом с конфигурацией
apiVersion: v1
kind: Template
metadata:
name: demo
parameters:
- name: CONFIG_IMAGE
description: Name of configuration image
value: k8spatterns/config-dev:1
objects:
- apiVersion: v1
kind: DeploymentConfig
// ....
spec:
template:
metadata:
// ....
spec:
initContainers:
- name: init
image: ${CONFIG_IMAGE}
args: [ "/config" ]
volumeMounts:
- mountPath: /config
name: config-directory
containers:
- image: k8spatterns/demo:1
// ...
volumeMounts:
- mountPath: /config
name: config-directory
volumes:
- name: config-directory
emptyDir: {}
oc new-app demo -p
CONFIG_IMAGE=k8spatterns/config-prod:1
Пояснение
Использование отдельных контейнеров с данными в паттерне
Immutable Configuration (Неизменяемая конфигурация) — не
самая простая задача. Однако этот паттерн предлагает
некоторые уникальные преимущества:
Дополнительная информация
Задача
В главе 19 «Конфигурация в ресурсах» вы узнали, как
использовать стандартные объекты ресурсов Kubernetes —
ConfigMap и Secret — для настройки приложений. Но иногда
конфигурационные файлы могут быть очень большими и
сложными. Поместить такие файлы непосредственно в
ConfigMaps может быть проблематично, поскольку они должны
быть правильно внедрены в определения ресурсов. Также
приходится быть очень осторожными и избегать специальных
символов, таких как кавычки и переносы строк, играющих
особую роль в синтаксисе определения ресурсов Kubernetes.
Размер конфигурации — еще один важный фактор, который
следует учитывать, потому что ресурсы ConfigMap или Secret
ограничивают суммарный объем всех значений величиной 1
Мбайт (это ограничение накладывается внутренним
хранилищем Etcd).
Большие конфигурационные файлы для разных окружений
обычно имеют незначительные отличия. Такое их сходство
приводит к большому количеству повторений и избыточности
в ресурсах ConfigMap, потому что в каждом окружении
используются в основном одни и те же данные. Паттерн
Configuration Template (Макет конфигурации), который мы
рассмотрим в этой главе, решает эти конкретные проблемы.
Решение
Чтобы уменьшить объем повторяющихся данных, имеет смысл
хранить в ConfigMap или даже непосредственно в переменных
окружения только различающиеся конфигурационные
значения, такие как параметры подключения к базе данных. Во
время запуска контейнера эти значения обрабатываются с
помощью паттерна Configuration Template (Макет
конфигурации), чтобы получить полный конфигурационный
файл (как, например, standalone.xml в JBoss WildFly).
Существует большое число инструментов, таких как Tiller
(Ruby) или Gomplate (Go), для обработки макетов на этапе
инициализации приложения. На рис. 21.1 показан пример
макета конфигурации, заполненный данными, поступающими
из переменных окружения или смонтированного тома,
возможно, основанного на ConfigMap.
....
<formatter name="COLOR-PATTERN">
<pattern-formatter pattern="{{(datasource
"config").logFormat}}"/>
</formatter>
....
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
example: cm-template
name: wildfly-cm-template
spec:
replicas: 1
template:
metadata:
labels:
example: cm-template
spec:
initContainers:
- image: k8spatterns/example-config-cm-
template-init
name: init
volumeMounts:
- mountPath:
"/params"
name: wildfly-parameters
- mountPath:
"/out"
name: wildfly-config
containers:
- image: jboss/wildfly:10.1.0.Final
name: server
command:
-
"/opt/jboss/wildfly/bin/standalone.sh"
- "-Djboss.server.config.dir=/config"
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath:
"/config"
name: wildfly-config
volumes:
- name: wildfly-parameters
configMap:
name: wildfly-parameters
- name: wildfly-config
emptyDir: {}
Пояснение
Паттерн Configuration Template (Макет конфигурации) построен
на основе паттерна Configuration Resource (Конфигурация в
ресурсах) и прекрасно подходит для ситуаций, когда
приложение требуется запускать в разных окружениях со
сложными и похожими конфигурациями. Однако настройка
приложений с использованием паттерна Configuration Template
(Макет конфигурации) более сложна и включает больше
компонентов, которые могут работать неправильно.
Используйте его, только если приложение требует огромного
объема конфигурационных данных. Часто такие
конфигурационные данные имеют весьма небольшие отличия
в разных окружениях. Даже если копирование всей
конфигурации для конкретного окружения непосредственно в
ConfigMap позволяет добиться желаемого, такой поход
усложнит обслуживание конфигураций в будущем, потому что
со временем конфигурации неизбежно будут расходиться все
дальше друг от друга. Для таких случаев паттерн Configuration
Template (Макет конфигурации) подходит идеально.
Дополнительная информация
• Gomplate (https://github.com/hairyhenderson/gomplate).
Задача
Вы уже знаете, что Kubernetes — это сложная и многогранная
платформа, предлагающая множество возможностей. Однако
это универсальная платформа управления контейнерами
охватывает далеко не все варианты использования
приложений. К счастью, она поддерживает точки расширения,
используя которые можно реализовать конкретные варианты,
опираясь на проверенные строительные блоки Kubernetes.
Главный вопрос, который мы рассмотрим здесь: как
расширить Kubernetes, не изменяя и не нарушая его работу, и
как использовать его возможности для поддержки
нестандартных сценариев.
В основе Kubernetes лежит декларативный API,
ориентированный на ресурсы. Что подразумевается под словом
декларативный? Декларативный подход, в противоположность
императивному, описывает не как должен действовать
Kubernetes, а каким должно быть целевое состояние.
Например, масштабируя развертывание Deployment вверх, мы
не создаем новые поды, требуя от Kubernetes «создать новый
под», а меняем свойство replicas ресурса Deployment через
Kubernetes API, присваивая ему желаемое число.
Но как создаются новые поды? Эту задачу решают
внутренние контроллеры. При каждом изменении состояния
ресурса (например, при изменении значения свойства
replicas в развертывании Deployment) Kubernetes создает
событие и передает его всем обработчикам. Затем эти
обработчики реагируют на событие, изменяя, удаляя или
создавая новые ресурсы, что, в свою очередь, приводит к
появлению других событий, таких как событие, требующее
создать новый под. Эти события затем передаются другим
контроллерам, которые выполняют свои конкретные действия.
Весь этот процесс известен как согласование состояний,
когда целевое состояние (требуемое количество реплик)
отличается от текущего (фактическое количество запущенных
экземпляров), и задача контроллера состоит в том, чтобы
согласовать и достичь желаемого целевого состояния. С этой
точки зрения Kubernetes представляется в роли диспетчера
распределенного состояния. Вы сообщаете ему параметры
желаемого состояния экземпляра компонента, а он пытается
достичь этого состояния и поддерживать его.
Можно ли как-то внедриться в этот процесс согласования
без изменения кода Kubernetes и создать контроллер для наших
конкретных потребностей?
Решение
В состав Kubernetes входит целая коллекция встроенных
контроллеров, управляющих стандартными ресурсами, такими
как ReplicaSet, DaemonSet, StatefulSet, Deployment или Service.
Эти контроллеры работают под управлением диспетчера
контроллеров, который развертывается (как отдельный
процесс или под) на главном узле. Контроллеры не знают о
существовании друг друга. Они выполняют бесконечный цикл
согласования, постоянно проверяя фактическое и желаемое
состояние своих ресурсов и предпринимая соответствующие
действия, чтобы приблизить фактическое состояние к
желаемому.
Однако архитектура Kubernetes, управляемая событиями,
позволяет подключать другие, нестандартные контроллеры.
Эти контроллеры могут добавлять новые возможности и
обрабатывать события подобно внутренним контроллерам. Все
контроллеры по своей природе являются реактивными, они
реагируют на события в системе и выполняют свои
конкретные действия. В общем и целом процесс согласования
состояния состоит из следующих основных этапов:
Наблюдение
Определение фактического состояния путем наблюдения за
событиями, которые распространяет Kubernetes при
изменении контролируемого ресурса.
Анализ
Выявление отличий от желаемого состояния.
Действие
Выполнение операций, необходимых для приведения
текущего состояния в желаемое.
Например, контроллер ReplicaSet наблюдает за
изменениями в ресурсе ReplicaSet, определяет, сколько подов
должно быть запущено, и выполняет действия, посылая
определения подов в API Server. После этого внутренние
механизмы Kubernetes производят запуск указанного пода на
узле.
На рис. 22.1 показано, как контроллер регистрирует себя,
подписываясь на события с целью обнаружения изменения
состояния контролируемых ресурсов, наблюдает за их текущим
состоянием и обращается к API Server (если это необходимо),
чтобы поддержать фактическое состояние как можно ближе к
желаемому.
Контроллеры
Реализуют простой процесс согласования, контролируя и
воздействуя на стандартные ресурсы Kubernetes. Чаще всего
эти контроллеры создаются с целью совершенствования
поведения платформы и добавления новых возможностей.
Операторы
Реализуют сложный процесс согласования, который
является основой паттерна Operator (Оператор),
обслуживают определение нестандартных ресурсов
CustomResourceDefinition (CRD). Как правило,
операторы инкапсулируют сложную предметную логику и
управляют полным жизненным циклом приложения. Мы
подробно рассмотрим паттерн Operator (Оператор) в главе
23.
Как отмечалось выше, такое деление помогает постепенно
внедрять новые идеи. В этой главе мы сосредоточимся на более
простых контроллерах, а в следующей познакомимся с CRD и
рассмотрим паттерн Operator (Оператор).
Чтобы исключить возможность одновременной обработки
одних и тех же ресурсов, контроллеры реализуются с
использованием паттерна Singleton Service (Служба-одиночка),
описанного в главе 10. Большинство контроллеров
развертываются с использованием ресурса Deployment, но с
единственной репликой, потому что Kubernetes использует
оптимистическую блокировку на уровне ресурсов для
предотвращения проблем, связанных с параллельной
обработкой при изменении объектов ресурсов. В конце концов,
контроллер — это не что иное, как приложение, которое
постоянно выполняется в фоновом режиме.
Поскольку сам фреймворк Kubernetes и клиентская
библиотека для доступа к Kubernetes написаны на языке Go,
многие контроллеры тоже написаны на Go. Однако при
желании контроллеры, посылающие запросы в Kubernetes API
Server, можно писать на любом языке. Далее, в листинге 22.1,
вы увидите контроллер, написанный на языке сценариев
командной оболочки.
Наиболее просто реализуются контроллеры, расширяющие
возможности Kubernetes управления ресурсами. Они
оперируют теми же стандартными ресурсами и выполняют те
же действия, что и внутренние контроллеры Kubernetes, но
невидимые пользователям кластера. Контроллеры
интерпретируют определения ресурсов и выполняют
некоторые действия в соответствии со сложившимися
условиями. Хотя они могут отслеживать и воздействовать на
любой параметр в определении ресурса, лучше всего для этой
цели подходят метаданные и карты конфигураций ConfigMap.
Ниже перечислены некоторые соображения, которые следует
учитывать при выборе места хранения данных контроллера:
Метки
Метки, как часть метаданных ресурса, доступны для анализа
любым контроллерам. Они индексируются во внутренней
базе данных, благодаря чему запросы с метками
выполняются очень эффективно. Метки следует
использовать всегда, когда требуется реализовать
функциональность селектора (например, для выявления
соответствующих подов в определениях Service или
Deployment). Недостаток меток в том, что в метках можно
использовать только буквенно-цифровые имена и значения
с некоторыми ограничениями. Описание синтаксиса меток и
набор допустимых символов можно найти в документации
Kubernetes.
Аннотации
Аннотации являются отличной альтернативой меткам. Их
следует использовать вместо меток, если значения не
вписываются в синтаксические ограничения меток.
Аннотации не индексируются, поэтому обычно они
используются для представления информации, которая не
используется в качестве ключей в запросах контроллеров.
Еще одно преимущество аннотаций перед метками, кроме
возможности представления произвольных метаданных, —
они не оказывают отрицательного влияния на внутреннюю
производительность Kubernetes.
fabric8/configmapcontroller
Этот контроллер (http://bit.ly/2uJ2FnI) следит за
изменениями в объектах ConfigMap и обновляет связанные с
ними развертывания Deployment. Его можно использовать с
приложениями, которые не способны наблюдать за
ConfigMap и динамически обновляться при изменении
конфигурации. Это особенно верно, когда ConfigMap
отображается в переменные окружения или когда
приложение не может быстро и надежно обновить свою
конфигурацию на лету, без перезапуска. Реализация такого
контроллера в виде сценария командной оболочки будет
показана в листинге 22.2.
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
annotations:
k8spatterns.io/podDeleteSelector:
"app=webapp"
data:
message: "Welcome to Kubernetes Patterns !"
namespace=${WATCH_NAMESPACE:-default}
base=http://localhost:8001
ns=namespaces/$namespace
curl -N -s $base/api/v1/${ns}/configmaps?
watch=true | \
while read -r event
do
# ...
done
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
На основе пространства имен сценарий контроллера
конструирует URL конечной точки Kubernetes API для
наблюдения за ConfigMap.
Как видите, наш контроллер связывается с Kubernetes API
Server через локальное соединение. Этот сценарий
необязательно должен развертываться непосредственно на
главном узле Kubernetes API, но тогда как он сможет работать,
используя локальное соединение? Как вы, наверное,
догадались, здесь на сцену выходит другой паттерн. Этот
сценарий разворачивается в поде вместе с контейнером-
посредником, который открывает порт 8001 на локальном
хосте и связывает его с настоящей службой Service в
Kubernetes. Более подробно о паттерне Ambassador (Посредник)
рассказывается в главе 17. Фактическое определение пода с
этим посредником будет показано далее в этой главе.
Конечно, простое наблюдение за событиями — не самое
надежное решение. Соединение может быть разорвано в любой
момент, поэтому нужно предусмотреть возможность
перезапустить цикл. Кроме того, события могут теряться,
поэтому, действуя в промышленном окружении, контроллеры
должны не только следить за событиями, но и время от
времени запрашивать у API Server все текущее состояние и
использовать его как новую основу. Но для демонстрации
паттерна такого решения вполне достаточно.
В листинге 22.4 показана логика, выполняющаяся в цикле.
Листинг 22.4. Цикл согласования в контроллере
curl -N -s $base/api/v1/${ns}/configmaps?
watch=true | \
while read -r event
do
type=$(echo "$event" | jq -r
'.type')
config_map=$(echo "$event" | jq -r
'.object.metadata.name')
annotations=$(echo "$event" | jq -r
'.object.metadata.annotations')
if [ "$annotations" != "null" ]; then
selector=$(echo $annotations |
\
jq -r "\
to_entries
|\
.
[]
|\
select(.key ==
\"k8spatterns.io/podDeleteSelector\") |\
.value
|\
@uri
\
")
fi
if [ $type = "MODIFIED" ] && [ -n "$selector"
]; then
pods=$(curl -s $base/api/v1/${ns}/pods?
labelSelector=$selector |\
jq -r .items[].metadata.name)
for pod in $pods;
do
curl -s -X DELETE
$base/api/v1/${ns}/pods/$pod
done
fi
done
selector=$(echo $annotations | \
jq -r "\
to_entries
|\
.
[]
|\
select(.key ==
\"k8spatterns.io/podDeleteSelector\") |\
.value
|\
@uri
\
")
• Переменная $annotations содержит все аннотации в
форме объекта JSON, в котором имена аннотаций играют роль
свойств.
{
"k8spatterns.io/pattern": "Controller",
"k8spatterns.io/podDeleteSelector":
"app=webapp"
}
в селектор app%3Dwebapp.
apiVersion: apps/v1
kind: Deployment
# ....
spec:
template:
# ...
spec:
serviceAccountName: config-watcher-
controller
containers:
- name: kubeapi-
proxy
image: k8spatterns/kubeapi-proxy
- name: config-
watcher
image: k8spatterns/curl-jq
# ...
command:
- "sh"
- "/watcher/config-watcher-
controller.sh"
volumeMounts:
- mountPath: "/watcher"
name: config-watcher-controller
volumes:
- name: config-watcher-
controller
configMap:
name: config-watcher-controller
apiVersion: v1
kind:
ConfigMap
metadata:
name: webapp-config
annotations:
k8spatterns.io/podDeleteSelector:
"app=webapp"
data:
message: "Welcome to Kubernetes Patterns
!"
---
apiVersion: apps/v1
kind:
Deployment
# ...
spec:
# ...
template:
spec:
containers:
- name: app
image: k8spatterns/mini-http-
server
ports:
- containerPort: 8080
env:
- name:
MESSAGE
valueFrom:
configMapKeyRef:
name: webapp-config
key: message
Пояснение
Подводя итоги, можно сказать, что контроллер — это активный
процесс согласования, наблюдающий за объектами, которые
представляют желаемое состояние окружения. Он анализирует
фактическое и желаемое состояния и посылает инструкции,
пытаясь привести текущее состояние окружения к желаемому.
Kubernetes реализует этот механизм во множестве своих
внутренних контроллеров, и вы тоже можете использовать тот
же механизм в своих собственных контроллерах. Мы увидели,
что необходимо для того, чтобы создать свой контроллер, как
он функционирует и расширяет возможности платформы
Kubernetes.
Мы можем добавлять свои контроллеры благодаря
модульной архитектуре Kubernetes, управляемой событиями.
Такая архитектура естественным образом способствует
созданию независимых и асинхронных контроллеров для ее
расширения. Также существенным преимуществом является
наличие четкой технической границы между самим
фреймворком Kubernetes и любыми расширениями. Однако с
асинхронной природой контроллеров связана одна проблема —
их часто трудно отлаживать, потому что поток событий не
всегда имеет простую структуру. Как следствие, нет простой
возможности установить контрольные точки в контроллере,
чтобы приостановить его и исследовать конкретную ситуацию.
В главе 23 вы познакомитесь с родственным паттерном
Operator (Оператор), который основан на паттерне Controller
(Контроллер) и предлагает еще более гибкий способ настройки
операций.
Дополнительная информация
Задача
В главе 22 «Контроллер» вы узнали, как расширить
возможности платформы Kubernetes простым способом без
внедрения в ее реализацию. Однако в некоторых случаях
обычных нестандартных контроллеров недостаточно, потому
что они могут контролировать только внутренние ресурсы
Kubernetes. Иногда бывает желательно добавить новые
понятия в платформу Kubernetes, требующие введения
дополнительных предметных объектов. Допустим, в качестве
решения для мониторинга мы выбрали Prometheus и хотим
добавить его в Kubernetes четко определенным образом. Было
бы просто замечательно, если бы у нас имелся ресурс
Prometheus, описывающий конфигурацию и все детали
развертывания средств мониторинга, подобный другим
ресурсам Kubernetes. Также было бы хорошо иметь ресурсы,
описывающие, какие службы следует охватить мониторингом
(например, с помощью селектора меток).
В таких ситуациях используются определения
нестандартных ресурсов (CustomResourceDefinition, CRD). Они
позволяют расширять Kubernetes API, добавляя нестандартные
ресурсы в кластер Kubernetes и используя их, как если бы они
были встроенными ресурсами. Нестандартные ресурсы вместе
с паттерном Controller (Контроллером) для управления ими
образуют паттерн Operator (Оператор).
Следующая цитата (http://bit.ly/2Fjlx1h) Джимми Зелински
(Jimmy Zelinskie) как нельзя лучше описывает особенности
паттерна Operator (Оператор):
Оператор — это контроллер Kubernetes, знающий две
предметные области: Kubernetes и какую-то еще. Объединяя
знания из обеих областей, он может автоматизировать
задачи, обычно требующие участия оператора-человека,
который понимает обе области.
Решение
В главе 22 «Контроллер» мы узнали о возможности эффективно
реагировать на изменение состояния ресурсов, встроенных в
Kubernetes. Теперь, понимая как работает первая половина
паттерна Operator (Оператор), рассмотрим другую половину —
представление нестандартных ресурсов в Kubernetes с
использованием CRD.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: prometheuses.monitoring.coreos.com
spec:
group: monitoring.coreos.com
names:
kind: Prometheus
plural: prometheuses
scope: Namespaced
version: v1
validation:
openAPIV3Schema: ....
Имя.
Группа API, к которой принадлежит ресурс.
Вид, используемый для идентификации экземпляров
этого ресурса.
Правило именования для создания формы
множественного числа, используется для определения списка
объектов.
Область видимости — может ли ресурс создаваться на
уровне кластера или только в некотором пространстве имен.
Версия CRD.
Схема OpenAPI V3 для проверки (здесь не показана).
Также можно указать схему OpenAPI V3, чтобы Kubernetes
мог проверить правильность определения нестандартного
ресурса. В простых случаях схему можно опустить, но в
промышленном окружении обязательно следует определить
схему, чтобы ошибки конфигурации можно было обнаружить
на самой ранней стадии.
Также Kubernetes позволяет указать в каждом CRD два
возможных вложенных ресурса в поле subresources:
scale
С помощью этого свойства можно указать, как определяется
количество реплик ресурса. В этом параметре можно указать
путь в формате JSON для поиска желаемого количества
реплик: путь к свойству, в котором хранится фактическое
количество запущенных реплик, и необязательный путь к
селектору меток, который можно использовать для поиска
копий экземпляров ресурса. Обычно этот селектор меток
можно не указывать, но он обязательно должен быть
определен, если вы хотите использовать свой
нестандартный ресурс с механизмом горизонтального
масштабирования подов HorizontalPodAutoscaler,
описанным в главе 24 «Эластичное масштабирование».
status
После установки этого свойства становится доступен новый
вызов API, позволяющий изменять только статус. Этот
вызов API можно защитить отдельно и позволить с его
помощью обновлять статус из-за пределов контроллера. С
другой стороны, при обновлении ресурса в целом раздел
status игнорируется точно так же, как для стандартных
ресурсов Kubernetes.
Листинг 23.2 показывает, что пути к вложенным ресурсам
определяются точно так же, как в обычных подах.
Листинг 23.2. Определение вложенных ресурсов в CustomResourceDefinition
kind: CustomResourceDefinition
# ...
spec:
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
labelSelectorPath: .status.labelSelector
Путь JSON к желаемому числу реплик.
Путь JSON к числу активных реплик.
Путь JSON к селектору меток для запроса количества
активных реплик.
Определив CRD, легко можно создать такой ресурс, как в
листинге 23.3.
Листинг 23.3. Нестандартный ресурс Prometheus
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus
spec:
serviceMonitorSelector:
matchLabels:
team: frontend
resources:
requests:
memory: 400Mi
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1alpha1.sample-api.k8spatterns.io
spec:
group: sample-api.k8spattterns.io
service:
name: custom-api-server
version: v1alpha1
Operator Framework
Operator Framework предлагает обширную поддержку
разработки операторов на Golang и состоит из следующих
компонентов:
Kubebuilder
Kubebuilder — это проект группы SIG API Machinery с обширной
документацией21. Подобно Operator SDK, он поддерживает
создание проектов на Golang и управление несколькими CRD в
одном проекте.
В отличие от Operator Framework, Kubebuilder работает
напрямую с Kubernetes API, тогда как Operator SDK добавляет
дополнительный слой абстракции поверх стандартного API,
что упрощает его использование (но ценой утраты некоторых
возможностей).
Поддержка установки и управления жизненным циклом
оператора не так сложна, как в OLM из Operator Framework. Тем
не менее оба проекта во многом совпадают и в конечном итоге
могут прийти к общему знаменателю.
Metacontroller
Metacontroller сильно отличается от двух других инструментов
разработки операторов. Он расширяет Kubernetes API своими
функциями, реализующими общие возможности создания
пользовательских контроллеров. Он действует подобно
диспетчеру Kubernetes Controller Manager, запуская несколько
контроллеров, которые определяются динамически с помощью
особых ресурсов CRD, поддерживаемых Metacontroller. Иначе
говоря, это контроллер, который вызывает службу с
фактической логикой контроллера.
Описать Metacontroller можно также с точки зрения
декларативного поведения. Ресурсы CRD дают возможность
хранить новые типы в Kubernetes API, а Metacontroller
позволяет легко определять поведение стандартных или
пользовательских ресурсов декларативно.
Определяя контроллер с помощью Metacontroller, мы
должны указать функцию, которая реализует бизнес-логику
нашего контроллера. Metacontroller берет на себя все хлопоты
по взаимодействию с Kubernetes API, запускает цикл
согласования от нашего имени и вызывает нашу функцию
через заданную точку входа. Функции передается четко
определенный набор данных, описывающий событие CRD. Так
как функция должна возвращать значение, мы возвращаем
определение ресурсов Kubernetes, которые требуется создать
(или удалить) от имени нашей функции контроллера.
Такой способ, основанный на делегировании, позволяет
писать функции на любом языке, способном обрабатывать
HTTP и JSON, без всяких зависимостей от Kubernetes API или
клиентских библиотек. Эти функции можно развертывать в
Kubernetes, на стороне внешних поставщиков FaaS (Functions-
as-a-Service — функция как услуга) или где-то еще.
Здесь у нас нет возможности подробно рассмотреть все
варианты, но если вам требуется лишь дополнить Kubernetes
простыми средствами автоматизации или управления и не
нужны дополнительные функции, обратите внимание на
Metacontroller, особенно если вы хотите реализовать свою
бизнес-логику на языке, отличном от Go. В интернете можно
найти примеры контроллеров, которые демонстрируют, как
реализовать службу с состоянием, сине-зеленое
развертывание, индексируемое задание и службу-одиночку
только с использованием Metacontroller.
Пример
Рассмотрим конкретный пример реализации паттерна Operator
(Оператор). Здесь мы расширим пример из главы 22
«Контроллер» и определим CRD типа ConfigWatcher. Экземпляр
этого CRD определяет ссылку на ресурс ConfigMap для
наблюдения и то, какие поды должны перезапускаться при его
изменении. Такой подход помогает устранить зависимость от
ConfigMap в подах, так как нам не нужно изменять сами
ресурсы ConfigMap и добавлять в них аннотации, отвечающие
за запуск логики. Кроме того, в упрощенном примере
контроллера, основанного на аннотациях, мы можем
подключить ConfigMap только к одному приложению. С
использованием CRD возможны произвольные комбинации из
ресурсов ConfigMap и подов.
В листинге 23.5 показано, как выглядит ресурс
ConfigWatcher.
Листинг 23.5. Простой ресурс ConfigWatcher
kind: ConfigWatcher
apiVersion: k8spatterns.io/v1
metadata:
name: webapp-config-watcher
spec:
configMap: webapp-config
podSelector:
app: webapp
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: configwatchers.k8spatterns.io
spec:
scope: Namespaced
group: k8spatterns.io
version: v1
names:
kind: ConfigWatcher
singular: configwatcher
plural: configwatchers
validation:
openAPIV3Schema:
properties:
spec:
properties:
configMap:
type: string
description: "Name of the
ConfigMap"
podSelector:
type: object
description: "Label selector for
Pods"
additionalProperties:
type: string
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: config-watcher-crd
rules:
- apiGroups:
- k8spatterns.io
resources:
- configwatchers
- configwatchers/finalizers
verbs: [ get, list, create, update, delete,
deletecollection,
watch ]
Пояснение
Мы увидели, как можно расширить возможности платформы
Kubernetes, но имейте в виду, что операторы не являются
универсальным решением на все случаи жизни. Прежде чем
приступать к созданию своего оператора, внимательно изучите
свой случай, чтобы определить, соответствует ли он парадигме
Kubernetes.
Во многих случаях вполне достаточно обычного
контроллера, работающего со стандартными ресурсами.
Преимущество этого подхода заключается в том, что он не
требует полномочий администратора кластера для регистрации
CRD, но он имеет также свои ограничения, когда речь заходит о
безопасности или проверке.
Операторы хорошо подходят для моделирования
предметной логики, сочетающейся с декларативным способом
обработки ресурсов в Kubernetes и использованием реактивных
контроллеров.
В частности, операторы и CRD хорошо подходят в любой из
следующих ситуаций, когда:
Дополнительная информация
• OpenAPI V3 (http://bit.ly/2Tluk82).
• Kubebuilder (http://bit.ly/2I8w9mz).
• Metacontroller (https://metacontroller.app/).
• Prometheus (http://bit.ly/2HICRjT).
• Etcd (http://bit.ly/2JTz8SK).
• Memhog (https://github.com/secat/memhog-operator).
20 Это ограничение может быть снято в будущем, так как уже существуют планы
регистрации определений CRD в рамках пространств имен.
Задача
Kubernetes автоматизирует управление распределенными
приложениями, состоящими из большого количества
неизменяемых контейнеров, поддерживая желаемое их
состояние, выраженное декларативно. Однако, учитывая
непостоянный характер многих рабочих нагрузок, которые
часто меняются со временем, непросто понять, как должно
выглядеть желаемое состояние. Точное определение, сколько
ресурсов потребуется контейнеру и сколько реплик службы
должно быть запущено в данный момент для выполнения
соглашений об уровне обслуживания, требует времени и
усилий. К счастью, Kubernetes позволяет легко изменять объем
ресурсов контейнера, число реплик службы или узлов в
кластере. Такие изменения могут происходить вручную или
автоматически, в соответствии с определенными правилами.
Kubernetes может не только следовать фиксированным
настройкам подов и кластера, но также следить за уровнем
нагрузки и событиями, связанными с изменением объемов
ресурсов, анализировать текущее состояние и
масштабироваться для достижения желаемой
производительности. Эта способность наблюдать позволяет
Kubernetes адаптироваться и обретать эластичность, опираясь
на фактические показатели использования, а не на ожидаемые
факторы. Давайте рассмотрим разные способы, которыми
можно достичь такого поведения, и то, как объединить разные
методы масштабирования.
Решение
Есть два основных подхода к масштабированию любых
приложений: горизонтальное и вертикальное. Под
горизонтальным масштабированием в мире Kubernetes
подразумевается создание большего количества реплик пода.
Под вертикальным масштабированием подразумевается
увеличение объема ресурсов для контейнеров, управляемых
подами. На бумаге все выглядит просто, но определить
конфигурацию приложения для автоматического
масштабирования на общей облачной платформе так, чтобы не
повлиять на другие службы и на сам кластер, часто можно
только методом проб и ошибок. Как всегда, Kubernetes
предлагает множество средств и методов, помогающих найти
лучшие настройки для приложений, и мы кратко рассмотрим
их здесь.
Императивное масштабирование
Контроллер, такой как ReplicaSet, отвечает за постоянное
выполнение заданного количества экземпляров пода.
Благодаря этому, чтобы масштабировать под, достаточно
изменить количество желаемых реплик. Масштабировать наше
развертывание Deployment с именем random-generator до
четырех экземпляров можно одной командой, как показано в
листинге 24.1.
Листинг 24.1. Изменение числа реплик развертывания Deployment из командной строки
Декларативное масштабирование
Масштабирование с использованием команды scale
выполняется тривиально просто и удобно для быстрого
реагирования в чрезвычайных ситуациях, но этот подход не
сохраняет настройки вне кластера. Обычно все приложения для
Kubernetes хранят определения своих ресурсов в системе
управления версиями, включая число реплик. Воссоздание
ReplicaSet из исходного определения приведет к изменению
числа реплик до прежнего уровня. Чтобы избежать такого
отклонения конфигурации и обеспечить обратное
распространение изменений, рекомендуется декларативно
изменять желаемое количество реплик в ReplicaSet или
некотором другом определении и применять изменения к
Kubernetes, как показано в листинге 24.2.
Листинг 24.2. Использование развертывания Deployment для декларативной настройки
числа реплик
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: random-generator
spec:
minReplicas: 1
maxReplicas: 5
scaleTargetRef:
apiVersion: extensions/v1beta1
kind: Deployment
name: random-generator
metrics:
- resource:
name: cpu
target:
averageUtilization: 50
type: Utilization
type: Resource
Рис. 24.1. Механизм автоматического горизонтального масштабирования подов
Автоматическое масштабирование — это область
Kubernetes со множеством мелких деталей, которые
продолжают быстро развиваться, и каждая из них может
оказать существенное влияние на общее поведение механизма
автоматического масштабирования. Поэтому здесь мы не
будем подробно обсуждать все детали, но в разделе
«Дополнительная информация» вы найдете ссылки на самую
последнюю актуальную информацию по этому вопросу.
В целом поддерживаются следующие типы метрик:
Стандартные метрики
Эти метрики объявлены с параметром
.spec.metrics.resource[:].type, содержащим
значение Resource, и представляют метрики
использования ресурсов, таких как процессор и память. Они
доступны для любого контейнера в любом кластере под
одними и теми же именами. Их значения можно определять
в процентах, как это сделано в предыдущем примере, или в
абсолютных значениях. В обоих случаях значения
описывают гарантированный объем ресурса, то есть объем в
поле requests контейнера, а не limits. Это самые простые
в использовании типы метрик и обычно поддерживаются
сервером метрик или компонентами Heapster, которые
можно запускать как дополнения кластеров.
Нестандартные метрики
Эти метрики с параметром
.spec.metrics.resource[:].type, содержащим
значение Object или Pod, требуют расширенных настроек
мониторинга кластера, которые могут отличаться для
разных кластеров. Нестандартная метрика с типом Pod, как
можно догадаться, описывает метрику, характерную для
пода, тогда как метрика с типом Object может описывать
любой другой объект. Нестандартные метрики
обслуживаются агрегирующим API Server с точкой входа
custom.metrics.k8s.io и поддерживаются различными
адаптерами метрик, такими как Prometheus, Datadog,
Microsoft Azure и Google Stackdriver.
Внешние метрики
К этой категории относятся метрики, которые описывают
ресурсы, не являющиеся частью кластера Kubernetes.
Например, у вас может быть под, принимающий сообщения
от облачной службы очередей. Очень часто в таком
сценарии бывает желательно масштабировать количество
подов-потребителей в зависимости от глубины очереди.
Такие метрики поддерживаются внешними плагинами
метрик и имеют много общего с нестандартными
метриками.
Правильно настроить автоматическое масштабирование
очень непросто. Для этого придется немного
поэкспериментировать с настройками. Ниже перечислены
основные аспекты, которые следует учитывать при настройке
HPA:
Выбор метрик
Одним из самых, пожалуй, важных решений, касающихся
автоматического масштабирования, является выбор
метрик. Чтобы добиться максимальной отдачи от HPA,
между значением метрики и количеством реплик пода
должна существовать прямая корреляция. Например, если
выбранная метрика определяет количество запросов в
секунду (например, HTTP-запросов в секунду), увеличение
количества подов приведет к уменьшению среднего числа
запросов, потому что запросы будут передаваться большему
количеству подов. То же верно для метрики, определяющей
использование процессора, потому что существует прямая
корреляция между частотой запросов и использованием
процессора (увеличение числа запросов ведет к увеличению
использования процессора). Другие метрики, такие как
потребление памяти, не имеют такой прямой корреляции.
Проблема метрики потребления памяти заключается в том,
что если служба потребляет определенный объем памяти,
запуск большего числа экземпляров пода, скорее всего, не
приведет к снижению потребления памяти, если
приложение ничего не знает о других экземплярах и не
имеет механизмов для перераспределения и освобождения
своей памяти. Если память не освобождается и это
отражается в метриках, HPA будет создавать все больше и
больше подов, чтобы уменьшить объем потребляемой
памяти, пока не достигнет верхнего порога на число реплик,
что, вероятно, не является желаемым поведением. Поэтому
выбирайте метрики, прямо (предпочтительно линейно)
зависящие от количества подов.
Отложенная реакция
Масштабирование, инициируемое изменением значения
метрики, — это многостадийный процесс, в который
вовлечено несколько компонентов Kubernetes. Первый —
это агент cAdvisor, действующий в каждом контейнере, и
регулярно собирающий метрики для Kubelet. Второй —
сервер метрик, извлекающий метрики из Kubelet через
регулярные интервалы. Наконец, периодически запускается
контроллер HPA, который анализирует собранные метрики.
Формула масштабирования HPA вводит некоторую
задержку, чтобы предотвратить частое применение
противоречивых решений (как описано в предыдущем
пункте). Вся эта деятельность накапливается в задержке
между причиной и реакцией масштабирования. Увеличение
задержки настройкой этих параметров делает HPA менее
отзывчивым, а уменьшение увеличивает нагрузку на
платформу. Настройка Kubernetes для балансировки
ресурсов и производительности — это постоянный процесс
обучения.
Knative Serving
Проект Knative Serving (с которым мы познакомимся в разделе
«Knative Build» главы 25) предлагает еще более продвинутые
методы горизонтального масштабирования. К их числу
относится поддержка «масштабирования до нуля», когда число
подов, поддерживающих службу Service, может быть
уменьшено до нуля и увеличивается, только когда происходит
определенное событие, например входящий запрос. В Knative
эта возможность реализована поверх «сетки служб» Istio,
которая, в свою очередь, предлагает прозрачные службы
внутренней маршрутизации для подов. Knative Serving
образует основу для бессерверной архитектуры, обладающей
еще более гибкими и быстрыми средствами горизонтального
масштабирования, превосходящими стандартные механизмы
Kubernetes.
apiVersion: poc.autoscaling.k8s.io/v1alpha1
kind: VerticalPodAutoscaler
metadata:
name: random-generator-vpa
spec:
selector:
matchLabels:
app: random-generator
updatePolicy:
updateMode: "Off"
Селектор меток
Определяет поды, подлежащие масштабированию.
Политика обновления
Определяет, как механизм VPA будет применять изменения.
Режим Initial позволяет выделять запрошенные ресурсы
только во время создания пода, но не позже. Режим по
умолчанию Auto позволяет выделять запрошенные ресурсы
во время создания пода, а также вытеснять и снова
планировать поды при изменении требований. Значение
Off отключает автоматическое применение изменившихся
требований подов, но позволяет определять предлагаемые
значения. Это своего рода пробный прогон для определения
нужного размера контейнера, но без непосредственного его
применения.
Определение VPA также может иметь политику выделения
ресурсов, которая влияет на то, как VPA вычисляет
рекомендуемый объем ресурсов (например, путем установки
для каждого контейнера нижней и верхней границ).
В зависимости от значения параметра
.spec.updatePolicy.updateMode, VPA вовлекает в работу
разные системные компоненты. Все три компонента VPA —
механизмы рекомендаций, согласования требований и
обновления — действуют независимо и могут заменяться
альтернативными реализациями. Интеллектуальный механизм
рекомендаций создавался с учетом опыта разработки системы
Google Borg. Текущая реализация анализирует фактическое
использование ресурсов контейнером под нагрузкой в течение
определенного периода (по умолчанию восемь дней), создает
гистограмму и выбирает значение, соответствующее
наибольшему процентилю за этот период. Кроме метрик, он
также учитывает события, связанные с ресурсами, в частности
с памятью, такие как вытеснение и OutOfMemory.
В нашем примере мы выбрали значение Off для параметра
.spec.updatePolicy.updateMode, но есть еще два
значения, каждое из которых определяет свой уровень
потенциальной дезорганизации работы масштабируемых
подов. Давайте посмотрим, как работают разные значения
updateMode, перечислив их в порядке увеличения
разрушительного влияния:
updateMode: Off
Механизм рекомендаций VPA собирает метрики и события
подов, а затем выдает рекомендации. Рекомендации всегда
хранятся в разделе status ресурса VPA. Однако дальше
этого механизм в режиме Off не идет. Он анализирует
имеющуюся информацию и выдает рекомендации, но не
применяет их. Этот режим можно использовать для
получения информации о потреблении ресурсов подами без
внесения каких-либо изменений и нарушения их работы.
Принятие решения о применении рекомендаций оставлено
на усмотрение пользователя.
updateMode: Initial
В этом режиме VPA делает еще шаг вперед. Кроме действий,
выполняемых механизмом рекомендацией, дополнительно
в работу вовлекается плагин согласования (admission
plugin), который применяет рекомендации только ко вновь
созданным подам. Например, если масштабирование пода
осуществляется вручную, через механизм HPA, при
изменении параметров развертывания Deployment или в
случае остановки и повторного запуска пода по какой-либо
причине, контроллер согласования VPA обновит значение
запроса на ресурс.
Этот контроллер является изменяющим плагином
согласования и переопределяет значения в поле requests
новых подов, соответствующих селектору меток VPA.
Данный режим не вызывает перезапуск действующих подов
и является частично разрушительным, потому что изменяет
требования к ресурсам для вновь создаваемых подов. Это
может повлиять на выбор места для запуска нового пода.
Более того, может так получиться, что после применения
рекомендуемых требований к ресурсам под будет
запланирован на другом узле, что может иметь
неожиданные последствия. Или, что еще хуже, планировщик
не сможет подобрать для пода подходящий узел, если ни на
одном узле в кластере не окажется достаточного объема
ресурсов.
updateMode: Auto
Кроме создания рекомендаций и их применения к вновь
создаваемым подам, как описано выше, в этом режиме VPA
вовлекает в работу свой компонент, выполняющий
обновления. Этот компонент останавливает запущенные
поды, которые соответствуют селектору меток, и запускает
их вновь с помощью плагина согласования VPA, который
обновляет требования к ресурсам. То есть этот режим
является наиболее разрушительным, поскольку
принудительно перезапускает все поды для применения
рекомендаций, что может вызвать неожиданные проблемы с
планированием, как описано выше.
Фреймворк Kubernetes предназначен для управления
неизменяемыми контейнерами с неизменяемыми
определениями spec в подах, как показано на рис. 24.2. Это
упрощает горизонтальное масштабирование, но создает
проблемы для вертикального масштабирования из-за
необходимости останавливать и повторно запускать поды, что
может повлиять на процесс планирования и вызвать сбои в
работе. Это верно, даже когда под сокращает требуемый объем
ресурсов и хочет освободить уже выделенные ресурсы.
Другая проблема связана с сосуществованием VPA и HPA. В
настоящее время эти два механизма действуют независимо
друг от друга, что может привести к нежелательному
поведению. Например, если HPA использует метрики ресурсов,
такие как процессорное время и объем памяти, и VPA влияет
на эти же значения, это может привести к одновременному
горизонтальному и вертикальному масштабированию подов
(то есть произойдет двойное масштабирование).
Мы не будем еще дальше углубляться в детали, потому что
механизм VPA все еще находится в состоянии бета-версии и
его поведение может измениться. Но имейте в виду, что его
применение может значительно улучшить потребление
ресурсов.
Cluster API
Все крупные облачные провайдеры поддерживают Kubernetes
CA. Однако плагины, реализующие эту поддержку, были
написаны самими провайдерами, что вызывает привязку к
одному провайдеру и несовместимость в поддержке CA. К
счастью, появился проект Cluster API Kubernetes, целью
которого является определение API для создания, настройки и
управления кластером. Все крупные провайдеры облачных
вычислений, такие как AWS, Azure, GCE, vSphere и OpenStack,
поддержали эту инициативу. Он также предусматривает
автоматическое масштабирование локально установленных
экземпляров Kubernetes. Сердцем Cluster API является
контроллер машины, действующий в фоновом режиме, для
которого уже существует несколько независимых реализаций,
таких как Kubermatic machine-controller или Open-Shift
machine-api-operator. Проект Cluster API стоит того, чтобы
внимательно следить за его развитием, так как в будущем он
может стать основой для любого другого механизма
автоматического масштабирования кластера.
Рис. 24.3. Механизм автоматического масштабирования кластера
Уровни масштабирования
В этой главе мы рассмотрели различные способы
масштабирования развернутых рабочих нагрузок для
удовлетворения меняющихся потребностей в ресурсах.
Большинство из перечисленных здесь действий вполне может
выполнить оператор-человек, но это не соответствует
облачному мышлению. Для крупномасштабного управления
распределенными системами необходима автоматизация
повторяющихся действий. Лучше автоматизировать
масштабирование и позволить операторам-людям
сосредоточиться на задачах, которые пока нельзя
автоматизировать с помощью операторов Kubernetes.
Давайте еще раз окинем взглядом приемы
масштабирования, в порядке от более тонких к более грубым,
как показано на рис. 24.4.
Рис. 24.4. Уровни масштабирования приложений
Настройка приложений
На самом детальном уровне находится метод, заключающийся
в настройке приложений, который мы не рассматривали в этой
главе, поскольку он не связан с Kubernetes. Тем не менее самое
первое, что можно предпринять, — настроить приложение,
выполняющееся в контейнере, чтобы оно оптимально
использовало выделенные ресурсы. Это действие не требуется
выполнять при каждом масштабировании службы, оно должно
быть выполнено изначально, перед выпуском приложения в
эксплуатацию. Например, для среды выполнения Java можно
настроить размер пула потоков выполнения, обеспечивающий
оптимальное использование ресурсов процессорного времени,
которые получает контейнер. Также можно настроить объемы
различных областей памяти, например размеры кучи, обычной
памяти и памяти для стека потоков выполнения.
Корректировка этих значений обычно выполняется с помощью
изменений конфигурации, а не кода.
В контейнерных приложениях используются запускающие
сценарии, которые могут вычислять оптимальные значения по
умолчанию для количества потоков и размеров областей
памяти приложения, опираясь на ресурсы, выделенные
контейнеру, а не на общую емкость узла. Использование таких
сценариев может стать отличным первым шагом. Можно пойти
еще дальше и использовать методы и библиотеки, такие как
Netflix Adaptive Concurrency Limits, с помощью которых
приложение может динамически вычислять свои ограничения
путем самопрофилирования и адаптироваться к ним. Это —
своеобразное автоматическое масштабирование на уровне
приложения, которое устраняет необходимость настройки
служб вручную.
Настройка приложений может вызвать регрессию, подобно
изменениям в коде, и должна сопровождаться тестированием.
Например, изменение размера кучи приложения может
привести к его краху из-за ошибки OutOfMemory, и
горизонтальное масштабирование в этом случае не поможет. С
другой стороны, масштабирование подов по вертикали или по
горизонтали или выделение большего количества узлов не даст
ожидаемого эффекта, если приложение не использует должным
образом ресурсы, выделенные для контейнера. То есть
настройка на этом уровне масштабирования может повлиять
на все другие методы и оказаться разрушительной, но ее
необходимо выполнить хотя бы один раз, чтобы добиться
оптимального поведения приложения.
Пояснение
Эластичность и различные методы масштабирования — это
область Kubernetes, которая продолжает активно развиваться.
Поддержка метрик лишь недавно была добавлена в HPA, а VPA
все еще находится на стадии экспериментальной разработки.
Кроме того, благодаря популяризации модели бессерверных
вычислений наибольшим спросом стали пользоваться
возможности масштабирования до нуля и быстрого
масштабирования вверх. Knative serving — это расширение для
Kubernetes, которое как раз решает эту проблему, обеспечивая
основу для масштабирования до нуля, как кратко описывается
во врезке «Knative Serving» выше и во врезке «Knative Build» в
главе 25. Проект Knative и лежащие в его основе сетки служб
продолжают быстро развиваться и представляют новые и очень
интересные облачные примитивы. Мы внимательно следим за
этой областью облачных вычислений и рекомендуем вам
присмотреться к Knative.
Опираясь на описание желаемого состояния
распределенной системы, Kubernetes может создавать и
поддерживать его. Он также обеспечивает надежность и
устойчивость к сбоям, постоянно выполняя мониторинг и
автоматически восстанавливая и обеспечивая соответствие
текущего состояния желаемому. Гибкость и надежность
системы находятся на достаточно высоком уровне для многих
нынешних приложений, но Kubernetes не останавливается на
достигнутом. Небольшая, но правильно настроенная система
Kubernetes сможет надежно функционировать даже при
большой нагрузке, масштабируя поды и узлы. То есть под
влиянием внешних факторов такая система будет становиться
больше и сильнее, пользуясь мощными возможностями
Kubernetes.
Дополнительная информация
• Knative (https://cloud.google.com/knative/).
Задача
Все паттерны, представленные выше в этой книге, касались
работы приложений в Kubernetes. Мы узнали, как
разрабатывать и подготавливать наши приложения для работы
в облачном окружении. Но как собираются сами приложения?
Классический подход заключается в создании образов
контейнеров вне кластера, добавлении их в реестр и
использовании ссылок на них в описаниях развертываний
Deployment. Однако сборка образов контейнеров внутри
кластера имеет несколько преимуществ.
Если экономическая политика компании позволяет иметь
только один кластер для всего, тогда сборка и запуск
приложений в одном месте могут значительно снизить затраты
на обслуживание. Это также упрощает планирование емкости и
снижает затраты на потребление ресурсов.
Как правило, для сборки образов используются системы
непрерывной интеграции (Continuous Integration, CI), такие как
Jenkins. Сборка с помощью CI-системы — это задача
планирования, эффективного поиска свободных
вычислительных ресурсов для заданий на сборку. В основе
Непривилегированная сборка
Когда сборка выполняется в Kubernetes, кластер полностью
контролирует ее процесс, но ему также нужны более высокие
стандарты безопасности, поскольку сборка выполняется не
изолированно. Для сборки в кластере важно, чтобы она
запускалась без привилегий root. К счастью, в настоящее время
существует множество способов организовать сборку в так
называемом непривилегированном режиме, когда не требуется
повышенных привилегий.
Решение
Один из первых и наиболее зрелых способов сборки образов в
кластере Kubernetes основан на использовании подсистемы
сборки в OpenShift. Она поддерживает несколько вариантов
сборки образов. Один из них — сборка образа из исходных кодов
(Source-to-Image, S2I) — консервативный способ организации
сборки с использованием так называемых построителей
образов. Мы подробнее рассмотрим S2I и способ сборки
образов в OpenShift в разделе «Сборка в OpenShift» ниже.
Другой механизм сборки образов внутри кластера основан
на использовании Knative Build. Этот инструмент работает
поверх Kubernetes и сетки служб Istio и является одной из
основных частей Knative (https://cloud.google.com/knative/),
платформы для создания, развертывания и управления
бессерверными приложениями. На момент написания этой
книги Knative все еще считался очень молодым проектом и
продолжал быстро развиваться. В разделе «Knative Build» я
кратко расскажу о проекте Knative и приведу примеры сборки
образов в кластере Kubernetes с помощью Knative Build.
Начнем знакомство с механизмами сборки с OpenShift.
Сборка в OpenShift
Red Hat OpenShift — это корпоративный дистрибутив
Kubernetes. Кроме поддержки всего того, что поддерживает
Kubernetes, он добавляет несколько дополнительных функций,
востребованных на предприятиях, таких как интегрированный
реестр образов контейнеров, поддержка единой службы
авторизации и новый пользовательский интерфейс, а также
добавляет в Kubernetes возможность создания собственных
образов. Для сообщества открытого программного
обеспечения выпускается дистрибутив OKD (https://www.okd.io/)
— ранее известный как OpenShift Origin, — поддерживающий
все возможности OpenShift.
Дистрибутив OpenShift стал первым предложившим
интегрированную в кластер возможность сборки образов под
управлением Kubernetes. Он поддерживает несколько
стратегий создания образов:
Конвейерная сборка
Представляет процесс сборки в виде серии заданий,
выполняющихся под управлением сервера Jenkins, позволяя
пользователю настроить конвейер Jenkins на свой лад.
Нестандартная сборка
Дает полный контроль над процессом сборки образов. В
этом случае вы должны сами создать образ в контейнере для
сборки и отправить его в реестр.
Данные для сборки могут извлекаться из разных
источников.
Git
Репозиторий, откуда извлекаются исходные данные,
определяется в виде URL.
Dockerfile
Файл Dockerfile, который доступен непосредственно, как
часть ресурса конфигурации сборки.
Образ
Образ другого контейнера, из которого извлекаются файлы
для текущей сборки. Поддержка этого типа источников дает
возможность выполнять конвейерную сборку, как будет
показано в листинге 25.2.
Ресурс Secret
Этот ресурс можно использовать для передачи
конфиденциальной информации в процесс сборки.
Двоичные данные
Источник этого типа предназначен для передачи данных
извне, которые должны быть подготовлены перед началом
сборки.
Выбор того или иного типа источника зависит от стратегии
сборки. Двоичные данные и Git являются
взаимоисключающими типами источников. Источники всех
остальных типов можно объединять или использовать по
отдельности. Ниже, в листинге 25.1, я покажу, как это делается.
Вся информация о сборке определяется в центральном
объекте ресурса BuildConfig. Этот ресурс можно создать
напрямую, применив его к кластеру, или с помощью клиента
командной строки oc, который является эквивалентом
kubectl в OpenShift. Клиент oc поддерживает команду build,
с помощью которой можно определить настройки сборки и
запустить ее.
Прежде чем взглянуть на BuildConfig, мы должны
разобраться с двумя дополнительными понятиями,
характерными для OpenShift.
ImageStream — это ресурс OpenShift, который ссылается на
один или несколько образов контейнеров. Он имеет некоторое
сходство с репозиторием Docker, который также содержит
несколько образов с разными тегами. OpenShift отображает
фактический образ с тегом в ресурс ImageStreamTag так, что
ImageStream (репозиторий) получает список ссылок на ресурсы
ImageStreamTag (образы с тегами). Зачем нужна эта
дополнительная абстракция? Она позволяет OpenShift
генерировать события для ImageStreamTag при обновлении
образа в реестре. Образы создаются во время сборки или когда
образ помещается во внутренний реестр OpenShift. При таком
подходе механизм сборки или развертывания получает
возможность прослушивать эти события и запускать новую
сборку или развертывание.
assemble
Сценарий, который вызывается в момент запуска сборки.
Его задача — получить исходный код из источников,
определяемых настройками, скомпилировать, если
необходимо, и скопировать получившиеся артефакты в
соответствующие местоположения.
run
Используется как точка входа для этого образа. OpenShift
вызывает этот сценарий при развертывании образа.
Сценарий run использует сгенерированные артефакты для
запуска приложения.
При желании вы также можете создать сценарий для вывода
сообщения о порядке использования, сохраняющий
сгенерированные артефакты для так называемых
инкрементальных сборок, которые будут доступны сценарию
assemble в последующих запусках сборки, или добавляющий
некоторые проверки.
Рассмотрим поближе механизм сборки S2I, изображенный
на рис. 25.1. Он состоит из двух компонентов: построителя
образов и средства ввода исходного кода. Оба объединяются
системой сборки S2I в момент, когда происходит запуск сборки
— либо триггером, после получения события, либо вручную.
Когда сборка образа завершится, например, путем компиляции
исходного кода, контейнер копируется в образ и передается в
сконфигурированный ImageStreamTag. Этот образ содержит
скомпилированные и подготовленные артефакты и сценарий
run в качестве точки входа.
Рис. 25.1. Система сборки S2I с источником входных данных Git
apiVersion: v1
kind: BuildConfig
metadata:
name: random-generator-build
spec:
source:
git:
uri:
https://github.com/k8spatterns/random-generator
strategy:
sourceStrategy:
from:
kind: DockerImage
name: fabric8/s2i-java
output:
to:
kind: ImageStreamTag
name: random-generator-build:latest
triggers:
- type: ImageChange
Конвейерная сборка
Механика конвейерной сборки показана на рис. 25.2. Процесс
конвейерной сборки включает этап начальной сборки S2I, на
котором создается выполняемый артефакт, например
двоичный выполняемый файл. Затем, на втором этапе, обычно
выполняемом средствами Docker, этот артефакт извлекается из
сгенерированного образа.
Рис. 25.2. Конвейерная сборка с использованием S2I для компиляции и Docker для сборки
образа приложения
apiVersion: v1
kind: BuildConfig
metadata:
name: runtime
spec:
source:
images:
- from:
kind: ImageStreamTag
name: random-generator-build:latest
paths:
- sourcePath: /deployments/.
destinationDir: "."
dockerfile: |-
FROM openjdk:8-alpine
COPY *.jar /
CMD java -jar /*.jar
strategy:
type: Docker
output:
to:
kind: ImageStreamTag
name: random-generator:latest
triggers:
- imageChange:
automatic: true
from:
kind: ImageStreamTag
name: random-generator-build:latest
type: ImageCh
Knative Build
В 2018 году компания Google запустила проект Knative с целью
привнести в Kubernetes дополнительные возможности,
имеющие прямое отношение к приложениям.
Основой для Knative стала сетка служб Istio (https://istio.io/),
которая предлагает готовые инфраструктурные службы для
управления трафиком, наблюдения и поддержания
безопасности. Сетки служб используют паттерн Sidecars
(Прицеп) для передачи приложениям новых инфраструктурных
возможностей.
Кроме сетки служб, проект Knative предлагает
дополнительные службы, в первую очередь предназначенные
для разработчиков приложений:
Knative Serving
Для поддержки масштабирования до нуля приложений,
которые могут использоваться, например, на платформах
FaaS (Function-as-a-Service — функция как служба). Наряду с
паттерном, описанным в главе 24 «Эластичное
масштабирование», и поддержкой сетки служб, Knative
Serving позволяет также масштабировать от нуля до
произвольного числа реплик.
Knative Eventing
Для доставки по каналам событий из источников в
приемники. События могут запускать службы Service,
используемые в роли приемников и масштабируемые от
нуля.
Knative Build
Для компиляции исходного кода приложений в образах
контейнеров внутри кластера Kubernetes. Продолжением
этой службы стал проект Tekton Pipelines, который в
конечном итоге заменит Knative Build.
Оба проекта, Istio и Knative, реализованы по паттерну
Operator (Operator) и используют определения нестандартных
ресурсов (CustomResourceDefinition, CRD) для объявления
своих предметных ресурсов.
В оставшейся части этого раздела мы сфокусируем свое
внимание на Knative Build реализации паттерна Image Builder
(Построитель образов) в Knative.
Простая сборка
Ресурс Build является основой Knative Build. Он определяет
конкретные шаги, которые должен выполнить оператор
Knative Build. Ниже перечислены основные ингредиенты,
использованные в листинге 25.3:
apiVersion: build.knative.dev/v1alpha1
kind: Build
metadata:
name: random-generator-build-jib
spec:
source:
git:
url:
https://github.com/k8spatterns/random-
generator.git
revision: master
steps:
- name: build-and-push
image: gcr.io/cloud-builders/mvn
args:
- compile
- com.google.cloud.tools:jib-maven-
plugin:build
- -
Djib.to.image=registry/k8spatterns/random-
generator
workingDir: /workspace
Имя объекта-построителя.
Ссылка на источник исходного кода с URL репозитория
GitHub.
Этапы сборки.
Образ с Java и Maven, который используется на этом этапе
сборки.
Аргументы для передачи в контейнер построителя,
который запускает Maven для компиляции, создает и
пересылает образ контейнера с помощью jib-maven-plugin.
Каталог /workspace — общий для всех этапов сборки.
Также интересно заглянуть внутрь, чтобы узнать, как
оператор Knative Build выполняет сборку.
apiVersion: build.knative.dev/v1alpha1
kind: Build
metadata:
name: random-generator-build-chained
spec:
source:
git:
url:
https://github.com/k8spatterns/random-
generator.git
revision: master
template:
name: maven-kaniko
arguments:
- name: IMAGE
value: registry:80/k8spatterns/random-
generator
Пояснение
Итак, мы познакомились с двумя способами создания образов
контейнеров в кластере. Система сборки OpenShift
демонстрирует одно из главных преимуществ, которые дает
сборка и запуск приложения в одном кластере. С помощью
триггеров ImageStream, реализованных в OpenShift, можно не
только связывать вместе несколько сборок, но и повторно
развертывать приложение, если в процессе сборки обновится
образ контейнера вашего приложения. Это особенно удобно,
когда за этапом сборки обычно следует этап развертывания.
Интеграция сборки и развертывания — это шаг вперед к
святому Граалю технологии непрерывного развертывания.
Сборка в OpenShift с помощью S2I — проверенная и
доказавшая свою надежность технология, но в настоящее
время S2I можно использовать только в дистрибутиве
OpenShift платформы Kubernetes.
Knative Build — еще одна реализация паттерна Image Builder
(Построитель образов). Основная цель Knative Build —
преобразовать исходный код в готовый образ контейнера и
передать его в реестр, чтобы потом его можно было загрузить с
помощью развертывания Deployment. Эти шаги выполняются
построителями образов, специализированными для поддержки
разных технологий. Knative Build не зависит от конкретных
этапов сборки, но управляет жизненным циклом сборки и ее
планированием.
Knative Build — это еще довольно молодой проект (по
состоянию на 2019 год), который предлагает строительные
блоки для сборки образов в кластере. Он не представляет
особого интереса для конечного пользователя и в основном
ориентирован на разработчиков инструментов. Можно
предположить, что новые и существующие инструменты будут
поддерживать Knative Build или другие проекты, основанные на
нем, поэтому вы наверняка увидите еще немало реализаций
паттерна Image Builder (Построитель образов).
Дополнительная информация
• Jib (https://github.com/GoogleContainerTools/jib).
• Img (https://github.com/genuinetools/img).
• Buildah (https://github.com/projectatomic/buildah).
• Kaniko (https://github.com/GoogleContainerTools/kaniko).
• Knative (https://cloud.google.com/knative/).
Универсальная платформа
В настоящее время Kubernetes считается самой популярной
платформой управления контейнерами. Она разрабатывается и
поддерживается в тесном сотрудничестве всеми основными
компаниями — разработчиками программного обеспечения и
предлагается в качестве услуги всеми основными
поставщиками облачных услуг. Она поддерживает системы
Linux и Windows и все основные языки программирования.
Kubernetes может управлять приложениями без состояния и с
состоянием, пакетными заданиями, периодическими
заданиями и бессерверными рабочими нагрузками. Это новый
уровень организации переносимых приложений и общий
знаменатель для облачных вычислений в целом. Если вы
разработчик ПО, которое используется в облачной среде, тогда
Kubernetes почти неизбежно станет частью вашей
повседневной жизни.
Многообразие обязанностей
В последние годы приложения предъявляют облачным
платформам все больше и больше нефункциональных
требований. Например, Kubernetes принимает на себя такие
обязанности, как подготовка, развертывание, обнаружение
служб, управление конфигурациями, управление заданиями,
изоляция ресурсов и проверка работоспособности. С ростом
популярности архитектуры микросервисов реализация даже
простой службы требует хорошего понимания технологии
распределенных вычислений и основ управления
контейнерами. Как следствие, разработчик должен свободно
владеть современным языком программирования для
реализации бизнес-функций и не менее свободно владеть
облачными технологиями для удовлетворения
нефункциональных требований.
О чем мы говорили
В этой книге мы рассмотрели 24 самых популярных паттерна
использования Kubernetes, сгруппированных следующим
образом:
В заключение
Все хорошее когда-нибудь заканчивается, подошла к концу и
эта книга. Надеемся, что она вам понравилась и изменила
ваше представление о Kubernetes. Мы искренне верим, что
платформа Kubernetes и заложенные в ней идеи станут такими
же фундаментальными, как идеи объектно-ориентированного
программирования. Эта книга — наша попытка повторить труд
«Банды четырех», но уже применительно к управлению
контейнерами. Мы надеемся, что это не конец, а начало
вашего путешествия в Kubernetes.
Удачной работы с kubectl!
Об авторах
Билджин Ибрам (Bilgin Ibryam, @bibryam) — главный
архитектор в Red Hat, член Apache Software Foundation и
участник нескольких проектов с открытым исходным кодом.
Блогер и популяризатор программного обеспечения с
открытым исходным кодом, энтузиаст блокчейна, лектор и
автор книги «Camel Design Patterns». Имеет более чем
десятилетний опыт создания и проектирования
высокомасштабируемых, отказоустойчивых распределенных
систем.
Билджин занимается наставничеством,
программированием и оказанием помощи компаниям в
создании успешных решений с открытым исходным кодом. В
настоящее время основной его работой является интеграция
приложений, организация корпоративных блокчейнов,
проектирование распределенных систем, а также разработка
микросервисов и облачных приложений в целом.
Доктор Роланд Хасс (Roland Hu , @ro14nd) — главный
разработчик в Red Hat, который осуществляет техническое
руководство проектом Fuse Online и недавно вошел в группу
разработки бессерверных вычислений для работы над Knative.
Уже больше 20 лет занимается разработкой на Java и недавно
обрел вторую любовь в лице языка Golang. Однако он никогда
не забывал свое прошлое системного администратора. Роланд
принимает активное участие в проектах с открытым исходным
кодом, является ведущим разработчиком моста JMX-HTTP
Jolokia и некоторых популярных инструментов сборки Java,
которые применяются для создания образов контейнеров и их
развертывания в Kubernetes и OpenShift. Помимо
программирования, любит делиться опытом работы на
конференциях и в рассылках.
Об обложке
На обложке «Паттерны Kubernetes» изображен красноносый
нырок (Netta rufina). Название вида rufina на латыни означает
«рыжий». Другое распространенное название — красноголовый
нырок. «Нырок» означает «ныряющая утка». Красноносый
нырок обитает в заболоченных местностях Европы и
Центральной Азии. Его ареал обитания охватывает также
болота Северной Африки и Южной Азии.
Длина тела взрослого красноносого нырка достигает 53–57
см, а вес — до 800–1250 г. Размах крыльев достигает почти
одного метра. Самки имеют однородный бурый окрас перьев
со светлой лицевой областью и менее красочны, чем самцы.
Самцы красного нырка отличаются бурым окрасом головы,
красным клювом, черной шеей, грудью и центральной частью
брюха и белыми боками.
Рацион красноносого нырка состоит в основном из корней,
семян и водных растений. Свои гнезда они устраивают в
растительных зарослях рядом с болотами и озерами и
откладывают яйца весной и летом. Обычно в выводке
насчитывается 8–12 утят. Голос красноносые нырки подают в
основном в период спаривания. Зов самца больше напоминает
хрип, а зов самки звучит как отрывистое «вра, вра, вра».
Многие животные, изображенные на обложках книг
издательства O’Reilly, находятся под угрозой вымирания; все
они очень важны для биосферы. Чтобы узнать, чем вы можете
помочь, посетите сайт animals.oreilly.com.
Иллюстрацию для обложки нарисовала Карен Монтгомери
(Karen Montgomery) на основе черно-белой гравюры из «British
Birds».