Learning Platform
Глоссарий Troubleshooting
Урок 03.06 · 28 мин
Продвинутый
kubeletkube-proxyCRICNICSIiptablesIPVSnftables

kubelet и kube-proxy: что работает на каждой ноде

На каждом worker-узле работают три обязательных процесса: kubelet (primary node agent, запускает Pod-ы через CRI), kube-proxy (реализует Services через netfilter), и container runtime (containerd или CRI-O — фактически выполняет контейнеры). Плюс DaemonSet-ы от CNI (Calico, Cilium, …) и CSI плагинов.

В этом уроке препарируем работу узла до уровня gRPC вызовов между kubelet и runtime, iptables-цепочек kube-proxy, и того, что такое ClusterIP с точки зрения netfilter. Это килер-момент модуля.


Namespaces и cgroups: фундамент контейнерной изоляции

kubelet: что и как делает primary node agent

kubelet — Go-процесс на каждой ноде. Его задачи:

  1. Watch на apiserver объекты Pod-ов с .spec.nodeName == own-hostname (или managed static pods).
  2. Для каждого Pod-а:
    • Pull container images через CRI image service.
    • Создать Pod sandbox (network namespace, cgroup, pause container).
    • Запустить контейнеры через CRI runtime service.
    • Mount volumes (через CSI или встроенные volume plugins).
    • Запускать probes (liveness, readiness, startup).
  3. Обновлять .status Pod-а в apiserver (containerStatuses, podIP, conditions).
  4. Обновлять Lease объект ноды (heartbeat).
  5. Cadvisor (встроен в kubelet) — собирает stats по ресурсам контейнеров, экспортирует через /stats/summary.
kubelet listens:
:10250 — HTTPS, server для apiserver проксирования (logs, exec, port-forward, attach)
:10248 — HTTP, /healthz (только localhost)

(Старый read-only port :10255 удалён из kubelet давно — на v1.35 его нет; stats доступны через /stats/summary на :10250 после авторизации.)

kubelet и его соседи
kube-apiserverWATCH на Pod-ы и Node объекты. UPDATE на .status.
HTTPS watch / update
kubeletPrimary node agent. Управляет жизненным циклом Pod-ов. Не запускает контейнеры сам — общается с runtime через CRI gRPC.
Unix socket (gRPC)
CRI runtimecontainerd (через cri-plugin) или CRI-O. Реализует CRI gRPC API: RuntimeService (запуск контейнеров) и ImageService (pull/list images). Слушает на /run/containerd/containerd.sock или /run/crio/crio.sock.
OCI runtime spec
runcOCI-runtime, который реально создаёт namespaces, cgroups, и запускает процесс контейнера. То, что было heart-ом Docker engine, но теперь stripped-down standalone binary.

CRI: gRPC между kubelet и runtime

Container Runtime Interface — gRPC API из двух сервисов:

service RuntimeService {
  rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse);
  rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse);
  rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
  rpc StartContainer(StartContainerRequest) returns (StartContainerResponse);
  rpc StopContainer(StopContainerRequest) returns (StopContainerResponse);
  rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse);
  rpc Exec(ExecRequest) returns (ExecResponse);
  rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
  rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse);
  // ...
}

service ImageService {
  rpc ListImages(ListImagesRequest) returns (ListImagesResponse);
  rpc PullImage(PullImageRequest) returns (PullImageResponse);
  rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse);
  rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse);
}

Когда kubelet запускает Pod, цепочка примерно такая:

