Симптом: ви розгорнули контейнер бази даних і раптом увесь хост почав працювати так, ніби це USB-флешка 2009 року. SSH набирає символи з секундами затримки. Інші контейнери таймаутять. CPU “в нормі”. Пам’ять “в нормі”. Але все працює жахливо.
Причина: контенція сховища. Конкретно: одне навантаження споживає (або викликає) більшість бюджету IOPS і підвищує затримки для всіх інших. Docker не “зламав” ваш сервер. Ви просто болісно дізналися, що диски спільні, черги реальні, а синхронні семантики баз даних не домовляються.
Як виглядає IOPS starvation на Docker-хості
IOPS starvation — це не “диск заповнений”. Це “диск дуже зайнятий”. Точніше: шлях сховища насичений так, що середня затримка запитів зростає, і кожне навантаження, що ділить цей шлях, платить податок.
На Linux-хостах з Docker це зазвичай проявляється як:
- Високий iowait (але не завжди). Якщо дивитися тільки на відсоток CPU, можна пропустити проблему.
- Стрибки затримки для читань і/або записів. Бази даних ненавидять затримки запису, бо
fsync— це контракт, а не пропозиція. - Накопичення черги. Запити складаються в шарі блочного вводу-виводу або на пристрої, і все стає “повільним”, включно з несумісними сервісами.
- Дивні, непрямі симптоми: повільний DNS всередині контейнерів, повільне логування, таймаути systemd-сервісів, глюки демонів Docker, навіть “випадкові” провали health check-ів.
Чому SSH лагає? Тому що ваш термінал пише в PTY, shell-і торкаються диска для історії, логи флашаться, а ядро зайняте супроводом IO. Система не мертва. Вона чекає, поки повільний спільний ресурс повернеться з обіду.
Чому один контейнер може нашкодити всім
Контейнери Docker — це ізоляція процесів плюс трохи файлової магії. Вони не дають фізичної ізоляції. Якщо кілька контейнерів ділять:
- той самий блочний пристрій (той самий root-том, той самий EBS-диск, той самий RAID-груп, той самий SAN LUN),
- ту саму файлову систему,
- ті самі черги планувальника вводу-виводу,
- і часто той самий механізм writeback та журналювання,
…то один контейнер цілком може деградувати всіх інших. Це “шумний сусід” у найчистішому вигляді.
Бази даних — підсилювачі затримки
Більшість СУБД виконують багато малих випадкових операцій вводу-виводу. Вони також роблять fsync (або подібне) для гарантії збереження. Якщо сховище повільне, БД не “намагається сильніше”, вона блокується й чекає. Це блокування може каскадно впливати на пули з’єднань, потоки застосунків і хвилі повторних спроб.
І ось що Docker робить гіршим: стандартний стек зберігання може додавати накладні витрати, коли БД пише багато дрібних файлів або активно змінює метадані.
Overlay-файлові системи можуть посилювати запис
Багато Docker-хостів використовують overlay2 для записного шару контейнера. Overlay-файлові системи чудові для шарування образів і зручності розробки. Бази даних не турбують ваша зручність; їм потрібна передбачувана затримка.
Якщо ваша БД пише в записний шар контейнера (замість виділеного тому), ви можете спровокувати додаткові операції з метаданими, copy-up-поведінку та менш дружні шаблони записів. Іноді все гаразд. Іноді це кримінальна сцена з доказами.
«Але диск зайнятий лише на 20% пропускної здатності» — це пастка
Пропускна здатність (MB/s) — не вся історія. IOPS і затримка важливіші для багатьох навантажень БД. Ви можете бачити диск, що робить 5 MB/s, і при цьому він бути повністю насичений по IOPS при випадкових 4K-записах.
Ось чому ваш моніторинг, що відстежує “пропускну здатність диска”, каже, що все спокійно, а хост тихо плаче в iowait.
Жарт №1: коли база даних каже, що вона “чекає на диск”, вона не пасивно-агресивна. Вона точна.
Цікавинки та історичний контекст
- IOPS стали популярним показником через OLTP: транзакційні бази даних змусили індустрію міряти “скільки дрібних операцій за секунду”, а не тільки MB/s.
- Раніше за замовчуванням використовувався планувальник CFQ для справедливості на багатьох дистрибутивах; сучасні системи часто використовують
mq-deadlineабоnoneдля NVMe, що змінює відчуття “справедливості” при контенції. - cgroups v1 рано отримали blkio-контролі (вага й троттлінг). cgroups v2 перейшли на інтерфейс (
io.max,io.weight), що ловить людей під час міграції. - Overlayfs створювався для union-монтувань і шарів, а не для баз даних з інтенсивною змінюваністю. Він суттєво покращився, але невідповідність навантаження все ще проявляється в гірших хвостах затримки.
- Бар’єри запису й флаші існують, бо кеші брешуть: пристрої сховища реорганізовують записи заради швидкості, тому ОС використовує flush/FUA, щоб забезпечити порядок, коли додатки вимагають збереження.
- Журнальні файлові системи обмінюють додаткові записи на консистентність: ext4 і XFS надійні, але журналювання може підвищити IO-навантаження при сильному churn-і метаданих.
- Хмарне блочне сховище часто має кредити на burst: ви можете бути швидким деякий час, а потім раптово сповільнитись. Контейнер тут ні до чого — це поведінка рівня сховища.
- NVMe значно покращив паралелізм з багатьма чергами, але це не прибрало контенцію; вона просто перемістилась в інші черги й межі.
- “fsync-шторм” — класичний режим відмови: багато потоків викликають fsync, черги заповнюються, затримка різко зростає, і система, що виглядала добре на p50, розвалюється на p99.
Швидкий плейбук діагностики
Це послідовність “в мене 5 хвилин і продакшен горить”. Не сперечайтеся про архітектуру поки хост гальмує. Виміряйте, ідентифікуйте вузьке місце, а потім вирішіть — тротлити, перемістити чи ізолювати.
Перше: підтвердіть, що це затримка вводу-виводу, а не CPU чи пам’ять
- Перевірте load, iowait і run queue.
- Перевірте затримку диска і глибину черги.
- Перевірте, чи один процес/контейнер — головний споживач IO.
Друге: зв’яжіть біль з пристроєм і монтуванням
- Який блочний пристрій повільний?
- Чи Docker root знаходиться на тому пристрої?
- Файли бази даних на overlay2 чи на виділеному томі?
Третє: вирішіть про негайну міра
- Тротлити шумний контейнер (IOPS або пропускна здатність), щоб врятувати решту хоста.
- Перемістити дані БД на виділений том/пристрій.
- Зупинити кровотечу: вимкнути голосне логування, паузувати некритичні задачі, зменшити конкурентність.
Четверте: плануйте реальне виправлення
- Використовуйте томи для даних БД (не записний шар контейнера).
- Використовуйте ізоляцію IO (cgroups v2 io.max / weights) де можливо.
- Оберіть сховище й опції файлової системи, відповідні для DB sync-патернів.
Практичні завдання: команди, виводи, рішення
Нижче — реальні завдання, які можна виконати на Linux Docker-хості. Кожне містить: команду, приклад виводу, що це значить, і рішення, яке слід прийняти. Виконуйте їх від root або з sudo, де потрібно.
Завдання 1: Перевірити загальний CPU, iowait і load
cr0x@server:~$ top -b -n 1 | head -n 5
top - 12:41:02 up 21 days, 4:17, 2 users, load average: 9.12, 7.84, 6.30
Tasks: 312 total, 4 running, 308 sleeping, 0 stopped, 0 zombie
%Cpu(s): 8.1 us, 2.4 sy, 0.0 ni, 61.7 id, 25.9 wa, 0.0 hi, 1.9 si, 0.0 st
MiB Mem : 64035.7 total, 3211.3 free, 10822.9 used, 499... buff/cache
MiB Swap: 8192.0 total, 8192.0 free, 0.0 used. 5... avail Mem
Значення: 25.9% iowait і високий load свідчать про багато потоків, що чекають на IO. CPU не “завантажений”, він чекає.
Рішення: Припиніть звинувачувати планувальник і почніть вимірювати затримку диска та черги.
Завдання 2: Визначити, які диски страждають (затримка, використання, глибина черги)
cr0x@server:~$ iostat -x 1 3
Linux 6.2.0 (server) 01/03/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
7.52 0.00 2.61 23.98 0.00 65.89
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz %util
nvme0n1 95.0 820.0 3200.0 9870.0 0.0 15.0 0.00 1.80 3.2 42.7 18.90 98.50
Значення: %util близько 100% і w_await ~43ms означають, що пристрій насичений під запис. aqu-sz (середній розмір черги) великий, що підтверджує backlog.
Рішення: Знайдіть процес, що пише. Не налаштовуйте БД навмання; ідентифікуйте процес/контейнер, що генерує ці записи.
Завдання 3: Подивитися, чи блочний шар має backlog (статистика по пристрою)
cr0x@server:~$ cat /proc/diskstats | grep -E "nvme0n1 "
259 0 nvme0n1 128930 0 5128032 12043 942110 0 9230016 390122 0 220010 402210 0 0 0 0
Значення: Поля щільні, але простий підказувач — великий час, витрачений на IO порівняно з базовими значеннями. Поєднуйте з iostat -x для перевірки здорового глузду.
Рішення: Якщо побачите такий сплеск лише під навантаженням БД, ви в класичній ситуації “одне навантаження насичує пристрій”.
Завдання 4: Знайти top процеси за IO (вид хосту)
cr0x@server:~$ sudo iotop -b -n 3 -o
Total DISK READ: 0.00 B/s | Total DISK WRITE: 74.32 M/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 71.91 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
21491 be/4 postgres 0.00 B/s 55.12 M/s 0.00 % 89.15 % postgres: checkpointer
21510 be/4 postgres 0.00 B/s 10.43 M/s 0.00 % 63.20 % postgres: walwriter
4321 be/4 root 0.00 B/s 3.01 M/s 0.00 % 12.10 % dockerd
Значення: Процеси Postgres домінують у записах і витрачають великий відсоток часу в IO-wait. dockerd трохи пише (логи, шари), але він не головний порушник.
Рішення: Підтвердіть, якому контейнеру належать ці процеси, а потім вирішіть, тротлити чи перемістити його сховище.
Завдання 5: Зв’язати процес з контейнером
cr0x@server:~$ ps -o pid,cgroup,cmd -p 21491 | sed -n '1,2p'
PID CGROUP CMD
21491 0::/docker/8b6c3b7e4a3a9b7d2a7b55c4a1a2f9b9b0f6c0f9d1a7b1e3c9e3a2c1e5b postgres: checkpointer
Значення: Процес у cgroup Docker з ідентифікатором контейнера.
Рішення: Перегляньте цей контейнер. Підтвердіть його монтування та чи використовує він overlay2 чи том.
Завдання 6: Перевірити монтування контейнера та драйвер зберігання
cr0x@server:~$ docker inspect -f 'Name={{.Name}} Driver={{.GraphDriver.Name}} DataRoot={{json .GraphDriver.Data}} Mounts={{json .Mounts}}' 8b6c3b7e4a3a
Name=/db Driver=overlay2 DataRoot={"LowerDir":"/var/lib/docker/overlay2/l/..","MergedDir":"/var/lib/docker/overlay2/1d3.../merged","UpperDir":"/var/lib/docker/overlay2/1d3.../diff","WorkDir":"/var/lib/docker/overlay2/1d3.../work"} Mounts=[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]
Значення: Драйвер зберігання — overlay2 для записного шару, але каталог даних БД змонтований як іменований том. Це добре. Якби дані БД були в UpperDir, було б гірше.
Рішення: Якщо дані БД не на томі — виправте це в першу чергу. Якщо на томі — перевірте, де фізично лежить цей том (на тому ж пристрої чи ні?).
Завдання 7: Перевірити, який пристрій під Docker data root і томи
cr0x@server:~$ docker info --format 'DockerRootDir={{.DockerRootDir}} Driver={{.Driver}}'
DockerRootDir=/var/lib/docker Driver=overlay2
cr0x@server:~$ df -hT /var/lib/docker /var/lib/docker/volumes/pgdata/_data
Filesystem Type Size Used Avail Use% Mounted on
/dev/nvme0n1p2 ext4 900G 612G 242G 72% /
/dev/nvme0n1p2 ext4 900G 612G 242G 72% /
Значення: Docker root і том бази даних розташовані на тій самій файловій системі ext4 на тій же NVMe-розділі. Отже контенція гарантована.
Рішення: Якщо БД і решта хоста мають співіснувати, потрібно ізолювати IO (контролі cgroup) або перемістити дані БД на виділений пристрій.
Завдання 8: Перевірити опції монтування файлової системи (barriers, atime тощо)
cr0x@server:~$ findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /
/ /dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro
Значення: relatime — нормально. Нічого явно дивного. Якщо бачите екзотичні опції (або неправильні тюнінги), це привід для розслідування.
Рішення: Не змінюйте опції монтування під час інциденту, якщо не впевнені; для першої допомоги безпечніше тротлити/перемістити.
Завдання 9: Перевірити, чи хост застряг у writeback congestion
cr0x@server:~$ cat /proc/meminfo | egrep 'Dirty|Writeback|WritebackTmp'
Dirty: 82456 kB
Writeback: 195120 kB
WritebackTmp: 0 kB
Значення: Підвищений Writeback може вказувати на багато даних, що скидаються на диск. Це не доводить провини БД, але підтримує гіпотезу “перевантажений пайплайн сховища”.
Рішення: Якщо writeback постійно високий і затримка велика, зменшіть тиск записів і ізолюйте важкого записувача.
Завдання 10: Подивитися лічильники IO по процесу (для орієнтиру)
cr0x@server:~$ sudo cat /proc/21491/io | egrep 'write_bytes|cancelled_write_bytes'
write_bytes: 18446744073709551615
cancelled_write_bytes: 127385600
Значення: Деякі ядра показують лічильники так, що це може вводити в оману (а деякі файлові системи не звітують коректно). Сприймайте ці значення як орієнтовні, а не абсолютні.
Рішення: Якщо це не дає відповіді, покладайтеся на iotop, iostat і метрики затримки на рівні пристрою.
Завдання 11: Перевірити обмеження ресурсів контейнера (CPU/пам’ять) і відсутність IO-лімітів
cr0x@server:~$ docker inspect -f 'CpuShares={{.HostConfig.CpuShares}} Memory={{.HostConfig.Memory}} BlkioWeight={{.HostConfig.BlkioWeight}}' 8b6c3b7e4a3a
CpuShares=0 Memory=0 BlkioWeight=0
Значення: Немає явних лімітів. CPU і пам’ять часто обмежують; IO часто ні. Ось як один контейнер може звалити хост.
Рішення: Додайте контролі IO (Docker blkio на cgroups v1 або systemd/cgroups v2 контролі), або ізолюйте навантаження на різне сховище.
Завдання 12: Застосувати тимчасовий throttle по пропускній здатності до контейнера (екстрена міра)
cr0x@server:~$ docker update --device-write-bps /dev/nvme0n1:20mb 8b6c3b7e4a3a
8b6c3b7e4a3a
Значення: Docker застосував тротлінг записної пропускної здатності для цього пристрою до контейнера. Це грубий інструмент. Він може захистити хост ціною затримки й пропускної здатності БД.
Рішення: Використовуйте це, щоб зупинити побічну шкоду. Потім перемістіть БД на виділене сховище або впровадьте справедливий поділ з IO-контролерами.
Завдання 13: Застосувати тротлінг по IOPS (більш релевантно для дрібного IO)
cr0x@server:~$ docker update --device-write-iops /dev/nvme0n1:2000 8b6c3b7e4a3a
8b6c3b7e4a3a
Значення: Обмежує write IOPS. Зазвичай це краще відповідає болю від БД ніж троттлінг по MB/s.
Рішення: Якщо тротлінг покращує відгук хоста, ви підтвердили, що “шумний сусід IO” — головний режим відмови. Тепер треба спроектувати це у виправлення.
Завдання 14: Перевірити статус IO-контролера cgroup v2
cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs
cr0x@server:~$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory pids
Значення: Хост використовує cgroups v2 і підтримує контролер io.
Рішення: Віддавайте перевагу контролю IO cgroups v2, де доступно; він зрозуміліший і загальна тенденція Linux йде саме в нього.
Завдання 15: Подивитися IO-обмеження контейнера через його cgroup (v2)
cr0x@server:~$ CID=8b6c3b7e4a3a; cat /sys/fs/cgroup/docker/$CID/io.max
8:0 rbps=max wbps=max riops=max wiops=max
Значення: Немає обмежень наразі. Major:minor 8:0 — приклад; у вашому NVMe може бути інший. “max” означає безліміт.
Рішення: Якщо ви використовуєте systemd-юнити або runtime, що інтегрується з cgroups v2, встановіть io.max або ваги для scope сервісу/контейнера.
Завдання 16: Перевірити major:minor пристрою для правильного таргетингу тротлінгу
cr0x@server:~$ lsblk -o NAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT | sed -n '1,6p'
NAME MAJ:MIN SIZE TYPE MOUNTPOINT
nvme0n1 259:0 953.9G disk
├─nvme0n1p1 259:1 512M part /boot/efi
└─nvme0n1p2 259:2 953.4G part /
Значення: Якщо конфігуруєте io.max, треба вказати правильний major:minor (наприклад, 259:0 для диска або 259:2 для розділу залежно від налаштувань).
Рішення: Таргетуйте реальний пристрій, що має контенцію. Тротлити неправильний major:minor — це як “виправити” нічого з максимальною впевненістю.
Кореневі причини, що трапляються в інцидентах
1) Записи БД живуть у записному шарі контейнера
Якщо ваша база даних зберігає дані всередині файлової системи контейнера (overlay2 upperdir), ви накладаєте навантаження, інтенсивно пишучи, на механізм, призначений для шарування образів. Очікуйте гірших затримок і більше churn-у метаданих. Використовуйте Docker-том або bind-mount на виділений шлях.
2) Один спільний пристрій, нуль контролю справедливості IO
Навіть якщо дані БД на томі, якщо цей том — просто директорія на тій самій root-файловій системі, ви все одно ділите пристрій. Без ваг або троттлінгу найактивніший писак перемагає. Решта програє.
3) Стрибки затримки через bursty-хмарне сховище
Багато хмарних томів дають “базовий рівень + burst”. Активна БД може витратити burst-кредити, після чого том падає до базового рівня. Інцидент починається саме коли кредити закінчуються. Нічого в Docker тут не пояснює таймінг, тому команди годинами дивляться не туди.
4) Журналювання + fsync + висока конкурентність = пекло хвостової затримки
Бази часто роблять багато конкурентних записів, плюс WAL/redo логи, плюс чекпоінти. Додайте журналювання файлової системи і флаші кешу пристрою — і отримаєте “ідеально середню” пропускну здатність з катастрофічною p99-затримкою запису.
5) Логування на тому ж пристрої, що й БД
Коли диск насичений, логи не просто “пишуться повільніше”. Вони можуть блокувати потоки застосунків, заповнювати буфери і створювати додатковий IO у найгірший момент. JSON-логи симпатичні, поки вони не стають вашим основним записуючим навантаженням під час аварії.
Жарт №2: Якщо ви поклали базу даних і гучні debug-логи на один диск, ви винайшли нову розподілену систему: «латентність».
Типові помилки (симптом → коренева причина → виправлення)
Тут ви припиняєте повторювати той самий інцидент під різними назвами.
1) Симптом: CPU низький, але load average високий
- Коренева причина: потоки у незупинному стані IO sleep; load їх враховує.
- Виправлення: перевірте
iostat -xнаawaitі%util; знайдіть top IO-процеси черезiotop; ізолюйте або тротльте.
2) Симптом: кожен контейнер стає повільним, включно з “непов’язаними” сервісами
- Коренева причина: спільний блочний пристрій насичений; writeback ядра і операції з метаданими впливають на всіх.
- Виправлення: перемістіть БД на виділений пристрій/том; застосуйте cgroup IO-контролі; розділіть Docker root, логи і дані БД на різні пристрої, де можливо.
3) Симптом: затримки БД під час чекпоінтів або компактування
- Коренева причина: короткі фази вибухових записів викликають накопичення черги і флаш-шторм.
- Виправлення: акуратно налаштуйте параметри чекпоінту/компактування БД; обмежте IO для фонового писання; забезпечте достатню стійку IOPS-потужність.
4) Симптом: “Пропускна здатність диска виглядає нормально” в моніторингу
- Коренева причина: ви моніторите MB/s, а не затримку або IOPS; дрібні випадкові IO перші насичують IOPS.
- Виправлення: моніторьте
await,aqu-sz, перцентилі затримки пристрою, і IOPS для томів у хмарному середовищі.
5) Симптом: контейнер БД швидкий сам по собі, повільний на продакшен-хості
- Коренева причина: тестовий хост мав ізольоване сховище або менше шумних сусідів; продакшен-хост ділить root-диск з іншими.
- Виправлення: тестуйте продуктивність на репрезентативному сховищі; забезпечте ізоляцію через відокремлені пристрої або IO-контролі.
6) Симптом: після “оптимізації” стало гірше
- Коренева причина: відключення збереження, зміни опцій монтування або збільшення конкурентності штовхнули систему у гіршу хвостову затримку або ризик пошкодження даних.
- Виправлення: віддавайте перевагу передбачуваній затримці й надійності; настроюйте по одній змінній; перевіряйте по затримках, а не за відчуттями.
Три корпоративні міні-історії зі сховищ
Міні-історія 1: Інцидент через хибне припущення
Команда мігрувала моноліт у “кілька контейнерів” на великій VM. Почали з бази, бо вона найбільш лякала, і в staging “все працювало”. У продакшені кожний деплой після появи контейнера БД приніс хвилю таймаутів від несумісних сервісів: API, job runner, навіть sidecar метрик.
Початкове припущення було класичне: “контейнери ізолюють ресурси”. Вони обмежили CPU і пам’ять для БД-контейнера, потиснули плечима і перейшли далі. Коли навантаження на хост поповзло до небес при практично вільному CPU, звинувачення почали кочувати між мережею, DNS, overlay-мережею Docker і короткою, але пристрасною суперечкою про версії ядра.
Один співробітник запустив iostat -x і поклав край дебатам. Кореневий диск був ~100% завантажений із затримками запису як гірський хребет. Каталог даних БД був bind-mount у /var/lib/docker на тій же root-файловій системі, що й все інше, включно з логами контейнерів і шарами образів.
Коли вони прийняли, що “контейнер” не означає “окремий диск”, виправлення було просте: приєднати виділений том для БД, змонтувати його в директорію даних і перемістити логи з root-диска. Хост перейшов з “таємничого системного провалу” до “нудного комп’ютера”, що в операціях — найвища похвала.
Міні-історія 2: Оптимізація, що вдарила в спину
Інша компанія мала проблему продуктивності: їхній Postgres-контейнер був вузьким місцем по записах у піковий трафік. Хтось запропонував оптимізацію: послабити fsync-натиск, аргументуючи “ми маємо реплікацію” і “хмарне сховище надійне”. Зміна покращила пропускну здатність в синтетичних тестах, і її запустили.
Через два тижні нода впала під час галасливого події сховища. Катастрофи не стало, але таймінг був ідеальний: високе навантаження запису, відставання репліки і failover. Вони не втратили всю базу, але достатньо транзакцій, щоб тиждень говорити незручно. Тим часом початковий симптом (затримка на рівні хоста) повернувся під час сплесків, бо реальний вузький момент був у насиченні черги пристрою і спільній IO-контенції, а не тільки в overhead fsync.
Вони відкотили компроміс по надійності і зробили доросле виправлення: ізолювали БД на виділеному блочному пристрої з передбачуваними IOPS, додали IO-ваги, щоб зберегти працездатність решти хоста, і обмежили найбільш агресивні фонові роботи. “Оптимізація” не була злою; вона була неправильно застосована. Вона оптимізувала не той шар і купила швидкість за рахунок ваших даних.
Урок: якщо ви збираєтеся міняти надійність на швидкість — озвучте це, зафіксуйте і отримайте підпис від тих, кого будуть пейджити при наслідках.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Ця історія менш драматична, що і зробило її успішною. Команда запускала кілька stateful-контейнерів на невеликому фліті: БД, черга і пошукова система. У них була політика: кожен stateful-сервіс має використовувати виділений монтувальний шлях, підкріплений виділеним класом тому, і кожен сервіс має мати явний IO-бюджет, прописаний в супровідних нотатках розгортання.
Нічого витонченого. Ніяких кастомних патчів ядра чи артистичних планувальників. Вони просто відмовились класти персистентні дані на Docker root filesystem і тримали логи додатків на окремому шляху з ротацією і здоровими налаштуваннями рівня логування.
Одного дня пакетна задача пішла врознос і почала забивати персистентність черги. Латентність для того сервісу підскочила, але решта хоста залишилась чуйною. Їхні алерти точно вказали на пристрій, що використовувався томом черги, а не “сервер повільний”. Вони тротлили IO контейнера пакетної задачі, стабілізувалися і провели постмортем без жодних проблем з SSH.
Іноді “нудні” практики — це просто передплачений інцидент-реагування.
Виправлення та запобіжні заходи, що працюють
1) Покладіть дані БД на реальний том, а не на записний шар контейнера
Якщо запам’ятати щось одне — нехай це буде це: бази даних повинні писати на виділений монтувальний шлях (іменований том або bind mount), бажано підкріплений виділеним пристроєм. Це означає:
- Каталог даних БД — це mount point, видимий в
findmnt. - Цей mount відповідає пристрою, який ви можете виміряти окремо.
- Docker root (
/var/lib/docker) не несе на собі обов’язок збереження вашої БД.
2) Розділяйте обов’язки: образи/логи vs персистентні дані
Docker data root зайнятий: розпакування образів, завантаження шарів, metadata overlay, запис логів контейнерів і багато іншого. Поєднайте це з БД, що пише WAL і робить чекпоінти — і ви отримали машину контенції.
Практичний розподіл:
- Диск A: OS + Docker root + логи контейнерів (достатньо швидкий, але не священний).
- Диск B: DB volume (передбачувані IOPS, низька затримка, моніторинг).
3) Використовуйте контролі IO для забезпечення справедливості
Якщо доводиться ділити пристрій, забезпечте справедливість. Для cgroups v2 контролер io дає ваги й тротлінг. UX Docker для цього варіюється за версією і runtime, але принцип стабільний: не дозволяйте одному контейнеру стати пилососом для диска.
Тротлінг не лише карає. Воно може зробити різницю між “БД повільна” і “все впало”. Під час інциденту часто треба зберегти чуйність решти хоста, поки вирішуєш долю БД.
4) Вимірюйте затримки, а не тільки завантаження
%util корисний, але недостатній. Пристрій може показувати менше ніж 100% завантаження і все одно мати жахливі затримки, особливо в віртуалізованому або мережевому сховищі, де “пристрій” — абстракція.
Що вам потрібно знати:
- Середня й хвостова затримка для читань/записів.
- Тренди глибини черги під навантаженням.
- Обмеження IOPS і чи ви їм близько.
5) Налаштовуйте поведінку БД лише після виправлення геометрії сховища
Тюнінг БД має своє місце. Але якщо БД просто ділить невірний диск з усіма, тюнінг — це витрата часу і подарунок майбутнім інцидентам.
Спочатку: ізолюйте шлях до пристрою. Потім: оцініть чекпоінти БД, налаштування WAL і фонові роботи. Інакше отримаєте крихку конфігурацію, яка працює до наступного піку.
6) Не дозволяйте логам контейнера стати IO-навантаженням
Якщо ви логируєте багато в JSON і дозволяєте Docker писати це на ту саму файлову систему, що й БД, ви конкуруєте за ту саму чергу. Використовуйте ротацію логів, зменшуйте verbosity і розгляньте варіант виносу великих обсягів логів поза хост або на окремий пристрій.
7) Не ігноруйте модель продуктивності хмарного тому
Якщо ваш том має базовий/burst-профіль, змоделюйте під це навантаження. БД, що “нормальна 20 хвилин, а потім жахлива”, зазвичай не містика — це спорожнення кредитного відра. Пропишіть або забезпечте стійку продуктивність, або спроектуйте систему навколо цієї моделі.
Цитата для пам’яті
Надія — не стратегія.
— General Gordon R. Sullivan
Чеклісти / покроковий план
Покроково: стабілізувати інцидент (30 хвилин)
- Підтвердіть IO wait/латентність: запустіть
topіiostat -x. - Знайдіть писача: запустіть
iotop -oі зв’яжіть PID-и з контейнерами через cgroups. - Перевірте розміщення сховища: перевірте
docker inspectмонтування іdf -hTдля Docker root і томів. - Мітегуйте: тротльте записні IOPS або bandwidth порушника, або тимчасово зменшіть його конкуренцію (ліміти підключень БД, фонова робота).
- Зменшіть побічний IO: знизьте verbosity логів; переконайтеся, що ротація логів працює; призупиніть несуттєві пакетні задачі.
- Комунікуйте: чітко вкажіть “затримка диска на хості насичена” і яке мітегування застосовано. Уникайте туманних “Docker повільний”.
Покроково: постійне виправлення (один спринт)
- Перенесіть дані БД на виділене сховище: окремий пристрій або клас тому з гарантією IOPS.
- Розділіть Docker root від stateful-tomів: тримайте
/var/lib/dockerпоза пристроєм БД. - Впровадьте контролі IO: використовуйте cgroup v2
io.max/io.weight(або Docker blkio де підтримується). - Налаштуйте моніторинг, що ловить це раніше: латентність пристрою, глибина черги і стан кредитів/burst для томів у хмарі.
- Навантажте тестами справжній стек: не “БД на моєму ноуті”, а реальний бекенд зберігання і runtime контейнерів.
- Напишіть ранубук: включіть точні команди з цієї статті і точки прийняття рішень.
Pre-deploy чекліст для будь-якого контейнера БД
- Каталог даних БД — це виділений mount (volume/bind mount), не overlay2 записний шар.
- Цей mount знаходиться на пристрої з відомими стійкими IOPS і характеристиками затримки.
- Контейнер має визначену IO-політику (вага або throttle), а не “безліміт”.
- Логи обмежені за швидкістю і ротацією; великі обсяги логів не потрапляють на диск БД.
- Налаштовані алерти по латентності диска і глибині черги, а не тільки по заповненості диска.
FAQ
1) Це баг Docker?
Зазвичай ні. Це контенція спільних ресурсів. Docker спрощує колокацію навантажень, а це спрощує випадкову колокацію їхніх проблем зі сховищем.
2) Чому тільки один контейнер БД викликає затримки всього хоста?
Тому що блочний пристрій спільний. Коли цей контейнер насичує IOPS або викликає високу затримку, черги ядра заповнюються і всі чекають за ним у черзі.
3) Чи вирішить проблему переміщення БД на іменований том?
Тільки якщо том підкладається іншим сховищем або іншим класом продуктивності. Іменований том під /var/lib/docker/volumes на тій же файловій системі — це організаційна міра, а не ізоляція.
4) У чому різниця між IOPS і пропускною здатністю у цьому контексті?
IOPS — операції за секунду (зазвичай дрібні 4K читання/записи). Пропускна здатність — MB/s. Бази даних часто вузько залежать від IOPS і затримки, а не від ширини каналу.
5) Чи слід змінювати планувальник IO?
Інколи так, але це рідко перша дія. Налаштування планувальника не врятує вас від спільного пристрою без ізоляції, коли БД інтенсивно робить синхронні записи.
6) Чи завжди overlay2 уповільнює бази даних?
Ні. Але розміщення “гарячих” даних БД у записному шарі — поширена помилка, і поведінка overlay може погіршувати операції з метаданими. Використовуйте томи для даних БД.
7) Як надійно обмежити IO для контейнера?
Використовуйте контролі cgroups для IO. Docker підтримує --device-read-bps, --device-write-bps і IOPS-варіанти для тротлінгу. На cgroups v2 також можна керувати io.max через менеджер сервісів/інтеграцію runtime.
8) Чому “100% завантаження диска” буває на NVMe? Хіба вони не швидкі?
NVMe швидкий, але не безмежний. Дрібні синхронні записи і флаші все ще можуть наситити черги, і один пристрій має скінченну характеристику затримки під навантаженням.
9) Чому мої додатки провалюють health check-и під час контенції IO?
Health check-и часто роблять операції диска чи мережі, що залежать від чуйного ядра і своєчасного логування. Під великим iowait усе відстає, включно з “простими” перевірками.
10) Яка найбезпечніша негайна міра, якщо продакшен тане?
Тротльте шумний контейнер по IO, щоб відновити чуйність хоста, потім плануйте міграцію даних на виділене сховище. Можливе зупинення БД необхідне, але тротлінг дає час.
Висновок: наступні кроки на цьому тижні
Якщо один контейнер БД робить усе повільним — повірте сигналам сховища. Вимірюйте затримку й глибину черги, ідентифікуйте контейнер і припиніть вірити, що спільні диски самі організуються справедливо.
Конкретні наступні кроки:
- Додайте до дашбордів затримість пристрою і глибину черги (не тільки % заповнення диска або MB/s).
- Аудит stateful-контейнерів: перевірте, що каталоги даних — це томи/bind-mount-и і відображаються на реальні пристрої.
- Розділіть сховище: перемістіть дані БД з Docker root filesystem на виділене сховище з відомою стійкою продуктивністю.
- Впровадьте контролі IO: встановіть розумні троттлінги або ваги, щоб один контейнер не міг захопити весь хост.
- Напишіть ранубук: скопіюйте “швидкий плейбук діагностики” і завдання в вашу on-call wiki, а потім програйте game day.