Pods и жизненный цикл

Pod — минимальная единица деплоя в Kubernetes. Не контейнер, а именно pod. Это принципиальный момент: Kubernetes не управляет контейнерами напрямую, он управляет pod-ами. Pod может содержать один или несколько контейнеров, которые разделяют сетевое пространство, хранилище и часть Linux namespaces. Понимание жизненного цикла pod-а — от создания до удаления, включая probes, hooks, и restart policy — необходимо для написания приложений, которые корректно работают в кластере.


Pod — минимальная единица деплоя

Зачем Pod, а не просто контейнер

Контейнер по дизайну инкапсулирует один процесс. Но приложение часто состоит из нескольких связанных процессов: основной сервис + sidecar-прокси, основной сервис + агент для логирования. Этим процессам нужно общаться через localhost, видеть одни и те же файлы, иметь общий IP. Pod решает эту задачу — группирует контейнеры с общим сетевым стеком.

Shared namespaces

Контейнеры внутри одного pod-а разделяют часть Linux namespaces:

NamespaceОбщий?Что это значит
netДаОдин IP-адрес, один набор сетевых интерфейсов. Контейнеры общаются через localhost, но не могут занимать один порт
UTSДаОдин hostname
IPCДаShared memory, message queues
PIDОпциональноПри shareProcessNamespace: true — общее дерево процессов, контейнеры видят процессы друг друга
mntНетУ каждого контейнера своя файловая система. Для обмена файлами используют shared volume
text
Pod (один IP: 10.244.2.4) +------------------------------+ | Container A Container B | | :8080 :8443 | | | | | | +---- lo ------+ | <-- localhost (127.0.0.1) | eth0 | <-- один внешний IP +------------------------------+

Ключевое следствие: pod никогда не растягивается на несколько нод. Все контейнеры pod-а всегда запущены на одной физической или виртуальной машине.

Когда объединять в один Pod, а когда разделять

В один Pod — когда процессы тесно связаны, должны работать на одном хосте, масштабируются вместе и дополняют друг друга (основной процесс + sidecar).

В разные Pod-ы — когда компоненты независимы, масштабируются по-разному или имеют разный lifecycle (frontend + backend, независимые микросервисы).

text
Антипаттерн: Правильно: +---------------------+ +----------+ +----------+ | Frontend Backend | | Frontend | | Backend | | container container | | container| | container| | Pod | | Pod | | Pod | +---------------------+ +----------+ +----------+ Нельзя скейлить Скейлятся независимо: по отдельности 3x frontend, 1x backend

Kubernetes масштабирует pod-ы целиком, а не отдельные контейнеры внутри pod-а. Если компоненты масштабируются по-разному — это разные pod-ы.


Минимальный манифест пода

yaml
apiVersion: v1 kind: Pod metadata: name: my-app spec: containers: - name: my-app image: myregistry/my-app:1.0 ports: - containerPort: 8080 # информативно, не влияет на доступность порта

Поле containerPort — чисто информационное. Оно не открывает и не закрывает порты. Но полезно для документации и для именования портов в Service.

Голый Pod (без Deployment или ReplicaSet) — одноразовый. Если он умрёт, никто его не пересоздаст. В production всегда используют Deployment, который создаёт ReplicaSet, а тот управляет pod-ами.


Multi-container Pods

Sidecar Pattern

Sidecar — вспомогательный контейнер, который дополняет основной, не являясь основным процессом. Преимущество: не нужно менять код приложения. Один и тот же sidecar-образ переиспользуется для многих приложений.

Типичные примеры sidecar-ов:

SidecarЧто делает
HTTPS proxyEnvoy/Nginx перед HTTP-приложением, терминирует TLS
Log collectorЧитает логи из shared volume, шлёт в central logging
Content syncerСкачивает контент в shared volume для web-сервера
Auth proxyOAuth2 proxy перед приложением
Monitoring agentPrometheus exporter, сбор метрик
text
+--------------------------------+ | Node.js Envoy proxy | | :8080 (HTTP) :8443 (HTTPS) | | <---- localhost ----> | | Pod | +--------------------------------+ Envoy принимает HTTPS, проксирует HTTP на Node.js