kubelet запускает Pod через CRI
1. PullImagekubelet вызывает ImageService.PullImage для каждого образа в .spec.containers. Runtime скачивает image, верифицирует digest, кэширует на диске. Возвращает image ID.
2. RunPodSandboxkubelet вызывает RuntimeService.RunPodSandbox. Runtime: 1) запускает pause-контейнер (минимальный процесс, который держит namespaces); 2) создаёт network namespace; 3) вызывает CNI plugin для setup сети (создание veth, IPAM, routing); 4) возвращает sandbox ID и pod IP.
3. CreateContainer + StartContainerДля каждого контейнера в Pod-е (включая init containers по очереди, потом sidecars и main containers): CreateContainer создаёт OCI bundle (rootfs + config.json) и регистрирует контейнер у runtime; StartContainer вызывает runc create + runc start. Контейнер запускается внутри Pod sandbox (наследует network namespace, IPC namespace, UTS namespace).
4. ProbesПосле старта kubelet начинает выполнять startupProbe (если есть), потом livenessProbe и readinessProbe по schedule. Каждый probe — это http GET / exec / tcp от kubelet к контейнеру (через network namespace Pod-а).
5. Status updatekubelet периодически (sync interval, default 1 сек) собирает status контейнеров через RuntimeService.ContainerStatus, считает Pod-level статус (.status.phase, .status.conditions), делает PATCH на apiserver.
NOTE

До v1.24 Docker как runtime поддерживался через dockershim — shim внутри kubelet, который транслировал CRI вызовы в Docker API. В v1.24 dockershim удалён. Сейчас работает только через CRI-совместимые runtimes: containerd (с CRI plugin), CRI-O, krunvm для виртуализованных контейнеров, gVisor, Kata Containers.


Static pods: pods без apiserver

Помимо Pod-ов из apiserver, kubelet может запускать static pods — Pod-ы из локальных манифестов в /etc/kubernetes/manifests/. Это обычные YAML-файлы, kubelet их следит inotify-ом и применяет как Pod-ы.

ls /etc/kubernetes/manifests/
# etcd.yaml
# kube-apiserver.yaml
# kube-controller-manager.yaml
# kube-scheduler.yaml

В kubeadm-кластере все control plane процессы — это static pods! kubelet запускает их сам, без участия scheduler-а и apiserver-а. Это решает классическую chicken-and-egg проблему: чтобы запустить apiserver через apiserver, нужно… уже иметь apiserver.

Static pod-ы регистрируются в apiserver как mirror pods — обычные Pod-объекты с suffix -<nodename>:

kubectl get pods -n kube-system
# NAME                                READY   STATUS
# etcd-master-1                       1/1     Running
# kube-apiserver-master-1             1/1     Running
# kube-controller-manager-master-1    1/1     Running
# kube-scheduler-master-1             1/1     Running

Mirror pod read-only с точки зрения apiserver — изменить через kubectl edit нельзя. Чтобы изменить — нужно править manifest файл на диске ноды.

TIP

Static pods полезны для bootstrap (control plane) и для случаев, когда вам нужен Pod, который запустится даже если apiserver недоступен (например, network agent на ноде). В CKAD scope static pods упоминаются — экзамен может попросить создать static pod в /etc/kubernetes/manifests/.


kube-proxy: что такое Service на самом деле

Теперь killer-момент модуля. ClusterIP — это не реальный IP. На нём никто не слушает, никто не отвечает по нему через ping. Это правило в netfilter, которое DNAT-ит трафик на endpoint Pod.

# Создаём Service
kubectl expose deployment nginx --port=80 --target-port=80
kubectl get svc nginx
# NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# nginx   ClusterIP   10.96.123.45    <none>        80/TCP    1m

# Можно ли запинговать ClusterIP?
ping 10.96.123.45
# нет ответа — никто не отвечает на ICMP

# Но curl работает
curl http://10.96.123.45
# <html><body>Welcome to nginx!</body></html>

Что происходит при curl 10.96.123.45:80:

  1. ядро Linux в network namespace клиента смотрит routing table — 10.96.0.0/12 (ClusterIP CIDR) маршрутизируется через default route на eth0 (или другой интерфейс).
  2. Пакет идёт через netfilter PREROUTING (если входящий) или OUTPUT (если исходящий с самой ноды) хук.
  3. kube-proxy установил правила iptables: dst=10.96.123.45:80 → DNAT → 10.244.1.5:80 (один из Pod IP, выбранный random).
  4. Пакет с DNAT-ом идёт дальше по сети (через CNI overlay или routing) до ноды с целевым Pod-ом.
  5. Pod видит пакет с dst=своему IP, обрабатывает, отвечает. Ответ проходит обратный SNAT через conntrack state.

kube-proxy modes

kube-proxy умеет реализовать Service через три механизма netfilter:

iptables (default)

