Learning Platform
Глоссарий Troubleshooting
Урок 07.04 · 20 мин
Начальный
SwapOvercommitOOM killerLinuxMemory pressure

Swap, overcommit, OOM killer — что делать когда RAM не хватает

Если у вас 16 GB RAM, и одна программа хочет 20 GB — что делает Linux? Не падает. Либо:

  1. Использует swap — неактивные страницы пишутся на диск, освобождая RAM.
  2. Включает OOM killer — убивает один из процессов, чтобы освободить память для остальных.

В этом уроке разберём три связанные концепции: swap (диск как продолжение RAM), overcommit (Linux позволяет выделять больше памяти, чем физически есть), и OOM killer (как kernel решает, кого убить при нехватке памяти).

Для junior data engineer это критично знать. Когда ваш Spark executor неожиданно убивается, когда Postgres backend исчезает с logs «killed by signal 9», когда сервер тормозит после многочасовой работы — ответы во взаимодействии RAM, swap и OOM killer.


Что такое swap

Swap — область на диске (раздел или файл), куда kernel может «выгружать» неактивные страницы памяти. Когда нужно освободить RAM, kernel выбирает страницы (например, давно не используемые) и пишет их в swap. Когда процесс обращается к swapped странице — major page fault, страница загружается обратно.

Swap -- расширение RAM за счёт диска
Active pagesЧасто используемые страницы. Остаются в RAM. Высокий accessed bit в page tables
Inactive pagesДавно не использованные. Кандидаты на eviction. Kernel помечает через LRU lists
Memory pressure -> swap outКогда не хватает RAM (free низкая), kernel выбирает cold страницы и пишет их в swap area. Освобождает RAM. Это запись на диск -- может занимать миллисекунды
Access swapped page -> major faultПрограмма обращается к swapped странице. Major page fault. Kernel читает страницу с диска обратно в RAM. ~5-50 мс на NVMe, 100+ мс на HDD
# Текущая ситуация со swap:
free -h
#               total  used   free  shared  buff/cache  available
# Mem:           62Gi   45Gi   2Gi   3Gi     14Gi       11Gi
# Swap:          8Gi    1Gi    7Gi

# Лучше -- swapon:
swapon --show
# NAME       TYPE     SIZE USED PRIO
# /dev/sda5  partition 8G   1G   -2
# /swapfile  file     2G   0    -3

# Сколько свопа использует каждый процесс:
for pid in $(ls /proc/ | grep -E '^[0-9]+$'); do
    swap=$(awk '/VmSwap/ {print $2}' /proc/$pid/status 2>/dev/null)
    if [ -n "$swap" ] && [ "$swap" -gt 1000 ]; then
        echo "$pid $(cat /proc/$pid/comm 2>/dev/null) ${swap} KB"
    fi
done | sort -k3 -n -r | head -10
# Покажет топ-процессов по swap usage

Anonymous vs file-backed — что попадает в swap

В swap идут anonymous pages — те, что не имеют backing file. Это:

  • Heap (malloc-аллокации)
  • Stack
  • .bss (uninitialized globals)
  • mmap anonymous (без file fd)

File-backed страницы (загруженный код, mmap файла) не идут в swap. Они «свопаются» в свой файл — если страница clean, её можно просто отбросить, при следующем обращении загрузим из файла. Если dirty — запись в file (обычный write-back).

Swap кандидаты -- только anonymous
Anonymous (heap, stack)Нет backing file. Эти страницы -- кандидаты на swap area
File-backed (code, libs)Backing file = ELF binary или .so. Clean страницы можно отбросить -- при доступе загрузим обратно из файла
Page cacheКэш файлов -- prefetched данные read/write. Не идут в swap. Просто evict из cache, при доступе загружаются из исходного файла

Важный момент: на нагруженном Linux может казаться, что почти вся RAM занята (free -h показывает мало free), но это часто page cache — кэш файлов. Эта память «бесплатная» — может быть отброшена под потребности процесса. Смотрите колонку available — это сколько реально может быть выделено новой allocation без swap.

free -h
#               total  used   free  shared  buff/cache  available
# Mem:           62Gi   45Gi   2Gi   3Gi     14Gi       11Gi
# 'used' включает page cache, поэтому free низкое
# 'available' -- 11 GiB реально доступно для новых программ

swappiness — настройка агрессивности swap

