MySQL vs PostgreSQL: обмеження пам’яті в Docker — як зупинити тихе зниження продуктивності

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

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

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

Що насправді означає «тихе» тротлінг у Docker

Обмеження пам’яті в Docker відрізняються від обмежень CPU. Для CPU є явні лічильники тротлінгу. Для пам’яті є жорстка стінка (memory.max / --memory) і далі відбувається одне з двох:

  • Ядро агресивно звільняє пам’ять (файловий кеш, анонімна пам’ять, slabs), що виглядає як «ніби все нормально», поки не виростають латентності й I/O.
  • OOM killer завершує процес, що принаймні чесно.

Між цими крайнощами є нещасне середовище: база даних жива, але бореться з ядром за пам’ять і повільно програє. Це і є ваше «тихе» тротлінг.

Пастка з трьома рівнями: кеш бази, кеш ядра, обмеження контейнера

На звичайному хості бази даних покладаються на два рівні кешування:

  • Кеш, який керує сама база (InnoDB buffer pool у MySQL, shared buffers у PostgreSQL).
  • Кеш сторінок ядра (файлова система).

Помістіть їх у контейнер зі строгим обмеженням пам’яті — ви додали третю межу: cgroup. Тепер кожний байт рахується двічі: і для пам’яті бази, і для «всього іншого» (підключення, робоча пам’ять, сорти/хеші, буфери реплікації, фонові процеси, спільні бібліотеки, накладні витрати malloc). Ядро байдуже до того, що ви «лише змінили одну конфігурацію»; воно враховує RSS і кеш у вашій cgroup і звільняє пам’ять залежно від тиску.

Чому це виглядає як проблема мережі або сховища

Тиск пам’яті проявляється як:

  • випадкові p99-спайки латентності
  • збільшення часу fsync/flush
  • більше операцій читання, незважаючи на стабільний набір запитів
  • CPU, що виглядає «нормальним», бо потоки блокуються в ядрі
  • накопичення підключень з наступними каскадними таймаутами

Жарт №1: Бази даних під тиском пам’яті схожі на людей під «припиненням кофеїну» — все ще технічно функціонують, але для кожного маленького запиту вони реагують як на особисту образу.

Цікаві факти й історія, які мають значення

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

  1. PostgreSQL завжди покладався на page cache ОС. Його архітектура навмисно залишає багато кешування за ядром; shared_buffers не призначений бути «усією пам’яттю».
  2. InnoDB buffer pool у MySQL став стандартним інструментом, коли InnoDB витіснив MyISAM (епоха MySQL 5.5). Це закріпило звичку «робити buffer pool величезним».
  3. Linux cgroups v1 і v2 відрізняються в обліку пам’яті настільки, що можуть плутати панелі моніторингу. Той самий контейнер може виглядати «нормально» на v1 і «таємниче обмеженим» на v2, якщо не перевіряти правильні файли.
  4. Docker з часом додав кращі налаштування за замовчуванням, але Compose-файли досі містять погану фольклорну практику. Ви все ще бачите mem_limit, встановлені без відповідного налаштування БД — це справжнє колесо фортуни продуктивності.
  5. Поведінка OOM killer у контейнерах раніше частіше дивувала команди. Раннє впровадження контейнерів навчило команди, що «на хості багато RAM» не має значення, якщо cgroup лімітує.
  6. PostgreSQL давно підтримує huge pages, але рідко використовує їх у контейнерах через операційну складність і сумісність із обмеженими середовищами.
  7. MySQL/InnoDB має кілька споживачів пам’яті поза buffer pool (adaptive hash index, буфери підключень, performance_schema, реплікація). Призначати лише buffer pool — класична пастка.
  8. Per-query пам’ять у PostgreSQL часто є справжнім винуватцем. Надмірний work_mem, помножений на конкурентність, — спосіб «випадково» вичерпати бюджет cgroup.

MySQL vs PostgreSQL: моделі пам’яті, що конфліктують із контейнерами

MySQL (InnoDB): один великий пул і смерть від тисяч буферів

