NNODE LOOP LABruntime observatoryNNEON · Статьи на 90 языках
CONNECTING
v24.18.0linux/x64
Desired state → controllers → rollout
20

Kubernetes: Pods, Services и reconciliation

Проследите путь image через Deployment, scheduling, readiness, Service routing и постепенное обновление Pods.

PROCESS IDтекущий сервер
UPTIMEпосле запуска
LOOP DELAY P95perf_hooks
UTILIZATIONevent loop
HTTP ROUNDTRIPbrowser → server
LIVE TRACE

Временная шкала

ГОТОВ
0 ms
События появятся здесьЗапустите выбранный сценарий
#ВРЕМЯИСТОЧНИКСОБЫТИЕ
Ожидаю запуск эксперимента…
ГЛАВА 20
ПОДРОБНЫЙ РАЗБОР · ОТ БАЗЫ К КОДУ

Разбираем: Kubernetes: Pods, Services и reconciliation

Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.

СНАЧАЛА ПРОСТЫМИ СЛОВАМИ

Docker умеет запустить контейнер на одной машине. Kubernetes похож на диспетчера парка машин: вы описываете, что должны работать три экземпляра приложения, а диспетчер постоянно сравнивает желаемое с реальностью, размещает Pods, заменяет упавшие и подключает готовые к общему адресу.

ТЕХНИЧЕСКАЯ ОСНОВА

Kubernetes — декларативный orchestrator контейнерных workloads. API server хранит желаемые objects, controllers выполняют reconciliation, scheduler выбирает node, kubelet поддерживает Pod lifecycle, а Service даёт стабильный virtual endpoint для динамического набора Ready Pods.

Зачем это знатьKubernetes нужен не для запуска одного container, а для управления множеством экземпляров и nodes: self-healing, rolling updates, service discovery, configuration, scheduling и resource governance. Цена — отдельная распределённая система, которую нельзя оправдывать одной модой.
ГДЕ ВЫПОЛНЯЕТСЯ РАБОТА
01MANIFESTdesired state
02CONTROL PLANEAPI · controllers · scheduler
03NODEkubelet · container runtime
04TRAFFICService · ready Pods
01 · СЛОВАРЬ

Термины этого эксперимента

Сначала поймите слова — затем порядок выполнения.

01

Cluster

Control plane и набор worker nodes, совместно запускающих и управляющих Kubernetes objects.

02

Pod

Минимальная deployable unit Kubernetes: один или несколько тесно связанных containers с общей сетью и volumes.

03

Deployment

Controller для stateless Pods, который управляет replicas, ReplicaSets и rolling updates.

04

Service

Стабильный network endpoint и DNS name для динамического набора Pods, выбранных selector-ом.

05

Label / selector

Label помечает object key/value; selector связывает controller или Service с подходящими objects.

06

Reconciliation loop

Controller снова и снова сравнивает desired state со observed state и делает шаги для устранения разницы.

07

Readiness probe

Проверка готовности принимать трафик. Failure исключает Pod из Service endpoints, но не обязан перезапускать container.

08

Liveness probe

Проверка, способен ли container продолжать работу. Устойчивый failure приводит к restart container.

09

Resource request / limit

Request участвует в scheduling и резервировании; limit ограничивает допустимое runtime consumption.

10

Rolling update

Постепенная замена старых Pods новыми с контролем maxSurge и maxUnavailable.

02 · МЕХАНИКА

Что происходит по шагам

Каждый шаг соответствует наблюдаемому состоянию runtime.

  1. 01
    Отправьте manifest

    kubectl передаёт YAML API server-у; schema validation проверяет apiVersion, kind, metadata и spec.

  2. 02
    Сохраните desired state

    Control plane сохраняет Deployment и увеличивает generation при изменении его Pod template.

  3. 03
    Создайте ReplicaSet

    Deployment controller обнаруживает расхождение и создаёт ReplicaSet для текущей revision.

  4. 04
    Запланируйте Pods

    Scheduler выбирает nodes, где выполняются requests, affinity, taints и прочие placement constraints.

  5. 05
    Запустите containers

    Kubelet на node просит container runtime pull-нуть image и поддерживает объявленный Pod lifecycle.

  6. 06
    Проверьте готовность

    Readiness probe допускает Pod в Service endpoints только после готовности приложения.

  7. 07
    Маршрутизируйте трафик

    Service selector находит Ready Pods и даёт клиентам стабильное имя независимо от Pod IP.

  8. 08
    Согласуйте изменения

    При новой image controller создаёт новые Pods, ждёт Ready и удаляет старые в рамках rollout strategy.

