Как работает OOM Killer в Linux и как с ним работать в продакшене?

«Как работает OOM Killer в Linux и как с ним работать в продакшене?» — вопрос из категории Linux, который задают на 23% собеседований Devops Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

OOM Killer (Out-of-Memory Killer) — это механизм ядра Linux, который аварийно завершает процессы, когда система исчерпывает всю доступную память (RAM + swap), чтобы предотвратить полный крах. В продакшен-среде неправильная работа с памятью или её настройками — прямая дорога к нестабильности.

Как это работает и как диагностировать:

  1. Механизм выбора «жертвы»: Ядро рассчитывает для каждого процесса oom_score (отображается в /proc/<PID>/oom_score). Чем выше балл, тем выше вероятность быть убитым. На балл влияют:

    • Объем потребляемой памяти.
    • Время жизни процесса (старые процессы имеют небольшой бонус).
    • Значение oom_score_adj (от -1000 до 1000), которое мы можем задать. Установка в -1000 гарантирует, что процесс никогда не будет выбран.
  2. Поиск корня проблемы: OOM Killer — это симптом, а не причина. Основные действия:

    • Анализ логов: Сообщение от OOM Killer попадает в dmesg и системные логи (/var/log/kern.log).
      $ dmesg -T | grep -i "killed process"
      [Mon Mar 11 10:23:15 2024] Out of memory: Killed process 12345 (java) total-vm:20468780kB, anon-rss:15234000kB
    • Мониторинг: Используем инструменты вроде htop, atop, или метрики в Prometheus (node_exporter) для отслеживания трендов использования памяти (MemAvailable, SwapUsed). Рост использования swap — ранний предупреждающий знак.

Практические стратегии управления в DevOps:

  1. Настройка через oom_score_adj: Защищаем критически важные системные процессы (например, sshd, systemd) и, возможно, ключевые компоненты инфраструктуры.

    # Защищаем процесс с PID 1 (systemd) от OOM Killer
    echo -1000 > /proc/1/oom_score_adj
    
    # Для долгоживущего сервиса через systemd (более правильный способ)
    # В файле сервиса /etc/systemd/system/myservice.service добавляем:
    [Service]
    OOMScoreAdjust=-500
  2. Лимитирование памяти через cgroups (основной современный подход): Это proactive, а не reactive метод. Мы явно задаем лимиты для контейнеров и сервисов.

    • В Docker/Kubernetes:
      # Kubernetes Pod spec
      resources:
        limits:
          memory: "512Mi"  # Контейнер будет убит (OOMKilled), если превысит этот лимит
        requests:
          memory: "256Mi"

      Контейнер, превысивший limit, будет убит немедленно (статус OOMKilled), что изолирует проблему и не затрагивает всю ноду.

    • На уровне системы (systemd): Можно настраивать лимиты для обычных сервисов.
      [Service]
      MemoryMax=1G  # Жесткий лимит
      MemoryHigh=800M # Мягкий лимит, после которого система начнет активно замедлять процесс
  3. Настройка параметров ядра (внимательно!):

    • vm.overcommit_memory:
      • 0 (по умолчанию): Эвристическое overcommit. Ядро разрешает выделять память, пока не наступит реальная нехватка.
      • 1: Всегда overcommit. Рискованно.
      • 2: Запрещает overcommit. Выделяется только память размером swap + RAM * overcommit_ratio. Более безопасно, но может привести к сбоям легитимных процессов с ошибкой "Cannot allocate memory", даже если память есть.
    • vm.swappiness (от 0 до 100): Контролирует склонность ядра к вытеснению страниц памяти в swap. На серверах с SSD swap часто ставят 1 или 10, чтобы использовать swap как буфер, а не ждать OOM. На серверах без swap или с очень медленным диском — 0.

Итоговая философия: В современной инфраструктуре мы стараемся не полагаться на OOM Killer как на механизм стабилизации. Вместо этого мы используем лимиты cgroups (в контейнерах) и тщательный мониторинг, чтобы предсказывать и предотвращать ситуации нехватки памяти.