Learning Platform
Глоссарий Troubleshooting
Урок 03.05 · 25 мин
Продвинутый
controller-managerReconcile loopInformersLeader electionownerReferences

kube-controller-manager: reconcile loops

kube-controller-manager — это процесс, внутри которого живут ~30 разных controller-ов. Каждый controller следит за своим типом ресурсов и поддерживает инвариант: actual state = desired state. Когда вы создаёте Deployment, в дело включается цепочка: DeploymentController создаёт ReplicaSet, ReplicaSetController создаёт Pod-ы, scheduler привязывает их к нодам, kubelet запускает контейнеры.

В этом уроке разбираем reconcile loop pattern, informer caches (фундамент масштабируемости client-go SDK), leader election через Lease, и ownerReferences — механизм каскадного удаления. Это самая важная часть курса для тех, кто планирует писать operators.


Pipes и IPC: как процессы общаются через kernel

Reconcile loop: универсальный паттерн

Каждый controller построен по одной схеме:

Reconcile loop одного controller-а
1. Watch API serverController подписывается на changes своего resource type (например, ReplicaSet) и связанных ресурсов (Pod-ы для ReplicaSet-а). Использует informer — обёртку над watch с локальным кэшем.
2. Add to work queueПри получении event-а (ADDED/MODIFIED/DELETED) controller кладёт ключ объекта (namespace/name) в work queue. Не сам объект — только ключ. Это rate-limited queue с deduplication: множественные события для одного объекта схлопываются в одно reconcile.
3. Worker берёт ключНесколько goroutine-worker-ов вытягивают ключи из очереди. Для каждого ключа — запускают reconcile функцию.
4. Reconcile: observe + actЧитает desired state из informer cache (БЫСТРО — это локально), читает actual state (Pod-ы для этого ReplicaSet) тоже из cache, считает diff. Если нужно создать Pod-ы — POST на apiserver. Если удалить — DELETE на apiserver.
любая API write triggers watch event
5. ...снова watch eventCreated/Updated/Deleted Pod-ы триггерят новые watch events, которые попадают в ту же work queue. Controller продолжает reconcile-ить, пока actual = desired.

Это level-triggered reconciliation. Controller не реагирует на отдельные events (“этот Pod упал — создать новый”). Он реагирует на состояние: “у этого ReplicaSet должно быть 3 Pod-а, сейчас 2 — создать ещё один”. Если упало 5 Pod-ов разом, controller увидит “должно быть 3, есть 0”, создаст 3 за раз.

TIP

Level-triggered подход даёт фундаментальную надёжность: даже если controller пропустил event (рестарт, network glitch, queue overflow) — следующий reconcile увидит правильный diff и исправит. Edge-triggered подход (реагируем на конкретный delta) такой устойчивости не даёт.


Informers: shared cache + watch

Если бы каждый controller делал свои watch-ы независимо и хранил свой кэш, мы бы получили N×M traffic на apiserver. Поэтому в client-go SDK есть shared informer — singleton-кэш per (resource type, namespace), который используют все controllers.

controller-manager process
├── SharedInformerFactory
│   ├── PodInformer  (один на весь процесс, кэш всех Pod-ов)
│   │   ├── Watch: GET /api/v1/pods?watch=true&resourceVersion=...
│   │   ├── Local cache: thread-safe in-memory store
│   │   └── EventHandlers: ReplicaSetController, NodeController, ...
│   ├── ReplicaSetInformer (кэш всех RS)
│   ├── DeploymentInformer
│   ├── ...
│   └── (~30 информеров для основных типов)

├── DeploymentController         подписан на DeploymentInformer + ReplicaSetInformer
├── ReplicaSetController         подписан на ReplicaSetInformer + PodInformer
├── NodeController               подписан на NodeInformer + PodInformer + LeaseInformer
├── JobController                подписан на JobInformer + PodInformer
└── ...

Каждый informer:

  1. На старте делает LIST → набивает локальный cache.
  2. Открывает WATCH с последним resourceVersion → получает updates стримом.
  3. Применяет updates к cache, вызывает EventHandlers (Add/Update/Delete callbacks).
  4. EventHandlers обычно кладут ключи в work queue своего controller-а.

Это означает: когда controller хочет прочитать Pod — он не делает GET на apiserver, а читает из локального cache. Это microsecond-операция, не millisecond. И apiserver обслуживает не N×M GET-ов, а один WATCH на тип ресурса от controller-manager.

WARNING