Init Containers

Init-контейнеры запускаются последовательно перед основными контейнерами. Каждый следующий стартует только после успешного завершения предыдущего. Основные контейнеры стартуют только после всех init-ов.

text
Init 1 --> Init 2 --> Init 3 --> [Main A + Main B] (параллельно) | | | | завершился завершился завершился работают

Правила:

  • Запускаются последовательно, не параллельно
  • Каждый должен завершиться успешно (exit code 0)
  • Если init-контейнер упал — pod перезапускает его (согласно restartPolicy)
  • Основные контейнеры стартуют только после всех init-ов
  • Имена контейнеров уникальны в пределах pod-а (init + regular вместе)

Типичные use cases:

  1. Загрузка конфигурации / сертификатов — скачать из vault, записать в shared volume
  2. Ожидание зависимости — curl/ping до тех пор, пока база данных или другой сервис не станет доступен
  3. Миграция БД — запустить миграции перед стартом приложения
  4. Инициализация сети — настройка iptables, маршрутов (Istio sidecar injection)
  5. Уведомление внешней системы — "pod скоро запустится"

Безопасность: после завершения init-контейнера его filesystem недоступен основным контейнерам — это уменьшает attack surface.

yaml
apiVersion: v1 kind: Pod metadata: name: app-with-init spec: initContainers: - name: wait-for-db image: busybox:1.36 command: ['sh', '-c', 'until nc -z postgres-svc 5432; do echo waiting; sleep 2; done'] - name: run-migrations image: myregistry/migrator:1.0 env: - name: DATABASE_URL valueFrom: secretKeyRef: name: db-credentials key: url containers: - name: app image: myregistry/my-go-app:1.0 ports: - containerPort: 8080

Native Sidecar Containers (K8s 1.28+)

Обычный sidecar (в секции containers) стартует параллельно с основным контейнером. Это создаёт проблемы: нет гарантии, что sidecar запустится до основного контейнера; при shutdown sidecar может завершиться раньше основного; init-контейнеры не могут использовать sidecar (он ещё не запущен).

Native sidecar решает все эти проблемы. Определяется как init-контейнер с restartPolicy: Always:

yaml
apiVersion: v1 kind: Pod metadata: name: app-with-native-sidecar spec: initContainers: - name: init-config # обычный init container image: busybox:1.36 command: ['sh', '-c', 'echo "config loaded"'] - name: log-collector # native sidecar image: fluent/fluent-bit:latest restartPolicy: Always # <-- это делает его native sidecar volumeMounts: - name: logs mountPath: /var/log/app - name: check-dependencies # обычный init, стартует ПОСЛЕ sidecar image: busybox:1.36 command: ['sh', '-c', 'echo "deps ok"'] containers: - name: app # основной контейнер image: myregistry/my-go-app:1.0 ports: - containerPort: 8080 volumeMounts: - name: logs mountPath: /var/log/app volumes: - name: logs emptyDir: {}

Порядок запуска: init-config -> log-collector (native sidecar, не ждёт завершения) -> check-dependencies -> app. Порядок остановки: SIGTERM основным -> ждём завершения -> SIGTERM native sidecar-ам в обратном порядке. Sidecar гарантированно живёт дольше основных контейнеров.

Когда использовать native sidecar: sidecar критичен для работы pod-а (service mesh), init-контейнеры зависят от sidecar, sidecar должен пережить основные контейнеры (log collector), или batch Jobs где sidecar не должен блокировать завершение.


Pod Lifecycle: фазы

Жизненный цикл pod-а проходит три стадии: Initialization (init-контейнеры последовательно) -> Run (основные контейнеры параллельно) -> Termination (graceful shutdown).

text
+------------------+ +----------------------+ +-----------------+ | Initialization | | Run | | Termination | | | | | | | | Init 1 -> Init 2 |->| [Container A + B] |->| SIGTERM -> KILL | | -> ... -> Init N | | параллельно | | параллельно | | последовательно | | + probes + hooks | | для всех cont. | +------------------+ +----------------------+ +-----------------+

Pod Phases

