Kubernetes-native reliability: SRE-практики внутри кластера

В главе «Инфраструктура как код» мы говорили про декларативное управление инфраструктурой. Kubernetes — это, по сути, фреймворк для декларативного управления подами, и в нём заложено много встроенных механизмов для надёжности. Грех ими не пользоваться. Эта глава — про то, какие именно.

Почему это отдельная тема

Kubernetes часто упоминается в книге: «ну, в K8s это», «в кластере K8s», «при деплое в K8s». Но свода SRE-практик именно для Kubernetes нет. А они свои, специфические: readiness probes, PodDisruptionBudget, topology spread, HPA, resource requests — это инструменты, которых нет в обычных VM-деплоях.

Если вы запускаете продакшн в Kubernetes, вы должны знать эти механизмы. Они дают бесплатную надёжность, которую не надо программировать.

Probes: liveness, readiness, startup

Probes — это проверки, по которым Kubernetes решает, готов ли под принимать трафик и жив ли он вообще.

Liveness probe

Что делает: проверяет, не завис ли процесс. Если проба падает N раз подряд — Kubernetes убивает контейнер и перезапускает.

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

Типичные ошибки: - Слишком жёсткая проверка (например, проверка внешних зависимостей) → под рестартится из-за проблем с базой - Слишком мягкая проверка (всегда возвращает 200) → реально зависший процесс не перезапускается - Нет livenessProbe вовсе → зависший под жрёт ресурсы до ручного вмешательства

Что должно проверять: что процесс вообще жив и отвечает. Не бизнес-логику. Не внешние зависимости.

Readiness probe

Что делает: проверяет, готов ли под принимать трафик. Если проба падает — под убирается из Service (балансировки), но не убивается.

readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  periodSeconds: 5
  failureThreshold: 2

Что должно проверять: что под может обслуживать запросы. Можно проверять подключение к базе, кэшу, брокеру. Если что-то из этого недоступно — лучше убрать под из балансировки, чем возвращать 5xx.

Startup probe

Что делает: даёт медленно стартующим приложениям время на инициализацию, прежде чем начнут работать liveness/readiness проверки.

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 0
  periodSeconds: 10
  failureThreshold: 30  # 5 минут на старт

Полезно для JVM-приложений с долгим warm-up или для приложений, загружающих модели в память при старте.

Resource requests и limits

Без requests/limits Kubernetes не знает, сколько ресурсов нужно поду, и планировщик принимает плохие решения.

Requests — гарантированный минимум

resources:
  requests:
    cpu: 100m      # 0.1 CPU
    memory: 256Mi

Что это даёт: планировщик ставит под только на ноду, где есть свободные 100m CPU и 256Mi памяти. Под получит минимум столько.

Limits — максимум

resources:
  limits:
    cpu: 500m      # 0.5 CPU
    memory: 512Mi

Что это даёт: контейнер не съест больше. По памяти — OOMKill при превышении. По CPU — throttling.

Best practices

  • Requests и limits должны быть установлены для всех контейнеров. Без них — под может сожрать всю ноду.
  • Requests < Limits. Если requests == limits, под получит класс QoS Guaranteed. Если requests не установлен — Burstable. Если requests = 0 — BestEffort (первый кандидат на вытеснение).
  • Не завышайте requests. Лучше начать с реальных значений (по VPA-рекомендациям), чем просить 4 CPU «на всякий случай».
  • Не занижайте memory limit. Память не сжимается, OOM приведёт к рестарту пода.

Quality of Service

Класс Условие Поведение при нехватке ресурсов
Guaranteed requests == limits для всех контейнеров Убивается последним
Burstable requests < limits или установлен только requests Убивается при сильной нехватке
BestEffort Нет requests и нет limits Убивается первым

Для критичных сервисов — Guaranteed. Для второстепенных — Burstable.

PodDisruptionBudget: защита от вытеснения

PodDisruptionBudget (PDB) — это обещание Kubernetes: даже во время добровольных disruption-ов (нод drain, деплой, autoscale) останется минимум N подов.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: payment-api
spec:
  minAvailable: 2        # минимум 2 пода всегда доступны
  selector:
    matchLabels:
      app: payment-api

Или в процентах:

spec:
  minAvailable: 75%     # минимум 75% подов

Когда срабатывает: - kubectl drain (обслуживание ноды) - Обновление кластера - HPA scale down - kubectl delete pod (если через deployment)

Когда НЕ защищает: - Случайный OOM kill - Авария ноды (Node failure) - Принудительное удаление (force delete)

Типичные ошибки: - minAvailable: 100% — Kubernetes не сможет сделать drain, застрянет - Нет PDB вообще — Kubernetes может убить все поды одновременно

HPA, VPA, Cluster Autoscaler

HorizontalPodAutoscaler (HPA)

Масштабирует количество подов по метрикам.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: payment-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payment-api
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "1k"

Что подавать на вход: CPU, memory, custom metrics (RPS, queue depth, latency).

VerticalPodAutoscaler (VPA)

Масштабирует ресурсы контейнеров (requests/limits), а не количество подов.

Когда использовать: - Stateful приложения, которые сложно горизонтально масштабировать - Поиск оптимальных resource requests

Что НЕ надо: использовать VPA одновременно с HPA на одних и тех же метриках. Они будут конфликтовать.

Cluster Autoscaler

