Операционные системы

Продвинутое управление памятью

PostgreSQL сервер с 128GB RAM внезапно падает ночью. В логах: "Out of memory: Killed process postgres". Но мониторинг показывал 40GB свободной памяти! Как такое возможно? Почему ОС убила критичную базу данных вместо фоновых worker'ов?

  • **NUMA в production:** Неправильная конфигурация NUMA может снизить производительность PostgreSQL/Redis на 30-40%. Процесс на узле 1 обращается к памяти узла 0 через медленный interconnect - каждый запрос в БД тормозит в 2-3 раза.
  • **Huge Pages для баз данных:** PostgreSQL с 64GB shared_buffers и huge pages экономит 128MB на page tables и устраняет миллионы TLB miss в секунду. Прирост производительности OLAP запросов: 20-30%. MongoDB, Redis, Oracle - все рекомендуют explicit huge pages.
  • **OOM Killer катастрофы:** Реальный инцидент: batch-обработка логов вызвала OOM, ядро убило PostgreSQL вместо batch процессов. Простой сервиса 3 часа. Решение: oom_score_adj=-900 для критичных процессов + cgroup memory limits для некритичных.

Цели урока

  • NUMA: local vs remote memory access, numactl, AutoNUMA balancing в Linux
  • Huge pages: 2MB vs стандартные 4KB; transparent huge pages (THP) и их trade-offs
  • Memory overcommit modes (0/1/2), Committed_AS vs CommitLimit
  • OOM killer: расчёт oom_score, oom_score_adj для защиты критичных процессов
  • Cgroup memory limits для per-process контроля и изоляции

NUMA: Non-Uniform Memory Access

**NUMA (Non-Uniform Memory Access)** - архитектура памяти в многопроцессорных системах, где каждый процессор имеет **локальную** память с быстрым доступом и **удалённую** память других процессоров с медленным доступом.

**Почему NUMA:** • **SMP проблема:** В классических SMP-системах все процессоры разделяют одну шину памяти → узкое горлышко при росте числа CPU • **NUMA решение:** Каждый процессор имеет свою локальную память → доступ в 2-3 раза быстрее, чем к удалённой • **Масштабируемость:** NUMA позволяет создавать серверы с десятками и сотнями ядер без деградации производительности

**Ключевая проблема NUMA:** Если процесс выделяет память на узле 0, но мигрирует на узел 1 - каждый доступ к памяти идёт через медленный interconnect (200-300ns вместо 100ns). Это **remote memory access** - главный враг производительности в NUMA.

Production пример: PostgreSQL

**Запуск PostgreSQL с NUMA pinning:** Вместо: ```bash postgres -D /data/pgdata ``` Используем numactl для привязки к конкретному узлу: ```bash numactl --cpunodebind=0 --membind=0 postgres -D /data/pgdata ``` Это гарантирует, что Postgres выполняется на CPU узла 0 и вся память выделяется из локальной памяти узла 0. **Прирост производительности:** 20-40% на больших серверах.

**NUMA политики выделения памяти:** • **Local allocation (default):** Память выделяется на узле, где выполняется процесс • **Interleave:** Память равномерно распределяется по всем узлам (для многопоточных приложений) • **Preferred:** Предпочтительный узел, но при нехватке - другие узлы • **Bind:** Строгая привязка к определённым узлам

**Production best practices:** • **Базы данных (PostgreSQL, MySQL):** Привязка к одному NUMA узлу через `--cpunodebind` + `--membind` • **Кеши (Redis, Memcached):** Interleave для равномерного распределения нагрузки • **Web-серверы (nginx):** Один worker на NUMA узел с привязкой • **Мониторинг:** Проверяй `numastat` - если видишь большой remote memory access, настрой политики

Процесс выделил 16GB памяти на NUMA узле 0, но ОС мигрировала его на CPU узла 1. Что произойдёт?

Huge Pages: Оптимизация TLB

**Huge Pages** - страницы памяти большого размера (2MB/1GB вместо стандартных 4KB), которые радикально сокращают накладные расходы на трансляцию виртуальных адресов в физические через **TLB (Translation Lookaside Buffer)**.

**Проблема стандартных 4KB страниц:** • **TLB ограничен:** Обычно 64-1024 записей в TLB • **Малое покрытие:** 1024 записи × 4KB = 4MB памяти покрывается TLB • **TLB miss:** Если адрес не в TLB → page table walk (100-200 циклов CPU!) • **Большие приложения:** База данных с 64GB памяти → миллионы TLB miss в секунду

