Docker: Повільні записи на overlay2 — коли переходити на томи і чому

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

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

У дев’яти випадках із десяти винна не «Docker повільний». Це типовий файловий драйвер контейнера (overlay2), який робить саме те, для чого його спроектували:
робить шари образів і контейнерів зручними. Не швидким для інтенсивних операцій запису та навантажених fsync робіт.

Overlay2 в одній ментальній моделі (і чому записи болять)

overlay2 — це драйвер зберігання Docker, який використовує Linux OverlayFS: уніонний файловий шар, що накладає записувальний «upperdir» поверх одного або кількох лише для читання «lowerdir» шарів (ваші шари образу).
Коли контейнер читає файл, що існує в образі, він читає з нижчих шарів. Коли записує — запис потрапляє в upper layer.

Біль починається на перетині copy-on-write і метаданих, що змінюються.
Якщо контейнер змінює файл, який є в lower layer, OverlayFS часто мусить спочатку «скопіювати вгору» файл у upper layer.
Це копіювання — реальний ввхід/вихід. Воно може бути великим. До того ж воно супроводжується операціями з метаданими, які на папері дешеві, але в реальному масштабі дорого коштують.

Для навантажень з великою кількістю записів особливо болючими є дві моделі:

  • Малі випадкові записи з fsync (бази даних, SQLite, черги повідомлень з прапорцями надійності). Union шар не основна проблема; це додаткова непряма робота й операції з метаданими, а також те, як поведеться ваш хостовий файловий диск під цим навантаженням.
  • Багато створень/видалень файлів (кеші збірок, розпаковка архівів, менеджери пакетів мов, тимчасові директорії). Overlay2 може перетворити «багато дрібних записів» на «багато дрібних записів плюс багато дрібних операцій з метаданими».

Томи існують тому, що ми зрештою визнаємо: ми запускаємо stateful навантаження. Docker volume — це, по суті, директорія, якою керує Docker і яка монтується в контейнер прямо з файлової системи хоста (цей шлях не проходить через union-шари).
Для змонтованого шляху ви обходите накладні витрати copy-on-write і зазвичай зменшуєте write amplification.

Рішення не філософське. Воно механічне: якщо дані змінюються і чутливі до продуктивності — перестаньте зберігати їх у шарі контейнера.
Тримайте файлову систему контейнера для бінарників і конфігурації; зберігайте стан десь нудно й прямо.

Цікаві факти та історичний контекст

Короткі конкретні факти, що пояснюють, як ми опинилися тут:

  1. Docker не починав з overlay2. Раніше Docker активно використовував AUFS; він був швидким і функціональним, але не дружнім до upstream Linux. Перехід до OverlayFS був частково про те, щоб бути «в ядрі» та легше підтримуваним.
  2. Сліди епохи device mapper відчутні. Драйвер зберігання devicemapper (особливо loop-lvm) викликав катастрофічні затримки запису та моменти «чому мій диск заповнений?». overlay2 став дефолтом не просто так.
  3. OverlayFS дозрів у кілька релізів ядра. Функції, такі як кілька нижніх шарів і виправлення продуктивності, з’являлися поступово. Версія вашого ядра важить більше, ніж люди хочуть визнати.
  4. На XFS важливий ftype. OverlayFS вимагає підтримки типу файлу в записах директорій (d_type). На XFS це контролюється параметром ftype=1. Помилка тут дасть попередження Docker — або, гірше, дивну поведінку/продуктивність.
  5. Copy-up — це не абстракція. Редагування файлу з образу запускає копію цього файлу в upperdir. За великі файли ви платите відразу.
  6. Whiteout — це спосіб видалення в union-файлових системах. Коли ви видаляєте файл, що існує в lower layer, OverlayFS записує «whiteout» в upperdir. Це додаткові метадані, які накопичуються.
  7. Журнальні файлові системи міняють затримку на безпеку. ext4 і XFS мають різні дефолтні поведінки; додайте fsync-насичені додатки і ви можете посилити проблему. Overlay2 цього не скасовує; може лише віддзеркалити й посилити.
  8. Логі контейнера не особливі. Якщо ви логируєте в файл всередині шару контейнера — ви пишете в overlay2. Якщо ви пишете в stdout, логер Docker записує куди інше — теж на диск, але іншим шляхом і з іншими режимами відмов.
  9. «На моєму ноуті швидко» часто — кеш сторінок, а не пропускна здатність. Overlay2 може виглядати чудово, поки ви не перейдете до синхронних записів, тиску пам’яті або вузла з шумними сусідами.