vm.swappiness — параметр от 0 до 100, контролирующий, насколько активно kernel свопает anonymous страницы vs evict page cache.

vm.swappiness -- баланс anon vs cache
swappiness = 0Минимум swap. Kernel предпочитает evict cached files. Anonymous страницы остаются в RAM до последнего. Используется на латенси-критичных серверах
swappiness = 60 (default)Сбалансированный default. Linux может swapping и cache eviction делать в зависимости от условий
swappiness = 100Максимально swap. Kernel предпочитает свопить anonymous, сохранять page cache. Для workloads, где cache I/O критичен
# Текущая swappiness:
sysctl vm.swappiness
# 60 (default)

# Уменьшить (для production-серверов с БД):
sudo sysctl vm.swappiness=10
# или persistent:
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf

# Для отдельных сценариев:
# - Database server: swappiness 1-10 (хотим, чтобы БД оставалась в RAM)
# - General workstation: swappiness 60 (default)
# - System с медленным диском: swappiness 5-10
# - Server с быстрым NVMe: можно 30-60

Overcommit — Linux может «обещать» больше

Linux позволяет процессам выделять больше памяти, чем физически есть (RAM + swap). Это называется overcommit. Идея: программы часто запрашивают больше, чем реально используют (sparse structures, лень malloc).

Overcommit -- overpromise memory
System: 16 GB RAMФизически 16 GB
Process A: malloc(10 GB)OK -- получает указатель на 10 GB virtual
Process B: malloc(10 GB)OK -- тоже получает указатель на 10 GB virtual. Но если оба touch'нут все -- 20 GB > 16 GB!
vm.overcommit_memory = 0Default. Heuristic overcommit. Kernel разрешает most allocations, отказывает только подозрительно большим. Возможен OOM
overcommit_memory = 1Always overcommit. Kernel никогда не отказывает в allocation. malloc всегда успешный. Полностью полагается на OOM killer
overcommit_memory = 2Strict. Kernel считает доступную память (RAM + swap) * ratio. Не позволяет allocate больше. malloc может вернуть NULL. Самый предсказуемый
# Текущий overcommit mode:
sysctl vm.overcommit_memory
# 0 -- default heuristic

sysctl vm.overcommit_ratio
# 50 -- при mode=2 разрешено RAM*50% + swap

# Сделать strict (для production -- предсказуемее):
sudo sysctl vm.overcommit_memory=2
sudo sysctl vm.overcommit_ratio=100  # разрешить RAM*100% + swap

# Тогда malloc будет возвращать NULL, если запрос превышает limit
# Программы должны обрабатывать NULL -- но многие не обрабатывают -> segfault

Когда какой mode:

  • mode=0 (default) — для general-purpose систем, работает в 99% случаев.
  • mode=1 — для специфических workloads (Redis recommends overcommit_memory=1 чтобы избежать fork() failures на full memory).
  • mode=2 — для production баз данных, систем с критичной предсказуемостью.
docker stats: память контейнеров и OOM в production

OOM killer — кто умрёт первым

Когда память закончилась, kernel запускает OOM killer (Out of Memory killer). Он выбирает процесс и убивает его с SIGKILL, освобождая память.

OOM killer -- выбор жертвы
oom_scoreКаждый процесс имеет oom_score -- эвристический скор, который kernel вычисляет. Чем выше -- тем больше шанс быть убитым
Factor: RSSЧем больше памяти процесс использует, тем выше oom_score. Хищники в первую очередь
Factor: forksСколько раз процесс делал fork. Many forks -> высокий score
oom_score_adjTunable: -1000 (никогда не убивать) до +1000 (приоритет на убийство). Userspace может выставить через /proc/PID/oom_score_adj
Kill SIGKILLOOM killer шлёт SIGKILL -- процесс не может перехватить, мгновенно умирает. Память освобождается
# Свой oom_score:
cat /proc/self/oom_score

# Score для всех больших процессов:
for pid in $(ls /proc/ | grep -E '^[0-9]+$' | head -50); do
    score=$(cat /proc/$pid/oom_score 2>/dev/null)
    if [ -n "$score" ] && [ "$score" -gt 100 ]; then
        echo "$score $(cat /proc/$pid/comm 2>/dev/null) (PID $pid)"
    fi
done | sort -rn | head -10
# Топ-10 кандидатов на OOM kill