**Типы Huge Pages в Linux:** • **Explicit Huge Pages (HugeTLBFS):** Статическое резервирование памяти через `/etc/sysctl.conf`, приложение запрашивает явно • **Transparent Huge Pages (THP):** Ядро автоматически использует 2MB страницы где возможно (по умолчанию включено с kernel 2.6.38)

Production пример: PostgreSQL

**PostgreSQL с Huge Pages:** PostgreSQL с 64GB shared_buffers без huge pages: - Page table занимает: 64GB / 4KB × 8 bytes = 128MB только на page tables! - TLB miss: миллионы в секунду - Производительность: -15-20% на больших запросах С huge pages (2MB): - Page table: 64GB / 2MB × 8 bytes = 256KB (в 500 раз меньше!) - TLB miss: практически отсутствуют - **Прирост производительности: 20-30% на OLAP запросах** Настройка: ```bash # postgresql.conf huge_pages = on # или = try shared_buffers = 64GB # /etc/sysctl.conf vm.nr_hugepages = 32768 # 32768 × 2MB = 64GB ```

**Transparent Huge Pages (THP) - осторожно!** THP автоматически объединяет 4KB страницы в 2MB, но имеет **серьёзные проблемы** для баз данных и высоконагруженных приложений.

**Best practices Huge Pages:** • **Базы данных:** ТОЛЬКО explicit huge pages, THP отключён (рекомендация Oracle, MongoDB, Redis, PostgreSQL) • **Кеши (Redis, Memcached):** Explicit huge pages для large instances (>16GB) • **HPC приложения:** 1GB huge pages для массивных научных расчётов • **Мониторинг:** `grep Huge /proc/meminfo` - если HugePages_Free сильно отличается от HugePages_Total, скорректируй выделение

Production пример: Redis

**Redis с Huge Pages:** Redis instance 32GB: ```bash # Explicit huge pages vm.nr_hugepages = 16384 # 16384 × 2MB = 32GB # redis.conf # Redis автоматически использует huge pages если доступны # Отключить THP (критично!) echo never > /sys/kernel/mm/transparent_hugepage/enabled ``` **Результат:** Латентность p99 снизилась с 5ms до 1.2ms из-за устранения TLB miss и THP defragmentation stalls.

Почему MongoDB, Redis и PostgreSQL рекомендуют ОТКЛЮЧАТЬ Transparent Huge Pages (THP)?

Memory Overcommit: Обещать больше, чем есть

**Memory Overcommit** - стратегия управления памятью, где ОС позволяет процессам выделить **больше виртуальной памяти**, чем доступно физической RAM + swap. Это работает, потому что большинство приложений **резервируют** память, но не используют её полностью.

**Почему overcommit работает:** • **Разреженное использование:** `malloc(1GB)` не означает, что приложение сразу запишет во все 1GB • **Copy-on-Write:** fork() создаёт дочерний процесс с копией памяти родителя, но физические страницы разделяются до первой записи • **Эффективность:** Без overcommit многие процессы не смогли бы запуститься из-за консервативного резервирования памяти

**Режимы Memory Overcommit в Linux:** Конфигурируется через `/proc/sys/vm/overcommit_memory`: • **0 (Heuristic, default):** Ядро использует эвристики - отклоняет явно безумные запросы (например, malloc(100TB)), но разрешает разумные • **1 (Always overcommit):** Всегда разрешает выделение памяти, независимо от доступности • **2 (No overcommit):** Строгий учёт - разрешает выделить только: RAM + swap × overcommit_ratio

Production пример: fork() без дублирования памяти

**fork() и Copy-on-Write с overcommit:** ```c // Родительский процесс занимает 8GB памяти char *data = malloc(8 * 1024 * 1024 * 1024); // 8GB memset(data, 'A', 8GB); // Заполнили данными pid_t pid = fork(); // Создаём дочерний процесс if (pid == 0) { // Дочерний процесс // Благодаря Copy-on-Write и overcommit: // - Виртуально имеет доступ к 8GB родителя // - Физически память НЕ копируется (разделяется) // - Только при записи data[i] = 'B' создаётся копия страницы } ``` **Без overcommit:** fork() потребовал бы физически 8GB свободной памяти для копии → отказ. **С overcommit:** fork() успешен, память копируется только при реальной записи → эффективно.

**Настройка overcommit для production:** • **Веб-серверы, приложения:** Mode 0 (heuristic) - оптимальный баланс • **Базы данных (PostgreSQL, Oracle):** Mode 2 (no overcommit) + правильный overcommit_ratio - предсказуемость важнее эффективности • **Контейнеры (Docker, Kubernetes):** Mode 0, но с memory limits через cgroups

