Сторінка спрацювала о 03:12. «Зростає затримка API, вузли почали свопити, один контейнер убито OOM.» Ви заходите в систему і бачите звичне: «used» пам’ять висока, керівництво на зв’язку, і хтось уже порадив «просто додайте RAM».
Витоки пам’яті — найгірший вид тихого інциденту: все працює… поки не перестане, і тоді це відбувається в найневигідніший момент. Хитрість на Debian 13 не в героїці. Вона в збиранні доказів, не перетворюючи продакшн на лабораторію, у внесенні невеликих відкатних змін та у завжди чіткій відмінності реальних витоків від очікуваної поведінки пам’яті.
Цікаві факти й контекст (коротко, конкретно)
- Linux не «звільняє пам’ять» так, як цього хочеться людям. Ядро агресивно використовує RAM для page cache; метрика
MemAvailableмає більше значення, ніж «free». - cgroups двічі змінили правила гри. cgroups v1 дозволяли конфігурування по контролеру; v2 об’єднав їх, а systemd зробив це стандартною площиною контролю для сервісів.
- /proc старший за більшість інструкцій для інцидентів. Він є канонічним інтерфейсом для статистики процесів ще з ранніх днів Linux і все ще найкращий для видимості без порушення продакшну.
- «OOM killer» — це не один тип події. Є system-wide OOM, cgroup OOM і user-space «померли, бо malloc повернув NULL». Вони виглядають схоже в дашбордах і зовсім по-різному в логах.
- Поведінка malloc — це політика, а не фізика. Розподілювач glibc використовує арени й може не повертати пам’ять ОС швидко; це часто виглядає як витік, хоча таким не є.
- Overcommit — це фіча. Linux може обіцяти більше віртуальної пам’яті, ніж є насправді; це нормально, поки не станеться збій. Режим відмови залежить від налаштувань overcommit і навантаження.
- eBPF зробив «спостерігати без зупинки» повсякденною практикою. Ви можете трасувати алокації та фолти з набагато меншим падінням продуктивності, ніж застарілі підходи з ptrace.
- Java винайшла нові категорії «витоків». Витоки в купі — одне; нативне відстеження пам’яті з’явилося через direct buffers, стек потоків і JNI, які можуть тихо роздуватися.
- Smaps — недооцінений герой. /proc/<pid>/smaps є деталізованим, відносно дорогим для читання на масштабі і абсолютно вирішальним, коли потрібно знати, чим насправді зайнято пам’ять.
Що насправді означає «витік пам’яті» в Linux
У продакшні «витік пам’яті» вживають для чотирьох різних проблем. Лише одна — класичний витік.
Якщо ви неправильно назвете проблему, то «виправите» не те і відчуєте тимчасову продуктивність, поки сторінка не знову не впаде.
1) Справжній витік: пам’ять стає недоступною й ніколи не звільняється
Це підручниковий випадок: алокації тривають, frees не відповідають, і live set безкінечно зростає.
Ви побачите монотонне зростання RSS або використання heap, що не корелює з навантаженням.
2) Утримана пам’ять: досяжна, але ненавмисно утримується
Кеші без евікції, незмірні мапи з ключами по user ID, контексти запитів, що зберігаються глобально. Технічно це не «витік», бо програма все ще має до цього доступ, але практично ефект той самий.
3) Поведінка алокатора: пам’ять внутрішньо звільнена, але не повернута ОС
glibc malloc, фрагментація, арени на потік і ефекти «high-water mark» можуть тримати RSS високим навіть після проходження пікового навантаження.
Додаток може бути здоровим. Ваші графіки все одно його звинувачуватимуть.
4) Тиск на kernel/page cache: RAM використовується, але не вашим процесом
«Used memory» зростає, бо ядро кешує сторінки файлів. Під тиском це повинно знижуватися.
Якщо цього не відбувається, можлива заторність через dirty pages, повільний IO або правила відбирання пам’яті cgroup, які роблять кеш «липким».
Завдання — з найменшим радіусом ураження визначити, до якої категорії ви належите. Діагностика панікою дорого коштує.
Одна перефразована ідея від Werner Vogels (CTO Amazon): Все рано чи пізно ламається; будуйте системи та звички, які роблять відмови пережитними.
Швидкий план діагностики (перший/другий/третій)
Перший: підтвердіть, що це проблема процесу, а не «Linux як Linux»
- Подивіться на MemAvailable, активність swap і major faults. Якщо MemAvailable у нормі, а swap тихий — ймовірно, у вас немає гострого витоку.
- Визначте топ-споживачів RSS і чи є зростання монотонним.
- Перевірте, чи домінує пам’ять anon (heap) або file (cache, mmaps).
Другий: вирішіть, чи на межі cgroup/systemd
- Чи обмежено сервіс через systemd MemoryMax? Якщо так, витоки будуть виглядати як cgroup OOM, а не system-wide OOM.
- Збирайте memory.current, memory.events і RSS процесів у межах cgroup юніта.
Третій: оберіть джерело доказів з найменшим порушенням
- /proc/<pid>/smaps_rollup для швидкого огляду PSS/RSS/Swap.
- cgroup v2 stats для відстеження на рівні сервісу та OOM-подій.
- Рідні профайлери мов (pprof, JVM NMT, Python tracemalloc), якщо їх можна ввімкнути без перезапуску.
- eBPF sampling, коли ви не можете торкатися додатка, але потребуєте атрибуції.
Жарт #1: Витоки пам’яті як офісні снеки — спочатку по трохи, а потім раптом все зникло і ніхто не визнає.
Практичні завдання: команди, значення виводу і рішення (12+)
Це дружні до продакшну дії. Більшість — лише читання. Декілька змінюють конфігурацію, і вони позначені відповідно до рішення, яке ви приймаєте.
Виконуйте їх по порядку; не пропускайте базові докази заради «крутих інструментів», поки ви їх не заслужили.
Завдання 1: Перевірте, чи ядро справді під тиском пам’яті
cr0x@server:~$ grep -E 'Mem(Total|Free|Available)|Swap(Total|Free)' /proc/meminfo
MemTotal: 65843064 kB
MemFree: 1234560 kB
MemAvailable: 18432000 kB
SwapTotal: 4194300 kB
SwapFree: 4096000 kB
Що це означає: MemFree низький (норма), MemAvailable все ще ~18 GB (добре), swap здебільшого вільний (добре).
Рішення: Якщо MemAvailable у нормі і swap не зменшується, ймовірно у вас немає гострого витоку. Перейдіть до підтвердження на рівні процесів перед тим, як будити всіх.
Завдання 2: Подивіться на активний свопінг та віддачу пам’яті
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
2 0 0 1234560 81234 21000000 0 0 5 12 820 1600 12 4 83 1 0
1 0 0 1200000 80000 21100000 0 0 0 8 790 1550 11 3 85 1 0
3 1 0 1100000 79000 21200000 0 0 0 120 900 1700 15 6 74 5 0
5 2 0 900000 78000 21300000 0 0 0 800 1200 2200 18 8 60 14 0
4 2 0 850000 77000 21350000 0 0 0 900 1180 2100 16 7 62 13 0
Що це означає: si/so нульові (немає swap-in/out), але wa (IO wait) зростає, а b (blocked) ненульовий.
Рішення: Якщо свопінг активний (si/so > 0 постійно), ви в аварійній зоні. Якщо ні, проблема може бути в IO-індукованих затримках або одному процесі, що роздувається, але ще не змушує своп.
Завдання 3: Визначте топ-споживачів RSS (швидка триаж)
cr0x@server:~$ ps -eo pid,ppid,comm,%mem,rss --sort=-rss | head -n 10
PID PPID COMMAND %MEM RSS
4123 1 api-service 18.4 12288000
2877 1 search-worker 12.1 8050000
1990 1 postgres 9.7 6450000
911 1 nginx 1.2 780000
Що це означає: api-service — головний мешканець пам’яті.
Рішення: Оберіть головного підозрюваного і тримайте фокус. Якщо кілька процесів зростають разом, підозрівайте спільний тиск кешу, ріст tmpfs або зміну навантаження.
Завдання 4: Підтвердіть монотонне зростання (не довіряйте одному знімку)
cr0x@server:~$ pid=4123; for i in 1 2 3 4 5; do date; awk '/VmRSS|VmSize/ {print}' /proc/$pid/status; sleep 30; done
Mon Dec 30 03:12:01 UTC 2025
VmSize: 18934264 kB
VmRSS: 12288000 kB
Mon Dec 30 03:12:31 UTC 2025
VmSize: 18959000 kB
VmRSS: 12340000 kB
Mon Dec 30 03:13:01 UTC 2025
VmSize: 19001000 kB
VmRSS: 12420000 kB
Mon Dec 30 03:13:31 UTC 2025
VmSize: 19042000 kB
VmRSS: 12510000 kB
Mon Dec 30 03:14:01 UTC 2025
VmSize: 19090000 kB
VmRSS: 12605000 kB
Що це означає: І VmSize, і VmRSS стабільно ростуть. Це або витік, або патерн утримання, а не одноразовий сплеск.
Рішення: Почніть збирати атрибуцію (smaps_rollup, метрики кучі, статистики алокатора). Також сплануйте пом’якшення (перезапуск, масштабування), бо у вас тепер є час до відмови.
Завдання 5: Визначте anon vs file-backed пам’ять (smaps_rollup)
cr0x@server:~$ pid=4123; cat /proc/$pid/smaps_rollup | egrep 'Rss:|Pss:|Private|Shared|Swap:'
Rss: 12631240 kB
Pss: 12590010 kB
Shared_Clean: 89240 kB
Shared_Dirty: 1024 kB
Private_Clean: 120000 kB
Private_Dirty: 12450000 kB
Swap: 0 kB
Що це означає: Переважно Private_Dirty анонімна пам’ять. Це зазвичай heap, стеки або анонімні mmaps.
Рішення: Зосередьтеся на алокаціях і утриманні додатка. Якщо домінувала б file-backed пам’ять, перевіряли б mmaps, кеші й файлові IO-патерни.
Завдання 6: Перевірте наявність cgroup OOM-подій під systemd (за замовчуванням у Debian 13)
cr0x@server:~$ systemctl status api-service.service --no-pager
● api-service.service - API Service
Loaded: loaded (/etc/systemd/system/api-service.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-30 02:10:11 UTC; 1h 2min ago
Main PID: 4123 (api-service)
Tasks: 84 (limit: 12288)
Memory: 12.4G (peak: 12.6G)
CPU: 22min 9.843s
CGroup: /system.slice/api-service.service
└─4123 /usr/local/bin/api-service
Що це означає: systemd відстежує поточну та пікову пам’ять; це вже цінна форма для розуміння профілю витоку.
Рішення: Якщо ви бачите «Memory: … (limit: …)» або недавні OOM-повідомлення, переходьте до статистики cgroup.
Завдання 7: Прочитайте лічильники пам’яті cgroup v2 і OOM-події
cr0x@server:~$ cg=$(systemctl show -p ControlGroup --value api-service.service); echo $cg; cat /sys/fs/cgroup$cg/memory.current; cat /sys/fs/cgroup$cg/memory.events
/system.slice/api-service.service
13518778368
low 0
high 0
max 0
oom 0
oom_kill 0
Що це означає: Сервіс використовує ~13.5 GB і ще не досяг cgroup OOM.
Рішення: Якщо oom_kill зростає, ви не переслідуєте «таємний краш» — ви впираєтеся в відоме обмеження ресурсів. Налаштуйте MemoryMax, виправте витік або масштабуйтесь.
Завдання 8: Виявте, чи пам’ять у tmpfs або через нестримні логи (хитрі винуватці)
cr0x@server:~$ df -h | egrep 'tmpfs|/run|/dev/shm'
tmpfs 32G 1.2G 31G 4% /run
tmpfs 32G 18G 14G 57% /dev/shm
Що це означає: /dev/shm великий. Це пам’ять, а не диск.
Рішення: Якщо /dev/shm або /run роздуваються, перевірте, що туди пише (shared memory segments, кеші у стилі браузера, IPC). Це не витік у heap; перезапуск сервісу може не допомогти, якщо винуватець — інший процес.
Завдання 9: Знайдіть топ-споживачів всередині cgroup сервісу (мульти-процесні юніти)
cr0x@server:~$ cg=$(systemctl show -p ControlGroup --value api-service.service); for p in $(cat /sys/fs/cgroup$cg/cgroup.procs | head -n 20); do awk -v p=$p '/VmRSS/ {print p, $2 "kB"}' /proc/$p/status 2>/dev/null; done | sort -k2 -n | tail
4123 12605000kB
Що це означає: Один головний процес відповідає за майже весь RSS.
Рішення: Добре: атрибуція простіша. Якщо багато процесів ділять зростання, підозрюйте воркерів, патерн форку або вибух арени алокатора по потоках.
Завдання 10: Перевірте частоту page fault, щоб відрізнити «дотик до нової пам’яті» від «повторного використання»
cr0x@server:~$ pid=4123; awk '{print "minflt="$10, "majflt="$12}' /proc/$pid/stat
minflt=48392011 majflt=42
Що це означає: Minor faults високі (нормально для алокацій), major faults низькі (ще немає трешингу на диску).
Рішення: Якщо major faults швидко зростають, ви вже в колапсі продуктивності. Віддайте пріоритет пом’якшенню (перезапуск/масштаб) перед глибоким профілюванням.
Завдання 11: Подивіться змеплені регіони за розміром (великі mmaps кидаються в очі)
cr0x@server:~$ pid=4123; awk '{print $1, $2, $6}' /proc/$pid/maps | head
55fdb5a6b000-55fdb5b2e000 r--p /usr/local/bin/api-service
55fdb5b2e000-55fdb5e7e000 r-xp /usr/local/bin/api-service
55fdb5e7e000-55fdb5f24000 r--p /usr/local/bin/api-service
7f01b4000000-7f01b8000000 rw-p
7f01b8000000-7f01bc000000 rw-p
Що це означає: Великі анонімні регіони (rw-p без файлу) вказують на арени heap або явні mmap-алокації.
Рішення: Якщо ви бачите гігантські file-backed mmaps (наприклад, файли даних), «витік» може бути стратегічним мапінгом. Це інше виправлення.
Завдання 12: Зробіть знімок pmap (тільки читання, корисно для порівнянь)
cr0x@server:~$ pid=4123; sudo pmap -x $pid | tail -n 5
---------------- ------- ------- ------- -------
total kB 18990000 12631240 12591000 0
Що це означає: Підтверджує підсумки; pmap — хороший «до/після» артефакт для тикетів.
Рішення: Збережіть цей вивід у хронології інциденту. Якщо пам’ять падає після зміни конфігурації або зміни навантаження, у вас є підказки про причинно-наслідковий зв’язок.
Завдання 13: Перевірте journald на OOM і записи про вбивства cgroup
cr0x@server:~$ sudo journalctl -k --since "1 hour ago" | egrep -i 'oom|out of memory|killed process|memory cgroup' | tail -n 20
Dec 30 02:55:10 server kernel: Memory cgroup out of memory: Killed process 2877 (search-worker) total-vm:9132000kB, anon-rss:7800000kB, file-rss:12000kB, shmem-rss:0kB
Dec 30 02:55:10 server kernel: oom_reaper: reaped process 2877 (search-worker), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Що це означає: Ядро вбило процес через Memory cgroup OOM. Це не сегфолт; це реалізація політики.
Рішення: Вирішіть, чи неправильно вказано ліміт (занадто низький для піків), або чи робота неправильна (витік/утримання). Часто — обидва.
Завдання 14: Отримайте погляд systemd на ліміти пам’яті та облік (і виправте те, що відсутнє)
cr0x@server:~$ systemctl show api-service.service -p MemoryAccounting -p MemoryMax -p MemoryHigh -p OOMPolicy
MemoryAccounting=yes
MemoryMax=infinity
MemoryHigh=infinity
OOMPolicy=continue
Що це означає: Облік увімкнено, але немає капу. OOMPolicy=continue означає, що systemd сам по собі не зупинить юніт при OOM.
Рішення: Якщо у вас multi-tenant хости — ставте MemoryHigh/MemoryMax, щоб захистити сусідів. Якщо це виділений вузол, можете віддати перевагу відсутності капу й покладатися на autoscaling + алерти.
Завдання 15: Тимчасово поставте «тривожний» кап (обережно), щоб примусити раніше відмову з кращими доказами
cr0x@server:~$ sudo systemctl set-property api-service.service MemoryHigh=14G MemoryMax=16G
cr0x@server:~$ systemctl show api-service.service -p MemoryHigh -p MemoryMax
MemoryHigh=15032385536
MemoryMax=17179869184
Що це означає: Ви застосували ліміт на живо (systemd записав у cgroup). MemoryHigh викликає reclaim pressure; MemoryMax — жорстка межа.
Рішення: Роби це лише якщо ви можете терпіти більш раннє вбивство сервісу. Це компроміс: краща ізоляція і чіткіші сигнали проти можливого впливу на користувачів. На спільних хостах це часто відповідальний вибір.
Завдання 16: Якщо це Java — увімкніть Native Memory Tracking (низьке порушення при плануванні)
cr0x@server:~$ jcmd 4123 VM.native_memory summary
4123:
Native Memory Tracking:
Total: reserved=13540MB, committed=12620MB
- Java Heap (reserved=8192MB, committed=8192MB)
- Class (reserved=512MB, committed=480MB)
- Thread (reserved=1024MB, committed=920MB)
- Code (reserved=256MB, committed=220MB)
- GC (reserved=1200MB, committed=1100MB)
- Internal (reserved=2300MB, committed=1700MB)
Що це означає: Процес — не лише «heap». Потоки та внутрішні алокації можуть домінувати.
Рішення: Якщо heap стабільний, але нативна/внутрішня пам’ять зростає — зосередьтеся на off-heap буферах, JNI, створенні потоків або фрагментації алокатора, а не на налаштуванні GC.
Завдання 17: Якщо це Go — візьміть pprof heap snapshot (мінімальні порушення, якщо endpoint доступний)
cr0x@server:~$ curl -sS localhost:6060/debug/pprof/heap?seconds=30 | head
\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff...
Що це означає: Це gzipped pprof профіль. У терміналі його не прочитати.
Рішення: Збережіть і аналізуйте офф-бокс. Якщо не можна безпечно виставляти pprof, не відкривайте великі порти в продакшні — використайте SSH-портфорвардинг або localhost-only binding.
Завдання 18: Якщо це Python — використайте tracemalloc (краще вмикати раніше)
cr0x@server:~$ python3 -c 'import tracemalloc; tracemalloc.start(); a=[b"x"*1024 for _ in range(10000)]; print(tracemalloc.get_traced_memory())'
(10312960, 10312960)
Що це означає: tracemalloc повідомляє поточні та пікові відстежені алокації (в байтах).
Рішення: У реальних сервісах tracemalloc вмикають на старті або через feature-flag. Якщо він не був увімкнений, не намагайтеся реконструювати історію алокацій Python тільки з RSS.
Міні-історія 1: хибне припущення (пастка «RSS = витік»)
Середня SaaS-компанія тримала Debian-флотацію для multi-tenant API. Що понеділок — один вузол підповзає за пам’яттю, поки не вдарить своп, і затримки ростуть експоненційно. На виклику роблять, що роблять на виклику: знайшли найбільший RSS-процес і записали тикет «витік пам’яті» команді API.
Команда API відповіла звичними аргументами: «працює на staging», кілька графіків heap і щира віра, що їхній garbage collector тут ні до чого. Операції продовжували вказувати на RSS. Дебати стали релігійними: «Linux кешує» проти «ваш сервіс протікає».
Хибне припущення було простим: що зростання RSS означає зростання live data. Це не так. Сервіс використовував бібліотеку, яка memory-mapped великі read-only датасети для швидких пошуків. У понеділок пакетна робота ротувала нові датасети, і сервіс змепив нові перед розмепінгом старих. Деякий час обидва набори співіснували. RSS підскочив. Це не був витік; це був патерн розгортання.
Виправлення не потребувало профайлера. Воно полягало в координуванні ротації датасетів і застосуванні стратегії мапінгу, яка змінює вказівник лише після готовності нового мапу, а потім негайно розмапується старий регіон. Після цього RSS іще піднімався по понеділках — але менше й коротшими вікнами. Також вони змінили алерти з «RSS > поріг» на «MemAvailable падає + major faults + latency».
Нікому не подобається мораль цієї історії, бо вона нудна: виміряйте правильну річ перш ніж звинувачувати. Але це дешевше, ніж щотижневий суд.
Міні-історія 2: оптимізація, що відбилася боком (налаштування алокатора проти реального трафіку)
Інша організація, інший квартал, інша «ініціатива економії коштів». Вони запускали C++ сервіс на Debian і хотіли зменшити витрати пам’яті. Доброзичливий інженер прочитав про арени glibc і вирішив зменшити відбиток пам’яті, встановивши MALLOC_ARENA_MAX=2 по всьому флоту.
Зміна спрацювала в пре-проді. RSS зменшився під синтетичним навантаженням. Графіки виглядали чудово. Потім настав продакшн: патерни трафіку мали більше довгоживучих з’єднань, більше короткочасного піку конкурентності і, що критично, інші строк життя об’єктів. Затримки почали стрибати. CPU піднявся. Пам’ять фактично не стала стабільною; вона стала контендитись.
При надто малих арені потоки боролися за блокування алокатора. Деякі запити сповільнились, черги зросли, і сервіс довше утримував пам’ять, бо був зайнятий тим, що працював повільно. На виклику бачили зростання RSS і знову звинувачували витік. Відкат виправив затримки. Пам’ять знову піднялася — але тепер поводилася передбачувано. Згодом вони перейшли на jemalloc для цього сервісу з профілюванням у staging і ретельним контролем rollout у продакшн. Урок не в «ніколи не тюнити malloc». Він у тому, що «налаштування алокатора специфічні для навантаження і можуть перетворити проблеми пам’яті на проблеми продуктивності».
Жарт #2: Тюнити malloc у продакшні — як переробляти панірувальну шафу під час вечері: теоретично можливо, але соціально сумнівно.
Міні-історія 3: нудна, але правильна практика, що виручила (бюджетування, ліміти й відпрацьовані інструкції)
Фінансова команда запускає Debian 13 вузли для фонової обробки. Сервіс не був гламурним: споживач черги, що трансформує документи й зберігає результати. Він також мав історію випадкових стрибків пам’яті через сторонні бібліотеки парсінгу.
Вони зробили дві нудні речі. По-перше, використали systemd MemoryAccounting з MemoryHigh, встановленим так, щоб викликати reclaim pressure до того, як вузол опиниться в біді. По-друге, вони побудували щотижневу «репетицію витоку» в staging: нарощення навантаження, перевірка плато пам’яті і захоплення smaps_rollup знімків у фіксовані інтервали.
Однієї ночі оновлення бібліотеки вендора змінило поведінку і почало утримувати величезні буфери. Пам’ять почала рости. Сервіс не знищив увесь вузол, бо cgroup limit ізолював його. Вузол залишився здоровим; постраждала лише та група воркерів.
На виклику не потрібно було гадати. Алерти містили memory.current і memory.events. У робочій інструкції вже було: якщо memory.current росте і приватна dirty домінує — перезапустіть юніт, зафіксуйте версію пакету і відкрийте інцидент на root cause. Вони слідували інструкції, повернулись спати і виправили витік належним чином наступного дня.
Нічого героїчного не сталося. Ось у чому суть. «Нудна» практика перетворила потенційний інцидент флоту на єдину дрібну прикру ситуацію.
Методи з низьким рівнем порушень, які справді працюють
Почніть з артефактів, які можна зібрати без зміни процесу
Ваші найбезпечніші інструменти: /proc, systemd і лічильники cgroup. Вони не вимагають перезапуску. Вони не приєднують дебагери. Вони не вводять ефект Гайзенберга, коли профайлер «виправляє» таймінги й ховає витік.
- /proc/<pid>/smaps_rollup скаже, чи пам’ять private dirty (похоже на heap) або file-backed.
- /sys/fs/cgroup/…/memory.current покаже, чи росте весь юніт, навіть якщо в нього кілька PID.
- journalctl -k показує, чи бачите ви cgroup OOM kills, глобальний OOM або щось інше.
Віддавайте перевагу семплюванню над трасуванням під навантаженням
Повне трасування алокацій може бути дорогим і змінювати поведінку. Семплінгові профайлери й періодичні знімки — продакшн-стандарт.
Якщо потрібні точні місця алокацій, робіть це недовго і з планом відкату.
Обмежуйте шкоду за допомогою cgroup-лімітів і чистих перезапусків
«Мінімальні незручності» не означають «ніколи не перезапускати». Це означає, що ви перезапускаєте свідомо: зливаєте трафік, котите один інстанс, перевіряєте плато пам’яті і продовжуєте.
Контрольований перезапуск, що уникає swap-шторма на вузлі, часто є найлегшою дією для користувачів.
Шукайте часову кореляцію з змінами навантаження
Витоки часто корелюють з конкретним типом запиту, cron-завданням, деплоєм або зміною форми даних.
Якщо пам’ять росте лише коли активна певна черга — ви вже звузили пошук набагато більше, ніж будь-який загальний інструмент.
systemd і cgroups v2: використовуйте їх, не боріться з ними
Debian 13 робить ставку на systemd і cgroups v2. Це не ідеологія; це реальність, в якій ви дебагуєте.
Якщо ви трактуватимете хост як «купу процесів» і ігноруватимете cgroups, ви пропустите межу примусового обмеження.
Чому мислення на рівні cgroup важливе
Сервіс може бути вбитий, хоча на хості ще є вільна пам’ять, бо він досяг свого cgroup limit. Навпаки, хост може бути в біді, хоча окремий сервіс виглядає нормально, бо інший cgroup захопив пам’ять.
Телекметрія на рівні сервісу відповідає не на питання «який процес великий», а на питання: який юніт відповідальний за тиск.
Використовуйте MemoryHigh перед MemoryMax
MemoryHigh викликає reclaim pressure; MemoryMax — жорстка межа вбивства. Практично MemoryHigh — ніжний ранній сигнал, що також дає час.
Якщо встановити лише MemoryMax, перший сигнал може бути крахом. Це як дізнатися, що бензин скінчився, коли мотор зупинився.
Майте явну OOMPolicy для юніта
Якщо systemd вб’є щось через OOM — що ви хочете, щоб відбулося? Перезапустити? Зупинити? Продовжити?
Розв’язуйте це залежно від поведінки сервісу: безстанні API можна перезапускати; станозбережні воркери потребують акуратного дрену й логіки ретраїв.
cr0x@server:~$ sudo systemctl edit api-service.service
cr0x@server:~$ sudo cat /etc/systemd/system/api-service.service.d/override.conf
[Service]
MemoryAccounting=yes
MemoryHigh=14G
MemoryMax=16G
OOMPolicy=restart
Restart=always
RestartSec=5
Що це означає: Ви визначили межу ізоляції і автоматизували відновлення.
Рішення: Якщо перезапуски безпечні і витік повільний, це перетворює «3 ранку смерть вузла» на «контрольовану перезавантаження екземпляра», купуючи час для розбору причини.
Не плутайте «used memory» з «memory charged»
cgroups рахують пам’ять по-різному для анонімної пам’яті і file cache, і точна поведінка залежить від версії ядра та налаштувань.
Суть не в тому, щоб знати всі подробиці; а в тому, щоб спостерігати тренди і знати, які лічильники ви використовуєте.
Мова та специфіка пошуку витоків (Java, Go, Python, C/C++)
Java: розділіть світ на heap та native
Якщо RSS росте, а heap стабільний — перестаньте звинувачувати GC. Ви маєте справу з native memory: direct buffers, стеки потоків, JNI або фрагментація алокатора.
NMT (Native Memory Tracking) — ваш друг, але його краще вмикати цілеспрямовано (startup flags) для низького оверхеду.
Підхід з низьким порушенням: періодично збирайте NMT summary, плюс GC-логи або JFR, якщо вони вже ввімкнені. Якщо потрібен heap dump — робіть це в контрольоване вікно і переконайтесь, що є місце на диску та IO-голова; дампи heap можуть бути дорогими.
Go: ставтесь до heap профілів як до доказів, а не думок
Runtime Go дає pprof і runtime метрики, які досить хороші. Ризик — експозиція: debug endpoint, доступний з неправильної мережі, сам по собі інцидент.
Тримайте pprof прив’язаним до localhost і тунелюйте при потребі.
Шукайте: зростаючий in-use heap, підвищену кількість об’єктів або зростання кількості goroutine (інший вид витоку).
Python: витоки можуть бути нативними теж
Python-сервіси можуть «текти» в чистому Python (що відстежує tracemalloc), але також у нативних розширеннях, що алокують поза трекером об’єктів Python.
Якщо tracemalloc виглядає нормально, а RSS росте — підозрюйте нативні бібліотеки, буфери і mmap-патерни.
C/C++: вирішіть, чи потрібна телеметрія алокатора або інструменти коду
У C/C++ справжній витік зустрічається часто, але й «неповернення пам’яті ОС» теж поширене.
Якщо можете дозволити — заміна glibc malloc на jemalloc у контрольованому rollout може дати профайлінг і часто стабільніший RSS. Але не вважайте заміну алокатора панацеєю.
Найменш руйнівний шлях: захоплюйте smaps_rollup, знімки pmap і, за потреби, коротке eBPF sampling стеків алокацій, а не постійне трасування.
Типові помилки: симптом → причина → виправлення
«Used memory 95%, ми протікаємо»
Причина: page cache виконує свою роботу; система все ще має здоровий MemAvailable.
Виправлення: алертуйте по MemAvailable, swap in/out, major faults і latency. Не кличте людей через те, що Linux використовує RAM.
«RSS росте, отже витік heap»
Причина: file-backed mmaps, фрагментація алокатора або нативний ріст пам’яті (Java direct buffers, Python extensions).
Виправлення: smaps_rollup для розподілу private dirty vs file-backed; використайте інструменти мови (JVM NMT, pprof) для підтвердження.
«Сервіс вмер, мусить бути segfault»
Причина: cgroup OOM kill (часто без явних слідів у додатку) або system-wide OOM.
Виправлення: journalctl -k для рядків OOM; перевірте memory.events у cgroup сервісу; вирішіть стратегію MemoryMax/OOMPolicy.
«Додамо swap — виправимо»
Причина: swap маскує витоки і перетворює їх на інциденти з високою затримкою.
Виправлення: тримайте swap помірним, моніторте активність свопа і вважайте стійкий swap-in/out P1. Використовуйте ізоляцію і перезапуски, не безкінечний swap.
«Увімкнули важке профілювання і витік зник»
Причина: ефект спостерігача; зміна таймінгів; інші патерни алокацій.
Виправлення: віддавайте перевагу семплінгу; збирайте кілька коротких вікон; корелюйте з навантаженням; відтворіть у staging з production-like трафіком.
«Встановили MemoryMax і тепер він випадково перезапускається»
Причина: ліміт занадто низький для законних піків або відсутній MemoryHigh, через що отримуєте раптову смерть.
Виправлення: ставте MemoryHigh нижче за MemoryMax, вимірюйте піки і коригуйте. Використайте graceful shutdown hooks і autoscaling, якщо можливо.
«Ліміт контейнера нормальний; хост все одно OOM»
Причина: інші cgroup без меж; тиск ядра; file cache і dirty pages; або некоректний облік.
Виправлення: перевірте memory.current у топ-рівневих cgroup; переконайтесь, що облік увімкнено; ставте розумні ліміти для галасливих сусідів.
Контрольні списки / покроковий план
Контрольний список A: підтвердження за 10 хвилин (без перезапусків, без нових агентів)
- Перевірте
/proc/meminfo: чи падаєMemAvailableз часом? - Запустіть
vmstat 1: чи активний swap (si/so), чи росте IO wait? - Знайдіть топ RSS-процес через
ps --sort=-rss. - Підтвердіть зростання за допомогою повторних читань
/proc/<pid>/status(VmRSS). - Використайте
/proc/<pid>/smaps_rollup: private dirty vs file-backed. - Перевірте пам’ять юніта через
systemctl statusі cgroup memory.current. - Пошукайте у kernel logs OOM-події й жертв.
Контрольний список B: ізоляція без драми (коли витік реальний)
- Оцініть час до відмови: поточна швидкість зростання RSS vs доступний запас.
- Визначте межу ізоляції: MemoryHigh/MemoryMax для сервісу або масштабування вузла.
- Спочатку встановіть MemoryHigh, потім MemoryMax, якщо потрібно. Віддавайте перевагу малим ітеративним змінам.
- Переконайтесь, що OOMPolicy і Restart налаштовані відповідно до типу сервісу.
- Заплануйте вікно rolling restart; зливайте трафік, якщо застосовано.
- Захопіть «перед перезапуском» артефакти: smaps_rollup, pmap totals, memory.current, memory.events.
- Після перезапуску: перевірте плато пам’яті під тим же навантаженням.
Контрольний список C: робота над root cause (коли користувачі у безпеці)
- Оберіть правильний інструмент: pprof/JFR/NMT/tracemalloc/профілювання алокатора.
- Корелюйте витік з навантаженням: ендпоінти, типи задач, розміри payload.
- Відтворіть у staging з production-like конкурентністю і даними.
- Виправте утримання: додайте евікцію, межі, таймаути і backpressure.
- Додайте дашборди: memory.current, доля private dirty, OOM events, кількість перезапусків.
- Додайте регресійний тест: прогін підозрюваного навантаження достатньо довго, щоб показати схил.
FAQ
1) Як відрізнити «витік» від «алокатор не повертає пам’ять»?
Дивіться на монотонне зростання в private dirty пам’яті (smaps_rollup), корельоване з кількістю об’єктів або метриками heap. Якщо live-дані додатка падають, а RSS залишається високим — підозрівайте поведінку алокатора/фрагментацію.
2) Чому RSS продовжує рости, коли трафік стабільний?
Справжній витік, утримання в кеші, фонова задача або повільний «раз на годину» таск. Підтвердіть повторними перевірками VmRSS і корелюйте з логами запитів/робіт. Стабільний трафік не означає стабільну форму навантаження.
3) Який найкорисніший /proc файл для цього?
/proc/<pid>/smaps_rollup. Він достатньо компактний для захоплення під час інцидентів і дає вирішальні сигнали.
4) Чи варто ставити MemoryMax для кожного systemd-сервісу?
На спільних хостах: так, майже завжди. На виділених вузлах: можливо. Ліміти захищають від галасливих сусідів, але також вводять жорсткі відмови, які потрібно враховувати в дизайні.
5) Як зрозуміти, чи OOM killer вдарив мій сервіс?
Перевірте journalctl -k на рядки «Killed process» і лічильники memory.events у cgroup сервісу (oom/oom_kill). Не гадати.
6) Чи є додавання swap валідним пом’якшенням?
Це тимчасове підпирачення, а не рішення. Swap може запобігти миттєвому OOM, але часто перетворює відмови на проблеми з латентністю і IO-шторми. Використовуйте помірно і моніторте.
7) Чи можна дебагувати витоки без перезапусків?
Іноді так. /proc і лічильники cgroup не вимагають перезапусків. eBPF sampling часто потребує лише привілеїв, а не перезапуску. Інструменти мов різняться: Go і JVM часто дозволяють приєднуватись; tracemalloc для Python краще ввімкнути на старті.
8) Який найменш руйнівний спосіб зібрати «до/після» докази?
Робіть періодичні знімки: smaps_rollup, pmap totals, memory.current і memory.events. Зберігайте з таймстемпами. Це дає вам схил і журнал змін без важкої інструментації.
9) Чому systemd показує «Memory: 12G», а ps дає інший RSS?
systemd відображає використання пам’яті cgroup для всього юніта (включно з дочірніми процесами і кешем, зарядженим на cgroup). ps показує RSS на процес. Вони відповідають на різні питання; використовуйте обидва.
10) Коли слід зупинити дебаг і просто відкотити?
Якщо витік почався після деплою і у вас є безпечний rollback — робіть його якомога раніше. Root cause може почекати. Користувачам байдуже, що ви знайшли ідеальне місце алокації о 04:40.
Висновок: наступні кроки, які ви можете зробити сьогодні
Витоки пам’яті в сервісах не вирішуються настроєм. На Debian 13 шлях з найменшим порушенням послідовний: підтвердьте тиск через MemAvailable і swap, знайдіть юніт, що росте, через cgroups, класифікуйте пам’ять через smaps_rollup і тільки потім обирайте глибші інструменти.
Наступні кроки, що приносять швидкий результат:
- Додайте дашборди сервісів для memory.current, memory.events і лічильників перезапусків на юніт.
- Встановіть MemoryHigh (а іноді й MemoryMax) для гучних сервісів, а також явну політику OOMPolicy.
- Оновіть алерти, щоб зосередитися на тренді MemAvailable, активності swap, major faults і латентності, а не на сирому «used memory».
- Звичка робити smaps_rollup snapshots під час інцидентів позбавить вас від суперечок і допоможе швидше вирішувати проблему.
Ви не зможете запобігти кожному витоку. Але можете не допускати, щоб більшість інцидентів з витоками ставали катастрофою для хосту. Оце й є справжня перемога.