Культура тюнінгу MySQL домінує навколо InnoDB buffer pool. На «голому» сервері правило часто таке: «60–80% RAM». У контейнерах така порада стає небезпечною, якщо не переформулювати «RAM» як «межа контейнера мінус накладні витрати».

Блоки пам’яті, які потрібно врахувати:

  • InnoDB buffer pool: головне число. Якщо ви виставили його на 75% ліміту контейнера — ви вже програли.
  • InnoDB log buffer і пам’ять, пов’язана з redo log: зазвичай невеликі, але не нульові.
  • Буфери на підключення: read buffer, sort buffer, join buffer, tmp tables. Множте на max_connections. А потім пам’ятайте, що «пікове» рідко дорівнює «середньому».
  • Performance Schema: може виявитися помітною, якщо ввімкнений повністю.
  • Реплікація: relay logs, binlog caches, мережеві буфери.

При тиску пам’яті InnoDB може продовжувати роботу, але з більшою кількістю дискових читань і фонової метушні. Це може виглядати як уповільнення сховища. Насправді ви виснажили кеш і змусили випадкові зчитування.

PostgreSQL: shared_buffers — це лише половина історії

Postgres використовує спільну пам’ять (shared_buffers) для кешування сторінок даних, але також сильно покладається на page cache ядра. Далі додається купа інших споживачів:

  • work_mem на вузол сорту/хешу, на запит, на підключення (і на кожного паралельного воркера). Саме туди йдуть бюджети контейнера.
  • maintenance_work_mem для vacuum/index build. Нічна повільна робота може стати денним інцидентом.
  • Autovacuum воркери і background writer: вони не лише використовують CPU, але створюють I/O і можуть посилювати тиск пам’яті непрямо.
  • Накладні витрати shared memory, каталоги, контексти підключень, розширення.

У Postgres «база працює повільно» під обмеженнями контейнера часто означає, що ядро звільняє page cache і Postgres виконує більше реальних читань. Тим часом кілька одночасних запитів можуть «роздути» пам’ять через work_mem. Це двопередова війна.

Шаблони тихого тротлінгу: MySQL vs Postgres

Шаблон MySQL: buffer pool занадто великий → мало запасу → page cache стискається → поведінка checkpoint/flush погіршується → сплески випадкових читань → латентність поступово зростає без очевидної помилки.

Шаблон Postgres: помірні shared_buffers, але щедрий work_mem → сплеск конкурентності → ріст анонімної пам’яті → тиск cgroup → reclaim і/або своп → час виконання запитів вибухає, інколи без єдиного «поганого» запиту.

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

Цитата, бо це й досі влучно: paraphrased ideaJim Gray: «Найкращий спосіб покращити продуктивність — спочатку її виміряти.»

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

Це порядок дій, що швидко знаходить вузькі місця, коли БД у Docker «раптом почала гальмувати». Ви можете зробити це за 10–15 хвилин, якщо тримати руки спокійними і сумніватися в припущеннях.

1) Підтвердьте реальний бюджет пам’яті контейнера (не те, що ви думали, що встановили)

  • Перевірте cgroup memory.max / docker inspect.
  • Перевірте, чи дозволено своп (memory.swap.max або docker --memory-swap).
  • Перевірте, чи оверрайдить оркестратор значення Compose.

2) Визначте: звільняє ядро, свопиться чи OOM?

  • Шукайте OOM kill у dmesg/journalctl.
  • Перевірте події пам’яті cgroup (memory.events на v2).
  • Перевірте великі сторінкові вкидання, своп in/out.

3) Корелюйте з поведінкою пам’яті БД

  • MySQL: розмір buffer pool, відсоток dirty pages, history list length, використання тимчасових таблиць, max connections.
  • Postgres: shared_buffers, work_mem, активні сорти/хеші, створення тимчасових файлів, активність autovacuum, кількість підключень.

4) Підтвердіть, чи є сховище жертвою чи винуватцем

  • Виміряйте read IOPS і латентність на рівні хоста.
  • Перевірте, чи збільшилися читання після початку тиску пам’яті.
  • Перевірте поведінку fsync (checkpointing, WAL, redo flush).