**Риски Overcommit:** • **OOM (Out of Memory):** Если процессы начинают использовать зарезервированную память, может не хватить физической RAM → OOM Killer начинает убивать процессы • **Swap thrashing:** Активный swap при нехватке памяти → система "встаёт" из-за постоянных page faults • **Непредсказуемость:** Приложение может успешно выделить память, но получить SIGKILL от OOM Killer при реальном использовании

Production катастрофа

**Real-world инцидент: OOM из-за overcommit** Компания запустила batch-обработку логов (10 worker процессов). Каждый worker: ```python data = bytearray(4 * 1024 * 1024 * 1024) # malloc 4GB # Обычно использует 500MB, но иногда весь массив ``` - Режим: overcommit_memory=0 (heuristic) - 10 процессов × 4GB = 40GB зарезервировано - Сервер: 32GB RAM + 8GB swap = 40GB доступно - Обычно работает: процессы используют ~5GB суммарно **Что пошло не так:** В один момент 8 процессов начали обрабатывать аномально большие файлы → каждый использовал 3.5GB → требуется 28GB → превысили физическую память → OOM Killer убил PostgreSQL (!!) вместо batch процессов. **Решение:** Mode 2 (no overcommit) + увеличен swap до 32GB + batch процессы с memory cgroup limits.

OOM Killer: Последняя инстанция

**OOM Killer (Out-Of-Memory Killer)** - механизм ядра Linux, который убивает процессы когда система **полностью исчерпала** физическую память и swap. Это последний рубеж защиты от полного зависания системы.

**Когда срабатывает OOM Killer:** 1. Процесс пытается выделить память (или обратиться к зарезервированной) 2. Физическая RAM + swap полностью заняты 3. Нет страниц, которые можно вытеснить (evict) 4. Ядро вызывает OOM Killer 5. OOM Killer выбирает "жертву" по специальному алгоритму 6. Процесс получает **SIGKILL** (невозможно перехватить!) 7. Память освобождается, система продолжает работу

**Как OOM Killer выбирает жертву:** Каждому процессу присваивается **OOM Score** (0-1000) на основе: • **Объём потребляемой памяти** (больше память → выше score) • **Время работы процесса** (новые процессы → выше score) • **Nice value** (низкий приоритет → выше score) • **Root процессы** (понижение score) • **oom_score_adj** (ручная корректировка администратором)

**Управление OOM поведением через oom_score_adj:** Диапазон: **-1000 до +1000** • **-1000:** Полная защита от OOM Killer (никогда не убьёт) • **0:** Стандартный score (без корректировки) • **+1000:** Первым кандидат на убийство

Production пример: приоритезация процессов

**Production стратегия защиты критичных процессов:** ```bash # 1. PostgreSQL - критичная база данных echo -900 > /proc/$(pgrep postgres)/oom_score_adj # 2. Redis - кеш, можно убить (данные не критичны) echo 300 > /proc/$(pgrep redis)/oom_score_adj # 3. Background workers - некритичные, убивать первыми for pid in $(pgrep -f worker); do echo 800 > /proc/$pid/oom_score_adj done # 4. nginx - фронтенд, средний приоритет echo 100 > /proc/$(pgrep nginx)/oom_score_adj ``` **Результат:** При OOM сначала умрут background workers, затем Redis (кеш восстановится), PostgreSQL защищён до последнего.

**Мониторинг и расследование OOM событий:** OOM события записываются в **kernel log** (`dmesg`, `/var/log/kern.log`).

Расследование инцидента

**Детальное расследование OOM инцидента:** ```bash # Шаг 1: Найти OOM событие в dmesg dmesg -T | grep -i oom # [Tue Dec 24 03:15:42 2024] Out of memory: Killed process 5678 (postgres) # Шаг 2: Посмотреть полный контекст (200 строк до события) dmesg -T | grep -B 200 'Killed process 5678' # Показывает: # - Какие процессы потребляли много памяти # - OOM scores всех процессов на момент события # - Состояние памяти: available, free, swap # Шаг 3: Проверить текущее состояние памяти free -h cat /proc/meminfo | grep -E 'MemAvailable|SwapFree|Committed_AS' # Шаг 4: Настроить алерты на почти-OOM ситуацию # Метрика: MemAvailable < 10% или SwapFree < 5% ```

**Предотвращение OOM:** • **Cgroup memory limits:** Изоляция процессов через cgroups - приложение убивается при превышении своего лимита, не затрагивая систему • **Swap правильного размера:** Swap = 0.5-1× RAM для серверов (больше swap → больше времени до OOM, но риск thrashing) • **Мониторинг:** Алерты на MemAvailable < 10%, Committed_AS > 90% от CommitLimit • **overcommit_memory=2:** Для критичных систем (БД) - предсказуемый отказ malloc() лучше, чем внезапный OOM