03 · КОНТЕКСТ

Где результат требует оговорки

Эти детали объясняют, почему похожий код иногда даёт другой trace.

01

Kubernetes не собирает image

CI собирает и отправляет image в registry. Kubernetes получает ссылку и запускает это содержимое на nodes.

02

Pod не является маленькой VM

Containers внутри Pod делят network namespace: обращаются друг к другу через localhost и имеют один Pod IP.

03

Replica не равна резервной копии

Три stateless Pods повышают availability процесса, но не заменяют backup данных или multi-zone database.

04

Readiness и liveness отвечают на разные вопросы

NotReady Pod убирают из трафика. Liveness failure вызывает restart. Если liveness зависит от PostgreSQL, outage базы может перезапустить весь fleet.

05

Requests важны для scheduler

Без requests scheduler не знает реальную потребность. CPU limit обычно ведёт к throttling, memory limit — к OOM kill при превышении.

06

Secret не шифруется одним именем

Kubernetes Secret отделяет данные от Pod spec, но base64 не является encryption. Нужны RBAC, encryption at rest и внешний secret workflow.

07

Service не публикует приложение в интернет автоматически

ClusterIP доступен внутри cluster. Внешний HTTP обычно проходит через Ingress/Gateway или Service типа LoadBalancer.

01
Теория

Сначала разберитесь, какие части Node участвуют в выполнении.

02
Упрощённый код

Затем уберите служебные детали и рассмотрите только главную идею.

03
Runtime-код

После этого сопоставьте модель с кодом, который создаёт live trace.

04 · Упрощённый код

Минимальная модель без служебного кода

src/demos.js · учебный фрагментJavaScript
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-loop-lab
spec:
  replicas: 3
  selector:
    matchLabels:
      app: node-loop-lab
  template:
    metadata:
      labels:
        app: node-loop-lab
    spec:
      containers:
        - name: app
          image: ghcr.io/example/node-loop-lab:1.0.0
          readinessProbe:
            httpGet:
              path: /api/health
              port: 3000
05 · Runtime-код

Полный код, который выполняет сценарий

Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.

ФАКТИЧЕСКИЙ SOURCE

Код сформирован из реальной серверной функции. Для сценариев с отдельным процессом или Worker показаны все участвующие файлы.

src/infrastructure-lab.js
infrastructure266 строк
const pause = (milliseconds) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

export const dockerfileExample = `# syntax=docker/dockerfile:1
FROM node:24-alpine AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM dependencies AS build
COPY . .
RUN npm run build

FROM node:24-alpine AS runtime
ENV NODE_ENV=production PORT=3000
WORKDIR /app
COPY --chown=node:node --from=build /app/.next/standalone ./
COPY --chown=node:node --from=build /app/.next/static ./.next/static
USER node
EXPOSE 3000
HEALTHCHECK CMD node -e "fetch('http://127.0.0.1:3000/api/health').then(r => { if (!r.ok) process.exit(1) })"
CMD ["node", "server.js"]`;

export const composeExample = `services:
  app:
    build:
      context: .
      target: runtime
    init: true
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DATABASE_URL: postgresql://app:password@postgres:5432/app
    depends_on:
      postgres:
        condition: service_healthy
    mem_limit: 2g
    pids_limit: 128
    restart: unless-stopped

  postgres:
    image: postgres:18-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 12`;

export const kubernetesManifestExample = `apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-loop-lab
spec:
  replicas: 3
  strategy:
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: node-loop-lab
  template:
    metadata:
      labels:
        app: node-loop-lab
    spec:
      containers:
        - name: app
          image: ghcr.io/example/node-loop-lab:1.0.0
          ports:
            - name: http
              containerPort: 3000
          readinessProbe:
            httpGet:
              path: /api/health
              port: http
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /api/health
              port: http
            periodSeconds: 10
            failureThreshold: 3
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 1Gi
---
apiVersion: v1
kind: Service
metadata:
  name: node-loop-lab
spec:
  selector:
    app: node-loop-lab
  ports:
    - name: http
      port: 80
      targetPort: http`;

