Gymterview
middle

Как ограничение ресурсов защищает от DoS-атак?

Ограничение ресурсов (resource limits) в Kubernetes и Docker предотвращает ситуацию, когда один контейнер потребляет все ресурсы хоста, лишая остальные контейнеры возможности нормально функционировать. Это защита от DoS (Denial of Service) — как от целенаправленных атак, так и от ошибок в коде (утечки памяти, бесконечные циклы, неконтролируемый рост числа потоков).

Requests и Limits в Kubernetes:

Пример
apiVersion: apps/v1
kind: Deployment
metadata:
  name: banking-service
  namespace: banking
spec:
  template:
    spec:
      containers:
      - name: app
        image: registry.bank.local/banking-service:1.0.0
        resources:
          requests:
            memory: "256Mi"  # Гарантированные ресурсы для планировщика
            cpu: "250m"      # 0.25 CPU core
          limits:
            memory: "512Mi"  # Максимум — при превышении контейнер получит OOMKill
            cpu: "1000m"     # 1 CPU core — при превышении контейнер будет троттлиться
        env:
        - name: JAVA_OPTS
          value: >-
            -XX:MaxRAMPercentage=75.0
            -XX:+UseG1GC
            -XX:+ExitOnOutOfMemoryError

Разница между requests и limits:

  • requests — минимальный гарантированный объём ресурсов. Планировщик Kubernetes использует requests при выборе ноды для размещения пода: под будет размещён только на той ноде, где имеется достаточно свободных ресурсов для покрытия requests всех контейнеров.
  • limits — максимально допустимое потребление. При превышении memory limit ядро Linux немедленно завершает процесс через OOMKill. При превышении CPU limit контейнер подвергается троттлингу (throttling) — ему выделяется меньше процессорного времени, но процесс продолжает работать.

Настройка JVM с учётом container limits:

Пример
ENTRYPOINT ["java", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:InitialRAMPercentage=50.0", \
    "-XX:+UseContainerSupport", \
    "-XX:+ExitOnOutOfMemoryError", \
    "-jar", "/app/app.jar"]

Флаг -XX:+UseContainerSupport (включён по умолчанию с Java 10+) позволяет JVM корректно определять memory и CPU limits контейнера через cgroups и соответствующим образом настраивать размер heap и количество потоков GC. Параметр -XX:MaxRAMPercentage=75.0 выделяет 75% от доступной памяти контейнера под Java heap, оставляя 25% для metaspace, стеков потоков, native memory (NIO-буферы, JNI) и самой операционной системы.

Флаг -XX:+ExitOnOutOfMemoryError приказывает JVM немедленно завершить процесс при OutOfMemoryError, чтобы Kubernetes мог перезапустить под через restartPolicy. Без этого флага JVM может оказаться в «зомби»-состоянии: процесс жив, но не обрабатывает запросы.

LimitRange — ограничения по умолчанию для namespace:

Пример
apiVersion: v1
kind: LimitRange
metadata:
  name: banking-limits
  namespace: banking
spec:
  limits:
  - type: Container
    default:          # Значения limits по умолчанию (если не указаны в Pod spec)
      memory: "512Mi"
      cpu: "500m"
    defaultRequest:   # Значения requests по умолчанию
      memory: "256Mi"
      cpu: "250m"
    max:              # Максимально допустимые limits
      memory: "2Gi"
      cpu: "2"
    min:              # Минимально допустимые requests
      memory: "64Mi"
      cpu: "50m"
  - type: Pod
    max:
      memory: "4Gi"
      cpu: "4"

LimitRange выполняет две функции: автоматически назначает limits/requests контейнерам, для которых они не указаны явно, и валидирует, что указанные значения попадают в допустимый диапазон. Это предотвращает ситуацию, когда разработчик забывает указать limits или устанавливает неоправданно высокие значения.

ResourceQuota — ограничение суммарных ресурсов namespace:

Пример
apiVersion: v1
kind: ResourceQuota
metadata:
  name: banking-quota
  namespace: banking
spec:
  hard:
    requests.cpu: "10"
    requests.memory: "20Gi"
    limits.cpu: "20"
    limits.memory: "40Gi"
    pods: "50"
    services: "20"
    secrets: "30"
    configmaps: "30"

ResourceQuota ограничивает суммарное потребление ресурсов всеми подами в namespace. Это защита от ситуации, когда одна команда создаёт слишком много подов или потребляет непропорционально много ресурсов кластера. Поле pods: "50" также ограничивает количество подов, что защищает от атаки через массовое создание ресурсов.

Ограничение ресурсов в Docker:

Пример
docker run \
    --memory=512m \
    --memory-swap=512m \    # Запретить использование swap
    --cpus=1.0 \
    --pids-limit=200 \      # Ограничение числа процессов (защита от fork bomb)
    --ulimit nofile=1024:2048 \
    my-java-app

Параметр --memory-swap=512m, равный --memory, полностью запрещает использование swap. Swap может скрывать проблемы с потреблением памяти и ухудшать производительность, поэтому в production-среде его рекомендуется отключать. Параметр --pids-limit=200 ограничивает количество процессов внутри контейнера, что защищает от fork bomb (атака, при которой процесс рекурсивно создаёт копии самого себя, исчерпывая PID-ы системы).

Сценарии атак, которые предотвращают limits:

Атака Без ограничений С ограничениями
Утечка памяти (memory leak) Падение всего хоста из-за исчерпания RAM OOMKill только конкретного пода, остальные продолжают работать
Бесконечный цикл (100% CPU) Деградация производительности всех сервисов на ноде Throttling только этого пода, другие получают своё процессорное время
Fork bomb Исчерпание PID-ов хоста, невозможность запустить новые процессы pids-limit ограничит число процессов внутри контейнера
Заполнение диска логами Диск заполнен, все сервисы на ноде перестают писать логи и данные emptyDir.sizeLimit + ephemeral-storage limits ограничат потребление дискового пространства