Коли Linux показує, що ваш CPU «простий», але сервер відчувається наче під водою, зазвичай ви маєте справу з одним і тим же злом: I/O wait. Ваші ядра не зайняті обчисленнями — вони стоять у черзі, чекаючи відповіді від сховища. Тим часом один контейнер Docker старанно «жує» записі, немов платять за кожен fsync, і весь хост перетворюється на повільно рухому трагедію.
Оце неприємна сторона щільності контейнерів: гучний сусід може змусити всіх запізнюватися. Добра новина в тому, що Linux дає інструменти, щоб виявити винного й хірургічно його обмежити — без перезавантаження, без гадань і без «давайте спочатку додамо диски» як першого кроку.
Ментальна модель: що насправді означає iowait на хості з контейнерами
У Linux I/O wait — це час, який CPU проводить у стані простого поки в системі є принаймні один незавершений I/O-запит. Це саме по собі не вимірювач завантаженості диска. Це вимір того, що «CPU хотів щось виконати, але завдання заблоковані на I/O, тож планувальник відзначає це як wait».
На Docker-хості «блокування на I/O» часто означає:
- Записи через Overlay Filesystem, що підсилюються в кілька реальних записів.
- Живий потік логів, що змушує синхронні записи на гарячу файлову систему.
- База даних, яка часто виконує fsync у щільному циклі.
- Резервне копіювання, що стрімує читання й може голодувати записи (або навпаки) залежно від планувальника та пристрою.
- Штурми метаданих: мільйони дрібних файлів, пошуки в директоріях, оновлення inode, тиск на журнал.
Контейнери не мають магічного сховища. Вони ділять ті самі блокові пристрої хоста, ту саму чергу й часто той самий журнал файлової системи. Якщо одне навантаження забиває чергу глибокими I/O без контролю чесності, інші навантаження також починають стояти в черзі. Ядро спробує забезпечити чесність, але чесність не означає «ваш p99 латентності залишиться адекватним».
Дві форми «I/O wait з пекла»
Пекло латентності: IOPS не дуже високі, але латентність стрибає. Думайте: збій прошивки NVMe, скидання кешу RAID-контролера, задушення журналу файлової системи або синхронно-важкі записи. Користувачі відчувають це як «усе зависло», хоча графіки пропускної здатності виглядають помірковано.
Пекло глибини черги: один контейнер тримає пристрій насиченим багатьма запитами в польоті. Пристрій «зайнятий» і середня пропускна здатність виглядає пристойно. Тим часом інтерактивні завдання та інші контейнери чекають за горою запитів.
Цитата, яку варто тримати під рукою
Парафразована ідея: «Надія — це не стратегія; потрібен план і петлі зворотного зв’язку.»
— Gene Kranz (місія-операційний підхід, парафраз)
Це саме та позиція, яка вам потрібна тут. Не відмахуйтесь. Вимірюйте, атрибутуйте, а потім застосовуйте контроль.
Швидкий план діагностики (перевірте це насамперед)
Якщо ваш хост «плавиться», у вас немає часу на інтерпретаційні танці. Робіть це в порядку.
1) Підтвердіть, що це затримка/черга сховища, а не CPU чи пам’ять
- Перевірте системне завантаження і iowait.
- Перевірте латентність диска і глибину черги.
- Перевірте тиск пам’яті (шторм свопінгу може виглядати як I/O-шторм, бо ним і є).
2) Визначте процеси з найбільшим I/O на хості
- Використовуйте
iotop(швидкість читання/запису за процесом). - Використовуйте
pidstat -d(статистика I/O по процесах в часі). - Використовуйте
lsof, щоб бачити, які файли піддаються навантаженню (логи? файли БД? overlay diff?).
3) Зіставте ці процеси з контейнерами
- Знайдіть ID контейнера через cgroups (
/proc/<pid>/cgroup). - Або зіставте шляхи файлів назад до diff-директорій у
/var/lib/docker/overlay2.
4) Застосуйте найменш погане обмеження
- Якщо у вас cgroup v2:
io.maxіio.weight. - Якщо у вас cgroup v1: blkio-тротлінг і ваги (краще працює на прямих блокових пристроях, менш ефективно на шаруватих ФС).
- Також зменшуйте ушкодження: обмежте логи, перемістіть гарячі шляхи на окремі диски та виправте поведінку додатка (частота флешу, батчинг тощо).
Жарт №1: Якщо ваш iowait 60%, ваші CPU не «ледачі» — вони просто застрягли в найдовшій касі у світі.
Цікаві факти та контекст (чому ця проблема повторюється)
- Linux «iowait» — це не час, проведений диском. Це час простою CPU, поки I/O очікується, який може зростати навіть на швидких пристроях, якщо завдання блокуються синхронно.
- cgroups для контролю I/O з’явилися пізніше за CPU/пам’ять. Початкові налаштування контейнерів добре керували CPU й пам’яттю, але були слабкими щодо справедливості зі збереженням даних.
- blkio-тротлінг історично краще працював з прямим доступом до блокових пристроїв. Коли все йде через шар файлової системи (наприклад overlay2), атрибуція й контроль стають мутнішими.
- Раніше Completely Fair Queuing (CFQ) був дефолтом для ротаційних носіїв. Сучасні ядра схиляються до планувальників на кшталт BFQ або mq-deadline залежно від класу пристрою й цілей.
- Write amplification у OverlayFS реальний. «Маленький запис» у контейнері може викликати copy-up у верхній шар і додаткові метадані на хості.
- Журналювання Ext4 може стати вузьким місцем при метаданих-важких навантаженнях. Багато створень/видалень файлів може навантажити журнал, навіть якщо пропускна здатність даних низька.
- Логи й досі частий кривдник. Єдина річ більш безмежна, ніж людський оптимізм — незамінований лог-файл на спільному диску.
- NVMe не застраховано від стрибків латентності. Прошивка, теплове обмеження, управління енергоспоживанням і збір сміття на пристрої можуть давати болісні хвостові затримки.
- «Load average» включає завдання в стані uninterruptible sleep (D state). Затримки сховища можуть надути load навіть коли завантаження CPU виглядає нормальним.
Практичні завдання: команди, виводи та рішення (12+)
Це інструменти для поля. Кожне завдання містить: команду, типовий вивід, що це означає та рішення, яке треба прийняти далі. Запускайте від root або з достатніми правами там, де потрібно.
Завдання 1: Підтвердьте симптоми iowait і load
cr0x@server:~$ uptime
14:22:05 up 21 days, 6:11, 2 users, load average: 28.12, 24.77, 19.03
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (server) 01/02/2026 _x86_64_ (32 CPU)
14:22:07 CPU %usr %nice %sys %iowait %irq %soft %steal %idle
14:22:08 all 3.21 0.00 1.18 62.44 0.00 0.09 0.00 33.08
14:22:09 all 2.97 0.00 1.11 64.02 0.00 0.07 0.00 31.83
14:22:10 all 3.45 0.00 1.29 61.88 0.00 0.08 0.00 33.30
Що це означає: load average величезний, але user+system CPU низькі, а iowait ≈ 60%. Багато завдань заблоковані через сховище.
Рішення: розглядайте це як проблему латентності/чергування диска. Перейдіть до метрик на рівні диска; не витрачайте час на налаштування CPU.
Завдання 2: Перевірте латентність диска і глибину черги з iostat
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (server) 01/02/2026 _x86_64_ (32 CPU)
Device r/s w/s rkB/s wkB/s await r_await w_await aqu-sz %util
nvme0n1 120.0 980.0 5120.0 82400.0 86.32 14.22 95.01 22.41 99.50
Що це означає: пристрій майже під завантаженням (%util ~99.5). Середня латентність записів жахлива (~95ms), а глибина черги (aqu-sz) висока.
Рішення: потрібна атрибуція. Знайдіть, які процеси створюють I/O.
Завдання 3: Перевірте, чи завдання застрягли в стані D (очікування сховища)
cr0x@server:~$ ps -eo state,pid,comm,wchan:32 --sort=state | head -n 15
D 18342 postgres io_schedule
D 19011 containerd-shim io_schedule
D 19102 python3 ext4_sync_file
D 24410 java io_schedule
R 1322 ksoftirqd/3 -
R 31201 iostat -
S 1 systemd ep_poll
Що це означає: процеси в стані D блокуються у шляхах I/O ядра (io_schedule, ext4_sync_file).
Рішення: знайдіть, які з них належать «галасливому» контейнеру(ам).
Завдання 4: Отримайте швидкості I/O по процесах (швидкий список підозрюваних)
cr0x@server:~$ iotop -oPa -d 1 -n 3
Total DISK READ: 5.00 M/s | Total DISK WRITE: 90.00 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
19102 be/4 root 0.00 B/s 45.00 M/s 0.00 % 95.00 % python3 /app/worker.py
18342 be/4 postgres 0.00 B/s 20.00 M/s 0.00 % 80.00 % postgres: checkpointer
24410 be/4 app 0.00 B/s 12.00 M/s 0.00 % 60.00 % java -jar service.jar
Що це означає: один Python-воркер домінує у записах; Postgres checkpointer теж активний (можливо нормальна поведінка, можливо підштовхнута кимось іншим).
Рішення: зіставте PID 19102 з контейнером і подивіться, що саме він пише.
Завдання 5: Зіставте PID з Docker-контейнером через cgroups
cr0x@server:~$ cat /proc/19102/cgroup
0::/docker/4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d
Що це означає: процес знаходиться в контейнері 4c3f1f8f2a7b....
Рішення: визначте людське ім’я, образ і призначення цього контейнера.
Завдання 6: Вирішіть ID контейнера в ім’я та перевірте налаштування
cr0x@server:~$ docker ps --no-trunc --filter id=4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d acme/etl:2.8.1 "python3 /app/wo..." 3 hours ago Up 3 hours etl-worker-07
cr0x@server:~$ docker inspect -f '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' etl-worker-07
json-file {"max-file":"0","max-size":"0"}
Що це означає: контейнер — ETL-воркер і використовує дефолтні json-file логи без ротації (max-size 0, max-file 0).
Рішення: підозрюйте, що логи спричиняють записи, і/або гарячий шлях додатку. Перевірте, які файли зайняті.
Завдання 7: Подивіться, які файли пише галасливий PID
cr0x@server:~$ lsof -p 19102 | awk '{print $4, $9}' | head -n 15
1u /dev/null
2u /dev/null
3w /var/lib/docker/containers/4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d/4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d-json.log
4w /data/spool/outgoing/events.ndjson
5w /data/spool/outgoing/events.ndjson
Що це означає: він пише в Docker json лог-файл і також у змонтований шлях хоста /data/spool/outgoing/events.ndjson.
Рішення: перевірте використання файлової системи, опції монтування, і чи цей шлях розділяє той самий пристрій з рештою.
Завдання 8: Визначте, яка файлова система/пристрій підкріплює гарячий шлях
cr0x@server:~$ df -T /var/lib/docker /data/spool/outgoing
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 ext4 960379012 712883120 198472344 79% /
/dev/nvme0n1p2 ext4 960379012 712883120 198472344 79% /
Що це означає: і Docker storage, і директорія spool розміщені на тій самій ext4 файловій системі на одному пристрої.
Рішення: будь-який шторм записів карає весь хост. Якщо не вдається негайно перемістити шляхи на інший пристрій, тротлінг стає аварійним гальмом.
Завдання 9: Перевірте драйвер зберігання Docker (поведінка overlay2 важлива)
cr0x@server:~$ docker info | sed -n '1,40p'
Client:
Version: 25.0.3
Server:
Containers: 47
Running: 39
Storage Driver: overlay2
Backing Filesystem: extfs
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Що це означає: overlay2 на ext4, json-file логи, cgroup v2 з systemd-драйвером. Це добра база для використання io.max/io.weight в cgroup v2.
Рішення: тротуйте через cgroup v2 (переважно) і виправляйте логування.
Завдання 10: Перевірте тиск пам’яті і своп (не пропустіть очевидне)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 125Gi 96Gi 2.1Gi 1.2Gi 27Gi 18Gi
Swap: 16Gi 9.5Gi 6.5Gi
cr0x@server:~$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 18 9961472 2200000 210000 22400000 80 150 1200 92000 1200 3100 3 1 34 62 0
1 20 9961600 2180000 210000 22390000 60 120 1100 89000 1180 3000 3 1 32 64 0
Що це означає: своп використовується і є I/O свопу (si/so). Це може посилити дисковий біль, але домінуючою проблемою є велика кількість блокових записів (bo) і високий wa.
Рішення: тримайте своп у полі зору, але продовжуйте атрибуцію I/O. Якщо своп шалений, також подумайте про обмеження пам’яті для кривдника.
Завдання 11: Ідентифікуйте шлях cgroup контейнера (systemd driver, cgroup v2)
cr0x@server:~$ systemctl status docker | sed -n '1,12p'
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-12-10 08:14:10 UTC; 3 weeks 2 days ago
Docs: man:docker(1)
Main PID: 1420 (dockerd)
cr0x@server:~$ cat /proc/19102/cgroup
0::/system.slice/docker-4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d.scope
Що це означає: з systemd + cgroup v2 контейнер живе в системному scope unit під /sys/fs/cgroup/system.slice/.
Рішення: застосуйте обмеження I/O до цього scope unit через файли cgroup (або властивості systemd), а не наосліп.
Завдання 12: Знайдіть major:minor блочного пристрою для файлової системи
cr0x@server:~$ findmnt -no SOURCE,TARGET,FSTYPE /var/lib/docker
/dev/nvme0n1p2 / ext4
cr0x@server:~$ lsblk -o NAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT /dev/nvme0n1p2
NAME MAJ:MIN SIZE TYPE MOUNTPOINT
nvme0n1p2 259:2 915G part /
Що це означає: цільовий блочний пристрій має major:minor 259:2. Контроли I/O в cgroup v2 вимагають цього ідентифікатора.
Рішення: встановіть io.max і/або io.weight для цього пристрою на cgroup контейнера.
Завдання 13: Застосуйте bandwidth-ліміт cgroup v2 (негайне стримування)
cr0x@server:~$ CG=/sys/fs/cgroup/system.slice/docker-4c3f1f8f2a7b6c7f2c7a1d3b8ad9b9d2f2b2a9c8e1d2a3b4c5d6e7f8a9b0c1d.scope
cr0x@server:~$ echo "259:2 wbps=20971520 rbps=10485760" | sudo tee $CG/io.max
259:2 wbps=20971520 rbps=10485760
cr0x@server:~$ cat $CG/io.max
259:2 rbps=10485760 wbps=20971520
Що це означає: читання обмежено 10 МіБ/с, записи — 20 МіБ/с для цього контейнера на кореневому пристрої.
Рішення: спостерігайте за латентністю й відгуком системи. Якщо хост відновиться, ви підтвердили «галасливого сусіда» і виграли час для реального виправлення.
Завдання 14: Негайно перевірте покращення
cr0x@server:~$ iostat -x 1 3
Device r/s w/s rkB/s wkB/s await aqu-sz %util
nvme0n1 110.0 260.0 4800.0 21000.0 12.40 2.10 78.00
cr0x@server:~$ mpstat 1 3 | tail -n 4
14:25:08 all 6.01 0.00 2.10 8.33 0.00 0.12 0.00 83.44
Що це означає: латентність пристрою впала (await ≈ 12ms) і iowait різко знизився (≈8%). Хост дихає краще.
Рішення: тримайте ліміт як тимчасовий запобіжник, а потім виправіть корінь проблеми: логування, батчинг, розміщення даних або дизайн додатка.
Завдання 15: Налаштуйте ротацію логів (зупиніть витік)
cr0x@server:~$ docker update --log-opt max-size=50m --log-opt max-file=3 etl-worker-07
etl-worker-07
cr0x@server:~$ docker inspect -f '{{json .HostConfig.LogConfig}}' etl-worker-07
{"Type":"json-file","Config":{"max-file":"3","max-size":"50m"}}
Що це означає: логи будуть ротаційними і не зростатимуть без обмежень.
Рішення: якщо логи були головним внеском, ви можете зменшити ліміт I/O або видалити його після підтвердження стабільності.
Завдання 16: Знайдіть контейнери, що створюють найбільшу зміну в writable-layer
cr0x@server:~$ docker ps -q | while read c; do
> name=$(docker inspect -f '{{.Name}}' $c | sed 's#^/##')
> rw=$(docker inspect -f '{{.GraphDriver.Data.UpperDir}}' $c 2>/dev/null)
> if [ -n "$rw" ]; then
> sz=$(sudo du -sh "$rw" 2>/dev/null | awk '{print $1}')
> echo "$sz $name"
> fi
> done | sort -h | tail -n 8
3.1G etl-worker-07
4.8G api-gateway
6.2G report-builder
9.7G search-indexer
Що це означає: writable layers (UpperDir) показують сильну зміну. Не всяка зміна — погана, але це запах проблеми.
Рішення: перемістіть шляхи з інтенсивними записами на volume/bind mount і зменшіть записи у writable layer контейнера.
Зіставлення I/O болю з винним контейнером
Найскладніша частина «Docker спричинив I/O wait» — це те, що диск не знає, що таке контейнер. Ядро знає PIDи, inodeи, блочні пристрої й cgroup. Ваше завдання — з’єднати ці крапки.
Почніть від диска і рухайтеся вгору
Якщо iostat показує один пристрій з жахливим await і високим %util, ви маєте або насичення, або латентність на рівні пристрою. Далі:
- Використовуйте
iotop, щоб ідентифікувати top writer PIDи. - Зіставте PID → cgroup → container ID.
- Використовуйте
lsof, щоб зрозуміти шлях: логи, том, overlay UpperDir, файли бази даних.
Почніть від контейнера і рухайтеся вниз
Іноді ви вже підозрюєте винного («це ж пакетна задача, чи не так?»). Підтвердіть без самообману:
- Перевірте конфіг логування контейнера і монтування томів.
- Перевірте розмір json лог-файлу й writable layer.
- Перевірте, чи це синхронно-важкий додаток (бази даних, брокери повідомлень, все, що цінує надійність).
Розуміння особливого хаосу overlay2
overlay2 ефективний для багатьох навантажень, але патерни з інтенсивними записами можуть стати дорогими. Записи у файли, що існують у нижньому (image) шарі, викликають copy-up в upper layer. Операції з метаданими також можуть вибухати, особливо для навантажень, що торкаються великої кількості дрібних файлів.
Якщо ви бачите інтенсивні записи під /var/lib/docker/overlay2, не «оптимізуйте overlay2». Правильний крок зазвичай — припинити записувати туди. Помістіть змінні дані на volume або bind mount, де ви контролюєте файлову систему, опції монтування й ізоляцію I/O.
Жарт №2: Overlay файлові системи — як офісна політика: усе виглядає просто, поки не спробуєш змінити одну дрібницю й раптом залучені три відділи.
Підходи до обмеження, що реально працюють (і чого уникати)
У вас є три класи контролю: на рівні контейнера (cgroups), на рівні хоста (планувальник I/O та вибір файлової системи) і на рівні додатка (як навантаження пише). Якщо робити лише одне — робіть спочатку cgroups, щоб зупинити зону ураження.
Опція A: cgroup v2 io.max (жорсткі обмеження)
На сучасних дистрибутивах і Docker-конфігураціях з cgroup v2, io.max — ваш найпряміший важіль: встановлює максимальну пропускну здатність читання/запису (і іноді обмеження IOPS) для кожного блочного пристрою.
Коли використовувати: хост не відгукується і потрібне негайне стримування; пакетну задачу можна сповільнити без порушення коректності.
Компроміси: це груба дія. Якщо обмежити занадто сильно, ви можете спричинити таймаути в тротлінгованому контейнері. Це може бути краще, ніж втратити весь хост.
Опція B: cgroup v2 io.weight (відносна чесність)
io.weight краще за жорсткий ліміт: воно вказує ядру, «цей cgroup важить менше/більше». Якщо у вас кілька навантажень і ви хочете, щоб вони ділилися чесно, ваги кращі за строгі обмеження.
Коли використовувати: ви хочете захистити чутливі до затримки сервіси, дозволяючи пакетним роботам використовувати надлишкову ємність.
Компроміси: якщо пристрій насичений одним завданням, ваги можуть не врятувати; можливо, все одно буде потрібен ліміт.
Опція C: застарілі blkio прапори Docker (ера cgroup v1)
Docker-параметри --blkio-weight, --device-read-bps, --device-write-bps та подібні були розроблені під cgroup v1. Вони все ще можуть допомогти залежно від ядра, пристрою і драйвера, але менш передбачувані на шаруватих ФС і сучасних мульти-чергових підсистемах блоків.
Суб’єктивна порада: якщо у вас cgroup v2, віддавайте перевагу io.max/io.weight у файловій системі cgroup. Ставте Docker blkio-флаги як «працює в лабораторії», якщо не перевірили на вашому стеку.
Опція D: systemd-властивості для Docker scope (чистіша автоматизація)
Якщо контейнери з’являються як systemd scope unit, ви можете встановлювати властивості через systemctl set-property. Це уникне ручного запису в файли cgroup і краще витримує деякі операційні робочі процеси.
Опція E: Виправити корінь проблеми (єдина постійна перемога)
Тротлінг — для стримування. Перемоги на рівні кореня виглядають так:
- Перенесіть write-heavy шляхи на виділені томи на окремі пристрої.
- Обмежуйте і маршрутизовуйте логи (ротація, стиснення або відправка поза хостом).
- Зменшіть частоту sync: батчинг, групові commit-настройки (обережно) або явно змініть політику надійності.
- Виберіть файлову систему, кращу для робочого навантаження (або принаймні опції монтування).
- Припиніть писати мільйони дрібних файлів, якщо можна зберігати їх як великі сегменти.
Чого уникати
- Не «вирішуйте» iowait додаванням CPU. Це як купити швидшу машину, щоб стояти в ще гіршому трафіку.
- Не вимикайте журналювання або характеристики надійності легковажно. Ви безперечно можете покращити продуктивність — аж до моменту, коли дізнаєтеся, що таке втрата живлення.
- Не звинувачуйте Docker як концепт. Docker — лише посланець. Справжній ворог — неврегульоване спільне сховище.
Три корпоративні міні-історії з I/O шахт
1) Інцидент через хибне припущення: «Воно в контейнері, отже ізольовано»
Команда мала завантажений хост: API-сервіси, кілька фоновых джобів і «тимчасовий» імпортер, який щовечора тягнув партнерські дані. Імпортер розгорнули без явних лімітів. Ніхто не турбувався. Адже він у Docker, правда?
Першої ночі, коли він запустився на спільному продакшн хості, оповіщення про латентність враз враз з’явилися по всій системі. Не тільки імпортер. API уповільнилися, пайплайн метрик відстав, SSH-сесії зависали посеред команд. Графіки CPU виглядали «нормально», що саме робить цей режим відмови таким дезорієнтуючим: CPU ввічливо чекали відповіді від сховища.
Рятувальний інженер бігав по логах додатків і upstream-залежностях двадцять хвилин, бо симптоми виглядали як розподілений інцидент. Лише перевірка iostat внесла ясність: один NVMe-пристрій був практично на 100% використанні з піками латентності записів. Потім iotop вказав на один процес, який постійно крутить записи.
Хибне припущення було тонким: «контейнери ізолюють ресурси за замовчуванням». Вони — ні. CPU і пам’ять можна легко лімітувати, але сховище спільне, якщо ви не ввели контролі або не розділили пристрої. Того вечора фіксом стало аварійне обмеження cgroup. Довгостроковий фікс був ще більш нецікавим: імпортер отримав свій том на окремому пристрої і I/O-ліміт як ремінь безпеки.
2) Оптимізація, що обернулася проти: «Більше паралелізму — швидші імпорти»
Команда платформи даних вирішила прискорити трансформацію. Вони подвоїли кількість воркерів і переключилися на менші шматочки файлів для більшої паралельності. На папері мало б бути швидше: більше воркерів, більше пропускної здатності, менше простоїв.
У продакшні це стало латентною гранатою. Джоба породжувала тисячі дрібних файлів і часто викликала fsync, щоб «бути в безпеці». Журнал файлової системи почав домінувати. Пропускна здатність не подвоїлася; вона впала. Гірше, решта хоста страждала, бо джоба тримала чергу сховища забитою дрібними операціями, які отруюють хвостову латентність.
Повернення до нормального стану відбулося шляхом зменшення паралелізму, батчингу виходів у більші сегменти та запису на том, налаштований під навантаження, на окремій файловій системі. Потім додали I/O-ліміт, щоб майбутні «оптимізації» не могли захопити хост. Заключна нотатка постмортему була різкою: оптимізувати задачу без визначення прийнятної шкоди — не оптимізація, а азартна гра.
3) Нудна, але правильна практика, що врятувала день: QoS на сервіс і окремі накопичувачі
Інша організація мала змішаний парк: веб-сервіси, кеші, кілька stateful баз даних і періодична аналітика. Їх уже палили раніше, тому вони склали нудний набір правил.
Сервіси зі станом отримали виділені томи на окремих пристроях. Без винятків. Пакетні навантаження запускалися в контейнерах з передвизначеними I/O вагами і лімітами. Логування було обмежено за замовчуванням. Команда платформи навіть тримала простий рукбук: iostat, iotop і «зіставити PID до cgroup». Нічого надзвичайного.
Одного дня новий образ контейнера втік з увімкненим debug-логуванням. Він почав активно писати, але хост не згорів. Логуючий контейнер досяг свого ліміту, сповільнився, і решта парку продовжувала працювати. На-колі все ще довелося виправляти misconfig, але це був контрольований інцидент, а не відмова.
Практика була не гламурною: виділяти сховище свідомо, застосовувати QoS і примусово впроваджувати дефолти. Але саме так виглядає надійність — менше героїки, більше бар’єрів.
Типові помилки: симптом → корінь проблеми → виправлення
1) Симптом: load average величезний, використання CPU низьке
Корінь: завдання заблоковані в стані D на сховищі; load включає uninterruptible sleep.
Виправлення: підтвердьте через ps/vmstat; визначте top I/O PIDи; зіставте з контейнером; застосуйте io.max або ваги; потім вирішіть поведінку запису.
2) Симптом: диск 100% зайнятий, але пропускна здатність не вражає
Корінь: дрібні випадкові I/O, штурми метаданих, contention журналу або часті скидання, що викликають високі накладні витрати на операцію.
Виправлення: зменште churn файлів; батчте записи; перемістіть гарячі шляхи на налаштований том; розгляньте BFQ для чесності на деяких пристроях; поставте ліміт кривднику.
3) Симптом: один контейнер «виглядає нормально» по CPU/пам’яті, але хост непридатний
Корінь: відсутність ізоляції I/O; контейнер б’є по спільному пристрою (логи, тимчасові файли, контрольні точки БД).
Виправлення: застосуйте I/O QoS через cgroups; обмежте логи; помістіть write-heavy шляхи на окремі томи.
4) Симптом: docker logs повільний і /var/lib/docker швидко росте
Корінь: json-file логування без ротації; великий лог-файл спричиняє зайві записи і оновлення метаданих.
Виправлення: встановіть max-size і max-file; відправляйте логи кудись ще; за замовчуванням вимикайте debug-логування в продакшні.
5) Симптом: після «тротлінгу» додаток починає таймаутити
Корінь: жорсткі обмеження занадто низькі для вимог латентності/надійності навантаження, або навантаження очікує сплесків I/O.
Виправлення: підніміть ліміти до відновлення SLO; віддавайте перевагу вагам перед жорсткими обмеженнями для змішаних навантажень; виправте батчинг і механізми зворотного тиску в додатку.
6) Симптом: iowait високий, але iostat виглядає нормально
Корінь: I/O може бути на іншому пристрої (loopback, мережеве сховище), або вузьке місце у замках файлової системи/метаданих не повністю видно в простих пристроєвих метриках.
Виправлення: перевірте mountи через findmnt; проскануйте всі пристрої з iostat -x; використовуйте pidstat -d і lsof, щоб локалізувати шляхи; окремо перевірте мережеві томи.
7) Симптом: один контейнер багато читає, а записи голодують (або навпаки)
Корінь: поведінка планувальника і конкуренція в черзі; змішане read/write навантаження може створювати несправедливість і хвостові сплески.
Виправлення: застосуйте окремі ліміти для читання/запису в io.max; ізолюйте навантаження на різні пристрої; плануйте пакетні читання у непіковий час.
Чеклісти / поетапний план
Аварійне стримування (15 хвилин)
- Підтвердіть iowait і латентність пристрою:
mpstat,iostat -x. - Знайдіть top I/O PIDи:
iotop -oPa,pidstat -d 1. - Зіставте PID → контейнер через
/proc/<pid>/cgroupіdocker ps. - Застосуйте cgroup v2
io.maxкап для контейнера на проблемному пристрої. - Перевірте покращення: iowait вниз, await вниз, відгук хоста повернувся.
- Комунікуйте чітко: «Ми обмежили контейнер X для стабілізації хоста; робота Y може працювати повільніше.»
Стабілізація (цей же день)
- Виправте логування: обмежте json-file логи або перейдіть на драйвер/агента, який не карає кореневий диск.
- Перенесіть write-heavy дані з overlay writable layer на volume або bind mount.
- Перевірте заповненість файлової системи: вільне місце і використання inode. Повні диски дають дивні проблеми з продуктивністю.
- Перевірте поведінку свопу; якщо своп інтенсивний, додайте ліміти пам’яті кривднику або відрегулюйте навантаження.
- Задокументуйте точні cgroup-настройки, щоб можна було відтворити і скасувати.
Укріплення (цей спринт)
- Встановіть дефолтні обмеження логів для всіх контейнерів (політика, а не пропозиція).
- Визначте класи I/O: чутливі до латентності сервіси проти пакетних/ETL; застосуйте ваги/ліміти відповідно.
- Відокремте сховище для stateful сервісів; не зосереджуйте файли баз даних разом з runtime storage, якщо можна уникнути.
- Побудуйте розділ рукбуку: «зіставити PID до контейнера» з точними командами для вашого режиму cgroup.
- Додайте оповіщення по латентності диска (
await) і глибині черги, а не тільки по пропускній здатності і %util.
Питання та відповіді
1) Чому високий iowait робить SSH і прості команди завислими?
Бо ці команди теж потребують диска: читання бінарників, запис історії shell, оновлення логів, торкання файлів, підкачка пам’яті. Коли черга диска глибока або латентність стрибає, усе, що потребує I/O, блокується.
2) Чи завжди iowait означає, що диск «повільний»?
Ні. Це може означати синхронно-важку поведінку додатка, contention файлової системи, активність свопу або насичення черги пристрою. Підтверджуйте за допомогою метрик латентності/черги як await і aqu-sz.
3) Я обмежив пропускну здатність контейнера і хост покращився. Чи доводить це, що контейнер — корінь проблеми?
Це доводить, що він суттєво сприяв черзі пристрою. Корінь проблеми може залишатися архітектурним: спільні диски, дефолти логування або дизайн, що перетворює дрібні записи в шторм флешів.
4) Краще використовувати ліміти IOPS чи bandwidth?
Bandwidth-ліміти — хороший грубий інструмент для стримінгових навантажень. IOPS-ліміти можуть бути кращими для випадкового I/O і навантажень, що важать на метадані. Використовуйте те, що відповідає болю: якщо латентність стрибає на дрібних операціях, IOPS-капи допоможуть більше.
5) Чи працюють Docker blkio-флаги з overlay2?
Іноді, але не так надійно, щоб покладатися на це. З cgroup v2 віддавайте перевагу io.max/io.weight на scope контейнера. Перевіряйте у вашому середовищі.
6) Чому допомагає перемістити дані на том?
Ви уникаєте write amplification в overlay і отримуєте контроль: можете розмістити том на іншому пристрої, вибрати опції файлової системи і ізолювати I/O чистіше.
7) Чи можна вирішити це, змінивши I/O планувальник?
Іноді можна покращити чесність або хвостові латентності, але налаштування планувальника не врятує від необмеженого записувача на спільному диску. Спочатку тротлінг, потім тюнінг.
8) Чому логи завдають стільки шкоди?
Тому що це записи, на які ви не заклали бюджет, часто напівсинхронні, часто без кінця, і за замовчуванням вони живуть на тому самому диску, що й усе інше. Ротуйте їх і відсилайте кудись ще.
9) Як зрозуміти, чи вузьке місце — журнал файлової системи?
Ви побачите високу латентність при багатьох дрібних записах, багато завдань у ext4_sync_file або подібних wait-каналах та інтенсивну метаданну активність. Виправлення зазвичай про форму навантаження (батчинг) і розміщення даних, а не про містичні прапори ядра.
10) Чи «безпечно» тротлити бази даних?
Залежить. Для бази, що обслуговує продакшн-трафік, жорсткі капи можуть спричинити таймаути і каскадні відмови. Віддавайте перевагу вагам і належній ізоляції сховища. Якщо мусите капати — робіть це обережно і моніторьте.
Наступні кроки (що робити в понеділок)
Коли один контейнер тягне ваш хост у чистилище I/O wait, переможний крок — не гадання. Це атрибуція і контроль: виміряйте латентність пристрою, ідентифікуйте top I/O PIDи, зіставте їх з контейнерами і застосуйте cgroup I/O ліміти, які тримають хост живим.
Потім зробіть дорослу частину: припиніть писати в overlay-шари, встановіть обмеження логів за замовчуванням і відокремте сховище для навантажень, що не повинні ділити чергу. Тримайте тротли як страховку, а не як заміну архітектури. Ваш майбутній on-call буде вам нудно вдячний.