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 |
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, независимые микросервисы).
Антипаттерн: Правильно:
+---------------------+ +----------+ +----------+
| Frontend Backend | | Frontend | | Backend |
| container container | | container| | container|
| Pod | | Pod | | Pod |
+---------------------+ +----------+ +----------+
Нельзя скейлить Скейлятся независимо:
по отдельности 3x frontend, 1x backend
Kubernetes масштабирует pod-ы целиком, а не отдельные контейнеры внутри pod-а. Если компоненты масштабируются по-разному — это разные pod-ы.
Минимальный манифест пода
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 proxy | Envoy/Nginx перед HTTP-приложением, терминирует TLS |
| Log collector | Читает логи из shared volume, шлёт в central logging |
| Content syncer | Скачивает контент в shared volume для web-сервера |
| Auth proxy | OAuth2 proxy перед приложением |
| Monitoring agent | Prometheus exporter, сбор метрик |
+--------------------------------+
| Node.js Envoy proxy |
| :8080 (HTTP) :8443 (HTTPS) |
| <---- localhost ----> |
| Pod |
+--------------------------------+
Envoy принимает HTTPS, проксирует HTTP на Node.js
Init Containers
Init-контейнеры запускаются последовательно перед основными контейнерами. Каждый следующий стартует только после успешного завершения предыдущего. Основные контейнеры стартуют только после всех init-ов.
Init 1 --> Init 2 --> Init 3 --> [Main A + Main B] (параллельно)
| | | |
завершился завершился завершился работают
Правила:
- Запускаются последовательно, не параллельно
- Каждый должен завершиться успешно (exit code 0)
- Если init-контейнер упал — pod перезапускает его (согласно restartPolicy)
- Основные контейнеры стартуют только после всех init-ов
- Имена контейнеров уникальны в пределах pod-а (init + regular вместе)
Типичные use cases:
- Загрузка конфигурации / сертификатов — скачать из vault, записать в shared volume
- Ожидание зависимости — curl/ping до тех пор, пока база данных или другой сервис не станет доступен
- Миграция БД — запустить миграции перед стартом приложения
- Инициализация сети — настройка iptables, маршрутов (Istio sidecar injection)
- Уведомление внешней системы — "pod скоро запустится"
Безопасность: после завершения init-контейнера его filesystem недоступен основным контейнерам — это уменьшает attack surface.
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:
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).
+------------------+ +----------------------+ +-----------------+
| Initialization | | Run | | Termination |
| | | | | |
| Init 1 -> Init 2 |->| [Container A + B] |->| SIGTERM -> KILL |
| -> ... -> Init N | | параллельно | | параллельно |
| последовательно | | + probes + hooks | | для всех cont. |
+------------------+ +----------------------+ +-----------------+
Pod Phases
| Phase | Описание |
|---|---|
| Pending | Pod создан, ожидает scheduling, pull images, или init-контейнеры ещё работают |
| Running | Хотя бы один контейнер запущен |
| Succeeded | Все контейнеры завершились с exit code 0 (типично для Jobs) |
| Failed | Хотя бы один контейнер завершился с ненулевым exit code |
| Unknown | Kubelet не отвечает (нода упала или проблемы с сетью) |
При запуске pod-а с init-контейнерами статус меняется так:
| STATUS | Что происходит |
|---|---|
| Pending | Pod создан, ожидает 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
# Посмотреть состояние контейнеров в pod-е
kubectl get pod my-app -o jsonpath='{.status.containerStatuses}' | jq .
Pod Conditions
Условия (conditions) pod-а описывают его состояние с разных сторон:
| Condition | Описание |
|---|---|
| PodScheduled | Pod назначен на ноду |
| Initialized | Все init-контейнеры завершились успешно |
| ContainersReady | Все контейнеры ready |
| Ready | Pod готов обслуживать клиентов (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 раз подряд — контейнер перезапускается.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10 # первая проверка через 10s после старта
periodSeconds: 5 # проверять каждые 5s
timeoutSeconds: 2 # ответ должен прийти за 2s
failureThreshold: 3 # 3 фейла подряд -> restart
successThreshold: 1 # 1 успех -> считать здоровым (default)
Временная шкала:
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, давая приложению время на старт.
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 (быстрая реакция)
Startup probe (мягкая) Liveness probe (строгая)
+---------------------+ +----------------------+
| period=10s | ok | period=5s |
| failure=12 |-------->| failure=2 |
| = до 120s на старт | | = быстрая реакция |
+---------------------+ +----------------------+
Fail = норма (ещё старт) Fail = проблема -> restart
Readiness Probe
Отвечает на вопрос: "контейнер готов принимать трафик?"
Принципиальное отличие от liveness:
| Probe | При fail |
|---|---|
| Liveness | Контейнер перезапускается |
| Readiness | Pod убирается из endpoints Service (перестаёт получать трафик), контейнер не перезапускается |
Use cases для readiness probe:
- Приложение загружает кэш при старте и ещё не готово
- Приложение временно перегружено
- Зависимость (база данных) временно недоступна
Конфигурация идентична liveness probe. Когда readiness снова проходит — pod возвращается в endpoints и начинает получать трафик.
Три типа probe
Каждая probe (liveness, readiness, startup) может использовать один из трёх методов проверки:
httpGet
livenessProbe:
httpGet:
path: /healthz
port: 8080
HTTP GET запрос. Код ответа 2xx или 3xx — success, всё остальное — fail. Эндпоинт /healthz не должен требовать аутентификацию.
tcpSocket
livenessProbe:
tcpSocket:
port: 5432
TCP-подключение к указанному порту. Порт открыт — success, не открыт — fail. Для non-HTTP приложений: Redis, PostgreSQL, gRPC.
exec
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
Выполняется параллельно со стартом основного процесса — не "после старта", а "при старте".
lifecycle:
postStart:
exec:
command: ["sh", "-c", "echo started > /tmp/started"]
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.
lifecycle:
preStop:
exec:
command: ["nginx", "-s", "quit"] # graceful shutdown nginx
lifecycle:
preStop:
httpGet:
path: /shutdown
port: 8080
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), запускается последовательность завершения:
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 НЕ доходит до приложения:
ENTRYPOINT /myapp arg1 arg2
Shell запускается как PID 1, myapp как дочерний процесс. SIGTERM отправляется shell-у, который его не передаёт дальше. Приложение не получает сигнал и будет убито через SIGKILL.
Exec form — SIGTERM доходит:
ENTRYPOINT ["/myapp", "arg1", "arg2"]
myapp запускается как PID 1 и получает SIGTERM напрямую.
Пример обработки SIGTERM в 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) |
| 137 | 128 + 9 (SIGKILL) — контейнер убит принудительно (не уложился в grace period или OOMKilled) |
| 143 | 128 + 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 -w | Watch — обновления в реальном времени |
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> -- bash | Shell внутри контейнера |
kubectl exec <pod> -c <cont> -- cmd | Команда в конкретном контейнере |
kubectl port-forward <pod> 8080:8080 | Проксирование порта на localhost |
kubectl cp <pod>:path localpath | Копировать файл из контейнера |
kubectl debug <pod> -it --image=nicolaka/netshoot | Ephemeral debug container |
Для distroless-контейнеров без debug-утилит: kubectl debug добавляет временный контейнер к существующему pod-у без пересоздания. С shareProcessNamespace: true debug-контейнер видит процессы всех контейнеров.
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. Основное приложение читает конфигурацию из этого файла.
Решение:
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) временную недоступность не должна приводить к рестарту.
Решение:
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.
Ответ:
Шаги диагностики:
# 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, отсутствующий файл).
Интерактивная практика
Что происходит, когда readiness probe начинает падать, но liveness probe остаётся успешной?
Что выведет этот код?
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"))
}
Реализуй ExitReason: код 137 означает OOMKilled, код 1 — обычную ошибку приложения, остальные причины пока считай неизвестными.