informer cache eventually consistent — данные могут быть устаревшими на сотни миллисекунд по сравнению с etcd. Это не баг, это feature. Controllers пишут idempotent reconcile — если выполнили действие на устаревшем state, следующий reconcile (после новых watch events) сделает правильный шаг.


Основные controllers внутри controller-manager

В одном процессе controller-manager работают многие controllers. Главные:

DeploymentController

Watch на Deployment-ы. Создаёт/обновляет ReplicaSet-ы. Управляет rollout-ом: создаёт новый RS при изменении podTemplate, плавно масштабирует старый вниз и новый вверх (RollingUpdate). НЕ создаёт Pod-ы напрямую.

ReplicaSetController

Watch на ReplicaSet-ы и Pod-ы. Поддерживает .spec.replicas Pod-ов с label selector. Создаёт новые Pod-ы через POST, удаляет лишние через DELETE.

DaemonSetController

Watch на DaemonSet-ы и Nodes. Для каждой ноды (соответствующей .spec.template.spec.nodeSelector и tolerations) обеспечивает один Pod. При добавлении ноды — создаёт Pod. При удалении ноды — удаляет.

StatefulSetController

Watch на StatefulSet-ы и Pod-ы. Создаёт Pod-ы строго упорядоченно (web-0, потом web-1, потом web-2). Управляет PVC-привязкой (каждый Pod имеет свой PVC через volumeClaimTemplate).

JobController

Watch на Job-ы и Pod-ы. Создаёт Pod-ы под параметры .spec.completions и .spec.parallelism. Когда .spec.completions Pod-ов завершились с success — Job помечается Complete.

CronJobController

Watch на CronJob-ы. Использует cron-expression в .spec.schedule. По наступлению времени создаёт Job-объект (дальше его обрабатывает JobController).

NodeController (часть NodeLifecycleController)

Watch на Node-ы и Lease-ы в kube-node-lease namespace. Если Lease не обновляется в течение --node-monitor-grace-period (50 сек по умолчанию в v1.32+, исторически 40 сек) — ставит taint node.kubernetes.io/not-ready:NoExecute. После таймера эвакуации (5 минут) — выгоняет Pod-ы с ноды.

EndpointSliceController

Watch на Service-ы и Pod-ы. Для каждого Service генерирует EndpointSlice-объекты — списки endpoint-ов (IP + port + ready/notReady) разбитые на shards по 100 endpoint-ов. Эти EndpointSlice-ы потом потребляются kube-proxy на каждой ноде.

NamespaceController

Watch на Namespace-ы. При delete (с finalizer-ом) удаляет все ресурсы в этом namespace (Pod-ы, ConfigMap-ы, Secret-ы, RBAC-объекты), потом снимает finalizer и Namespace удаляется окончательно.

ServiceAccountController + TokenController

SA controller автоматически создаёт ServiceAccount default в каждом namespace. TokenController исторически создавал legacy Secret-ы с токенами; в современных версиях используется bound projected tokens, генерируемые kubelet-ом на лету.

PersistentVolumeBinder

Watch на PVC и PV. Подбирает подходящий PV для каждого Pending PVC (по requested storage size, accessModes, storageClassName). Привязывает (Bound) PVC к PV. Для dynamic provisioning — вызывает CSI driver через external-provisioner.

GarbageCollector

Watch на ВСЕ ресурсы. Следит за ownerReferences. Когда удаляется owner — каскадно удаляет owned объекты. Это механизм за kubectl delete deployment — он удаляет Deployment, GC удаляет ReplicaSet (owned by Deployment), GC удаляет Pod-ы (owned by RS).


Killer-момент: Deployment не создаёт Pod-ы

Это самое непонимаемое место в Kubernetes для тех, кто только начал. Diagram cascade:

Cascade при kubectl apply -f deployment.yaml
Deployment myappСоздаётся в apiserver. .spec.replicas=3, .spec.selector.matchLabels.app=myapp, .spec.template.metadata.labels.app=myapp.
DeploymentController watch
ReplicaSet myapp-7d4b9fСоздан DeploymentController-ом. Имя: <deployment>-<hash-podtemplate>. ownerReferences указывает на Deployment myapp. .spec.replicas=3 наследуется от Deployment.
ReplicaSetController watch
Pod myapp-7d4b9f-abcСоздан ReplicaSetController-ом. Имя: <rs>-<random5>. ownerReferences указывает на ReplicaSet. .spec.nodeName пустой — ждёт scheduler.
Pod myapp-7d4b9f-defТо же.
Pod myapp-7d4b9f-ghiТо же.
каскадное удаление
kubectl delete deployment myappGarbageCollector видит, что Deployment удалён. Удаляет owned ReplicaSet. Pod-ы owned by RS — удаляются следом. Это происходит ТРАНЗИТИВНО — GC реагирует на каждое удаление и каскадно идёт глубже.

