Docker Swap-шторм: чому контейнери «працюють», а хост «тане» (і як це виправити)

Було корисно?

Сторінка відповідає. Pod-и зелені. Логи контейнерів нудні. Тоді як ваш хост кричить:
середнє навантаження в стратосфері, SSH чекає 30 секунд, щоб відобразити символ, а kswapd їсть CPU як за гонорар за цикл.

Це особливий тип інциденту в продакшені, коли все «працює» аж до моменту, коли бізнес помічає латентність,
тайм‑аути та загадково «повільні бази даних». Ласкаво просимо до swap‑шторму: без явного краху, лише повільний розпад.

Що таке swap-шторм (і чому він вас обманює)

Swap‑шторм — це тривалий тиск на пам’ять, який змушує ядро постійно виштовхувати сторінки з RAM у swap,
а потім знову підвантажувати їх, з переповторенням циклу. Це не просто «трохи використовують swap». Це коли система витрачає стільки часу
на переміщення сторінок, що корисна робота відходить на другий план.

Неприємність у тому, що багато застосунків продовжують «працювати». Вони відповідають, але повільно. Вони повторюють запити. Вони тайм‑аутяться й перезапускаються.
Оркестратор бачить процеси живими, перевірки здоров’я ледве проходять і думає, що все нормально.
Люди помічають першими: усе відчувається липким.

Два сигнали, що відділяють «використання swap» від «swap-шторму»

  • Сплески major page faults (читання сторінок із swap на диск).
  • PSI memory pressure показує тривалі затримки (задачі чекають на reclaim / IO).

Якщо дивитися лише на «відсоток використаного swap», вас можуть ввести в оману. Swap може бути зайнятий на 20% і стабільний тижнями без драм.
Навпаки, при 5% використання swap може відбуватися шторм, якщо робочий набір постійно міняється.

Цікаві факти й історичний контекст (бо в цього безладу є історія)

  1. Раніше OOM у Linux був відомо грубим. OOM‑кілер ядра еволюціонував десятиліттями; він і досі дивує під навантаженням.
  2. cgroups з’явилися, щоб зупинити «галасливих сусідів». Вони були створені для шардів систем задовго до популярності контейнерів.
  3. Облік swap у cgroups був контроверсійним. Це додає накладні витрати й мав реальні баги; багато платформ вимикали його за замовчуванням.
  4. Kubernetes історично відмовлявся від swap. Не тому, що swap—зло, а тому, що передбачувана ізоляція пам’яті ускладнюється зі swap.
  5. Число «вільної пам’яті» неправильно розуміють з незапам’ятних часів. Linux агресивно використовує RAM для page cache; низький free часто є нормою.
  6. Pressure Stall Information (PSI) — відносно новий інструмент. Це один з найкращих сучасних інструментів для виявлення «очікування пам’яті» без гадань.
  7. SSD‑swap зробив шторми тихішими, але не безпечнішими. Швидший swap зменшує біль… поки не маскує проблему і ви не натрапите на write amplification та провали латентності.
  8. Параметри overcommit — це культурний артефакт. Linux за замовчуванням допускає більше алокацій, ніж фактично торкається код; це працює, поки не перестає.

Чому контейнери виглядають здоровими, поки хост вмирає

Контейнери не мають власного ядра. Це процеси, згруповані cgroups і просторами імен.
Тиск на пам’ять керує хостове ядро, і саме воно робить reclaim і swap.

Ось ілюзія: контейнер може продовжувати виконуватися й відповідати, поки хост інтенсивно свопить, бо
процеси контейнера все ще заплановані і роблять прогрес — але за жахливу ціну.
Використання CPU контейнера може навіть виглядати нижчим, бо він заблокований на IO (swap‑in), а не витрачає CPU.

Основні режими відмов, що породжують шаблон «контейнери норм, хост плавиться»

  • Немає лімітів пам’яті (або вони неправильні). Один контейнер росте, поки хост пересилає й свопить усіх.
  • Ліміти задані, але swap неконтрольований. Контейнер тримається в межах RAM‑капа, але все одно спричиняє глобальний reclaim через патерни page cache і спільні ресурси.
  • Page cache та файловий IO домінують. Контейнери з інтенсивним IO можуть виштовхнути кеш, змушуючи інші робочі навантаження вичерпувати пам’ять і свопитися.
  • Overcommit + піки навантаження. Багато сервісів одночасно алокує агресивно; ви не отримуєте миттєвого OOM, натомість — churn.
  • Політика OOM уникає вбивань. Система свопить замість швидкого відмовлення, жонглюючи доступністю у найгірший спосіб.

Ще одна деталь: телеметрія на рівні контейнера може вводити в оману. Деякі інструменти показують використання пам’яті cgroup, але не біль хостового reclaim.
Ви бачите контейнери «в межах лімітів», поки хост проводить весь день, переставляючи сторінки.