PhaseОписание
PendingPod создан, ожидает scheduling, pull images, или init-контейнеры ещё работают
RunningХотя бы один контейнер запущен
SucceededВсе контейнеры завершились с exit code 0 (типично для Jobs)
FailedХотя бы один контейнер завершился с ненулевым exit code
UnknownKubelet не отвечает (нода упала или проблемы с сетью)

При запуске pod-а с init-контейнерами статус меняется так:

STATUSЧто происходит
PendingPod создан, ожидает scheduling
Init:0/2Первый init-контейнер запущен
Init:1/2Второй init-контейнер запущен
PodInitializingВсе init-ы завершились, pull основных images
RunningОсновные контейнеры работают

Container States

Каждый контейнер внутри pod-а имеет своё состояние:

  • Waiting — ожидает запуска (pull image, ожидание init). Reason: ContainerCreating, CrashLoopBackOff, ErrImagePull
  • Running — процесс запущен и работает
  • Terminated — процесс завершился. Содержит exitCode, reason, startedAt, finishedAt
bash
# Посмотреть состояние контейнеров в pod-е kubectl get pod my-app -o jsonpath='{.status.containerStatuses}' | jq .

Pod Conditions

Условия (conditions) pod-а описывают его состояние с разных сторон:

ConditionОписание
PodScheduledPod назначен на ноду
InitializedВсе init-контейнеры завершились успешно
ContainersReadyВсе контейнеры ready
ReadyPod готов обслуживать клиентов (ContainersReady + readiness gates)

Каждая condition содержит: type, status (True/False/Unknown), reason, message. Conditions PodScheduled и Initialized переходят в True и остаются; Ready и ContainersReady могут переключаться туда-сюда в процессе работы.


Probes: Liveness, Readiness, Startup

Probes — механизм, через который Kubelet проверяет состояние контейнеров. Без probes Kubernetes знает только одно: процесс упал (exit code != 0). Но процесс может зависнуть (deadlock, infinite loop, OOM без crash) — pod будет показывать Running, но не отвечать на запросы.

Liveness Probe

Отвечает на вопрос: "контейнер ещё жив и работает?"

Без liveness probe: приложение зависло, K8s не знает об этом, pod показывает Running. С liveness probe: K8s периодически проверяет здоровье; если probe fails failureThreshold раз подряд — контейнер перезапускается.

yaml
livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 # первая проверка через 10s после старта periodSeconds: 5 # проверять каждые 5s timeoutSeconds: 2 # ответ должен прийти за 2s failureThreshold: 3 # 3 фейла подряд -> restart successThreshold: 1 # 1 успех -> считать здоровым (default)

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

text
Container started | +-- 10s (initialDelaySeconds) | v Probe ok --- 5s --> Probe ok --- 5s --> Probe FAIL | 5s v Probe FAIL | 5s v Probe FAIL <-- 3 fails (failureThreshold) | v RESTART container

Startup Probe

Отвечает на вопрос: "контейнер уже запустился?"

Проблема: приложение стартует 2 минуты (JVM warmup, загрузка данных). Liveness probe с periodSeconds=5 и failureThreshold=3 считает приложение мёртвым через 15 секунд — перезапуск — бесконечный цикл.

Решение: startup probe. Пока startup probe не прошла успешно, liveness probe не запускается. Startup probe может иметь больший failureThreshold, давая приложению время на старт.

yaml
startupProbe: # фаза запуска httpGet: path: /healthz port: 8080 periodSeconds: 10 # проверять каждые 10s failureThreshold: 12 # до 120s на запуск (10 * 12) livenessProbe: # после запуска httpGet: path: /healthz port: 8080 periodSeconds: 5 # проверять каждые 5s failureThreshold: 2 # 2 фейла -> restart (быстрая реакция)
text
Startup probe (мягкая) Liveness probe (строгая) +---------------------+ +----------------------+ | period=10s | ok | period=5s | | failure=12 |-------->| failure=2 | | = до 120s на старт | | = быстрая реакция | +---------------------+ +----------------------+ Fail = норма (ещё старт) Fail = проблема -> restart

Readiness Probe

Отвечает на вопрос: "контейнер готов принимать трафик?"

Принципиальное отличие от liveness:

ProbeПри fail
LivenessКонтейнер перезапускается
ReadinessPod убирается из endpoints Service (перестаёт получать трафик), контейнер не перезапускается

Use cases для readiness probe:

  • Приложение загружает кэш при старте и ещё не готово
  • Приложение временно перегружено
  • Зависимость (база данных) временно недоступна

Конфигурация идентична liveness probe. Когда readiness снова проходит — pod возвращается в endpoints и начинает получать трафик.

Три типа probe

Каждая probe (liveness, readiness, startup) может использовать один из трёх методов проверки:

httpGet

yaml
livenessProbe: httpGet: path: /healthz port: 8080

HTTP GET запрос. Код ответа 2xx или 3xx — success, всё остальное — fail. Эндпоинт /healthz не должен требовать аутентификацию.

tcpSocket

yaml
livenessProbe: tcpSocket: port: 5432

TCP-подключение к указанному порту. Порт открыт — success, не открыт — fail. Для non-HTTP приложений: Redis, PostgreSQL, gRPC.

exec

yaml
livenessProbe: exec: command: ["/bin/healthcheck"]

Запуск команды внутри контейнера. Exit code 0 — success, всё остальное — fail. Для приложений без сети (batch processing). Учитывай, что exec запускает процесс внутри контейнера и потребляет его ресурсы.

Best Practices для Probes

Рекомендуется:

  • Определяй liveness probe для всех pod-ов
  • /healthz должен проверять только внутреннее здоровье приложения
  • Не проверяй внешние зависимости (БД, другие сервисы) в liveness probe — иначе: backend упал -> frontend restart -> каскадный отказ всей системы
  • Probe handler должен быть лёгким и быстрым
  • Используй failureThreshold вместо retry-логики внутри handler-а
  • Для Java: httpGet лучше exec (exec каждый раз запускает JVM-процесс)

Не рекомендуется:

  • Проверять connectivity к другим сервисам в liveness
  • Делать тяжёлые операции (запросы к БД, обработка данных) в probe handler

Lifecycle Hooks: postStart и preStop

Lifecycle hooks позволяют выполнить действия при создании и перед удалением контейнера. Доступны два типа: postStart и preStop. Оба поддерживают exec и httpGet (но не tcpSocket).

postStart Hook

Выполняется параллельно со стартом основного процесса — не "после старта", а "при старте".

yaml
lifecycle: postStart: exec: command: ["sh", "-c", "echo started > /tmp/started"]
yaml
lifecycle: postStart: httpGet: path: /warm-up port: 8080

Поведение: пока hook не завершится, контейнер в статусе Waiting; kubectl logs и port-forward не работают; fail hook приводит к рестарту контейнера; блокирует создание следующего контейнера в pod-е. Важно: httpGet postStart к своему контейнеру может вызвать бесконечный restart loop (приложение ещё не готово).

preStop Hook

Выполняется перед отправкой SIGTERM. Последовательность: preStop hook -> (завершился) -> SIGTERM -> (grace period) -> SIGKILL.

yaml
lifecycle: preStop: exec: command: ["nginx", "-s", "quit"] # graceful shutdown nginx
yaml
lifecycle: preStop: httpGet: path: /shutdown port: 8080
yaml
lifecycle: preStop: sleep: seconds: 5 # подождать 5s перед SIGTERM

Поведение: ошибка hook не предотвращает termination; время preStop входит в terminationGracePeriodSeconds; вызывается при liveness fail, pod delete, scale down; не вызывается когда процесс завершился сам.


Termination: graceful shutdown

Когда pod удаляется (kubectl delete, scale down, rolling update), запускается последовательность завершения:

text
kubectl delete pod <name> terminationGracePeriodSeconds (default = 30s) |-----------------------------------------------------| +----------+ +----------+ +----------+ |Container | |Container | |Container | | A | | B | | C | | | | | | | | preStop | | SIGTERM | | preStop | <-- параллельно для всех | hook | | | | | hook | | | | | process | | | | | SIGTERM | | exits | | SIGTERM | | | | | | | | | | process | | | | SIGKILL | <-- если не завершился за grace period | exits | | | | | +----------+ +----------+ +----------+ После всех основных контейнеров -> terminate native sidecars (обратный порядок)