function dockerStages(source) {
  const stages = [];
  for (const line of source.split('\n')) {
    const match = line.match(/^FROM\s+(\S+)(?:\s+AS\s+(\S+))?/i);
    if (match) {
      stages.push({
        image: match[1],
        name: match[2] ?? `stage-${stages.length + 1}`,
      });
    }
  }
  return stages;
}

export async function dockerBuildAndRun(emit) {
  const stages = dockerStages(dockerfileExample);

  emit(
    'build-context',
    'context',
    'Docker client собирает build context; .dockerignore исключает node_modules, .git и секреты',
  );
  await pause(15);

  emit(
    'dockerfile',
    'parse',
    `Dockerfile описывает ${stages.length} стадии: ${stages.map((stage) => stage.name).join(' → ')}`,
  );

  for (const stage of stages) {
    await pause(15);
    emit(
      'buildkit',
      'stage',
      `BuildKit строит stage ${stage.name} из immutable base image ${stage.image}`,
    );
  }

  emit(
    'cache',
    'layer',
    'COPY package*.json расположен до COPY исходников: изменение кода не инвалидирует слой npm ci',
  );
  await pause(15);
  emit(
    'image',
    'artifact',
    'Runtime image получает standalone build, но не исходный build toolchain',
  );
  await pause(15);
  emit(
    'container',
    'process',
    'Container запускает node server.js как USER node; init передаёт сигналы и убирает zombie processes',
  );
  await pause(15);
  emit(
    'network',
    'publish',
    'Port mapping 127.0.0.1:3000:3000 публикует container port только на loopback хоста',
  );
  await pause(15);
  emit(
    'health',
    'probe',
    'Healthcheck проверяет /api/health; healthy не означает, что все внешние зависимости доступны',
  );
  emit(
    'result',
    'summary',
    'Image — неизменяемый шаблон; container — запущенный process с writable layer и runtime configuration',
  );
}

function readyPods(pods) {
  return pods.filter((pod) => pod.ready);
}

export async function kubernetesReconciliation(emit) {
  const desiredReplicas = 3;
  let generation = 1;
  let pods = [
    { name: 'node-loop-lab-old-1', version: '1.0.0', ready: true },
  ];

  emit(
    'api-server',
    'desired-state',
    `Deployment принят: desired replicas=${desiredReplicas}, image=1.0.0`,
  );
  await pause(15);

  while (pods.length < desiredReplicas) {
    const pod = {
      name: `node-loop-lab-old-${pods.length + 1}`,
      version: '1.0.0',
      ready: false,
    };
    pods.push(pod);
    emit(
      'deployment-controller',
      'reconcile',
      `Actual=${pods.length - 1}, desired=${desiredReplicas}: ReplicaSet создаёт ${pod.name}`,
    );
    await pause(15);
    pod.ready = true;
    emit(
      'kubelet',
      'readiness',
      `${pod.name} прошёл readinessProbe и добавлен в endpoints Service`,
    );
  }

  emit(
    'service',
    'routing',
    `Service выбирает по label ${readyPods(pods).length} ready Pods из ${pods.length}`,
  );
  await pause(15);

  pods[1].ready = false;
  emit(
    'readiness',
    'traffic',
    `${pods[1].name} стал NotReady: container продолжает работать, но Service исключил его из трафика`,
  );
  await pause(15);
  pods[1].ready = true;

  generation += 1;
  const newPod = {
    name: `node-loop-lab-new-${generation}`,
    version: '1.1.0',
    ready: false,
  };
  pods.push(newPod);
  emit(
    'rolling-update',
    'surge',
    `maxSurge=1: создан ${newPod.name}, старые ready Pods пока обслуживают трафик`,
  );
  await pause(15);
  newPod.ready = true;
  pods = pods.filter((pod) => pod.name !== 'node-loop-lab-old-1');
  emit(
    'rolling-update',
    'replace',
    'Новый Pod стал Ready; controller удалил один старый Pod без снижения ready replicas',
  );

  emit(
    'scheduler',
    'resources',
    'Scheduler размещает Pod по requests; limits ограничивают runtime, но не резервируют дополнительный ресурс',
  );
  emit(
    'result',
    'summary',
    'Kubernetes непрерывно сравнивает desired и actual state; controller исправляет расхождение, а Service маршрутизирует только Ready Pods',
  );
}