Жарт #1: Swap — як складське приміщення: здається, що все організовано, поки не зрозумієш, що ти щомісяця платиш за те, чого потребуєш щодня.

Основи пам’яті Linux, які вам справді потрібні

Вам не треба зубрити код ядра. Потрібні кілька концептів, щоб міркувати про swap‑шторм без забобонів.

Робочий набір проти виділеної пам’яті

Більшість застосунків алокують пам’ять, яку не завжди активно торкаються. Ядру байдуже на «виділену» пам’ять, йому важливі
«останні використані» сторінки.
Ваш робочий набір — це сторінки, які ви торкаєтеся часто, і їх виштовхування боляче.

Swap‑шторм відбувається, коли робочий набір не вміщується, або коли він вміщується, але ядро змушене весь час перемішувати сторінки через
конкурентні вимоги (page cache, інші cgroup, або один агрегатор, що постійно засмічує пам’ять).

Анонімна пам’ять проти файл‑підтримуваної

  • Анонімна: heap, stack — підлягає swap.
  • Файл‑підтримувана: page cache — можна витиснути без swap (просто перечитати з файлу), якщо сторінки не dirty.

Коли ви запускаєте бази даних, кеші, JVM і сервіси з інтенсивними логами на одному хості, анонімне і файл‑підтримуване звільнення
пам’яті взаємодіють доволі «цікаво». «Цікаво» тут означає «постмортем о 2‑й ночі».

Reclaim, kswapd і direct reclaim

Ядро намагається звільняти пам’ять у фоні (kswapd). При сильному тиску процеси самі можуть увійти в
direct reclaim — вони зависають, намагаючись звільнити пам’ять. Саме там губиться латентність.

Чому swap‑шторм відчувається як проблема CPU

Reclaim спалює CPU. Стиснення може спалювати CPU (zswap/zram). Підвантаження сторінок також витрачає CPU і IO.
І потоки вашого застосунку можуть бути заблоковані, що заплутує графіки використання: низький user CPU, високий system CPU, високий IO wait.

cgroups, Docker і гострі краї навколо swap

Docker використовує cgroups для обмеження ресурсів. Але «обмеження пам’яті» — це набір варіантів залежно від версії ядра,
cgroup v1 проти v2 і налаштувань Docker.

cgroup v1 проти v2: практичні відмінності для swap‑штормів

У cgroup v1 пам’ять і swap керувалися окремими параметрами (memory.limit_in_bytes, memory.memsw.limit_in_bytes),
і облік swap міг бути вимкнений. У cgroup v2 пам’ять більш уніфікована й інтерфейс чистіший:
memory.max, memory.swap.max, memory.high, плюс метрики тиску.

Якщо ви на cgroup v2 і не використовуєте memory.high, ви втрачаєте один із найкращих інструментів, щоб не дозволити одній cgroup перетворити
хост на своп‑тостер.

Прапори пам’яті Docker: що вони справді означають

  • --memory: жорсткий ліміт. При перевищенні cgroup спробує звільнити; якщо не вийде — OOM (всередині тієї cgroup).
  • --memory-swap: у багатьох налаштуваннях — ліміт total memory+swap. Семантика варіюється; на деяких системах ігнорується без обліку swap.
  • --oom-kill-disable: майже завжди погана ідея в продакшені. Це заохочує хост страждати довше.

Те, що контейнер «працює», поки хост плавиться, часто — результат політичного рішення:
ми сказали системі «не вбивати, просто намагайся сильніше». Ядро послухалося.

Цитата, яку варто втатуйте у свої runbook‑и

«Надія — це не стратегія.» — перефразована думка, поширена в інженерних/операційних колах; сенс залишається.

Швидкий порядок діагностики

Це порядок дій, який я використовую, коли хтось каже «хост повільний», і я підозрюю тиск на пам’ять. Він призначений
допомогти швидко прийняти рішення: вбити, обмежити, перемістити або налагодити.

Спочатку: підтвердіть, що це саме swap‑шторм (а не просто «використовується swap»)

  1. Перевірте активність swap (швидкості swap‑in/out) і major faults.
  2. Перевірте PSI memory pressure на предмет тривалих затримок.
  3. Перевірте, чи не насичена підсистема IO (swap — це IO).

По‑друге: знайдіть винну cgroup/container

  1. Порівняйте використання пам’яті по контейнерах, включно з RSS та кешем.
  2. Перевірте, які cgroup тригерять OOM або високе reclaim.
  3. Шукайте патерни навантажень (зростання heap у JVM, батч‑завдання, сплески логів, компакти, перестроювання індексів).