Масштабирует ноды кластера (добавляет/убирает EC2-инстансы в AWS, ВМ в Yandex Cloud и т.п.).

Триггеры: поды в Pending из-за нехватки ресурсов (scale up), ноды подолгу пустые (scale down).

Topology spread constraints

Ограничивает, как поды распределяются по нодам/зонам. Защита от того, чтобы все поды оказались на одной ноде или в одной зоне.

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: payment-api

Что даёт: если у вас 3 зоны и 6 подов, будет по 2 пода в каждой зоне. Падение одной зоны оставит 4 пода доступными.

maxSkew: максимальная разница между зонами. 1 = строго равномерно. 2 = допускается разница в один под.

whenUnsatisfiable: DoNotSchedule: если правило нельзя выполнить, под не будет создан. Альтернатива — ScheduleAnyway (попытаться, но не гарантировать).

PodDisruptionConditions и graceful shutdown

Когда под получает SIGTERM (например, при drain), у него есть terminationGracePeriodSeconds (по умолчанию 30 секунд), чтобы завершить текущие запросы.

Что должно приложение: 1. Получить SIGTERM 2. Перестать принимать новые запросы (readiness probe начнёт падать → Service уберёт под) 3. Дождаться завершения текущих запросов (или прервать их, если есть deadline) 4. Закрыть соединения с базой 5. Завершиться

Best practices: - Время graceful shutdown должно быть меньше terminationGracePeriodSeconds - Readiness probe должен быть настроен так, чтобы быстро убирать под из балансировки (periodSeconds: 1-2) - PreStop hook может помочь: sleep 5 — даём балансировщику время среагировать на изменение readiness

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 60

Anti-affinity

Запрещает ставить поды с одинаковыми labels на одну ноду или в одну зону.

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: payment-api
          topologyKey: kubernetes.io/hostname

Разница с topology spread: - Anti-affinity — запрещает соседство (hard или soft) - Topology spread — контролирует балансировку (численное распределение)

Network Policies

По умолчанию все поды в кластере могут общаться друг с другом. Это удобно для разработки, опасно для прода.

NetworkPolicy ограничивает сетевой трафик между подами:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payment-api
spec:
  podSelector:
    matchLabels:
      app: payment-api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - port: 5432
    - to:
        - namespaceSelector:
            matchLabels:
              name: kube-system
      ports:
        - port: 53
        - protocol: UDP

Default deny: хорошая практика — сначала запретить всё, потом разрешать только нужное.

Замечание: NetworkPolicy работает только если CNI это поддерживает. Calico, Cilium — да. Flannel — нет.

Persistent Volumes и state

Если у вас stateful приложения (базы данных, очереди с persistent storage), нужно правильно настроить StorageClass, PersistentVolume, PersistentVolumeClaim.

Ключевые практики: - Использовать ReadWriteOnce для одного пода, ReadWriteMany для shared storage - Не хранить данные на ноде напрямую — только через PV - Регулярные снапшоты (VolumeSnapshot API) - Тестировать восстановление из бэкапа (отдельная глава про DR)

Security context

Минимальные права для контейнера:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL
  seccompProfile:
    type: RuntimeDefault

Что даёт: - Не запускаем от root (компрометация контейнера ≠ компрометация ноды) - Read-only root filesystem (нельзя записать бинарник, который будет выполнен) - Без capabilities (нет CAP_NET_RAW, CAP_SYS_ADMIN и т.п.) - seccomp профиль (ограниченный набор syscall-ов)

Распространённые ошибки

1. Нет PDB. Любой kubectl drain убивает все поды одновременно. Забыли — узнали на первом maintenance.

2. Readiness = liveness. Разные проверки для разных целей. Liveness = процесс жив. Readiness = может обслуживать.

3. Лимиты по памяти слишком жёсткие. OOMKill при первом скачке нагрузки.

4. Anti-affinity required при 3 репликах на 2 нодах. Не сможем зашедулить третий под. Используйте preferredDuringScheduling.

5. HPA scale-up слишком медленный. По умолчанию HPA увеличивает реплики на 100% каждые 30 секунд. Для нагруженных систем — настройте behavior для быстрого scale-up.

6. Нет timeouts на входящем трафике через Ingress. Под может зависнуть на неопределённое время, если downstream сервис медленный.

7. Secrets в environment variables. Environment variables видны в kubectl describe pod. Лучше mount as files с subPath или использовать external secret operator.

Резюме

Kubernetes даёт десятки механизмов надёжности бесплатно — нужно только правильно их настроить:

  • Probes для автоматического рестарта и балансировки
  • Requests/limits для планирования и предотвращения OOM
  • PDB для защиты от вытеснения
  • HPA/VPA/Cluster Autoscaler для масштабирования
  • Topology spread и anti-affinity для распределения по зонам/нодам
  • Network Policies для сетевой изоляции
  • Security context для минимальных прав
  • Graceful shutdown для корректного завершения

Каждый из этих инструментов не заменяет хорошую архитектуру, но вместе они дают надёжность, которую в эпоху VM приходилось настраивать вручную. Не используйте K8s как «просто scheduler» — используйте его как платформу надёжности.

См. также: «Инфраструктура как код», «Иммутабельная инфраструктура», «Canary и Progressive Delivery», «Chaos Engineering», «Service Mesh», «eBPF».