Именно вызовы emit(...) превращаются в строки live trace. await и Promise удерживают HTTP-поток открытым до завершения сценария.

06 · РЕЦЕПТЫ

Практические шаблоны, которые можно подсмотреть

Сравнивайте цель, код и оговорки — не запоминайте синтаксис без модели.

01

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

Создать objects и наблюдать фактическое состояние Deployment.

kubectl apply -f node-loop-lab.yml
kubectl rollout status deployment/node-loop-lab
kubectl get pods -l app=node-loop-lab
kubectl describe deployment node-loop-lab
  • apply создаёт или declaratively обновляет objects.
  • rollout status ждёт доступности новой revision.
  • describe показывает conditions и recent events.
02

Deployment skeleton

Поддерживать три взаимозаменяемых Pods.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-loop-lab
spec:
  replicas: 3
  selector:
    matchLabels:
      app: node-loop-lab
  template:
    metadata:
      labels:
        app: node-loop-lab
    spec:
      containers:
        - name: app
          image: ghcr.io/example/node-loop-lab:1.0.0
  • Pod template change создаёт новую Deployment revision.
  • Image tag должен указывать на опубликованный registry artifact.
03

Service selector

Дать Pods стабильное DNS-имя и virtual IP.

apiVersion: v1
kind: Service
metadata:
  name: node-loop-lab
spec:
  selector:
    app: node-loop-lab
  ports:
    - port: 80
      targetPort: http
  • Selector должен совпасть с Pod label.
  • Service не зависит от меняющихся Pod IP.
04

Три разных probes

Разделить медленный запуск, готовность к трафику и зависание.

startupProbe:
  httpGet: { path: /api/health, port: http }
  failureThreshold: 30
  periodSeconds: 2
readinessProbe:
  httpGet: { path: /api/health, port: http }
  periodSeconds: 5
livenessProbe:
  httpGet: { path: /api/health, port: http }
  periodSeconds: 10
  failureThreshold: 3
  • Startup probe задерживает liveness/readiness до старта.
  • Readiness failure прекращает новый traffic.
  • Liveness failure после threshold вызывает restart.
05

Requests и limits

Дать scheduler сигнал и поставить runtime boundary.

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: "1"
    memory: 1Gi
  • 250m означает четверть CPU core как request.
  • Mi/Gi — binary units памяти.
  • Значения выбирают по измерениям, не копируют вслепую.
06

Обновление и rollback

Перейти на immutable version и вернуться при инциденте.

kubectl set image deployment/node-loop-lab \
  app=ghcr.io/example/node-loop-lab:1.1.0

kubectl rollout status deployment/node-loop-lab
kubectl rollout undo deployment/node-loop-lab
  • set image меняет desired Pod template.
  • Rollback возвращает предыдущую Deployment revision, но не откатывает database migration.
07

Диагностика Pod

Различить application logs, object status и node events.

kubectl get pod <pod> -o wide
kubectl logs <pod> -c app --previous
kubectl describe pod <pod>
kubectl get events --sort-by=.metadata.creationTimestamp
  • --previous читает logs предыдущего container после restart.
  • describe показывает probe failures, scheduling и image pull events.
07 · PRODUCTION-КЕЙСЫ

Как учебная ошибка превращается в инцидент

Реалистичный сервис: исходный код, наблюдаемая проблема, исправление и причина, по которой оно работает.

КЕЙС 01

Один общий health endpoint превращает сбой БД в restart storm

Deployment использует image:latest, не задаёт requests и направляет liveness/readiness на endpoint, который возвращает 500 при кратком outage PostgreSQL.

КОНТЕКСТ ИНЦИДЕНТА

Kubelet одновременно перезапускает все Pods из-за внешней зависимости. Оставшиеся replicas получают больше трафика, а scheduler без requests может плотно разместить приложение на перегруженном node.

ДОПРОБЛЕМНАЯ РЕАЛИЗАЦИЯ
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: api
          image: ghcr.io/example/api:latest
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            periodSeconds: 2