terminationGracePeriodSeconds — по умолчанию 30 секунд. Этого достаточно для большинства приложений, но может потребоваться увеличение для сервисов с длинными запросами или потоковой обработкой.

SIGTERM и Dockerfile

Критически важно, как написан ENTRYPOINT в Dockerfile:

Shell form — SIGTERM НЕ доходит до приложения:

dockerfile
ENTRYPOINT /myapp arg1 arg2

Shell запускается как PID 1, myapp как дочерний процесс. SIGTERM отправляется shell-у, который его не передаёт дальше. Приложение не получает сигнал и будет убито через SIGKILL.

Exec form — SIGTERM доходит:

dockerfile
ENTRYPOINT ["/myapp", "arg1", "arg2"]

myapp запускается как PID 1 и получает SIGTERM напрямую.

Пример обработки SIGTERM в Go:

go
package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" ) func main() { srv := &http.Server{Addr: ":8080"} // Запускаем сервер в горутине go func() { if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("HTTP server error: %v", err) } }() // Ждём SIGTERM или SIGINT quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT) <-quit log.Println("Shutting down server...") // Graceful shutdown с таймаутом ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } log.Println("Server exited") }

Exit Codes

CodeЗначение
0Успешное завершение
1Ошибка приложения (generic)
137128 + 9 (SIGKILL) — контейнер убит принудительно (не уложился в grace period или OOMKilled)
143128 + 15 (SIGTERM) — контейнер завершился по SIGTERM

restartPolicy и exponential backoff

restartPolicy задаётся на уровне pod-а и применяется ко всем контейнерам:

PolicyПоведение
AlwaysПерезапуск при любом exit code (default)
OnFailureПерезапуск только при exit code != 0
NeverНикогда не перезапускать

Важно: "restart" в Kubernetes — это не перезапуск процесса, а пересоздание контейнера. Старый контейнер уничтожается, новый создаётся. Данные на файловой системе контейнера теряются (если нет volume).

Exponential Backoff

При повторных падениях Kubernetes увеличивает задержку перед перезапуском:

RestartЗадержка
1-й0s (сразу)
2-й10s
3-й20s
4-й40s
5-й80s
6-й160s
7+300s (cap, 5 минут)

Если контейнер проработал 10+ минут без падений — backoff сбрасывается до 0. Пока контейнер ожидает перезапуска, его статус — CrashLoopBackOff.


kubectl — команды для работы с pod-ами

КомандаЧто делает
kubectl get podsСписок pod-ов
kubectl get pods -wWatch — обновления в реальном времени
kubectl describe pod <name>Подробная информация + events
kubectl logs <pod>Логи контейнера (stdout/stderr)
kubectl logs <pod> -fСтриминг логов
kubectl logs <pod> -c <container>Логи конкретного контейнера
kubectl logs <pod> --previousЛоги предыдущего контейнера (после restart)
kubectl exec -it <pod> -- bashShell внутри контейнера
kubectl exec <pod> -c <cont> -- cmdКоманда в конкретном контейнере
kubectl port-forward <pod> 8080:8080Проксирование порта на localhost
kubectl cp <pod>:path localpathКопировать файл из контейнера
kubectl debug <pod> -it --image=nicolaka/netshootEphemeral debug container

Для distroless-контейнеров без debug-утилит: kubectl debug добавляет временный контейнер к существующему pod-у без пересоздания. С shareProcessNamespace: true debug-контейнер видит процессы всех контейнеров.

bash
kubectl debug my-app -it --image nicolaka/netshoot # netshoot содержит: tcpdump, curl, dig, strace, ip, ss...

Вопросы на собеседовании

Что такое Pod и почему это не просто контейнер?

Pod — минимальная единица деплоя в Kubernetes, которая может содержать один или несколько контейнеров. Контейнеры в pod-е разделяют сетевое пространство (один IP, один hostname, общаются через localhost), IPC namespace и могут шарить volumes. Pod нужен потому, что контейнер по дизайну — один процесс, а приложению часто нужны вспомогательные процессы (sidecar proxy, log collector), которые должны работать на одном хосте и иметь общую сеть. Kubernetes масштабирует pod-ы целиком, а не отдельные контейнеры.