Ключевые идеи

  • **NUMA (Non-Uniform Memory Access):** В многопроцессорных системах каждый CPU имеет локальную память (100ns) и удалённую (200-300ns). Remote memory access снижает производительность в 2-3 раза. Решение: numactl --cpunodebind + --membind для привязки процессов к узлам.
  • **Huge Pages (2MB/1GB):** Увеличивают TLB coverage в 500-1000 раз, устраняют TLB miss. Explicit huge pages (HugeTLBFS) для БД - прирост 20-30%. Transparent Huge Pages (THP) ОТКЛЮЧИТЬ для production БД из-за defragmentation stalls (задержки до 500ms).
  • **Memory Overcommit:** ОС разрешает выделить больше виртуальной памяти, чем доступно физически, потому что приложения резервируют, но не используют всю память. Mode 0 (heuristic) для приложений, mode 2 (no overcommit) для БД. Риск: OOM Killer при реальном использовании зарезервированной памяти.
  • **OOM Killer:** Убивает процессы при полной нехватке памяти, выбирая жертву по OOM Score (зависит от объёма памяти, времени работы). Защита критичных процессов: oom_score_adj=-900. Cgroup memory limits для изоляции некритичных процессов. Мониторинг: MemAvailable < 10%, алерты на близость к OOM.

Связанные темы

Продвинутое управление памятью - вершина пирамиды знаний об ОС, связанная с архитектурой, производительностью и надёжностью систем:

  • Виртуальная память — Huge Pages - оптимизация поверх виртуальной памяти. TLB cache ускоряет трансляцию виртуальных адресов в физические через page tables.
  • Процессы и потоки — Memory overcommit критичен для fork() - Copy-on-Write позволяет создавать дочерние процессы без дублирования памяти. OOM Killer убивает процессы при нехватке памяти.
  • Производительность I/O — NUMA влияет на производительность I/O: DMA передачи данных идут через локальную память NUMA узла. Неправильная привязка → удалённый доступ → падение throughput.
  • Контейнеризация — Cgroup memory limits изолируют контейнеры от OOM хост-системы. Memory overcommit в Kubernetes: pod requests vs limits, oom_score_adj для QoS классов.

Вопросы для размышления

  • Почему MongoDB, Redis и PostgreSQL категорически рекомендуют ОТКЛЮЧИТЬ Transparent Huge Pages, хотя они дают производительность? В чём разница между THP и explicit huge pages?
  • Сервер с 2 NUMA узлами (по 64GB на узел). PostgreSQL использует 100GB памяти, привязан к узлу 0. Что произойдёт? Как это отразится на производительности?
  • Почему memory overcommit считается безопасным для большинства приложений, но опасным для критичных баз данных? Когда стоит использовать overcommit_memory=2?
  • OOM Killer убил PostgreSQL вместо фоновых worker процессов. Как это предотвратить? Почему недостаточно просто увеличить swap?

Связанные уроки

  • os-07-memory — Базовые концепции виртуальной памяти перед продвинутыми темами
  • os-08-virtual-memory — Page tables и TLB - фундамент для понимания mmap и huge pages
  • ca-12 — Cache hierarchy в CPU связана с уровнями памяти ОС
  • db-14-mvcc — MVCC в PostgreSQL использует copy-on-write, как fork() в ОС
  • arch-10-virtual-memory
Продвинутое управление памятью

0

1

Войти

Сервер: 16GB RAM, 8GB swap, overcommit_memory=2, overcommit_ratio=50. Приложение пытается malloc(24GB). Что произойдёт?

OOM Killer можно полностью отключить, и тогда система просто будет использовать swap вместо убийства процессов

OOM Killer НЕЛЬЗЯ отключить - это критичный механизм защиты ядра. Без него система полностью зависнет при нехватке памяти

Многие думают, что swap спасёт от OOM, но это не так. Когда исчерпаны и RAM, и swap, процесс всё равно пытается выделить память. Без OOM Killer ядро войдёт в бесконечный цикл page fault → eviction → page fault, система полностью зависнет (kernel panic или hard freeze). OOM Killer - это меньшее зло: лучше потерять один процесс, чем всю систему. Правильная стратегия: не отключать OOM Killer, а защищать критичные процессы через oom_score_adj=-900 и использовать cgroup memory limits для изоляции.

На сервере запущены: PostgreSQL (64GB RAM, PID 100), Redis (8GB RAM, PID 200), nginx (1GB RAM, PID 300). Произошёл OOM. Какой процесс с наибольшей вероятностью убьёт OOM Killer по умолчанию (без oom_score_adj)?