5) Зробіть одну безпечну зміну

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

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

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

Task 1: Identify the container and its configured memory limit

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
NAMES           IMAGE                 STATUS
db-mysql        mysql:8.0             Up 3 days
db-postgres     postgres:16           Up 3 days
cr0x@server:~$ docker inspect -f '{{.Name}} mem={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}}' db-mysql
/db-mysql mem=2147483648 swap=2147483648

Значення: пам’ять — 2 ГіБ. Swap рівний пам’яті, отже контейнер може свопитись приблизно на 2 ГіБ (залежить від налаштувань). Це не «безкоштовно». Це латентність.

Рішення: Якщо ви бачите своп для чутливих до латентності БД, або відключіть своп для контейнера, або виставте пам’ять так, щоб він не був потрібен.

Task 2: Verify cgroup v2 memory.max from inside the container

cr0x@server:~$ docker exec -it db-postgres bash -lc 'cat /sys/fs/cgroup/memory.max; cat /sys/fs/cgroup/memory.current'
2147483648
1967855616

Значення: ліміт 2 ГіБ, поточне використання ≈1.83 ГіБ. Це близько до стелі.

Рішення: Якщо memory.current під час нормального навантаження сидить поруч із memory.max, це не «проблема спайків». Це проблема розмірування.

Task 3: Check cgroup memory pressure events (v2)

cr0x@server:~$ docker exec -it db-postgres bash -lc 'cat /sys/fs/cgroup/memory.events'
low 0
high 214
max 0
oom 0
oom_kill 0

Значення: інкременти high вказують на стійкий тиск пам’яті. OOM ще немає, але ядро звільняє пам’ять.

Рішення: Якщо high продовжує зростати під час інцидентів, сприймайте це як сигнал першої важливості. Зменшуйте споживання пам’яті або підвищуйте ліміт.

Task 4: Find OOM kills from the host

cr0x@server:~$ sudo journalctl -k --since "2 hours ago" | grep -i oom | tail -n 5
Dec 31 09:12:44 server kernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker-8b1...,mems_allowed=0,oom_memcg=/docker/8b1...
Dec 31 09:12:44 server kernel: Killed process 27144 (mysqld) total-vm:3124280kB, anon-rss:1782400kB, file-rss:10240kB, shmem-rss:0kB

Значення: mysqld був вбитий cgroup OOM killer. Це не «аварійний збій MySQL». Це «ви вичерпали бюджет».

Рішення: Не запускати цикл перезапусків. Спочатку виправте невідповідність бюджету й конфігурації.

Task 5: Confirm swap activity on the host

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  1  524288  31264  10240  88432  120  210  1820  2410  920 1640 12  8 61 19  0
 1  2  526336  29812  10320  87020  140  180  2100  1980  980 1710 10  7 58 25  0

Значення: swap-in (si) і swap-out (so) ненульові під час навантаження. Це податок на латентність.

Рішення: Якщо латентність БД важлива — уникайте свопу. Збільшуйте ліміт пам’яті або зменшуйте використання пам’яті БД; також розгляньте встановлення --memory-swap рівним --memory, щоб уникнути додаткового свопінгу (залежить від середовища).

Task 6: See container-level memory usage and page cache via docker stats

cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}'
NAME          MEM USAGE / LIMIT     MEM %     CPU %
db-mysql      1.95GiB / 2GiB        97.5%     35.2%
db-postgres   1.82GiB / 2GiB        91.0%     18.7%

Значення: обидва працюють близько до ліміту. Очікуйте тиск на звільнення пам’яті і/або OOM.

Рішення: Вважайте будь-що понад 90% стійке використання «поганою конфігурацією», а не просто «навантаженням».

Task 7: Inspect per-process RSS inside the container (who is eating memory?)

cr0x@server:~$ docker exec -it db-mysql bash -lc 'ps -eo pid,comm,rss --sort=-rss | head'
  PID COMMAND           RSS
    1 mysqld        1789320
  112 bash            17520
   98 ps               4440

Значення: mysqld RSS ≈1.7 ГіБ. Це без урахування файлового кешу й інших накладних витрат.