По‑третє: вирішіть миттєве пом’якшення

  • Якщо латентність важливіша за завершення: відмовляйте швидко (жорсткі ліміти, дозволяйте OOM, перезапускайте чисто).
  • Якщо завершення важливіше за латентність: ізолюйте (виділені вузли, зменшити swappiness, контрольований swap, повільніше але стабільно).
  • Якщо ви в сліпому режимі: додайте PSI + метрики пам’яті по cgroup спочатку. Налаштовувати без видимості — гра в азартні ігри.

Практичні завдання: команди, виводи, рішення

Це команди, які я реально виконую на хості. Кожне завдання включає, що означає вивід і яке рішення воно дозволяє прийняти.
Налаштуйте імена інтерфейсів і шляхи під своє оточення.

Завдання 1: Підтвердити наявність swap і скільки використовується

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            62Gi        54Gi       1.2Gi       1.1Gi       6.8Gi       2.3Gi
Swap:           16Gi        12Gi       4.0Gi

Значення: Swap активно використовується (12Gi) і доступна пам’ять низька (2.3Gi). Це не доказ шторму, але підозріло.
Рішення: Перейдіть до метрик активності; одне лише використання swap не дає підстав діяти.

Завдання 2: Виміряти активність swap і тиск сторінок

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 3  1 12453248 312000  68000 512000   60  210   120   980 1200 2400 12 18 42 28  0
 2  2 12454800 298000  66000 500000  180  640   200  1800 1800 3200  8 22 30 40  0
 4  2 12456000 286000  64000 490000  220  710   260  2100 1900 3300  9 23 24 44  0
 3  3 12456800 280000  62000 482000  240  680   300  2000 2000 3500 10 24 22 44  0
 2  2 12458000 276000  60000 475000  210  650   280  1900 1950 3400  9 23 25 43  0

Значення: Нетривіальні si/so (swap‑in/out) щосекунди і високий wa (IO wait). Це активна пагінація.
Рішення: Вважати це штормом. Далі: визначити, чи насичений IO і яка cgroup створює тиск пам’яті.

Завдання 3: Перевірити PSI на затримки пам’яті (рівень хоста)

cr0x@server:~$ cat /proc/pressure/memory
some avg10=18.40 avg60=12.12 avg300=8.50 total=192003210
full avg10=6.20 avg60=3.90 avg300=2.10 total=48200321

Значення: full тиск означає, що завдання часто стоять у паузі, бо reclaim не встигає. Це сильно корелює з піками латентності.
Рішення: Перестаньте шукати «баги CPU». Це питання пам’яті. Знайдіть винного і обмежте/вбийте/ізолюйте.

Завдання 4: Визначити, чи у вас cgroup v1 чи v2

cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs

Значення: Активний cgroup v2. Ви можете використати memory.high і memory.swap.max.
Рішення: Віддавайте перевагу контролям v2; уникайте старих порад для v1, які тут не застосовні.

Завдання 5: Побачити найбільших споживачів пам’яті по контейнерах

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME              CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
a1b2c3d4e5f6   api-prod          35.20%    1.8GiB / 2GiB         90.00%    2.1GB / 1.9GB  120MB / 3GB  210
b2c3d4e5f6g7   search-indexer     4.10%    7.4GiB / 8GiB         92.50%    150MB / 90MB   30GB / 2GB   65
c3d4e5f6g7h8   metrics-agent      0.50%    220MiB / 512MiB       42.97%    20MB / 18MB    2MB / 1MB    14

Значення: search-indexer близький до свого ліміту пам’яті і виконує величезний блоковий IO (30GB reads/writes), що може бути збиванням page cache, компактом або spill’ом.
Рішення: Заглибитися в метрики cgroup цього контейнера (reclaim, swap, OOM‑події).

Завдання 6: Перевірити ліміти пам’яті + swap (v2) для підозрілого контейнера

cr0x@server:~$ CID=b2c3d4e5f6g7
cr0x@server:~$ CG=$(docker inspect -f '{{.HostConfig.CgroupParent}}' "$CID")
cr0x@server:~$ docker inspect -f '{{.Id}} {{.Name}}' "$CID"
b2c3d4e5f6g7h8i9j0 /search-indexer
cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.max
8589934592
cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.swap.max
max

Значення: Ліміт RAM — 8GiB, але swap — необмежений (max). Під тиском ця cgroup може інтенсивно використовувати swap.
Рішення: Встановити memory.swap.max або налаштувати Docker, щоб обмежити swap для контейнерів, які не повинні сторити.

Завдання 7: Перевірити події по cgroup: чи ви досягаєте reclaim/OOM?

cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.events
low 0
high 1224
max 18
oom 2
oom_kill 2

Значення: cgroup часто досягала high і мала OOM‑вбивання. Це не «випадкова нестабільність»; це проблема розмірів.
Рішення: Підняти пам’ять, зменшити робочий набір або прийняти рестарти, але запобігти глобальному thrash, обмеживши swap і використавши memory.high.