Як «повільні записи» виглядають у продакшні

Повільність overlay2 рідко — один-єдиний курок. Це набір симптомів, що римуються:

  • П99 латентності росте, тоді як CPU лишається спокійним. Потоки додатка блокуються на I/O.
  • Контрольні точки або компакти бази даних займають більше часу всередині контейнерів, ніж на хості.
  • «Диск не заповнений», але записи зависають: фактично ви могли вичерпати inode, застрягнути через конкуренцію журналу або бути обмежені чергою блочного пристрою.
  • iowait на вузлі стрибає вгору без відповідної історії «великої пропускної здатності». Класичний знак синхронно-насичених дрібних записів.
  • Перезапуски контейнера стають довшими з часом, якщо у writable layer накопичується багато файлів і whiteout’ів. Обходи метаданих старіють погано.

Складність у тому, що overlay2 не завжди є вузьким місцем. Вузьке місце може бути в блочному сховищі, опціях монтування файлової системи, вашому ядрі,
або у тому, що ви поклали WAL бази даних на union-файлову систему і потім змусили її fsync-ити, ніби від цього залежить робота (так воно і є).

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

Коли ви на виклику, не хочете 40-крокове розслідування. Потрібна коротка драбина, що виведе вас до «перенести дані на том» або «це проблеми диска» швидко.

По-перше: підтвердьте, хто пише і куди

  • Визначте топ-писачів на рівні хоста (процес, пристрій).
  • Зіставте PID контейнера з іменем контейнера.
  • Визначте, чи гарячий шлях знаходиться в /var/lib/docker/overlay2 або це змонтований том/bind mount.

По-друге: вирішіть, це затримка sync-записів чи пропускна здатність

  • Якщо await високий і %util високий: пристрій насичений або чергується.
  • Якщо await високий, але %util помірний: ви можете платити за латентність операції (fsync, блокування журналу, буря метаданих).
  • Якщо додаток постійно викликає fsync: ви в грі «латентності», а не «МБ/с».

По-третє: перевірте підсилювачі, специфічні для overlay2

  • Тригерні події copy-up (запис у файли, що прийшли з образу).
  • Велика кількість файлів у writable layer (тиск на inode, вартість обходу директорій).
  • Невідповідність базової файлової системи (XFS ftype, дивні опції монтування).

По-четверте: оберіть найменшу безпечну зміну

  • Якщо дані змінювані: перемістіть їх на том або bind mount. Вирішіть на користь томів для операційної гігієни.
  • Якщо це ефемерні дані для збірки/кешу: розгляньте tmpfs, якщо поміщається, або прийміть повільніші записи, але не зберігайте їх постійно.
  • Якщо це базовий диск: вирішіть історію диска (IOPS, латентність, планувальник, клас сховища). overlay2 тут лише посланець.

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

Це ті завдання, які ви справді запускаєте опівночі. Кожне має: команду, що типовий вивід означає, та яке рішення ви приймаєте далі.
Команди припускають Linux-хост з Docker Engine і overlay2.

Завдання 1: Підтвердити, що Docker використовує overlay2 (і на якій файловій системі він лежить)

cr0x@server:~$ docker info --format '{{.Driver}} {{.DockerRootDir}}'
overlay2 /var/lib/docker

Що це означає: Драйвер зберігання — overlay2; Docker data root — /var/lib/docker.

Рішення: Всі writable шари контейнерів живуть під цим коренем, якщо ви його не переміщували. Саме там шукайте «гарячі» місця.

Завдання 2: Перевірити тип файлової системи і опції монтування для Docker root

cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/docker
/dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro

Що це означає: Docker root на ext4 з типовими опціями.

Рішення: Якщо це мережеве сховище або повільний HDD — не звинувачуйте overlay2, звинувачуйте фізику. Якщо це XFS, перевірте ftype=1 (Завдання 3).

Завдання 3: Якщо використовується XFS, перевірити підтримку d_type (ftype=1)

cr0x@server:~$ xfs_info /dev/nvme0n1p2 | grep ftype
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1

Що це означає: OverlayFS може працювати коректно. ftype=0 — червоний прапорець.

Рішення: Якщо ftype=0, плануйте міграцію на коректно відформатовану файлову систему. Не «налаштовуйте довкола» структурної невідповідності.