# Защитить процесс от OOM:
echo -1000 | sudo tee /proc/PID/oom_score_adj
# -1000 = никогда не убивать (kernel сам уже не запустится без RAM, поэтому осторожно)

# Сделать процесс жертвой:
echo 1000 | sudo tee /proc/PID/oom_score_adj
# Будет убит первым при OOM

# Проверить, был ли OOM kill недавно:
sudo dmesg | grep -i 'killed process' | tail -10
# [12345.678] Out of memory: Killed process 4321 (postgres) total-vm:... rss:... ...

Heuristic OOM killer:

OOM killer прioritizes:

  1. Процессы с большим RSS — освободят больше памяти.
  2. Процессы с высоким oom_score_adj — пользователь явно пометил.
  3. Дочерние процессы — меньше «системного» вклада.
  4. Не root процессы — предпочитает не убивать system services.

Kernel НЕ убивает (обычно):

  • init (PID 1)
  • kthreadd и его потомков
  • Процессы с oom_score_adj = -1000

Когда swap полезен и когда нет

Swap полезен:

  • На desktop — даёт возможность поработать, когда память кончилась. Лучше медленно, чем краш.
  • Для редко используемых сервисов на сервере — они уйдут в swap, активные останутся в RAM.
  • Для hibernation (suspend to disk) — нужен swap чтобы записать RAM.

Swap вреден:

  • На latency-critical серверах — swap I/O вызывает spike’и latency.
  • На сервере с быстрой OOM-стратегией — лучше пусть kernel убьёт зомби-job чем все будут swapping.
  • На облачных VMs с медленным диском — swap I/O может быть катастрофой.
Стратегии swap в production
No swapswapoff -a. Когда RAM кончилась, OOM killer убивает процесс мгновенно. Никаких latency spike'ов, но нужен мониторинг
Small swap (1-4 GB)Эмulтентный buffer. Если короткий peak -- система переживёт. Не для long-term reliance
Equal to RAMСтарая рекомендация (Linus, 2000s). Имеет смысл для hibernation. Для серверов часто слишком много
Kubernetes: no swapРаньше kubelet падал при включенном swap (-fail-swap-on=true). Сейчас опционально, но best practice все ещё no-swap
Database servers: minimal swapPostgres, Redis, MongoDB рекомендуют swappiness=1 или вовсе no swap. Хотим, чтобы БД оставались в RAM
# Полностью отключить swap:
sudo swapoff -a
# Сразу освобождает swap, выгружает данные обратно в RAM
# Persistent -- закомментируйте swap entries в /etc/fstab

# Включить обратно:
sudo swapon -a

Реальный пример: OOM kill Postgres

Production-сценарий: у вас 32 GB RAM, Postgres настроен с shared_buffers 24 GB, и запущен Spark с executor 8 GB. Под нагрузкой Spark пытается выделить дополнительные 4 GB:

Total used: 24 (Postgres) + 8 (Spark) + 2 (system) + 4 (новая) = 38 GB > 32 GB

Что произойдёт:

  1. Linux попытается освободить кеш — but Postgres uses shared_buffers, не cache.
  2. Попытается swap — но swappiness=10, медленно. Latency spike.
  3. Память всё равно не хватает. OOM killer запускается.
  4. Postgres имеет высокий RSS (~24 GB) — oom_score высокий. Postgres убит SIGKILL.

dmesg | grep oom покажет:

[12345] Out of memory: Killed process 4321 (postgres) total-vm:25600000kB

Решение:

  1. Capacity planning: не allocate Postgres shared_buffers больше чем 75% RAM. Оставлять buffer для другого.
  2. oom_score_adj для critical processes: echo -500 > /proc/postgres-pid/oom_score_adj — защищаем Postgres.
  3. cgroups limits: Spark в cgroup с CPU+Memory limits, чтобы не превысил allocation.
  4. Monitoring: alert на free memory < 10%, реагировать ДО OOM.

Попробуй сам

# 1. Текущая ситуация:
free -h
# Колонка 'available' -- реальный буфер
# Колонка 'used' включает page cache

swapon --show
# Список swap areas

# 2. Сколько swap использует каждый процесс:
ps -eo pid,vsz,rss,comm --sort -rss | head -10

# 3. Триггерим swap (НЕ В PRODUCTION):
# Заполним RAM
stress-ng --vm 4 --vm-bytes 80% --timeout 30s &
# В другом окне:
vmstat 1
# Колонки si (swap in) и so (swap out) -- если активны, swapping происходит
# Колонка 'wa' -- iowait, тоже растёт при swap