Рішення: Якщо mysqld RSS близький до ліміту — зменшіть InnoDB buffer pool і буфери на підключення або підвищте ліміт.

Task 8: MySQL—confirm InnoDB buffer pool size and other big knobs

cr0x@server:~$ docker exec -it db-mysql bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES WHERE Variable_name IN (\"innodb_buffer_pool_size\",\"max_connections\",\"tmp_table_size\",\"max_heap_table_size\",\"performance_schema\");"'
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 1610612736|
| max_connections         | 500       |
| tmp_table_size          | 67108864  |
| max_heap_table_size     | 67108864  |
| performance_schema      | ON        |
+-------------------------+-----------+

Значення: buffer pool — 1.5 ГіБ у 2 ГіБ контейнері. Max connections — 500, тож пам’ять на підключення може легко з’їсти запас.

Рішення: Зріжте buffer pool до захищеного значення (часто 40–60% ліміту для маленьких контейнерів) і зменшіть max_connections або додайте пулінг. Контейнери не люблять «на всякий випадок» великі ліміти підключень.

Task 9: MySQL—check if you’re doing lots of temp table work (often memory→disk amplification)

cr0x@server:~$ docker exec -it db-mysql bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE \"Created_tmp%tables\";"'
+-------------------------+--------+
| Variable_name           | Value  |
+-------------------------+--------+
| Created_tmp_disk_tables | 184220 |
| Created_tmp_tables      | 912340 |
| Created_tmp_files       | 2241   |
+-------------------------+--------+

Значення: значна частка тимчасових таблиць потрапляє на диск. При тиску пам’яті це погіршується і виглядає як «регресія сховища».

Рішення: Зменшіть витоки на диск (індекси, плани запитів), акуратно налаштуйте тимчасові параметри і забезпечте запас пам’яті, щоб тимчасові операції не перекидалися на диск частіше.

Task 10: PostgreSQL—confirm shared_buffers, work_mem, and max_connections

cr0x@server:~$ docker exec -it db-postgres bash -lc 'psql -U postgres -d postgres -c "SHOW shared_buffers; SHOW work_mem; SHOW maintenance_work_mem; SHOW max_connections;"'
 shared_buffers
----------------
 512MB
(1 row)

 work_mem
----------
 64MB
(1 row)

 maintenance_work_mem
----------------------
 1GB
(1 row)

 max_connections
-----------------
 300
(1 row)

Значення: work_mem 64MB помножений на конкурентність — пастка. maintenance_work_mem 1GB у 2GB контейнері — бомба уповільненого часу, коли запускаються vacuum/index.

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

Task 11: PostgreSQL—see temp files (a strong indicator of memory shortfalls or bad plans)

cr0x@server:~$ docker exec -it db-postgres bash -lc 'psql -U postgres -d postgres -c "SELECT datname, temp_files, temp_bytes FROM pg_stat_database ORDER BY temp_bytes DESC LIMIT 5;"'
  datname  | temp_files |  temp_bytes
-----------+------------+-------------
 appdb     |      12402 | 9876543210
 postgres  |          2 |      819200
(2 rows)

Значення: великі temp_bytes вказують на сорти/хеші, що виливаються на диск. У контейнерах такі виливи плюс хвилі звільнення — сумні наслідки для користувачів.

Рішення: Знайдіть запити з найбільшими виливами, оптимізуйте індекси й встановіть work_mem на основі реальної конкурентності, а не оптимістичних очікувань.

Task 12: PostgreSQL—spot autovacuum pressure (it can look like random I/O “mystery”)

cr0x@server:~$ docker exec -it db-postgres bash -lc 'psql -U postgres -d postgres -c "SELECT relname, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"'
   relname   | n_dead_tup |        last_autovacuum
------------+------------+-------------------------------
 events     |    4821932 | 2025-12-31 08:44:12.12345+00
 sessions   |    1120044 | 2025-12-31 08:41:02.54321+00
(2 rows)

Значення: велика кількість dead tuples означає навантаження vacuum. Vacuum створює I/O і може посилити чахання кешу, особливо при тісних лімітах пам’яті.

Рішення: Налаштуйте пороги autovacuum по таблицях і зменшуйте write amplification. Не «вирішуйте» це, даючи vacuum необмежену пам’ять у маленькому контейнері.

Task 13: Measure host I/O latency (are we forcing disk reads because cache is gone?)

cr0x@server:~$ iostat -x 1 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          11.22    0.00    6.11   18.33    0.00   64.34

Device            r/s     w/s   rkB/s   wkB/s  await  r_await  w_await  svctm  %util
nvme0n1         820.0   210.0 50240.0 18432.0  18.2    21.4      5.6   1.2   98.0

Значення: високе завантаження і велике r_await свідчать, що ви робите реальні читання і чекаєте. Якщо це збігається з тиском пам’яті, диск часто є лише посередником у повідомленні про проблему.

Рішення: Не купуйте одразу швидші диски. Спочатку підтвердіть голод кешу й поведінку звільнення пам’яті.

Task 14: Check major page faults (a reclaim/swap smell)

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' db-postgres); sudo cat /proc/$pid/stat | awk '{print "majflt="$12, "minflt="$10}'
majflt=48219 minflt=12984321

Значення: великі majflt вказують, що процес вимушено зчитував сторінки з диска (або swap). Під тиском це зростає разом із латентністю.

Рішення: Якщо major faults ростуть під час інцидентів — зосередьтеся на запасі пам’яті і поведінці кешу перед тим, як звинувачувати лише плани запитів.

Task 15: Verify Compose vs runtime settings (the “I set mem_limit” lie)

cr0x@server:~$ docker compose config | sed -n '/db-mysql:/,/^[^ ]/p'
db-mysql:
  image: mysql:8.0
  mem_limit: 2g
  environment:
    MYSQL_DATABASE: appdb
cr0x@server:~$ docker inspect -f 'mem={{.HostConfig.Memory}}' db-mysql
2147483648

Значення: тут конфігурація Compose відповідає runtime. У багатьох середовищах це не так, бо Swarm/Kubernetes/інші інструменти можуть оверрайдити.

Рішення: Завжди довіряйте runtime-інспекції більше за конфігураційні файли. Файли — це прагнення, а не обов’язкова реальність.

Три корпоративні історії з поля бою

1) Інцидент через хибне припущення: «На хості 64 GB, ми в безпеці»