Завдання 8: Спостерігати використання swap по процесах (знайти справжнього хижака)

cr0x@server:~$ sudo smem -rs swap | head -n 8
  PID User     Command                         Swap      USS      PSS      RSS
18231 root     java -jar indexer.jar          6144M    4096M    4200M    7000M
 9132 root     python3 /app/worker.py          820M     600M     650M    1200M
 2210 root     dockerd                          90M      60M      70M     180M
 1987 root     containerd                       40M      25M      30M      90M
 1544 root     /usr/bin/prometheus              10M     900M     920M     980M
 1123 root     /usr/sbin/sshd                    1M       2M       3M       8M

Значення: Java‑індексер має 6GiB у swap. Це пояснює «контейнер живий, але повільний»: він постійно фолить сторінки.
Рішення: Якщо це навантаження не повинно свопитися — обмежте його й примусово OOM/restart. Якщо повинно — ізолюйте на хості з швидшим swap і меншим contention.

Завдання 9: Перевірити насичення диска (swap — це IO; IO — це латентність)

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server) 	01/02/2026 	_x86_64_	(16 CPU)

avg-cpu:  %user %nice %system %iowait  %steal %idle
          10.21  0.00   22.11   41.90    0.00  25.78

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz  aqu-sz  %util
nvme0n1        120.0   12800.0     2.0   1.64    18.2   106.7    210.0   24400.0     8.0   3.67   32.5   116.2    9.80  99.20

Значення: %util близько 100% і високий await. NVMe насичений; swap‑in/out буде чергуватися, спричиняючи затримки скрізь.
Рішення: Негайна міра: зменшити тиск пам’яті (вбити винуватця, знизити конкурентність). Довгостроково: відокремити swap від IO‑шляху або використовувати швидше сховище.

Завдання 10: Подивитися, які процеси застрягли в reclaim або IO wait

cr0x@server:~$ ps -eo pid,stat,wchan:20,comm --sort=stat | head -n 12
  PID STAT WCHAN                COMMAND
18231 D    io_schedule          java
19102 D    io_schedule          java
 9132 D    io_schedule          python3
24011 D    balance_pgdat        postgres
24022 D    balance_pgdat        postgres
 2210 Ssl  ep_poll              dockerd
 1987 Ssl  ep_poll              containerd

Значення: Стан D + io_schedule вказує на не переривний сон, що чекає на IO. balance_pgdat натякає на direct reclaim.
Рішення: Ваша латентність — на рівні ядра. Припиніть масштабувати трафік; це погіршить чергу. Зменшіть навантаження або зупиніть винуватця.

Завдання 11: Перевірити логи ядра на OOM і попередження reclaim

cr0x@server:~$ sudo dmesg -T | tail -n 12
[Thu Jan  2 10:14:22 2026] Memory cgroup out of memory: Killed process 18231 (java) total-vm:12422392kB, anon-rss:7023120kB, file-rss:10244kB, shmem-rss:0kB
[Thu Jan  2 10:14:22 2026] oom_reaper: reaped process 18231 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
[Thu Jan  2 10:14:25 2026] Out of memory: Killed process 9132 (python3) total-vm:2048320kB, anon-rss:1192200kB, file-rss:9120kB, shmem-rss:0kB

Значення: Відбулись OOM‑вбивання cgroup. Це насправді краще, ніж глобальні swap‑штормі — якщо ваш сервіс може коректно перезапуститися.
Рішення: Підтвердіть політику перезапуску, відкоригуйте ліміти і встановіть очікування: контрольований OOM кращий за неконтрольований колапс хоста.

Завдання 12: Перевірити swappiness хоста і положення overcommit

cr0x@server:~$ sysctl vm.swappiness vm.overcommit_memory vm.overcommit_ratio
vm.swappiness = 60
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

Значення: Swappiness 60 — приблизно за замовчуванням і може бути занадто прагматичним на змішаних контейнерних хостах. Overcommit — евристичний (0).
Рішення: Якщо хост працює latency‑чутливі сервіси, розгляньте зниження swappiness і жорсткіший overcommit, але лише після введення per‑cgroup лімітів.

Завдання 13: Перевірити статус zswap/zram (іноді допомагає, завжди ховає проблему)

cr0x@server:~$ grep -H . /sys/module/zswap/parameters/enabled /sys/block/zram0/disksize 2>/dev/null
/sys/module/zswap/parameters/enabled:Y

Значення: zswap увімкнений; сторінки swap можуть бути стиснені в RAM. Це знижує IO, але підвищує CPU і може маскувати тиск до критичної точки.
Рішення: Залишайте його, якщо це дає стабільність. Не використовуйте як ліцензію запускати необмежені навантаження пам’яті.

