От одиночных Pod'ов к самоисцелению: концепция контроллеров и ReplicaSet
От одиночных Pod'ов к самоисцелению: концепция контроллеров и ReplicaSet
Вы уже умеете запускать Pod'ы, читать их логи, погружаться в их Namespaces через nsenter и отлаживать процессы с помощью эфемерных контейнеров. Но давайте представим production-сценарий: 3 часа ночи, в дата-центре выходит из строя блок питания на Worker Node, где работает ваш идеально настроенный Pod. Что произойдет с приложением?
Оно умрет. И, что самое страшное, никто его не перезапустит.
В этой статье мы разберем, почему создание «голых» (bare) Pod'ов — это антипаттерн, как Kubernetes реализует принцип самоисцеления (self-healing) и почему для управления приложениями используются контроллеры, простейшим из которых является ReplicaSet.
Хрупкость «голых» Pod'ов
Как мы помним из архитектуры Control Plane, когда вы выполняете kubectl apply -f pod.yaml, компонент kube-scheduler находит подходящий узел и выполняет операцию Binding, жестко прописывая имя узла в поле nodeName манифеста Pod'а.
С этого момента Pod навсегда привязан к конкретному Worker Node. Жизненный цикл Pod'а неразрывно связан с жизненным циклом узла.
Если процесс внутри контейнера падает с ошибкой (Exit Code 1), локальный kubelet перезапустит его, опираясь на механизм Exponential Backoff. Но если падает сам узел, kubelet перестает отправлять Heartbeat в kube-apiserver.
Спустя таймаут Control Plane пометит узел как NotReady. Статус вашего Pod'а изменится на Terminating, а затем на Unknown. Kubernetes не будет пытаться перенести этот Pod на другой узел, потому что Pod — это примитив самого низкого уровня. У Control Plane нет инструкций создавать новый Pod взамен утраченного.
Создание одиночных Pod'ов напрямую через
kind: Podв production строго запрещено. Любой Pod должен управляться контроллером.
Паттерн Controller и делегирование ответственности
Чтобы система была отказоустойчивой, нам нужен кто-то, кто будет непрерывно следить за количеством живых экземпляров приложения и реагировать на инциденты. В Kubernetes эту роль выполняет компонент kube-controller-manager.
Внутри него работают десятки различных контроллеров. Их суть сводится к реализации паттерна Reconciliation Loop (цикла согласования), с которым вы уже знакомы по работе kubelet. Но если kubelet следит за состоянием контейнеров на одном узле, то контроллеры в Control Plane следят за глобальным состоянием кластера.
Базовая логика любого контроллера описывается простым псевдокодом:
- Прочитать желаемое состояние (Desired State) из
etcd. - Прочитать текущее фактическое состояние (Actual State) кластера.
- Если Actual Desired, выполнить действия для их уравнивания.
Для управления множеством одинаковых Pod'ов был создан контроллер ReplicaSet. Его единственная, математически строгая задача — гарантировать, что в любой момент времени в кластере работает ровно экземпляров (реплик) определенного Pod'а.
Если , а фактически работает 2 (один узел сгорел), ReplicaSet Controller немедленно отправляет запрос в kube-apiserver на создание одного нового Pod'а. Этот новый Pod будет подхвачен планировщиком и запущен на любом другом доступном узле. Так достигается самоисцеление.
Анатомия связи: Labels и Selectors
Возникает фундаментальный архитектурный вопрос: как именно ReplicaSet понимает, какие Pod'ы принадлежат ему?
В императивных системах родительский объект хранил бы массив идентификаторов (ID) созданных им потомков. Kubernetes использует декларативный подход. Связь между объектами устанавливается динамически через систему меток (Labels) и селекторов (Selectors).
- Labels (Метки) — это простые пары ключ-значение, которые прикрепляются к любому объекту (например, к Pod'у). Они несут бизнес-смысл:
app: frontend,env: production,tier: cache. - Selector (Селектор) — это запрос, который контроллер использует для поиска своих объектов.
ReplicaSet не знает имен своих Pod'ов. Он регулярно опрашивает API: "Дай мне все Pod'ы, у которых есть метка app=nginx". Если таких Pod'ов меньше, чем нужно — он берет шаблон и штампует новые. Если больше (например, случился сетевой сплит, и старый узел вернулся в строй) — он безжалостно убивает лишние.
Эта слабая связность (loose coupling) — гениальное решение. Она позволяет контроллерам управлять ресурсами, не блокируя друг друга, и дает инженерам огромную гибкость при отладке.
YAML-манифест ReplicaSet
Давайте посмотрим, как желаемое состояние описывается в коде. Манифест ReplicaSet состоит из трех ключевых блоков:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend-rs
spec:
# 1. Желаемое количество (Desired State)
replicas: 3
# 2. Правило поиска своих Pod'ов
selector:
matchLabels:
app: frontend
# 3. Шаблон для создания новых Pod'ов (Pod Blueprint)
template:
metadata:
labels:
app: frontend # Обязательно должно совпадать с selector!
spec:
containers:
- name: nginx
image: nginx:1.21
Обратите внимание на блок template. Это, по сути, манифест обычного Pod'а, вложенный внутрь ReplicaSet (без apiVersion и kind, так как они наследуются). Когда ReplicaSet видит нехватку реплик, он берет этот template, генерирует уникальное имя (например, frontend-rs-x7b9q) и отправляет его в kube-apiserver как новый Pod.
Защита от сиротства: ownerReferences
Если связь строится только на метках, как Kubernetes понимает, кого удалять при удалении самого ReplicaSet?
Когда ReplicaSet создает Pod на основе шаблона, он неявно внедряет в метаданные Pod'а скрытое поле ownerReferences. Это системная ссылка на родителя.
Вы можете увидеть её, выполнив kubectl get pod <pod-name> -o yaml:
metadata:
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: frontend-rs
uid: 4d5e6f7a-8b9c...
Благодаря ownerReferences работает механизм Cascading Deletion (каскадное удаление). Когда вы удаляете ReplicaSet (kubectl delete rs frontend-rs), Garbage Collector в Control Plane видит эту операцию, находит все дочерние Pod'ы по их ownerReferences и удаляет их вслед за родителем.
Ограничения ReplicaSet
ReplicaSet блестяще справляется со своей единственной задачей — масштабированием (Scaling) и поддержанием количества реплик.
Но что произойдет, если мы захотим обновить версию приложения с nginx:1.21 на nginx:1.22?
Мы можем отредактировать манифест ReplicaSet и изменить image в блоке template. Объект в etcd обновится. Но ничего не произойдет.
Почему? Потому что цикл согласования ReplicaSet проверяет только количество Pod'ов, совпадающих по Label Selector. Если и фактически работает 3 Pod'а с нужными метками, ReplicaSet считает свою работу выполненной. Ему абсолютно неважно, что внутри этих Pod'ов крутится старая версия образа. Новый template будет использован только в том случае, если старый Pod умрет и потребуется создать замену.
Для решения задачи бесшовного обновления (Rolling Update) и управления версиями шаблонов поверх ReplicaSet была создана еще одна абстракция, которая сегодня является стандартом де-факто для запуска stateless-приложений. Но об этом — в следующих главах.