Liveness отвечает не на вопрос «process необратимо завис?», а на вопрос «доступна ли сейчас БД?». Moving tag скрывает точную revision, а отсутствие requests/strategy делает размещение и rollout непредсказуемее.

ПОСЛЕИСПРАВЛЕННАЯ РЕАЛИЗАЦИЯ
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  strategy:
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: ghcr.io/example/api:1.7.3
          startupProbe:
            httpGet: { path: /health/live, port: 3000 }
            failureThreshold: 30
            periodSeconds: 2
          livenessProbe:
            httpGet: { path: /health/live, port: 3000 }
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet: { path: /health/ready, port: 3000 }
            periodSeconds: 5
          resources:
            requests: { cpu: 250m, memory: 256Mi }
            limits: { cpu: "1", memory: 1Gi }

Live endpoint проверяет способность process продвигаться без требования доступности всего мира. Ready endpoint снимает Pod с трафика при временной неспособности обслуживать запросы. Version, strategy и resources делают rollout и placement наблюдаемыми.

ФУНКЦИИ И КОНСТРУКЦИИ

Что делают непривычные вызовы из обоих фрагментов кода.

replicas: 3
Задаёт желаемое количество Pods; controllers асинхронно создают или удаляют instances для достижения этого состояния.
matchLabels
Selector связывает Deployment с управляемыми Pods; те же labels позволяют Service найти traffic backends.
startupProbe
Даёт медленно запускающемуся container отдельное окно и не активирует liveness/readiness до первого успеха.
livenessProbe
После последовательных failures сообщает kubelet, что container нужно restart-нуть для восстановления progress.
readinessProbe
Управляет готовностью Pod к новому traffic; failure убирает endpoint из Service без обязательного restart.
resources.requests / limits
Requests участвуют в scheduling и capacity accounting, limits задают верхнюю runtime-границу CPU/memory.
maxUnavailable / maxSurge
Ограничивают число недоступных и дополнительных Pods во время постепенной замены Deployment revision.
ПОЧЕМУ ИСПРАВЛЕНИЕ РАБОТАЕТ

Probe contract проектируется как часть приложения. Перед production load-test измеряет startup/p99 и memory, PodDisruptionBudget и topology spread защищают от planned/node failures, а alerts следят за restarts и unavailable replicas.

ЧТО БЫЛО ВИДНО В PRODUCTION
  • Краткий database outage совпадает со всплеском container restarts.
  • Pods имеют статус OOMKilled или CPU throttling без capacity baseline.
  • Нельзя определить, какое содержимое было запущено под latest.
08 · НЕ ПЕРЕПУТАЙТЕ

Популярные заблуждения

Миф слева, корректная модель справа.

МИФ

Kubernetes заменяет Dockerfile и registry.

НА САМОМ ДЕЛЕ

Orchestrator запускает готовые images; build и supply chain остаются отдельной задачей CI.

МИФ

Если container Running, приложение готово.

НА САМОМ ДЕЛЕ

Process может ещё загружаться или быть неспособным обслуживать запросы; это различает readiness.

МИФ

Liveness должна проверять все dependencies.

НА САМОМ ДЕЛЕ

Внешний outage не всегда лечится restart-ом приложения и может вызвать cascading failure.

МИФ

Можно использовать image: latest.

НА САМОМ ДЕЛЕ

Движущийся tag делает rollout и rollback невоспроизводимыми; используют immutable version или digest.

МИФ

Три replicas гарантируют high availability.

НА САМОМ ДЕЛЕ

Scheduler может разместить их на одном node или в одной зоне без topology constraints.

МИФ

kubectl apply сразу обновляет все Pods.

НА САМОМ ДЕЛЕ

API изменяет desired state, после чего controllers асинхронно выполняют rollout.

09 · САМОПРОВЕРКА

Ответьте своими словами

Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.

  1. Чем Pod, Deployment и Service отличаются друг от друга?
  2. Кто сравнивает desired replicas с actual replicas?
  3. Почему Service selector должен совпасть с Pod labels?
  4. Что случится при readiness failure и при liveness failure?
  5. Чем resource request отличается от limit?
  6. Почему latest мешает воспроизводимому rollback?
  7. Почему три Pods не заменяют backup PostgreSQL?
  8. Что реально делает kubectl apply?