Что нужно знать о Docker

Что нужно знать о Docker

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

Pod против виртуальных машин

К этому моменту вы должны чётко понимать: именно Pod, а не контейнер, является наименьшей единицей оркестрации в проекте Kubernetes. Эта конструкция отражена в объектах API, где контейнер стал лишь обычным полем в атрибутах Pod. Естественно, возникает вопрос: какие атрибуты относятся к объекту Pod, а какие — к Container?

Pod играет роль «виртуальной машины» в традиционных средах развертывания. Такая конструкция призвана сделать переход от традиционных сред (среды виртуальных машин) к Kubernetes (среды контейнеров) более плавным.

Если считать Pod «машиной» в традиционной среде, а контейнер — «пользовательской программой», работающей на этой «машине», то многие аспекты проектирования объекта Pod становятся гораздо понятнее.

Например, атрибуты, связанные с планированием, сетью, хранилищем и безопасностью, по сути, находятся на уровне Pod. Общая черта этих атрибутов — они описывают «машину» в целом, а не «программы», работающие внутри.

Например, настройка сетевой карты «машины» (т.е. определение сети Pod), настройка диска «машины» (т.е. определение хранилища Pod), настройка брандмауэра «машины» (т.е. определение безопасности Pod). Не говоря уже о том, на каком сервере работает эта «машина» (т.е. планирование Pod).

Kubernetes YAML

В Kubernetes мы определяем ресурсы Pod с помощью декларативных файлов.

nodeSelector: это поле позволяет пользователю привязать Pod к узлу, как показано ниже:

apiVersion: v1
kind: Pod
...
spec:
  nodeSelector:
    disktype: ssd

Такая конфигурация означает, что этот Pod может работать только на узле с меткой disktype: ssd; в противном случае планирование завершится ошибкой.

nodeName: как только это поле Pod заполнено, проект Kubernetes считает, что Pod уже запланирован, и результатом планирования является имя назначенного узла. Поэтому это поле обычно устанавливается планировщиком, но пользователи также могут задать его, чтобы «обмануть» планировщик, хотя такая практика обычно используется только при тестировании или отладке.

hostAliases: определяет содержимое файла hosts Pod (например, /etc/hosts), как показано ниже:

apiVersion: v1
kind: Pod
...
spec:
  hostAliases:
  - ip: "10.1.2.3"
    hostnames:
    - "foo.remote"
    - "bar.remote"
...

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

Помимо описанных выше конфигураций, связанных с «машиной», вы также можете заметить, что любые атрибуты, относящиеся к пространству имен Linux контейнера, также находятся на уровне Pod. Это тоже легко понять: конструкция Pod предназначена для того, чтобы контейнеры внутри него могли совместно использовать как можно больше пространств имен Linux, оставляя лишь необходимые возможности изоляции и ограничения. Таким образом, эффект, имитируемый Pod, очень похож на отношения между программами внутри виртуальной машины.

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]

Это YAML-файл Pod из официальной документации Kubernetes. На самом деле он очень прост: просто определяет контейнер с образом nginx. Однако в разделе containers этого YAML-файла вы увидите, что у контейнера установлены параметры postStart и preStop.

Что это означает?

Начнем с postStart. Это операция, которая выполняется сразу после запуска контейнера. Следует понимать, что операция, определенная в postStart, выполняется после ENTRYPOINT контейнера Docker, но строгой гарантии порядка нет. То есть, когда postStart запускается, ENTRYPOINT может ещё не завершиться.

Конечно, если выполнение postStart превысит время ожидания или вызовет ошибку, Kubernetes сообщит об ошибке (в Events соответствующего Pod) о том, что контейнер не запустился, из-за чего Pod также перейдет в состояние сбоя.

Аналогично, момент выполнения preStop наступает до того, как контейнер будет завершен (например, при получении сигнала SIGKILL). Важно уточнить: выполнение preStop является синхронным. Поэтому оно блокирует процесс завершения текущего контейнера до тех пор, пока определенная операция не будет завершена — это отличается от postStart.

Итак, в этом примере после успешного запуска контейнера он записывает «приветственное сообщение» в /usr/share/message (то есть операция, определенная в postStart). А перед удалением этого контейнера мы сначала вызываем команду завершения nginx (то есть операция, определенная в preStop), тем самым обеспечивая «корректное завершение» контейнера.

После того как мы ознакомились с основными полями Pod и его раздела Container, я расскажу о жизненном цикле такого объекта Pod в Kubernetes.

Изменения жизненного цикла Pod в основном отражаются в части Status объекта API Pod — это его третье важное поле после Metadata и Spec. Среди них pod.status.phase представляет текущее состояние Pod и может принимать следующие значения:

  1. Pending. Этот статус означает, что YAML-файл Pod был отправлен в Kubernetes, объект API создан и сохранен в Etcd. Однако некоторые контейнеры в этом Pod не могут быть созданы по какой-то причине. Например, сбой планирования.

  2. Running. В этом состоянии Pod успешно запланирован и привязан к конкретному узлу. Все контейнеры, которые он содержит, успешно созданы, и как минимум один из них в данный момент работает.

  3. Succeeded. Это состояние означает, что все контейнеры в Pod завершили выполнение и вышли. Эта ситуация чаще всего встречается при выполнении одноразовых задач.

  4. Failed. В этом состоянии хотя бы один контейнер в Pod завершился с ошибкой (ненулевой код возврата). Появление этого состояния означает, что вам нужно выяснить, как отлаживать приложение в контейнере, например, проверить Events и логи Pod.

  5. Unknown. Это аномальное состояние, указывающее на то, что kubelet не может непрерывно сообщать о статусе Pod в kube-apiserver, что, скорее всего, связано с проблемой связи между master и kubelet.

Кроме того, поле Status объекта Pod может быть дополнительно разбито на набор Conditions. Эти более детальные значения статуса включают: PodScheduled, Ready, Initialized и Unschedulable. Они в основном используются для описания конкретных причин текущего Status.

Например, если текущий статус Pod — Pending, а соответствующее ConditionUnschedulable, это означает, что возникла проблема с его планированием.

Среди них подстатус Ready заслуживает особого внимания: он означает, что Pod не только нормально запущен (в состоянии Running), но и готов предоставлять услуги извне. Между этими двумя состояниями (Running и Ready) есть разница, так что стоит подумать над этим внимательнее.

Эти статусные сообщения Pod являются важным критерием для оценки рабочего состояния приложения, особенно когда Pod переходит в состояние, отличное от Running. Вы должны уметь быстро реагировать, начинать отслеживание и локализацию на основе представленной аномальной ситуации, а не панически перелистывать документацию.

Заключение

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

Фактически, объект API Pod является самой важной концепцией во всей системе Kubernetes и также используется в контроллерах, которые я объясню позже.