Середня SaaS-команда перенесла MySQL primary у Docker, щоб стандартизувати деплой. На хості було багато RAM. Вони встановили ліміт контейнера 4 GB, бо «дані маленькі», і скопіювали старий innodb_buffer_pool_size з VM: 3 GB. Все працювало. Тижнями.

Потім пройшла маркетингова кампанія. Підключення різко зросли, почали використовуватися sort buffers, і раптом латентність пішла з «нормально» до «що сталося з продуктом». CPU не зашкалював. Диск спочатку не був перевантажений. Сервери додатку таймаутили.

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

Нарешті хтось перевірив ядро і побачив тиск reclaim у cgroup і поодинокі OOM kill допоміжних процесів. MySQL не завжди помирав; він жив на краю і змушував ядро постійно красти кеш. Читання зросли, checkpointing став гіршим, і кожен I/O wait перетворювався на видиму користувачеві латентність.

Виправлення було нудно-простим: зменшити buffer pool, залишивши реальний запас, обмежити підключення пулером і підвищити ліміт контейнера згідно з реальною конкурентністю. Дані не були проблемою — робоче навантаження було.

2) Оптимізація, що повернулась бумерангом: «Давайте піднімемо work_mem — буде швидше»

Сервіс з великими даними запускав PostgreSQL у контейнерах. Інженер побачив, що сорти під час EXPLAIN (ANALYZE) виливаються на диск, і підвищив work_mem з 4MB до 128MB. Бенчмарки покращилися. Усі потиснули один одному руки і повернулися у Slack.

Через два тижні інцидент. Не під час деплою — ще гірше. Під час звичайного вівторка запустився батч, виконав кілька паралельних запитів, і кожен запит використовував кілька вузлів сорту/хешу. Помножте на число підключень. Помножте ще раз на паралельних воркерів. Раптом контейнер відчув тиск пам’яті і почав агресивно звільняти.