ownerReferences под микроскопом

В каждом дочернем объекте — ссылка на родителя:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-7d4b9f-abc
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    kind: ReplicaSet
    name: myapp-7d4b9f
    uid: 12345-...
    controller: true                    # это primary owner
    blockOwnerDeletion: true            # owner не удалится пока child живой (foreground delete)

GC следит за этими ссылками. Когда owner удаляется, у GC два режима:

  • Background (default) — apiserver удаляет owner сразу, GC потом удаляет owned-объекты в фоне.
  • Foreground — apiserver сначала удаляет owned-объекты (через finalizer foregroundDeletion на owner), потом owner. kubectl delete --cascade=foreground.
  • Orphan — owner удаляется, owned-объекты остаются как сироты. kubectl delete --cascade=orphan. Полезно когда хочется заменить контроллер не теряя Pod-ы.
# Default — background
kubectl delete deployment myapp

# Foreground — ждёт удаления Pod-ов и RS до удаления Deployment
kubectl delete deployment myapp --cascade=foreground

# Orphan — оставляет ReplicaSet и Pod-ы
kubectl delete deployment myapp --cascade=orphan
NOTE

blockOwnerDeletion: true в ownerReferences важен только для foreground delete. Если false — owner может быть удалён даже если child ещё жив. Это используется в редких случаях, когда контроллер хочет управлять жизненным циклом независимо.


Leader election: один активный из N

В HA control plane controller-manager (и scheduler) работает в нескольких репликах — обычно 3, по одной на каждый control plane узел. Но активна только одна — лидер. Остальные на standby, ждут.

Это сделано так, чтобы не было гонок: если бы все три инстанса одновременно reconcile-или один Deployment, они могли бы создать дублирующие ReplicaSet-ы. Лидер один — гонок нет.

Lease как distributed lock

Механизм — обычный Lease объект в API server:

apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  name: kube-controller-manager
  namespace: kube-system
spec:
  holderIdentity: ip-10-0-1-12_a1b2c3d4-5e6f-7890     # уникальный per-instance ID
  leaseDurationSeconds: 15                            # сколько lease живёт без renew
  acquireTime: "2026-05-13T09:00:00.000000Z"
  renewTime: "2026-05-13T09:42:30.000000Z"            # обновляется ~каждые 10 сек
  leaseTransitions: 3                                 # сколько раз лидер менялся

Алгоритм:

  1. На старте каждый инстанс пытается claim lease — попытка PUT через optimistic concurrency (с resourceVersion). Если объекта нет, или текущий holderIdentity не свой и leaseDuration истёк — claim проходит.
  2. Если claim удался → этот инстанс становится leader, начинает работать.
  3. Раз в renewDeadline / 2 (по умолчанию 10 сек при duration 15) лидер делает PUT, обновляя renewTime. Если PUT не удался — лидер пытается заново, в течение renewDeadline. Если не получилось — лидер сдаётся, ставит себя в standby.
  4. Standby инстансы периодически (retryPeriod, ~2 сек) проверяют lease: renewTime + leaseDuration < now() → пробуют claim.
# Идиомы из kube-controller-manager флагов
--leader-elect=true
--leader-elect-lease-duration=15s
--leader-elect-renew-deadline=10s
--leader-elect-retry-period=2s
--leader-elect-resource-name=kube-controller-manager
--leader-elect-resource-namespace=kube-system

Историческая нота: endpoints/configmap-based locks

До coordination.k8s.io/v1 Lease (added в v1.14) лидерство хранилось в:

  • endpoints/configmap-based locks — Endpoints или ConfigMap объект с annotation control-plane.alpha.kubernetes.io/leader. Безобразный hack, но работал. Сейчас deprecated.
  • В v1.20+ default стал Lease, в v1.24+ старые варианты помечены deprecated, в v1.27 убраны.

Если посмотрите на старые apiserver-ы в production — увидите configmap kube-controller-manager в kube-system namespace с annotation про leader. Это legacy lock, который должен мигрировать на Lease.


Cloud Controller Manager