Завдання 14: Перевірити припущення Docker daemon і ядра щодо обліку пам’яті

cr0x@server:~$ docker info | sed -n '1,40p'
Client:
 Version:    26.1.0
 Context:    default
 Debug Mode: false

Server:
 Containers: 38
  Running: 33
  Paused: 0
  Stopped: 5
 Server Version: 26.1.0
 Storage Driver: overlay2
 Cgroup Driver: systemd
 Cgroup Version: 2
 Kernel Version: 6.5.0
 Operating System: Ubuntu 24.04 LTS
 OSType: linux

Значення: systemd + cgroup v2. Добре. Ваші контролі є; їх просто треба використовувати.
Рішення: Впроваджуйте політики, що знають про cgroup v2 (memory.high, memory.swap.max), а не старі рекомендації.

Три корпоративні міні‑історії з полів пам’яті

Міні‑історія 1: Інцидент через неправильне припущення

Середня SaaS‑компанія запускала флот індексерів у Docker на кількох потужних хостах. У них був swap ввімкнений «на безпеку»,
і були встановлені ліміти пам’яті для контейнерів. Усі почувалися відповідальними. Усі спали.

Під час навантаженого тижня backlog індексації виріс і інженер підвищив concurency воркерів всередині контейнера.
Контейнер більшість часу тримався під своїм --memory капом, але патерн алокацій став стрибкоподібним:
великі тимчасові буфери, інтенсивний файловий IO і агресивне кешування. Хост почав свопити.

Неправильне припущення було тонким: «Якщо контейнери мають ліміти пам’яті, хост не свопиться в безодню».
Насправді, ліміти запобігають безмежному використанню RAM однією cgroup, але вони не гарантують справедливість на рівні хоста
і не запобігають глобальному reclaim‑болю — особливо коли swap необмежений і шлях IO спільний.

Симптоми були класичними. Латентність API подвоїлася, потім потроїлася. SSH‑логіни зависали. Моніторинг показував «пам’ять контейнера в межах ліміту»,
тож команда годинами шукала налаштування мережі і БД. Нарешті хтось запустив cat /proc/pressure/memory
і історія написала себе сама.

Виправлення не було екзотичним. Вони встановили memory.swap.max для індексерів, додали memory.high щоб їх гальмувати перед OOM,
і перемістили індексування на виділені вузли з власним IO‑бюджетом. Найбільший прогрес приніс нудний крок:
задокументувати, що «ліміти пам’яті не є захистом хоста» і зробити це вимогою при деплої.

Міні‑історія 2: Оптимізація, яка обернулася проти

Інша організація мала мульти‑тенантний логінговий пайплайн. Щоб зменшити навантаження на диск, вони увімкнули zswap і збільшили розмір swap.
Початкові результати виглядали чудово: менше сплесків записів, плавніші IO‑графіки, менше миттєвих OOM.

Потім стався дрібний інцидент: клієнт увімкнув детальні логи плюс компресію на рівні застосунку.
Обсяг логів виріс, CPU піднявся, і тиск на пам’ять зріс. З zswap ядро спочатку стискало сторінки у RAM.
Це зменшило IO swap, але збільшило CPU‑час на стиснення і reclaim.

На панелі це виглядало як «насичення CPU», а не «помилка пам’яті». Команда підправила ліміти CPU, додала ядра та збільшила хости.
Система погіршилася. Більше RAM означало більше кешів і більше churn; більше CPU означало, що zswap міг стиснути більше, відтягуючи очевидний провал.
Джиттер латентності став постійним.

Провал не був у zswap сам по собі. Провал полягав у тому, що його сприймали як оптимізацію продуктивності, а не як буфер тиску.
Справжня проблема — неконтрольована розростання пам’яті в парсер‑стадії і відсутність memory.high для backpressure. Своп був симптомом,
zswap — підсилювач, що ускладнив виявлення.

Вони виправили це, встановивши суворі ліміти по кожній стадії, додавши backpressure у пайплайн і використовуючи zswap тільки на вузлах,
призначених для батч‑обробки, де латентність не важлива. Також змінили алерт: тривале PSI memory > порог стало page‑worthy подією.

Міні‑історія 3: Нудна, але правильна практика, що врятувала ситуацію

Фінансова команда запускала суміш API для клієнтів і нічний батч‑звір у тому ж Kubernetes‑кластері.
Вони були надзвичайно консервативні: request/limit по сервісах були вказані, пороги евікшенів переглядалися щоквартально,
і у кожного pool‑а вузлів була письмова «політика swap». Це не було захопливо. Саме тому в них не було захопливих аварій.

Однієї ночі батч‑завдання почало використовувати більше пам’яті після оновлення бібліотеки від вендора. Воно росло поступово, не вибухово.
В команді з менш дисципліною це перетворилося б на повільний swap‑шторм і інцидент з невизначеним звітом.