В чём разница между liveness, readiness и startup probes?

Liveness probe проверяет, жив ли контейнер. При fail — контейнер перезапускается. Нужна для обнаружения deadlock-ов и зависаний, которые не приводят к crash-у процесса. Readiness probe проверяет, готов ли контейнер принимать трафик. При fail — pod убирается из endpoints Service (перестаёт получать запросы), но контейнер не перезапускается. Нужна при загрузке кэша, временной перегрузке, недоступности зависимости. Startup probe проверяет, завершился ли запуск приложения. Пока startup probe не прошла — liveness probe не запускается. Нужна для медленно стартующих приложений, чтобы liveness не убила контейнер, который ещё инициализируется.

Что такое init-контейнеры и чем они отличаются от обычных?

Init-контейнеры запускаются последовательно перед основными. Используются для ожидания зависимостей, миграции БД, загрузки конфигурации. После завершения init-контейнера его filesystem недоступен основным контейнерам. Native sidecar (K8s 1.28+) — init-контейнер с restartPolicy: Always, стартует в init-фазе, живёт всё время работы pod-а, завершается после основных контейнеров.

Что происходит при удалении pod-а?

Termination sequence: (1) preStop hook для каждого контейнера параллельно; (2) SIGTERM; (3) ожидание terminationGracePeriodSeconds (default 30s); (4) SIGKILL если не завершился. Время preStop входит в grace period. Native sidecar-ы завершаются после основных контейнеров. Важно: exec form в ENTRYPOINT (ENTRYPOINT ["/app"]) чтобы SIGTERM дошёл до приложения.

Что такое CrashLoopBackOff и как с ним разбираться?

CrashLoopBackOff — статус контейнера, который повторно падает с exponential backoff (0s, 10s, 20s...300s cap). Диагностика: kubectl logs --previous и kubectl describe pod. Частые причины: OOMKilled (exit 137), неверные env vars, недоступность зависимости, ошибка в ENTRYPOINT/CMD, panic при инициализации.

Почему не стоит проверять зависимости (БД) в liveness probe?

При недоступности БД все pod-ы начнут перезапускаться одновременно — каскадный отказ. После восстановления — thundering herd. Liveness должна проверять только внутреннее здоровье (deadlock, процесс отвечает). Для зависимостей — readiness probe: pod убирается из endpoints, но не перезапускается.

В чём разница между restartPolicy: Always, OnFailure и Never?

Always (default) — перезапуск при любом exit code, для long-running сервисов. OnFailure — перезапуск только при exit code != 0, для Jobs. Never — без перезапуска, для одноразовых задач. Применяется ко всем контейнерам в pod-е. "Restart" — это пересоздание контейнера (данные на filesystem теряются без volume).


Задачи

Задача 1. Проектирование pod-а с init-контейнерами

Уровень: Средняя

Что проверяет: понимание init-контейнеров, shared volumes, порядка запуска

Условие: Напиши манифест pod-а для Go-приложения, которое перед стартом должно: (1) дождаться доступности Redis на redis-svc:6379, (2) загрузить конфигурацию из URL http://config-server/api/config в файл /etc/app/config.json. Основное приложение читает конфигурацию из этого файла.

Решение:

yaml
apiVersion: v1 kind: Pod metadata: name: my-go-app labels: app: my-go-app spec: initContainers: - name: wait-for-redis image: busybox:1.36 command: ['sh', '-c', 'until nc -z redis-svc 6379; do echo "waiting for redis..."; sleep 2; done; echo "redis is ready"'] - name: load-config image: curlimages/curl:8.5.0 command: ['sh', '-c', 'curl -f -o /config/config.json http://config-server/api/config'] volumeMounts: - name: app-config mountPath: /config containers: - name: app image: myregistry/my-go-app:1.0 ports: - containerPort: 8080 volumeMounts: - name: app-config mountPath: /etc/app readOnly: true livenessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /readyz port: 8080 periodSeconds: 5 volumes: - name: app-config emptyDir: {}

Порядок: wait-for-redis (ждёт TCP-подключения к Redis) -> load-config (скачивает JSON в shared volume) -> app (читает конфиг из /etc/app/config.json). Shared volume app-config связывает init-контейнер и основной контейнер.