Завдання 4: Знайти топ блокових пристроїв і перевірити, чи вони насичені

cr0x@server:~$ iostat -xz 1 5
Linux 6.2.0 (server) 	01/03/2026 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.10    0.00    3.40   22.80    0.00   61.70

Device            r/s     w/s   rMB/s   wMB/s  rrqm/s  wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz  rareq-s  wareq-s  svctm  %util
nvme0n1         10.0   980.0    0.5    12.0     0.0    20.0    0.0    2.0   1.20   18.50  20.10    52.0    12.5   0.90  89.0

Що це означає: Записи домінують; w_await високий і %util майже насичення. Пристрій чергується.

Рішення: Це не лише overlay2. Потрібно або більше IOPS/нижча латентність диска, або менше sync-записів, або ізолювати навантаження.

Завдання 5: Ідентифікувати, який процес робить I/O

cr0x@server:~$ sudo iotop -oPa
Total DISK READ:         0.00 B/s | Total DISK WRITE:      25.30 M/s
  PID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
23144 be/4  999        0.00 B/s   18.20 M/s  0.00 %  35.00 % postgres: wal writer process
19872 be/4  root       0.00 B/s    5.10 M/s  0.00 %  10.00 % dockerd --host=fd://

Що це означає: Процес бази даних (ймовірно всередині контейнера) штовхає WAL-записи; dockerd теж активний.

Рішення: Зіставте PID з контейнером і перевірте, де лежить його директорія даних. Бази даних на overlay2 — відома самосаботажна практика.

Завдання 6: Зіставити PID з контейнером

cr0x@server:~$ ps -o pid,cmd -p 23144
  PID CMD
23144 postgres: wal writer process

cr0x@server:~$ sudo cat /proc/23144/cgroup | grep -E 'docker|kubepods' | head -n 1
0::/docker/5c3b1f2d0b0a8b3d5a2d6c5d8c1f0e9a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3

Що це означає: Процес належить контейнеру Docker з тим ID-префіксом.

Рішення: Перевірте монтовані томи цього контейнера (Завдання 7). Якщо PGDATA не на томі/bind mount — виправляйте це.

Завдання 7: Перевірити монтовані точки контейнера і підтвердити, чи дані на overlay2

cr0x@server:~$ docker inspect 5c3b1f2d0b0a --format '{{range .Mounts}}{{println .Destination .Type .Source}}{{end}}'
/var/lib/postgresql/data volume /var/lib/docker/volumes/pgdata/_data

Що це означає: Директорія даних — Docker volume (добре). Якби ви не бачили жодного монтування для PGDATA — це було б всередині overlay2 (погано).

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

Завдання 8: Довести, чи записи потрапляють у шляхи overlay2

cr0x@server:~$ sudo lsof -p 23144 | grep overlay2 | head
postgres 23144 999  cwd    DIR  8,2     4096  131081 /var/lib/docker/overlay2/9d2f.../merged/var/lib/postgresql/data

Що це означає: Якщо ви бачите відкриті файли під /var/lib/docker/overlay2/.../merged, цей процес оперує на union-монті.

Рішення: Перемістіть цей шлях на том. Якщо це база даних — не дискутуйте, робіть.

Завдання 9: Перевірити вичерпання inode (підступний «диск заповнений»)

cr0x@server:~$ df -hi /var/lib/docker
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/nvme0n1p2    20M    19M     1M   95% /var/lib/docker

Що це означає: Ви близькі до вичерпання inode. Записи можуть падати або деградувати, коли FS починає «боротися».

Рішення: Почистіть образи/шари, перемістіть директорії з високою турбулентністю на томи, і розгляньте файлову систему з більшим числом inode або іншу розмітку для Docker root.

Завдання 10: Знайти великі writable шари і «churners»

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.ID}}'
NAMES                 ID
api-1                  2f1c9b8c0d3a
worker-1               8a7d6c5b4e3f
postgres-1             5c3b1f2d0b0a

cr0x@server:~$ docker container inspect api-1 --format '{{.GraphDriver.Data.UpperDir}}'
/var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper

cr0x@server:~$ sudo du -sh /var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper
18G	/var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper

Що це означає: Writable шар контейнера великий. Зазвичай це означає, що хтось пише дані, які мали б бути на томі, або агресивно кешує всередині файлової системи контейнера.

