Контейнерные приложения, действующие в облачном окружении, не управляют своим жизненным циклом и поэтому должны слушать события, генерируемые управляющей платформой, и, соответственно, корректировать свои жизненные циклы. Паттерн Managed Lifecycle (Управляемый жизненный цикл) определяет, как приложения могут и должны реагировать на события жизненного цикла.
В главе 4 «Проверка работоспособности» мы узнали, почему контейнеры должны предоставлять программный интерфейс для различных проверок работоспособности. Программный интерфейс проверки работоспособности — это комплекс конечных точек, доступных только для чтения, которые платформа постоянно опрашивает, чтобы получить представление о приложении. Это — механизм, используемый платформой для извлечения информации из приложения.
Кроме мониторинга состояния контейнера, платформа иногда может выдавать команды, ожидая определенной реакции от приложения. Опираясь на внутренние правила и внешние факторы, облачная платформа может в любой момент принять решение о запуске или остановке управляемых ею приложений. Но только само контейнерное приложение может определить, на какие события и как оно будет реагировать. Фактически приложение предлагает программный интерфейс, который платформа использует для связи и отправки команд приложению. Кроме того, приложения могут получать дополнительные выгоды от управления жизненным циклом извне или игнорировать управляющие воздействия, если эта услуга им не нужна.
Выше мы видели, что простая проверка статуса процесса — недостаточно хороший показатель работоспособности приложения. Вот почему существуют разные API для мониторинга работоспособности контейнера. Аналогично, использования одной только модели процесса для запуска и остановки приложения недостаточно. Часто приложения требуют более тонких воздействий и механизмов управления жизненным циклом. Некоторым приложениям нужна помощь для разогрева, а некоторым требуется выполнить точную и четкую процедуру завершения. Для этих и других случаев платформа генерирует некоторые события, как показано на рис. 5.1, которые контейнер может принимать и обрабатывать, если это необходимо.
Рис. 5.1. Управляемый жизненный цикл контейнера
Единицей развертывания приложения является под. Как вы уже знаете, под состоит из одного или нескольких контейнеров. На уровне пода имеются другие конструкции, такие как init-контейнеры (рассматриваются в главе 14 «Init-контейнер») и defer-контейнеры (контейнеры с отложенным запуском, находившиеся на стадии предложения на момент написания этих строк), которые могут помочь в управлении жизненным циклом контейнера. Все события и точки входа, которые описываются в этой главе, применяются на уровне отдельного контейнера, а не на уровне пода.
Всякий раз, когда фреймворк Kubernetes решает остановить контейнер, например, останавливая под, которому тот принадлежит, или перед повторным запуском с целью устранения неисправности, выявленной при проверке работоспособности, контейнер получает сигнал SIGTERM. SIGTERM — это вежливое предложение контейнеру завершиться самому, прежде чем Kubernetes отправит более резкий «окрик» — сигнал SIGKILL. Получив сигнал SIGTERM, приложение должно завершиться как можно быстрее. Одни приложения могут быстро остановиться в ответ на этот сигнал, другим приложениям может потребоваться завершить уже запущенные запросы, закрыть соединения и очистить временные файлы, что может занять немного больше времени. В любом случае реакция на SIGTERM обозначает подходящий момент для аккуратного завершения контейнера.
Если процесс контейнера не остановился после сигнала SIGTERM, он принудительно завершается следующим сигналом SIGKILL. Kubernetes не посылает сигнал SIGKILL немедленно, а ожидает 30 секунд по умолчанию после отправки сигнала SIGTERM. Этот период можно настроить отдельно для каждого пода, определив параметр .spec.terminationGracePeriodSeconds, но соблюдение этой настройки не гарантируется, потому что ее можно переопределить в командах Kubernetes. Поэтому разработчики должны стремиться проектировать и реализовать контейнерные приложения так, чтобы они быстро запускались и завершались.
Однако одних только сигналов для управления жизненным циклом процесса недостаточно. Вот почему в Kubernetes существуют дополнительные обработчики событий жизненного цикла, такие как postStart и preStop. В листинге 5.1 показано определение пода с точкой входа postStart.
Листинг 5.1. Контейнер с обработчиком postStart
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 «Проверка работоспособности», и поддерживают следующие типы обработчиков:
Выполняет команду непосредственно в контейнере.
Выполняет HTTP-запрос GET, посылая его в порт, открытый одним из контейнеров в поде.
Будьте осторожны, закладывая критически важную логику в обработчик postStart, потому что нет никаких гарантий относительно порядка его выполнения. Поскольку обработчик выполняется параллельно с процессом контейнера, есть вероятность, что он выполнится до запуска контейнера. Кроме того, обработчик должен соответствовать семантике выполнения «не менее одного раза», то есть он должен позаботиться о попытках повторного запуска. Еще один аспект, о котором следует помнить, — платформа не выполняет повторных попыток, если HTTP-запрос не достиг обработчика.
Обработчик preStop — это блокирующий запрос, посылаемый контейнеру перед его завершением. Он имеет ту же семантику, что и сигнал SIGTERM, и должен использоваться для запуска процедуры остановки контейнера, когда реализовать перехват сигнала SIGTERM невозможно. Обработчик preStop, как показано в листинге 5.2, должен завершиться до того, как в среду выполнения контейнера будет послан запрос на удаление контейнера, который инициирует отправку сигнала SIGTERM.
Листинг 5.2. Контейнер с обработчиком preStop
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
Посылает запрос в конечную точку /shutdown приложения.
Но даже при том, что preStop имеет блокирующий характер, он не может предотвратить завершение контейнера, вернув признак ошибки, или удерживать его от остановки до бесконечности. Обработчик preStop — это лишь более удобная альтернатива сигналу SIGTERM, позволяющая организовать правильное завершение приложения, и ничего более. Он поддерживает те же типы обработчиков и гарантии, что и описанный выше обработчик postStart.
В этой главе мы до сих пор рассматривали обработчики, позволяющие выполнять команды в ответ на события жизненного цикла контейнера. Но существует и другой механизм, находящийся не на уровне контейнера, а на уровне пода, который позволяет выполнять инструкции инициализации.
Мы подробно поговорим об этом механизме в главе 14 «Init-контейнер», а здесь лишь кратко опишем его, чтобы сравнить с обработчиками жизненного цикла. В отличие от обычных контейнеров приложений, init-контейнеры запускаются последовательно, выполняются до завершения и запускаются перед любыми контейнерами приложений в поде. Эти гарантии позволяют использовать init-контейнеры для задач инициализации на уровне пода. Обработчики событий жизненного цикла и init-контейнеры работают на разных уровнях детализации (на уровне контейнера и на уровне пода соответственно) и могут использоваться взаимозаменяемо или дополнять друг друга. В табл. 5.1 перечислены основные отличия между ними.
Таблица 5.1. Обработчики событий жизненного цикла и init-контейнеры
Аспект | Обработчики событий жизненного цикла | Init-контейнеры |
Активируется в | Этапы жизненного цикла контейнера | Этапы жизненного цикла пода |
На этапе запуска | Выполняется обработчик postStart | Выполняются контейнеры из списка initContainers |
На этапе остановки | Выполняется обработчик presto | Пока нет эквивалентного механизма |
Гарантии относительно порядка выполнения | Обработчик postStart выполняется одновременно с ENTRYPOINT контейнера | Все init-контейнеры должны завершиться с признаком успеха до того, как будет запущен первый прикладной контейнер |
Случаи использования | Для выполнения некритичных операций запуска/остановки, характерных для контейнера | Выполнение последовательности операций с использованием контейнеров; повторное использование контейнеров для выполнения задач |
Нет никаких строгих правил, предписывающих, какой механизм использовать. Единственный критерий — необходимость конкретных гарантий относительно порядка выполнения. Можно полностью отказаться от использования обработчиков событий жизненного цикла и init-контейнеров и использовать сценарии bash для выполнения определенных действий на этапах запуска и завершения контейнеров. Это возможно, но такой подход тесно связывает контейнер со сценарием и усложняет его сопровождение.
Можно ограничиться использованием обработчиков событий жизненного цикла Kubernetes для выполнения некоторых действий, как описано в этой главе, а можно пойти дальше и запускать контейнеры, которые выполняют отдельные действия с помощью init-контейнеров. Для организации такой последовательности требуется больше усилий, зато она предлагает более надежные гарантии и допускает повторное использование.
Знание этапов и доступных обработчиков событий жизненного цикла контейнеров и подов имеет решающее значение для создания приложений, которые получают дополнительные выгоды от выполнения под управлением Kubernetes.
Одним из основных преимуществ облачной платформы является возможность надежного и предсказуемого выполнения и масштабирования приложений в потенциально ненадежной облачной инфраструктуре. Эти платформы предлагают набор ограничений и контрактов для приложений, работающих под их управлением. В интересах приложения следовать этим контрактам, чтобы воспользоваться всеми возможностями, предлагаемыми облачной платформой. Обработка этих событий гарантирует правильный запуск и завершение приложения с минимальным воздействием на потребляющие их службы. На данный момент это означает, что контейнеры должны действовать подобно хорошо спроектированному процессу POSIX. В будущем могут появиться другие события, подсказывающие приложению, когда оно будет масштабироваться, или предлагающие освободить ресурсы, чтобы предотвратить преждевременное завершение. Важно понимать, что жизненный цикл приложения больше не контролируется человеком, а полностью автоматизируется платформой.
Помимо управления жизненным циклом приложения, другой большой обязанностью платформ управления, таких как Kubernetes, является распределение контейнеров по массиву узлов. Паттерн Automated Placement (Автоматическое размещение) определяет приемы влияния на решения по планированию извне.
• Пример организации управления жизненным циклом (http://bit.ly/2udxws4).
• Обработчики событий жизненного цикла контейнера (http://bit.ly/2Fb38Uk).
• Подключение обработчиков к событиям жизненного цикла контейнера (http://bit.ly/2Jn9ANi).
• Аккуратное завершение (http://bit.ly/2TcPnJW).
• Аккуратное завершение подов в Kubernetes (http://bit.ly/2CvDQjs).
• Defer-контейнеры (http://bit.ly/2TegEM7).