Деплой и инфра
Объекты Kubernetes: цикл сверки за каждым Pod, Service и rollout
Команда выкатывает рутинное обновление версии. Rollout выглядит зелёным в kubectl get pods — новые поды Running. Но дашборд дежурного загорается: на каждом деплое error rate подскакивает до 4% на девяносто секунд, потом успокаивается. И так каждый rollout. Причина — одно отсутствующее поле: у Deployment нет readiness-пробы. Как только процесс контейнера в поде стартует, Kubernetes помечает его Ready, и Service начинает слать на него живой трафик — пока приложение ещё грузит конфиг, прогревает пул соединений и JIT-компилируется. Запросы приходят на процесс, который ещё не обслуживает, и получают 500.
Декларативно, а не императивно: цикл сверки
Единственная идея, которая делает Kubernetes цельным, такова: ты не говоришь кластеру, что делать, ты говоришь, что должно быть истиной. Ты задаёшь желаемое состояние — «хочу 5 реплик этого образа» — а контроллеры кластера крутят цикл вечно, сравнивая фактическое состояние с желаемым и предпринимая корректирующее действие, чтобы закрыть разрыв. Это цикл сверки (reconciliation loop), и именно поэтому Kubernetes самоисцеляется. Узел умер и три пода исчезли? Контроллер ReplicaSet наблюдает actual=2, desired=5 и создаёт три новых. Ты не писал «перезапускать при отказе» — ты задал инвариант, а контроллер его держит.
Это и есть самая глубокая причина, почему в проде никогда не делают kubectl run голого Pod. Голый Pod императивен: им никто не владеет, его никто не сверяет. Узел, на котором он живёт, перезагрузился — и Pod исчез навсегда, ни один контроллер его не пересоздаст. Тот же цикл, что исцеляет твой Deployment, игнорирует сиротский Pod, потому что ни один контроллер не держит его как цель желаемого состояния.
Почему это работает
Цикл обязан быть идемпотентным: запустить его дважды с тем же желаемым состоянием не должно ничего удваивать. Поэтому контроллеры сравнивают-потом-действуют, а не просто действуют. Контроллер, который «создаёт под, когда видит Deployment», создавал бы бесконечные поды на каждом тике; контроллер, который «обеспечивает, что число подов равно replicas», сходится и затем ничего не делает. Идемпотентность — это то, что позволяет циклу крутиться непрерывно без дрожания.
Иерархия рабочей нагрузки: Pod → ReplicaSet → Deployment
Три вложенных объекта, каждый добавляет одну способность:
- Pod — наименьшая разворачиваемая единица. Один или несколько контейнеров, которые делят сетевой namespace (один IP, общаются через
localhost) и могут делить тома. Поды эфемерны: они получают новый IP при каждом перезапуске и спроектированы, чтобы их выбрасывали и заменяли, а не чинили. - ReplicaSet — держит ровно N идентичных подов живыми. Вся его работа — это число: он следит за своими подами и сводит к
replicas: N. Напрямую его почти никогда не создают. - Deployment — управляет ReplicaSet-ами, чтобы дать rollouts и rollback. Когда ты меняешь образ, Deployment создаёт новый ReplicaSet и масштабирует его вверх, масштабируя старый вниз, под управлением
maxSurgeиmaxUnavailable(оба по умолчанию 25%). Старый ReplicaSet остаётся в масштабе 0 — именно так работаетkubectl rollout undo: он масштабирует предыдущий ReplicaSet обратно вверх.
| Объект | Владеет | Какую одну способность добавляет |
|---|---|---|
Pod | Контейнерами | Запуск контейнеров вместе (общая сеть + тома); эфемерен |
ReplicaSet | Подами | Держать ровно N копий живыми (сверка числа) |
Deployment | ReplicaSet-ами | Rollouts + rollback (постепенная замена ReplicaSet-ов) |
Service | (выбирает поды) | Стабильный IP + DNS поверх меняющегося набора IP подов |
Services и клей из label-селекторов
Поды эфемерны и их IP постоянно меняются, поэтому ничто никогда не должно обращаться к IP пода напрямую. Service даёт тебе стабильный виртуальный IP и DNS-имя (my-svc.my-namespace.svc.cluster.local), которое остаётся фиксированным, пока поды за ним сменяются. Магия, которая привязывает Service к его подам, — это метки и селекторы (labels & selectors): Service объявляет selector: { app: web }, и Kubernetes поддерживает объект Endpoints (или EndpointSlice), перечисляющий IP каждого пода, чьи метки совпадают. kube-proxy затем балансирует трафик по этим endpoint-ам. Метки — универсальный клей по всему Kubernetes: Deployment использует тот же механизм, чтобы знать, какими подами он владеет.
Три основных типа Service — это многослойная эскалация доступа:
- ClusterIP (по умолчанию) — виртуальный IP, доступный только внутри кластера. Внутренний трафик между сервисами.
- NodePort — открывает статический порт (диапазон по умолчанию
30000–32767) на IP каждого узла, проброс на ClusterIP. Грубый внешний доступ; для прод-HTTP редко правильный ответ. - LoadBalancer — провижинит облачный балансировщик, указывающий на NodePort. Цепочка трафика: внешний клиент → облачный LB → node:NodePort → ClusterIP → Pod. Один LB на каждый Service, что быстро становится дорого.
- Ingress — не тип Service, а L7 HTTP-роутер перед Services. Один LoadBalancer питает Ingress-контроллер (NGINX, Traefik), который маршрутизирует по host и path на множество ClusterIP-сервисов. Так ты выставляешь десятки приложений за одним внешним IP и TLS-сертификатом.
ConfigMap, Secret и прод-провал
Конфиг и креды живут в своих объектах, чтобы ты не запекал их в образ: ConfigMap для нечувствительного конфига, Secret для кредов (закодированы в base64 и не зашифрованы at rest, пока не включишь шифрование). Оба монтируются как env-переменные или файлы. Один острый угол: изменение ConfigMap не триггерит rollout — поды держат старые значения до перезапуска, поэтому команды хешируют конфиг в аннотацию пода, чтобы форсировать новый ReplicaSet при изменении.
Теперь провал из Hook, по механике. Service маршрутизирует на под в тот момент, когда под становится Ready. Без readiness-пробы «ready» значит лишь «процесс контейнера стартовал» — а не «приложение может обслуживать запросы». Поэтому на каждом rollout Service добавляет новый под в свои endpoints, пока приложение ещё прогревается, и часть трафика даёт 500, пока оно не закончит. Фикс — readiness-проба (HTTP GET /healthz, TCP-проверка или exec), которая возвращает успех, только когда приложение по-настоящему обслуживает. Падающая readiness-проба вытаскивает под из endpoints Service — никакого трафика, пока она не пройдёт. Это отличается от liveness-пробы, которая перезапускает зависший под. Классический инцидент: использовать liveness там, где нужна была readiness, и медленно стартующее приложение убивают и перезапускают в цикле вместо того, чтобы просто держать вне ротации. Дефолты проб агрессивны — periodSeconds: 10, timeoutSeconds: 1, failureThreshold: 3 — и медленный /healthz под нагрузкой будет флапать; для медленного старта используй startupProbe, чтобы придержать остальные.
Stateless HTTP API прогревается ~20с (загрузка конфига + пул соединений + прогрев кэша), прежде чем может обслуживать. Нужны rollout-ы без ошибок. Что настроишь?
Узел перезагрузился и забрал с собой три твоих пода. Твой Deployment запросил 5 реплик. Что произойдёт и почему?
Как Service узнаёт, на какие поды слать трафик?
Расставь, что происходит при смене образа Deployment (rolling update):
- 1 Ты применяешь новый образ; Deployment записывает новое желаемое состояние
- 2 Deployment создаёт новый ReplicaSet для нового образа
- 3 Новый RS масштабируется вверх, старый — вниз, в границах maxSurge / maxUnavailable
- 4 Каждый новый под проходит readiness-пробу, прежде чем Service шлёт на него трафик
- 5 Старый ReplicaSet достигает масштаба 0, но сохраняется, чтобы rollout undo мог поднять его обратно
- 01Объясни, почему Deployment самоисцеляется после отказа узла, а голый Pod — нет.
- 02Пройди по шагам, как именно отсутствующая readiness-проба вызывает 500 во время rollout и как проба это чинит.
Kubernetes декларативен: ты задаёшь желаемое состояние, а контроллеры вечно крутят цикл сверки, сравнивая фактическое с желаемым и действуя, чтобы закрыть разрыв — именно это делает кластер самоисцеляющимся и поэтому в проде никогда не запускают голый, ничейный Pod. Иерархия рабочей нагрузки складывает по одной способности на слой: Pod запускает контейнеры с общей сетью и томами, но эфемерен; ReplicaSet держит ровно N подов живыми, сверяя число; Deployment управляет ReplicaSet-ами, чтобы выкатывать и откатывать, постепенно меняя их под maxSurge и maxUnavailable. Services дают стабильный виртуальный IP и DNS поверх сменяющихся IP подов, привязанные label-селекторами в список Endpoints, эскалируя от ClusterIP к NodePort и LoadBalancer, а Ingress делает L7-маршрутизацию впереди. ConfigMap и Secret выносят конфиг и креды наружу. И прод-урок, который всё связывает: Service маршрутизирует на под в тот же миг, как он Ready, поэтому без readiness-пробы каждый rollout шлёт трафик на приложения, которые стартовали, но ещё не могут обслуживать — и они дают 500, пока не прогреются.