Рішення: Визначте, які директорії зростають, і перемістіть їх на томи/bind mounts (або tmpfs). Великий upperdir — це операційний «запах».

Завдання 11: Підтвердити, чи навантаження обмежене fsync

cr0x@server:~$ sudo strace -p 23144 -e trace=fdatasync,fsync -tt -T -f
18:20:11.102938 fdatasync(7)            = 0 <0.024981>
18:20:11.128201 fdatasync(7)            = 0 <0.031442>

Що це означає: Кожний виклик sync коштує ~25–30ms. Це катастрофа для пропускної здатності бази даних. Overlay2 може додавати накладні витрати, але реальний ворог — латентність синхронізації.

Рішення: Покладіть WAL/дані на найшвидше доступне сховище і уникайте union FS для цих шляхів. Якщо не можете змінити сховище — зменшіть частоту fsync лише якщо модель надійності це дозволяє.

Завдання 12: Бенчмарк шлях overlay2 проти шляху на томі (A/B тест, не відажі)

cr0x@server:~$ docker run --rm -it alpine sh -lc 'apk add --no-cache fio >/dev/null && fio --name=randwrite --directory=/tmp --size=512m --bs=4k --rw=randwrite --iodepth=1 --direct=1 --numjobs=1 --runtime=20 --time_based --fsync=1'
randwrite: (groupid=0, jobs=1): err= 0: pid=23: Fri Jan  3 18:20:45 2026
  write: IOPS=120, BW=480KiB/s (492kB/s)(9600KiB/20001msec)
    clat (usec): min=2000, max=65000, avg=8000.00, stdev=5000.00

cr0x@server:~$ docker volume create fiotest
fiotest

cr0x@server:~$ docker run --rm -it -v fiotest:/data alpine sh -lc 'apk add --no-cache fio >/dev/null && fio --name=randwrite --directory=/data --size=512m --bs=4k --rw=randwrite --iodepth=1 --direct=1 --numjobs=1 --runtime=20 --time_based --fsync=1'
randwrite: (groupid=0, jobs=1): err= 0: pid=23: Fri Jan  3 18:21:15 2026
  write: IOPS=320, BW=1280KiB/s (1311kB/s)(25600KiB/20002msec)
    clat (usec): min=1500, max=32000, avg=3500.00, stdev=2000.00

Що це означає: Шлях на томі дає суттєво кращі IOPS і нижчу латентність для синхронно-насичених 4k записів.

Рішення: Якщо ваше реальне навантаження схоже на цей патерн (бази даних, черги), перемістіть його з overlay2.

Завдання 13: Перевірити драйвер логування Docker і вплив шляху логів

cr0x@server:~$ docker info --format '{{.LoggingDriver}}'
json-file

cr0x@server:~$ docker inspect api-1 --format '{{.LogPath}}'
/var/lib/docker/containers/2f1c9b8c0d3a.../2f1c9b8c0d3a...-json.log