База не злетіла. Вона просто стала повільною. Дуже повільною. Батч уповільнився, тримав блокування довше і блокував користувацькі транзакції. Це викликало повторні спроби з боку додатку, що підняло конкурентність ще більше. Класична позитивна петля, але ніхто не був у захваті.

Вони відкотили work_mem, але продуктивність довго залишалась поганою, бо autovacuum відстав під час хаосу. Коли vacuum наздогнав — усе нормалізувалося. Реальне виправлення — розраховувати work_mem за найгіршою конкурентністю, а не за одиничними бенчмарками, і відокремлювати батчові навантаження зі своїми бюджетами.

Жарт №2: Підвищити work_mem у маленькому контейнері — як узяти більший чемодан в аеропорту: ви почуватиметесь готовими, поки хтось не поміряє це.

3) Нудна, але правильна практика, що врятувала день: бюджети, запас і тривоги

Інша команда запускала обидві СУБД у Docker по середовищах. У них було правило: кожен DB-контейнер має «лист бюджету пам’яті» — короткий документ у репозиторії з лімітом, очікуваною піковою кількістю підключень і розрахованими найгіршими сценаріями використання пам’яті.

Також у них були алерти по подіям тиску пам’яті cgroup (v2) і по активності swap, а не лише «відсоток пам’яті контейнера». Тривога була не «ви на 92%», а «memory.events high швидко зростає». Це різниця між попередженням і сигналом пожежної тривоги.

Одного дня реліз функції підвищив конкурентність звітів. Панелі показали зростання memory.events high, поки користувацька латентність ще була прийнятною. Вони обмежили конкурентність звітів на стороні додатку і запланували підвищення ліміту контейнера на наступне вікно обслуговування.

Жодного простою. Жодної драми. Команду одного разу звинуватили в «перебудові», що є ознакою правильного підходу.

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

1) Симптом: p99-спайки латентності, CPU виглядає нормально

Корінь: тиск reclaim в ядрі всередині cgroup; потоки блокуються в I/O або шляхах алокації.

Виправлення: перевірте /sys/fs/cgroup/memory.events і memory.current. Зменшіть настройки пам’яті БД або підвищте ліміт. Підтвердіть major faults і I/O await.

2) Симптом: випадкові сплески читань з диска після «посилення» лімітів пам’яті

Корінь: page cache стискається; кеш бази або занадто малий, або занадто великий відносно контейнера; reclaim виштовхує корисний кеш.

Виправлення: залишайте запас для page cache і інших непередбачених витрат. Для MySQL не встановлюйте buffer pool поруч із лімітом. Для Postgres не думайте, що shared_buffers замінює page cache.

3) Симптом: база рестартується без очевидної DB-помилки

Корінь: cgroup OOM kill. БД не «падала» сама — її завершило ядро.

Виправлення: перевірте journalctl -k. Виправте розмір пам’яті, зменшіть конкурентність і уникайте залежності від swap.

4) Симптом: «Повільно, поки не перезапустимо контейнер»

Корінь: накопичений тиск пам’яті, надування підключень, чахлення кешу, backlog autovacuum або фрагментація; перезапуск скидає симптоми.

Виправлення: виміряйте тиск пам’яті і внутрішні лічильники БД. Додайте пулінг, правильно виставте буфери і заплануйте обслуговування/vacuum.

5) Симптом: вибух тимчасових файлів Postgres і заповнення диска

Корінь: недостатній work_mem для форми запиту або погані плани; під тиском пам’яті виливи на диск трапляються частіше.

Виправлення: ідентифікуйте запити з найбільшим temp_bytes, налаштуйте індекси і встановіть work_mem згідно з конкурентністю. Розгляньте таймаути для аналітики в OLTP.

6) Симптом: MySQL використовує «набагато більше пам’яті, ніж innodb_buffer_pool_size»

Корінь: буфери на підключення, performance_schema і накладні витрати аллокатора; також файловий кеш ОС і метадані файлової системи, що рахуються в cgroup.