Їхня система зробила банальну річ: завдання досягло ліміту пам’яті, було OOM‑вбите і перезапущене з обмеженою паралельністю
(попередньо визначений fallback). Батч виконався довше. API лишалися швидкими. On‑call отримав конкретний алерт:
«batch job OOMKilled; host PSI normal».

Наступного ранку вони відкотили бібліотеку, зареєстрували баг у upstream і назавжди підкорегували request пам’яті для цього job.
Ніхто не писав героїчних повідомлень у Slack. Оце й суть. Правильна практика — ліміти плюс контрольований провал — запобігла явищу «хост плавиться».

Рішення, які працюють: ліміти, OOM, swappiness і моніторинг

1) Встановлюйте ліміти пам’яті на кожен важливий контейнер

Відсутність ліміту — це теж рішення. Це рішення, що хост буде вашим лімітом. Хост — поганий ліміт, бо він виходить з ладу колективно: всі робочі навантаження страждають, потім усе колапсує.

Використовуйте per‑service ліміти, підібрані під робочий набір, а не під фантазії про пікові алокації. Для JVM це означає свідомо задавати heap
і залишати запас для off‑heap і native алокацій.

2) Використовуйте memory.high (cgroup v2) щоб гальмувати перед OOM

memory.max — це обрив. memory.high — це обмеження швидкості. Коли ви встановлюєте memory.high, ядро починає reclaim і гальмувати алокації після перевищення,
що допомагає зменшити глобальний thrash.

Для стрибкоподібних сервісів memory.high, трохи нижчий за memory.max, часто дає систему, яка повільніше поводиться під тиском, але лишається контрольованою, замість хаотичної.

3) Обмежуйте swap по навантаженню або вимикайте його для latency‑чутливих сервісів

Необмежений swap — шлях до серверу, що «живий», але непридатний. Для сервісів, що мають бути чутливими, віддавайте перевагу або нульовому використанню swap, або дуже малому допуску.

Якщо swap потрібен для батчових навантажень — ізолюйте їх. Swap не безкоштовний; це рішення обміняти latency на завершення.
Змішувати «має бути швидко» з «може бути повільно» на одному swap‑підтримуваному вузлі — соціологічний експеримент.

4) Знизьте swappiness (обережно) на змішаних контейнерних хостах

vm.swappiness контролює, наскільки агресивно ядро свопить анонімну пам’ять проти витіснення page cache.
На хостах з базами даних або low‑latency сервісами нижче значення (наприклад 10 або 1) може зменшити свопінг «гарячих» сторінок.

Не копіюйте vm.swappiness=1 усюди механічно. Якщо ваш хост покладається на swap щоб уникнути OOM і ви знизите swappiness не виправивши розміри,
ви просто отримаєте заміну swap‑шторму на OOM‑шторм.

5) Віддавайте перевагу контрольованому OOM над неконтрольованим thrash

Це те, що люди емоційно не люблять, але операційно — так: для багатьох сервісів перезапуск дешевший за пагінацію.
Якщо сервіс не може працювати в межах бюджету, вбивство його — чесна дія.

Уникайте відключення OOM‑кілера для контейнерів, якщо ви не розумієте наслідки. Вимкнення OOM‑kill — шлях перетворити один поганий процес на інцидент усього хоста.

Жарт #2: Вимкнути OOM‑кілер — як прибрати пожежну сигналізацію через те, що вона голосна — тепер ви можете з комфортом спостерігати вогонь.

6) Слідкуйте за PSI і метриками reclaim, а не лише за «використана пам’ять»

Найкорисніші алерти — ті, що говорять «ядро затримує задачі».
PSI дає це безпосередньо. Поєднуйте його з швидкостями swap‑in/out і латентністю диска.

7) Розділяйте IO‑шляхи, коли swap неминучий

Якщо swap і ваш основний робочий шлях використовують один диск, ви створюєте зворотний зв’язок: swap викликає черги IO,
черги IO уповільнюють застосунки, повільні застосунки тримають пам’ять довше, тиск пам’яті зростає, swap зростає. Вітаємо, ви побудували карусель.

На серйозних системах swap має лежати на швидкому сховищі, іноді на окремих пристроях, або використовують zram для обмеженого аварійного запасу
(але з урахуванням CPU‑запасу).

8) Зробіть бюджет пам’яті частиною розгортання, а не постмортему

Корпоративна реальність: команди не «запам’ятають додати ліміти пізніше». Вони відправлять сьогодні. Ваше завдання — перетворити це на запобіжник:
CI‑перевірки для Compose, admission‑політики для Kubernetes і рантайм‑алерти «немає ліміту».

Поширені помилки: симптом → корінь → виправлення

1) Симптом: Середнє навантаження хоста величезне; CPU здається помірним