cr0x@server:~$ sudo ls -lh /var/lib/docker/containers/2f1c9b8c0d3a.../*json.log
-rw-r----- 1 root root 9.2G Jan  3 18:21 /var/lib/docker/containers/2f1c9b8c0d3a.../2f1c9b8c0d3a...-json.log

Що це означає: Логи ростуть на Docker root filesystem. Вони можуть конкурувати з записами overlay2 і швидко заповнити диск.

Рішення: Налаштуйте ротацію логів, обмеження розмірів або змініть драйвер логування відповідно до середовища. Не дозволяйте «printf-діагностиці» стати DoS для сховища.

Завдання 14: Шукати помилки з propagation монтів (том не завантажився)

cr0x@server:~$ docker exec -it api-1 sh -lc 'mount | grep -E "/data|overlay" | head -n 3'
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
/dev/nvme0n1p2 on /data type ext4 (rw,relatime)

Що це означає: /data — реальний монтуємий том (добре). Якщо ви бачили лише overlay-монт і не бачили окремого монту для потрібного шляху, ваш «том» не змонтований там, де думаєте.

Рішення: Виправте специфікацію контейнера (неправильний шлях, опечатка, відсутній -v), потім повторно протестуйте продуктивність. У припущеннях живуть відмови.

Коли переходити на томи (а коли ні)

Перейдіть на томи, коли дані змінюються і виконуються одна з цих умов

  • Це база даних (Postgres, MySQL, MongoDB, Redis з AOF тощо). Бази даних — це не «просто файли». Це ретельно скоординовані машини fsync.
  • Це журнал WAL або журнал транзакцій (сегменти типу Kafka, журнали надійності черг, translog’и пошукових індексів). Такі навантаження карають за латентність.
  • Це стан з високою турбулентністю (завантаження, директорії кешу, що важливі, дані, створені користувачами).
  • Це вивід збірки, на який ви покладаєтесь між перезапусками (CI кеші, кеші пакетів), і ви хочете передбачуваності.
  • Вам потрібні механізми бекапу/відновлення, що не «записати контейнер і молитися». Том дає чітку межу для сніпшотів і міграції.

Залишайте overlay2 для того, у чому він сильний

  • Незмінні частини додатка: бінарники, бібліотеки, шаблони конфігурації. Саме для цього призначені шари образу.
  • Короткоживучі тимчасові файли, які не треба зберігати і які не роблять мільйон fsync викликів. Якщо потрібна швидкість — використовуйте tmpfs. Якщо потрібна збереженість — використовуй том.

Томи проти bind mounts: обирайте свідомо

Bind mount — це «змонтируй цей шлях хоста в контейнер». Docker volume — це «Docker керує хостовим шляхом і монтує його для вас».
Продуктивність може бути подібною, бо обидва обходять union-шар для цього шляху. Різниця — в операційності:

  • Томи легше перелічувати, мігрувати і розуміти за допомогою Docker-інструментів. Вони також уникнуть випадкового зв’язування з структурою директорій хоста.
  • Bind mounts чудові, коли потрібен строгий контроль (існуючі директорії, конкретна файлова система або інтеграція з хост-інструментами). Вони також полегшують випадкові помилки, коли щось змонтовано не те, що треба.

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

Жарт №1: Запуск бази даних на overlay2 — це як ходити в шльопанцях на будівництві — можливо, але звіт про ушкодження напише себе.

Як переключитися безпечно: шаблони, що не викличуть дзвінків вночі

Шаблон 1: Явні директорії даних, ніколи «те, що образ використовує за замовчуванням»

Образи часто визначають дефолтні шляхи даних всередині файлової системи контейнера. Якщо ви їх не перевизначите томом, ви неявно використовуєте overlay2.
Для баз даних будьте явними і голосними.

cr0x@server:~$ docker volume create pgdata
pgdata

cr0x@server:~$ docker run -d --name pg \
  -e POSTGRES_PASSWORD=example \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16
c1d2e3f4a5b6c7d8e9f0

Шаблон 2: Переміщуйте лише «гарячий» шлях, а не всю файлову систему

Не потрібно монтувати /. Не потрібно переплатформлювати усе. Визначте директорії з інтенсивними записами:
дані бази, WAL, завантаження, кеші, логи (іноді).
Монтуйте їх. Решту залиште як є.

Шаблон 3: Розділіть WAL/логи від даних, коли вимоги до латентності різняться

Не кожне розгортання потребує цього, але коли треба — це рятівна практика:
покладіть WAL (або еквівалент) на найшвидший клас сховища, тримайте «важке» bulk-дані на ємнісному сховище.
Це простіше робити через оркестратори, але й у Docker так можна, якщо дисципліновано підходити.

Шаблон 4: Уникайте запису логів у шари контейнера

Логування в stdout не автоматично безкоштовне, але воно уникає union-файлової системи для логів додатка.
Якщо ви пишете логи у файли, змонтуйте том або bind mount для директорії логів і робіть ротацію.

Шаблон 5: Вирішіть, яку надійність вам реально потрібно

Дискусія про драйвер зберігання часто ховає бізнес-рішення: чи прийнятна втрата даних?
Якщо ви відключите fsync (або використаєте асинхронні режими), отримуєте кращі числа — аж поки не втратите дані.
«Можемо втратити останні 5 секунд даних» — валідна політика. «Ми не думали про це» — невалідна.

Три корпоративні міні-історії з передової

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

Команда випустила новий API-сервіс, що інгестував події і записував їх у локальну чергу перед відправкою в основний пайплайн.
У стейджингу все здавалося нормальним. У продакшні під навантаженням почалися таймаути. На графіках on-call видно зростання латентності і різкий стрибок iowait.
CPU був низький, тож усі підозрювали «мережу», бо ми так робимо, коли CPU не винен.

Вони припустили, що оскільки черга «лише файл», то вона поводитиметься як хостовий файл. Але файл жив всередині файлової системи контейнера.
Це означало overlay2. Це означало семантику union mount. І під навантаженням черга робила те, що черги роблять: багато дрібних додавань, багато синхронізацій.

Симптом був дивним: пропускна здатність була нормальна кілька хвилин після деплою, потім деградувала.
Writable layer зростав, метадані нагрівалися, і черга пристрою накопичувалася.
Хтось намагався додати «більше CPU», бо дашборди виглядали порожніми. Це нічого не дало. Звісно, не дало.

Виправлення було нудним: змонтувати том у директорію черги і повторно розгорнути.
Латентність стабілізувалась. Записна ампліфікація впала. Інцидент завершився тихо — і таке завершення має сенс.

Справжній урок: якщо компонент є межею надійності (черга, журнал, база даних), розглядайте його шлях зберігання як інфраструктуру, а не як деталь реалізації.
Шар контейнера — це пакування, а не інфраструктура.

Міні-історія 2: Оптимізація, що відзвітувалася боком

Платформна команда побачила повільні збірки у CI. Контейнери повторно розпаковували залежності.
Вони вирішили «прискорити» процес, кешуючи директорії пакетів всередині файлової системи контейнера, думаючи, що так уникнуть зовнішнього I/O.
Їм також подобалась простота: менше томів, менше монтів, менше «стану».

Це спрацювало — недовго. Потім використання диска під /var/lib/docker/overlay2 зросло.
Хост не був без байтів, але в нього почалися кровоточити inode. Роботи з прибирання стали довшими.
Нові збірки стали повільнішими, бо кожен контейнер стартував з товстим writable layer і великою кількістю змін директорій.

Команда «оптимізувала» прибирання, агресивно підчищаючи образи між задачами.
Це зменшило використання диска, але додало трафіку до реєстру і пропуски кеша, і підвищило тиск запису під час розпаковування, бо нічого не було «теплим».
Загальний ефект — більша варіативність і частіші таймаути.

Виправили, перемістивши кеші в виділений том (або директорію кеша на раннері через bind mount) і керуючи ним навмисно:
обмеження, час життя, чітка власність. Вивід збірки знову став передбачуваним.

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

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

Інша організація запускала кілька stateful сервісів в контейнерах — не через моду, а бо це зменшувало дрейф і робило оновлення менш страшними.
У них було одне правило, яке звучало нудно на дизайнових рев’ю: весь постійний стан — тільки на іменованих томах, і кожен сервіс має документувати свої монтування томів.

Коли вузол почав показувати підвищену латентність запису, вони могли одразу відокремити «записи overlay2» від «записів на томи».
Дашборди відстежували латентність диска по пристроях, а спекти розгортання робили очевидним, що де живе.
Тріаж став швидким, бо розклад був консистентним між сервісами.

Під час інциденту вони live-migrated навантаження на вузол з кращим сховищем і перемонтували томи.
Ніякі дані з шару контейнера не були критичними, тож не довелося копіювати випадкові директорії з /var/lib/docker/overlay2.
Час відновлення рахувався хвилинами, а не археологією.

Власники сервісів ненавиділи це правило під час прототипування, бо воно змушувало думати про шляхи рано.
On-call команда полюбила його назавжди.

Жарт №2: Нічого не вбиває оповідь про «stateless microservice» так, як 20GB writable layer під назвою «upper».

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

Цей розділ читаєте після того, як вже пробували перезапуск. Не хвилюйтеся, ми всі там були.

1) Записи повільні лише всередині контейнера

Симптом: Та сама операція на хості швидка; всередині контейнера — повільна.

Корінь: Шлях даних у overlay2, ви платите copy-on-write і накладні на метадані; або контейнер використовує інший патерн fsync через конфіг.

Виправлення: Перемістіть writable шлях на том/bind mount. Перевірте через mount у контейнері і проведіть A/B бенчмарк.

2) Латентність бази зростає під час checkpoint/flush

Симптом: Періодичні паузи; процес WAL writer або checkpoint показує високий IO wait.

Корінь: fsync латентність і конкуренція журналу на підлягаючому сховищі; overlay2 може посилити це, якщо DB-файли в writable layer.

Виправлення: Переконайтеся, що DB data/WAL на томах на швидкому сховищі. Якщо все ще повільно — вирішуйте проблеми IOPS/латентності (клас сховища, пристрій, тюнінг файлової системи).

3) Помилки «диск повний», але df -h показує місце

Симптом: Записи падають; docker pull не вдаються; контейнери крашаться; байти не вичерпані.

Корінь: Вичерпання inode на Docker root, часто через високу зміну файлів у upperdir або кеші збірок.

Виправлення: Перевірте df -hi. Проніть, зменшіть churn, перемістіть інтенсивні шляхи на томи і розгляньте файлову систему/розмітку з достатньою кількістю inode.

4) Продуктивність погіршується з часом без збільшення навантаження

Симптом: Такий самий RPS, гірша латентність через дні/тижні.

Корінь: Writable шари накопичують багато файлів/whiteout’ів; операції з метаданими уповільнюються; бекапи/антивіруси торкаються Docker root; логи ростуть.

Виправлення: Тримайте стан поза overlay2, робіть ротацію логів, підчищайте невикористовувані образи/контейнери, і тримайте Docker root на файловій системі, що не пересічена з галасливими процесами хоста.

5) «Ми перейшли на томи, і все ще повільно»

Симптом: Дані на томі, але латентність запису висока.

Корінь: Підлягаюче сховище — вузьке місце (мережеві диски з обмеженням IOPS, кредитні системи, тротлінг), або навантаження обмежене латентністю синку.

Виправлення: Виміряйте латентність пристрою з iostat, подивіться таймінги fsync і оновіть/ізолюйте сховище. Том не зробить повільний диск швидким.

6) Дивні помилки на Docker root, що на XFS

Симптом: Дивна поведінка файлів або попередження; інколи погана продуктивність.

Корінь: XFS відформатований з ftype=0 (без d_type) або неподтримувана комбінація ядра/файлової системи.

Виправлення: Міграція Docker root на XFS з ftype=1 (або використання ext4). Це робота з перебиранням/міграцією, не просте перемикання.

7) Швидкий ріст Docker root і повільний вузол

Симптом: /var/lib/docker швидко росте; вузол стає млявим; операції з контейнерами повільні.

Корінь: Незапитані логи контейнерів, великі writable шари або runaway артефакти збірки всередині контейнерів.

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

Контрольні списки / покроковий план

Контрольний список A: Вирішіть, чи треба переходити на томи (швидка перевірка)

  • Чи дані змінюються? Якщо так — тягніть у бік тому.
  • Чи навантаження часто робить fsync/fdatasync? Якщо так — уникайте overlay2 для цього шляху.
  • Чи шлях очікується рости понад кількасот МБ? Якщо так — не тримайте його в upperdir.
  • Чи потрібні бекапи або міграція? Якщо так — томи роблять це осмисленим.
  • Чи шлях — кеш, який можна скидати? Якщо так — розгляньте tmpfs або залишайте в overlay2, але ставте обмеження/чистку.

Контрольний список B: План міграції stateful сервісу (без драми)

  1. Визначте справжні директорії даних. Читайте конфіг додатка. Не гадати. Для баз даних знайдіть data dir і WAL/redo журнали.
  2. Створіть томи з осмисленими іменами. Уникайте «data1» для всього; потім пошкодуєте.
  3. Зупиніть сервіс акуратно. Дайте йому виплюнути дані. «Kill -9» не інструмент міграції.
  4. Скопіюйте дані зі старого шляху на том. Збережіть власника і права доступу.
  5. Змонтуйте том точно в той самий шлях — додаток очікує консистентності.
  6. Запустіть і перевірте на рівні додатка. Не обмежуйтесь «контейнер працює».
  7. Проведіть бенчмарк шляху запису (fio або метрики додатка) і порівняйте з базою.
  8. Встановіть політику: ротація логів, ліміти розміру, розклади підчищення.

Контрольний список C: Якщо томи не допомогли (реальність сховища)

  • Виміряйте латентність і насичення пристрою з iostat.
  • Перевірте тротлінг для burstable сховищ (хмарні томи можуть це робити непомітно).
  • Підтвердьте здоров’я файлової системи і опції монтування.
  • Шукайте «шумних сусідів»: інші контейнери, що пишуть логи або роблять компакти.
  • Розгляньте розділення Docker root, томів і логів на різні пристрої.

Питання та відповіді (FAQ)

1) Чи overlay2 «повільний» за дизайном?

overlay2 оптимізований для шарування образів і звичайного використання файлової системи контейнера. Він не призначений як високопродуктивна файлова система для баз даних.
Для навантажень з великою кількістю синхронних записів він часто повільніший за пряме монтування.

2) Чи Docker томи завжди швидші за overlay2?

Не завжди. Якщо підлягаюче сховище повільне, том не виправить фізику.
Але томи прибирають накладну union-файлової системи для змонтованого шляху, що часто допомагає при роботі з метаданими і fsync-насичених навантаженнях.

3) А bind mounts — «швидші» за томи?

Продуктивність зазвичай схожа. Різниця — у керованості та безпечності.
Томи легше інвентаризувати і мігрувати через Docker-інструменти; bind mounts — це явні шляхи хоста і корисні, коли потрібний суворий контроль.

4) Чому малі записи болять більше, ніж великі послідовні записи?

Малі синхронні записи домінуються затримкою на операцію: оновлення метаданих, коміти журналу, flush’і і інколи бар’єри.
overlay2 додає додаткові шари файлової роботи. Великі послідовні записи можна буферизувати й стрімити ефективніше.

5) Чи можна «налаштувати» overlay2, щоб він був таким же швидким, як томи?

Зменшити біль можна (оновлення ядра, вибір файлової системи, уникнення патернів copy-up), але фундаментальні семантики union-файлової системи не зникнуть.
Якщо вам важлива продуктивність запису змінюваних даних — монтуйте їх з хоста.

6) Чи ставити /var/lib/docker на окремий диск?

Часто так — у продакшні. Відокремлення Docker root від диска ОС зменшує конкуренцію і полегшує планування ємності.
Якщо логи теж на тому ж диску — ви фактично запрошуєте «гучну службу» жити з легковісним сплячим.

7) Чи tmpfs — хороший альтернатив для продуктивності?

Для справді ефемерних даних — так. tmpfs швидкий і уникає латентності диска.
Але він споживає RAM (і може свопитись, якщо неакуратно). Не кладіть важливі дані на tmpfs, якщо вам не подобається пояснювати втрату даних.

8) Чи Kubernetes змінює пораду?

Принцип лишається: не зберігайте змінюваний, чутливий до продуктивності стан у шарі контейнера.
В Kubernetes ви використовуватимете PersistentVolumes, hostPath (обережно) або епhemeral volumes, як emptyDir (на диску або в пам’яті), залежно від потреб у надійності.

9) Якщо моя база на томі, чи можу я ігнорувати overlay2 повністю?

Не повністю. Старт/зупинка контейнера, pull образів і будь-які записи поза змонтованими директоріями все ще потрапляють в overlay2 і Docker root.
Тримайте Docker root здоровим: місце, inode, ротацію логів і гігієну образів.

10) Яке найпростіше правило, що запобігає більшості інцидентів з overlay2?

Якщо це зберігається через перезапуски і ви будете засмучені, якщо воно зникне — це йде на том. Якщо це база даних — на том у будь-якому разі.

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

overlay2 — чудовий дефолт для пакування і запуску софту. Це не найкраще місце для mutable, high-churn і durability-чутливих даних.
Коли записи повільні, правильне питання: «Який шлях гарячий, і чи він живе в шарі контейнера?»

Наступні кроки, які можна зробити сьогодні:

  1. Пройдіть швидкий план діагностики: ідентифікуйте писача, зіставте з контейнером, підтвердьте шлях даних.
  2. Бенчмарк overlay2 проти шляху на томі за допомогою швидкого fio A/B тесту, що відображає ваше навантаження (sync vs async має значення).
  3. Перемістіть бази даних, черги і важливі логи на томи. Решту тримайте в образі.
  4. Встановіть охоронні механізми: обмеження логів, моніторинг inode і проста політика: «постійний стан ніколи не живе в upperdir».

Одна парафразована ідея від Werner Vogels (CTO Amazon): надійність будують, очікуючи відмови і проєктуючи системи так, щоб вони все одно працювали.
Розглядайте overlay2 як шар пакування, а не як стратегію зберігання, і ваші системи стануть нудними у найкращому сенсі.

← Попередня
Debian/Ubuntu «Працює в LAN, не працює в WAN»: перевірки маршрутизації та NAT, що виявляють причину (випадок №25)
Наступна →
Змішаний вміст WordPress: чому HTTPS досі показує попередження і як виправити правильно

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