# 4. oom_score процессов:
for pid in $(pgrep -f 'firefox|chrome|code' | head -5); do
    echo "$(cat /proc/$pid/oom_score 2>/dev/null) $(cat /proc/$pid/comm)"
done | sort -rn

# 5. Защитить процесс от OOM:
# Например, ваш main shell:
sudo bash -c "echo -500 > /proc/$$/oom_score_adj"
cat /proc/$$/oom_score_adj
# -500 -- очень низкий, защищён

# 6. Посмотреть OOM events:
sudo dmesg | grep -iE 'oom|killed process' | tail -20

# 7. Симулировать low-memory scenario:
# (НЕ на production!)
echo 1 | sudo tee /proc/sys/vm/drop_caches
# Сбрасывает page cache. used уменьшится, but доступная память увеличится
# Полезно перед benchmarks

Проверка знанийKnowledge check
Junior пишет: 'У меня Postgres постоянно убивается OOM killer'ом. В dmesg вижу: 'Killed process X (postgres)'. У сервера 64 GB RAM, Postgres настроен shared_buffers=48GB. Что делать?'
ОтветAnswer
Это типичная capacity planning ошибка. Давай разберём. Проблема: shared_buffers 48 GB = 75% RAM. Это очень близко к лимиту. Любая дополнительная нагрузка (другие процессы, OS overhead, page cache) -- и Linux запускает OOM. Postgres -- самый большой по RSS, его убивают первым. Что делать -- по приоритетам: 1. Уменьшить shared_buffers. Стандартная рекомендация: shared_buffers = 25-40% RAM. На 64 GB -- 16-25 GB. Остальное -- для OS page cache (который Postgres тоже использует), work_mem на сессии, и других процессов. ALTER SYSTEM SET shared_buffers = '20GB'; sudo systemctl restart postgresql 2. Установить oom_score_adj для Postgres. Защитить от OOM kill: # В systemd unit: [Service] OOMScoreAdjust=-500 # Или через /proc: for pid in $(pgrep postgres); do echo -500 | sudo tee /proc/$pid/oom_score_adj done Помечает Postgres как 'не убивать первым'. Kernel предпочитает других кандидатов. 3. Запретить overcommit или сделать его strict. Если у вас overcommit_memory=0 (default), kernel легко позволяет alloc'ы которые могут привести к OOM. Strict mode: sudo sysctl vm.overcommit_memory=2 sudo sysctl vm.overcommit_ratio=80 # Разрешает alloc до 80% RAM + swap 4. Найти других потребителей памяти. Кто ещё использует RAM на сервере? ps -eo pid,rss,vsz,comm --sort -rss | head -10 Возможно, Spark, Python скрипты, какие-то batch jobs. Они должны быть в cgroups с memory limits. 5. cgroups для остальных процессов. Кроме Postgres, всех в memory cgroup: # systemd-run --slice=batch.slice -p MemoryMax=8G ./my-spark-job Тогда batch не сможет вылезти за свой лимит и убить Postgres. 6. Мониторинг и alerting. Не дожидаться OOM kill, реагировать заранее: - Prometheus + node_exporter -- метрики MemAvailable - Alert: `MemAvailable < 10%` - Alert: `vm.swap_used > 0` на серверах со swap - dmesg watcher на 'Killed process' 7. Verify with cat /proc/meminfo. После настройки: - shared_buffers = 20 GB - other processes: до 5 GB - OS overhead: до 4 GB - Total used: до 30 GB на 64 GB -- комфортный запас `MemAvailable` должен быть выше 20 GB постоянно. 8. Postgres-specific. Также: - work_mem -- per-session, может вырасти при многих сессиях. Лимитировать или уменьшать. - max_connections -- каждый backend ~5-10 MB. 1000 backends = 5-10 GB только под бэкенды. - Использовать pgbouncer для connection pooling. В общем, capacity planning. shared_buffers 75% RAM -- recipe for OOM disaster. 25-40% -- правильный диапазон. Plus защита через oom_score_adj и cgroups для других процессов. Если действительно нужно 48 GB shared_buffers -- купите 128 GB RAM. Дешевле чем production outages.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Какие страницы памяти могут попадать в swap?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5