Виправлення: обмежте підключення, налаштуйте буфери на потік, розгляньте відключення або обмеження performance_schema якщо доречно, і залиште запас.

7) Симптом: здається, що пам’ять контейнера обмежена, але на хості росте swap

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

Виправлення: відключіть своп для контейнера або встановіть swap limit рівним memory limit; переконайтеся, що навантаження поміщається в RAM.

8) Симптом: «Ми вказали mem_limit у Compose, але в проді це не працює»

Корінь: оркестратор оверрайдить; поля Compose відрізняються залежно від режиму; runtime-конфіг відрізняється.

Виправлення: інспектуйте runtime-настройки і зафіксуйте їх у механізмі розгортання (Swarm/Kubernetes). Довіряйте docker inspect, а не YAML-фольклору.

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

Покроково: зупинка тихого тротлінгу для MySQL у Docker

  1. Підтвердіть реальний ліміт: docker inspect і /sys/fs/cgroup/memory.max.
  2. Зарезервуйте запас: ціль — принаймні 25–40% ліміту для непулових витрат і page cache для маленьких контейнерів (менше 8GB). Так, це консервативно. І так має бути.
  3. Встановіть innodb_buffer_pool_size за бюджетом, а не інтуїцією: для 2GB контейнера часто підходить 768MB–1.2GB залежно від навантаження і пулінгу підключень.
  4. Обмежте підключення: зменшіть max_connections і додайте пулінг на стороні додатку, якщо можливо.
  5. Аудит буферів на потік: тримайте sort/join/read buffers скромними, якщо ви не розумієте найгіршої конкурентності.
  6. Слідкуйте за тимчасовими таблицями на диску: їх зростання означає, що ви платите I/O за рішення щодо пам’яті.
  7. Алерти на тиск cgroup: відстежуйте memory.events high/max/oom_kill.
  8. Тестуйте під піковою конкурентністю: тести з 10 підключеннями приємні; у проді може бути 300, бо хтось забув закрити сокети.

Покроково: зупинка тихого тротлінгу для PostgreSQL у Docker

  1. Підтвердіть ліміт: ще раз — не сперечайтесь з cgroup.
  2. Встановіть shared_buffers помірковано: у контейнерах великі значення можуть витіснити все інше. Багато OLTP доходів працюють нормально з 256MB–2GB залежно від бюджету.
  3. Розраховуйте work_mem з урахуванням конкурентності: почніть з малого (4–16MB) і підвищуйте вибірково для конкретних ролей/запитів.
  4. Не давайте maintenance повністю з’їдати систему: налаштуйте maintenance_work_mem, щоб autovacuum і побудова індексів не голодували інші процеси.
  5. Використовуйте пулінг: підключення Postgres дорогі, і в контейнерах ця накладна витрата відчутніша.
  6. Відстежуйте temp_bytes і відставання autovacuum: виливи й борг vacuum — ранні попереджувальні сигнали.
  7. Алерти на події тиску пам’яті: сприймайте memory.events high як загрозу для SLO продуктивності.
  8. Розділяйте OLTP й аналітику: якщо не можете — ставте таймаути і ліміти конкурентності.

Гігієна на рівні контейнера (обидві БД)

  • Визначайте ліміти пам’яті свідомо: уникайте «малих лімітів для безпеки» без налаштування. Це не безпека; це відкладені відмови.
  • Визначте політику щодо свопу: для баз даних «своп як аварійний запас» зазвичай перетворюється на «своп як постійний стиль життя».
  • Спостерігайте з хоста і зсередини: host swap, host I/O і події cgroup — все важливо.
  • Не ховайтеся за перезапусками: якщо рестарт «вирішує» проблему — у вас є витік, backlog, цикл тиску пам’яті або несумісність кешування. Сприймайте це як підказку, а не лікування.

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

1) Чи Docker «тротлить» пам’ять моєї бази?

Не так, як тротлінг CPU. Обмеження пам’яті створюють тиск і жорсткі відмови (reclaim, swap, OOM). «Тротлінг» проявляється як уповільнення бази даних через те, що вона не може тримати гарячі сторінки в пам’яті.

