Раньше сервис деплоили так: написали скрипт, зашли по ssh, остановили процесс, скопировали новый jar, запустили снова. Если нода упала — всё, сервис лежит, кто-то идёт чинить руками. Если нужно пять копий — пять раз делаешь одно и то же.
Kubernetes решает эту проблему иначе: вы описываете, что должно работать, а кластер сам решает как — и поддерживает это состояние непрерывно.
Как это работает вживую — на схеме ниже: один и тот же цикл сравнения отрабатывает и упавший под, и ручную команду, и правку манифеста.
Один и тот же цикл сравнения отрабатывает все три случая: пода не хватает — создаст, лишний — удалит, поменяли манифест — подтянет факт к нему.
Главная идея: желаемое состояние
Kubernetes работает как постоянный наблюдатель. Вы говорите ему: «хочу три копии этого сервиса, каждой нужно 512 МБ памяти, наружу смотрит порт 8080». Kubernetes сравнивает это с тем, что есть на самом деле, и устраняет расхождения.
Упала одна копия — Kubernetes создаёт новую. Умерла целая нода — поды переезжают на живые машины. Вы ничего не делаете руками: кластер сам поддерживает нужную картину.
Из этого следует важное правило: состояние кластера меняют правкой деклараций, не ручными командами. Зайти и что-то поправить «по-быстрому» — это то же самое, что менять файл на работающем сервере в обход системы контроля версий.
Кто именно исполняет ваши намерения
«Кластер сам решает как» — удобное упрощение, но дальше по фазе придётся читать события и разбирать инциденты, а для этого надо знать, кто их пишет. Внутри кластера четыре роли, и все они обычные программы.
api-server — единственная дверь. Всё, что делает kubectl, всё, что делают контроллеры и все инструменты, идёт через его HTTP API. Он проверяет права, проверяет корректность объекта и записывает его. Практическое следствие: если kubectl отвечает ошибкой доступа или таймаутом, дело не в поде, а в этой двери.
etcd — база, в которой лежат все объекты кластера. Ничего кроме api-server с ней не разговаривает. Знать про неё нужно ради одного: состояние кластера — это записи в базе, и резервная копия кластера — это копия etcd.
scheduler — выбирает, на какой узел поставить новый под. Он смотрит на запрошенные ресурсы (о них ниже), на свободную ёмкость узлов и на ограничения размещения. Именно он оставляет под в состоянии Pending, когда подходящего узла нет — и пишет об этом событие с причиной.
controller-manager — набор контроллеров, каждый со своим циклом «сравнил желаемое с текущим — сделал шаг». Контроллер Deployment создаёт и удаляет поды, контроллер узлов помечает узел недоступным, и так далее. Отсюда важное свойство: изменения происходят не мгновенно, а за несколько проходов цикла, поэтому между kubectl apply и появлением пода проходят секунды.
kubelet — агент на каждом узле. Он получает от api-server список подов, которые должны работать на его узле, запускает контейнеры через среду выполнения (containerd), следит за пробами, перезапускает упавшее и сообщает состояние обратно. Всё, что происходит с контейнером на узле, — его работа, и логи контейнера тоже приходят через него.
Как это складывается в один рассказ: kubectl apply отправляет объект в api-server, тот пишет его в etcd, контроллер Deployment замечает новый объект и создаёт поды, scheduler назначает им узлы, kubelet на этих узлах запускает контейнеры и докладывает статус. Каждый шаг оставляет событие (kubectl get events, kubectl describe) — и именно поэтому события читаются как хронология: их пишут разные участники этой цепочки.
Pod: один процесс (или несколько рядом)
Kubernetes не запускает контейнеры по одному: ему нужна единица, которую можно целиком создать, перенести на другую машину и убить. Эта единица — Pod: в большинстве случаев один контейнер с вашим приложением.
Два свойства пода важно понять с самого начала:
Под одноразовый. Его не лечат — его убивают и создают новый. Если приложение упало, Kubernetes не пытается его починить: он просто запускает свежий контейнер из образа. Из этого следует: никакого важного состояния внутри пода быть не должно. Файлы на диске исчезнут. IP-адрес сменится.
IP пода непостоянен. Пока под жив — у него есть адрес. Когда под пересоздаётся — адрес другой. Полагаться на него нельзя. Для постоянного адреса существует Service — об этом ниже.
Иногда в поде несколько контейнеров: например, ваше приложение плюс агент сбора логов. Они делят одну сеть и один диск. Это называется sidecar-паттерн, но новичку это редко нужно — почти всегда под = один контейнер.
Deployment: кто управляет подами
Поды вручную почти никогда не создают. Deployment — это декларация: «запусти N копий вот такого пода и поддерживай их в рабочем состоянии».
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels: { app: order-service }
template:
metadata:
labels: { app: order-service }
spec:
containers:
- name: app
image: registry.local/order-service:1.42.0
ports:
- containerPort: 8080
resources:
requests: { cpu: "500m", memory: "768Mi" }
limits: { memory: "768Mi" }
Deployment даёт три вещи:
- Самовосстановление — упала реплика, Kubernetes создаёт новую.
- Масштабирование — поменяли
replicas: 3наreplicas: 5, через несколько секунд их пять. - Обновление без остановки — при смене образа Deployment заменяет поды порциями, не роняя сервис целиком.
Как Deployment находит свои поды? Не по именам — по меткам (labels). В примере выше все поды имеют метку app: order-service, а Deployment ищет именно их через selector.matchLabels. Этот же механизм используется в Service.
Deployment держит поды по своей метке, Service ищет по своей: разойдётся одна буква - endpoints пуст, а поды при этом живы.
Одна ловушка, о которой лучше узнать заранее: селектор у Deployment менять нельзя. Захотели переименовать метку в уже работающем Deployment — кластер откажет, придётся удалять объект и создавать заново. Метки в selector.matchLabels выбирают один раз и всерьёз.
Есть ещё одно звено, которое вы почти не трогаете руками, но которое стоит знать по имени, — ReplicaSet. Deployment не создаёт поды сам: на каждую версию шаблона пода он заводит отдельный ReplicaSet, а уже тот держит нужное число подов. Отсюда и мгновенный откат на предыдущую версию: старый ReplicaSet никуда не делся, его просто снова разворачивают до нужного числа реплик.
requests и limits: сколько ресурсов просит под
В манифесте выше есть блок resources, и это не украшение: от этих двух чисел зависит, где под запустится и что с ним будет под нагрузкой.
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "768Mi"
cpu: "1"
requests — это то, что кластер обязуется дать. Планировщик складывает запросы всех подов на узле и ставит новый под только туда, где остался запас. Поэтому requests — про размещение: слишком большой запрос оставляет под в Pending навсегда, слишком маленький ставит его на переполненный узел, где он будет голодать.
limits — это потолок, дальше которого не дадут. По памяти потолок жёсткий: процесс, который его перешагнул, убивает ядро, и контейнер получает состояние OOMKilled (тот самый код 137 из статей про Docker). По процессору потолок мягкий: процесс не убивают, его притормаживают — он получает свою долю времени и ждёт остальное. Это неприятный эффект: приложение внезапно отвечает вдвое медленнее, а в логах ничего нет.
Единицы стоит запомнить, потому что они необычные. Память: Mi и Gi — это двоичные мегабайты и гигабайты (1024), а M и G — десятичные; разница в пять процентов, и в узких лимитах она заметна. Процессор: 1 означает одно ядро, 250m — четверть ядра («m» значит тысячные доли).
Как выбирать, коротко. Запрос памяти — примерно то, что процесс занимает под нагрузкой, с запасом; лимит памяти близко к запросу, чтобы под не рос неограниченно. Запрос процессора — обычное потребление, а лимит по процессору многие намеренно не ставят вовсе: он приносит притормаживание чаще, чем пользу. Про то, как это связано с настройками рантайма, — отдельные статьи: JVM считает кучу от лимита памяти, а число потоков — от лимита процессора; у Go то же делают GOMEMLIMIT и GOMAXPROCS, у Node — размер старого поколения V8, у Python — число воркеров.
Из пары requests и limits Kubernetes сам выводит класс обслуживания пода, и он решает, кого убьют первым, когда узлу не хватает памяти. Guaranteed: запросы равны лимитам по памяти и процессору у каждого контейнера; такой под вытесняют последним, и это выбор для баз и брокеров. Burstable: запросы есть, но меньше лимитов или заданы не всюду; обычный класс для сервисов, и среди них первым вытесняют того, кто сильнее превысил свой запрос. BestEffort: ни запросов, ни лимитов; первый кандидат на вытеснение и худшее размещение. Класс виден в kubectl describe pod строкой QoS Class. Поэтому запросы указывают всегда, даже приблизительные: под без них попадает в BestEffort.
Service: постоянный адрес над сменяющимися подами
Поды приходят и уходят, их адреса меняются. Service — это стабильное имя и виртуальный IP, который всегда указывает на поды, готовые принимать трафик:
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector: { app: order-service }
ports:
- port: 80
targetPort: 8080
Теперь любой другой сервис в кластере обращается по имени http://order-service — и Kubernetes направит запрос на один из готовых подов с меткой app: order-service.
Важная деталь: под попадает в список доступных адресов только тогда, когда его readiness probe возвращает успех. Если приложение ещё не прогрелось или временно недоступно — трафик на него не идёт. Как настроить probe — в следующей статье.
Как запрос из интернета добирается до пода
Полная цепочка выглядит так:
Каждый шаг отвечает за своё: балансировщик даёт внешний адрес, Ingress разбирает домены и пути, Service находит живые поды. Под при этом может переезжать и менять адрес — снаружи этого не видно.
Ingress — это декларация маршрутизации: «запросы на api.example.com/orders отправляй в Service order-service». Плюс там же настраивается TLS. Выполняет эту декларацию Ingress-контроллер (например, nginx или traefik) — он уже реально слушает входящие запросы.
Когда что-то не работает, эту цепочку проверяют звено за звеном: LoadBalancer поднят? Ingress сконфигурирован правильно? Service видит поды? Поды отвечают на probe?
ConfigMap и Secret: настройки отдельно от образа
Образ один на все среды — это правило из статей про Docker, и в Kubernetes оно поддержано двумя объектами.
ConfigMap — набор пар «ключ — значение» или целых файлов с настройками:
apiVersion: v1
kind: ConfigMap
metadata:
name: orders-config
data:
SPRING_PROFILES_ACTIVE: prod
LOG_LEVEL: INFO
Secret — то же самое, но для паролей, ключей и токенов. Отличается он тремя вещами: значения хранятся в кодировке base64 (это не шифрование, а способ передать двоичные данные), доступ к секретам обычно ограничен правами отдельно от остальных объектов, и содержимое не печатается в описании пода.
Подключают их к поду двумя способами:
envFrom:
- configMapRef:
name: orders-config # все ключи станут переменными окружения
env:
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: # одно значение из секрета
name: orders-db
key: password
volumeMounts:
- name: config # или файлом на диске
mountPath: /config
Что важно знать сразу. Переменные окружения читаются при старте: изменили ConfigMap — под продолжает работать со старыми значениями, пока его не перезапустят. Файлы, наоборот, обновляются на месте через некоторое время, и приложение может их перечитать. И base64 в секрете никого не защищает: настоящая защита — это права доступа к объектам и шифрование в самой базе кластера, а пароли в репозитории с манифестами — отдельная тема, о которой статья про доставку.
Namespace: разделяем окружения
Два окружения в одном кластере — и в обоих сервис называется order-service. Чтобы имена не сталкивались, а права и квоты раздавались по окружениям, объекты раскладывают по namespace — папкам кластера: payments-prod, payments-staging.
Изоляция в namespace административная: права доступа, квоты на ресурсы. Сетевой изоляции namespace не даёт — сервисы из разных namespace могут общаться, если явно не запрещено через NetworkPolicy.
Минимальный набор команд
Большинство задач разработчика покрывают пять команд:
kubectl -n payments-prod apply -f deployment.yaml
kubectl -n payments-prod get pods
kubectl -n payments-prod describe pod order-service-7d4b9c-x2x4v
kubectl -n payments-prod logs deploy/order-service --tail=200
kubectl -n payments-prod port-forward svc/order-service 8080:80
Первая — отправить в кластер поправленный манифест; именно так меняют состояние, а не ручными командами. Вторая — посмотреть, что запущено. Третья — разобраться, почему под не стартует или лежит. Четвёртая — прочитать логи; учтите, что у деплоймента с тремя репликами kubectl возьмёт логи одного пода, а не всех сразу. Пятая — временно пробросить порт к себе, чтобы проверить работу сервиса локально.
Что Kubernetes не делает за вас
Kubernetes заменяет упавшие поды — но не чинит код внутри. Если приложение падает из-за бага, Kubernetes будет перезапускать под снова и снова (это называется CrashLoopBackOff) — причину придётся искать в логах и чинить самому.
Перезапуски при этом не идут подряд: после каждого падения кластер ждёт всё дольше — 10 секунд, 20, 40, 80 — и упирается в потолок 5 минут. Поэтому упавший под может минутами просто лежать и «ничего не делать»: это не зависание, а пауза перед следующей попыткой.
Kubernetes не делает сервис отказоустойчивым сам по себе. Повторные попытки, таймауты, автоматическое отключение неработающего соседа — это по-прежнему задача разработчика. Kubernetes лишь следит, чтобы нужное количество подов было живо.
База данных в Kubernetes — отдельная, сложная тема (StatefulSet, операторы). Большинству команд проще держать управляемую БД вне кластера.
Когда Kubernetes не нужен
Статья до этого места объясняет, как он работает, и не отвечает на вопрос, стоит ли его брать. Ответ не всегда «да», и цена у него вполне измеримая.
Что придётся содержать. Управляемый кластер у облачного провайдера — это плата за узлы и за сам кластер, а ещё реестр образов, хранилище, балансировщик, система сбора логов и метрик. Свой кластер на своих машинах добавляет сюда обновления версий (несколько раз в год, с ломающими изменениями в API), сертификаты, резервные копии базы кластера и дежурство. Кто-то должен это уметь: не «прочитал руководство», а понимать пробы, ресурсы, сеть и права.
Когда не стоит.
- Один сервис на паре машин. Контейнер плюс
docker composeили systemd, перезапуск при падении, обновление по одному узлу за раз. Это закрывает те же задачи меньшими силами, и разбор такого отказа не требует знания десяти объектов. - Нет платформенной поддержки. Если в команде четыре разработчика и никто не хочет заниматься кластером, кластер будет заниматься вами. Признак: манифесты правит один человек, и когда он в отпуске, выкаты останавливаются.
- Нагрузка ровная и небольшая. Автоматическое масштабирование, ради которого часто и берут кластер, имеет смысл при пиках в разы; при ровной нагрузке достаточно фиксированного числа экземпляров.
- Всё живёт в одном приложении. Монолиту с базой Kubernetes не даёт почти ничего, кроме нового слоя, в котором надо разбираться при каждом сбое.
Что брать вместо. Контейнеры плюс docker compose на одной машине; готовая площадка для контейнеров у облака (запускает образ, сама масштабирует и обновляет, без кластера); или обычный выкат на виртуальные машины через systemd. Все три дают меньше возможностей и требуют меньше знаний — и это правильный размен, пока сервисов немного.
Когда стоит. Сервисов больше пяти и они выкатываются независимо; несколько команд, которым нужны свои пространства и квоты; нагрузка с пиками, которую надо масштабировать автоматически; требование к доступности, из которого следуют несколько узлов и автоматический перенос подов. Тогда те же задачи всё равно придётся решать — и решать их вручную выйдет дороже.
Шпаргалка по объектам
| Объект | Что делает |
|---|---|
| Pod | Запускает один или несколько контейнеров вместе |
| Deployment | Следит, чтобы нужное число подов всегда работало |
| Service | Постоянное имя и адрес над меняющимися подами |
| Ingress | Маршрутизация внешнего трафика к нужному Service |
| ConfigMap / Secret | Конфигурация и секреты отдельно от образа |
| Namespace | Группа объектов с правами и квотами |
Глубже: куда девать состояние: PersistentVolume, PVC, StorageClass и StatefulSetрасширенное
«Файлы на диске исчезнут» это правило для пода, а не приговор для данных. У Kubernetes есть свой способ дать поду диск, который переживёт пересоздание, и четыре объекта, которые для этого нужны.
PersistentVolume это сам диск: том в облаке, раздел на узле, сетевая папка. Объект кластерный, заводит его администратор или облако. PersistentVolumeClaim это заявка от приложения в своём namespace: «мне нужно 20 ГБ с таким-то режимом доступа», и Kubernetes связывает заявку с подходящим томом. Под ссылается на заявку, а не на том, поэтому манифест приложения не знает, где диск физически. StorageClass избавляет от ручного заведения томов: класс описывает, как их создавать (тип облачного диска, скорость), и заявка с именем класса получает свежий том автоматически. Режим доступа важнее, чем кажется: ReadWriteOnce означает, что том монтируется на один узел, и Deployment с двумя репликами на разных узлах со вторым подом не поднимется; ReadWriteMany дают только сетевые файловые системы. Политика Retain против Delete решает, что будет с диском при удалении заявки, и для баз ставят Retain.
StatefulSet это Deployment для тех, кому нужны и диск, и постоянное имя. Поды называются db-0, db-1, стартуют и останавливаются по порядку, каждый получает свою заявку из шаблона volumeClaimTemplates, и при пересоздании db-1 снова подключает свой диск, а не чужой. Headless Service даёт каждому поду постоянное DNS-имя. Так живут базы, брокеры, всё, где у реплик разные роли.
Для сервиса без состояния на любом стеке из этого следует простое правило: ему диск не нужен. Файлы пользователей в объектное хранилище, состояние в базу, кэш в Redis, временные файлы в emptyDir, который живёт ровно столько, сколько под. Заявка на диск у сервиса без состояния это либо ошибка проектирования, либо журнал, который должен идти в stdout. А базу данных в кластере запускают через оператор (CloudNativePG, оператор Kafka), который знает про резервные копии и переключение реплик, либо берут управляемую базу вне кластера; StatefulSet руками это последнее, что делают с базой.
Глубже: несколько контейнеров в поде: sidecar, init и порядок остановкирасширенное
«Новичку редко нужен» относится к тому, чтобы писать sidecar самому. Встречать их придётся сразу: сборщик журналов, прокси сервисной сетки (istio-proxy), агент секретов (vault-agent) приходят в под без вашего участия, и три вещи про них стоит знать заранее.
Что делят. Контейнеры одного пода видят друг друга по localhost и могут делить тома, но у каждого свой образ, свои ресурсы и свой журнал. kubectl logs order-service-7d9f -c app читает журнал одного контейнера, --all-containers всех; без -c при нескольких контейнерах команда откажет, и это первое, обо что спотыкаются. Готовность пода это готовность всех контейнеров: если sidecar не поднялся, под не получит трафик, хотя приложение живо. Запросы ресурсов складываются: под с прокси просит на пол-ядра и сто мегабайт больше, чем приложение, и это видно в квотах.
Init-контейнеры запускаются до основных, по порядку, и обязаны завершиться: ждать базу, скачать конфигурацию, проверить миграции. Их журнал читают тем же -c.
Порядок остановки. При завершении пода все контейнеры получают SIGTERM одновременно. Прокси сетки, который умирает раньше приложения, рвёт исходящие вызовы ровно в тот момент, когда приложение доделывает запросы; агент секретов, ушедший первым, оставляет приложение без обновления токена. Старый обходной путь это задержка в preStop у sidecar или его настройка «жить, пока есть соединения». Штатный ответ появился в Kubernetes 1.29: контейнер объявляют в initContainers с restartPolicy: Always, и он становится настоящим sidecar: стартует до приложения, живёт вместе с ним и останавливается после него. Сервисные сетки умеют работать в этом режиме, и включать его стоит там, где приложение при остановке ещё делает исходящие вызовы, о чём статья про корректное завершение в Kubernetes.
Коротко
- Kubernetes поддерживает желаемое состояние: вы декларируете, он выполняет и восстанавливает при сбоях.
- Pod — одноразовая единица. Его не лечат, а пересоздают. Никакого важного состояния внутри. Sidecar приходят без вас: журнал по
-c, готовность пода это все контейнеры, ресурсы складываются;SIGTERMприходит всем сразу, и штатный порядок остановки дают sidecar черезinitContainersсrestartPolicy: Always. - Deployment управляет N копиями пода, даёт самовосстановление, масштабирование и обновление без остановки. Labels — механизм связи: Deployment находит свои поды по меткам, Service — тоже.
- Service — постоянный адрес над живыми подами. Трафик идёт только на поды с зелёной readiness probe. Ingress маршрутизирует внешний трафик к нужному Service.
- Namespace разделяет окружения административно, не сетево. ConfigMap и Secret держат настройки вне образа: переменные читаются при старте и требуют перезапуска, файлы обновляются на месте, а
base64в секрете — не защита. - Kubernetes заменяет упавшие поды, но не чинит код и не делает сервис отказоустойчивым сам по себе.
- Состояние живёт не в поде: PVC по StorageClass даёт диск,
ReadWriteOnceэто один узел, StatefulSet это постоянные имена и свой диск у каждой реплики; сервису без состояния диск не нужен, базу ставят оператором или берут снаружи. - Намерения исполняют четыре участника: api-server как единственная дверь, etcd как хранилище, scheduler для размещения, контроллеры и kubelet на узлах — и каждый пишет события, по которым потом читается хронология.
requestsопределяют размещение (маленькие — голодание, большие —Pending),limits— потолок: по памяти жёсткий (OOMKilled); класс обслуживания выводится из них —Guaranteed(равны) вытесняют последним,Burstable— обычный слой.- Kubernetes не нужен одному сервису на паре машин и команде без платформенной поддержки: там дешевле compose, готовая площадка для контейнеров или systemd.
Что почитать дальше
- Spring Boot в Kubernetes — probes, ресурсы и graceful shutdown, то, за что отвечает разработчик, на примере Spring Boot; для Go, Node и Python то же в статьях про остановку в Kubernetes (Go, Node, Python).
- Сеть и трафик — типы Service, DNS, Ingress и Gateway API подробно.
- Деплой и конфигурация — манифесты, Helm, rolling update.
- Эксплуатация и отладка — kubectl и типичные инциденты.