Стандартный и default-режим до сих пор (в v1.35 включая). kube-proxy генерирует chains в netfilter: KUBE-SERVICES → KUBE-SVC-* → KUBE-SEP-*. Linear search по правилам — медленный при тысячах Services (latency растёт линейно).

IPVS (GA с v1.11)

Использует IPVS (kernel-level Layer 4 load balancer). Hash-таблица вместо linear chain — O(1) lookup. Намного быстрее для больших кластеров (>1000 Services). Несколько scheduling алгоритмов: rr, lc, sh, dh.

nftables (alpha v1.29, beta v1.31, GA v1.33)

Замена iptables — современный API netfilter. Поддерживает set/map с O(1) lookup, верифицируется в kernel, легче отлаживать. По производительности близко к IPVS, но без зависимости от ipvs модуля. На v1.35 включается через --proxy-mode=nftables, default остаётся iptables.

iptables mode под микроскопом

Когда kube-proxy работает в iptables mode, он создаёт цепочки в таблице nat:

# Главная entry point
*nat
:PREROUTING ACCEPT
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

# KUBE-SERVICES — DNAT для всех Services
:KUBE-SERVICES
# match Service ClusterIP → jump в Service-specific chain
-A KUBE-SERVICES -d 10.96.123.45/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-NPX46M4PTMTKRN6Y

# KUBE-SVC-NPX46M4PTMTKRN6Y — load balancing между endpoints
:KUBE-SVC-NPX46M4PTMTKRN6Y
# 33% chance → endpoint 1
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m statistic --mode random --probability 0.33333 -j KUBE-SEP-AAAAA
# 50% chance из оставшихся 67% → endpoint 2
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m statistic --mode random --probability 0.50000 -j KUBE-SEP-BBBBB
# fallback → endpoint 3
-A KUBE-SVC-NPX46M4PTMTKRN6Y -j KUBE-SEP-CCCCC

# KUBE-SEP-AAAAA — endpoint specific (один Pod)
:KUBE-SEP-AAAAA
# SNAT для hairpin (когда Pod-source = Pod-destination, чтобы ответ вернулся через kube-proxy)
-A KUBE-SEP-AAAAA -s 10.244.1.5/32 -j KUBE-MARK-MASQ
# реальный DNAT на Pod IP:port
-A KUBE-SEP-AAAAA -p tcp -m tcp -j DNAT --to-destination 10.244.1.5:80

Чтобы посмотреть это на реальной ноде:

# Цепочки в nat-таблице
iptables -t nat -L KUBE-SERVICES -n --line-numbers | head -20

# Конкретный Service
iptables-save | grep KUBE-SVC | head
iptables-save | grep KUBE-SEP | head

# В nftables mode
nft list table ip nat | head -50

Probability cascade важна: 33% → 50% → fallback. Не три раза по 33%! Каждый probability check — это --mode random --probability 1/(N-i), где i — порядковый номер. Так math работает: каждый endpoint получает равную долю траффика.

IPVS mode

Вместо linear chain iptables использует kernel IPVS module:

ipvsadm -L -n
# IP Virtual Server version 1.2.1 (size=4096)
# Prot LocalAddress:Port Scheduler Flags
#   -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
# TCP  10.96.123.45:80 rr
#   -> 10.244.1.5:80                Masq    1      0          0
#   -> 10.244.2.7:80                Masq    1      0          0
#   -> 10.244.3.9:80                Masq    1      0          0

rr — round-robin. Другие schedulers: lc (least connections), sh (source hashing — sticky sessions), dh (destination hashing).

Виртуальный server (ClusterIP) — на dummy interface kube-ipvs0:

ip addr show kube-ipvs0
# Виртуальный интерфейс с ClusterIP-ами всех Services

IPVS на больших кластерах (1000+ Services, 10000+ endpoints) даёт огромную разницу — iptables linear search становится bottleneck-ом.

EndpointSlices: что слушает kube-proxy

kube-proxy на ноде WATCH-ит EndpointSlice-объекты (не сами Service-ы):

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: nginx-abc12
  namespace: default
  labels:
    kubernetes.io/service-name: nginx
addressType: IPv4
ports:
- name: http
  port: 80
  protocol: TCP