Корінь: Потоки заблоковані в IO wait під час swap‑in або direct reclaim. Load рахує runnable + uninterruptible tasks.

Виправлення: Підтвердіть vmstat/iostat/ps (D‑стан). Негайно зменште тиск пам’яті; обмежте/вбийте винуватця; додайте memory.high.

2) Симптом: Контейнери показують «в межах ліміту», але хост інтенсивно свопить

Корінь: Ліміти є, але swap необмежений, churn page cache змушує глобальний reclaim, або кілька контейнерів разом перевищують здатність хоста.

Виправлення: Встановіть per‑cgroup swap‑ліміти (memory.swap.max) і реалістичні бюджети пам’яті. Не передплатовуйте ресурси без явної політики.

3) Симптом: Випадкові сплески латентності по незалежних сервісах

Корінь: Глобальний reclaim і черги IO створюють крос‑сервісну залежність. Один меморі‑винуватець карає усіх.

Виправлення: Ізолюйте шумні навантаження на окремі вузли/пули; застосуйте ліміти; слідкуйте за PSI.

4) Симптом: «Ми додали більше swap і стало гірше»

Корінь: Більше swap збільшує час, протягом якого хост може залишатися в деградірованому стані пагінації, підсилюючи хвостову латентність і операційну плутанину.

Виправлення: Розглядайте swap як аварійний буфер, а не як ємність. Віддавайте перевагу OOM/restart для latency‑чутливих сервісів, або ізолюйте батч‑роботи.

5) Симптом: Відбуваються OOM‑вбивання, але хост досі повільний

Корінь: Swap залишається заповненим; reclaim і refaulting тривають; черга IO ще не порожня; кеші фрагментовані.

Виправлення: Після видалення винуватця дайте системі час на відновлення; зменшіть IO‑тиск; розгляньте тимчасове скидання кешів лише як крайній захід і усвідомлюючи його вплив.

6) Симптом: «Вільної пам’яті» близько нуля; хтось панікує

Корінь: Linux використовує вільну RAM для кешу; низький free — нормальний стан, якщо available низький і PSI низький.

Виправлення: Навчіть команди дивитися на available і PSI, а не на free. Аларміть за тиском і churn, а не за естетикою.

7) Симптом: Після увімкнення zswap/zram CPU зріс і пропускна здатність впала

Корінь: Накладні витрати стиснення плюс продовжений тиск пам’яті. Ви перемістили витрати з диска на CPU.

Виправлення: Увімкнюйте лише коли є CPU‑запас; обмежте використання swap; виправте реальний бюджет пам’яті.

8) Симптом: «Swappiness=1 вирішив проблему» (поки не наступного тижня)

Корінь: Зниження swap тимчасово приховало погане розмірування пам’яті; тиск все ще є і може тепер стати різким OOM.

Виправлення: Розміріруйте робочі навантаження, встановіть ліміти, додайте memory.high/backpressure. Налаштовуйте swappiness як завершальний крок, а не перший акт.

Чеклісти / покроковий план

Негайна реакція на інцидент (15 хвилин)

  1. Запустіть vmstat 1 і cat /proc/pressure/memory. Підтвердіть активну пагінацію + затримки.
  2. Запустіть iostat -xz 1. Підтвердіть насичення диска / await.
  3. Знайдіть винуватця: docker stats, потім per‑process swap з smem або події cgroup.
  4. Міри пом’якшення:
    • Зменшити concurency / трафік.
    • Зупинити контейнер‑винуватець або перезапустити його з меншим споживанням пам’яті.
    • За потреби тимчасово перемістити винуватця на виділений хост.
  5. Підтвердіть відновлення: швидкості swap‑in/out падають, full PSI наближається до ~0, IO await нормалізується.

План стабілізації (той самий день)

  1. Встановіть ліміти пам’яті для всіх контейнерів без них.
  2. На cgroup v2: додайте memory.high для стрибкоподібних навантажень, щоб зменшити thrash.
  3. Обмежте swap там, де це доречно (memory.swap.max), особливо для latency‑чутливих сервісів.
  4. Перегляньте використання --oom-kill-disable; видаліть, якщо немає вагомої причини.
  5. Розділіть ролі вузлів: батч проти latency‑чутливих сервісів не мають ділити один memory/IO‑фейт.

План підсилення (на наступний спринт)

  1. Додайте алерти на PSI memory (some і full) із тривалими порогами.
  2. Додайте алерти на швидкості swap‑in/out, major faults і disk await.
  3. Реалізуйте policy‑as‑code: лінтери Compose або admission‑контролі Kubernetes, що вимагають memory limits.
  4. Документуйте бюджети пам’яті по сервісах і поведінку при рестарті (що відбувається на OOM, як швидко відновлюється).
  5. Протестуйте під навантаженням із тиском на пам’ять: імітуйте spike concurrency і перевірте, що хост лишається інтерактивним.