2) Чому продуктивність кращає після перезапуску DB-контейнера?

Перезапуск скидає кеші, звільняє накопичену пам’ять на підключеннях, прибирає фрагментацію і інколи дозволяє ядру відновити page cache. Це як вимкнути радіо, щоб виправити пробите колесо: шум зміниться, але проблема лишається.

3) Для MySQL, чи можна виставити innodb_buffer_pool_size на 80% пам’яті контейнера?

Зазвичай ні. У маленьких контейнерах потрібен запас для буферів на підключення, фонових потоків, performance_schema і частини файлового кешу. Почніть з нижчого значення і доведіть, що можна додати більше.

4) Для Postgres, чи має shared_buffers бути великим у контейнерах?

Не за замовчуванням. Postgres отримує вигоду від page cache ОС, і в контейнерах усе це працює в межах одного бюджету пам’яті. Надмірне виділення shared_buffers може позбавити місця для work_mem, autovacuum і page cache.

5) Який найшвидший сигнал тиску пам’яті в cgroup v2?

/sys/fs/cgroup/memory.events. Якщо high зростає під час латентності — ви звільняєте пам’ять. Якщо інкрементує oom_kill — ви втрачаєте процеси.

6) Чи вимикати своп для контейнерів з БД?

Якщо вам важлива стабільна латентність — так, у більшості випадків. Своп може запобігти аваріям, але часто перетворюється на повільні відмови. Якщо залишаєте своп — моніторьте своп I/O і ставте реалістичні ліміти.

7) Чому диск виглядає повільним лише коли БД «зайнята»?

Бо «зайнята» може означати «без кешу». З меншим кешем БД робить більше реальних читань і генерує більше тимчасових даних. Диск не погіршився; ви змусили його працювати більше.

8) Чи можна вирішити це лише збільшенням ліміту контейнера?

Іноді так. Але якщо БД налаштована на «розширення до всього, що доступно» (занадто багато підключень, надмірний work_mem, великі буфери), вона врешті-решт вдариться об нову стелю. Поєднуйте збільшення ліміту з дисципліною конфігурації.

9) Хто чутливіший до сюрпризів пам’яті: MySQL чи PostgreSQL?

Різні сюрпризи. Команди MySQL часто перепризначають buffer pool і забувають про буфери на підключення. Команди Postgres недооцінюють множення work_mem при конкурентності. Обидві системи виглядають «нормальними», поки раптом не стануть ні.

10) Що, якщо я використовую Kubernetes замість звичайного Docker?

Принципи ті самі: cgroups, reclaim, OOM. Механіка змінюється (requests/limits, поведінка eviction). Ваша робота лишається: узгодити DB-настройки пам’яті з реальним обмеженням і відстежувати сигнали тиску.

Наступні кроки

Робіть це в порядку. Вони призначені, щоб перетворити нечітке «Docker дивний» скарження на контрольовану систему.

  1. Виміряйте реальний ліміт контейнера і чи дозволено своп. Запишіть це туди, де люди можуть знайти.
  2. Перевірте події тиску пам’яті cgroup під час повільних періодів. Якщо high зростає — у вас є курильна рушниця.
  3. Оберіть одну БД і складіть бюджет пам’яті: кеш/буфери + пам’ять на підключення/запит + обслуговування + накладні витрати. Будьте песимістичними в розрахунках.
  4. Зменшіть конкурентність перед тим, як ганятися за мікрооптимізаціями: пулінг підключень, черги, ліміти для аналітики.
  5. Переналаштуйте параметри БД під бюджет, а не навпаки.
  6. Додайте алерти на сигнали тиску (не лише відсоток використання). Саме тиск відчувають користувачі.
  7. Тестуйте навантаження з реальною конкурентністю. Якщо ваш тест не може викликати тиск — це юніт-тест у костюмі продуктивності.

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

← Попередня
DNS: Сильні прийоми dig/drill — команди, що відповідають на 90% DNS-загадок
Наступна →
Apache для WordPress: модулі й правила, що ламають сайти (та як це виправити)

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