endpoints:
- addresses: ["10.244.1.5"]
  conditions:
    ready: true
    serving: true
    terminating: false
  nodeName: worker-1
  zone: us-east-1a
- addresses: ["10.244.2.7"]
  conditions:
    ready: true
    serving: true
    terminating: false
  nodeName: worker-2
  zone: us-east-1b

Один Service может иметь несколько EndpointSlice-ов (шардирование по 100 endpoint-ов в shard). Это масштабируемее, чем старые Endpoints-объекты (один Endpoints на Service, мог расти до мегабайт при тысяче endpoint-ов).

WARNING

Когда Pod удаляется, kubelet ставит terminating: true в EndpointSlice и ready: false. kube-proxy убирает endpoint из rotation. Pod ещё некоторое время живой (terminationGracePeriodSeconds), но трафик на него уже не шлют. Это работает только если кубернетес-aware health check на LB и preStop хук в Pod-е дают graceful shutdown.


CNI: сеть Pod-ов

Container Network Interface — спецификация, как kubelet (через container runtime) конфигурирует сеть Pod-а. CNI plugin — это binary в /opt/cni/bin/, который вызывается с CNI command (ADD/DEL/CHECK) и stdin-ом JSON-конфигом.

CNI plugins для K8s:

  • Calico — BGP + IP-in-IP / VXLAN encapsulation; полная NetworkPolicy реализация; eBPF mode для производительности.
  • Cilium — eBPF-based, очень быстрый; продвинутая NetworkPolicy (L7); встроенная замена kube-proxy через eBPF.
  • Flannel — простой VXLAN overlay; нет NetworkPolicy.
  • Weave Net — encrypted mesh.
  • AWS VPC CNI — Pod IP-ы из VPC subnet (нативная маршрутизация).
  • GKE Dataplane V2 / Azure CNI Overlay — managed cloud CNI.

Когда kubelet запускает Pod sandbox:

1. Создаёт network namespace.
2. Вызывает CNI plugin:
   /opt/cni/bin/calico ADD
   stdin: {
     "cniVersion": "1.0.0",
     "name": "k8s-pod-network",
     "type": "calico",
     "ipam": {"type": "calico-ipam"},
     "containerID": "abc...",
     "ifName": "eth0",
     "netns": "/proc/12345/ns/net"
   }

3. CNI plugin:
   - Allocates IP from IPAM (172.17.5.7)
   - Creates veth-pair: veth-cali-abc (host) + eth0 (Pod ns)
   - Sets routing on host: 172.17.5.7 → veth-cali-abc
   - Sets routing in Pod ns: 0.0.0.0/0 → 169.254.1.1 (link-local)
   - Outputs JSON with assigned IP

4. kubelet записывает IP в .status.podIP

NetworkPolicy enforcement

Сам Kubernetes не enforce-ит NetworkPolicy — это работа CNI plugin. Calico, Cilium, Weave умеют. Flannel — нет (нужно ставить отдельно Canal = Flannel + Calico).

Когда вы создаёте NetworkPolicy объект:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-only-from-app
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: app
    ports:
    - protocol: TCP
      port: 5432

CNI plugin watch-ит NetworkPolicy объекты, и для каждого Pod-а в кластере применяет соответствующие правила (через iptables, eBPF, OVS — в зависимости от CNI).


CSI: storage plugins

Container Storage Interface — спецификация для volume provisioning. CSI driver — это два компонента:

  • Controller plugin — централизованный (часто Deployment в kube-system). Делает CreateVolume / DeleteVolume в backend (AWS EBS, Ceph, NFS).
  • Node plugin — DaemonSet на каждой ноде. Делает NodePublishVolume (mount на конкретной ноде).
# StorageClass — описывает backend
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
volumeBindingMode: WaitForFirstConsumer
# PVC — запрос на storage
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-data
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 10Gi
  storageClassName: fast-ssd