Задача 2. Настройка probes для Go-сервиса

Уровень: Средняя

Что проверяет: выбор типов probes и правильная конфигурация

Условие: У тебя есть Go HTTP-сервис, который при старте загружает данные из PostgreSQL в in-memory кэш (занимает до 60 секунд). После загрузки сервис отвечает на /healthz и /readyz. Напиши секцию probes в манифесте, учитывая: (1) медленный старт, (2) быструю реакцию на зависание, (3) временную недоступность не должна приводить к рестарту.

Решение:

yaml
containers: - name: cache-service image: myregistry/cache-service:1.0 ports: - containerPort: 8080 startupProbe: httpGet: path: /healthz port: 8080 periodSeconds: 5 failureThreshold: 15 # до 75 секунд на старт (5 * 15) livenessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 5 failureThreshold: 2 # 2 фейла (10s) -> restart timeoutSeconds: 2 readinessProbe: httpGet: path: /readyz port: 8080 periodSeconds: 3 failureThreshold: 2 successThreshold: 2 # 2 успеха подряд, чтобы вернуть в endpoints timeoutSeconds: 2

startupProbe даёт до 75 секунд на загрузку кэша — liveness не запускается пока старт не завершён. livenessProbe с failureThreshold: 2 быстро обнаруживает зависание (10 секунд). readinessProbe на отдельном эндпоинте /readyz — если кэш перезагружается или сервис временно перегружен, pod убирается из endpoints, но не перезапускается. successThreshold: 2 предотвращает flapping (попеременное добавление/удаление из endpoints).

Задача 3. Диагностика CrashLoopBackOff

Уровень: Средняя

Что проверяет: практические навыки отладки pod-ов

Условие: После деплоя Go-приложения pod показывает статус CrashLoopBackOff. Опиши пошагово процесс диагностики: какие команды kubectl ты выполнишь и что будешь искать в выводе каждой команды. Назови 5 возможных причин CrashLoopBackOff.

Ответ:

Шаги диагностики:

bash
# 1. Смотрим текущий статус и количество рестартов kubectl get pod <name> -o wide # 2. Смотрим events — причины рестартов, ошибки scheduling kubectl describe pod <name> # Ищем: exit code, reason (OOMKilled, Error), events (FailedMount, FailedScheduling) # 3. Логи текущего (падающего) контейнера kubectl logs <name> # Ищем: panic, fatal error, ошибки подключения, невалидный конфиг # 4. Логи предыдущего контейнера (до рестарта) kubectl logs <name> --previous # Часто текущий контейнер ещё не успел ничего записать # 5. Проверяем exit code kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].lastState}' # 137 = OOMKilled, 1 = ошибка приложения, 2 = bash error

Пять возможных причин: (1) OOMKilled (exit code 137) — приложение потребляет больше памяти, чем указано в limits; (2) неверная конфигурация — переменные окружения отсутствуют или содержат неправильные значения; (3) недоступность зависимости — приложение падает при невозможности подключиться к БД (нужны retry и init-контейнеры); (4) ошибка в ENTRYPOINT/CMD — бинарник не найден или не является исполняемым; (5) panic в приложении — необработанная ошибка при инициализации (nil pointer dereference, отсутствующий файл).


Интерактивная практика

Quiz+10 XP

Что происходит, когда readiness probe начинает падать, но liveness probe остаётся успешной?

  • Kubernetes немедленно перезапускает контейнер
  • Pod убирается из endpoints Service, но контейнер продолжает работать
  • Pod удаляется навсегда и больше не создаётся
  • Scheduler переносит pod на другую ноду
Predict+15 XP

Что выведет этот код?

go
package main import "fmt" func probeAction(probe string) string { switch probe { case "liveness": return "restart" case "readiness": return "remove-from-service" default: return "wait-startup" } } func main() { fmt.Println(probeAction("liveness")) fmt.Println(probeAction("readiness")) fmt.Println(probeAction("startup")) }
Задача+20 XP

Реализуй ExitReason: код 137 означает OOMKilled, код 1 — обычную ошибку приложения, остальные причины пока считай неизвестными.