Модуль: DevOps · Уровень: Middle+/Senior
TL;DR#
- Pod — минимальная единица деплоя (1+ контейнеров с общими network namespace и volumes). Поды эфемерны и заменяемы.
- Deployment управляет ReplicaSet’ами, обеспечивает rolling update и self-healing. Service даёт стабильный виртуальный IP/DNS и L4-балансировку поверх меняющихся подов. Ingress — L7-роутинг (host/path) снаружи.
- ConfigMap — некритичная конфигурация, Secret — чувствительные данные (base64, не шифрование по умолчанию). Монтируются как env или файлы.
- Probes: liveness (перезапуск зависшего), readiness (убрать из балансировки, не убивая), startup (для медленного старта, отключает остальные пока не пройдёт).
- requests — гарантированный минимум для планировщика, limits — потолок. CPU limit троттлит, memory limit → OOMKill.
- Главная Go-проблема: GOMAXPROCS не видит cgroup-лимит CPU — по умолчанию берёт число ядер ноды, что вызывает throttling. Решение:
automaxprocsили Go 1.25+ (cgroup-aware runtime). АналогичноGOMEMLIMITпод memory limit. - Graceful shutdown: SIGTERM → дренаж readiness → дослужить запросы → exit до истечения
terminationGracePeriodSeconds.preStopхук для синхронизации с балансировщиком.
Теория#
Базовые объекты#
Pod — один или несколько контейнеров, разделяющих сетевой namespace (один IP, общий localhost) и volumes. В norm-практике 1 контейнер на под + sidecar’ы (логи, прокси). Под не переживает перезапуск ноды — его пересоздаёт контроллер.
ReplicaSet — поддерживает N идентичных реплик. Напрямую почти не создают.
Deployment — декларативное управление ReplicaSet’ами: rolling update, rollback, масштабирование, self-healing.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: registry/api:1.4.2
ports: [{ containerPort: 8080 }]
env:
- name: GOMEMLIMIT
valueFrom:
resourceFieldRef: { resource: limits.memory }
resources:
requests: { cpu: "250m", memory: "128Mi" }
limits: { cpu: "1", memory: "256Mi" }
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 2
lifecycle:
preStop:
exec: { command: ["sleep", "5"] }Service — стабильная точка доступа поверх подов (по label selector), L4-балансировка через kube-proxy/iptables/IPVS. Типы:
ClusterIP— внутренний виртуальный IP (дефолт).NodePort— порт на каждой ноде.LoadBalancer— внешний LB облака.Headless(clusterIP: None) — DNS отдаёт IP всех подов напрямую (для StatefulSet, клиентской балансировки, gRPC).
Ingress — L7 (HTTP/HTTPS) роутинг по host/path, TLS-терминация. Нужен Ingress Controller (nginx, traefik). Современная замена — Gateway API.
ConfigMap и Secret#
apiVersion: v1
kind: ConfigMap
metadata: { name: app-config }
data:
LOG_LEVEL: "info"
config.yaml: |
timeout: 30s
---
apiVersion: v1
kind: Secret
metadata: { name: app-secret }
type: Opaque
data:
DB_PASSWORD: c2VjcmV0 # base64, НЕ шифрованиеПодключение:
envFrom:
- configMapRef: { name: app-config }
- secretRef: { name: app-secret }
volumeMounts:
- { name: cfg, mountPath: /etc/app, readOnly: true }- Secret в etcd по умолчанию только base64, не шифрование. Включайте encryption-at-rest для etcd и/или внешние хранилища (Vault, External Secrets Operator, cloud KMS).
- env vs volume: значения через
envфиксируются на старте пода — обновление ConfigMap не подхватится без рестарта. Смонтированные файлы обновляются автоматически (с задержкой), но приложение должно их перечитывать (watch/SIGHUP).
Probes#
| Probe | Что делает при провале | Зачем |
|---|---|---|
| liveness | перезапускает контейнер | вытащить из дедлока/зависания |
| readiness | убирает под из endpoints Service (трафик не идёт), под живёт | временная неготовность: прогрев кэша, потеря БД, дренаж при shutdown |
| startup | не убивает, пока не превышен failureThreshold; до прохождения liveness/readiness не запускаются | защита медленно стартующих приложений от преждевременного liveness-kill |
Критично разделять /healthz (liveness — процесс жив) и /readyz (readiness — готов принимать трафик). Ошибка: liveness, проверяющий БД — при кратком сбое БД k8s начнёт каскадно рестартовать все поды (crash loop), хотя приложение в порядке. Зависимости проверяйте в readiness.
requests / limits и QoS#
- requests — то, что планировщик резервирует для пода; используется для bin-packing на ноды.
- limits — жёсткий потолок. CPU limit → throttling (CFS quota, под не убивают, но тормозят). Memory limit → OOMKill при превышении.
- QoS-классы:
Guaranteed(requests==limits для всех ресурсов),Burstable(есть requests < limits),BestEffort(ничего не задано). При нехватке памяти на ноде первыми эвиктятся BestEffort, потом Burstable.
Споры по CPU limit: многие убирают CPU limit (оставляя только request), чтобы избежать throttling, но это снижает изоляцию. Memory limit ставить обязательно.
HPA#
Horizontal Pod Autoscaler масштабирует реплики по метрикам.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: api }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: api }
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }- Целевой % считается от requests, не от limits. Поэтому корректный CPU request критичен для HPA.
- Нужен metrics-server (для resource-метрик) или Prometheus Adapter / KEDA (для custom-метрик: RPS, длина очереди).
- GOMAXPROCS-проблема прямо бьёт по HPA: если рантайм троттлится из-за неверного GOMAXPROCS, метрики CPU искажаются и автоскейл работает неправильно.
Rolling update#
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # не уменьшать доступную ёмкость
maxSurge: 1 # можно поднять +1 под сверх replicasПоды заменяются по одному: поднимается новый, ждём readiness, гасим старый. maxUnavailable: 0 + maxSurge гарантирует отсутствие просадки ёмкости. Откат: kubectl rollout undo deployment/api.
Graceful shutdown (важнейшее для backend на Go)#
Последовательность при удалении пода:
- Под помечается
Terminating, удаляется из endpoints Service (асинхронно). - Выполняется
preStopхук (если есть). - Контейнеру шлётся SIGTERM.
- Ждём до
terminationGracePeriodSeconds(дефолт 30s). - Если жив — SIGKILL.
Гонка: удаление из endpoints и SIGTERM происходят параллельно. Балансировщик может ещё слать трафик в под, который уже получил SIGTERM. Решение — preStop: sleep 5–10s: даём времени iptables/endpoint-контроллеру обновиться ДО того, как начнём останавливать сервер.
Go-реализация:
func main() {
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop()
<-ctx.Done() // получили SIGTERM
// снимаем readiness, чтобы новый трафик не шёл
atomic.StoreInt32(&ready, 0)
// дослуживаем активные запросы
shCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
if err := srv.Shutdown(shCtx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
}
}Важно: srv.Shutdown дожидается завершения in-flight запросов, но не прерывает долгие. Таймаут shutdown должен быть < terminationGracePeriodSeconds, иначе SIGKILL прервёт на середине.
GOMAXPROCS, GOMEMLIMIT и cgroups#
Проблема: Go runtime по умолчанию ставит GOMAXPROCS = число логических CPU ноды (через /proc), игнорируя cgroup CPU quota. На ноде с 64 ядрами и лимитом limits.cpu: 2 Go создаст 64 P, runtime-scheduler будет раскидывать горутины на 64 потока, CFS начнёт жёстко throttle’ить → высокая latency, скачки p99, неэффективный GC.
Решения:
go.uber.org/automaxprocs—import _ "go.uber.org/automaxprocs", читает cgroup quota и выставляет GOMAXPROCS = floor(quota).- Go 1.25+ — рантайм стал cgroup-aware и сам учитывает CPU quota (но проверяйте версию; automaxprocs остаётся надёжным фолбэком).
- Вручную:
env GOMAXPROCSиз downward API.
GOMEMLIMIT (Go 1.19+) — soft memory limit для GC. Без него Go может вырасти до memory limit и получить OOMKill раньше, чем GC решит собрать мусор. Выставляйте GOMEMLIMIT в ~80–90% от limits.memory:
env:
- name: GOMEMLIMIT
value: "230MiB" # при limits.memory: 256MiGOMEMLIMIT — soft limit: GC станет агрессивнее у границы, но не гарантирует, что процесс не превысит лимит (может крутить GC в死, “GC death spiral”, если памяти реально не хватает). Комбинируйте с реалистичным memory limit.
Подводные камни / gotchas#
- Liveness проверяет внешние зависимости → каскадные рестарты при сбое БД. Зависимости — только в readiness.
- Нет preStop sleep → 502/connection reset во время деплоя из-за гонки endpoints vs SIGTERM.
- GOMAXPROCS = ядра ноды → CPU throttling, рост p99. Ставьте automaxprocs.
- Нет GOMEMLIMIT → OOMKill вместо GC. Симптом: под рестартует с reason
OOMKilled, exit code 137. - env из ConfigMap не обновляется → меняете ConfigMap, под не видит до рестарта. Используйте volume + watch или сделайте rollout.
- CPU limit ставит throttling даже без превышения среднего — CFS quota применяется в окнах 100ms, burst-нагрузка троттлится. Многие убирают CPU limit.
- HPA не масштабирует → не выставлен resource request (HPA считает % от request) или нет metrics-server.
- Shutdown timeout > terminationGracePeriodSeconds → SIGKILL обрывает graceful shutdown. Держите запас.
- Secret в git как plain YAML → утечка. Используйте SOPS/sealed-secrets/External Secrets.
imagePullPolicy: Alwaysс тегомlatestvs кэш — невоспроизводимость. Пинуйте теги/digest.- Под не реагирует на SIGTERM — приложение запущено через shell-обёртку (не PID 1) или не ловит сигнал. Exec-форма ENTRYPOINT + signal.NotifyContext.
Вопросы на собеседовании#
В: Разница между liveness и readiness probe? О: Liveness при провале перезапускает контейнер (лечит зависание/дедлок). Readiness при провале убирает под из endpoints Service — трафик не идёт, но под не убивают (временная неготовность, прогрев, дренаж). Внешние зависимости проверяют только в readiness, иначе сбой БД вызовет каскадные рестарты.
В: Что происходит при kubectl delete pod или rolling update, на уровне сигналов?
О: Под → Terminating, параллельно удаляется из endpoints; выполняется preStop; контейнер получает SIGTERM; ждём terminationGracePeriodSeconds; если жив — SIGKILL. Приложение должно по SIGTERM снять readiness и сделать graceful shutdown активных запросов до истечения grace period.
В: Почему Go-сервис в k8s тормозит, хотя CPU не утилизирован полностью? О: GOMAXPROCS взят по числу ядер ноды, а не по cgroup CPU quota. Runtime раскидывает горутины на много потоков, CFS троттлит до квоты → латентность растёт. Лечится automaxprocs или Go 1.25+ cgroup-aware рантаймом.
В: В чём разница requests и limits, и что такое QoS? О: requests — резерв для планировщика (bin-packing, основа для HPA). limits — потолок: CPU → throttling, memory → OOMKill. QoS: Guaranteed (requests==limits), Burstable (requests<limits), BestEffort (ничего). При нехватке памяти эвиктятся сначала BestEffort.
В: Как избежать OOMKill у Go-сервиса? О: Выставить GOMEMLIMIT (~80–90% от limits.memory), чтобы GC становился агрессивнее у границы и не давал процессу дорасти до лимита. Плюс реалистичный memory limit и профилирование heap.
В: Зачем preStop хук с sleep? О: Из-за гонки: удаление пода из endpoints и доставка SIGTERM происходят асинхронно. preStop sleep даёт балансировщику время перестать слать трафик до начала остановки сервера, исключая 502/RST во время деплоя.
В: ConfigMap через env vs через volume? О: env фиксируется на старте — обновление ConfigMap требует рестарта пода. Volume-файлы обновляются автоматически (с задержкой), но приложение должно их перечитывать. Для динамической перезагрузки — volume + fsnotify/SIGHUP.
В: Как работает HPA и от чего считается целевая нагрузка? О: HPA масштабирует реплики по метрикам; для CPU считает Utilization как % от requests (не limits). Нужен metrics-server (resource-метрики) или Prometheus Adapter/KEDA (custom: RPS, длина очереди). Без корректного request HPA работает неправильно.
В: Чем Service отличается от Ingress? О: Service — L4 (TCP/UDP), стабильный ClusterIP + балансировка по подам через kube-proxy. Ingress — L7 (HTTP), роутинг по host/path и TLS-терминация, требует Ingress Controller. Service для внутренней связности, Ingress для внешнего HTTP-входа.
На что копают на senior+#
- Глубокое понимание cgroups v1/v2 и как Go runtime взаимодействует с CFS quota; что именно делает automaxprocs и почему дробный CPU limit (например 1.5) округляется вниз.
- Endpoint propagation delay: как именно EndpointSlice-контроллер и kube-proxy обновляют iptables, почему preStop sleep — это про устранение гонки, а не “магическая задержка”.
- GC tuning под контейнер: связка GOMEMLIMIT + GOGC, риск GC death spiral, soft vs hard memory limit, как читать
runtime/metricsи профилировать в проде (pprof endpoint, continuous profiling). - PodDisruptionBudget для защиты от одновременной потери реплик при drain ноды/upgrade; как он взаимодействует с rolling update.
- Topology spread / anti-affinity для распределения реплик по зонам и нодам.
- StatefulSet vs Deployment: стабильные сетевые идентичности и ordered shutdown для stateful Go-сервисов (например с локальным кэшем/leader election).
- Leader election (через Lease API / client-go leaderelection) для синглтон-воркеров в нескольких репликах.
- Service mesh (Envoy/Istio sidecar) и нюанс порядка запуска/остановки sidecar относительно приложения при graceful shutdown.
- Resource overcommit и noisy neighbors: как requests/limits и QoS влияют на стабильность p99 под нагрузкой соседей по ноде.