Цепочка:

  1. PVC создан, ждёт provisioning.
  2. external-provisioner (sidecar в CSI controller plugin) видит PVC, вызывает CSI driver: CreateVolume(size=10Gi, params).
  3. CSI driver делает AWS API: aws ec2 create-volume --size 10 --volume-type gp3. Получает VolumeID vol-abc123.
  4. external-provisioner создаёт PV объект в K8s с этим VolumeID, привязывает к PVC.
  5. Pod, использующий PVC, попадает на ноду.
  6. external-attacher делает ControllerPublishVolume — attach EBS к EC2 instance.
  7. kubelet вызывает CSI node plugin: NodeStageVolume (mount к global mount point на ноде), NodePublishVolume (bind-mount в Pod-ный mount namespace).
  8. Контейнер видит volume по mountPath.

Killer-момент: трафик от Pod-A до Pod-B через Service

Соберём всё вместе. Pod-A в namespace default на node worker-1 хочет вызвать http://nginx.default.svc.cluster.local.

1. Pod-A: curl http://nginx.default.svc.cluster.local
2. DNS resolve через CoreDNS (Service в kube-system):
   - Pod-A читает /etc/resolv.conf (kubelet положил туда CoreDNS IP)
   - UDP запрос на CoreDNS:53
   - CoreDNS отвечает: nginx.default.svc.cluster.local → 10.96.123.45 (ClusterIP)
3. Pod-A открывает TCP connection на 10.96.123.45:80
4. ядро Linux в Pod-namespace:
   - Routing: 10.96.0.0/12 через default GW (на host через veth)
   - Пакет уходит на host network namespace через veth pair
5. Host network namespace, netfilter NAT prerouting:
   - iptables правило: dst=10.96.123.45:80 → KUBE-SVC-NPX46M4PTMTKRN6Y
   - Random choice одного из 3 endpoint-ов: dst=10.244.2.7:80 (Pod-B на worker-2)
   - DNAT в conntrack, dst-IP пакета теперь 10.244.2.7:80
6. CNI routing на host:
   - 10.244.2.0/24 — это worker-2
   - Пакет идёт через overlay (VXLAN) или native routing на worker-2
7. На worker-2 пакет приходит к Pod-B (10.244.2.7):
   - CNI на worker-2 знает 10.244.2.7 → veth → Pod-B namespace
   - Pod-B принимает пакет, обрабатывает
8. Ответ:
   - Pod-B отвечает на src=10.244.2.7, dst=Pod-A IP
   - Возвращается на worker-1 через тот же overlay
   - На worker-1 conntrack делает reverse SNAT (восстанавливает Service IP как src)
   - Пакет приходит в Pod-A namespace с src=10.96.123.45 (как будто от Service)

Никто не “проксировал” соединение — Linux kernel netfilter сделал NAT, и connection идёт напрямую между Pod-A и Pod-B. kube-proxy только настраивает правила, в data path его нет. Это объясняет почему K8s networking быстрый — нет user-space прокси, только kernel.

В IPVS mode цепочка такая же, но IPVS вместо iptables делает lookup в hash table. В nftables mode — set lookup. С eBPF (Cilium) — даже не netfilter, а eBPF program в TC ingress hook, который делает то же самое ещё раньше в data path.


Проверка знанийKnowledge check
Если на ноде запустить `ping ClusterIP-Service-а` — что произойдёт и почему? И что произойдёт если запустить `curl ClusterIP-Service-а:port`?
ОтветAnswer
ping не получит ответа. ClusterIP — это не реальный IP, на нём никто не слушает и не отвечает на ICMP. Это просто значение в iptables/IPVS правилах, и netfilter DNAT работает только для TCP/UDP с указанным портом — для ICMP echo request правила DNAT нет, пакет в итоге дропается (никто его не маршрутизирует к Pod-у). curl на ClusterIP:port — работает: пакет matches dst=ClusterIP, dst-port=Service-port, iptables/IPVS DNAT-ит на один из endpoint Pod-ов, и соединение устанавливается с Pod-ом напрямую через conntrack (с reverse SNAT для исходящих ответов, чтобы Pod-A видел src=ClusterIP). Это часто удивляет: 'service не пингуется но работает'. Это нормально — Services не для ICMP.

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

Результат: 0 из 0
Аналитический
Вопрос 1 из 5. Что такое ClusterIP сервиса с точки зрения Linux networking?

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

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

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

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