Питання та відповіді

1) Чи завжди swap поганий для Docker‑хостів?

Ні. Swap — інструмент. Він корисний як аварійний буфер і для батчових навантажень. Він небезпечний, коли стає костилем для
недостатньо розмірених сервісів і робить хост «живим, але непридатним».

2) Чому мої контейнери показують низький CPU, а система повільна?

Тому що вони заблоковані. У swap‑штормі потоки часто сидять у IO wait або direct reclaim. Ваш графік CPU не покаже «очікування на диск»,
якщо ви не дивитесь на iowait, PSI або заблоковані процеси.

3) Чи варто вимкнути swap, щоб запобігти шторми?

Якщо ви запускаєте latency‑чутливі сервіси і маєте хороші ліміти, відключення swap може підвищити передбачуваність.
Але це також збільшує ймовірність OOM. Правильний підхід зазвичай: спочатку встановіть ліміти й політики, а потім вирішіть стосовно swap.

4) У чому різниця в користувацькому впливі між OOM і swap‑трясінням?

OOM — різкий і гучний: процес помирає і перезапускається. Swap‑трясіння — довге і підступне: усе повільне, тайм‑аути каскадують,
виникають вторинні відмови. У багатьох системах контрольований OOM є меншим злом.

5) Чому додавання RAM іноді не вирішує проблему?

Більше RAM допомагає лише якщо робочий набір вміщується і ви зменшите churn. Якщо навантаження «заповнює» пам’ять (кеші, heap, компакти),
ви можете отримати той самий тиск, але на більшому полі.

6) Як встановити swap‑ліміти для Docker‑контейнерів на cgroup v2?

Прапори Docker можуть бути непослідовними в різних налаштуваннях. Надійний шлях — використовувати контролі cgroup v2 напряму через systemd‑sсopes
або runtime‑налаштування, переконавшись, що memory.swap.max встановлено для cgroup контейнера. Перевіряйте читанням файлу в /sys/fs/cgroup.

7) Чому «вільна пам’ять» низька, коли система начебто в порядку?

Linux використовує вільну RAM під кеш. Цей кеш підлягає reclaim. Дивіться «available» і метрики тиску, а не «free».
Низький free + низький PSI зазвичай нормально.

8) Які метрики слід відстежувати, щоб рано помітити swap‑шторм?

Тривалий PSI memory (/proc/pressure/memory), швидкості swap‑in/out, major page faults, disk await/%util,
та per‑cgroup memory events (high/max/oom). Аларми мають спрацьовувати на тривалі умови, а не на одно‑секундні сплески.

9) Чи може overlay2 або поведінка файлової системи сприяти swap‑штормам?

Посередньо — так. Інтенсивний churn метаданих файлової системи і write amplification може наситити IO, роблячи swap‑in/out набагато болючішим.
Це не створює тиск пам’яті безпосередньо, але прискорює перетворення тиску пам’яті в системний outage.

10) Чи кращий zram за дисковий swap для контейнерів?

zram уникає дискового IO через стиснення в RAM. Він може пом’якшити шторми, якщо є CPU‑запас, але це все ще swap — латентність збільшується,
і він може ховати проблеми з ємністю. Використовуйте як буфер, а не як дозвіл на погане розмірування.

Практичні наступні кроки

Якщо ваш Docker‑хост «плавиться», тоді як контейнери «працюють», припиніть дискусії про моральну сторону swap.
Сприймайте його як інструмент: борговий інструмент продуктивності з непередбачуваною процентною ставкою і небезпечним компаундінгом.

Зробіть ці кроки наступними, у такому порядку:

  1. Інструментуйте тиск: додайте алерти і дашборди на базі PSI разом із swap‑активністю і IO‑латентністю.
  2. Бюджет пам’яті по сервісу: встановіть реалістичні ліміти; приберіть «необмежено» як дефолт.
  3. Керуйте поведінкою swap: обмежуйте swap по навантаженню (особливо для latency‑чутливих) і розгляньте memory.high як throttle.
  4. Віддавайте перевагу контрольованому провалу: дозвольте cgroup OOM + рестарт для сервісів, що можуть відновитися, замість глобального thrash.
  5. Ізолюйте проблемні навантаження: індексація, компакти і все, що любить роздуватися — не повинно ділити вузли з низьколатентними API.

Ваша мета — не «ніколи не використовувати swap». Ваша мета — «не дозволити swap вирішувати часову шкалу інциденту». Ядро робитиме те, що ви просите.
Просіть те, що можна пережити.

← Попередня
Proxmox Windows VM без мережі: виправлення драйвера VirtIO NIC, які реально працюють
Наступна →
Debian 13: systemd timers vs cron — перехід заради надійності (і уникнення типових пасток)

Залишити коментар