Что такое kubernetes узел и кто на нем живет
Worker-узел (node) - это просто Linux-машина (VM или железо) с тремя обязательными компонентами. Если хоть одного нет или он сломан - узел бесполезен.
- kubelet - главный агент. Это процесс-демон, который общается с api-server и отвечает за то, чтобы на этой конкретной ноде крутились ровно те поды, которые ему назначены. Он НЕ запускает контейнеры сам - он командует runtime через стандартный интерфейс.
- container runtime - то, что реально создает и запускает контейнеры: containerd, CRI-O. Снизу под ними - runc, который и делает системные вызовы для namespaces и cgroups.
- kube-proxy - программирует сетевые правила ядра так, чтобы виртуальный IP Service-а превращался в реальные IP подов. По сути это контроллер сетевых правил, а не прокси в привычном смысле.
Аналогия: control-plane - это диспетчер, который раздает наряды по рации. kubelet - бригадир на объекте, который наряды принимает и распределяет. runtime - рабочие с инструментом. kube-proxy - электрик, который протягивает провода так, чтобы по одному адресу (Service) ток шел к нужной бригаде. CNI - тот, кто вообще завел электричество на объект.

kubelet: что он делает на самом деле
kubelet - это reconcile-цикл, как и любой контроллер Kubernetes. Он постоянно сравнивает "что мне назначено" с "что реально запущено" и устраняет разницу.
Откуда он берет список подов? Основной источник - api-server: kubelet держит watch на поды, у которых spec.nodeName равен имени этой ноды (то есть те, что уже распределил scheduler). Есть и второй источник - static pods: манифесты, лежащие прямо в файле на ноде (обычно /etc/kubernetes/manifests). Их kubelet запускает БЕЗ участия scheduler и api-server. Именно так на control-plane поднимаются сами api-server, etcd, scheduler - курица и яйцо решаются через static pods.
Что kubelet делает с каждым подом:
- вызывает CNI, чтобы под получил сетевой namespace и IP;
- через CRI создает pause-контейнер (sandbox), который держит сетевой и IPC namespace пода - остальные контейнеры подключаются к нему;
- тянет образы и стартует контейнеры в нужном порядке (initContainers, потом основные, нативные sidecar с 1.29 как initContainer с restartPolicy: Always запускаются раньше и глушатся позже);
- следит за пробами: livenessProbe (перезапустить контейнер, если завис), readinessProbe (убрать под из эндпоинтов Service, пока не готов), startupProbe (дать медленному приложению прогреться, не убивая его раньше времени);
- монтирует volumes, прокидывает Secret/ConfigMap, считает использование ресурсов;
- репортит статус пода и ноды обратно в api-server.
container runtime и почему dockershim выпилили
Раньше kubelet умел разговаривать с Docker напрямую через встроенный костыль - dockershim. Проблема: это был специальный код именно под Docker внутри kubelet, который команда Kubernetes была вынуждена тащить и чинить. С версии 1.24 dockershim удален полностью. Это НЕ значит, что Docker-образы перестали работать - образы как были OCI-совместимыми, так и остались. Просто kubelet больше не говорит с Docker напрямую.
Теперь все runtime подключаются через CRI (Container Runtime Interface) - это gRPC-контракт. kubelet шлет вызовы вроде RunPodSandbox, CreateContainer, StartContainer на Unix-сокет runtime, и runtime сам решает, как их выполнить. Стандартный сокет containerd - /run/containerd/containerd.sock, у CRI-O - /var/run/crio/crio.sock.
Кто сейчас в проде:
- containerd kubernetes - дефолт почти везде (Yandex Managed Kubernetes, VK Cloud, EKS, GKE, k3s). Легкий, родом из Docker, но без лишней обвязки.
- CRI-O - заточен строго под Kubernetes, любим в OpenShift/Deckhouse-окружениях.
Посмотреть, что реально крутится на ноде, можно через crictl - это CRI-аналог docker ps, ходит прямо в runtime мимо Kubernetes:
Код: Выделить всё
crictl ps
CONTAINER IMAGE CREATED STATE NAME POD ID
a1b2c3d4e5f6 nginx@sha256 2 minutes ago Running nginx 9f8e7d6c5b4a
kube-proxy: как ClusterIP превращается в реальные поды
ClusterIP Service-а - это виртуальный адрес, которого физически нет ни на одном интерфейсе. Никто на нем не слушает. Магия в том, что kube-proxy на каждой ноде следит за объектами Service и EndpointSlice и программирует ядро так, чтобы трафик на этот виртуальный IP перенаправлялся (DNAT) на IP одного из живых подов.
Раньше kube-proxy подписывался на Endpoints, теперь - на EndpointSlice: вместо одного гигантского объекта со всеми эндпоинтами Service-а они нарезаны на куски примерно по 100 адресов. Для Service на тысячи подов это снимает огромную нагрузку с api-server и etcd при каждом изменении.
Режимы kube-proxy:
- iptables - дефолт по сей день. kube-proxy генерит цепочки iptables-правил. Минус: число правил растет линейно числу Service-ов и эндпоинтов, на больших кластерах обновление ruleset тормозит.
- IPVS - использует L4-балансировщик ядра на dummy-интерфейсе kube-ipvs0, поиск по хеш-таблице, сложность примерно O(1), плюс реальные алгоритмы балансировки (rr, lc, wrr). Важно: IPVS объявлен deprecated, новые кластеры на него ставить уже не стоит.
- nftables - новый бэкенд, стал GA (стабильным) в Kubernetes 1.33. Структура с map-lookup примерно O(1), масштабируется заметно лучше iptables. Требует ядро Linux 5.13+. Дефолтом пока остается iptables - ради совместимости, но для крупных кластеров на свежем ядре nftables это правильный выбор. Включается так: mode: nftables в конфиге kube-proxy или флаг --proxy-mode nftables.
Посмотреть правила для конкретного Service в iptables-режиме:
Код: Выделить всё
iptables -t nat -L KUBE-SERVICES -n | grep 10.96.0.10
KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns-tcp */ tcp dpt:53
Ресурсы узла: allocatable и reserved
Нода с 8 ГБ RAM НЕ отдает все 8 ГБ под поды. Часть зарезервирована, иначе системные демоны и сам kubelet помрут от OOM раньше пользовательских подов. Формула:
Код: Выделить всё
Allocatable = Capacity - kube-reserved - system-reserved - eviction-threshold
- Capacity - вся память/CPU ноды.
- kube-reserved - под kubelet, runtime, container-демоны.
- system-reserved - под ОС: sshd, systemd, логи.
- eviction-threshold - буфер, ниже которого kubelet начинает выселять поды, чтобы не свалиться в системный OOM-killer.
Код: Выделить всё
kubectl describe node node-3
Capacity:
cpu: 4
memory: 8141816Ki
Allocatable:
cpu: 3920m
memory: 7290936Ki
Регистрация узла и диагностика NotReady
При старте kubelet сам регистрирует ноду в api-server (создает объект Node) и дальше шлет heartbeat. Механизм heartbeat - объект Lease в namespace kube-node-lease: kubelet обновляет его раз в несколько секунд. Если api-server перестал видеть свежий Lease - через node-monitor-grace-period (по умолчанию около 40 секунд) нода помечается NotReady, а еще позже ее поды начинают выселять на другие ноды.
Что чаще всего загоняет узел в NotReady:
- сеть не настроена - CNI-плагин не установлен или его DaemonSet упал. Классика на свежем кластере: все ноды NotReady, пока не накатишь Calico/Cilium.
- kubelet не может достучаться до api-server (firewall, протухший сертификат, неверный kubeconfig).
- runtime лежит - сокет containerd недоступен, kubelet не может ни создать, ни проверить контейнеры.
- узел уперся в ресурсы - DiskPressure, MemoryPressure, PIDPressure. Эти условия kubelet выставляет сам.
Код: Выделить всё
systemctl status kubelet
journalctl -u kubelet -f --no-pager
crictl info
crictl ps
Типичные грабли и антипаттерны
- "Установлю обратно Docker как runtime" - не надо. Образы Docker и так работают, а dockershim мертв. Ставь containerd, это дефолт.
- livenessProbe, которая ходит наружу - проба должна проверять только сам процесс. Если она зависит от БД или другого сервиса, при их недоступности kubelet начнет бесконечно перезапускать здоровый под. Для зависимостей - readinessProbe.
- Поды без requests - scheduler считает по requests. Без них он думает, что под ничего не ест, набивает ноду под завязку, и при пике все валится в OOM/eviction. Всегда ставь хотя бы memory requests.
- Перезапуск kube-proxy "на всякий случай" в проде - на больших iptables-кластерах полная перестройка ruleset дает заметный сетевой провал. Понимай режим (iptables/IPVS/nftables) и его цену.
- Диагностика NotReady с ноутбука - kubectl покажет только статус. Корень почти всегда виден лишь в journalctl -u kubelet и crictl на самой ноде.
Подойдет любой кластер - kind, minikube, k3s или managed (Yandex/VK Cloud).
- Посмотри ноды и их статус: kubectl get nodes -o wide. Запиши версии runtime в колонке CONTAINER-RUNTIME - убедись, что там containerd, а не docker.
- Зайди в детали ноды: kubectl describe node <имя>. Найди секции Capacity и Allocatable, посчитай разницу - это твои резервы. Глянь Conditions: Ready, MemoryPressure, DiskPressure.
- Если есть SSH-доступ к ноде (k3s/kind подойдут) - выполни crictl ps и crictl pods. Сопоставь sandbox (POD ID) с тем, что показывает kubectl get pods.
- Создай Service: kubectl expose deployment <твой> --port=80. Затем iptables -t nat -L KUBE-SERVICES -n | grep <ClusterIP> (или nft list ruleset, если режим nftables) - найди цепочку своего сервиса.
- Сломай специально: kubectl scale deployment <твой> --replicas=0. Снова посмотри правила kube-proxy - SEP-цепочки эндпоинтов исчезнут, потому что живых подов больше нет.
- Посмотри Lease ноды: kubectl -n kube-node-lease get lease - это пульс твоего узла.
- Почему kubelet с версии 1.24 не запускает контейнеры через Docker напрямую и что пришло на смену dockershim?
- В чем разница между Capacity и Allocatable, и какое из значений видит scheduler при планировании пода?
- Чем kube-proxy в режиме nftables принципиально лучше iptables на больших кластерах и почему iptables все еще дефолт?
- Узел ушел в NotReady. Какие 3-4 причины проверишь в первую очередь и какими командами на самой ноде?
Worker-узел - это не магия, а три демона плюс плагин. kubelet принимает поды от api-server и гонит их через CRI в containerd и дальше в runc. kube-proxy превращает виртуальные ClusterIP в DNAT-правила ядра (iptables по умолчанию, nftables - будущее, IPVS уходит). CNI дает подам IP. allocatable, а не capacity, определяет, влезет ли под. А когда узел падает в NotReady, ответ почти всегда живет в journalctl -u kubelet и crictl - на самой ноде, а не в дашборде. Понимаешь этот слой - перестаешь бояться продакшен-инцидентов.