Из kube-controller-manager выделили отдельный процесс — cloud-controller-manager (CCM). Он содержит controllers, которые делают cloud-specific вещи:

  • NodeController (cloud variant) — общается с EC2/GCE/Azure API чтобы узнать, что нода реально удалена в cloud (и тогда удалить Node объект из K8s).
  • ServiceController — для Service типа LoadBalancer создаёт реальный cloud LB (ELB/NLB в AWS, GCP LB, Azure LB), привязывает к Service.
  • RouteController — настраивает routing tables в VPC чтобы Pod-IP были routable между нодами (используется в облаках без overlay-сетей).

Это позволило вытащить cloud-логику из main K8s codebase. K8s ядро остаётся cloud-agnostic, а каждый cloud provider предоставляет свой CCM binary с in-tree или out-of-tree implementation.

# Service с типом LoadBalancer
apiVersion: v1
kind: Service
metadata:
  name: web
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 8080

Когда вы создаёте такой Service:

  1. apiserver сохраняет в etcd.
  2. EndpointSliceController создаёт EndpointSlice для backing pods.
  3. ServiceController (внутри CCM) видит новый Service, вызывает AWS API (elbv2:CreateLoadBalancer), создаёт NLB, привязывает target group к Pod IP-ам.
  4. После создания NLB его DNS-имя пишется в .status.loadBalancer.ingress[0].hostname.
  5. kube-proxy на нодах настраивает iptables для трафика от NLB target group до Pod-ов (DNAT по ClusterIP или прямо на NodePort).

В onPrem кластерах (kubeadm, kind, k3s) обычно нет CCM — Service типа LoadBalancer останется в Pending с <pending> external IP. Решение — MetalLB или kube-vip, которые имитируют поведение CCM в bare-metal окружении.


Killer-момент: что видит controller-manager на старте

Если запустить controller-manager с --v=4 и посмотреть логи в первые 10 секунд:

I0513 09:00:00 leaderelection.go:243] attempting to acquire leader lease kube-system/kube-controller-manager...
I0513 09:00:00 leaderelection.go:253] successfully acquired lease kube-system/kube-controller-manager
I0513 09:00:00 controllermanager.go:614] Starting "deployment"
I0513 09:00:00 controllermanager.go:614] Starting "replicaset"
I0513 09:00:00 controllermanager.go:614] Starting "daemonset"
I0513 09:00:00 controllermanager.go:614] Starting "statefulset"
I0513 09:00:00 controllermanager.go:614] Starting "job"
I0513 09:00:00 controllermanager.go:614] Starting "cronjob"
I0513 09:00:00 controllermanager.go:614] Starting "node-lifecycle"
I0513 09:00:00 controllermanager.go:614] Starting "endpoint-slice"
I0513 09:00:00 controllermanager.go:614] Starting "namespace"
I0513 09:00:00 controllermanager.go:614] Starting "service-account"
I0513 09:00:00 controllermanager.go:614] Starting "persistentvolume-binder"
I0513 09:00:00 controllermanager.go:614] Starting "garbagecollector"
... (всего ~30 controllers)
I0513 09:00:01 shared_informer.go:285] Waiting for caches to sync for deployment
I0513 09:00:01 shared_informer.go:295] Caches are synced for deployment
I0513 09:00:01 deployment_controller.go:165] Starting deployment controller workers

Видно: после acquire lease — старт всех controllers один за другим. Каждый ждёт sync своего informer (LIST → набивка cache), потом стартует workers. Через секунду-две всё работает.

Если бы lease acquire не удался — мы бы увидели repeated retries без старта controllers. Это и есть standby режим.


Проверка знанийKnowledge check
Что делает GarbageCollector, когда вы выполняете `kubectl delete pod myapp-7d4b9f-abc` (Pod, который owned by ReplicaSet myapp-7d4b9f)?
ОтветAnswer
Ничего не делает с другими объектами. Pod удаляется напрямую — GC не реагирует, потому что удаляется child (Pod), а не owner (ReplicaSet). После удаления Pod-а ReplicaSetController в свой следующий reconcile увидит, что у него теперь 2 живых Pod-а вместо нужных 3, и создаст новый Pod (с новым random suffix). Это ключевое свойство reconcile-loop: ReplicaSet поддерживает желаемое количество, удалить отдельный Pod не помогает — он будет восстановлен. GC активируется в обратной ситуации — когда удаляется owner (Deployment или ReplicaSet), и тогда он каскадно удаляет owned-объекты по ownerReferences.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Что создаёт Deployment, когда вы делаете `kubectl apply -f deployment.yaml`?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6