Что такое kubelet
kubelet, единственный k8s-компонент, который работает с
контейнерами на ноде. Всё, что вы видите в kubectl get pod
результат того, что kubelet:
- Подписался на PodSpec'ы для своей ноды через kube-apiserver
- Сравнивает desired (от API) и actual (что реально запущено)
- Делает изменения через CRI/CSI/CNI
- Репортит status обратно в API
Не путать с container runtime (containerd), kubelet это уровень выше, он командует runtime'ом через CRI gRPC.
kube-apiserver
▲ ▼ list/watch + status
kubelet (на каждой ноде)
│
├── CRI gRPC → containerd → [[runc-and-runsc|runc]] → контейнер
├── CSI gRPC → csi-driver → mount/attach volume
└── CNI exec → calico/cilium → сеть pod'а
Главные подсистемы
Pod sync loop
Сердце kubelet'а, syncLoop в pkg/kubelet/kubelet.go.
Цикл every-N-seconds (default 10s) или event-driven:
- Получить desired set of pods из источников: API server,
--pod-manifest-path(static pods для control plane), HTTP-URL. - Сравнить с running pods.
- Для каждого pod, который должен быть, но не запущен,
syncPod. - Для каждого, который запущен но не должен,
killPod.
Источников три:
- api: основной, watch на kube-apiserver
- file: static pods из
/etc/kubernetes/manifests/(apiserver, etcd, controller-manager сами стартуют так на control-plane) - http: внешний URL с PodList
CRI, Container Runtime Interface
Разговор с container runtime через gRPC. Сокет, unix:///var/run/containerd/containerd.sock
(или CRI-O аналог). Два сервиса:
- RuntimeService, pods/containers lifecycle (RunPodSandbox, CreateContainer, StartContainer, StopPodSandbox)
- ImageService, pull/list/remove image
Pod в CRI, это PodSandbox (контейнер с pause-image, держит
netns + ipc/uts namespace) плюс N containers внутри него,
все шарят netns с pause.
Pause container нужен, чтобы:
- PID 1 в pod-namespace всегда был жив (для reap zombie)
- Перезапуск application-контейнера не убивал pod-network
Container Manager
Управляет cgroup-tree на ноде:
/sys/fs/cgroup/
├── system.slice/ # systemd
├── kubepods.slice/
▸kubelet root
│ ├── kubepods-burstable.slice/ # QoS class
│ │ └── kubepods-burstable-pod<UID>.slice/
│ │ └── cri-containerd-<containerID>.scope/
│ ├── kubepods-besteffort.slice/
│ └── kubepods-pod<UID>.slice/ # Guaranteed
Иерархия, по QoS-классу pod'а:
- Guaranteed: requests == limits на всех контейнерах. Высший приоритет.
- Burstable: requests < limits или часть контейнеров без обоих
- BestEffort: ни requests, ни limits
При [[oom-killer|OOM]] на ноде первыми падают BestEffort, потом Burstable (по oom_score), Guaranteed, последние.
Полезно: --reserved-cpus, --cpu-manager-policy=static
выделять физические CPU под Guaranteed-pod'ы.
Volume Manager
Реактирует на pod'ы с volumes: вызывает CSI-drivers (Attach → Mount → unmount → Detach). Для CSI это всё gRPC к socket'у CSI-Node-plugin'а на ноде.
Особенности:
- Mount/Unmount idempotent, kubelet ретраит при ошибке
- Attach/Detach делает не kubelet, а external-attacher sidecar
при
attachRequired: trueв CSI driver'е (но решение принимает A/D Controller в kube-controller-manager) - Mount-флаги (
noatime,nodev) задаются в [[kubernetes-storage|StorageClass]]
Probes
Health checking подов:
- livenessProbe, pod жив? Если N раз провалил → kill + restart
- readinessProbe, pod готов принимать трафик? Если нет → убрать из Endpoints (но не убивать)
- startupProbe, заменяет liveness на старте, для медленно стартующих приложений (избегаем kill во время загрузки)
Типы probe'ов: httpGet, tcpSocket, exec (запустить команду
внутри контейнера, exit-code 0, ok), grpc (с k8s 1.27+).
Probe-логика, целиком в kubelet, без сети к API.
Image GC и Disk eviction
kubelet не оставляет неиспользуемые images вечно, диск заполнится. Алгоритм:
Периодически (--image-gc-high-threshold, default 85%) проверяет
использование /var/lib/containerd (или CRI-эквивалент). Если
threshold, удаляет неиспользуемые images до
--image-gc-low-threshold(default 80%).
Сортировка: LRU по времени последнего использования. Image, на который ссылается running container, не удаляется.
Для логов: kubelet ротирует stdout/stderr через CRI (
--container-log-max-size, default 10Mi, --container-log-max-files 5).
Node-pressure eviction
Когда ресурсы ноды на исходе, kubelet проактивно убивает pods. Соблюдается приоритет:
| Сигнал | Default threshold | Что эвиктится |
|---|---|---|
memory.available | < 100Mi | BestEffort → Burstable → Guaranteed |
nodefs.available | < 10% | то же по QoS |
nodefs.inodesFree | < 5% | то же |
imagefs.available | < 15% | image GC сначала, потом eviction |
pid.available | < 10% | то же |
Soft eviction (--eviction-soft) с --eviction-soft-grace-period
pod получает время на graceful shutdown. Hard eviction, сразу SIGKILL.
Это отдельно от [[oom-killer|OOM-killer]] kernel'а: kernel убивает за секунды, kubelet проактивно эвиктит за минуты.
kubelet config
ConfigMap-based конфиг (вместо CLI-флагов):
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd # должен совпадать с containerd
containerRuntimeEndpoint: unix:///var/run/containerd/containerd.sock
clusterDNS: [10.96.0.10]
clusterDomain: cluster.local
podCIDR: 10.244.1.0/24 # из node-spec
evictionHard:
memory.available: "200Mi"
nodefs.available: "10%"
systemReserved:
cpu: "500m"
memory: "1Gi"
kubeReserved:
cpu: "500m"
memory: "1Gi"
maxPods: 110
serializeImagePulls: false # параллельно pull
cgroupDriver, частая боль. systemd (default современных) vs cgroupfs. Должен совпадать с containerd-config'ом, иначе kubelet видит pods «не свои» и творится бардак.
Static pods
Pods, которые kubelet запускает независимо от API server'а,
читая YAML из /etc/kubernetes/manifests/. Используется для
control-plane (kube-apiserver, etcd, controller-manager,
scheduler, каждый из них static-pod на control plane node'е).
В API появляются зеркальные pods (mirror pod), но kubelet
не слушает их edits, источник истины файл.
Полезно: для self-bootstrapping control plane (как kubeadm), для standalone-агентов.
CSI / CNI / Device Plugin
kubelet вызывает три внешних:
- CSI через gRPC (mounted в well-known path
/var/lib/kubelet/plugins/<driver>/csi.sock) - CNI через exec бинарника (
/opt/cni/bin/<plugin>) - Device Plugin через gRPC (для GPU, FPGA, RDMA, DaemonSet регистрируется в kubelet, kubelet пробрасывает в pod)
Когда что-то пошло не так
Node not readyсразу после старта, kubelet не достучался до CRI socket'а, или CNI не настроен (/etc/cni/net.d/пустой).journalctl -u kubelet.failed to get system container stats: failed to get cgroup statscgroup driver mismatch (systemd vs cgroupfs) между kubelet и containerd.- Pod
Evictedбез видимой причины,kubectl describe node→Conditions:(MemoryPressure/DiskPressure/PIDPressure). Чаще всего, disk full из-за кэша images.crictl rmi --prune. exec format errorв pause container, multi-arch image не подходит ноде.kubectl describe pod→ image arch.- Pod stuck in Terminating, finalizer держит, или CRI не
отвечает на StopPodSandbox.
crictl ps→ есть ли container,crictl logs <id>. - kubelet OOM на самом себе, слишком много pod'ов и больших
PodSpec'ов в RAM. Tune
--max-pods, добавить--kube-api-qps/burst. - CSI mount висит, csi-node-plugin pod упал.
kubectl get pods -n kube-system | grep csi. - kubelet log shows
syncPod errored, частое и общее. Полезно--v=4для verbose, но логи раздуются.
Полезные диагностические артефакты
journalctl -u kubelet -f, основные логиcrictl ps(вместоdocker psдля CRI runtime'ов)crictl logs <id>, логи контейнераcrictl images, что pulled на нодеcat /var/lib/kubelet/config.yaml, текущий конфигcurl -k https://localhost:10250/metrics, Prometheus метрикиcurl -k https://localhost:10250/healthz