Ubuntu 24.04: DKMS зламався після оновлення ядра — відновіть драйвери без простоїв

Ви патчуєте флот. Ядро оновлюється. Через кілька хвилин ваша система моніторингу починає сигналити: зникли GPU, ZFS-пулі скаржаться, зникли NIC offload-и, можливо, фібрика InfiniBand флапає. Сервіси поки що працюють… але ненадовго. І тоді ви бачите істину: DKMS не зібрав модулі для нового ядра, тож наступний перезавантаження — пастка.

Це реальність експлуатації Ubuntu 24.04 у продакшені. Оновлення ядра — рутинна справа; відмови DKMS — плата за використання out-of-tree драйверів. Мета тут не «полагодити після того, як поламалося». Мета: виправити, поки система залишається онлайн, і зробити наступне оновлення ядра нудною рутиною.

Як DKMS насправді ламається після оновлення ядра

DKMS (Dynamic Kernel Module Support) існує тому, що вендори продовжують постачати модулі ядра, які не входять у mainline. NVIDIA, ZFS-on-Linux, деякі NIC і RAID драйвери, VirtualBox, деякі агенти безпеки — будь-що, що компілюється проти заголовків ядра, може опинитися тут.

Коли Ubuntu встановлює нове ядро, скрипти пакетів намагаються перебудувати DKMS-модулі для цього ядра. Якщо ця перебудова провалюється, ви можете не помітити цього відразу, бо поточне запущене ядро все ще має робочі модулі завантажені. Поломка проявляється, коли:

  • Ви перезавантажуєтесь і нове ядро стартує без потрібного модуля.
  • initramfs був згенерований без модуля, що викликає ранні помилки завантаження (сховище, root-on-ZFS, шифрування тощо).
  • Secure Boot блокує неподписаний модуль, і ви отримуєте специфічну помилку «зібрано, але не завантажується».
  • Заголовки для нового ядра не були встановлені, тож DKMS просто нічого не зможе скомпілювати.

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

Сухий факт: DKMS не «динамічний» у тому сенсі, як уявляє менеджмент. Він «динамічний» так само, як динамічна паперова форма: її треба заповнювати щоразу, коли змінюється ядро.

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

Коли ви намагаєтеся уникнути простою, важлива швидкість. Найшвидший шлях: визначте цільове ядро, підтвердіть, чи існує модуль для нього, підтвердіть, чи можна його завантажити, а потім перевірте артефакти завантаження (initramfs). Усе інше — прикраса.

Перше: яке ядро зараз запущене й які ядра встановлені?

  • Якщо ви все ще працюєте на старому ядрі, можете спокійно перебудувати перед перезавантаженням.
  • Якщо ви вже на новому ядрі й модулі відсутні, потрібно відновлювати функціональність на живому ядрі (іноді можливо, іноді ні).

Друге: чи показує DKMS «built» для цільового ядра?

  • Якщо не «built»: ви в режимі «перебудувати і виправити залежності збірки».
  • Якщо «built»: перевірте, чи встановлено в /lib/modules/<kernel> і чи проходить modprobe.

Третє: чи блокує модуль Secure Boot?

  • Secure Boot увімкнений + неподписаний модуль = він може добре збиратись, але впаде при завантаженні з помилкою підпису.
  • Це головна причина циклу «перебудували трохи — нічого не змінилось».

Четверте: чи містить initramfs те, що вам потрібно?

  • Якщо модуль потрібен для раннього завантаження (storage/network root, ZFS root, crypto), то «зібрано» недостатньо.
  • Перегенеруйте initramfs для цільового ядра і перевірте, чи містить він модуль.

П’яте: заблокуйте ризикові зміни, поки виправляєте

  • Утримуйте пакети ядра, якщо unattended-upgrades продовжує тягнути нові ядра під час процесу відновлення.
  • Зафіксуйте відоме робоче ядро як опцію відкату.

Цікаві факти та контекст (чому це повторюється)

  • DKMS походить із екосистеми Dell середини 2000-х, щоб тримати вендорні драйвери здатними збиратись під час оновлень ядра, особливо на корпоративних флотах.
  • Ubuntu довго інтегрує DKMS, але це все одно залежить від скриптів пакування та наявності заголовків — немає заголовків, немає модуля.
  • Застосування Secure Boot перетворило “помилки збірки” на “помилки завантаження”. Модуль може компілюватись ідеально, але все одно бути відхиленим ядром.
  • ZFS on Linux тривалий час жив поза деревом ядра через ліцензійні нюанси; саме тому багато інсталяцій Ubuntu досі покладаються на DKMS для ZFS-модулів.
  • Стабільність ABI ядра не гарантується для out-of-tree модулів. Невеликі оновлення ядра можуть зламати збірки, якщо модуль використовує внутрішні API.
  • Cadence HWE і SRU Ubuntu може вас здивувати: оновлення ядра може прийти через unattended-upgrades, навіть якщо ви «нічого не міняли».
  • initramfs часто — справжня зона відмов. Система завантажує ядро; потім ранній userspace не знаходить потрібний модуль для сховища.
  • Збори DKMS можуть впливатися змінами інструментарію (gcc, make, binutils). «Оновлено ядро» іноді означає «також змінився компілятор».
  • Деякі вендори постачають попередньо зібрані модулі для певних версій ядра, але версії ядра Ubuntu змінюються; DKMS стає запасним планом — доки не перестане працювати.

Одна цитата, що пережила більше постмортемів, ніж заслуговує будь-хто один: «Надія — не стратегія.» — Gene Kranz. Вона застосовна і до перебудов DKMS.

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

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

Завдання 1: Підтвердити запущене ядро

cr0x@server:~$ uname -r
6.8.0-51-generic

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

Рішення: Якщо запущене ядро — відоме робоче, перебудуйте DKMS для нового ядра зараз, а потім заплануйте контрольований перезавантаження пізніше.

Завдання 2: Переглянути встановлені ядра і що буде за замовчуванням при наступному перезавантаженні

cr0x@server:~$ dpkg -l 'linux-image-*generic' | awk '/^ii/{print $2,$3}'
linux-image-6.8.0-51-generic 6.8.0-51.52
linux-image-6.8.0-52-generic 6.8.0-52.53

Що це означає: У вас щонайменше два ядра встановлені; зазвичай найвища версія обирається при завантаженні.

Рішення: Визначте «цільове» ядро, для якого потрібно мати DKMS-модулі (тут: 6.8.0-52-generic).

Завдання 3: Перевірити статус DKMS по ядрах

cr0x@server:~$ dkms status
zfs/2.2.2, 6.8.0-51-generic, x86_64: installed
zfs/2.2.2, 6.8.0-52-generic, x86_64: built
nvidia/550.90.07, 6.8.0-51-generic, x86_64: installed
nvidia/550.90.07, 6.8.0-52-generic, x86_64: added

Що це означає: «installed» означає, що модуль зібраний і скопійований у /lib/modules/<kernel>. «built» — скомпільований, але може не бути встановленим. «added» — DKMS знає про модуль, але ще не будував його для цього ядра.

Рішення: Для цільового ядра все, що не «installed», — ризик. Збирайте й інсталюйте зараз.

Завдання 4: Перевірити, чи існують заголовки ядра для цільового ядра

cr0x@server:~$ dpkg -l | awk '/linux-headers-6.8.0-52-generic/{print $1,$2,$3}'
ii linux-headers-6.8.0-52-generic 6.8.0-52.53

Що це означає: DKMS потребує заголовків. Якщо їх бракує, DKMS провалиться з помилками на кшталт «Kernel headers for target not found».

Рішення: Якщо заголовки відсутні — встановіть їх перед перебудовою DKMS-модулів.

Завдання 5: Встановити відсутні заголовки (якщо потрібно)

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y linux-headers-6.8.0-52-generic
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  linux-headers-6.8.0-52-generic
Setting up linux-headers-6.8.0-52-generic (6.8.0-52.53) ...

Що це означає: Заголовки тепер присутні; у DKMS з’явився реальний шанс.

Рішення: Перебудуйте DKMS-модулі для цільового ядра.

Завдання 6: Запустити DKMS autoinstall для цільового ядра

cr0x@server:~$ sudo dkms autoinstall -k 6.8.0-52-generic
Sign command: /lib/modules/6.8.0-52-generic/build/scripts/sign-file
Signing key: /var/lib/shim-signed/mok/MOK.priv
Public certificate (MOK): /var/lib/shim-signed/mok/MOK.der
Building module:
Cleaning build area... done.
Building module(s).... done.
Installing /lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko
Installing /lib/modules/6.8.0-52-generic/updates/dkms/nvidia.ko
depmod... done.

Що це означає: DKMS зібрав і встановив модулі для цього конкретного ядра, а depmod оновив карти залежностей модулів.

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

Завдання 7: Якщо збірка впала — читайте лог DKMS уважно

cr0x@server:~$ sudo tail -n 40 /var/lib/dkms/nvidia/550.90.07/build/make.log
CONFTEST: drm_prime_pages_to_sg_has_drm_device_arg
CONFTEST: drm_gem_object_put_unlocked
error: implicit declaration of function ‘drm_gem_object_put_unlocked’
make[2]: *** [scripts/Makefile.build:243: /var/lib/dkms/nvidia/550.90.07/build/nvidia-drm/nvidia-drm-gem.o] Error 1
make[1]: *** [Makefile:1926: /var/lib/dkms/nvidia/550.90.07/build] Error 2
make: *** [Makefile:234: __sub-make] Error 2

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

Рішення: Перестаньте пробувати випадкові перебудови. Потрібна версія драйвера/модуля, сумісна з цим ядром (наприклад, оновити пакет NVIDIA), або завантажитись у старе ядро поки не буде сумісного драйвера.

Завдання 8: Перевірити наявність модуля для цільового ядра без перезавантаження

cr0x@server:~$ ls -l /lib/modules/6.8.0-52-generic/updates/dkms/ | egrep 'zfs|nvidia' | head
-rw-r--r-- 1 root root  8532480 Dec 29 10:12 nvidia.ko
-rw-r--r-- 1 root root 17362944 Dec 29 10:12 zfs.ko

Що це означає: Файли існують у місці, куди DKMS їх поміщає для цього ядра.

Рішення: Далі перевірте можливість завантаження та стан підпису (особливо при Secure Boot).

Завдання 9: Перевірити стан Secure Boot (детектор «зібрано, але заблоковано»)

cr0x@server:~$ mokutil --sb-state
SecureBoot enabled

Що це означає: Ядро застосовуватиме перевірку підпису модулів. Непідписані DKMS-модулі не будуть завантажені.

Рішення: Якщо Secure Boot увімкнено — забезпечте підпис модулів DKMS ключем, що занесено в довірені, або заплануйте процес MOK-реєстрації.

Завдання 10: Спробувати завантажити модуль на запущеному ядрі (тільки якщо безпечно)

cr0x@server:~$ sudo modprobe -v zfs
insmod /lib/modules/6.8.0-51-generic/updates/dkms/spl.ko
insmod /lib/modules/6.8.0-51-generic/updates/dkms/zfs.ko

Що це означає: На запущеному ядрі модуль завантажується. Це перевірка, що установка DKMS не зіпсована глобально.

Рішення: Якщо modprobe впадає з «Required key not available», у вас проблема з підписом Secure Boot. Якщо «Unknown symbol» — невідповідність ядро/модуль.

Завдання 11: Переглянути логи ядра на предмет помилок підпису або символів

cr0x@server:~$ sudo dmesg -T | tail -n 20
[Mon Dec 29 10:19:02 2025] Lockdown: modprobe: unsigned module loading is restricted; see man kernel_lockdown.7
[Mon Dec 29 10:19:02 2025] nvidia: module verification failed: signature and/or required key missing - tainting kernel

Що це означає: Secure Boot або політика lockdown блокує або позначає ядро. Деякі середовища толерують taint; деякі вважають це невідповідністю.

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

Завдання 12: Перевірити, що initramfs перегенеровано для цільового ядра

cr0x@server:~$ ls -lh /boot/initrd.img-6.8.0-52-generic
-rw-r--r-- 1 root root 98M Dec 29 10:14 /boot/initrd.img-6.8.0-52-generic

Що це означає: Initramfs існує і було нещодавно оновлено, але це не гарантує, що він містить ваш модуль.

Рішення: Якщо модуль потрібен при завантаженні (ZFS root, HBA для сховища, особливий NIC) — обов’язково перевірте його присутність всередині initramfs.

Завдання 13: Підтвердити, що модуль є всередині initramfs (крок «довіряй, але перевіряй»)

cr0x@server:~$ lsinitramfs /boot/initrd.img-6.8.0-52-generic | egrep '/zfs\.ko|/nvidia\.ko' | head
usr/lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko

Що це означає: ZFS включено в ранній userspace для цього ядра. Для GPU зазвичай не потрібно включати в initramfs; для сховища/мережі при завантаженні це може бути критично.

Рішення: Якщо відсутній — перегенеруйте initramfs після виправлення інсталяції DKMS.

Завдання 14: Перегенерувати initramfs для конкретного ядра (цільовий, не «розстрільний»)

cr0x@server:~$ sudo update-initramfs -u -k 6.8.0-52-generic
update-initramfs: Generating /boot/initrd.img-6.8.0-52-generic

Що це означає: Ви примусово згенерували initramfs для ядра, яке вас турбує.

Рішення: Знову прогляньте lsinitramfs; лише потім розглядайте перезавантаження.

Завдання 15: Переконатися, що карта залежностей модулів правильна для цільового ядра

cr0x@server:~$ sudo depmod -a 6.8.0-52-generic

Що це означає: modules.dep та інші файли оновлені. Деякі postinst скрипти виконують це; деякі відмови його пропускають. Запустити вручну — недорого і корисно.

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

Завдання 16: Утримати оновлення ядра, поки стабілізуєтесь (опціонально, але часто розумно)

cr0x@server:~$ sudo apt-mark hold linux-image-generic linux-headers-generic
linux-image-generic set on hold.
linux-headers-generic set on hold.

Що це означає: Ви зупиняєте мета-пакети від витягування нових ядер автоматично.

Рішення: Використовуйте це під час інциденту. Знімайте холди, коли у вас буде відтворювана pipeline з DKMS та контроль перезавантажень.

Завдання 17: Підтвердити, яка буде стандартна запись завантаження (щоб не перезавантажитись у пастку)

cr0x@server:~$ grep -E 'GRUB_DEFAULT|GRUB_TIMEOUT|GRUB_SAVEDEFAULT' /etc/default/grub
GRUB_DEFAULT=0
GRUB_TIMEOUT=5

Що це означає: За замовчуванням обирається перший пункт меню, зазвичай найновіше ядро.

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

Завдання 18: Знайти «напівналаштовані» пакети після некоректного оновлення

cr0x@server:~$ sudo dpkg --audit
The following packages are in a mess due to serious problems during installation. They must be reinstalled for them to work properly:
 linux-image-6.8.0-52-generic

Що це означає: Встановлення пакета ядра не завершилося коректно, що могло пропустити тригер DKMS і генерацію initramfs.

Рішення: Виправте стан пакування перш ніж нескінченно дебажити DKMS.

Завдання 19: Відновити стан пакетного менеджера і заново виконати postinst тригери

cr0x@server:~$ sudo apt-get -f install
Reading package lists... Done
Building dependency tree... Done
Correcting dependencies... Done
Setting up linux-image-6.8.0-52-generic (6.8.0-52.53) ...
update-initramfs: Generating /boot/initrd.img-6.8.0-52-generic

Що це означає: Пост-інсталяційні хуки для ядра виконалися. Це часто включає тригер DKMS на перебудову.

Рішення: Заново перевірте dkms status для цільового ядра; знову перевірте наявність модулів і вміст initramfs.

Жарт 1: DKMS — як абонемент у спортзал: ви помічаєте, що він не працює, лише коли намагаєтесь скористатися ним.

Відновлення драйверів без простоїв: стратегія, що працює

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

Крок 1: Визначте, що означає «критичний драйвер» на цьому хості

Не всі DKMS-модулі однаково важливі. Відсутність VirtualBox-модуля на сервері дратує; відсутність модуля сховища на node з root-on-ZFS — катастрофа. Класифікуйте хост:

  • Критичний для сховища: ZFS root, ZFS data pools, HBA-драйвери, залежності dm-crypt.
  • Критичний для мережі: out-of-tree NIC-драйвери (рідко на Ubuntu, але трапляється), DPDK-модулі, SR-IOV стеки, вендорні offload-и.
  • Критичний для обчислень: NVIDIA GPU-ноди, ML-кластери, відеотранскодери.
  • «Непотрібно терміново»: робочі станції розробників та неважливі модулі.

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

Крок 2: Збирайте для ядра, в яке будете завантажуватись, а не для того, яке запущене

Конфігурації DKMS можуть вводити в оману. Якщо ви просто запустите dkms autoinstall без -k, воно часто орієнтується на запущене ядро. Це не те, що вам потрібно під час відновлення. Вам потрібне ядро наступного перезавантаження.

Завжди збирайте явного для цільової версії ядра.

Крок 3: Надавайте перевагу вендорським пакетам, що відстежують вашу лінію ядра

Коли DKMS-модуль не компілюється через несумісність API, у вас є дві реалістичні опції:

  • Оновити пакет драйвера/модуля до версії, сумісної з новим ядром.
  • Відтермінувати перезавантаження і зафіксувати версію ядра, поки не з’явиться сумісний драйвер.

Пробувати патчити джерела модуля прямо на продакшн-боксі о 2:00 — хобі, а не SRE-практика.

Крок 4: Валідируйте «чи можна завантажити» і «чи в initramfs»

Наявність на диску — недостатньо. Потрібне принаймні одне з:

  • Тест завантаження модуля на цільовому ядрі (важко без перезавантаження).
  • Перевірка підпису (якщо увімкнено Secure Boot).
  • Перевірка включення в initramfs для ранньо-завантажувальних модулів.

Практичний компроміс: ви перевіряєте шлях артефакта DKMS, запускаєте modinfo перевірки, перевіряєте стан підпису модулів і вміст initramfs. Потім перезавантажуєтесь у контрольованому вікні з відкатним ядром наготові.

Крок 5: Не ламайте свою мережу під час виправлення драйвера

Більшість відновлень DKMS навантажують CPU й диск, але не порушують трафік. Зона ризику — коли ви unload/reload модулі на живій системі. Якщо у вас немає резерву (bonding, multipath, кластеринг), уникайте перезавантаження мережевих/сховищних модулів на однонодовому критичному хості під час робочого часу.

Перебудова і інсталяція — безпечно. Вивантаження/завантаження — зміна.

Крок 6: Створіть «ворота перезавантаження»

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

  • dkms status для цільового ядра показує «installed» для критичних модулів
  • lsinitramfs містить ранньо-завантажувальні модулі
  • mokutil --sb-state і стан підписів узгоджені

Підключіть це в процес змін. Нудно. Працює.

Secure Boot і підпис модулів (MOK): тихий зламник

Якщо ви запускаєте Ubuntu 24.04 на обладнанні з увімкненим Secure Boot — а багато організацій так роблять через політику комплаєнсу — DKMS може «успішно зібрати» і ви все одно втратите функціональність. Ось чому:

  • DKMS компілює модуль.
  • Ядро відмовляється його завантажити, якщо модуль неподписаний (або підписаний ключем, що не занесений у довірені).
  • Ви дізнаєтесь про це лише тоді, коли драйвер стане потрібним, зазвичай після перезавантаження.

Як швидко розпізнати помилки підпису Secure Boot

Типові симптоми:

  • modprobe: ERROR: could not insert '...': Required key not available
  • dmesg показує «module verification failed» або обмеження lockdown
  • dkms status каже «installed», але функціональність відсутня

Що робити (практичні опції)

  1. Підписуйте DKMS-модулі і реєструйте ключ (MOK). Це чисте рішення, якщо Secure Boot має залишатися увімкненим.
  2. Вимкнути Secure Boot у прошивці. Операційно найпростіше, але може порушити політику.
  3. Використовувати підписані in-tree драйвери, коли це можливо. Довгостроково найкраще, але не завжди доступно.

Перевірити, чи DKMS підписує модулі

cr0x@server:~$ sudo grep -R "sign-file" -n /etc/dkms /etc/modprobe.d 2>/dev/null | head

Що це означає: Може не бути явної конфігурації. В Ubuntu підпис модулів DKMS часто пов’язаний із shim/MOK інструментарієм і скриптами пакування.

Рішення: Якщо Secure Boot увімкнено і ви бачите помилки підпису — не гадати. Перевірте підпис модулів за допомогою modinfo.

Переглянути метадані підпису модуля

cr0x@server:~$ modinfo -F signer /lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko
Canonical Ltd. Secure Boot Signing

Що це означає: Модуль має поле signer. Якщо порожнє, модуль може бути неподписаним (або метадані були видалені).

Рішення: Якщо signer відсутній і Secure Boot увімкнено — потрібно підписати і зареєструвати ключ, або прийняти, що модуль не завантажиться.

Перевірити зареєстровані ключі MOK

cr0x@server:~$ sudo mokutil --list-enrolled | head
[key 1]
SHA1 Fingerprint: 12:34:56:78:90:...
Subject: CN=Canonical Ltd. Secure Boot Signing

Що це означає: Система довіряє набору ключів. Якщо ваш підпис DKMS використовує інший ключ, ядро відхилить модуль.

Рішення: Узгодьте ключ підпису з зареєстрованими, або зареєструйте потрібний ключ через MOK (зазвичай це потребує перезавантаження в MOK manager).

Жарт 2: Secure Boot — це швейцар біля дверей ядра: ваш модуль може бути чудово одягнений і все одно не бути у списку дозволених.

initramfs, ранній завантажувальний етап і чому «зібрано» недостатньо

initramfs — це стиснений ранній userspace-образ, який ядро завантажує, щоб пройти шлях від «ядро запущено» до «маунт реального root-файлової системи». Якщо ваш критичний модуль не в initramfs, то наявність модуля на диску стає неважливою, бо диск може бути ще недоступний.

Це важливо для:

  • Систем з root-on-ZFS
  • Зашифрованого root, який потребує специфічних модулів на ранньому етапі
  • Деяких екзотичних потоків завантаження зі сховищ або по мережі

Режим відмови: DKMS встановив модулі, але initramfs згенеровано раніше

Це трапляється під час перерваних оновлень, паралельних операцій пакетного менеджера або коли DKMS запускається пізно, а initramfs — раніше. Ви перезавантажуєтесь і виявляєте, що ранній userspace не знаходить ZFS/SPL або драйвер сховища не присутній.

Виправлення: перегенеруйте initramfs після інсталяції DKMS для цільового ядра і перевірте вміст за допомогою lsinitramfs.

Режим відмови: кілька ядер, застарілий initramfs

Може бути так, що у вас є коректний initramfs для запущеного ядра, але немає для найновішого встановленого ядра. Так виникає пастка при перезавантаженні. Завжди перевіряйте initramfs, що відповідає ядрі, у яке ви плануєте завантажитися.

Три корпоративні міні-історії (реалістичні, анонімізовані)

Міні-історія 1: Інцидент через хибні припущення

У них був невеликий GPU-кластер для пакетного inference. Нічого екзотичного: хости на Ubuntu, NVIDIA DKMS-драйвер, планувальник задач і вікно змін по вівторках. Каденс оновлень був «ядро оновлюється автоматично, драйвери оновлюються коли хтось поскаржиться». Це працювало, поки не перестало.

Оновлення ядра прилетіло в п’ятницю ввечері через unattended-upgrades. Ніхто не помітив, бо ноди ще працювали на старому ядрі і GPU були доступні. Вранці в понеділок вони дренували одну ноду для планового обслуговування і перезавантажили її. Вона повернулась без завантажених модулів NVIDIA.

Хибне припущення було тонким: «якщо драйвер встановлено, то він встановлено». Вони ніколи не перевіряли, чи драйвер зібрався для щойно встановленого ядра. Нода перезавантажилась у найновіше ядро (як і повинна), а DKMS тихо провалився ще кілька днів тому.

Класичне виправлення — перевстановити пакет драйвера. Це не допомогло. Зрештою хтось подивився dmesg і знайшов повідомлення про примусову перевірку підпису Secure Boot. Secure Boot був увімкнений у прошивці після оновлення обладнання, але ніхто не оновив інструкцію.

Вирішення було простим — підпис і реєстрація ключа — але потребувало перезавантажень у MOK manager. Вони витратили день на координацію перезавантажень по нодах, чого можна було уникнути, якби мався pre-reboot gate і перевірка «DKMS встановлено для найновішого ядра».

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

Фінансова організація втомилась від повільних патчів. Вони вирішили «оптимізувати» і видалили інструменти збірки з продакшн-серверів: ні gcc, ні make, ні заголовків, мінімальний набір пакетів. Безпека це схвалила: образ став легшим, скани чистішими, і сервери виглядали як appliances.

Потім прийшло оновлення ядра. DKMS намагався перебудувати out-of-tree NIC-модуль, від якого вони залежали для специфічних функцій карти. Ні компілятора, ні заголовків, ні збірки. DKMS впав, але поточне ядро продовжувало працювати. Помилка залишалась непомітною.

Наступна хвиля перезавантажень відбулась під час планового обслуговування дата-центру. Декілька хостів запустились із новим ядром без NIC-модуля. Вбудований драйвер працював настільки, щоб пройти завантаження, але не мав offload-функцій, які вони підлаштували під свою латентність. Симптом не був «немає мережі». Він був гірший: періодичний колапс продуктивності і таймаути під навантаженням.

Вони скасували рішення про «мінімальний образ» для цього флоту і перенесли збірки DKMS у контрольований pipeline: передзбирати модулі для цільового ядра в підготовчому середовищі, доставити артефакти і верифікувати перед перезавантаженням. Оптимізація в принципі була слушною. Проте помилковою без заміни implicit вимоги DKMS на явний ланцюжок постачання модулів.

Урок: якщо ви видаляєте компілятори з хостів, ви берете на себе відповідальність за процес збірки модулів до кінця. Інакше ви просто відкладаєте відмову до часу перезавантаження.

Міні-історія 3: Скучно правильна практика, що врятувала ситуацію

Медіакомпанія керувала безліччю серверів зі сховищем, деякі з яких мали ZFS-пулі. Їхня практика була болісно нудною: після кожного оновлення ядра запускали автоматичну перевірку готовності до перезавантаження. Вона перевіряла статус DKMS для ZFS щодо найновішого встановленого ядра, перевіряла initramfs на наявність ZFS і переконувалась, що відоме робоче ядро залишається встановленим як опція відкату.

Одного ранку перевірка виявила помилку на підмножині хостів. DKMS показував ZFS «built», але не «installed» для найновішого ядра. Хости все ще працювали нормально, тож паніки не було. Вони заблокували перезавантаження через оркестратор і відкрили тікет.

Корінь проблеми — гонитва при пакуванні під час попереднього unattended-upgrade: генерація initramfs стала раніше, потім DKMS інсталяція впала і була повторно запущена, що лишило непослідовний стан. Скуча перевірка зловила це до перезавантаження. Вони виконали dkms autoinstall -k, перегенерували initramfs для цільового ядра і зняли блокування.

Ніякого простою, жодної драми, жодних вихідних. Ось як виглядає операційна досконалість, коли прибрати PowerPoint.

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

1) Симптом: dkms status показує «added» для нового ядра

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

Виправлення: Встановіть заголовки для цільового ядра, потім запустіть sudo dkms autoinstall -k <kernel>. Перевірте файли під /lib/modules/<kernel>/updates/dkms.

2) Симптом: збірка DKMS впадає з «Kernel headers not found»

Корінна причина: Відсутній пакет linux-headers-<kernel>, або симлінк /lib/modules/<kernel>/build пошкоджено.

Виправлення: Встановіть відповідні заголовки; перевірте ls -l /lib/modules/<kernel>/build щоб переконатися, що він вказує на заголовки.

3) Симптом: Модуль збирається, але modprobe впадає з «Required key not available»

Корінна причина: Secure Boot увімкнено; модуль неподписаний або підписаний ключем, що не занесений у довірені.

Виправлення: Переконайтеся, що DKMS-модулі підписано довіреним ключем і зареєструйте його через MOK, або вимкніть Secure Boot, якщо політика дозволяє.

4) Симптом: Після завантаження в нове ядро втрачається ZFS/кореневе сховище

Корінна причина: initramfs для нового ядра не містить потрібних модулів, часто через часові розбіжності DKMS або збої postinst тригерів.

Виправлення: Після інсталяції DKMS запустіть update-initramfs -u -k <kernel>, потім підтвердьте вміст за допомогою lsinitramfs.

5) Симптом: Помилки компіляції DKMS про відсутні символи / неявні декларації

Корінна причина: Зміни в API ядра; версія драйвера несумісна з новим ядром.

Виправлення: Оновіть пакет драйвера/модуля (наприклад, нові версії NVIDIA/ZFS), або зафіксуйте ядро і завантажтесь у старе ядро, доки сумісні пакети не стануть доступні.

6) Симптом: Все виглядає встановленим, але апарат не працює після перезавантаження

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

Виправлення: Підтвердіть встановлені ядра, перевірте вибір завантаження, перебудуйте явно для ядра, в яке будете завантажуватись, за допомогою dkms autoinstall -k.

7) Симптом: Оновлення пакетів зависають або лишають «напівналаштований» стан

Корінна причина: Перерване оновлення, блокування dpkg, заповнений FS, або збої postinst скриптів (часто DKMS).

Виправлення: Виправте стан dpkg: apt-get -f install, перевірте місце на диску і перезапустіть збірки DKMS після відновлення стану пакувального шару.

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

Чекліст A: Відновлення без простою на хості, що ще працює на старому ядрі

  1. Визначте цільове ядро (найновіше встановлене): використайте dpkg -l для встановлених образів.
  2. Перевірте статус DKMS для критичних модулів щодо цього ядра: dkms status.
  3. Встановіть заголовки для цільового ядра, якщо їх бракує: apt-get install linux-headers-<kernel>.
  4. Перебудуйте модулі для цільового ядра: dkms autoinstall -k <kernel>.
  5. Перевірте наявні артефакти в /lib/modules/<kernel>/updates/dkms.
  6. Перевірка Secure Boot: mokutil --sb-state і підтвердіть signer модуля через modinfo.
  7. Перегенеруйте initramfs для цільового ядра (на хостах, критичних для сховища): update-initramfs -u -k <kernel>.
  8. Перевірте вміст initramfs: lsinitramfs містить потрібні модулі.
  9. Тримайте опцію відкату: переконайтесь, що старіше відоме робоче ядро встановлене.
  10. Заплануйте перезавантаження з планом відкату (доступ до консолі, вибір GRUB, віддалений персонал на місці при потребі).

Чекліст B: Якщо ви вже перезавантажились у зламане ядро

  1. Підтвердіть, що відсутнє: lsmod, modprobe і dmesg.
  2. Негайно перевірте Secure Boot; не марнуйте час на перебудову неподписаних модулів, якщо Secure Boot їх відкине.
  3. Встановіть необхідні засоби збірки тимчасово: заголовки, компілятор, інструменти, якщо DKMS їх потребує.
  4. Перебудуйте DKMS для запущеного ядра: dkms autoinstall -k $(uname -r).
  5. Якщо збірка впала через несумісність API: зупиніться і підберіть сумісну версію драйвера або відкатуйтесь до попереднього ядра через GRUB.
  6. Виправте initramfs, якщо задіяні ранні модулі, потім протестуйте перезавантаження.

Чекліст C: Запобігти цьому наступного разу (гігієна продакшена)

  1. Створіть «ворота перезавантаження», які перевіряють DKMS встановленим для найновішого ядра і верифікують initramfs там, де потрібно.
  2. Стейджіть оновлення ядра на канарних хостах з репрезентативним апаратним забезпеченням.
  3. Розглядайте політику Secure Boot як головний критерій, а не як нотатку BIOS.
  4. Тримайте принаймні одне відкатне ядро встановленим і завантажуваним завжди.
  5. Контролюйте unattended-upgrades, щоб ядра не мінялися без валідації.

FAQ

1) Чому DKMS «зламався» лише після оновлення ядра?

Тому що DKMS-модулі компілюються проти конкретних заголовків ядра. Коли ядро змінюється, модуль потрібно перебудувати. Якщо ця перебудова провалюється, ви не помітите цього, доки не завантажитесь у нове ядро або не спробуєте завантажити модуль для нього.

2) Чи можна виправити DKMS без перезавантаження?

Ви можете перебудувати і встановити модулі для наступного ядра без перезавантаження, так. Зазвичай ви не можете протестувати їх завантаження в тому наступному ядрі без фактичного перезавантаження. Саме тому важливо верифікувати артефакти, підписи і вміст initramfs перед перезавантаженням.

3) Що означає «added» vs «built» vs «installed» у dkms status?

added: DKMS знає про джерело модуля, але ще не будував його для цього ядра. built: скомпільовано, але не обов’язково встановлено в дерево модулів ядра. installed: поміщено в /lib/modules/<kernel> і, ймовірно, виконано depmod.

4) Чи дійсно потрібні точні заголовки ядра?

Так. DKMS збирається проти заголовків точної версії ядра, яку ви таргетуєте. «Достатньо близько» тут не працює; встановіть linux-headers-<exact-version>.

5) Чому Secure Boot ускладнює все так сильно?

Тому що він перетворює проблему на часі компіляції у проблему часу виконання. Ви можете успішно зібрати модуль і все одно не змогти його завантажити. Ядро відкине модулі, які не підписані довіреним ключем, коли Secure Boot та політики lockdown це вимагають.

6) Якщо Secure Boot увімкнено, чи варто вимикати його?

Лише якщо ваша політика це дозволяє. Вимкнення Secure Boot може бути найпростішим операційним шляхом, але правильне рішення в регульованому середовищі — підписувати DKMS-модулі ключем, який ви контролюєте, і зареєструвати його через MOK.

7) Чому система завантажилась, але потім сховище або мережа зламались?

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

8) Який найнадійніший відкат, якщо не вдається зібрати DKMS для нового ядра?

Завантажте попереднє відоме робоче ядро і тимчасово зафіксуйте мета-пакети ядра. Потім оновіть пакет драйвера/модуля до версії, що підтримує нове ядро, перед наступною спробою перезавантаження.

9) Чи варто тримати компілятори поза продакшн-серверами?

Залежить від підходу. Якщо ви покладаєтесь на on-host збірки DKMS, вам потрібен інструментарій збірки і заголовки. Якщо ви їх видаляєте — ви маєте замінити on-host збірку DKMS на pipeline, який виробляє і доставляє сумісні модулі для кожного ядра, яке ви розгортаєте.

10) Як запобігти накопиченню «пасток перезавантаження» в майбутньому?

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

Наступні кроки, які ви можете виконати сьогодні

Якщо ви запускаєте Ubuntu 24.04 з драйверами, якими керує DKMS, припиніть ставитись до оновлень ядра як до «звичайних security-патчів». Це також події перебудови драйверів. Практичний шлях до відсутності простоїв короткий:

  1. Визначте критичні DKMS-модулі для ролі кожного хоста (сховище, мережа, GPU).
  2. Після кожної інсталяції ядра перебудовуйте модулі для найновішого встановленого ядра (dkms autoinstall -k).
  3. Перевіряйте підписи, якщо увімкнено Secure Boot; не припускайте, що успіх збірки означає успіх завантаження.
  4. Перегенеруйте і перевірте initramfs для модулів, які критичні на ранньому етапі завантаження.
  5. Лише після цього перезавантажуйтеся. Тримайте відкатне ядро встановленим і завантажуваним.

Зробіть це, і «DKMS зламався після оновлення ядра» перестане бути інцидентом. Це стане пунктом чеклісту, який виконується до того, як хтось щось помітить.

Ubuntu 24.04: Диск «повний», але df показує вільне — вичерпання inode пояснено (і виправлено)

Ви підключені по SSH до сервера Ubuntu 24.04, де df -h каже, що «місця вдосталь».
Проте кожен деплой ламається, логи не обертаються, apt не може розпакувати пакети, а ядро постійно викидає
No space left on device, наче йому платять за кожну помилку.

Це один із тих інцидентів, що змушує розумних людей сумніватися у власних очах. Диск не переповнений.
Переповнене щось інше. Зазвичай: іноди. І як тільки ви зрозумієте, що це означає, виправлення майже нудне.
Майже.

Що ви бачите: «диск повний», хоча гігабайти вільні

В Ubuntu «диск повний» часто означає одне з трьох:

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

Вичерпання інодів — підступне, бо воно не показується в тій команді, яку всі запускають першою.
df без прапорців повідомляє лише про використані/доступні блоки. Ви можете мати 200 ГБ вільних і при цьому
не створити 0-байтний файл. Файлова система не може виділити новий інод, отже не може створити файл.

Ви помітите дивні побічні ефекти:

  • Нові лог-файли не створюються, через що сервіси падають або перестають логувати в найважливіший момент.
  • apt падає посеред встановлення, бо не може створити тимчасові файли або розпакувати архіви.
  • Збірки Docker починають падати на операціях «writing layer», хоча томи виглядають в порядку.
  • Деякі додатки повідомляють «диск повний», а інші продовжують працювати (бо не створюють файли).

Є простий тест: спробуйте створити файл у ураженій файловій системі. Якщо це завершується помилкою «No space left»
при тому, що df -h показує вільне місце, припиняйте сперечатися з df і перевіряйте іноди.

Іноди пояснені для продакшну

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

Важлива операційна істина: більшість Linux-файлових систем мають два окремі «бюджети»:
блоки (байти) і іноди (кількість файлів). Якщо ви витратите будь-який із цих бюджетів — все.

Чому іноди закінчуються в реальних системах

Вичерпання інодів зазвичай не через «безліч великих файлів». Це «мільйони дрібних файлів».
Думайте про кеші, поштові черги, артефакти збірок, шари контейнерів, робочі простори CI,
тимчасові завантаження і буфери метрик, які хтось забув видаляти.

1 КБ файл все одно коштує одного інода. 0-байтний файл теж коштує інод. Директорія також займає інод.
Коли у вас 12 мільйонів дрібних файлів, диск може бути переважно порожнім по байтах, але таблиця інодів — вичерпана.

Які файлові системи найчастіше кусають

  • ext4: поширена на Ubuntu; іноди зазвичай створюються при форматуванні на основі співвідношення bytes-per-inode. Якщо ви невірно вгадали — можна вичерпати іноди.
  • XFS: іноди більш динамічні; вичерпання інодів трапляється рідше, але не виключено.
  • btrfs: алокація метаданих інша; все ще можна вдаритися об проблеми з метаданими, але це не та сама історія зі «фіксованою кількістю інодів».
  • overlayfs (Docker): це не самостійний тип файлової системи, але воно підсилює поведінку «багато файлів» на хостах із великою кількістю контейнерів.

Цитата, яку варто тримати у своєму мануалі:

«Надія — не стратегія.» — General Gordon R. Sullivan

Швидкий план діагностики (перший/другий/третій)

Коли алерт каже «No space left on device», а графіки показують, що все гаразд, не блукайте.
Дійте за планом.

Перший: підтвердьте, який саме «ресурс» вичерпано

  1. Перевірте блоки (df -h) і іноди (df -i) для ураженого монту.
  2. Спробуйте створити файл на цьому монті; підтвердіть шлях помилки.
  3. Перевірте dmesg на предмет переведення в режим лише читання або помилок файлової системи.

Другий: знайдіть монту та топ-споживачів інодів

  1. Визначте, який шлях файлової системи ламається (логи, тимчасові файли, директорія даних).
  2. Знайдіть директорії з великою кількістю файлів за допомогою find і парочціці цілеспрямованих підрахунків.
  3. Якщо це Docker/Kubernetes, перевірте overlay2, images, containers та логи.

Третій: безпечно звільніть іноди

  1. Почніть з очевидного безпечного прибирання: старі логи, кеші, тимчасові файли, вакуум журналу, сміття контейнерів.
  2. Видаляйте файли, не директорії, якщо додаток очікує структуру директорій.
  3. Підтвердіть зниження використання інодів (df -i) і відновлення сервісів.

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

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

Нижче — реальні завдання, які можна виконати на Ubuntu 24.04. Кожне містить, на що звертати увагу і яке рішення прийняти.
Виконуйте послідовно, якщо ви на чергуванні; вибирайте вибірково, якщо ви вже знаєте уражений монтувальний шлях.

Завдання 1: Підтвердити використання блоків (очевидна перевірка)

cr0x@server:~$ df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/sda2      ext4   200G   62G  128G  33% /
tmpfs          tmpfs  3.1G  1.2M  3.1G   1% /run
/dev/sdb1      ext4   1.8T  1.1T  648G  63% /data

Значення: Блоки в порядку на / і /data.
Рішення: Якщо ви все ще отримуєте «No space left», переходьте до перевірки інодів. Не витрачайте час на пошук великих файлів поки що.

Завдання 2: Перевірити використання інодів (реальний винуватець у половині випадків)

cr0x@server:~$ df -iT
Filesystem     Type    Inodes  IUsed   IFree IUse% Mounted on
/dev/sda2      ext4   13107200 13107190     10  100% /
tmpfs          tmpfs    790000     420 789580    1% /run
/dev/sdb1      ext4  122142720  982134 121160586   1% /data

Значення: Файлова система кореневого розділу має IUse% 100%. Лише 10 інодів вільні. Це робить неможливим створення файлів.
Рішення: Треба видалити файли на / (або перемістити їх), щоб звільнити іноди. Видалення одного великого файлу не допоможе, якщо це все одно один інод.

Завдання 3: Відтворіть помилку контрольовано

cr0x@server:~$ touch /tmp/inode-test-file
touch: cannot touch '/tmp/inode-test-file': No space left on device

Значення: Файлова система не може виділити інод для крихітного файлу.
Рішення: Розглядайте це як інцидент доступності. Все, що потребує записів (логи, сокети, PID-файли, тимчасові файли), може наступним впасти.

Завдання 4: Визначте, який шлях знаходиться на ураженому монту

cr0x@server:~$ findmnt -T /var/log
TARGET SOURCE    FSTYPE OPTIONS
/      /dev/sda2 ext4   rw,relatime,errors=remount-ro

Значення: /var/log знаходиться на /. Якщо логи вибухнули у мільйони файлів, це ваше поле бою.
Рішення: Зосередьте пошук під /var, /tmp, /var/lib та в будь-яких директоріях додатків на /.

Завдання 5: Виявити підозрілі директорії за розміром (байти)

cr0x@server:~$ sudo du -xh --max-depth=1 /var | sort -h
12M	/var/cache
180M	/var/log
2.1G	/var/lib
2.4G	/var

Значення: Використання байтів не екстремальне. Це ваш перший натяк, що проблема в кількості файлів, а не в розмірі.
Рішення: Припиніть оптимізувати під ГБ. Почніть оптимізувати під кількість файлів.

Завдання 6: Знайдіть директорії з величезною кількістю файлів (очищення верхнього рівня)

cr0x@server:~$ sudo bash -lc 'for d in /var/* /tmp /home; do [ -d "$d" ] && printf "%s\t" "$d" && find "$d" -xdev -type f 2>/dev/null | wc -l; done | sort -n -k2 | tail -n 10'
/var/cache	1320
/var/log	5402
/var/lib	12877190
/tmp	120
/home	88

Значення: /var/lib має ~12.8 мільйона файлів. Це не «трохи безладу»; це ваші іноди.
Рішення: Зосередьтесь на /var/lib. Якщо це хост контейнерів, очікуйте /var/lib/docker або /var/lib/containerd.

Завдання 7: Звузьте пошук всередині /var/lib швидко

cr0x@server:~$ sudo bash -lc 'for d in /var/lib/*; do [ -d "$d" ] && printf "%s\t" "$d" && find "$d" -xdev -type f 2>/dev/null | wc -l; done | sort -n -k2 | tail -n 10'
/var/lib/systemd	2200
/var/lib/dpkg	9800
/var/lib/docker	12866012

Значення: Docker споживає ваш бюджет інодів через шари, кеш збірок і логи контейнерів.
Рішення: Вирішіть, чи можна безпечно виконати prune зараз. Якщо це вузол для конкретного призначення, pruning зазвичай коректний. Якщо це «pet» сервер з невідомими робочими навантаженнями — будьте обережніші.

Завдання 8: Перевірка з боку Docker (байти, не іноди, але корисно)

cr0x@server:~$ sudo docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          48        12        23.4GB    18.1GB (77%)
Containers      65        18        4.2GB     2.9GB (69%)
Local Volumes   26        20        11.8GB    1.1GB (9%)
Build Cache     152       0         6.4GB     6.4GB

Значення: Є що почистити, але цей вивід не показує кількість файлів.
Рішення: Продовжуйте з prune-дiями, якщо ви розумієте їхній вплив: зупинені контейнери, dangling images, кеш збірок.

Завдання 9: Безпечне prune Docker (почніть консервативно)

cr0x@server:~$ sudo docker image prune -a -f
Deleted Images:
deleted: sha256:1a2b3c...
deleted: sha256:4d5e6f...
Total reclaimed space: 17.8GB

Значення: Видалені невикористовувані образи; кількість інодів теж має зменшитись, бо шари містять багато дрібних файлів.
Рішення: Повторно перевірте df -i. Якщо використання інодів не знизилось достатньо, почистіть контейнери та build cache далі.

Завдання 10: Prune build cache (часто мовчазна фабрика інодів)

cr0x@server:~$ sudo docker builder prune -a -f
Deleted build cache objects:
k2r1m3n4o5p6...
Total reclaimed space: 6.4GB

Значення: CI/хости збірок люблять залишати фрагменти кешу.
Рішення: Якщо вам потрібні відтворювані та швидкі збірки, плануйте цю очистку, а не виконувати її постійно вручну. Наразі ваша мета — стабілізувати вузол.

Завдання 11: Підтвердіть відновлення інодів (це ваш KPI по успішності)

cr0x@server:~$ df -i /
Filesystem     Inodes   IUsed    IFree IUse% Mounted on
/dev/sda2     13107200 8123400  4983800   63% /

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

Завдання 12: Якщо логи — винуватець, очистіть і обмежте їх (systemd-journald)

cr0x@server:~$ sudo journalctl --disk-usage
Archived and active journals take up 1.7G in the file system.

Значення: Журнали не обов’язково завантажують іноди (вони — менше, більші файли), але можуть створювати тиск по байтах.
Рішення: Якщо байти теж важливі — вакуумуйте. Якщо проблема саме в інодах — зосередьтесь на додатках, які створюють велику кількість окремих лог-файлів, а не на journal.

cr0x@server:~$ sudo journalctl --vacuum-time=7d
Vacuuming done, freed 1.2G of archived journals from /var/log/journal.

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

Завдання 13: Якщо apt падає, очистіть кеш пакетів

cr0x@server:~$ sudo apt-get clean

Значення: Очищає завантажені архіви пакетів під /var/cache/apt/archives.
Рішення: Добра гігієна, але зазвичай не панацея для інодів. Допомагає, коли кеші містять багато дрібних частинних файлів.

Завдання 14: Знайдіть директорії з великою кількістю файлів за допомогою du (стиль інодів)

cr0x@server:~$ sudo du -x --inodes --max-depth=2 /var/lib | sort -n | tail -n 10
1200	/var/lib/systemd
9800	/var/lib/dpkg
12866012	/var/lib/docker
12877190	/var/lib

Значення: Це ключовий вигляд: споживання інодів по директоріях.
Рішення: Цільте на найбільшого споживача. Не «чистіть трохи скрізь» — ви витратите час і все одно залишитесь на 100%.

Завдання 15: За сумнівів, інспектуйте патологічний розкид файлів

cr0x@server:~$ sudo find /var/lib/docker -xdev -type f -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -n | tail -n 5
  42000 /var/lib/docker/containers/8a7b.../mounts
  78000 /var/lib/docker/overlay2/3f2d.../diff/usr/lib
 120000 /var/lib/docker/overlay2/9c1e.../diff/var/cache
 250000 /var/lib/docker/overlay2/b7aa.../diff/usr/share
 980000 /var/lib/docker/overlay2/2d9b.../diff/node_modules

Значення: Шар контейнера з node_modules може згенерувати абсурдну кількість файлів.
Рішення: Виправте збірку (multi-stage builds, видалення dev-залежностей, .dockerignore) або перемістіть дані Docker на файлову систему, спроєктовану для такого навантаження.

Завдання 16: Підтвердіть тип файлової системи та деталі провізії інодів

cr0x@server:~$ sudo tune2fs -l /dev/sda2 | egrep -i 'Filesystem features|Inode count|Inode size|Block count|Reserved block count'
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
Inode count:              13107200
Inode size:               256
Block count:              52428800
Reserved block count:     2621440

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

Жарт №1: Іноди схожі на переговорні кімнати: ви можете мати порожню будівлю і все одно бути «повними», якщо в кожній кімнаті приклеєно по записці.

Три корпоративні міні-історії (анонімізовано, болісно реально)

1) Інцидент через хибне припущення: «df каже, що ми в порядку»

Середній SaaS запустив флот серверів Ubuntu для обробки webhook’ів. Інженери моніторили використання диска у відсотках,
налаштували алерти на 80% і були впевнені: «диски більше не переповнюються». У вівторок вдень pipeline обробки
почав повертати переривчасті 500. Повторні спроби накопичувалися, черги росли, дашборди загорілися.

На чергуванні виконали стандартну рутину: df -h виглядав здоровим. CPU був у нормі. Пам’ять не найкраща, але терпима.
Вони перезапустили сервіс, і він помер відразу, бо не зміг створити PID-файл. Нарешті з’явилася зрозуміла помилка:
No space left on device.

Хтось сказав «можливо диск збрехав», що було по-людськи чарівно інтерпретовано як «ми не вимірювали правильну річ».
Запустили df -i і побачили корінь на 100% використання інодів. Винуватцем не була база даних.
Це був «тимчасовий репозиторій повторів», реалізований як один JSON-файл на подію webhook у /var/lib/app/retry/.
Кожен файл був крихітним. Мільйони.

Виправлення було миттєве: видалити файли старші за поріг і перезапустити. Реальне виправлення зайняло спринт:
перенести retry store до черги, перестати використовувати файлову систему як дешеву базу даних і додати алерти на іноди.
Постмортем мав ввічливу назву. Внутрішній чат — ні.

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

Команда з обробки даних прискорила ETL, кешуючи проміжні артефакти на локальний SSD.
Вони перейшли від «один артефакт на батч» до «один артефакт на розділ», щоб підвищити паралелізм.
Продуктивність зросла. Витрати виглядали кращими. Усі забули про це.

Через кілька тижнів вузли почали падати по черзі. Не всі одразу, що ускладнювало діагностику.
Деякі задачі виконувалися, інші випадково падали при спробі записати вихід. Помилки були різні:
винятки Python, IO-помилки Java, іноді «read-only filesystem» після того, як ядро перемонтувало диск у режим лише читання.

Корінь проблеми був принизливо механічним: кеш створив десятки мільйонів дрібних файлів.
Файлова система ext4 була відформатована зі стандартним співвідношенням інодів, придатним для загальних навантажень, але не для «мільйонів шард».
Вузли не вичерпали байти; вони вичерпали ідентичності файлів. Оптимізація фактично стала стрес-тестом для інодів.

Вони намагалися «вирішити» це, збільшивши розмір диска. Це не допомогло, бо кількість інодів залишилася фіксованою.
Потім відформатували заново з вищою щільністю інодів і змінили стратегію кешування, пакуючи партії в tar-подібні бандли.
Продуктивність трохи впала. Надійність значно покращилась. Це прийнятна компромісна угода.

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

Інша організація запускала змішані навантаження на Kubernetes-нодах: системні сервіси, Docker/containerd і локальний scratch.
У них було одне правило: все, що може вибухнути у кількості файлів — на окрему файлову систему. Docker жив на /var/lib/docker,
змонтованому з виділеного тому. Scratch був на окремому монту з агресивними політиками очищення.

Також вони мали два нудні монітори: «блоки» і «іноди». Ніякого складного ML. Просто дві часові серії і алерти, які дзвонили до краю.
Вони тестували алерти щоквартально, створюючи тимчасову бурю інодів у staging (так, таке роблять).

Одного дня новий pipeline збірки почав продукувати патологічні шари з величезними деревами залежностей.
Іноди на Docker-тобі зросли швидко. Алерт спрацював раніше. Черговий не мусив вивчати нічого нового під стресом.
Вони зробили prune, відкотили pipeline і підвищили ліміти. Решта ноди залишилась здорова, бо кореневий розділ не постраждав.

Звіт по інциденту був коротким. Виправлення — нудне. Усі спали.
Ось в чому суть SRE.

Цікаві факти та трохи історії (бо це пояснює режими відмов)

  • Іноди походять з раннього Unix: концепція сягає оригінального дизайну файлової системи Unix, де метадані і блоки даних були окремими структурами.
  • Традиційні ext-файлові системи передвиділяють іноди: ext2/ext3/ext4 зазвичай визначають кількість інодів під час mkfs на основі співвідношення bytes-per-inode, а не динамічно під навантаження.
  • Стандартні співвідношення інодів — компроміс: вони орієнтовані на загальні навантаження; не оптимізовані для шарів контейнерів, кешів CI або вибухової maildir-активності.
  • Директорії теж коштують інодів: «ми лише створили директорії» не є виправданням; кожна директорія також споживає інод.
  • «No space left on device» має багато значень: той самий рядок помилки може означати вичерпання блоків, інодів, перевищення квоти або навіть те, що файлова система переведена у режим лише читання після помилок.
  • Резервні блоки існують не просто так: ext4 зазвичай резервує відсоток блоків для root, щоб система лишалася працездатною під тиском; іноди так само не резервуються.
  • Навантаження з дрібними файлами важче, ніж здається: операції з метаданими домінують; ефективність роботи з інодами і пошуком у директоріях може важити більше, ніж пропускна здатність.
  • Контейнерні образи підсилюють патерни дрібних файлів: екосистеми мов з великими деревами залежностей (Node, Python, Ruby) можуть створювати шари з масивною кількістю файлів.
  • Деякі файлові системи рухаються в бік динамічних метаданих: XFS і btrfs інакше працюють з метаданими, що змінює форму «повних» відмов, але не усуває їх повністю.

Виправлення: від швидкого очищення до постійного запобігання

Негайна стабілізація (хвилини): звільніть іноди, не погіршуючи ситуацію

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

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

Якщо це пов’язано з логами: вирішуйте проблему з кількістю файлів, а не лише з ротацією

Logrotate вирішує «один файл виростає вічно». Він не вирішує автоматично «ми створюємо один файл на запит».
Якщо ваш додаток створює унікальні лог-файли на одиницю роботи (request ID, job ID, tenant ID), ви самі атакуєте свою таблицю інодів.

Надавайте перевагу:

  • єдиному потоку логів зі структурованими полями (JSON підходить, але тримайте його в розумних межах)
  • інтеграції з journald там, де доречно
  • обмеженому локальному спулінгу з явним збереженням термінів

Якщо це Docker: обирайте data root відповідно до навантаження

Docker на ext4 може працювати добре, поки не стане погано. Якщо ви знаєте, що вузол буде збирати образи, запускати багато контейнерів
і активно псувати шари — ставтесь до /var/lib/docker як до datastore з високим churn і дайте йому окремий том.

Практичні опції:

  • Окремий монтувальний том для /var/lib/docker з щільністю інодів, що відповідає очікуваній кількості файлів.
  • Планове очищення кешу збірок, а не ручне під час аварій.
  • Виправлення образів для зменшення розгалуження файлів: multi-stage builds, обрізання залежностей, .dockerignore.

Постійне виправлення: проєктуйте файлову систему під навантаження

Якщо вичерпання інодів повторюється, у вас не проблема «прибрати». У вас проблема планування потужностей.
На ext4 кількість інодів фіксується під час створення. Єдиний реальний вихід — міграція на файлову систему з більшою кількістю інодів
(або інший лейаут), що означає:

  • створити нову файлову систему з вищою щільністю інодів
  • перенести дані
  • оновити монтування та сервіси
  • додати моніторинг і політики зберігання

Як створити ext4 з більшою кількістю інодів (планована міграція)

Кількість інодів у ext4 впливається параметром -i (bytes-per-inode) і -N (явна кількість інодів).
Менше байтів-на-інод означає більше інодів. Більше інодів — більше метаданих. Це компроміс, не безкоштовні ласощі.

cr0x@server:~$ sudo mkfs.ext4 -i 16384 /dev/sdc1
mke2fs 1.47.0 (5-Feb-2023)
Creating filesystem with 976754176 4k blocks and 61079552 inodes
Filesystem UUID: 9f1f4a1c-8b1d-4c1b-9d88-8d1aa14d4e1e
Superblock backups stored on blocks:
	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632
Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

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

Жарт №2: Якщо ви поводитесь з файловою системою як з базою даних — вона колись виставить вам рахунок інодами.

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

1) «df показує 30% використано, але я не можу записати файли» → вичерпання інодів → перевірити та видалити точки з великою кількістю дрібних файлів

  • Симптом: записи не вдаються, touch падає, apt ламається, сервіси падають при створенні тимчасових файлів.
  • Корінь: df -i показує 100% використання інодів на монті.
  • Виправлення: знайдіть директорії з найбільшою кількістю файлів за допомогою du --inodes / find ... | wc -l, видаліть безпечні ефемерні файли, потім запобігайте повторенню.

2) «Видалив великий файл, але все ще не працює» → ви звільнили блоки, не іноди → видаліть багато файлів замість одного

  • Симптом: кількість вільних ГБ зросла, але «No space left» продовжує з’являтися.
  • Корінь: використання інодів не змінилося.
  • Виправлення: звільніть іноди, видаляючи велику кількість файлів, а не одиничні великі файли. Цільте кеші, спули, артефакти збірок.

3) «Тільки root може писати; користувачі — ні» → резервні блоки або квоти → перевірте за допомогою tune2fs та інструментів квот

  • Симптом: root може створювати файли, non-root — ні.
  • Корінь: відсоток резервних блоків на ext4 або досягнуті користувацькі квоти.
  • Виправлення: перевірте резервні блоки за допомогою tune2fs; перевірте квоти; налаштовуйте обережно. Не ставте резервні блоки в 0% на системних розділах без розуміння наслідків.

4) «Він став read-only і тепер все ламається» → помилки файлової системи → дивіться dmesg, запускайте fsck офлайн

  • Симптом: ядро перемонтувало з errors=remount-ro, записи падають з помилками read-only.
  • Корінь: помилки введення/виведення або корупція, а не брак ємності.
  • Виправлення: інспектуйте dmesg; плануйте ребут у recovery та запустіть fsck. Очищення місця не виправить корупцію.

5) «Kubernetes node має DiskPressure, але df виглядає добре» → inode-тиск від runtime контейнерів → prune і окремі монти

  • Симптом: поди відкидаються; kubelet скаржиться; вузол нестабільний.
  • Корінь: директорії runtime заповнили бюджет інодів (overlay2, логи).
  • Виправлення: очистіть runtime storage, примусьте image garbage collection, розмістіть runtime на виділеному томі з моніторингом.

6) «Ми почистили /tmp; допомогло на годину» → додаток відтворює бурю → фіксуйте утримання у джерелі

  • Симптом: повторні інциденти інодів після очищення.
  • Корінь: баг додатку, поганий дизайн (один файл на подію) або відсутній TTL/ротація.
  • Виправлення: додайте політику утримання, переробіть сховище (БД/черга/object store), примусьте ліміти і алерти.

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

Чекліст для чергового (стабілізувати за 15–30 хвилин)

  1. Запустіть df -hT і df -iT для проблемного монту.
  2. Підтвердіть через touch у ураженому шляху.
  3. Знайдіть відповідний монтувальний шлях з findmnt -T.
  4. Визначте топ-споживачів інодів з du -x --inodes --max-depth=2 і цілеспрямованими find ... | wc -l.
  5. Виберіть одне безпечне та високоефективне очищення (Docker prune, очищення кешу, видалення за часом).
  6. Пере-перевіряйте df -i поки не опуститесь під ~90% на критичному монті.
  7. Перезапустіть впливові сервіси (тільки після того, як записи знову працюють).
  8. Зберіть докази: виконані команди, рахунок інодів до/після, відповідальні директорії.

Інженерний чекліст (запобігання повторенню)

  1. Додайте моніторинг і алерти на використання інодів по файлових системах (не лише відсоток диску).
  2. Помістіть високочастотні директорії на виділені монти: /var/lib/docker, спул аплікацій, кеш збірок.
  3. Впровадьте утримання у виробника: TTL, ліміти, періодична компакція або інше сховище.
  4. Перегляньте збірки та образи: зменште кількість файлів у шарах; уникайте вендорингу великих дерев залежностей в runtime.
  5. Якщо використовуєте ext4 для навантажень з дрібними файлами — задайте щільність інодів під час форматування і задокументуйте вибір.
  6. Проведіть game day у staging: симулюйте тиск інодів і перевірте алерти та recovery-процедури.

Чекліст міграції (коли кількість інодів ext4 фундаментально не підходить)

  1. Виміряйте поточну кількість файлів і темп росту (щоденні нові файли, поведінка зберігання).
  2. Виберіть ціль: ext4 з вищою щільністю інодів, XFS, або іншу архітектуру (object storage, БД, черга).
  3. Проведіть підготовку нового тому і файлової системи; змонтуйте на призначений шлях.
  4. Зупиніть робоче навантаження, скопіюйте дані (збережіть власність/права), перевірте, потім переключіться.
  5. Увімкніть робоче навантаження з увімкненими політиками утримання з першого дня.
  6. Залиште запобіжники: алерти, таймери очищення і жорстку політику ліміту.

FAQ

1) Що таке інод в одному реченні?

Інод — це запис метаданих файлової системи, що представляє файл або директорію; для створення нового файлу потрібен вільний інод.

2) Чому Ubuntu каже «No space left on device», коли df -h показує вільне місце?

Тому що «місце» може означати байти (блоки) або метадані файлів (іноди). df -h показує блоки; df -i — іноди.

3) Як швидко підтвердити вичерпання інодів?

Запустіть df -i на монті і перевірте на IUse% 100%, потім спробуйте touch, щоб підтвердити, що створення файлу не вдається.

4) Чи можна збільшити кількість інодів на існуючій ext4-файловій системі?

Практично — ні. Кількість інодів ext4 фактично фіксується при створенні файлової системи. Реальне рішення — міграція на нову файлову систему з потрібною кількістю інодів.

5) Чому контейнери роблять проблеми з інодами більш імовірними?

Шари образів і дерева залежностей можуть містити величезну кількість дрібних файлів. Overlay-сховище множить операції з метаданими, а кеші збірок накопичуються непомітно.

6) Чи безпечно видаляти одну велику директорію?

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

7) Що варто моніторити, щоб виявити це раніше?

Моніторте використання інодів по файлових системах (df -i) і ставте алерти на стійке зростання або порогові значення (наприклад, 85% і 95%), а не лише на відсоток диску.

8) Якщо я вичерпав іноди, чи варто перезавантажуватися?

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

9) Чому інодами іноді страждає лише один додаток?

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

10) Чи можуть журнали journald викликати вичерпання інодів?

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

Наступні кроки, які варто зробити

Якщо ви візьмете лише одну звичку з цього: коли бачите «No space left on device», запускайте df -i так само автоматично, як df -h.
Файлова система має два ліміти, і продакшн не піклується про те, який саме ви забули моніторити.

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

  1. Додайте алерти на іноди на кожному персистентному монту (особливо / і сховище runtime контейнерів).
  2. Перенесіть шляхи з високою частотою створення файлів на окремі файлові системи, щоб одне погане навантаження не виводило з ладу весь вузол.
  3. Виправте поведінку виробника: TTL, ліміти, менше файлів, краще пакування артефактів.
  4. Для ext4 плануйте щільність інодів наперед під навантаження з дрібними файлами та документуйте вибір.
  5. Проведіть мікро-тест: створіть контрольовану бурю інодів у staging, перевірте алерти і процедури відновлення.

Вам не потрібні більше диску. Потрібно менше файлів, кращий lifecycle і розкладка файлової системи, що відповідає реальності, а не припущенням.

Майбутня безпека CPU: чи закінчилися сюрпризи класу Spectre?

У вашому інцидентному тікеті написано «CPU став повільнішим на 40% після патчу». У вашому безпековому тікеті — «пом’якшення мають залишатися увімкненими». У вашому плані потужностей — «лол». Поміж цих трьох тверджень знаходиться реальність сучасної безпеки CPU: наступний сюрприз не буде точнісінько як Spectre, але буде римуватися.

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

Відповідь уперед (і що з нею робити)

Ні, сюрпризи класу Spectre ще не закінчилися. Ми пройшли фазу «все горить» 2018 року, але базовий урок лишається: можливості для підвищення продуктивності створюють вимірювані побічні ефекти, а нападникам подобаються вимірювані побічні ефекти. CPU все ще агресивно оптимізують. ПЗ все ще будує абстракції на цих оптимізаціях. Ланцюжок постачання залишається складним (прошивка, мікрокод, гіпервізори, ядра, бібліотеки, компілятори). Поверхня атаки — рухома ціль.

Хороша новина: ми вже не безпорадні. Апаратура тепер постачається з більшим числом керувальних перемикачів, кращими налаштуваннями за замовчуванням і чіткішими контрактами. Ядра навчилися нових трюків. Хмарні провайдери мають операційні практики, що не включають панічні патчі о 3 ранку. Погана новина: мітгації — це не «увімкнув і забув». Це конфігурація, управління життєвим циклом та інженерія продуктивності.

Що вам слід зробити (думка автора)

  • Перестаньте ставитись до мітгацій як до бінарного параметра. Визначте політику для кожного навантаження: мультиорендне проти одноорендного, браузер/JS-експозиція проти серверного, чутлива криптографія проти статeless-кешу.
  • Ведіть облік CPU/прошивки. «Ми запатчені» нічого не значить без версій мікрокоду, версій ядра та перевірених мітгацій на кожному класі хостів.
  • Бенчмарьте з увімкненими мітгаціями. Не один раз. Безперервно. Прив’язуйте до розгортань ядра і мікрокоду.
  • Віддавайте перевагу нудній ізоляції над хитромудрими перемикачами. Присвячені хости, сильні межі VM і відключення SMT там, де потрібно, краще, ніж сподівання, що прапорець мікрокоду врятує вас.
  • Інструментуйте вартість. Якщо ви не можете пояснити, куди пішли цикли (syscalls, переключення контексту, помилки передбачення гілок, I/O), ви не зможете безпечно обирати мітгації.

Одна перефразована ідея від Gene Kim (надійність/операції): Швидкі, часті зміни безпечніші, коли у вас є сильні зворотні зв’язки й ви можете швидко виявити та відновити. Так ви переживаєте безпекові сюрпризи: зробіть зміну рутинною, а не героїчною.

Що змінилося з 2018 року: чіпи, ядра та культура

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

  1. 2018 рік змусив індустрію говорити про мікроархітектуру, ніби це має значення. Раніше багато команд ops ставилися до внутрішньої архітектури CPU як до «магії вендора» й зосереджувалися на налаштуванні ОС/додатків.
  2. Початкові мітгації були грубими інструментами. Перші відповіді ядер часто міняли латентність заради безпеки, бо альтернативою було «нічого не випускати».
  3. Retpoline був стратегією компілятора, не особливістю апаратури. Він зменшував ризики певних ін’єкцій цілей переходів, не покладаючись виключно на поведінку мікрокоду.
  4. Hyper-threading (SMT) перестав бути «безкоштовною продуктивністю» й став «ручкою ризику». Деякі шляхи витоку стають гіршими, коли сусідні потоки ділять ресурси ядра.
  5. Мікрокод став операційною залежністю. Оновлення BIOS/прошивки раніше були рідкісними; тепер це регулярний пункт обслуговування, іноді доставлений через пакети ОС.
  6. Хмарні провайдери потай змінили політики планування. Рівні ізоляції, присвячені хости і контролі «шумного сусіда» раптом набули безпекового сенсу, а не лише продуктивностного.
  7. Дослідження атак змістилися до нових бокових каналів. Кеші були лише початком; предиктори, буфери й ефекти транзитного виконання стали мейнстримом.
  8. Безпекова позиція почала включати «регресії продуктивності як ризик». Мітгація, що зменшує пропускну здатність удвічі, може змусити до небезпечних скорочень або відкладених патчів — і те, й інше є провалом безпеки.

Апаратне забезпечення стало явнішим

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

Але прогрес апаратного забезпечення нерівномірний. Різні покоління, вендори й SKU сильно відрізняються. Ви не можете трактувати «Intel» або «AMD» як однорідну поведінку. Навіть у межах сімейства моделі, ревізії мікрокоду можуть змінювати поведінку мітгацій і їхній вплив на продуктивність.

Ядра навчилися торгуватися

Linux (і інші ОС) навчилися виявляти можливості CPU, умовно застосовувати мітгації й показувати стан так, щоб оператори могли аудіюватися. Це велика річ. У 2018 році багато команд просто переключали параметри завантаження й сподівалися. Сьогодні ви можете запитати: «Чи активний IBRS?» «Чи ввімкнено KPTI?» «Чи вважається SMT тут небезпечним?» — і зробити це в масштабі.

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

Жарт #1: Спекулятивне виконання — як стажер, що починає одночасно три завдання «щоб бути ефективним», а потім розливає каву в продакшені. Швидко і дивовижно креативно.

Чому «Spectre» — це клас, а не баг

Коли питають, чи «закінчився» Spectre, зазвичай мають на увазі: «Чи покінчено з вразливостями спекулятивного виконання?» Це як питати, чи покінчено з «багами в розподілених системах». Ви можете закрити тикет. Ви не закрили категорію.

Базовий патерн

Проблеми класу Spectre зловживають невідповідністю між архітектурною поведінкою (те, що CPU обіцяє зробити) і мікроархітектурною поведінкою (що фактично відбувається внутрішньо для пришвидшення). Транзитне виконання може звертатись до даних, що мають бути недоступні, а потім витікати натяк про них через час або інші бокові канали. CPU пізніше «відкочує» архітектурний стан, але він не може відкотити фізику. Кеші прогріті. Предиктори навчені. Буфери заповнені. Хитрий нападник може виміряти цей залишок.

Чому мітгації складні

Мітгація складна, бо:

  • Ви боретесь з вимірюванням. Якщо нападник може послідовно вимірювати кілька наносекунд, у вас проблема — навіть якщо архітектурно нічого «поганого» не сталося.
  • Мітгації живуть на кількох рівнях. Апаратні можливості, мікрокод, ядро, гіпервізор, компілятор, бібліотеки й іноді саме застосунки.
  • Робочі навантаження реагують по-різному. Навантаження з великою кількістю syscall може страждати від певних мітгацій ядра; обчислювальне навантаження може ледве помічати їх.
  • Моделі загроз різні. Пісочниця браузера відрізняється від одноорендного HPC-бокса від спільних вузлів Kubernetes.

«Ми запатчили» — це не стан, це твердження

Операційно ставтесь до безпеки класу Spectre як до збереження даних у сховищі: ви не вимагаєте її — ви перевіряєте її постійно. Перевірка повинна бути дешево, автоматизованою й прив’язаною до контролю змін.

Звідки прийдуть наступні сюрпризи

Наступна хвиля не обов’язково називатиметься «Spectre vNext», але вона все одно скористається тим самим мета-проблемним моментом: можливості CPU створюють спільний стан, а спільний стан витікає.

1) Предиктори, буфери та «невидимі» спільні структури

Кеші — знамениті бокові канали. Справжні нападники також цікавляться предикторами гілок, предикторами повернення, store buffers, line fill buffers, TLB та іншим мікроархітектурним станом, який можна впливати й вимірювати через межі безпеки.

У міру того, як чіпи додають більше хитрості (більші предиктори, глибші конвеєри, ширше issue), кількість місць, де може ховатися «залишковий стан», зростає. Навіть якщо вендори додають розділення, все одно лишаються переходи: user→kernel, VM→hypervisor, container→container на одному хості, process→process.

2) Гетерогенні обчислення та акселератори

CPU зараз ділять роботу з GPU, NPU, DPU і «енклявами безпеки». Це змінює поверхню бокових каналів. Деякі з компонентів мають власні кеші і планувальники. Якщо вам здається, що спекулятивне виконання складне, почекайте, поки вам доведеться розбиратися зі спільною пам’яттю GPU і багаторендними ядрами.

3) Ланцюжок постачання прошивки і дрейф конфігурацій

Мітгації часто залежать від мікрокоду і налаштувань прошивки. Флоти дрейфують. Хтось замінює материнську плату, оновлення BIOS повертає налаштування, або вендор встановлює «продуктивні» значення за замовчуванням, що знову вмикають ризикову поведінку. Ваша модель загроз може бути бездоганною і все одно провалитися, бо ваша інвентаризація — вигадка.

4) Тиск з боку крос-орендності у хмарі

Бізнес-реальність: мультиорендність платить рахунки. Саме там бокові канали важливі. Якщо ви оперуєте спільними вузлами, повинні припустити допитливих сусідів. Якщо у вас одноорендне обладнання — все одно турбуйтеся про втечі пісочниці, браузерну експозицію чи зловмисні робочі навантаження, які ви самі запускаєте (привіт, CI/CD).

5) «Податок на мітгації» провокує небезпечну поведінку

Це недооцінений сценарій провалу: мітгації, що шкодять продуктивності, можуть змусити команди вимикати їх, затримувати патчі або перевантажувати вузли, щоб виконувати SLO. Так виникає борг безпеки з відсотками. Наступний сюрприз може бути організаційним, а не мікроархітектурним.

Жарт #2: Нічого так не мотивує «прийняття ризику», як регресія продуктивності на 20% і кінець кварталу.

Моделі ризику, що відповідають продакшену

Почніть з меж, а не з імен CVE

Проблеми класу Spectre стосуються витоку через межі. Тож змепіть ваше середовище за межами:

  • Користувач ↔ ядро (недовірені локальні користувачі, пісочниці, шляхи втечі з контейнерів)
  • VM ↔ гіпервізор (мультиорендна віртуалізація)
  • Процес ↔ процес (спільний хост з різними доменами довіри)
  • Потік ↔ потік (SMT-сусіди)
  • Хост ↔ хост (менш пряме, але думайте про спільні кеші в деяких конструкціях, NIC offloads або бокові канали спільного сховища)

Три поширені продакшен-позиції

Позиція A: «Ми запускаємо недовірений код» (найсуворіші мітгації)

Приклади: публічна хмара, CI-ранери для зовнішніх контрибуторів, ферми рендерингу для браузерів, хости плагінів, мультиорендний PaaS. Тут не варто хитрувати. Вмикайте мітгації за замовчуванням. Розгляньте вимкнення SMT на спільних вузлах. Розгляньте виділені хости для чутливих орендарів. Ви зменшуєте ймовірність витоку даних між орендарями.

Позиція B: «Ми запускаємо напівдовірений код» (збалансовано)

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

Позиція C: «Ми запускаємо довірений код на виділеному обладнанні» (але це не freesbie)

Приклади: виділені коробки баз даних, однопроцесорні пристрої, HPC. Ви можете прийняти певний ризик заради продуктивності, але остерігайтеся двох пасток: (1) браузери й JIT-рантайми можуть вводити «напівнедовірені» властивості, і (2) внутрішня загроза й ланцюжок постачання — реальні. Якщо ви вимикаєте мітгації, документуйте це, ізолюйте систему і постійно перевіряйте, що вона лишається ізольованою.

Робіть політику виконуваною

Політика, що живе у вікі — казка перед сном. Політика, що живе в автоматизації — контроль. Вам потрібні:

  • Мітки вузлів (наприклад, «smt_off_required», «mitigations_strict»)
  • Профілі параметрів завантаження, керовані конфігураційними інструментами
  • Безперервні перевірки відповідності: версія мікрокоду, прапорці ядра, статус вразливостей
  • Ворота для регресій продуктивності при розгортанні ядра/мікрокоду

Практичні завдання: аудит, верифікація та вибір мітгацій (з командами)

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

Завдання 1: Перевірити статус вразливостей, який повідомляє ядро

cr0x@server:~$ grep . /sys/devices/system/cpu/vulnerabilities/*
/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Mitigation: Clear CPU buffers; SMT Host state unknown
/sys/devices/system/cpu/vulnerabilities/itlb_multihit:KVM: Mitigation: VMX disabled
/sys/devices/system/cpu/vulnerabilities/l1tf:Mitigation: PTE Inversion; VMX: conditional cache flushes, SMT vulnerable
/sys/devices/system/cpu/vulnerabilities/mds:Mitigation: Clear CPU buffers; SMT vulnerable
/sys/devices/system/cpu/vulnerabilities/meltdown:Mitigation: PTI
/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Mitigation: Clear CPU buffers; SMT Host state unknown
/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected
/sys/devices/system/cpu/vulnerabilities/retbleed:Mitigation: IBRS
/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Mitigation: Safe RET
/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl and seccomp
/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization
/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Enhanced IBRS, IBPB: conditional, RSB filling, STIBP: conditional
/sys/devices/system/cpu/vulnerabilities/srbds:Mitigation: Microcode
/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected

Що це означає: Ядро каже, які мітгації активні і де лишається ризик (особливо рядки з «SMT vulnerable» або «Host state unknown»).

Рішення: Якщо ви запускаєте мультиорендні або недовірені навантаження й бачите «SMT vulnerable», ескалюйте й розгляньте відключення SMT або суворішу ізоляцію для цих вузлів.

Завдання 2: Підтвердити стан SMT (hyper-threading)

cr0x@server:~$ cat /sys/devices/system/cpu/smt/active
1

Що це означає: 1 означає, що SMT активний; 0 — відключений.

Рішення: На спільних вузлах, що обробляють недовірені навантаження, віддавайте перевагу 0, якщо у вас немає кількісного обґрунтування протилежного. На виділених одноорендних коробках — вирішуйте залежно від навантаження і толерантності до ризику.

Завдання 3: Подивитись, з якими мітгаціями ядро завантажилось

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.6.15 root=UUID=... ro mitigations=auto,nosmt spectre_v2=on

Що це означає: Параметри ядра визначають поведінку високого рівня. mitigations=auto,nosmt просить автоматичні мітгації та вимкнення SMT.

Рішення: Трактуйте це як бажаний стан. Потім перевірте фактичний стан через /sys/devices/system/cpu/vulnerabilities/*, бо деякі прапорці ігноруються, якщо не підтримуються.

Завдання 4: Перевірити ревізію мікрокоду, що зараз завантажена

cr0x@server:~$ dmesg | grep -i microcode | tail -n 5
[    0.612345] microcode: Current revision: 0x000000f6
[    0.612346] microcode: Updated early from: 0x000000e2
[    1.234567] microcode: Microcode Update Driver: v2.2.

Що це означає: Ви бачите, чи відбулося раннє оновлення мікрокоду і яка ревізія активна.

Рішення: Якщо у флоті змішані ревізії на однаковій моделі CPU — у вас дрейф. Виправте дрейф перед дебатами про продуктивність. Змішаний мікрокод = змішана поведінка.

Завдання 5: Скорелюйте модель CPU і stepping (бо це важливо)

cr0x@server:~$ lscpu | egrep 'Model name|Vendor ID|CPU family|Model:|Stepping:|Flags'
Vendor ID:                       GenuineIntel
Model name:                      Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz
CPU family:                      6
Model:                           106
Stepping:                        6
Flags:                           fpu vme de pse tsc ... ssbd ibrs ibpb stibp arch_capabilities

Що це означає: Прапорці на кшталт ibrs, ibpb, stibp, ssbd і arch_capabilities підказують, які механізми мітгації існують.

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

Завдання 6: Валідувати статус KPTI / PTI (пов’язане з Meltdown)

cr0x@server:~$ dmesg | egrep -i 'pti|kpti|page table isolation' | tail -n 5
[    0.000000] Kernel/User page tables isolation: enabled

Що це означає: PTI увімкнено. Це зазвичай підвищує накладні витрати на syscalls на вразливих системах.

Рішення: Якщо ви бачите раптову латентність у навантаженнях з великою кількістю syscalls — PTI підозрілий. Але не вимикайте його легковажно; краще оновити апаратуру, де це менш дорого або не потрібно.

Завдання 7: Перевірити деталі режиму мітгації Spectre v2

cr0x@server:~$ cat /sys/devices/system/cpu/vulnerabilities/spectre_v2
Mitigation: Enhanced IBRS, IBPB: conditional, RSB filling, STIBP: conditional

Що це означає: Ядро обрало певну суміш мітгацій. «Conditional» часто означає, що ядро застосовує це при переключеннях контексту або коли виявляє ризикові переходи.

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

Завдання 8: Підтвердити, чи ядро вважає SMT безпечним для MDS-подібних проблем

cr0x@server:~$ cat /sys/devices/system/cpu/vulnerabilities/mds
Mitigation: Clear CPU buffers; SMT vulnerable

Що це означає: Очищення буферів CPU допомагає, але SMT все одно залишає шляхи експозиції, які ядро відмічає.

Рішення: Для мультиорендних хостів це сильний сигнал відключити SMT або перейти на виділену оренду.

Завдання 9: Швидко виміряти тиск переключень контексту і syscalls

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 842112  52124 912340    0    0    12    33  820 1600 12  6 82  0  0
 3  0      0 841900  52124 912500    0    0     0     4 1100 4200 28 14 58  0  0
 4  0      0 841880  52124 912600    0    0     0     0 1300 6100 35 18 47  0  0
 1  0      0 841870  52124 912650    0    0     0     0  900 2000 18  8 74  0  0

Що це означає: Слідкуйте за cs (переключення контексту) і sy (системний CPU). Якщо cs стрибає і sy росте після зміни мітгацій — ви знайшли, куди «сідає» податок.

Рішення: Розгляньте зменшення частоти syscalls (пакування, асинхронний I/O, менше процесів) або перемістіть навантаження на нові CPU з дешевшими мітгаціями.

Завдання 10: Виявити накладні витрати, пов’язані з мітгаціями, через perf (загально)

cr0x@server:~$ sudo perf stat -a -- sleep 5
 Performance counter stats for 'system wide':

        24,118.32 msec cpu-clock                 #    4.823 CPUs utilized
     1,204,883,112      context-switches         #   49.953 K/sec
        18,992,114      cpu-migrations           #  787.471 /sec
         2,113,992      page-faults              #  87.624 /sec
  62,901,223,111,222      cycles                 #    2.608 GHz
  43,118,441,902,112      instructions           #    0.69  insn per cycle
     9,882,991,443      branches                #  409.687 M/sec
       412,888,120      branch-misses           #    4.18% of all branches

       5.000904564 seconds time elapsed

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

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

Завдання 11: Перевірити, чи KVM задіяний і що він повідомляє

cr0x@server:~$ lsmod | grep -E '^kvm|^kvm_intel|^kvm_amd'
kvm_intel             372736  0
kvm                  1032192  1 kvm_intel

Що це означає: Хост — гіпервізор. Механізми контролю спекуляції можуть застосовуватись при вході/виході VM, і деякі вразливості відкривають ризик між VM.

Рішення: Трактуйте цей клас хостів як підвищену чутливість. Уникайте кастомних «продуктивних» переключень, якщо не можете показати, що безпека між VM збережена.

Завдання 12: Підтвердити встановлені пакети мікрокоду (приклад Debian/Ubuntu)

cr0x@server:~$ dpkg -l | egrep 'intel-microcode|amd64-microcode'
ii  intel-microcode  3.20231114.1ubuntu1  amd64  Processor microcode firmware for Intel CPUs

Що це означає: Мікрокод, керований ОС, присутній і версіонований, що полегшує оновлення флоту порівняно з лише BIOS-підходом.

Рішення: Якщо мікрокод лише через BIOS і у вас немає конвеєра прошивки — ви відставатимете по мітгаціях. Побудуйте цей конвеєр.

Завдання 13: Підтвердити встановлені пакети мікрокоду (приклад RHEL)

cr0x@server:~$ rpm -qa | egrep '^microcode_ctl|^linux-firmware'
microcode_ctl-20240109-1.el9.x86_64
linux-firmware-20240115-2.el9.noarch

Що це означає: Доставка мікрокоду — частина патчування ОС з власним графіком.

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

Завдання 14: Валідувати, чи мітгації були відключені (навмисно чи випадково)

cr0x@server:~$ grep -Eo 'mitigations=[^ ]+|nospectre_v[0-9]+|spectre_v[0-9]+=[^ ]+|nopti|nosmt' /proc/cmdline
mitigations=off
nopti

Що це означає: Цей хост працює з явно відключеними мітгаціями. Це не «можливо». Це вибір.

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

Завдання 15: Кількісно виміряти різницю в продуктивності безпечно (A/B завантаження ядра)

cr0x@server:~$ sudo systemctl reboot --boot-loader-entry=auto-mitigations
Failed to reboot: Boot loader entry not supported

Що це означає: Не кожне середовище підтримує легке перемикання записів завантажувача. Можливо, потрібен інший підхід (GRUB профілі, kexec або виділені канарні хости).

Рішення: Побудуйте відтворюваний канарний механізм. Якщо ви не можете A/B тестувати пари ядро+мікрокод, ви будете вічно сперечатись про продуктивність.

Завдання 16: Перевірити realtime-ядро проти generic (чутливість до латентності)

cr0x@server:~$ uname -a
Linux server 6.6.15-rt14 #1 SMP PREEMPT_RT x86_64 GNU/Linux

Що це означає: PREEMPT_RT або ядра низької латентності взаємодіють з накладними мітгацій по-іншому, бо поведінка планування й пріоритету змінюється.

Рішення: Якщо ви запускаєте RT-навантаження, тестуйте мітгації саме на RT-ядрах. Не робіть висновків на основі generic-ядра.

Плейбук швидкої діагностики

Це для дня, коли ви пропатчили ядро або мікрокод і ваші SLO-дашборди перетворилися на модерн-арт.

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

  1. Швидко перевірте стан мітгацій: grep . /sys/devices/system/cpu/vulnerabilities/*. Шукайте змінені формулювання порівняно з останнім відомим добрим станом.
  2. Перевірте прапорці завантаження: cat /proc/cmdline. Підтвердіть, що ви не успадкували mitigations=off або випадково не додали суворіші прапорці в новому образі.
  3. Перевірте ревізію мікрокоду: dmesg | grep -i microcode. Зміна мікрокоду може змінити поведінку без зміни ядра.

По-друге: локалізуйте витрати (куди поділися цикли?)

  1. Тиск на syscalls/переключення контексту: vmstat 1. Якщо sy і cs ростуть, підозрюйте мітгації, що впливають на переходи в ядро.
  2. Стрес планування: перевірте міграції та чергу виконання. Високі cpu-migrations в perf stat або підвищене r у vmstat вказують на взаємодію з планувальником.
  3. Симптоми предикторів/гілок: perf stat з фокусом на пропуски гілок і IPC. Не остаточно, але корисний компас.

По-третє: ізолюйте змінні та оберіть найменш погане виправлення

  1. Канаруйте один клас хостів: та сама модель CPU, те саме навантаження, та сама форма трафіку. Змініть лише одну змінну: або ядро, або мікрокод, але не обидва.
  2. Порівняйте «суворі» й «авто» політики: якщо треба налаштувати — робіть це по пулах вузлів, а не глобально.
  3. Віддавайте перевагу структурним виправленням: виділені вузли для чутливих навантажень, зменшення переходів у ядро, уникнення моделей з великою кількістю churn потоків, прив’язка критичних процесів.

Якщо ви не можете відповісти на питання «який перехід сповільнився?» (user→kernel, VM→host, thread→thread), ви не діагностуєте — ви ведете переговори з фізикою.

Три міні-історії з корпоративного життя

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

Середня SaaS-компанія мала змішаний флот: новіші сервери для баз даних, старі вузли для батчу і великий Kubernetes-кластер для «усіх інших». Після спринту з безпеки вони увімкнули суворіший профіль мітгацій по всьому пулу Kubernetes. Здавалось чисто в конфігураційному менеджменті: одна настройка, одне розгортання, один зелений чек.

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

Неправильне припущення було простим: вони думали, що всі вузли в пулі мають однакову поведінку CPU. Насправді пул містив два покоління CPU. На одному поколінні режим мітгацій опирався на дорожчі переходи, і API-навантжування виявилось syscall-важким через бібліотеку логування й налаштування TLS, що збільшувало переходи в ядро. На новішому поколінні ті ж налаштування були значно дешевші.

Вони виявили проблему лише після порівняння виводів /sys/devices/system/cpu/vulnerabilities/spectre_v2 між вузлами й помітили різні рядки мітгацій на «однакових» вузлах. Ревізії мікрокоду теж були нерівномірні, бо деякі сервери мали мікрокод від ОС, інші покладалися на BIOS-оновлення, які ніколи не були заплановані.

Виправленням не стало «вимкнути мітгації». Вони розділили пул вузлів за моделлю CPU і базовою лінією мікрокоду, а потім перебалансували навантаження: syscall-важкі API-поди перемістили на новий пул. Вони також додали перевірку відповідності мікрокоду в admission процесі вузла.

Урок: коли ваш ризик і продуктивність залежать від мікроархітектури, однорідні пули — це не розкіш. Це контроль.

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

Фінтех-команда гналася за хвостовою латентністю у сервісі ціноутворення. Вони зробили все очікуване: закріпили потоки, налаштували черги NIC, зменшили алокації й вивели гарячі шляхи з ядра, де можливо. Потім вони пішли далі. Вимкнули SMT з теорії, що менше спільних ресурсів зменшить джиттер. Трохи допомогло.

Заохочені, вони дозволили собі послабити деякі налаштування мітгацій у виділеному середовищі. Система була «одноорендною», зрештою. Продуктивність покращилась у синтетичних бенчмарках, і вони відчули себе розумними. Розгорнули це у продакшен з нотаткою про прийняття ризику.

Два місяці потому окремий проєкт повторно використав той самий образ хоста для запуску CI-завдань для внутрішніх репозиторіїв. «Внутрішній» швидко став «напівдовіреним», бо підрядники і зовнішні залежності були залучені. CI-навантження були шумними, JIT-важкими і неприємно близькими до процесу ціноутворення за плануванням. Нічого не було експлуатовано (наскільки відомо), але перегляд з безпеки виявив невідповідність: образ хоста ґрунтувався на моделі загроз, яка вже не була справедливою.

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

Виправлення було нудним і дорогим: окремі пули хостів і образи. Ціноутворення працювало на строгих, виділених вузлах. CI — в іншому місці з сильнішою ізоляцією і іншими очікуваннями продуктивності. Вони також почали трактувати налаштування мітгацій як частину «API» між платформою і командами застосунків.

Урок: оптимізації, що змінюють безпекову позицію, мають властивість повторно використовуватись не у контексті. Образи поширюються. Так само поширюється ризик.

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

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

Після 2018 року вони зробили болісно нецікаву річ: побудували конвеєр інвентаризації. Кожен хост відмічав модель CPU, ревізію мікрокоду, версію ядра, параметри завантаження і вміст /sys/devices/system/cpu/vulnerabilities/*. Ці дані живили дашборд і рушій політик. Вузли, що відхилялися від відповідності, автоматично кордонилися в Kubernetes або дренувалися у планувальнику VM.

Через роки нове оновлення мікрокоду внесло помітну зміну продуктивності на підмножині хостів. Оскільки у них була інвентаризація і канари, вони помітили це за кілька годин. Оскільки у них були класи хостів, радіус ураження був обмежений. Оскільки був шлях відкату, вони відновилися до того, як вплив на клієнтів став заголовком новин.

Аудиторський слід теж мав значення. Безпека спитала: «Які вузли все ще вразливі в цьому режимі?» Вони відповіли запитом, а не зустріччю.

Урок: протилежність сюрпризу — не передбачення. Це спостережуваність плюс контроль.

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

1) Симптом: «CPU високе після патчу»

  • Корінь: Більше часу витрачається на переходи в ядро (PTI/KPTI, бар’єри спекуляції), часто підсилено навантаженням з великою кількістю syscalls.
  • Виправлення: Виміряйте vmstat (sy, cs), зменшіть частоту syscalls (пакування, асинхронний I/O), оновіть на CPU з дешевшими мітгаціями або ізолюйте навантаження в відповідний клас вузлів.

2) Симптом: «Хвостова латентність зросла, середнє виглядає нормальним»

  • Корінь: Умовні мітгації на межах переключення контексту взаємодіють з планувальником; контенція SMT-сусідів; шумні сусіди.
  • Виправлення: Вимкніть SMT для чутливих пулів, закріпіть критичні потоки, зменшіть міграції і відокремте шумні навантаження. Перевірте за допомогою perf stat і метрик планувальника.

3) Симптом: «Деякі вузли швидкі, деякі повільні, той самий образ»

  • Корінь: Дрейф мікрокоду і змішані stepping-и CPU; ядро обирає різні шляхи мітгацій.
  • Виправлення: Забезпечте базові лінії мікрокоду, сегментуйте пули за моделлю/stepping-ом CPU і зробіть стан мітгацій частиною готовності вузла.

4) Симптом: «Скан безпеки каже вразливий, але ми запатчили»

  • Корінь: Патч застосований лише на рівні ОС; відсутня прошивка/мікрокод; або мітгації відключені через прапорці завантаження.
  • Виправлення: Перевіряйте через /sys/devices/system/cpu/vulnerabilities/* і ревізію мікрокоду; усувайте за допомогою пакетів мікрокоду або BIOS-оновлень; видаліть ризиковані прапорці завантаження.

5) Симптом: «VM-навантаження стали повільнішими, bare metal — ні»

  • Корінь: Підвищені витрати на вхід/вихід VM через гачки мітгацій; гіпервізор застосовує суворіші бар’єри.
  • Виправлення: Виміряйте накладні витрати віртуалізації; розгляньте виділені хости, нові покоління CPU або налаштування щільності VM. Уникайте глобального відключення мітгацій на гіпервізорах.

6) Симптом: «Ми вимкнули мітгації і нічого поганого не сталося»

  • Корінь: Плутання відсутності доказів з доказом відсутності; модель загроз тихо змінилася пізніше (нові навантаження, нові орендарі, нові рантайми).
  • Виправлення: Трактуйте зміни мітгацій як безпеково-чутливий API. Вимагайте явної політики, гарантій ізоляції і періодичної перевалідації моделі загроз.

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

Покроково: побудуйте позицію щодо класу Spectre, з якою можна жити

  1. Класифікуйте пули вузлів за межею довіри. Спільні мультиорендні, внутрішні спільні, виділені чутливі, виділені загальні.
  2. Зробіть інвентаризацію CPU і мікрокоду. Збирайте lscpu, ревізію мікрокоду з dmesg і версію ядра з uname -r.
  3. Зробіть інвентаризацію стану мітгацій. Збирайте /sys/devices/system/cpu/vulnerabilities/* по вузлу і зберігайте централізовано.
  4. Визначте профілі мітгацій. Для кожного пулу вкажіть прапорці завантаження ядра (наприклад, mitigations=auto, опційно nosmt) і потрібну базу мікрокоду.
  5. Зробіть відповідність виконуваною. Вузли, що не відповідають профілю, не повинні приймати чутливі навантаження (cordon/drain, taints у планувальнику, або обмеження розміщення VM).
  6. Канаруйте кожне оновлення ядра/мікрокоду. Один клас хостів за раз; порівнюйте латентність, пропускну здатність і лічильники CPU.
  7. Бенчмарьте з реальними формами трафіку. Синтетичні мікробенчмарки пропускають патерни syscalls, поведінку кеша і хвилювання алокатора.
  8. Документуйте прийняття ризику з датою завершення. Якщо ви вимикаєте щось, поставте термін дії і примусовий повторний перегляд.
  9. Навчіть реагувальників інцидентів. Додайте «Плейбук швидкої діагностики» до вашого runbook для on-call і проводьте тренування.
  10. Плануйте оновлення апаратури з думкою про безпеку. Нові CPU можуть зменшити податок мітгацій; це бізнес-кейс, не якась гарна річ.

Чекліст: перед тим як відключити SMT

  • Підтвердіть, чи ядро повідомляє «SMT vulnerable» для релевантних проблем.
  • Виміряйте різницю продуктивності на репрезентативних навантаженнях.
  • Приймайте рішення по пулах, а не по хостах.
  • Забезпечте резерв потужності для падіння пропускної здатності.
  • Оновіть правила планування, щоб чутливі навантаження потрапляли в потрібний пул.

Чекліст: перед тим як ослабити мітгації заради продуктивності

  • Чи дійсно система одноорендна наскрізь?
  • Чи може запускатися недовірений код (CI-завдання, плагіни, браузери, JIT-рантайми, скрипти клієнтів)?
  • Чи доступний хост локально для потенційних атакувальників?
  • Чи маєте ви виділене обладнання й сильний контроль доступу?
  • Чи є у вас шлях відкату, який не потребує героя?

FAQ

1) Чи закінчилися сюрпризи класу Spectre?

Ні. Початковий шок минув, але базова динаміка лишається: можливості продуктивності створюють спільний стан, спільний стан витікає. Очікуйте подальших досліджень і періодичних оновлень мітгацій.

2) Якщо моє ядро каже «Mitigation: …», чи я в безпеці?

Ви в безпеці більше, ніж при статусі «Vulnerable», але «в безпеці» залежить від вашої моделі загроз. Звертайте увагу на фрази на кшталт «SMT vulnerable» і «Host state unknown». Це ядро, що каже вам про залишковий ризик.

3) Чи варто відключити SMT скрізь?

Ні. Вимикайте SMT там, де є ризик крос-орендності або запуску недовіреного коду і де ядро вказує на експозицію, пов’язану зі SMT. Залишайте SMT там, де апаратні гарантії і довіра навантаження виправдовують це і де ви виміряли вигоду.

4) Чи це в основному проблема хмари?

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

5) Який найпоширеніший операційний режим провалу?

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

6) Чи можу я покладатися на ізоляцію контейнерів?

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

7) Чому мітгації іноді більше шкодять латентності, ніж пропускній здатності?

Тому що багато мітгацій оподатковують переходи (перемикання контексту, syscalls, виходи з VM). Хвостова латентність чутлива до додаткової роботи на критичному шляху і взаємодій з планувальником.

8) Що зберігати в моєму CMDB або системі інвентаризації?

Модель/stepping CPU, ревізію мікрокоду, версію ядра, параметри завантаження, стан SMT і вміст /sys/devices/system/cpu/vulnerabilities/*. Цей набір дозволяє швидко відповісти на більшість аудиторських і інцидентних питань.

9) Чи «імунні» нові CPU?

Ні. Нові CPU часто мають кращу підтримку мітгацій і можуть зменшити вартість продуктивності, але «імунітет» — надто сильне слово. Безпека — рухома ціль, а нові можливості можуть вводити нові шляхи витоку.

10) Якщо продуктивність критична, який найкращий довгостроковий крок?

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

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

Якщо хочете менше сюрпризів — не прагніть ідеального передбачення. Прагніть швидкої верифікації і контрольованого розгортання.

  1. Реалізуйте безперервний аудит мітгацій шляхом скрапінгу /sys/devices/system/cpu/vulnerabilities/*, /proc/cmdline і ревізії мікрокоду у ваш pipeline метрик.
  2. Розділіть пули за поколінням CPU і базовою лінією мікрокоду. Однорідність — це функція продуктивності й контроль безпеки.
  3. Створіть два–три профілі мітгацій узгоджені з межами довіри і забезпечте їх через автоматизацію (мітки вузлів, taints, правила розміщення).
  4. Побудуйте канарний процес для оновлень ядра і мікрокоду з реальними бенчмарками навантаження і відстеженням хвостової латентності.
  5. Вирішіть вашу позицію щодо SMT явно для кожного пулу, зафіксуйте її й робіть дрейф виявлюваним.

Ера Spectre не закінчилась. Вона дозріла. Команди, що ставлять безпеку CPU як до будь-якої іншої продакшен-проблеми — інвентаризація, канари, спостережуваність і нудні контролі — сплять спокійно.

VoIP через VPN: припиніть роботизований звук — MTU, джиттер і основи QoS

Ви знаєте цей звук. Дзвінок починається нормально, а потім хтось наче факс, що проходить відбір на роль робота в кіно.
Усі звинувачують «VPN», потім провайдера, потім софтфон, потім фазу Місяця. Насправді справжній винуватець зазвичай прозаїчний:
невідповідність MTU/MSS, джиттер через bufferbloat або QoS, який не переживає подорож через тунель.

Я керую продукційними мережами, де голос — це просто ще навантаження, поки раптом ним не стане. Голос карає за ліниві припущення.
Йому байдуже, що тест пропускної здатності виглядає чудово; важливіше, щоб невеликі пакети приходили вчасно, послідовно, з мінімальними втратами і перестановками.

Модель мислення, яка реально передбачає відмови

Якщо запам’ятати одне: голос — це потік реального часу на мережі з best-effort. VPN додає заголовки, приховує внутрішні DSCP-маркування, якщо ви навмисно не зберігаєте їх, і може змінювати ритміку пакетів.
Звичні режими відмов не містять містики. Це фізика й черги.

Що зазвичай означає «роботизований звук»

«Робот» рідко є проблемою кодека. Це в дії втрати пакетів і маскування.
RTP-аудіо приходить маленькими пакетами (часто по 20 мс аудіо в пакеті). Втратите кілька, джиттер підстрибує, jitter buffer розтягується, декодер домислює, і ви чуєте робота.
Голос може пережити деякі втрати; просто він не приховує це чемно.

Стек VoIP поверх VPN в одній концепції

Уявіть пакет як набір вкладених конвертів:

  • Внутрішній: SIP сигналізація + RTP медіа (зазвичай UDP) з DSCP-маркуванням, яке ви хочете зберегти
  • Потім: ваша VPN-обгортка (WireGuard/IPsec/OpenVPN) додає оверхед і може змінити MTU
  • Зовнішній: черги ISP та інтернету (де живе bufferbloat) і де QoS може працювати або ігноруватися
  • Кінці: софтфон або IP-телефон, та PBX/ITSP

Поломка зазвичай відбувається в одному з трьох місць:
(1) розмір (MTU/фрагментація),
(2) час (джиттер/черги),
(3) пріоритет (QoS/DSCP і шейпінг).

Перефразовуючи W. Edwards Deming: «Без даних ви — просто ще одна людина з думкою.» Ставтеся до проблем голосу як до інцидентів: вимірюйте, ізолюйте, змінюйте одну змінну, знову вимірюйте.

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

Коли CEO каже «дзвінки не працюють», не починайте з дебатів про кодеки. Почніть з звуження охоплення проблеми та локалізації черги.
Ось порядок, який швидко знаходить корені.

Спочатку: підтвердіть, чи це втрата, джиттер чи MTU

  1. Перевірте статистику RTP у клієнті/PBX: відсоток втрат, джиттер, пізні пакети. Якщо цього немає, захопіть пакети і обчисліть (пізніше).
    Якщо ви бачите навіть 1–2% втрат під час «робота», розглядайте це як мережеву проблему, поки не доведено протилежне.
  2. Швидко виконайте тест PMTUD через VPN. Якщо PMTUD зламаний, великі пакети можуть глушитися, особливо на UDP-основних VPN.
  3. Перевірте затримку в чергах під навантаженням на найвужчому аплінку (зазвичай це завантаження користувача). Bufferbloat — тихий вбивця голосу.

По-друге: ізолюйте місце поломки

  1. Обійдіть VPN для одного тестового дзвінка (split tunnel або тимчасова політика). Якщо голос значно покращується, зосередьтеся на накладних витратах тунелю, MTU та обробці QoS на краях тунелю.
  2. Порівняйте провідне підключення та Wi‑Fi. Якщо Wi‑Fi гірший, ви в зоні конкуренції ефірного часу та повторних передач. Виправляйте це окремо.
  3. Тестуйте з відомо-робочої мережі (лабораторний канал, інший ISP або VM у хмарі з софтфоном). Якщо там чисто, проблема на краю користувача.

По-третє: застосуйте «нудні виправлення»

  • Встановіть MTU інтерфейсу VPN явно і обмежте TCP MSS там, де це релевантно.
  • Застосуйте smart queue management (fq_codel/cake) там, де реальний вузький місце, і трохи шейпте під лінійну швидкість.
  • Відмітьте голосовий трафік і пріоритезуйте його де ви контролюєте чергу (зазвичай WAN-край), а не лише в мріях.

Жарт №1: VPN — як валіза: якщо в неї продовжувати пхати додаткові заголовки, зрештою блискавка (MTU) здасться в найгірший момент.

MTU, MSS і фрагментація: чому «роботизований» часто означає «дрібні втрати»

Проблеми з MTU не завжди виглядають як «не можу підключитися». Вони можуть виглядати як «підключається, але іноді звучить як примара».
Це тому, що сигналізація може виживати, тоді як деякі медіа-пакети або re-INVITE-и втрачаються, або фрагментація підвищує чутливість до втрат.

Що змінюється при додаванні VPN

Кожний тунель додає оверхед:

  • WireGuard додає зовнішній UDP/IP-заголовок плюс власний оверхед.
  • IPsec додає ESP/AH оверхед (плюс можливу UDP-інкапсуляцію для NAT‑T).
  • OpenVPN додає оверхед в простір користувача і може додавати кадрування в залежності від режиму.

Внутрішній пакет, який нормально поміщався при MTU 1500, може вже не проходити. Якщо шлях не підтримує фрагментацію так, як ви вважаєте, щось відкидається.
А UDP не відправляє повторно; він просто підводить вас у реальному часі.

Path MTU discovery (PMTUD) і як воно ламається

PMTUD покладається на ICMP «Fragmentation Needed» (для IPv4) або Packet Too Big (для IPv6). Багато мереж блокують або лімітують ICMP.
Результат: ви надсилаєте пакети, що надто великі, роутери їх відкидають, а відправник не дізнається. Це називається «PMTUD black hole».

Чому RTP зазвичай не «занадто великий» — але все одно страждає

RTP-голосові пакети зазвичай маленькі: від декількох десятків до кількох сотень байт корисного навантаження, плюс заголовки. Чому ж тоді MTU впливає на дзвінки?

  • Сигналізація і зміни сесії (SIP INVITE/200 OK з SDP, TLS записи) можуть ставати великими.
  • Інкапсуляція VPN може фрагментувати навіть помірні пакети, підвищуючи ймовірність втрат.
  • Джиттер-піки виникають, коли фрагментація і збирання взаємодіють із перевантаженими чергами.
  • Деякі софтфони групують або відправляють більші UDP-пакети за певних налаштувань (comfort noise, SRTP або незвичний ptime).

Практичні рекомендації

  • Для WireGuard почніть з MTU 1420, якщо не впевнені. Це не магія; це консервативне значення, що уникає поширених проблем з оверхедом.
  • Для OpenVPN будьте явними з MTU тунелю і клацанням MSS для TCP-потоків, що йдуть через тунель.
  • Не «знижуйте MTU скрізь» без розбору. Ви можете виправити один шлях і нашкодити іншому. Вимірюйте, а потім встановлюйте.

Джиттер, bufferbloat і чому speed test обманює

Ви можете мати 500 Mbps в завантаженні і при цьому звучати так, ніби дзвоните з підводного човна. Голос потребує низької варіації затримки, а не рекордів пропускної здатності.
Найбільший практичний ворог — bufferbloat: надмірні черги в роутерах/модемах, що наростають під навантаженням і додають сотні мілісекунд затримки.

Джиттер vs затримка vs втрата пакетів

  • Затримка: скільки часу пакет йде від кінця до кінця.
  • Джиттер: наскільки ця затримка змінюється пакет за пакетом.
  • Втрата: пакети, що ніколи не приходять (або приходять занадто пізно, щоб мати значення).

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

Де народжується джиттер

Більшість джиттера в інцидентах VoIP-over-VPN — не «інтернет», а черга на краю:

  • домашній роутер користувача з глибокою upstream-чергою
  • офісний фаєрвол гілки, що робить інспекцію і буферизацію
  • насичення CPU VPN-концентратора, що спричиняє затримки планування пакетів
  • конкуренція Wi‑Fi/повторні передачі (виглядає як джиттер і втрата)

Менеджмент черг, який реально працює

Якщо ви контролюєте вузьке місце, ви можете виправити голос.
Smart queue management (SQM) алгоритми як fq_codel та cake активно заважають чергам рости без меж і утримують стабільну затримку під навантаженням.

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

Жарт №2: Bufferbloat — це коли ваш роутер збирає пакети, ніби вони колекційні антикварні речі.

Основи QoS/DSCP для голосу через VPN (і що часто зривається)

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

DSCP і міф про «end-to-end QoS»

Голос часто маркує RTP як DSCP EF (Expedited Forwarding), а SIP як CS3/AF31 залежно від політики.
Всередині LAN це може допомогти. Через інтернет більшість провайдерів ігнорують це. Через VPN маркування може навіть не зберегтися після інкапсуляції.

Що ви можете контролювати

  • Край LAN: пріоритезуйте голос від телефонів/софтфонів до вашого VPN-шлюзу.
  • WAN-egress VPN-шлюзу: шейпте і пріоритезуйте зовнішні пакети, що відповідають голосовим потокам.
  • Гілка/край користувача: якщо ви ним керуєте, розгорніть SQM і маркуйте голос локально.

VPN-специфіка: внутрішні vs зовнішні маркування

Багато імплементацій тунелів інкапсулюють внутрішні пакети у зовнішні. Зовнішній пакет пересилається провайдером.
Якщо зовнішній пакет не маркований (або маркування знімається), ваше внутрішнє «EF» — лише декоративний напис.

Практичний підхід:

  • Класифікуйте голос до шифрування коли можливо, а потім застосуйте пріоритет до зашифрованого потоку (зовнішній заголовок) на egress.
  • Зберігайте DSCP через тунель, якщо ваше обладнання і політика це підтримують.
  • Не покладайтеся на Wi‑Fi WMM, якщо ваша uplink-черга плавиться.

Обережність з QoS: можна зробити гірше

Погана політика QoS може позбавити трафіку керування або створити мікросплески і перестановки. Голос любить пріоритет, але також любить стабільність.
Тримайте класи простими: голос, інтерактив, об’ємний. Потім шейпте.

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

Це завдання «запустіть зараз». Кожне містить команду, що каже вивід, і яке рішення ухвалити.
Використовуйте їх на Linux-ендпойнтах, VPN-шлюзах або хостах для трасування. Налаштуйте імена інтерфейсів і IP під своє оточення.

Завдання 1: Підтвердіть MTU інтерфейсу тунелю VPN

cr0x@server:~$ ip link show dev wg0
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/none

Значення: MTU інтерфейсу wg0 — 1420. Добра консервативна відправна точка для WireGuard.
Рішення: Якщо MTU на тунелі 1500, вважайте це потенційною проблемою, якщо ви не довели, що шлях це підтримує. Якщо роботизований звук корелює з певними шляхами, тестуйте нижчий MTU.

Завдання 2: Виміряйте path MTU за допомогою ping з «do not fragment» (IPv4)

cr0x@server:~$ ping -M do -s 1372 -c 3 10.20.30.40
PING 10.20.30.40 (10.20.30.40) 1372(1400) bytes of data.
1380 bytes from 10.20.30.40: icmp_seq=1 ttl=63 time=18.4 ms
1380 bytes from 10.20.30.40: icmp_seq=2 ttl=63 time=18.7 ms
1380 bytes from 10.20.30.40: icmp_seq=3 ttl=63 time=18.2 ms

--- 10.20.30.40 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms

Значення: 1400-байтовий пакет (включно з заголовками) проходить без фрагментації.
Рішення: Збільшуйте -s поки не впаде, щоб знайти максимум. Встановіть MTU тунелю безпечно нижче від знайденого мінус оверхед інкапсуляції.

Завдання 3: Спостерігайте симптоми PMTUD-відмови (IPv4)

cr0x@server:~$ ping -M do -s 1472 -c 2 10.20.30.40
PING 10.20.30.40 (10.20.30.40) 1472(1500) bytes of data.
ping: local error: message too long, mtu=1420
ping: local error: message too long, mtu=1420

--- 10.20.30.40 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1013ms

Значення: Локальний MTU інтерфейсу — 1420; ядро відмовляється відправляти більші пакети з DF-встановленим.
Рішення: Якщо додатки все одно відправляють більші пакети (UDP-інкапсуляція VPN може), обмежте або налаштуйте MTU/MSS так, щоб вони не робили цього.

Завдання 4: Перевірте правила TCP MSS clamping (iptables)

cr0x@server:~$ sudo iptables -t mangle -S | grep -i mss
-A FORWARD -o wg0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Значення: TCP SYN-пакети мають MSS, підкоригований за PMTU.
Рішення: Якщо ви прямуєте SIP через TCP/TLS через VPN і бачите застої або повторні передачі, увімкніть це. Це не виправить RTP (UDP), але стабілізує сигналізацію.

Завдання 5: Перевірте DSCP-маркування на вихідних пакетах

cr0x@server:~$ sudo tcpdump -ni eth0 -vv udp and portrange 10000-20000 -c 5
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:10:41.112233 IP (tos 0xb8, ttl 63, id 44211, offset 0, flags [DF], proto UDP (17), length 214) 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
12:10:41.132244 IP (tos 0xb8, ttl 63, id 44212, offset 0, flags [DF], proto UDP (17), length 214) 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186

Значення: TOS 0xb8 відповідає DSCP EF (46). Ваш хост маркує RTP.
Рішення: Далі перевірте, чи маркування переживає інкапсуляцію і чи ваша WAN-черга його враховує. Якщо воно зникає на зовнішньому пакеті, вам потрібен QoS на egress тунелю, а не надії та мрії.

Завдання 6: Підтвердіть DSCP на зовнішньому пакеті VPN

cr0x@server:~$ sudo tcpdump -ni eth0 -vv udp port 51820 -c 5
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:12:03.220011 IP (tos 0x00, ttl 64, id 12001, offset 0, flags [DF], proto UDP (17), length 208) 203.0.113.5.51820 > 203.0.113.9.51820: UDP, length 180
12:12:03.240022 IP (tos 0x00, ttl 64, id 12002, offset 0, flags [DF], proto UDP (17), length 208) 203.0.113.5.51820 > 203.0.113.9.51820: UDP, length 180

Значення: Зовнішні пакети немарковані (tos 0x00). Навіть якщо внутрішній RTP — EF, провайдер бачить тільки зовнішній.
Рішення: Застосуйте класифікацію на VPN-шлюзі: ідентифікуйте голосові потоки до шифрування (або за портом/параметрами peer) і встановіть DSCP/пріоритет на egress.

Завдання 7: Визначте реальне вузьке місце і поточний qdisc

cr0x@server:~$ tc qdisc show dev eth0
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn

Значення: fq_codel активний. Це пристойна відправна точка для низької затримки під навантаженням.
Рішення: Якщо ви бачите pfifo_fast або глибокий вендорський qdisc на WAN-краю, плануйте розгорнути шейпінг + fq_codel/cake там, де відбувається перевантаження.

Завдання 8: Перевірте статистику qdisc на наявність drop/overlimits (проблеми шейпінга)

cr0x@server:~$ tc -s qdisc show dev eth0
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn
 Sent 98234123 bytes 84521 pkt (dropped 213, overlimits 0 requeues 12)
 backlog 0b 0p requeues 12
  maxpacket 1514 drop_overlimit 213 new_flow_count 541 ecn_mark 0

Значення: Сталися деякі дропи. Дропи не завжди погані — контрольовані дропи можуть запобігти величезній затримці. Але дропи + роботизований звук вказують, що ви втрачаєте RTP, а не bulk-трафік.
Рішення: Додайте класифікацію, щоб голос отримував пріоритет (або принаймні ізоляцію), і переконайтеся, що швидкість шейпінга відповідає реальному аплінку.

Завдання 9: Швидка перевірка джиттера і втрат з mtr (базовий рівень)

cr0x@server:~$ mtr -rwzc 50 203.0.113.9
Start: 2025-12-28T12:20:00+0000
HOST: server                          Loss%   Snt   Last   Avg  Best  Wrst StDev
  1. 192.0.2.1                         0.0%    50    1.1   1.3   0.9   3.8   0.6
  2. 198.51.100.1                      0.0%    50    8.2   8.5   7.9  13.4   1.1
  3. 203.0.113.9                       0.0%    50   19.0  19.2  18.6  26.8   1.4

Значення: Немає втрат, стабільна затримка, низький джиттер (StDev). Добрий базовий рівень.
Рішення: Якщо ви бачите втрати на хопі 1 під навантаженням, це ваш LAN/Wi‑Fi/роутер. Якщо втрати починаються пізніше, це апстрім — але все ще може бути виправлено шейпінгом на вашому краї.

Завдання 10: Подивіться, чи CPU VPN-шлюзу спричиняє затримки планування пакетів

cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.5.0 (vpn-gw) 	12/28/2025 	_x86_64_	(8 CPU)

12:21:01     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
12:21:02     all   18.20    0.00   22.40    0.10    0.00   21.70    0.00    0.00    0.00   37.60
12:21:02       0   20.00    0.00   28.00    0.00    0.00   30.00    0.00    0.00    0.00   22.00

Значення: Високий softirq може вказувати на інтенсивну обробку пакетів (шифрування, форвард).
Рішення: Якщо softirq завантажено під час проблем з дзвінками, розгляньте multiqueue, перехід на швидший крипто-стек, додавання CPU-голівки або зменшення оверхеду VPN (MTU і offloads).

Завдання 11: Перевірте NIC offloads (можуть псувати захоплення, іноді — таймінг)

cr0x@server:~$ sudo ethtool -k eth0 | egrep 'tso|gso|gro'
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on

Значення: Offloads увімкнені. Зазвичай це нормально, але може сплутати захоплення пакетів і в деяких краях взаємодіяти з тунелями.
Рішення: Для точної діагностики тимчасово вимкніть GRO/LRO на тестовому хості, потім повторіть тест. Не відключайте offloads на навантажених шлюзах без плану.

Завдання 12: Перевірте помилки прийому UDP і дропи

cr0x@server:~$ netstat -su
Udp:
    128934 packets received
    12 packets to unknown port received
    0 packet receive errors
    4311 packets sent
UdpLite:
IpExt:
    InOctets: 221009331
    OutOctets: 198887112

Значення: Помилок прийому UDP немає. Добре.
Рішення: Якщо під час дзвінків кількість помилок зростає, можливо, ви досягаєте меж сокетних буферів або ядро відкидає; налаштуйте буфери, вирішіть насичення CPU або зменшіть конкуренцію трафіку.

Завдання 13: Підтвердіть частоту SIP/RTP пакетів під час дзвінка (саніті-чек)

cr0x@server:~$ sudo tcpdump -ni any udp portrange 10000-20000 -ttt -c 10
tcpdump: listening on any, link-type LINUX_SLL2, snapshot length 262144 bytes
 0.000000 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.019884 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.020042 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.019901 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186

Значення: Інтер-пакетні інтервали близько 20 ms вказують на ptime=20ms (поширено). Великі паузи свідчать про джиттер або затримки планування.
Рішення: Якщо таймінг нерегулярний біля точки захоплення, перевіряйте CPU/ Wi‑Fi відправника. Якщо він регулярний на відправнику, але нерегулярний на приймачі, проблема в мережі/чергах.

Завдання 14: Визначте, чи йде трафік через VPN чи обходить його

cr0x@server:~$ ip route get 198.51.100.20
198.51.100.20 via 10.10.0.1 dev wg0 src 10.10.0.2 uid 1000
    cache

Значення: Маршрут до кінцевої точки медіа проходить через wg0.
Рішення: Якщо ваш тест «обійти VPN» все одно йде через wg0, ви нічого не обійшли. Виправте політику маршрутизації/split tunnel, потім порівняйте якість дзвінків.

Завдання 15: Підтвердіть MTU на фізичному WAN-інтерфейсі (і помітте невідповідність jumbo)

cr0x@server:~$ ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff

Значення: WAN-інтерфейс стандартно має MTU 1500.
Рішення: Якщо ви на PPPoE або певних cellular-лінках, WAN MTU може бути меншим (1492, 1428 тощо). Це штовхає до зниження MTU тунелю.

Завдання 16: Виявіть bufferbloat під навантаженням простим ping під час насичення uplink

cr0x@server:~$ ping -i 0.2 -c 20 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=57 time=18.9 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=57 time=210.4 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=57 time=245.7 ms
64 bytes from 1.1.1.1: icmp_seq=4 ttl=57 time=198.1 ms

--- 1.1.1.1 ping statistics ---
20 packets transmitted, 20 received, 0% packet loss, time 3812ms
rtt min/avg/max/mdev = 18.4/156.2/265.1/72.9 ms

Значення: Затримка різко зростає під навантаженням: класичний bufferbloat.
Рішення: Розгорніть SQM шейпінг на uplink і пріоритезуйте голос; не марнуйте час на гоніння за кодеками.

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

Інцидент №1: Неправильне припущення (MTU «не може бути, у нас 1500 скрізь»)

Середня компанія перевела контакт-центр на софтфони через full-tunnel VPN. У пілоті все працювало. Потім розгорнули кілька сотень віддалених агентів.
Через день черга helpdesk стала другим контакт-центром — але з гіршою якістю голосу.

Перше припущення мережевої команди було класичним: «MTU не може бути, Ethernet 1500, VPN налаштований чисто».
Вони зосередилися на SIP-провайдері, потім звинувачували домашній Wi‑Fi, потім міняли кодеки.
Дзвінки випадково покращувалися, що найгірше, бо це заохочувало забобони.

Успадкована закономірність, що зламала справу: роботизований звук сплескував під час певних сценаріїв — трансфери, консультації та коли софтфон погоджував SRTP заново.
Саме тоді сигналізаційні пакети ставали більшими, і в деяких сценаріях шлях тунелю вимагав фрагментації. ICMP «fragmentation needed» блокувався на боці користувача через «безпекове» налаштування.
PMTUD black holes. Не гламурно. Дуже реально.

Виправлення було нудним і рішучим: встановили консервативний MTU тунелю, клацнули MSS для TCP сигналізації і задокументували «не блокувати весь ICMP» у стандарті віддаленого доступу.
Також додали односторінковий тест: DF ping через тунель до відомої кінцевої точки. Це ловило регресії пізніше.

Урок: «1500 скрізь» — не дизайн. Це бажання.

Інцидент №2: Оптимізація, що обернулась проти (пріоритизація голосу… шляхом прискорення всього)

Інша організація мала спроможний VPN-шлюз і хотіла «преміум-якість голосу». Хтось увімкнув апаратне прискорення і fast-path фічі на крайовому фаєрволі.
Пропускна здатність зросла. Затримка в синтетичних тестах впала. Усі святкували.

Через два тижні — скарги: «Робот лише під час великих завантажень файлів». Ця деталь важлива.
Під навантаженням fast path обходив частини стека QoS і менеджменту черг. Bulk-трафік і голос опинялися в одній глибокій черзі на стороні WAN.
Прискорення покращило пікову пропускну здатність, але прибрало механізм, що підтримував стабільну затримку.

Інженери зробили те, що інженери зазвичай роблять: додали більше правил QoS. Більше класів. Більше match-висловів. Стало гірше.
Класифікація навантажувала CPU на slow path, у той час як fast path все одно відправляв більшу частину трафіку в ту саму чергу-горло.
Тепер у них була складність і все ще bufferbloat.

Остаточне виправлення не було «більше QoS». Воно полягало в: шейпити uplink трохи нижче реальної пропускної здатності, увімкнути сучасний qdisc і зберегти модель класів простою.
Потім вирішити, чи сумісне прискорення з цією політикою. Там, де ні — голос переміг.

Урок: оптимізація для throughput без врахування поведінки черг — це як побудувати швидший шлях звучати жахливо.

Інцидент №3: Нудна практика, що врятувала день (стандартні тести + контроль змін)

Глобальна компанія передавала голос через IPsec між філіями та HQ. Нічого надзвичайного. Ключова різниця: вони ставилися до голосу як до production-сервісу.
Кожна зміна мережі мала pre-flight і post-flight чекліст, включно з кількома VoIP-орієнтованими тестами.

Одної п’ятниці ISP поміняв обладнання доступу в регіональному офісі. Користувачі помітили «легкий робот» у дзвінках.
Локальна команда виконала стандартні тести: ping idle vs ping під навантаженням uplink, DF pings для MTU і швидку перевірку DSCP на WAN-egress.
Вони не сперечалися. Вони вимірювали.

Дані показали, що PMTUD зламаний на новому доступі, а upstream-буфер став глибшим, ніж раніше. Дві проблеми. Обидві — що робити.
Вони трохи зменшили MTU тунелю, увімкнули MSS clamping і підкоригували шейпінг, щоб тримати затримку стабільною. Дзвінки стабілізувалися миттєво.

У понеділок вони підняли питання до ISP з чіткими доказами: таймстемпи, поріг відмови MTU і графіки затримки під навантаженням.
ISP пізніше виправив обробку ICMP. Але компанія не чекала, щоб повернути працездатний голос.

Урок: найефективніша функція надійності — це відтворюваний тест, який ви справді виконуєте.

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

1) Симптом: роботизований звук під час завантажень або демонстрацій екрану

  • Корінь: bufferbloat на upstream; голосові пакети застрягають за bulk-трафіком в глибокій черзі.
  • Виправлення: увімкніть SQM (fq_codel/cake) і шейпте трохи нижче uplink; додайте простий пріоритет для RTP/SIP на WAN-egress.

2) Симптом: дзвінок підключається, а потім аудіо падає або стає рваним через хвилину

  • Корінь: MTU/PMTUD black hole, спричинений рекієм, SRTP-ренеготацією або збільшенням розміру SIP re-INVITE.
  • Виправлення: явно встановіть MTU тунелю; дозвольте ICMP «frag needed»/PTB; для TCP сигналізації — clamp MSS.

3) Симптом: односпрямоване аудіо (ви чуєте їх, вони не чують вас)

  • Корінь: проблема NAT traversal або асиметрична маршрутизація; RTP прив’язаний до неправильного інтерфейсу; таймаути stateful-фаєрволу для UDP.
  • Виправлення: забезпечте правильні налаштування NAT (SIP ALG зазвичай вимкнути), підтвердіть маршрути, збільште UDP timeout на stateful-пристроях, перевірте симетричний RTP якщо підтримується.

4) Симптом: нормально на дроті, погано на Wi‑Fi

  • Корінь: конкуренція ефірного часу, повторні передачі або поведінка енергозбереження клієнта; VPN додає оверхед і чутливість до джиттера.
  • Виправлення: переведіть дзвінки на 5 GHz/6 GHz, зменшіть конкуренцію каналів, вимкніть агресивне енергозбереження для голосових пристроїв, віддавайте перевагу дроту для ролей з інтенсивними дзвінками.

5) Симптом: лише віддалені користувачі певного ISP мають проблеми

  • Корінь: upstream shaping/CGNAT від ISP, погане піринг, або фільтрація ICMP що впливає на PMTUD.
  • Виправлення: зменште MTU тунелю, забезпечте шейпінг на боці користувача якщо керуєте ним, протестуйте альтернативний транспорт/порт і збирайте докази для ескалації.

6) Симптом: «QoS увімкнено», але голос все одно деградує під навантаженням

  • Корінь: QoS налаштовано на LAN, а перевантаження на WAN; або DSCP марковано в середині, але не на зовнішніх пакетах тунелю.
  • Виправлення: пріоритезуйте там, де відбувається перевантаження: на egress; мапуйте/класифікуйте голос для зовнішніх пакетів; перевірте tcpdump і статистику qdisc.

7) Симптом: спорадичні сплески робота, особливо в години піку

  • Корінь: мікросплески і осциляція черг; CPU-конкуренція VPN-концентратора; або апстрім-конгестія.
  • Виправлення: перевірте softirq/CPU, увімкніть pacing/SQM, забезпечте запас потужності шлюзу і уникайте надмірно складних ієрархій класів.

8) Симптом: дзвінки нормальні, але «hold/resume» ламається або трансфери не проходять

  • Корінь: фрагментація SIP сигналізації або MTU-проблеми, що впливають на великі SIP-повідомлення; іноді SIP over TCP/TLS уражається MSS.
  • Виправлення: MSS clamping, зменшення MTU, дозвіл ICMP PTB і перевірка налаштувань SIP (і вимкнення SIP ALG, якщо він каламутить пакети).

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

Покроково: стабілізувати VoIP через VPN за тиждень (не за квартал)

  1. Виберіть одного репрезентативного користувача з проблемами і відтворіть проблему на вимогу (завантаження під час дзвінка зазвичай вистачає).
    Без відтворюваності — немає прогресу.
  2. Зберіть статистику голосу (з софтфону/PBX): джиттер, втрати, concealment, RTT якщо доступно.
    Вирішіть: чи це loss-driven (мережа) vs CPU-driven (ендпойнт) vs signaling-driven (SIP транспорт/MTU).
  3. Підтвердіть маршрутизацію: впевніться, що медіа справді йде через VPN, коли ви так вважаєте. Виправте плутанину зі split tunnel рано.
  4. Виміряйте path MTU через тунель використовуючи DF pings до відомих кінцевих точок.
    Вирішіть безпечний MTU (консервативний краще, ніж теоретичний).
  5. Встановіть MTU тунелю явно на обох кінцях (і задокументуйте чому).
    Уникайте «auto», якщо ви не протестували це по всіх типах доступу (домашній broadband, LTE, готельний Wi‑Fi тощо).
  6. Клацніть TCP MSS на тунелі для перенаправлених TCP-потоків (SIP/TLS, provisioning, management).
  7. Знайдіть реальне вузьке місце (зазвичай uplink). Використайте ping під навантаженням, щоб підтвердити bufferbloat.
  8. Розгорніть SQM шейпінг на вузькому місці, трохи нижче лінійної швидкості, з fq_codel або cake.
  9. Тримайте QoS класи простими і пріоритезуйте голос лише там, де це має значення: egress-черга.
  10. Перевірте обробку DSCP пакетом-захватом: внутрішнє маркування, зовнішнє маркування і чи поважає черга це.
  11. Повторно протестуйте початковий кейс (дзвінок + завантаження) і підтвердіть покращення джиттера/втрат.
  12. Розгортайте поступово з канарською групою і планом відкату. Зміни в голосі відчутні миттєво; ставтеся до них як до production-деплою.

Операційний чекліст: кожного разу, коли торкаєтесь VPN або WAN

  • Запишіть поточні MTU налаштування (WAN + тунель) та qdisc/shaping політики.
  • Запустіть DF ping MTU тест через тунель до стабільної кінцевої точки.
  • Запустіть ping idle vs ping під навантаженням, щоб виміряти регрес bufferbloat.
  • Захопіть 30 секунд RTP під час тестового дзвінка і перевірте на втрати/джиттер-сплески.
  • Підтвердіть DSCP на зовнішньому пакеті на WAN-стороні (якщо ви покладаєтесь на маркування).
  • Перевірте softirq CPU шлюзу під навантаженням.

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

  • Факт 1: RTP (Real-time Transport Protocol) був стандартизований у середині 1990-х для передачі реального часу медіа по IP-мережах.
  • Факт 2: SIP став популярним частково тому, що виглядав як HTTP для дзвінків — текстовий, розширюваний — зручний для фіч, іноді болісний для MTU.
  • Факт 3: Багато болю PMTUD походить від практик фільтрації ICMP, що стали поширені як груба відповідь на безпеку в ранньому інтернет-епоху.
  • Факт 4: Ранні VoIP-розгортання часто покладалися на DiffServ маркування всередині підприємства, але «QoS через публічний інтернет» так і не став надійним.
  • Факт 5: Хвиля прийняття VPN для віддаленої роботи спричинила скарги на голос, бо споживчі uplink-и типовно є найвужчими та найбільш bufferbloated сегментами.
  • Факт 6: WireGuard став популярним частково тому, що він легкий і швидкий, але «швидка криптографія» не скасовує «погані черги».
  • Факт 7: Bufferbloat був ідентифікований і отримав назву через те, що споживче обладнання відвантажувалося з надмірно глибокими буферами, які підвищували бенчмарки throughput, але руйнували затримкочутливі додатки.
  • Факт 8: Сучасні Linux qdisc-і як fq_codel були створені спеціально, щоб тримати затримку обмеженою під навантаженням — велика річ для голосу й ігор.
  • Факт 9: Багато корпоративних VPN-дизайнів історично припускали MTU 1500; поширення PPPoE, LTE і стекування тунелів зробило це припущення крихким.

Поширені питання

1) Чому звук «роботизований», а не просто тихий або запізнений?

Тому що ви чуєте маскування втрат пакетів. Декодер домислює відсутні аудіо-фрейми. Тихий звук зазвичай пов’язаний з гейном або проблемами пристрою; робот — зазвичай втрати/джиттер.

2) Який відсоток втрат стає чутним для VoIP?

Залежить від кодека і concealment, але навіть ≈1% втрат може бути помітним, особливо якщо вони сгруповані. Стабільні 0.1% можуть бути прийнятні; 2% у сплесках часто неприйнятні.

3) Чи MTU — це лише TCP-проблема? RTP ж UDP.

MTU шкодить і UDP. Якщо пакети перевищують path MTU і PMTUD зламаний, їх відкидають. Також фрагментація підвищує чутливість до втрат і джиттера, коли фрагменти конкурують в чергах.

4) Чи просто встановити MTU на 1200 і забути?

Ні. Ви зменшите ефективність і можете необґрунтовано зіпсувати інші протоколи або шляхи. Вимірюйте path MTU, оберіть безпечне значення і задокументуйте його. Консервативно, але не екстремально.

5) Чи DSCP-маркування допомагає через публічний інтернет?

Іноді всередині домену ISP, часто — ні end-to-end. Надійний виграш — пріоритизація там, де ви контролюєте чергу: ваш WAN-egress і керовані краї.

6) Чи QoS виправить втрати пакетів від поганого ISP?

QoS не створить додаткову пропускну здатність. Воно може запобігти self-inflicted втратам/джиттеру, керуючи власними чергами. Якщо провайдер дійсно втрачає upstream, потрібен кращий маршрут або інший провайдер.

7) Чому це ламається тільки коли хтось завантажує файл?

Upload насичує upstream. Upstream-черги надуваються, затримка і джиттер вибухають, і RTP приходить запізно. Speed tests рідко це показують, бо вони винагороджують глибокі буфери.

8) Чи WireGuard автоматично кращий за OpenVPN для голосу?

WireGuard зазвичай має нижчий оверхед і простіший розуміння, але якість голосу переважно залежить від правильного MTU, керування чергами й стабільної маршрутизації. Ви можете зламати голос на будь-якому VPN.

9) Яка найпростіша політика QoS, що реально працює?

Шейпте WAN трохи нижче реальної швидкості, потім пріоритезуйте голос (RTP) над bulk. Тримайте модель класів малою. Перевірте за допомогою qdisc-статистики та реальних тестових дзвінків.

10) Як довести, що це MTU, а не «кодек»?

Відтворіть проблему за допомогою DF ping порогів і спостерігайте відмови навколо певних розмірів пакетів; зіставте з подіями дзвінка, що збільшують розмір сигналізації; виправте MTU і подивіться, чи зникає проблема без зміни кодеків.

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

Якщо хочете, щоб дзвінки звучали людяно — ставте голос як SLO затримки, а не як настрій.

  1. Пройдіть швидкий план діагностики на одному ураженому користувачі і зберіть докази: MTU, джиттер під навантаженням, поведінка DSCP.
  2. Встановіть явний MTU тунелю (почніть консервативно) і клацніть MSS для TCP сигналізації.
  3. Розгорніть SQM шейпінг на вузькому uplink з fq_codel/cake і пріоритезуйте голос на цій черзі.
  4. Підтвердіть вимірюваннями (qdisc-статистика, tcpdump DSCP, mtr і клієнтські статистики джиттера/втрат), а не відчуттями.
  5. Задокументуйте: обраний MTU, чому його обрали, та регресійні тести. Майбутній ви буде втомленим і не враженим.

Більшість «таємниць» VoIP-over-VPN — це просто мережі, що роблять мережеві речі. Зробіть пакети меншими, зробіть черги розумнішими і зробіть пріоритети реальними там, де відбувається перевантаження.

Тай‑аути Docker NFS томів — опції монтування, що справді підвищують стабільність

Тайм‑аути Docker NFS томів — опції монтування, що справді підвищують стабільність

Ви запускаєте абсолютно нудний контейнер. Він записує кілька файлів. Потім, о 02:17, усе зависає, ніби чекає на довідку.
df зависає. Потоки вашого застосунку накопичуються. Логи Docker нічого корисного не кажуть. Єдина підказка — слід «nfs: сервер не відповідає».

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

Чому Docker NFS томи тайм‑аутяться (і чому це виглядає випадково)

Docker «NFS томи» — це просто Linux NFS монти, створені на хості й потім прив’язані (bind-mounted) у контейнери.
Звучить просто, і так воно є — поки ви не згадаєте, що NFS — це мережевий файловий сервіс із семантикою повторів,
RPC‑таймаутами, блокуванням і станом.

Більшість «тайм‑аутів» — це не один єдиний тайм‑аут

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

  • Клієнт намагається нескінченно повторювати (жорстке монтування, hard) і потік застосунку блокується в незмінному сні (D стан). Це виглядає як зависання.
  • Клієнт здався (м’яке монтування, soft) і повернув помилку I/O. Це виглядає як корупція, невдалі записи або «мій застосунок рандомно помиляється».
  • Сервер зник і повернувся, але у клієнта більше не співпадає уява про експорт (stale file handles). Це виглядає як «вчора працювало».
  • Проблеми з RPC‑інфраструктурою (особливо NFSv3: portmapper/rpcbind, mountd, lockd) спричиняють часткові збої, коли деякі операції працюють, а інші тайм‑аутять.
  • Флапи вирішення імен або маршрутизації спричиняють періодичні затори, які самостійно зцілюються. Це найгірше, бо породжує забобони.

Docker підсилює режими відмов

NFS чутливий до таймінгу монтувань. Docker дуже добре вмикає контейнери швидко, конкурентно і часом до того, як мережа готова.
Якщо NFS монтування ініціюється за потребою, ваш «старт контейнера» перетворюється на «старт контейнера плюс мережа плюс DNS плюс доступність сервера».
Це прийнятно в лабораторії. У виробництві це складний спосіб перетворити невелике мережеве джиттер‑переривання на відмову сервісу.

Hard vs soft — не налаштування продуктивності, а рішення про ризик

Для більшості станкоутримних навантажень безпечним дефолтом є hard монтування: продовжувати повторювати, не вдавати, що запис пройшов.
Але hard‑монти можуть зависати процеси, коли сервер недоступний. Тож ваше завдання — зробити «сервер недоступний» рідкісним і коротким
та зробити монтування стійким до звичайного хаосу мереж.

Є парафразована ідея від Вернера Фогельса (CTO Amazon), яка варта пам’яті: «Усе ламається, тож проектуйте з урахуванням відмов».
NFS‑монти — саме те місце, де ця філософія перестає бути натхненням і стає чеклістом.

Цікаві факти та контекст (коротко, конкретно, корисно)

  • NFS існує набагато довше за контейнери. Він зʼявився в 1980‑х як спосіб ділитися файлами мережею без складності клієнтського стану.
  • NFSv3 — переважно безстанний. Це полегшувало відновлення в деяких аспектах, але перенесло складність в допоміжні демони (rpcbind, mountd, lockd).
  • NFSv4 обʼєднав бокові канали. v4 зазвичай використовує один добре відомий порт (2049) і інтегрує блокування та стан, що зазвичай покращує дружність з фаєрволами та NAT.
  • «Hard mount» — історичний дефолт не просто так. Тихе втрачання даних гірше за очікування; hard‑монти віддають перевагу коректності, а не життєздатності.
  • Linux NFS клієнт має кілька рівнів таймаутів. Є базовий RPC‑таймаут (timeo), кількість повторів (retrans) і вищі механізми відновлення.
  • Stale file handle — класичний податок NFS. Виникає, коли сервер змінює зіставлення inode/file handle; часто після failover чи змін експорту.
  • NFS поверх TCP не завжди був дефолтом. Раніше популярний UDP; зараз TCP — здорова опція для надійності й контролю заторів.
  • DNS важливіший, ніж здається. NFS клієнти можуть кешувати відображення імен в IP інакше, ніж ваш застосунок; зміна DNS під час роботи може спричинити «півсвіту працює» симптоми.

Жарт №1: NFS як спільний принтер в офісі — коли працює, ніхто не помічає; коли ламається, раптом у всіх термінові «бізнес‑критичні» документи.

Швидка схема діагностики (швидко знайти вузьке місце)

Мета — не «зібрати кожну метрику». Мета — швидко вирішити, чи у вас проблема маршруту мережі,
проблема сервера, семантика монтування у клієнта або проблема оркестрації Docker.
Ось порядок, що економить час.

По‑перше: підтвердіть, що це NFS, а не застосунок

  1. На хості Docker, спробуйте простий stat або ls на змонтованому шляху. Якщо це зависає — проблема не в застосунку, а в монтуванні.
  2. Перевірте dmesg на наявність server not responding / timed out / stale file handle. Повідомлення ядра грубі, але зазвичай коректні.

По‑друге: вирішіть «мережа чи сервер» одним тестом

  1. З клієнта перевірте доступність порту 2049 і (якщо використовуєте NFSv3) rpcbind/portmapper. Якщо не вдається підключитися — припиніть звинувачувати параметри монтування.
  2. З іншого хоста в тій самій мережевій підмережі протестуйте те ж саме. Якщо проблема ізольована на одному клієнті — підозрівайте локальний фаєрвол, вичерпання conntrack, MTU або поганий маршрут.

По‑третє: перевірте версію протоколу й опції монтування

  1. Переконайтеся, чи ви на NFSv3 чи NFSv4. Багато «рандомних» тайм‑аутів — це насправді проблеми rpcbind/mountd від NFSv3 у сучасних мережах.
  2. Підтвердіть hard, timeo, retrans, tcp і чи використовували ви intr (застаріла поведінка) або інші legacy‑флаги.

По‑четверте: перегляньте логи сервера та насиченість

  1. Середнє навантаження сервера не достатньо. Дивіться кількість NFS‑потоків, затримки диска та мережеві втраї.
  2. Якщо сервер — NAS, визначте, чи він навантажений ЦП (шифрування, контрольні суми) або ввід/вивід (диски, rebuild, видалення snapshot).

Якщо ви пройдете ці чотири фази, зазвичай зможете назвати клас відмови менше ніж за десять хвилин. Довгий шлях — це політика.

Патерни конфігурації Docker, що вам не шкодитимуть

Патерн 1: драйвер local Docker з NFS‑опціями (добре, але перевірте, що реально змонтувалося)

Драйвер local Docker може монтувати NFS за допомогою type=nfs і o=....
Це звично в Compose і Swarm.
Підводний камінь: люди припускають, що Docker «робить щось розумне». Ні. Він передає опції mount‑хелперу.
Якщо mount‑хелпер відкотиться до іншої версії або проігнорує опцію, ви можете не помітити.

Патерн 2: попередньо змонтувати на хості й bind‑монтувати в контейнери (часто більш передбачувано)

Якщо ви попередньо монтуєте через /etc/fstab або systemd‑unit‑и, ви можете контролювати порядок, повтори і спостерігати монтування безпосередньо.
Docker тоді просто робить bind‑mount локального шляху. Це зменшує «магію Docker», що загалом добре для сну.

Патерн 3: розділяйте монти за класом навантаження

Не використовуйте один NFS‑експорт і один набір опцій для всього.
Ставтеся до NFS як до сервісу з SLO: низьколатентні метадані (CI кеші), пропускна здатність для великих файлів (медіа), коректність перш за все (дані станових застосунків).
Різні монти — різні опції — різні очікування.

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

Це хід дій на виклику, що перетворює «NFS ненадійний» на чітке наступне завдання. Запускайте їх на хості Docker, якщо не зазначено інакше.
Кожне завдання включає (1) команду, (2) що означає вивід, (3) яке рішення прийняти.

Завдання 1: Визначити, які монти — NFS і як вони сконфігуровані

cr0x@server:~$ findmnt -t nfs,nfs4 -o TARGET,SOURCE,FSTYPE,OPTIONS
TARGET                SOURCE                     FSTYPE OPTIONS
/var/lib/docker-nfs    nas01:/exports/appdata     nfs4   rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.10.8.21

Значення: Підтверджує версію NFS, proto і чи ви на hard чи soft. Також показує, чи rsize/wsize великі і потенційно не співпадають.

Рішення: Якщо бачите vers=3 несподівано, плануйте перехід на v4 або перевірку портів rpcbind/mountd. Якщо бачите soft для навантажень із записом — змініть це.

Завдання 2: Підтвердити конфігурацію Docker volume (що Docker вважає, що він запросив)

cr0x@server:~$ docker volume inspect appdata
[
  {
    "CreatedAt": "2026-01-01T10:12:44Z",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/appdata/_data",
    "Name": "appdata",
    "Options": {
      "device": ":/exports/appdata",
      "o": "addr=10.10.8.10,vers=4.1,proto=tcp,hard,timeo=600,retrans=2,noatime",
      "type": "nfs"
    },
    "Scope": "local"
  }
]

Значення: Це конфігурація, а не правда. Опції Docker можуть бути вірними, тоді як фактичне монтування інше.

Рішення: Порівняйте з findmnt. Якщо вони різняться — дослідіть поведінку mount‑хелпера, дефолти і підтримку ядром.

Завдання 3: Пошук помилок в NFS‑клієнті ядра прямо зараз

cr0x@server:~$ dmesg -T | egrep -i 'nfs:|rpc:|stale|not responding|timed out' | tail -n 20
[Fri Jan  3 01:58:41 2026] nfs: server nas01 not responding, still trying
[Fri Jan  3 01:59:12 2026] nfs: server nas01 OK

Значення: «Not responding, still trying» вказує на hard‑монтування, що повторює спроби через розрив.

Рішення: Якщо ці події співпадають із застопореннями застосунку — досліджуйте мережеві пропадання або зависання сервера; не «чіпайте» застосунок.

Завдання 4: Підтвердити стани процесів під час зависання (чи в D‑стані?)

cr0x@server:~$ ps -eo pid,stat,comm,wchan:40 | egrep 'D|nfs' | head
 8421 D    php-fpm          nfs_wait_on_request
 9133 D    rsync            nfs_wait_on_request

Значення: D стан з nfs_wait_on_request вказує на заблокований I/O ядра в очікуванні NFS.

Рішення: Розглядайте як інфраструктурний інцидент. Перезапуск контейнерів не допоможе, якщо монтування жорстко застрягло.

Завдання 5: Перевірити базову TCP‑зʼєднаність до NFS‑сервера

cr0x@server:~$ nc -vz -w 2 10.10.8.10 2049
Connection to 10.10.8.10 2049 port [tcp/nfs] succeeded!

Значення: Порт 2049 доступний зараз.

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

Завдання 6: Якщо використовуєте NFSv3, підтвердити, що rpcbind доступний (поширена прихована залежність)

cr0x@server:~$ nc -vz -w 2 10.10.8.10 111
Connection to 10.10.8.10 111 port [tcp/sunrpc] succeeded!

Значення: rpcbind/portmapper доступний. Без нього NFSv3 монти можуть падати або зависати під час узгодження монтування.

Рішення: Якщо 111 заблокований і ви на v3 — переходьте на v4 або відкрийте потрібні порти належним чином (і документуйте це).

Завдання 7: Виявити версію NFS та адресу сервера (піймати DNS‑сюрпризи)

cr0x@server:~$ nfsstat -m
/var/lib/docker-nfs from nas01:/exports/appdata
 Flags: rw,hard,noatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.10.8.21,local_lock=none

Значення: Підтверджує узгоджені налаштування. Зверніть увагу на імʼя сервера проти IP і поведінку local_lock.

Рішення: Якщо монтування використовує hostname і ваш DNS нестабільний — перейдіть на IP або закешуйте запис у hosts — потім сплануйте кращу DNS‑стратегію.

Завдання 8: Виміряти кількість повторів і біль на рівні RPC (чи є втрата пакетів?)

cr0x@server:~$ nfsstat -rc
Client rpc stats:
calls      retrans    authrefrsh
148233     912        148245

Значення: Retrans вказує на RPC, які довелося повторно відправляти. Зростання retrans корелює з втратами, заторами або зависаннями сервера.

Рішення: Якщо retrans зростає під час інцидентів — перевіряйте втрати в мережі та навантаження сервера; розгляньте помірне збільшення timeo, а не зменшення.

Завдання 9: Перевірити помилки інтерфейсу і втрати (не гадати)

cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    RX:  bytes packets errors dropped  missed   mcast
    128G  98M     0     127      0      1234
    TX:  bytes packets errors dropped carrier collsns
    141G  92M     0      84      0      0

Значення: Втрати на RX/TX можуть бути достатніми, щоб викликати NFS «не відповідає» під навантаженням.

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

Завдання 10: Швидко виявити MTU‑невідповідність (jumbo frames підозріло до доказів)

cr0x@server:~$ ping -c 3 -M do -s 8972 10.10.8.10
PING 10.10.8.10 (10.10.8.10) 8972(9000) bytes of data.
From 10.10.8.21 icmp_seq=1 Frag needed and DF set (mtu = 1500)
From 10.10.8.21 icmp_seq=2 Frag needed and DF set (mtu = 1500)
From 10.10.8.21 icmp_seq=3 Frag needed and DF set (mtu = 1500)

--- 10.10.8.10 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2043ms

Значення: Path MTU = 1500, але ваш хост думає 9000. Це спричиняє «чорні діри» й «рандомні» затрини.

Рішення: Виправте MTU по всьому шляху або зменшіть до 1500. Потім знову оцініть стабільність NFS перед правками опцій монтування.

Завдання 11: Підтвердити, що експорт на сервері існує й права адекватні (вид сервера)

cr0x@server:~$ showmount -e 10.10.8.10
Export list for 10.10.8.10:
/exports/appdata 10.10.8.0/24
/exports/shared  10.10.0.0/16

Значення: Показує експорти (корисно для NFSv3; для v4 все одно дає підказку).

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

Завдання 12: Захопити короткий пакетний трас під час події (доведіть втрату vs мовчання сервера)

cr0x@server:~$ sudo tcpdump -i eth0 -nn host 10.10.8.10 and port 2049 -c 30
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
10:02:11.101223 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232
10:02:12.102988 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232
10:02:13.105441 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232

Значення: Повторні ретрансмісії без відповідей сервера вказують, що сервер не відповідає або відповіді не повертаються.

Рішення: Якщо бачите клієнтські ретрансмісії без відповіді — ідіть до перевірки здоровʼя сервера / шляху повернення мережі. Якщо бачите відповіді сервера, але клієнт все ще ретрансмить — підозрюйте асиметричну маршрутизацію або фаєрвол‑стан.

Завдання 13: Перевірити логи демонa Docker на предмет спроб монтування і помилок

cr0x@server:~$ journalctl -u docker --since "30 min ago" | egrep -i 'mount|nfs|volume|rpc' | tail -n 30
Jan 03 09:32:14 server dockerd[1321]: time="2026-01-03T09:32:14.112345678Z" level=error msg="error while mounting volume 'appdata': failed to mount local volume: mount :/exports/appdata:/var/lib/docker/volumes/appdata/_data, data: addr=10.10.8.10,vers=4.1,proto=tcp,hard,timeo=600,retrans=2: connection timed out"

Значення: Підтверджує, що Docker не зміг змонтувати, а не що застосунок впав пізніше.

Рішення: Якщо монти падають під час старту контейнера — пріоритет зробіть на готовність мережі і доступність сервера; не ганяйтесь за раннім тюнінгом.

Завдання 14: Інспектувати порядок systemd (network‑online ≠ просто network)

cr0x@server:~$ systemctl status network-online.target
● network-online.target - Network is Online
     Loaded: loaded (/lib/systemd/system/network-online.target; static)
     Active: active since Fri 2026-01-03 09:10:03 UTC; 1h 2min ago

Значення: Якщо цей таргет не активний, коли відбуваються монти — ваше NFS монтування може змагатися з готовністю мережі.

Рішення: Якщо бачите проблеми з порядком, перенесіть монти в systemd‑unit‑и з After=network-online.target і Wants=network-online.target, або використайте automount.

Завдання 15: Перевірити, що монтування відповідає (швидка саніті‑перевірка)

cr0x@server:~$ time bash -c 'stat /var/lib/docker-nfs/. && ls -l /var/lib/docker-nfs >/dev/null'
real    0m0.082s
user    0m0.004s
sys     0m0.012s

Значення: Базові метадані швидкі. Якщо це іноді займає секунди або зависає — у вас періодична затримка або стопи.

Рішення: Якщо метадані повільні — досліджуйте затримки диска сервера і насиченість NFS‑потоків; опції монтування не врятують тонучий масив.

Три корпоративні міні‑історії (як це насправді ламається)

1) Інцидент через неправильне припущення: «Том локальний, тому що Docker каже ‘local’»

Середня компанія вела Swarm‑кластер для внутрішніх сервісів. Команда створила Docker‑том із драйвером local і NFS‑опціями.
Усі прочитали «local» і припустили, що дані лежать на кожному вузлі локально. Це припущення визначило все: плани відновлення, вікна техобслуговування, навіть відповідальність за інциденти.

Під час мережевого техобслуговування один комутатор у стійці підгорів. Тільки деякі вузли втратили звʼязок з NAS на кілька секунд.
Вражені вузли мали hard‑монти NFS. Їхні контейнери не падали; вони просто припинили робити прогрес. Хелсчеки тайм‑аутнули.
Оркестратор почав пересаджувати, але нові таски потрапляли на ті ж самі постраждалі вузли, бо планування не знало, що проблемою є NFS.

Реакція на виклик була класичною: перезапустити сервіс. Це лише створило більше заблокованих процесів. Хтось спробував видалити й створити том наново.
Docker підкорився, але монтування ядра все ще було застрягле. Хост перетворився на музей зависших тасків.

Виправлення не було героїчним. Документували, що «local driver» може бути віддаленим сховищем, додали превʼю‑перевірку в pipeline розгортання,
щоб верифікувати тип монтування за допомогою findmnt, і відчепили критичні сервіси від вузлів, які не могли дістатися storage VLAN.
Найбільша зміна була культурною: сховище перестало бути «чужою проблемою» з моменту появи контейнерів.

2) Оптимізація, що відкинула назад: «Ми зменшили таймаути, щоб відмови відбувалися швидше»

Інша організація мала періодичну проблему: застосунки зависали при NFS‑хиках. Хтось запропонував «прості» зміни:
перемкнутися на soft, знизити timeo і підвищити retrans, щоб клієнт здавався швидко й застосунок міг це обробити.
У квитку це виглядало розумно — бо в квитку все виглядає розумно.

На практиці застосунки не були готові обробляти EIO під час запису.
Фоновий воркер писав у тимчасовий файл, а потім перейменовував його на місце. Під soft‑монтуваннями і низькими таймаутами
запис іноді падав, але робочий процес не завжди поширював помилку. Перейменування відбувалося з частковим вмістом.
Подальші таски обробляли «сміття».

Інцидент не був чистим простою; він був гіршим. Система залишалася «увімкненою», виробляючи неправильні результати.
Це спричинило повільне реагування: відкат, повторна обробка, аудит результатів. Зрештою опції монтування відкотили.
Потім вони виправили справжню проблему: періодичну втрату пакетів через некоректну конфігурацію LACP та MTU‑невідповідність, що виявлялася лише під навантаженням.

Висновок у їхньому рукоописі був різким: «Fail fast» чудово, коли відмова надійно піднімається.
Soft‑монти зробили відмову легшою для ігнорування, а не для обробки.

3) Скучно, але правильно: pre‑mount + automount + явні залежності врятували день

Фінансова компанія запускала станкові пакетні завдання в контейнерах, записуючи артефакти на NFS.
У них була прісна правило: NFS‑монти керуються systemd, а не створенням Docker‑тома під час запуску.
Кожен монтування мав automount‑unit, визначений таймаут і залежність від network-online.target.

Одного ранку плановий перезавантаження вузла збігся з патчингом NAS. NAS був доступний, але кілька хвилин працював повільно.
Контейнери стартували, але їхні NFS‑шляхи монтувалися лише при зверненні. Спроба automount чекала й потім успішно завершилася, коли NAS відновився.
Завдання почалися з невеликим запізненням, і ніхто не прокинувся.

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

Це саме те, за що керівництво не дає винагороди, бо нічого не сталося. Але це практика, що тримає вас на роботі.

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

1) Симптом: контейнери «замерзають» і не зупиняються; docker stop зависає

Корінна причина: hard‑монтування NFS застопорене; процеси в D стані чекають I/O ядра.

Виправлення: відновити конектність/здоровʼя сервера; не чекайте, що сигнали спрацюють. Якщо треба відновити вузол — відмонтуйте після повернення сервера або як крайній захід перезавантажте хост. Запобігайте повторенню стабільними мережами і розумними timeo/retrans.

2) Симптом: «працює на одному вузлі, тайм‑аутить на іншому»

Корінна причина: відмінності маршрутизації/фаєрвола/MTU по вузлах або вичерпання conntrack на підмножині вузлів.

Виправлення: порівняйте ip route, ip -s link і правила фаєрволу. Перевірте MTU за допомогою DF‑ping. Забезпечте ідентичну мережеву конфігурацію по кластеру.

3) Симптом: монти падають під час завантаження або одразу після перезавантаження хоста

Корінна причина: спроби монтування перегорають з готовністю мережі; Docker запускає контейнери до того, як network‑online стане true.

Виправлення: керуйте монтуваннями через systemd‑unit‑и з явним порядком або використайте automount. Уникайте монтувань за вимогою, ініційованих стартом контейнера.

4) Симптом: періодичні «permission denied» або дивні проблеми з ідентифікацією

Корінна причина: невідповідність UID/GID, поведінка root‑squash або проблеми idmapping у NFSv4. Контейнери це ускладнюють через user namespaces і різних користувачів у образах.

Виправлення: стандартизувати UID/GID для писачів, перевірити опції експорту на сервері і для NFSv4 перевірити конфігурацію idmapping. Не загортайте проблему в 0777; це не стабільність, це капітуляція.

5) Симптом: часті «stale file handle» після failover NAS або техобслуговування експорту

Корінна причина: серверне зіставлення file handle змінилося; клієнти тримають посилання, які більше не резольвляться.

Виправлення: уникайте переміщення/перезапису експортів під клієнтами; використовуйте стабільні шляхи. Для відновлення — перемонтуйте і перезапустіть постраждалі робочі навантаження. Для архітектури — віддавайте перевагу стабільним HA‑методам, підтримуваним вашим NAS, і тестуйте failover з реальними клієнтами.

6) Симптом: «рандомні» помилки монтувань лише в захищених мережах

Корінна причина: динамічні порти NFSv3 заблоковані; rpcbind/mountd/lockd не пропускаються через фаєрвол/секюріті групи.

Виправлення: перейдіть на NFSv4, якщо можливо. Якщо привʼязані до v3 — зафіксуйте порти демонів на сервері і відкрийте їх навмисно — і документуйте це, щоб наступний інженер не «оптимізував» ваш фаєрвол.

7) Симптом: пікові затримки, потім відновлення, що повторюється під навантаженням

Корінна причина: затримки диска сервера (rebuild/snapshot), насичення NFS‑потоків або переповненість мережевих черг.

Виправлення: виміряйте I/O‑латентність на сервері і кількість NFS‑потоків; ліквідуйте вузьке місце. Клієнтські опції як rsize/wsize не врятують перенасичений масив.

8) Симптом: перехід на soft «вирішує» зависання, але приносить загадкові проблеми з даними

Корінна причина: soft‑монти перетворюють відмови в I/O помилки; застосунки неправильно обробляють часткові збої.

Виправлення: поверніть hard для станкових записів, виправте основну конектність і оновіть застосунки для обробки помилок там, де це потрібно.

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

Покроково: стабілізувати існуючий Docker NFS деплой

  1. Зробіть інвентар монтів за допомогою findmnt -t nfs,nfs4. Запишіть vers, proto, hard/soft, timeo, retrans і чи використовувалися hostnames.
  2. Підтвердьте реальність за допомогою nfsstat -m. Якщо Docker каже одне, а ядро зробило інше — довіряйте ядру.
  3. Визначте протокол: віддавайте перевагу NFSv4.1+. Якщо ви на v3 — перелічіть залежності фаєрволу і випадки відмов, які ви не можете терпіти.
  4. Виправте мережу перед тюнінгом: перевірте MTU по всьому шляху; усуньте дропи інтерфейсу; підтвердіть симетрію маршрутизації; забезпечте стабільність порту 2049.
  5. Виберіть семантику монтування:
    • Станкові записи: hard, помірний timeo, доволі низький retrans, TCP.
    • Лише читання / кеш: розгляньте soft тільки якщо застосунок обробляє EIO і ви готові до «помилка замість зависання».
  6. Зробіть монти передбачуваними: попередньо монтуйте через systemd або використайте automount. Уникайте runtime‑монтувань, ініційованих стартом контейнера коли це можливо.
  7. Протестуйте відмову: відключіть мережу сервера (в лабораторії), перезавантажте клієнта, пропустіть маршрут, і спостерігайте. Якщо ваш тест — «почекати і сподіватися», ви не тестуєте.
  8. Операціоналізуйте: додайте дашборди для retransmits, дропів інтерфейсу, насичення NFS‑потоків сервера і затримок диска. Напишіть runbook на основі Швидкої схеми діагностики і попрактикуйте його у спокійний час.

Короткий чекліст опцій монтування (передбачуваність перш за все)

  • Використовуйте TCP: proto=tcp
  • Віддавайте перевагу NFSv4.1+: vers=4.1 (або 4.2 якщо підтримується)
  • Коректність перш за все: hard
  • Не перестарайтеся з тюнінгом: почніть з timeo=600, retrans=2 і регулюйте тільки за наявності доказів
  • Зменшіть метаданний шум: noatime для типової робооти
  • Обережно з actimeo та схожими; кешування не є «безкоштовним прискоренням»
  • Розгляньте nconnect лише після вимірювання здібностей сервера і пропускних здібностей фаєрволу

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

1) Чи використовувати NFSv3 чи NFSv4 для Docker томів?

Використовуйте NFSv4.1+ якщо нема специфічної сумісності, що забороняє. У мережах із великою кількістю контейнерів менше допоміжних демонів і портів зазвичай означає менше «рандомних» помилок монтування.

2) Чи soft коли‑небудь прийнятний?

Так — для даних лише для читання або кеш‑даних, де помилка I/O краща за зависання, і де застосунок побудований оброблювати EIO як норму. Для станкових записів це міна.

3) Чому docker stop зависає, коли NFS впав?

Тому що процеси заблоковані в I/O ядра на жорстко змонтованій файловій системі. Сигнали не можуть перервати потік у незмінному сні. Виправте доступність монтування.

4) Що саме роблять timeo і retrans?

Вони керують поведінкою повторів RPC. timeo — базовий таймаут для RPC; retrans — скільки повторів відбувається перед тим, як клієнт повідомить «не відповідає» (і для soft‑монтувань — до повернення помилки I/O).

5) Чи варто тюнити rsize і wsize до величезних значень?

Не за приписами. Сучасні дефолти часто хороші. Забагато може зіпсувати взаємодію з MTU, лімітами сервера або мережевими втратами. Тюнити тільки після вимірювання пропускної здатності і retransmits.

6) Чи допомагає використання IP замість імені хоста?

Може допомогти. Якщо DNS хиткий, повільний або змінюється несподівано, використання IP усуває вирішення імен як точку відмови. Мінус — втрата легкості міграції сервера, якщо ви не керуєте IP як стабільну ціль.

7) Що спричиняє «stale file handle» і як це запобігти?

Зазвичай спричинене серверними змінами, що інвалідовують file handles: переміщення експорту, поведінка failover або зміни файлової системи під експортом. Запобігайте, тримаючи експорти стабільними і застосовуючи HA‑методи вашого NAS, тестуючи failover з реальними клієнтами.

8) Монтованти через Docker volumes чи попередньо на хості?

Попереднє монтування (systemd mounts/automount) часто більш передбачуване й легше для дебагу. Docker‑volume монтування працює, але звʼязує життєвий цикл монтування з життєвим циклом контейнера — і це не найкраще місце для вашої історії надійності.

9) А nolock як фікс для зависань?

Уникайте, якщо не впевнені, що навантаження не покладається на блокування. Воно може «вирішити» деякі lockd‑проблеми в NFSv3, але обмінює відмови на баги коректності.

10) Якщо мій NFS‑сервер у порядку, чому лише деякі клієнти бачать тайм‑аути?

Бо «сервер у порядку» часто значить «відповів на ping». Локальні проблеми клієнта — MTU‑невідповідність, асиметрична маршрутизація, ліміти conntrack та дропи NIC — можуть окремо ламати NFS, лишивши інший трафік в основному працездатним.

Висновок: наступні кроки, що зменшують навантаження на виклик

Якщо ви боретеся з тайм‑аутами Docker NFS томів, не починайте крутити timeo як радіодіал.
Почніть з іменування відмови: шлях мережі, насыщення сервера, тертя версій протоколу або таймінг оркестрації.
Потім зробіть свідомий вибір щодо семантики: коректність перш за все — hard‑монти для станкових записів, і лише ретельно обґрунтовані soft‑монти для одноразових даних.

Практичні наступні кроки на цей тиждень:

  1. Перевірити кожен хост Docker за допомогою findmnt і nfsstat -m; зафіксувати фактичні опції і версію NFS.
  2. Стандартизувати NFSv4.1+ поверх TCP, якщо нема причин інакше.
  3. Виправити MTU і лічильники падінь до зміни тюнінгу монтувань.
  4. Перевести критичні монти в systemd‑керовані монти (найкраще — automount) з явним порядком network‑online.
  5. Написати runbook на основі Швидкої схеми діагностики і відрепетирувати його раз, поки все тихо.

Кінцева мета — не «NFS ніколи не підглючить». Кінцева мета: коли він підглючить, поведінка передбачувана, відновлення чисте і ваші контейнери не перетворюються на сучасне мистецтво.

ZFS ZVOL проти Dataset: рішення, що визначає ваші майбутні проблеми

Ви можете роками використовувати ZFS, не задумуючись про різницю між dataset і zvol.
Потім ви віртуалізуєте щось важливе, додаєте снапшоти «на всякий випадок», реплікація стає вимогою ради директорів,
і раптом ваша платформа зберігання починає мати власну думку. Голосну.

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

Datasets і zvol: що вони насправді означають (не те, що пишуть у Slack)

Dataset: файловa система з надздібностями ZFS

ZFS dataset — це ZFS filesystem. Вона має файлову семантику: директорії, дозволи, власність, розширені атрибути.
Її можна експортувати через NFS/SMB, монтувати локально та оперувати звичними інструментами. ZFS додає власний набір можливостей:
снапшоти, клони, стиснення, контрольні суми, квоти/резервації, налаштування recordsize і всю транзакційну безпеку, що
дає вам трохи більше спокою.

Коли ви зберігаєте дані в dataset, ZFS керує тим, як розміщує змінного розміру «записи» (блоки, але не фіксованого розміру як у традиційних ФС).
Це важливо, бо впливає на ампліфікацію, ефективність кешування і шаблони IO. Ключовий регулятор — recordsize.

Zvol: блочний пристрій, вирізаний із ZFS

Zvol — це ZFS volume: віртуальний блочний пристрій, що експонується як /dev/zvol/pool/volume. Він не розуміє файлів.
Ваш гостьовий файловий менеджер (ext4, XFS, NTFS) або база даних бачить диск і записує блоки. ZFS зберігає ці блоки як об’єкти
з фіксованим розміром, яким керує volblocksize.

Zvol існують для випадків, коли споживач потребує блочного пристрою: iSCSI LUN, диски VM, деякі рантайми контейнерів, деякі гіпервізори
і інколи стекі застосунків, що наполягають на «сирих» пристроях.

Реальний світ простою мовою

  • Dataset = «ZFS — це файловa система; клієнти говорять через файлові протоколи; ZFS бачить файли і може оптимізувати під них.»
  • Zvol = «ZFS надає імітацію диска; хтось інший будує файлову систему; ZFS бачить блоки і робить припущення.»

ZFS чудово працює в обох моделях, але вони поводяться по-різному. Біль виникає, коли вважають, що це однакові речі.

Короткий жарт про те, що зберігання потребує смиренності: якщо ви хочете розпалити пристрасну дискусію в датацентрі, заговоріть про RAID.
Якщо хочете її завершити — згадайте zvol volblocksize і спостерігайте, як усі тихенько переглядають нотатки.

Правила вибору: коли використовувати dataset, коли — zvol

За замовчуванням: datasets — нудний вибір, і нудний виграє

Якщо ваше навантаження може використовувати сховище як файлову систему (локальне монтування, NFS, SMB), використовуйте dataset. Ви отримуєте простішу експлуатацію:
легше інспектувати, простіше копіювати/відновлювати, пряміші права доступу і менше крайових випадків навколо розмірів блоків та TRIM/UNMAP.
Також ви отримуєте поведінку ZFS, налаштовану за замовчуванням під файли.

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

Коли zvol — правильний інструмент

Використовуйте zvol, коли споживач вимагає блочний пристрій:

  • диски VM (особливо для гіпервізорів, які хочуть raw volumes, або коли потрібні ZFS-снапшоти віртуального диска)
  • iSCSI-таргети (LUN за визначенням блочні)
  • деякі кластерні налаштування, які реплікують блочні пристрої або потребують SCSI-семантики
  • успадковані застосунки, що підтримують тільки «базу даних на сирому пристрої» (рідко, але буває)

Модель zvol потужна: снапшоти диска VM швидкі, клони — миттєві, реплікація працює, і ви можете стискати та контролювати контрольні суми.

Але: блочні пристрої множать відповідальність

Коли ви використовуєте zvol, ви тепер відповідаєте за шар між гостьовою ФС і ZFS. Вирівнювання важливе. Розмір блоку важливий.
Бар’єри запису важливі. Trim/UNMAP важливі. Налаштування sync стають політикою, а не дрібною опцією.

Проста матриця рішень, яку можна захищати

  • Потрібні NFS/SMB або локальні файли? Dataset.
  • Потрібен iSCSI LUN або сирий блок для гіпервізора? Zvol.
  • Потрібна видимість по файлах, просте відновлення одного файлу? Dataset.
  • Потрібні миттєві клони VM від golden image? Zvol (або dataset зі sparse-файлами, але знайте свій інструментальний стек).
  • Потрібен консистентний снапшот застосунку, який керує власною ФС? Zvol (і координуйте flush/quiesce).
  • Хочете «оптимізувати продуктивність», не знаючи шаблонів IO? Dataset, потім вимірюйте. Геройське тонке налаштування прийде пізніше.

Recordsize vs volblocksize: тут вирішується продуктивність

Datasets: recordsize — це максимальний розмір блока даних, який ZFS використовуватиме для файлів. Великі послідовні файли (резервні копії,
медіа, логи) люблять більші recordsize, наприклад 1M. Бази даних і випадкові IO віддають перевагу меншим значенням (16K, 8K), бо перезапис невеликого
регіону не провокує велику перезаписну ампліфікацію.

Zvols: volblocksize фіксується при створенні. Вибравши неправильно, ви не зможете змінити його пізніше без перебудови zvol.
Це не «неприємно». Це «ми заплануємо міграцію в Q4, бо графіки затримок нагадують пилкоподібний малюнок».

Снапшоти: оманливо схожі, але операційно різні

Снапшот dataset захоплює стан файлової системи. Снапшот zvol захоплює сирі блоки. У обох випадках ZFS використовує copy-on-write,
тому снапшот дешевий на створенні і дорожчий пізніше, якщо ви постійно перезаписуєте блоки, що посилаються снапшотами.

З zvol цю витрату легше спровокувати, бо диски VM постійно перезаписують блоки: оновлення метаданих, журналювання, обслуговування ФС,
іноді «нічого не відбувається», але всередині щось перезаписується. Снапшоти, що тримаються занадто довго, стають податком.

Квоти, резервації і пастка thin-provisioning

Datasets дають вам quota і refquota. Zvols дають фіксований розмір, але ви можете створювати sparse-томи
і прикидатися, що у вас більше місця, ніж є. Це бізнес-рішення під виглядом інженерної функції.

Thin-provisioning прийнятне, якщо у вас є моніторинг, алерти і відповідальна експлуатація. Це катастрофа, коли його використовують,
щоб уникнути слова «ні» у черзі запитів.

Другий короткий жарт (і останній): thin provisioning — це як замовити штани на розмір менші як «мотивацію».
Іноді працює, але переважно просто не вистачає повітря.

Режими відмов, що зустрічаються лише в продукції

Ампліфікація записів через невідповідний розмір блоків

Dataset з recordsize=1M, що підтримує базу даних з 8K випадковими записами, може викликати болісну ампліфікацію:
кожне невелике оновлення зачіпає великий запис. ZFS має логіку для обробки менших записів (і в деяких випадках зберігатиме менші блоки),
але не покладайтеся на неї, щоб врятувати вас від поганого вибору. Натомість zvol з volblocksize=128K, що обслуговує VM ФС,
яка пише 4K блоки, буде так само невідповідний.

Симптом: добрий пропуск у бенчмарках, жахливі хвостові затримки в реальних навантаженнях.

Синхронні семантики: де ховається латентність

ZFS поважає синхронні записи. Якщо застосунок (або гіпервізор) робить sync-записи, ZFS має зафіксувати їх безпечно — тобто на стабільне сховище, а не лише в RAM.
Без виділеного SLOG-пристрою (швидкого, із захистом від втрати живлення) навантаження з великою кількістю sync-записів може застрягти на латентності основного пулу.

Споживачі zvol часто більш агресивно використовують sync-запити, ніж файлові протоколи. VM і бази даних зазвичай дбають про довговічність.
NFS-клієнти можуть робити sync-записи залежно від опцій монтовання і поведінки застосунку. У будь-якому випадку, якщо сплески затримок корелюють
з навантаженням sync, дискусія «zvol vs dataset» перетворюється на «чи розуміємо ми шлях sync-записів».

TRIM/UNMAP і міф про «автоматичне повернення вільного місця»

Datasets звільняють блоки при видаленні файлів. Zvols залежать від того, чи гість надсилає TRIM/UNMAP (і чи ваш стек передає його далі).
Без цього ваш zvol виглядає повним назавжди, снапшоти здуті, і ви починаєте звинувачувати «фрагментацію ZFS» замість відсутності сигналу збору сміття.

Вибухове зростання утримання снапшотів

Утримувати hourly снапшоти протягом 90 днів для VM zvol здається відповідальним, поки ви не усвідомите, що зберігаєте кожен перезаписаний блок під час
кожного Windows-оновлення, запуску пакетного менеджера і ротації логів. Математика швидко стає некрасивою. Datasets теж страждають, але VM churn — це особливий вид ентузіазму.

Сюрприз реплікації: datasets дружніші до інкрементальної логіки

Реплікація ZFS працює і для datasets, і для zvol, але реплікація zvol може бути більша та більш чутлива до перезаписів блоків.
Невелика зміна в гостьовій ФС може перезаписати блоки по всьому простору. Ваш incremental send може виглядати підозріло як повний send, і WAN-канал підкаже вам, що він проти.

Фрикція інструментів: люди — частина системи

Більшість команд мають кращі операційні навички щодо файлових систем, ніж блочних пристроїв. Вони знають, як перевіряти дозволи, як копіювати файли,
як монтувати й інспектувати. Робочі процеси з zvol тягнуть вас у інші інструменти: таблиці розділів, гостьові ФС, блок-рівневі перевірки.
Фрикція проявляється о 3:00 ранку, а не на зустрічі з дизайну.

Цікаві факти та історія, які можна використати на дизайн-рев’ю

  1. ZFS ввів наскрізну перевірку контрольних сум як базову функцію, а не доповнення; це змінило підхід до «тихої корупції».
  2. Copy-on-write не був новим коли з’явився ZFS, але ZFS зробив його експлуатаційно звичним для загальних стеків зберігання.
  3. Zvol були спроектовані для інтеграції з блочними екосистемами як iSCSI і VM-платформи, задовго до того, як «гіперконвергентність» стала маркетинговим терміном.
  4. За замовчуванням recordsize вибрано для загальних файлових навантажень, а не для баз даних; значення за замовчуванням — це політика, вбудована в код.
  5. Volblocksize незмінний після створення zvol у більшості реалізацій; ця деталь викликає багато міграцій.
  6. ARC (Adaptive Replacement Cache) зробив поведінку кешу ZFS відмінною від багатьох традиційних ФС; це не просто page cache.
  7. L2ARC з’явився як кеш другого рівня, але він не замінює потребу правильно розмірити RAM; він змінює лише показники влучань, а не творить дива.
  8. SLOG-пристрої стали стандартним паттерном тому, що латентність синхронних записів домінує в деяких навантаженнях; «швидкий SSD» без захисту від втрати живлення — це не SLOG.
  9. Send/receive реплікація дала ZFS вбудований примітив для бекапу; це не «rsync», це потік транзакцій блоків.

Практичні завдання: команди, виводи та що вирішувати

Це не «демо-команди». Це ті команди, що ви запускаєте, коли намагаєтеся вибрати між dataset і zvol, або коли доводите собі, що зробили правильний вибір.

Завдання 1: інвентаризація того, що у вас насправді є

cr0x@server:~$ zfs list -o name,type,used,avail,refer,mountpoint -r tank
NAME                         TYPE   USED  AVAIL  REFER  MOUNTPOINT
tank                         filesystem  1.12T  8.44T   192K  /tank
tank/vm                      filesystem   420G  8.44T   128K  /tank/vm
tank/vm/web01                volume      80.0G  8.44T  10.2G  -
tank/vm/db01                 volume     250G   8.44T   96.4G  -
tank/nfs                     filesystem  320G  8.44T   320G  /tank/nfs

Значення: У вас є і datasets (filesystem), і zvols (volume). Zvols не мають mountpoint.

Рішення: Визначте, які навантаження знаходяться на volumes, і запитайте: чи справді їм потрібен блок, або це інерція?

Завдання 2: перевірте recordsize dataset (і чи відповідає він навантаженню)

cr0x@server:~$ zfs get -o name,property,value recordsize tank/nfs
NAME      PROPERTY    VALUE
tank/nfs  recordsize  128K

Значення: Цей dataset використовує 128K records, пристойний загальний дефолт.

Рішення: Якщо dataset хостить бази даних або образи VM як файли, розгляньте 16K або 32K; якщо це бекапи — 1M.

Завдання 3: перевірте volblocksize zvol ( «не змінюється пізніше»)

cr0x@server:~$ zfs get -o name,property,value volblocksize tank/vm/db01
NAME         PROPERTY     VALUE
tank/vm/db01 volblocksize 8K

Значення: Цей zvol використовує 8K блоки — зазвичай доречно для баз даних з випадковими IO і багатьох VM ФС.

Рішення: Якщо ви бачите 64K/128K тут для загальних VM-дисків, очікуйте ампліфікацію записів і розгляньте перебудову правильно.

Завдання 4: перевірте політику sync, бо вона контролює довговічність vs латентність

cr0x@server:~$ zfs get -o name,property,value sync tank/vm
NAME     PROPERTY  VALUE
tank/vm  sync      standard

Значення: ZFS буде поважати sync-запити, але не змушуватиме всі записи бути sync.

Рішення: Якщо хтось поставив sync=disabled «для продуктивності», заплануйте розмову про ризики і план відкату.

Завдання 5: подивіться статус стиснення і коефіцієнт (часто вирішує вартість)

cr0x@server:~$ zfs get -o name,property,value,source compression,compressratio tank/vm/db01
NAME         PROPERTY       VALUE   SOURCE
tank/vm/db01 compression    lz4     local
tank/vm/db01 compressratio  1.62x   -

Значення: LZ4 увімкнено і допомагає.

Рішення: Залиште його. Якщо стиснення вимкнене на VM-дисках, увімкніть, якщо немає доведеного CPU-ші\

кання.

Завдання 6: перевірте, скільки снапшотів ви тягнете з собою

cr0x@server:~$ zfs list -t snapshot -o name,used,refer -S used | head
NAME                               USED  REFER
tank/vm/db01@hourly-2025-12-24-23  8.4G  96.4G
tank/vm/db01@hourly-2025-12-24-22  7.9G  96.4G
tank/vm/web01@hourly-2025-12-24-23 2.1G  10.2G
tank/nfs@daily-2025-12-24          1.4G  320G

Значення: Стовпець USED — це простір снапшота, унікальний для цього снапшота.

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

Завдання 7: визначте «простір, утримуваний снапшотами» на zvol

cr0x@server:~$ zfs get -o name,property,value usedbysnapshots,usedbydataset,logicalused tank/vm/db01
NAME         PROPERTY         VALUE
tank/vm/db01 usedbysnapshots  112G
tank/vm/db01 usedbydataset    96.4G
tank/vm/db01 logicalused      250G

Значення: Снапшоти утримують більше місця, ніж поточні «живі» дані.

Рішення: Ваша «політика бекапу» тепер — політика планування ємності. Виправте зберігання і розгляньте поведінку guest TRIM/UNMAP.

Завдання 8: перевірте здоров’я пулу та помилки перш ніж налаштовувати продуктивність

cr0x@server:~$ zpool status -v tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 02:14:33 with 0 errors on Sun Dec 22 03:10:11 2025
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        ONLINE       0     0     0
          mirror-0                  ONLINE       0     0     0
            ata-SSD1                ONLINE       0     0     0
            ata-SSD2                ONLINE       0     0     0

errors: No known data errors

Значення: Пул здоровий, scrub пройшов чисто.

Рішення: Якщо ви бачите checksum errors або degraded vdevs, зупиніться. Виправте апарат/шлях до накопичувача перед дискусією zvol vs dataset.

Завдання 9: дивіться IO в реальному часі по dataset/zvol

cr0x@server:~$ zpool iostat -v tank 2 3
                              capacity     operations     bandwidth
pool                        alloc   free   read  write   read  write
--------------------------  -----  -----  -----  -----  -----  -----
tank                        1.12T  8.44T    210    980  12.4M  88.1M
  mirror-0                  1.12T  8.44T    210    980  12.4M  88.1M
    ata-SSD1                    -      -    105    490  6.2M   44.0M
    ata-SSD2                    -      -    105    490  6.2M   44.1M
--------------------------  -----  -----  -----  -----  -----  -----

Значення: Ви бачите read/write IOPS і пропускну здатність; це показує, чи ви обмежені IOPS або throughput.

Рішення: Велика кількість write IOPS з низьким bandwidth вказує на дрібні випадкові записи — тут мають значення volblocksize і шлях sync.

Завдання 10: перевірте навантаження на ARC (бо RAM — ваш перший рівень зберігання)

cr0x@server:~$ arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  size     c
12:20:01   532    41      7    12   2     29   5      0   0  48.1G  52.0G
12:20:02   611    58      9    16   2     42   7      0   0  48.1G  52.0G
12:20:03   590    55      9    15   2     40   7      0   0  48.1G  52.0G

Значення: Показники пропуску ARC низькі; кешування здорове.

Рішення: Якщо miss% постійно високий під навантаженням, додавання RAM часто кращий варіант, ніж хитрі зміни макету зберігання.

Завдання 11: чи є zvol sparse (тонкий) і чи це безпечно

cr0x@server:~$ zfs get -o name,property,value refreservation,volsize,used tank/vm/web01
NAME          PROPERTY        VALUE
tank/vm/web01 refreservation  none
tank/vm/web01 volsize         80G
tank/vm/web01 used            10.2G

Значення: Немає refreservation: фактично це thin з точки зору гарантованого простору.

Рішення: Якщо запускаєте критичні VM, встановіть refreservation, щоб гарантувати місце, або прийміть ризик pool-full відмов.

Завдання 12: підтвердьте ashift (ваш базовий вирівнювач фізичного сектора)

cr0x@server:~$ zdb -C tank | grep -E 'ashift|vdev_tree' -n | head
45:        vdev_tree:
67:            ashift: 12

Значення: ashift=12 означає 4K сектори. Зазвичай правильно для сучасних SSD і HDD.

Рішення: Якщо ashift неправильний (занадто малий), продуктивність може бути назавжди знижена; потрібно перебудувати пул, а не «налаштовувати».

Завдання 13: оцініть розмір send перед реплікацією по вузькому каналу

cr0x@server:~$ zfs send -nvP tank/vm/db01@hourly-2025-12-24-23 | head
send from @hourly-2025-12-24-22 to tank/vm/db01@hourly-2025-12-24-23 estimated size is 18.7G
total estimated size is 18.7G

Значення: Інкрементальна реплікація все ще 18.7G — перезапис блоків високий.

Рішення: Зменште частоту/утримання снапшотів, покращте guest TRIM або змініть архітектуру (зберігання даних у dataset) якщо можливо.

Завдання 14: перевірте, чи TRIM увімкнено на пулі

cr0x@server:~$ zpool get -o name,property,value autotrim tank
NAME  PROPERTY  VALUE
tank  autotrim  on

Значення: Пул відправляє TRIM у SSD.

Рішення: Якщо autotrim вимкнено на SSD-пулах, розгляньте ввімкнення — особливо якщо ви покладаєтеся на звільнення простору від zvol гостей.

Завдання 15: перевірте властивості по dataset, що змінюють поведінку непомітно

cr0x@server:~$ zfs get -o name,property,value atime,xattr,primarycache,logbias tank/vm
NAME     PROPERTY      VALUE
tank/vm  atime         off
tank/vm  xattr         sa
tank/vm  primarycache  all
tank/vm  logbias       latency

Значення: atime off зменшує записи метаданих; xattr=sa зберігає xattr ефективніше; logbias=latency віддає перевагу латентності sync.

Рішення: Для VM zvols logbias=latency зазвичай доречно. Якщо logbias=throughput — перевірте, чи це не копіювання налаштування без розуміння причин.

Швидка діагностика (знайти вузьке місце за хвилини)

Коли продуктивність погана, дискусія про zvol vs dataset часто відволікає. Використайте цю послідовність, щоб швидко знайти реальне обмеження.

Перше: доведіть, що пул не хворий

  1. Запустіть: zpool status -v
    Шукайте: degraded vdevs, checksum errors, resilver in progress, повільний scrub.
    Інтерпретація: Якщо пул нездоровий — все інше шум.
  2. Запустіть: dmesg | tail (і логи вашої ОС)
    Шукайте: link resets, timeouts, NVMe errors, HBA issues.
    Інтерпретація: Флапаючий шлях диска виглядає як «випадкові сплески латентності».

Друге: класифікуйте IO (малі випадкові vs великі послідовні, sync vs async)

  1. Запустіть: zpool iostat -v 2
    Шукайте: високу кількість IOPS з низьким MB/s (випадкові) vs високий MB/s (послідовні).
    Інтерпретація: Випадковий IO навантажує латентність, послідовний — пропускну здатність.
  2. Запустіть: zfs get sync tank/... і перевірте налаштування застосунків
    Шукайте: sync-важкі навантаження без SLOG або на повільних носіях.
    Інтерпретація: Sync-записи виявлять найповільніший шлях до надійного зберігання.

Третє: перевірте пам’ять і кешування перед тим як купувати обладнання

  1. Запустіть: arcstat 1
    Шукайте: високий miss%, ARC не росте, тиск пам’яті.
    Інтерпретація: Якщо ви постійно промахуєтесь у кеші, змушуєте диск читати те, що можна уникнути.
  2. Запустіть: zfs get primarycache secondarycache tank/...
    Шукайте: чи хтось встановив кешування тільки метаданих «щоб зекономити RAM».
    Інтерпретація: Це може бути виправданим, але часто — випадкова самопоразка.

Четверте: перевірте розміри блоків і податок снапшотів

  1. Запустіть: zfs get recordsize tank/dataset або zfs get volblocksize tank/volume
    Інтерпретація: Невідповідність = ампліфікація = хвостова латентність.
  2. Запустіть: zfs get usedbysnapshots tank/...
    Інтерпретація: Якщо снапшоти утримують масивний простір, вони також збільшують метадані та роботу алокації.

Три корпоративні міні-історії (анонімізовані, але болісно реальні)

Міні-історія 1: інцидент через хибні припущення («zvol — це по суті dataset»)

Середня SaaS-компанія мігрувала з legacy SAN на ZFS. Інженер зберігання — розумний, швидкий, трохи надто впевнений — стандартизувався на zvol для всього
«бо диски VM — це volumes, і саме так робив SAN». NFS-подібне сховище застосунку також перемістили в ext4 на zvol. У тестах працювало. У продукції теж поки що.

Перші ознаки були тонкі: вікна бекапів почали розтягуватися. Реплікація почала пропускати RPO, але тільки на певних томах.
Потім пул, що був стабільним місяцями, раптом впав у край по ємності. «У нас 30% вільного», — хтось сказав, вказуючи на дашборд пулу. «То чому не можна створити новий VM-диск?»

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

Паралельно навантаження, схоже на NFS, всередині гостя ext4 не мало причин перебувати на zvol взагалі. Вони хотіли файлову семантику, але побудували торт
file-on-block-on-ZFS. Реакція on-call була видалити «старі снапшоти», поки пул не заспівав, що працювало тимчасово і потім стало ритуалом надзвичайної ситуації.

Виправлення не було гламурним: міграція NFS-подібних даних у datasets, експортування напряму, введення адекватного зберігання снапшотів для VM zvols і
валідація TRIM end-to-end. Розв’язати дизайн, побудований на хибному припущенні «volume vs filesystem — це просто пакування», зайняло місяць ретельної міграції.

Міні-історія 2: оптимізація, що обернулася проти («set sync=disabled, все нормально»)

Інша організація, пов’язана з фінансами і вкрай алергічна до простоїв, вела віртуалізований кластер баз даних. Латентність під час піку росла.
Хтось знайшов в форумах чарівні слова sync=disabled і запропонував це як швидке рішення. Зміна була зроблена на ієрархії zvol, що підсилювала VM-диски.

Латентність одразу покращилася. Графіки виглядали чудово. Команда оголосила перемогу і перейшла до інших пожеж. Протягом кількох тижнів усе було спокійно,
що якраз і навчає ризик ігнорувати його.

Потім стався подій з живленням: не акуратне завершення, не граційний failover — просто момент, коли UPS не впорався. Гіпервізор піднявся.
Деякі VMs завантажились. Кілька — ні. База даних завелася, але відкотила більше транзакцій, ніж комусь подобалося, і хоча б одна ФС вимагала відновлення.

Рев’ю інциденту було незручним, бо ніхто не міг прямо сказати, що вони не обміняли довговічність на продуктивність. Вони обміняли. Це те, що робить налаштування.
Відкатом було відновлення sync=standard і додавання належного SLOG-пристрою з захистом від втрати живлення. Довгострокове виправлення — культурне:
жодне «виправлення продуктивності», що змінює семантику довговічності, без письмового прийняття ризику і тесту симуляції втрати живлення.

Міні-історія 3: нудна, але правильна практика, що врятувала день (перевірка розміру send і дисципліна снапшотів)

Велика внутрішня платформа вела два датацентри з реплікацією ZFS між ними. У них була тиха, нудна практика: перед підключенням нового навантаження вони
робили тижневу «реплікаційну репетицію» зі снапшотами і zfs send -nvP, щоб оцінити інкрементальні розміри. Також вони дотримувались політик утримання
снапшотів явно: коротке зберігання для churn-томів, довше — для dataset з стабільними даними.

Команда продукту попросила «годинні снапшоти на шість місяців» для флоту VM. Платформна команда не сперечалась філософськи. Вони провели репетицію.
Інкрементали були величезними і непередбачуваними, і WAN-лінк регулярно б був насичений. Замість «ні» вони запропонували нудну альтернативу:
щоденне довготривале зберігання, годинне коротке зберігання, плюс бекапи на рівні застосунку для критичних даних. Частину даних вони також винесли з VM-дисків
у datasets через NFS, бо це були файлові дані, що прикидалися блочними.

Через місяці збій в одному сайті змусив зробити фейловер. Реплікація була актуальна, відновлення передбачуване, і постмортем був приємно неінформативним.
Похвала дісталась практиці, якій ніхто не хотів займатись, бо це не було «інженерією», а було «процесом». Вона врятувала їх.

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

1) Зберігання VM повільне і стрибкоподібне, особливо під час оновлень

  • Симптоми: хвостові сплески латентності, зависання UI, повільні завантаження, періодичні IO-стали.
  • Корінь: zvol volblocksize не відповідає розміру IO гостя; снапшоти утримуються занадто довго; sync-записи блокуються повільним носієм.
  • Виправлення: перебудувати zvol з розумним volblocksize (часто 8K або 16K для загальних VM), зменшити утримання снапшотів, перевірити SLOG для sync-важких навантажень.

2) Пул показує багато вільного місця, але ви стикаєтесь з поведінкою “out of space”

  • Симптоми: невдачі алокації, блокування записів, неможливість створити нові zvol, дивний ENOSPC в гостях.
  • Корінь: thin provisioning без refreservation; снапшоти утримують простір; пул надто заповнений (ZFS потребує запасу).
  • Виправлення: вимагати резервації для критичних zvols, видаляти/вичерпувати снапшоти, тримати пул нижче розумного порогу використання, реалізувати алерти ємності з урахуванням росту снапшотів.

3) Інкрементали реплікації великі для VM на zvol

  • Симптоми: send/receive триває вічно, насичення мережі, пропускання RPO.
  • Корінь: перезапис блоків гостьовою ФС; відсутність TRIM/UNMAP; невідповідна частота снапшотів.
  • Виправлення: увімкнути та перевірити TRIM від гостя до zvol, відрегулювати каденцію снапшотів, перемістити файлові дані у datasets, протестувати estimated send sizes перед політикою.

4) «Ми вимкнули sync і нічого поганого не сталося» (поки що)

  • Симптоми: дивовижна латентність; підозріло спокійні дашборди; відсутність негайних збоїв.
  • Корінь: змінена семантика довговічності; ви підтверджуєте записи раніше, ніж вони безпечні.
  • Виправлення: повернути sync=standard або sync=always як потрібно; додати належний SLOG; тестувати сценарії втрати живлення і документувати прийняття ризику, якщо наполягаєте на «обмані фізики».

5) NFS-навантаження працює погано, якщо зберігається всередині VM на zvol

  • Симптоми: навантаження, що багато працює з метаданими, повільне; бекапи і відновлення незручні; відлагодження болісне.
  • Корінь: зайве нашарування: файлове навантаження поміщене всередину гостя ФС на zvol, втрачені оптимізації ZFS на рівні файлів і видимість.
  • Виправлення: зберігайте і експортуйте як dataset напряму; налаштуйте recordsize і atime; спростіть стек.

6) Відкат снапшоту «працює», але застосунок запускається пошкодженим

  • Симптоми: після відкату ФС монтується, але дані застосунку неконсистентні.
  • Корінь: невідповідність crash-consistency; снапшоти zvol захоплюють блоки, а не координовану паузу застосунку; снапшоти dataset теж можуть бути некоординованими без координації.
  • Виправлення: зупиняти застосунки (fsfreeze, flush бази даних, hooks гостьового агента гіпервізора) перед снапшотом; періодично перевіряти процедури відновлення.

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

Покроково: вибір dataset vs zvol для нового навантаження

  1. Визначте інтерфейс, який потрібен: файловий протокол (NFS/SMB/локальне монтування) → dataset; блочний протокол (iSCSI/VM диск) → zvol.
  2. Запишіть припущення про IO-шаблон: здебільшого послідовне? здебільшого випадкове? sync-важке? Це вирішує recordsize/volblocksize і потребу в SLOG.
  3. Виберіть найпростішу робочу схему: уникайте file-on-block-on-ZFS, якщо це не необхідно.
  4. Увімкніть compression=lz4 за замовчуванням, якщо немає доказів зворотного.
  5. Визначте політику снапшотів наперед: частота і утримання; не дозволяйте їй рости як «замінник бекапу».
  6. Визначте очікування реплікації: проведіть репетицію з estimated send sizes, якщо вам важливі RPO/RTO.
  7. Оберіть обмежувачі ємності: резервації для критичних zvols; квоти для datasets; тримайте запас у пулі.
  8. Документуйте відновлення: як відновити файл, VM або базу даних; включіть кроки з координації quiesce.

Чекліст: конфігурація zvol для дисків VM (продуктивний базовий набір)

  • Створіть з розумним volblocksize (часто 8K або 16K; узгодьте з гостем і гіпервізором).
  • Увімкніть compression=lz4.
  • Тримайте sync=standard; додайте SLOG, якщо sync-латентність важлива.
  • Плануйте утримання снапшотів відповідно до churn; тестуйте zfs send -nvP для оцінки реплікації.
  • Перевірте TRIM/UNMAP end-to-end, якщо очікуєте повернення простору.
  • Розгляньте refreservation для критичних гостьових ОС, щоб запобігти pool-full катастрофам.

Чекліст: конфігурація dataset для застосунків і файлового зберігання

  • Виберіть recordsize згідно з IO: 1M для бекапів/медіа; менше — для навантажень, подібних до БД.
  • Увімкніть compression=lz4.
  • Вимкніть atime, якщо він справді не потрібен.
  • Використовуйте quota/refquota, щоб запобігти ситуації «один орендар з’їв пул».
  • Робіть снапшоти з чіткою політикою утримання, а не накопиченням.
  • Експортуйте через NFS/SMB з розумними клієнтськими налаштуваннями; вимірюйте реальними навантаженнями.

FAQ

1) Чи завжди zvol швидший за dataset для дисків VM?

Ні. Zvol може бути відмінним для дисків VM, але «швидше» залежить від поведінки sync, розмірів блоків, churn снапшотів і шляху IO гіпервізора.
Dataset, що містить QCOW2/raw файли, також може працювати дуже добре з правильним recordsize і налаштуванням кешування. Вимірюйте, не вірте інтуїції.

2) Чи можна змінити volblocksize пізніше?

Практично: ні. Розглядайте volblocksize як незмінний. Якщо ви помилилися, чисте вирішення — міграція на новий zvol з правильним розміром і контрольоване переключення.

3) Чи ставити recordsize=16K для баз даних на datasets?

Часто доречно, але не універсально. Багато баз даних використовують 8K сторінки; 16K може бути компромісом. Якщо ваше навантаження переважно послідовні сканування або великі блоби,
більший recordsize може допомогти. Профілюйте IO.

4) Чи є ZFS снапшоти бекапом?

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

5) Чому видалення файлів всередині VM не звільняє місце в пулі ZFS?

Тому що ZFS бачить блочний пристрій. Якщо гість не надсилає TRIM/UNMAP або стек не передає його далі, ZFS не знає, які блоки всередині гостя вільні.

6) Чи варто використовувати dedup на zvols, щоб заощадити місце?

Зазвичай ні. Dedup вимагає багато RAM і важкий в експлуатації. Стиснення зазвичай дає безпечний прибуток з меншим ризиком. Якщо ви хочете dedup — доведіть ефективність на реальних даних і закладіть RAM відповідно.

7) Чи допомагає SLOG всім записам?

Ні. SLOG допомагає синхронним записам. Якщо ваше навантаження в основному асинхронне, SLOG мало що змінить. Якщо навантаження sync-важке, коректний SLOG може стати різницею між «все добре» і «все горить».

8) Коли слід віддавати перевагу datasets для контейнерів?

Якщо ваша контейнерна платформа може використовувати ZFS datasets напряму (поширено для багатьох Linux-настроювань), datasets зазвичай дають кращу видимість і простіші операції,
ніж розміщення контейнерного сховища в VM-дисках на zvol. Мінімізуйте шари.

9) Чи можна безпечно використовувати sync=disabled для дисків VM, якщо є UPS?

UPS знижує ризик; він не усуває його. Kernel panics, controller resets, firmware bugs і людські помилки все ще існують. Якщо вам потрібна довговічність, тримайте семантику sync правильно і проектуйте апаратний шлях (SLOG з захистом від втрати живлення).

10) Який найкращий дефолт: zvol чи dataset?

За замовчуванням обирайте dataset, якщо споживач не вимагає блоку. Коли потрібен блок — використовуйте zvol навмисно: виберіть volblocksize, сплануйте снапшоти і підтвердіть TRIM та поведінку sync.

Наступні кроки, які можна виконати цього тижня

Ось практичний шлях, що зменшить майбутній біль без перетворення вашого сховища на науковий експеримент.

  1. Інвентаризуйте середовище: перелічіть datasets проти volumes і зіставте їх із навантаженнями. Будь-що «файлоподібне», що живе всередині zvol — червоний прапор для розслідування.
  2. Аудит незворотних налаштувань: перевірте volblocksize на zvols і recordsize на ключових datasets. Запишіть невідповідності з шаблонами навантажень.
  3. Виміряйте податок снапшотів: визначте, які zvols/datasets мають великий usedbysnapshots. Узгодьте утримання з бізнес-потребою, а не з тривогою.
  4. Перевірте поведінку sync: знайдіть будь-який sync=disabled і обробляйте як запит на зміну, що потребує явного прийняття ризику. Якщо sync-латентність проблема — інженеруйте вирішення з SLOG, а не сподівайтеся.
  5. Запустіть реплікаційну репетицію: використайте zfs send -nvP, щоб оцінити інкременти протягом тижня. Якщо числа дики, виправте драйвери churn перед обіцянкою жорстких RPO.

Одна перефразована ідея від John Allspaw (операції/надійність): Інциденти виникають через нормальну роботу в складних системах, а не через одну погану людину в поганий день.

Вибір між zvol і dataset — саме такого роду «звичайна робота» рішення. Прийміть його свідомо. Майбутнє-ви все одно матиме відмови,
але вони будуть цікавими — ті, які ви зможете виправити, а не повільними, що витікають через один неправильний примітив зберігання,
вибраний роками тому і захищений з гордістю.

Docker Compose: змінні середовища підводять вас — помилки в .env, що ламають продакшн

Аварія починається непомітно. Контейнер стартує з NODE_ENV=development, або ваша база раптом приймає з’єднання з
паролем за замовчуванням. Ніби нічого не «змінювалося» в додатку. CI зелений. Ви відправили той самий
Compose-файл, що й минулого тижня.

Змінилася найкрихкіша частина розгортання: невидимий ланцюжок змінних середовища, що проходить через Docker Compose, ваш shell і маленький файл .env,
який ніхто не переглядає, бо «це не код».
Це код. Просто його не лінтують.

Модель мислення, яка не брешe вам

Docker Compose використовує змінні середовища двома різними способами, і більшість продакшн-помилок трапляються, коли команди
трактують їх як одне і те ж:

1) Змінні, які використовує сам Compose (час рендеру)

Ці змінні існують, щоб відрендерити конфігурацію Compose: наприклад ${IMAGE_TAG} всередині
compose.yaml, COMPOSE_PROJECT_NAME або COMPOSE_PROFILES.
Compose вирішує їх перед тим, як стартувати контейнери. Якщо Compose отримує їх неправильно, ваші контейнери можуть навіть
не відповідати тому, що ви вважаєте задеплоєним.

2) Змінні, передані в контейнери (час виконання)

Ці змінні є частиною середовища контейнера: те, що ваш додаток читає через getenv.
Вони надходять із environment:, env_file: і інколи з вашого shell через неявну передачу.

Змінні часу рендеру впливають на фінальний YAML. Змінні часу виконання впливають на поведінку процесу в контейнері.
Плутанина між цими двома призводить до ситуацій, коли ви «виправляєте» баг у контейнері, редагуючи профіль shell на хості,
а потім дізнаєтеся, що systemd не читає ваш shell-профіль.

Одна операційна істина: не можна «просто перевірити .env». Потрібно перевірити, що Compose
реально відрендерив і що контейнер фактично отримав.

Цитата для столу: paraphrased idea«Сподівання — не стратегія.» (парафраз ідеї,
часто приписують Gordon Sullivan)

Жарт №1: Змінні середовища як офісні плітки — ніхто не визнає, що почав їх, але вони якось є в кожній кімнаті.

Факти та історія, які варто знати (щоб перестати сперечатися з YAML)

  • Факт 1: Файл .env, який використовує Docker Compose, не автоматично має той самий синтаксис, що й shell-скрипт. Це простіший парсер «KEY=VALUE» зі своїми нюансами.
  • Факт 2: Compose походить від Fig (2014), і багато поведінки змінних — це спадкова зручність, а не витончена архітектура.
  • Факт 3: Compose v2 реалізований як плагін Docker CLI, і поведінка може трохи відрізнятися між версіями, бо тепер він ближчий до екосистеми Docker CLI.
  • Факт 4: Compose використовує змінні середовища для рендерингу конфігурації та для середовища контейнерів; для кожного шляху діють різні правила пріоритету.
  • Факт 5: Інтерполяція змінних відбувається перед більшістю валідацій. Відсутня змінна може мовчки стати порожнім рядком і все одно утворити «валідне» YAML-значення.
  • Факт 6: env_file — це вхід для часу виконання контейнерів; він зазвичай не впливає на інтерполяцію Compose, якщо ви явно не завантажите змінні в shell або не використовуєте інструментарій, який це робить.
  • Факт 7: Команда docker compose config — це найближче до «сироватки правди»: вона показує повністю відрендерену конфігурацію, яку запустить Compose.
  • Факт 8: Той самий проект на двох хостах може відрендеритись по-різному, бо Compose читає середовище хоста, поточний каталог і опційні входи --env-file.
  • Факт 9: COMPOSE_PROJECT_NAME впливає на імена мереж, томів і контейнерів. Зміна імені проекту може «осиротити» старі томи та створити нові.

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

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

Першим: підтвердьте, що відрендерив Compose

  1. Запустіть docker compose config і перегляньте інтерпольовані значення (теги образів, порти, шляхи томів,
    назву проекту, профілі). Якщо відрендерений конфіг неправий — не витрачайте час в контейнерах.
  2. Перевірте на порожні рядки, «null»-подібні значення, неочікувані дефолти або дубльовані визначення сервісів через профілі.

Другим: підтвердьте, що контейнер фактично отримав

  1. Перегляньте середовище контейнера (docker inspect) або виведіть його всередині контейнера
    (env).
  2. Порівняйте це з тим, що ви думаєте, що встановили через env_file і environment.

Третім: підтвердьте, який .env файл і яке середовище хоста використовувались

  1. Перевірте робочий каталог і вибраний env файл. Якщо ви запускали Compose з невірного каталогу, можливо,
    ви читаєте не той .env.
  2. Перевірте CI/CD: чи передається --env-file? Чи експортуються змінні? Чи systemd обнуляє середовище?

Якщо зі сховищем або мережею щось дивно — підозрюйте назву проекту і імена томів

Зміна COMPOSE_PROJECT_NAME або імені каталогу може створити нові мережі й нові томи.
Додаток «втратив» дані, бо пише в інший том з іншим ім’ям.

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

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

Завдання 1: Перевірте версію Compose (поведінка відрізняється)

cr0x@server:~$ docker compose version
Docker Compose version v2.24.6

Значення: Ви на Compose v2.x. Добре — більшість сучасних поведінок і прапорів застосовні.
Якби це була v1, кілька прапорів і крайових випадків відрізнялися.
Рішення: Зафіксуйте цю версію в нотатках інциденту; якщо поведінка різниться між хостами, уніфікуйте версії.

Завдання 2: Подивіться, яку назву проекту бачить Compose

cr0x@server:~$ docker compose ls
NAME                STATUS              CONFIG FILES
payments-prod        running(6)          /srv/payments/compose.yaml

Значення: Проект — payments-prod. Мережі/томи будуть префіксовані цим іменем.
Рішення: Якщо ви очікували іншу назву проекту, зупиніться: можливо, ви працюєте з неправильним проектом.

Завдання 3: Відрендеріть повністю інтерпольований конфіг («правда»)

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ docker compose config
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3
    ports:
      - mode: ingress
        target: 8080
        published: "8080"
        protocol: tcp
  db:
    environment:
      POSTGRES_DB: payments
    image: postgres:15
volumes:
  payments-prod_db-data: {}
networks:
  default:
    name: payments-prod_default

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

Завдання 4: Визначте, який env файл використовується

cr0x@server:~$ ls -la /srv/payments/.env
-rw------- 1 root root 412 Jan  2 09:11 /srv/payments/.env

Значення: Локальний .env існує в директорії проєкту.
Рішення: Переконайтеся, що ви запускаєте Compose з цього каталогу; інакше ви не читаєте цей файл.

Завдання 5: Знайдіть засідки з пробілами та лапками в .env

cr0x@server:~$ sed -n '1,120p' /srv/payments/.env
IMAGE_TAG=1.9.3
DB_PASSWORD=correct-horse-battery-staple
LOG_LEVEL=info
API_BASE_URL=https://payments.internal
BAD_SPACES =oops
QUOTED="literal quotes?"

Значення: BAD_SPACES =oops викликає підозру: багато парсерів трактують цей ключ як BAD_SPACES (із пробілом наприкінці) або відхиляють його.
QUOTED="literal quotes?" може зберігати лапки в залежності від парсера.
Рішення: Виправте формат: без пробілів навколо =, уникайте лапок, якщо не впевнені в поведінці парсера.

Завдання 6: Перевірте, чи змінна відсутня в час рендеру

cr0x@server:~$ grep -n 'IMAGE_TAG' -n /srv/payments/compose.yaml
12:    image: registry.local/payments-api:${IMAGE_TAG}

Значення: Compose потребує IMAGE_TAG для рендерингу рядка образу.
Рішення: Переконайтеся, що IMAGE_TAG встановлений у правильному .env або експортований у середовищі, яке використовує Compose.

Завдання 7: Виявити мовчазну порожню інтерполяцію

cr0x@server:~$ IMAGE_TAG= docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:

Значення: Порожній IMAGE_TAG рендерить майже недійсний рядок образу, який все ще може пройти YAML-парсинг.
Рішення: Додайте перевірки обов’язкових змінних, використайте дефолти в інтерполяції (див. далі) і робіть CI відмовою на пусті значення.

Завдання 8: Перегляньте середовище всередині запущеного контейнера

cr0x@server:~$ docker compose exec -T api env | egrep 'DB_|LOG_LEVEL|API_BASE_URL'
API_BASE_URL=https://payments.internal
DB_HOST=db
LOG_LEVEL=info

Значення: Контейнер отримав змінні. Якщо щось відсутнє — це проблема інжекції середовища у часі виконання.
Рішення: Порівняйте з docker compose config і вмістом env_file.

Завдання 9: Підтвердити, що Docker вважає середовище авторитетним

cr0x@server:~$ docker inspect payments-prod-api-1 --format '{{json .Config.Env}}'
["API_BASE_URL=https://payments.internal","DB_HOST=db","LOG_LEVEL=info","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]

Значення: Це те, що Docker передасть процесу. Якщо цього немає — ваш додаток його не побачить.
Рішення: Якщо в конфігурації Compose воно є, але в inspect — ні, у вас дрейф розгортання або проблема з пересозданням.

Завдання 10: Виявити проблему «контейнер не був пересозданий»

cr0x@server:~$ docker compose up -d
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Running

Значення: Compose не пересоздав контейнери. Зміни env не застосуються до запущеного контейнера, доки його не пересоздати.
Рішення: Якщо ви змінили змінні, примусово пересоздайте: docker compose up -d --force-recreate.

Завдання 11: Примусово пересоздати і підтвердити застосування нового env

cr0x@server:~$ docker compose up -d --force-recreate
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Started

Значення: API-контейнер був перезапущений/пересозданий.
Рішення: Повторіть Завдання 8/9, щоб підтвердити, що зміни середовища дійсно застосовані.

Завдання 12: Виявити дрейф імені проекту, що створює «нові» томи

cr0x@server:~$ docker volume ls | grep -E 'payments.*db-data'
local     payments-prod_db-data
local     payments_db-data

Значення: Існує два схожі томи — ймовірно з різних імен проектів.
Рішення: Підтвердіть, який том приєднано до запущеного DB-контейнера, перш ніж щось видаляти.

Завдання 13: Підтвердити, який том використовує контейнер

cr0x@server:~$ docker inspect payments-prod-db-1 --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
payments-prod_db-data /var/lib/postgresql/data

Значення: БД використовує payments-prod_db-data.
Рішення: Якщо додаток «втратив дані», порівняйте з томом, який ви очікували. Не видаляйте томи, поки не доведете, що вони не використовуються.

Завдання 14: Визначити, який .env використовується, якщо ви запускаєте з іншого каталогу

cr0x@server:~$ cd /tmp
cr0x@server:~$ docker compose -f /srv/payments/compose.yaml config | head
services:
  api:
    image: registry.local/payments-api:

Значення: Запуск з /tmp ймовірно призвів до того, що Compose не знайшов потрібний /srv/payments/.env, тож інтерполяція провалилась.
Рішення: Завжди запускайте з директорії проєкту або вказуйте --env-file /srv/payments/.env.

Завдання 15: Доведіть пріоритет між хостом та .env

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ export IMAGE_TAG=2.0.0
cr0x@server:~$ docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:2.0.0

Значення: Експортована змінна хоста переважила значення в .env.
Рішення: У продакшні уникайте залежності від «що експортовано в shell». Зробіть джерело змінних явним.

Завдання 16: Виявити випадкові Windows CRLF у .env (так, ще буває)

cr0x@server:~$ file /srv/payments/.env
/srv/payments/.env: ASCII text, with CRLF line terminators

Значення: CRLF може прослизнути в ключі/значення, спричиняючи загадкові «змінна не знайдена» або значення з прихованим \r.
Рішення: Конвертуйте в LF: sed -i 's/\r$//' /srv/payments/.env, потім перерендеріть конфіг.

Завдання 17: Підтвердити приховані символи повернення рядка в конкретному значенні

cr0x@server:~$ python3 -c 'import os;print(repr(open("/srv/payments/.env","rb").read().splitlines()[-1]))'
b'QUOTED="literal quotes?"\r'

Значення: Той прихований \r реальний. Він може зламати токени авторизації, URL або паролі.
Рішення: Нормалізуйте кінці рядків у CI і трактуйте .env як текстовий артефакт, який потребує перевірок.

Завдання 18: Показати відмінності між env_file і environment у фінальному конфігу

cr0x@server:~$ docker compose config | sed -n '1,80p'
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3

Значення: Ви бачите inline-значення environment: явно. Якщо ви використовували env_file, воно може не розгорнутись у виводі так, як ви очікуєте.
Рішення: Якщо аудит залежить від видимості змінних, не покладайтеся лише на env_file; використовуйте явні кроки валідації конфігурації.

Пріоритети та область дії: хто перемагає при колізіях змінних

Більшість команд не може відповісти на це питання без гадання: «Якщо я встановив FOO в shell, в .env
і в environment:, що переможе?» Відповідь залежить від того, чи ви маєте на увазі час рендеру чи час виконання.
Саме тому це постійно ламає продакшн: люди говорять про різні речі.

Пріоритет рендеру (інтерполяція в compose.yaml)

Коли Compose інтерполює ${VAR} в YAML, він дивиться на свої джерела середовища. На практиці експортоване середовище процесу Compose — головний претендент. Локальний .env часто слугує запасним зручним джерелом.

Іншими словами: якщо ваш CI експортує IMAGE_TAG, воно зазвичай перекриє .env. Якщо ваш unit systemd запускає Compose з майже порожнім середовищем, він ігнорує те, що є в інтерактивному shell.

Операційне правило: змінні часу рендеру мають бути явними. Передавайте їх через CI контрольовано або вказуйте явний env файл через --env-file. Не дозволяйте випадковим shell-функціям вирішувати.

Пріоритет часу виконання (що отримує контейнер)

Середовище контейнера будується з визначень сервісів у Compose:

  • environment: записи явні й видимі у Compose-файлі.
  • env_file: завантажує ключі/значення з файлу в середовище контейнера.
  • Деякі змінні можуть передаватися з хоста, якщо ви посилаєтеся на них у environment: без значення, залежно від синтаксису.

Практичне правило: тримайте environment: як контракт API для того, що очікує контейнер, і
тримайте env_file як реалізацію того, як ви подаєте ці значення. Під час налагодження завжди перевіряйте, що справді прибуло через docker inspect.

Назва проекту — це прихована частина історії вашого середовища

COMPOSE_PROJECT_NAME здається «лише ім’ям». Це не так.
Воно змінює імена мереж і томів. Якщо ви прив’язуєте дані до томів і моніторинг до імен контейнерів,
назва проекту — це продакшн-змінна, чи визнаєте ви це, чи ні.

Інтерполяція та парсинг: гострі краї .env і Compose

Формат .env виглядає як shell. Він ним не є. Це файл ключ-значення з достатньою гнучкістю,
щоб зробити вас занадто впевненим.

Пробіли: тихий вбивця

Багато парсерів трактують KEY =value як інший ключ, ніж KEY=value, або відхиляють його.
В обох випадках ви отримаєте «KEY не встановлений», і Compose тихо підставить порожній рядок.

Не будьте «терпимими». Будьте суворими. Для продакшн-файлів env:
без пробілів навколо знака рівності, і ключі повинні відповідати [A-Z0-9_]+.

Лапки: інколи літеральні, інколи знімаються, завжди заплутують

В деяких екосистемах FOO="bar" означає значення bar без лапок. В інших — лапки є частиною значення.
Поведінка Compose може здивувати в залежності від парсера, який використовується.
Єдиний безпечний підхід: уникайте лапок у .env, якщо ви не перевірили поведінку через docker compose config та запущений контейнер.

Дефолти інтерполяції: використовуйте, але розумійте їх

Compose підтримує конструкції на кшталт:
${VAR:-default} та ${VAR?error} у багатьох контекстах.
Саме тут команди можуть перетворити невидиму помилку на голосну.

Якщо IMAGE_TAG має існувати в продакшні — зробіть його обов’язковим. Якщо LOG_LEVEL може мати дефолт — задайте його.
Спрацьовування відмов має бути явним для всього, що змінює поведінку невидимим чином.

Порожнє відрізняється від відсутнього

Інтерполяція Compose часто трактує порожню змінну як «встановлену», що може відбити дефолти. Якщо пайплайн встановив
IMAGE_TAG у порожній рядок (таке буває), ваш ${IMAGE_TAG:-latest} може поводитися не так, як ви очікуєте.
Тестуйте це явно у вашому середовищі.

.env проти env_file: схожий зовнішній вигляд, різна семантика

Файл .env (для Compose) використовується для інтерполяції змінних Compose і деяких налаштувань Compose.
Директива env_file: подає змінні в середовище контейнера під час виконання.
Люди плутають їх, бо файли виглядають однаково. Результат — надійний хаос.

Якщо ви хочете, щоб значення впливали на інтерполяцію, вони мають бути в середовищі, яке використовує Compose для рендеру
(експорт shell, явний --env-file або обробка env вашою оркестрацією). Якщо ви хочете значення всередині контейнера,
вони мають бути в environment: або env_file:.

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

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

Історія 1: Інцидент через хибне припущення

Команда фінтеху запускала клієнтський API на парі VM з Docker Compose. У репозиторії був .env
і окремий prod.env, збережений на хості. В їхніх головах «Compose завантажує env з env_file». Вони були
напівправі і повністю приречені.

Compose-файл використовував ${IMAGE_TAG} для фіксації образу API. Змінні середовища для часу виконання бралися з
env_file: ./prod.env. Потрібен був хотфикс для релізу, тому інженер оновив IMAGE_TAG
у prod.env, запустив docker compose up -d і очікував, що новий образ розгорнеться.

Нічого не сталося. Інтерполяція Compose не дивиться в env_file для рендерингу поля image:.
Контейнери залишилися на старому тегу. Тим часом інженер також оновив runtime-змінну в prod.env
і припустив, що контейнер її підхопив; він не підхопив, бо Compose не пересоздав контейнер. В результаті у них був
старий код, старе середовище і нове переконання.

Через дві години API почав кидати помилки, що виглядали як регресія хотфиксу. Це не була регресія — хотфикс
ніколи не задеплоївся. Моніторинг показував «деплой пройшов», бо джоб завершився; він не валідував
docker compose config або не перевіряв ID образів запущених контейнерів.

Виправлення було нудним: зробити теги образів обов’язковими змінними рендеру і встановлювати їх явно в команді деплою,
перевіряти через docker compose config, потім примусово пересоздати або коректно оновлювати контейнери.
Вони також перестали використовувати prod.env як магічний файл, що «контролює все». Він контролює
тільки те, що ви явно до нього підключили.

Історія 2: Оптимізація, яка відгукнулась боком

Медіакомпанія хотіла пришвидшити деплоя. Хтось помітив, що пересоздання контейнерів займає час, особливо для сервісу з багатьма sidecar-ами.
Вони змінили процес: оновлювати .env, а потім запускати docker compose up -d
без примусового пересоздання, щоб «уникнути даунтайму».

Деякий час здавалося, що це працює — бо більшість змін були змінами тегів образів, і Compose підтягував і перезапускав сервіси при виявленні нового образу.
Але змінні середовища — не образи. Критична конфігураційна зміна перемкнула feature-flag маршрутизації запитів. Половина фліта оновилася
(на вузлах, де контейнери випадково пересоздалися), половина — ні. Вийшов split-brain, де запити ішли різними шляхами залежно від VM.

Налагодження було болісним, бо Compose-файл виглядав правильно, .env — правильно, а контейнери — всі «up».
Баг був у процесі: вони оптимізували ту єдину дію, яка надійно застосовує зміни env.

Плейбук відновлення виявився простим: якщо змінено env — контейнери пересоздаються. Якщо ви прагнете безперервної роботи,
робіть це через балансувальники і поступові рестарти, а не сподівайтеся, що Compose виведе ваш намір.

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

Команда B2B SaaS запускала Compose-стек для внутрішніх сервісів: метрики, джоб-ранери і легасі БД.
Вони не любили «хитрощі». Їхнє продакшн-розгортання вимагало трьох перевірок:
відрендерити конфіг, валідувати ID запущених образів і записати контрольну суму ефективного середовища.

Одної п’ятниці вмержили зміну, яка додала нову змінну RATE_LIMIT_MODE, використану в інтерполяції Compose
для вибору образу sidecar. Розробник додав її в .env.example, але забув у продакшн-джерелі.
CI-пайплайн теж її не експортував.

Job розгортання впав рано, бо їхній Compose-файл використовував ${RATE_LIMIT_MODE?must be set}.
Оце і є фокус: вони перетворили мовчазну порожню інтерполяцію на жорстку зупинку. Ніякого часткового деплою, ніякої містичної поведінки.

Вони виправили пайплайн, задеплоїли в понеділок, і ніхто не отримав пейджі. Було настільки нудно, що команда обурювалася.
Саме так розумієш, що все зроблено правильно.

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

1) Симптом: тег образу стає пустим або «latest» несподівано

Корінь: Відсутня або порожня змінна часу рендеру, Compose інтерполює в порожній рядок; або CI експортує порожню змінну, що перекриває .env.

Виправлення: Використовуйте обов’язкову інтерполяцію: image: myapp:${IMAGE_TAG?set IMAGE_TAG}. У CI відмовляйте, якщо IMAGE_TAG порожній. Валідуйте через docker compose config.

2) Симптом: «Я оновив .env, але контейнер не змінив поведінку»

Корінь: Контейнер не був пересозданий; запущений контейнер зберігає старе середовище.

Виправлення: Застосуйте зміни з docker compose up -d --force-recreate (або docker compose restart, якщо підходить, але пересоздання безпечніше для змін env). Перевірте через docker inspect ... Config.Env.

3) Симптом: прод використовує dev-налаштування хоча є prod.env

Корінь: Compose читає .env з поточного робочого каталогу, а не з потрібного шляху; або --env-file не передається в автоматизації.

Виправлення: У systemd/CI запускайте з директорії проєкту або вказуйте --env-file /srv/app/.env. Додайте перевірку контрольної суми env файлу під час деплою.

4) Симптом: автентифікація паролем не працює, хоча значення «виглядає правильно»

Корінь: CRLF або кінцеві пробіли в .env додають приховані символи (часто \r) у значення.

Виправлення: Нормалізуйте кінці рядків (sed -i 's/\r$//'), і валідуйте друком repr або hexdump значення у контрольованому тестовому контейнері.

5) Симптом: база «втратила» дані після редеплою

Корінь: Змінилася назва проекту (зміна імені каталогу, COMPOSE_PROJECT_NAME), створився новий том з іншим ім’ям.

Виправлення: Закріпіть назву проекту явно для продакшну. Аудитуйте docker volume ls і docker inspect mounts перед очищенням. Розглядайте імена томів як частину стану.

6) Симптом: змінні в контейнері не збігаються з тим, що в .env

Корінь: Плутанина між .env (рендеру) і env_file (runtime); або середовище хоста перекриває значення.

Виправлення: Визначте джерело авторитету. Для критичних runtime-значень використовуйте явні ключі environment: і подавайте їх із контрольованого env файлу. Для рендеру передавайте через --env-file та валідуйте вивід конфігурації.

7) Симптом: змінна з лапками поводиться дивно

Корінь: Лапки трактуються буквально або знімаються інакше, ніж очікували; різні парсери в ланцюжку.

Виправлення: Видаліть лапки в .env, якщо це можливо. Коли необхідні — перевірте через docker compose config і усередині контейнера.

8) Симптом: сервіс не стартує, мапінг портів нісенітниця

Корінь: Інтерполяція створила недійсний рядок порту (порожній, нечисловий, містить пробіли), але YAML все ще парситься.

Виправлення: Зробіть змінні обов’язковими і валідуйте порти в CI, грепаючи відрендерений конфіг. Використовуйте дефолти лише для безпечних дев-значень.

9) Симптом: «працює локально, падає в CI» з тим самим Compose-файлом

Корінь: Локальний shell експортує змінні, а CI — ні; або CI використовує іншу локаль/кінці рядків; або CI запускає з іншого каталогу.

Виправлення: Зробіть джерело env явним у CI. Друкуйте docker compose config (або принаймні релевантні рядки) і забезпечте детермінованість.

10) Симптом: секрети з’являються в логах або підтримці

Корінь: Зберігання секретів у .env та друк відрендереного конфігу або контейнерного env під час налагодження; змінні оточення легко витікають у логи, дампи і процес-лістинги.

Виправлення: Використовуйте Compose secrets коли можливо, або монтуйте файли з креденціалами з жорсткими правами. В інструментах інцидентів за замовчуванням редагуйте виводи env.

Чеклисти / покроковий план для продакшну

Чеклист A: Зробіть інтерполяцію Compose детермінованою

  1. Зафіксуйте джерело env: В автоматизації завжди запускайте з явним шляхом --env-file і фіксованим робочим каталогом.
  2. Зробіть критичні змінні обов’язковими: Використовуйте ${VAR?message} для тегів образів, зовнішніх endpoint-ів і назв проектів.
  3. Припиніть експортувати випадкові змінні: Очистіть середовище у CI job-ах. Якщо змінна потрібна — встановіть її явно.
  4. Рендерьте та порівнюйте: Зберігайте вихід docker compose config як артефакт збірки і робіть diff проти попередніх деплоїв.

Чеклист B: Зробіть середовище контейнера аудитованим

  1. Документуйте контракт: Перелічіть обов’язкові runtime-змінні для кожного сервісу (імена, призначення, дозволені значення).
  2. На користь явного environment:: Це робить контракт видимим під час рев’ю коду.
  3. Використовуйте env_file для масових значень, а не для містичних речей: Тримайте його мінімальним і структурованим. Уникайте змішування «dev» і «prod» в одному файлі.
  4. Пересоздавайте при зміні env: Якщо змінено runtime env — контейнери мають бути пересоздані. Плануйте даунтайм/поступові рестарти відповідно.

Чеклист C: Не допускайте дрейфу стану (томи/мережі)

  1. Зафіксуйте назву проекту: Встановіть name: в моделі Compose або COMPOSE_PROJECT_NAME у контрольованому джерелі env.
  2. Оголосіть томи явно: Використовуйте іменовані томи для стану; уникайте ненавмисних анонімних томів.
  3. Аудитуйте перед очищенням: Завжди переглядайте mount-и і посилання контейнерів перед видаленням томів.

Чеклист D: Трактуйте .env як продакшн-код

  1. Права: chmod 600 .env, якщо файл містить чутливі дані.
  2. Нормалізуйте кінці рядків: Примусовий LF у CI.
  3. Правила лінтингу: Ніяких пробілів навколо =, без табуляцій, без кінцевих пробілів, передбачувані шаблони ключів.
  4. Контроль змін: Вимагайте рев’ю для змін env і зберігайте історію (навіть якщо файл зберігається безпечно поза Git).

Операційні поради, що запобігають більшості інцидентів з .env

Використовуйте дефолти лише для зручності розробника, а не для безпеки продакшну

Дефолти на кшталт ${LOG_LEVEL:-debug} підходять для локальної роботи. У продакшні вони можуть перетворити відсутню конфігурацію
на неочікувану поведінку. Віддавайте перевагу явним значенням у продакшн-джерелах env і обов’язковим змінним для всього, що впливає на цілісність даних, автентифікацію або маршрутизацію.

Провалюйте раніше на хості, а не пізно в контейнері

Якщо змінна обов’язкова — відмовляйте на часі рендеру. Ви хочете, щоб деплой зупинився до того, як буде тягнутися образ, до того, як торкнеться томів або перезапустить щось.
Це дешевше й безпечніше.

Перестаньте трактувати секрети як «просто змінні оточення»

Змінні оточення тягнуть витоки. Вони витікають у звіти про помилки, debug-endpoint-и, списки процесів, випадкові bundle-і підтримки і людські скриншоти. Вони також лишаються в метаданих контейнера довше, ніж ви очікуєте.
Використовуйте механізми секретів, коли можете. Якщо не можете — відокремте секрети від небезпечних налаштувань і проєктуйте діагностичні команди так, щоб за замовчуванням редагували.

Зробіть конфігурацію спостережуваною

Система має повідомляти ефективну версію конфігурації, не виливаючи секретів. Контрольна сума конфігу, git-SHA, дайджест образу і небезпечна «режим»-змінна зазвичай достатні, щоб підтвердити, що система — це те, що ви думаєте.

FAQ

1) Чи Compose автоматично завантажує .env?

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

2) Чи .env — це те саме, що env_file?

Ні. .env зазвичай впливає на інтерполяцію часу рендеру Compose. env_file інжектить
змінні в контейнер під час виконання. Файли виглядають однаково; семантика різна. Плутанина між ними — класичний режим відмови.

3) Чому моя зміна .env не застосувалась після docker compose up -d?

Бо контейнери не поглинають нові змінні оточення автоматично. Якщо Compose не пересоздав контейнер,
запущене середовище залишиться старим. Використовуйте docker compose up -d --force-recreate, коли змінено env,
і перевіряйте через docker inspect.

4) Що переважає: експортовані shell-змінні чи .env?

У багатьох налаштуваннях експортовані змінні в середовищі процесу, що запускає Compose, перекривають значення з .env.
Ось чому «працює на моїй машині»: ваш shell експортує щось, чого CI не робить, або навпаки. Робіть джерело env явним в автоматизації.

5) Чи можна мати кілька env-файлів?

Так, але будьте навмисними в їх призначенні: один для рендеру (передається з --env-file) і, можливо, один або кілька для runtime-інжекції (env_file: на сервіс). Уникайте нашарувань стільки, щоб ніхто не міг передбачити результат.

6) Чому мій додаток бачить лапки у значеннях?

Тому що ваш парсер може трактувати лапки як літеральні. Формат .env не є універсальним стандартом і різні інструменти інтерпретують лапки й екрани по-різному. Якщо вам потрібні спеціальні символи, тестуйте точний шлях: рендер через docker compose config і runtime через docker inspect.

7) Як не допустити пустих змінних у продакшн?

Використовуйте обов’язкову інтерполяцію (${VAR?message}) для критичних значень і додавайте CI-перевірки, що відмовляють, якщо
відрендерений конфіг містить порожні теги образів, порти або імена хостів. Це одне з найефективніших виправлень.

8) Чому повторний деплой створив нові томи і «видалив» дані?

Ймовірно, змінилася назва проекту. Compose префіксує імена томів і мереж назвою проекту, яка походить від імені каталогу, явної конфігурації або середовища.
Зафіксуйте її для продакшну, щоб томи залишалися стабільними. Перед очищенням переконайтеся, що DB-контейнер приєднаний до очікуваного тому.

9) Чи безпечно друкувати docker compose config в CI-логах?

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

10) Коли використовувати Compose secrets замість env-змінних?

Використовуйте secrets коли можете: креденціали, API-токени, приватні ключі — усе, що ви пошкодуєте бачити в логах або дампах.
Змінні оточення підходять для нечутливої конфігурації і feature-toggle-ів. Якщо змушені тримати секрети в env — обмежте права доступу і мінімізуйте місця, де вони відображаються.

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

  1. Додайте «render check» у CI: запускайте docker compose config і відмовляйте на порожніх критичних полях
    (теги образів, порти, хости). Зберігайте відрендерений конфіг як артефакт з редагованими секретами.
  2. Зробіть критичні змінні обов’язковими: перетворіть ${VAR} на ${VAR?set VAR} для
    ключових точок інтерполяції в продакшні.
  3. Зафіксуйте назву проекту в продакшні: припиніть випадковий дрейф томів і мереж. Трактуйте її як стан.
  4. Стандартизуйтесь щодо запуску деплою: фіксований робочий каталог, явний --env-file,
    і політика: зміни env вимагають пересоздання або поступового рестарту.
  5. Перестаньте зберігати секрети в випадкових .env: перенесіть їх у механізм секретів або в змонтовані файли і
    налаштуйте діагностичні інструменти так, щоб не витікали під час інцидентів.

Docker Compose працює. Проблема — у непроявлених припущеннях навколо .env.
Робіть змінні явними, спостережуваним відрендерений конфіг і верифікуйте середовище контейнера.
Тоді наступна «містична регресія» стане п’ятихвилинним diff’ом замість уїк-енду дебагу.

Google Search Console — «Сторінка з переадресацією»: коли це нормально і коли шкодить

Ви відкриваєте Google Search Console, переходите до Сторінки, і ось вона: «Сторінка з переадресацією».
Не індексується. Немає права на показ. Не запрошена на вечірку. Тим часом ваш продуктовий менеджер (PM) питає, чому впав трафік, маркетингова команда оновлює дашборди, ніби це спорт, а ви дивитесь на ідеально «працюючий» редирект у браузері.

Погляд із продакшн-систем: «Сторінка з переадресацією» часто є нормальним і навіть бажаним станом. Але це стає токсичним,
коли ви випадково навчите Google, що ваші пріоритетні URL нестабільні, суперечливі або повільні у вирішенні.
Це різниця між чистим шаром канонізації і лабіринтом редиректів, що створений колегіально.

Що насправді означає «Сторінка з переадресацією»

У Search Console «Сторінка з переадресацією» — це статус індексації, а не моральний вирок.
Це означає, що Google спробував отримати URL і отримав відповідь з редиректом замість фінального контенту
(зазвичай 200). Google вирішив, що початковий URL не є канонічним, індексованим URL. Тому він не індексує
цей початковий URL; натомість слідує за редиректом і (можливо) індексує цільовий URL.

Саме тому у цьому звіті часто багато URL, які ви навмисно не хочете індексувати: старі HTTP-варіанти,
старі шляхи після міграції, варіанти з косою рискою наприкінці, варіанти в різному регістрі та параметризований непотріб.

Ключове операційне питання не «Як змусити цей статус зникнути?» а:
Чи отримують індексацію, ранжування та стабільне обслуговування правильні цільові URL?

Що це не означає

  • Не є помилкою за замовчуванням. Редирект — це валідна відповідь.
  • Не доказ того, що Google збитий з пантелику. Це доказ того, що Google звертає увагу.
  • Не гарантія, що цільовий URL буде індексовано. Редирект може вести у безвихідь.

Як Google обробляє це «під капотом» (практична версія)

Googlebot отримує URL A, отримує редирект до URL B і фіксує зв’язок.
Якщо сигнали послідовні (редирект стабільний, B повертає 200 і є канонічним, внутрішні посилання вказують на B, карта сайту
містить B, hreflang вказує на B тощо), Google схильний консолідувати сигнали індексації на B і відкинути A.
Якщо сигнали суперечливі — ви отримаєте еквівалент плечима від Search Console.

Одна інженерна реальність, яку іноді недооцінюють SEO-фахівці: редиректи не є безкоштовними. Вони витрачають бюджет краулінгу,
додають затримку, можуть дивно кешуватися і створювати прикордонні випадки у взаємодії CDN, браузерів і ботів. У продакшні
найпростішим редиректом є той, який вам не потрібен.

Коли це нормально (і коли варто ігнорувати)

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

Сценарії, де це нормальне

  • HTTP → HTTPS. Ви хочете, щоб HTTP-URL постійно переадресовувалися.
    GSC часто показуватиме HTTP-URL як «Сторінка з переадресацією». Це нормально.
  • www → без www (або навпаки). Непріоритетний хост має редиректити на пріоритетний.
  • Нормалізація слешу в кінці. Оберіть один варіант і редиректіть інший.
  • Стара структура URL після міграції. Старі шляхи редиректять на нові.
  • Очищення параметрів (деякі параметри трекінгу, ідентифікатори сесій). Редирект або канонікалізація в залежності від семантики.
  • Локалізоване або регіональне роутування, якщо виконане обережно (і не на базі ненадійних IP-евристик).

Як виглядає «нормально» в метриках

  • Цільові URL з’являються в розділі Індексується і показують покази/клацання.
  • Редиректи одношагові (A → B), а не A → B → C → D.
  • Тип редиректу переважно 301/308 для постійних переміщень (за рідкісними винятками).
  • Внутрішні посилання, rel=canonical та карти сайту переважно вказують на цільові URL.
  • Лог-файли сервера показують успішні звернення Googlebot до цільового контенту (200).

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

Коли це шкодить (і як проявляється)

«Сторінка з переадресацією» шкодить, коли вона приховує нестиковку між тим, що ви хочете індексувати, і тим,
що Google здатен надійно отримати, відрендерити та вважати канонічним. Також це шкодить, коли редиректи використовуються як тимчасове
латкування для глибших проблем (дубльований контент, неправильно маршрутизовані локалі, биті внутрішні посилання, непослідовний протокол/хост).

Сценарії високого впливу

1) Ланцюги редиректів і витрата краул-бюджету

Ланцюги виникають, коли накопичуються кілька правил нормалізації: HTTP → HTTPS → www → додати слеш → переписати на новий шлях.
Кожен етап додає затримку і ймовірність помилки. Google слідує за ланцюгами, але не безмежно, і не без витрат.
Ланцюги також підвищують шанс випадково створити цикл.

2) Редирект на неіндексований ресурс

Редирект на URL, що повертає 404, 410, 5xx, заблокований robots.txt або має «noindex» — це тихе видалення сторінок.
GSC покаже початковий URL як «Сторінка з переадресацією», але реальна проблема — що цільовий URL не можна індексувати.

3) Використання 302/307 як «постійного» редиректу

Тимчасові редиректи не завжди погані, але ними легко зловживати. Якщо ви тримаєте 302 місяцями, Google може
зрештою трактувати це як 301 або навпаки — зберігати старий URL в індексі довше, ніж потрібно.
Це не стратегія; це нерішучість у формі HTTP.

4) Змішані сигнали: редирект каже одне, canonical — інше

Якщо URL A редиректить на B, але canonical сторінки B вказує назад на A (або на C), ви створили суперечку за канонічність.
Google обере переможця. Це може бути не ваш улюблений варіант.

5) Редиректи, що тригеряться UA, гео, куками або JS

Умовні редиректи — найшвидший шлях отримати «працює на моєму ноуті» SEO. Googlebot — не ваш браузер.
Ваш крайовий CDN — не ваш origin. Origin — не ваш staging. Якщо редирект залежить від умов,
тестуйте його так, як бачить його Google.

6) Карти сайту, напхані URL з редиректами

Карта сайту призначена для переліку канонічних, індексованих URL. Коли ви даєте Google тисячі перенаправлених URL у sitemap,
ви по суті посилаєте його у доручення. Він виконуватиме довгий час, а потім тихо знизить пріоритет.

Жарт №1: Ланцюги редиректів — як ланцюги корпоративного погодження: ніхто не знає, хто додав останній крок, але всі терплять затримку.

Як виглядає «шкода» у результатах

  • Переважні URL не індексуються або індексуються нестабільно.
  • У звіті Coverage з’являються сплески «Duplicate, Google chose different canonical» поруч із редиректами.
  • У звіті Performance покази падають для мігрованих сторінок і не відновлюються.
  • Логи сервера показують, що Googlebot натрапляє на редиректні URL замість канонічних цілей.
  • Велика частина краулінгу витрачається на редиректи замість контентних URL.

Фізика редиректів: 301/302/307/308, кешування та канонізація

Коди стану, з погляду операторів

  • 301 (Moved Permanently): «Це нова адреса.» Агресивно кешується клієнтами та проміжними вузлами. Добре для канонічних переміщень.
  • 302 (Found): «Тимчасово тут.» Історично трактувався як тимчасовий. Пошукові системи стали гнучкішими, але ваш намір має значення.
  • 307 (Temporary Redirect): Як 302, але суворіше зберігає семантику методу. Більш релевантний для API.
  • 308 (Permanent Redirect): Як 301, але збереження семантики методу. Стає більш поширеним.

Теги canonical vs редиректи: обирайте інструмент

Редирект — це інструкція на стороні сервера: «Не залишайся тут.» Canonical — підказка всередині сторінки: «Індексувати інший.»
Якщо ви можете безпечно редиректити (без впливу на користувачів, без функціональної потреби зберігати старий URL), робіть це. Це сильніше й чистіше.
Використовуйте canonical для випадків, коли дублі мають залишатися доступними (фільтри, сортування, параметри трекінгу, друковані версії).

Але не змішуйте їх необережно. Якщо ви робите редирект A → B, то canonical сторінки B майже завжди має бути B. Ви хочете одну узгоджену історію.

Затримка та надійність: чому SRE звертають увагу на «просто редирект»

Кожен хоп — ще один запит, який може впасти; ще один TLS-рукопотиск; ще один пошук в кеші; ще одне місце, де некоректний заголовок
може все зламати. Помножте це на швидкість краулінгу Googlebot і ваш власний трафік — і отримаєте реальну вартість.

Цитата, яку варто прикріпити до дашборда: Надія — не стратегія. — Gene Kranz.
Редиректи, які роблять ставку на те, що пошукові системи «розберуться», — повільний інцидент.

Поведіка кешу, яку не варто ігнорувати

Постійні редиректи можуть довго кешуватися. Якщо ви випадково випустили поганий 301, то не лише виправили сервер і все добре.
Браузери, CDN та боти можуть продовжувати слідувати за старим шляхом. Тому зміни в редиректах заслуговують управління змінами.

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

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

1) Підтвердіть фінальну ціль і кількість хопів

  • Візьміть вибірку проблемних URL зі звіту GSC.
  • Прослідкуйте редиректи і зафіксуйте: кількість хопів, коди стану, фінальний URL, фінальний статус.
  • Якщо кількість хопів > 1 — у вас вже є конкретна робота.

2) Перевірте, чи цільовий URL індексується

  • Фінальний статус має бути 200.
  • Немає заголовка або мета-тега «noindex».
  • Не заблокований robots.txt.
  • Canonical вказує на себе (або на чітко визначений канонічний URL).

3) Перевірте, чи ваш власний сайт не саботує вас

  • Внутрішні посилання повинні вказувати на фінальні цілі, а не на редиректні URL.
  • Карта сайту має містити канонічні URL, а не тих, що редиректять.
  • Hreflang (якщо використовується) має посилатися на канонічні цілі.

4) Подивіться логи сервера щодо поведінки Googlebot

  • Чи Googlebot повторно звертається до редиректних URL? Це свідчить, що джерела дискавері все ще вказують на них.
  • Чи Googlebot зазнає помилок на цілі (timeout-и, 5xx, блокування)? Це питання надійності, а не «SEO».

5) Якщо це міграція: порівняйте старе й нове покриття

  • Старі URL мають бути «Сторінка з переадресацією». Нові URL мають індексуватися.
  • Якщо нові URL не індексуються, швидше за все це: noindex, блокування robots, слабкі внутрішні посилання або конфлікти canonical.

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

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

Завдання 1: Прослідкуйте редиректи і порахуйте хопи

cr0x@server:~$ curl -sSIL -o /dev/null -w "final=%{url_effective} code=%{http_code} redirects=%{num_redirects}\n" http://example.com/Old-Path
final=https://www.example.com/new-path/ code=200 redirects=2

Значення: Відбулося два редиректи. Фінальний статус 200.
Рішення: Якщо redirects > 1, намагайтеся звести правила так, щоб перша відповідь вказувала безпосередньо на фінальний URL.

Завдання 2: Друк повного ланцюга редиректів (подивіться кожен Location)

cr0x@server:~$ curl -sSIL http://example.com/Old-Path | sed -n '1,120p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/Old-Path
HTTP/2 301
location: https://www.example.com/new-path/
HTTP/2 200
content-type: text/html; charset=UTF-8

Значення: HTTP→HTTPS, потім нормалізація хоста/шляху.
Рішення: Змініть перший редирект так, щоб він вказував одразу на фінальний хост/шлях, а не на проміжний.

Завдання 3: Виявлення циклів редиректів

cr0x@server:~$ curl -sSIL --max-redirs 10 https://www.example.com/loop-test/ | tail -n +1
curl: (47) Maximum (10) redirects followed

Значення: Цикл або надмірний ланцюг.
Рішення: Трактувати як інцидент: ідентифікуйте набір правил (CDN, load balancer, origin), що створює цикл, і виправте його перш ніж думати про індексацію.

Завдання 4: Перевірити rel=canonical на цілі

cr0x@server:~$ curl -sS https://www.example.com/new-path/ | grep -i -m1 'rel="canonical"'
<link rel="canonical" href="https://www.example.com/new-path/" />

Значення: Canonical вказує на себе. Добре.
Рішення: Якщо canonical вказує не туди, виправте шаблони чи заголовки; інакше Google може ігнорувати ваш намір з редиректом.

Завдання 5: Перевірити на наявність noindex в мета-тезі на цілі

cr0x@server:~$ curl -sS https://www.example.com/new-path/ | grep -i -m1 'noindex'

Значення: Відсутність виводу означає, що мета-тег «noindex» не знайдено серед перших матчів.
Рішення: Якщо ви бачите noindex, зупиніться. Саме це перешкоджає індексації. Виправте конфіг релізу, прапорці CMS або витік середовища зі staging.

Завдання 6: Перевірити заголовок X-Robots-Tag на наявність noindex

cr0x@server:~$ curl -sSIL https://www.example.com/new-path/ | grep -i '^x-robots-tag'
X-Robots-Tag: index, follow

Значення: Заголовки дозволяють індексацію.
Рішення: Якщо бачите «noindex», виправте його у вихідному коді (app, правила CDN або security middleware). Заголовки переважують інші наміри.

Завдання 7: Підтвердити, що robots.txt не блокує ціль

cr0x@server:~$ curl -sS https://www.example.com/robots.txt | sed -n '1,120p'
User-agent: *
Disallow: /private/

Значення: Показано базовий robots.txt.
Рішення: Якщо шлях цілі заборонено, Google може бачити редирект, але не сканувати контент. Оновіть robots.txt і перевірте повторно в GSC.

Завдання 8: Перевірити, чи карта сайту містить редиректні URL

cr0x@server:~$ curl -sS https://www.example.com/sitemap.xml | grep -n 'http://example.com' | head
42:  <loc>http://example.com/old-path</loc>

Значення: Карта сайту містить неканонічні URL (HTTP-хост).
Рішення: Перегенеруйте карти сайту, щоб у них були лише фінальні канонічні URL. Це недорога та ефективна очистка.

Завдання 9: Знайти внутрішні посилання, що все ще вказують на редиректні URL

cr0x@server:~$ curl -sS https://www.example.com/ | grep -oE 'href="http://example.com[^"]+"' | head
href="http://example.com/old-path"

Значення: Головна сторінка все ще посилається на старий HTTP-URL.
Рішення: Виправте генерацію внутрішніх посилань (шаблони, поля CMS). Внутрішні посилання — це ваш власний внесок краул-бюджету у фонд редиректів.

Завдання 10: Перевірити тип редиректу (301 vs 302) на краю

cr0x@server:~$ curl -sSIL https://example.com/old-path | head -n 5
HTTP/2 302
location: https://www.example.com/new-path/

Значення: Встановлено тимчасовий редирект.
Рішення: Якщо переміщення постійне — змініть на 301/308. Якщо це дійсно тимчасово (обслуговування, A/B), переконайтеся, що це обмежено у часі і моніториться.

Завдання 11: Переконатися, що ціль стабільно повертає 200 (не іноді 403/500)

cr0x@server:~$ for i in {1..5}; do curl -sS -o /dev/null -w "%{http_code} %{time_total}\n" https://www.example.com/new-path/; done
200 0.142
200 0.151
200 0.139
500 0.312
200 0.145

Значення: Періодично 500. Це баг на надійності.
Рішення: Не сперечайтеся зі звітом GSC, поки origin не стабілізовано. Виправте upstream-помилки, потім попросіть повторну індексацію.

Завдання 12: Перевірити правила Nginx на подвійну нормалізацію

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'return 301|rewrite .* permanent' | head -n 20
123:    return 301 https://$host$request_uri;
287:    rewrite ^/Old-Path$ /new-path/ permanent;

Значення: Кілька директив редиректу можуть складатися (протокол + шлях).
Рішення: Об’єднайте в один канонічний редирект де можливо, або забезпечте порядок, який запобігає мультихопам.

Завдання 13: Перевірити правила Apache на неочікувані матчі

cr0x@server:~$ sudo apachectl -S 2>/dev/null | sed -n '1,80p'
VirtualHost configuration:
*:443                  is a NameVirtualHost
         default server www.example.com (/etc/apache2/sites-enabled/000-default.conf:1)

Значення: Підтверджує, який vhost за замовчуванням; неправильний дефолтний vhost може створювати небажані хост-редиректи.
Рішення: Переконайтеся, що канонічний хост налаштований правильно і що неканонічні хости явно редиректять в один хоп.

Завдання 14: Використати access-логи, щоб побачити, чи Googlebot застряг на редиректних URL

cr0x@server:~$ sudo awk '$9 ~ /^30/ && $0 ~ /Googlebot/ {print $4,$7,$9,$11}' /var/log/nginx/access.log | head
[27/Dec/2025:09:12:44 /old-path 301 "-" 
[27/Dec/2025:09:12:45 /old-path 301 "-" 

Значення: Googlebot повторно запитує редиректний URL. Джерела дискавері все ще вказують на них.
Рішення: Виправте внутрішні посилання та карти сайту; розгляньте оновлення зовнішніх посилань, якщо контролюєте їх (профілі, власні властивості).

Завдання 15: Перевірити непослідовну поведінку за User-Agent (небезпечні «розумні» редиректи)

cr0x@server:~$ curl -sSIL -A "Mozilla/5.0" https://www.example.com/ | head -n 5
HTTP/2 200
content-type: text/html; charset=UTF-8
cr0x@server:~$ curl -sSIL -A "Googlebot/2.1" https://www.example.com/ | head -n 5
HTTP/2 302
location: https://www.example.com/bot-landing/

Значення: Різні відповіді для Googlebot. Це червоний прапор, якщо немає дуже виправданої причини.
Рішення: Видаліть логіку редиректів на основі UA; це може виглядати як клоакинг і створює нестабільність індексації.

Завдання 16: Перевірити, чи HSTS не винна у вашій плутанині з редиректами

cr0x@server:~$ curl -sSIL https://www.example.com/ | grep -i '^strict-transport-security'
Strict-Transport-Security: max-age=31536000; includeSubDomains

Значення: HSTS увімкнено, отже браузери після першого контакту примусово підніматимуть HTTPS.
Рішення: Не «дебажте» редиректи лише в браузері; використовуйте curl у чистому середовищі. HSTS може приховувати HTTP-поведіку і змушувати вас ганятися за примарами.

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

Інцидент: неправильне припущення («Google сам це розбереться»)

Середня SaaS-компанія мігрувала з легасі CMS на сучасний фреймворк. План виглядав чистим:
старі URL редиректитимуться на нові, а новий сайт буде швидшим. Інженери реалізували редиректи на рівні додатку,
і QA перевірив це в браузері. Всі розійшлися додому.

На першому тижні Search Console почав заповнюватися «Сторінка з переадресацією», а покази впали для цінних сторінок.
SEO-команда занепокоїлася і вимагала «прибрати редиректи». Це було неправильним вогнегасником.
SRE на виклику зробив непрестижну річ: витягнув логи сервера для Googlebot і відтворив запити через curl.

Припущення, яке зламало їх: «Якщо в браузері працює — Googlebot бачить те ж саме.»
Їхній CDN мав правила пом’якшення бот-трафіку, які поводилися інакше для невідомих user-agent-ів під час піків трафіку.
Коли origin працював повільно, edge повертав тимчасовий редирект на загальну сторінку «будь ласка, спробуйте пізніше» — для людей це прийнятно,
але для індексації — катастрофа. Googlebot ішов за редиректом і знаходив тонкий контент.

Виправлення не було «SEO-магією». Це була продукційна гігієна:
вони виняткували перевірені боти з правила пом’якшення, покращили кешування для нових сторінок і припинили редиректити на загальний fallback.
Після цього «Сторінка з переадресацією» залишилася для старих URL (очікувано), а нові URL стабілізувалися і знову індексувалися.

Оптимізація, що призвела до відкату: згортання параметрів через редиректи

У e‑commerce організації була проблема з параметрами: нескінченні URL типу ?color=blue&sort=popular&ref=ads.
Статистика краулінгу виглядала погано, і хтось запропонував «просте» рішення: редиректити будь-який URL з параметрами на сторінку категорії без параметрів.
Одне правило переписування, щоб правити всім.

Це швидко запустилося. Надто швидко. Конверсія впала. Органічний трафік для довгохвостих варіантів категорій обрушився.
Search Console показував багато «Сторінка з переадресацією», але справжнє пошкодження було в тому, що вони
редиректували реальні сторінки з пошуковим наміром. Деякі комбінації параметрів відображали значимі відфільтровані сторінки,
які користувачі шукали (і які мали унікальний товар).

Гірше того, правило редиректу створювало ланцюги: URL з параметром → чиста категорія → гео-редирект → локалізована категорія.
Googlebot витрачав більше часу на стрибки, ніж на сканування. Зросла затримка. Сайт виглядав «нестабільним».

Відкат був болючим, але необхідним. Вони замінили грубе правило на політику:
відкидати лише відомі трекінгові параметри (utm/ref), залишати функціональні фільтри індексованими там, де контент це виправдовує,
і використовувати rel=canonical для дублів. Раптом «Сторінка з переадресацією» обмежилася лише сміттєвими URL, а не прибутковими.

Сумна, але правильна практика: гігієна sitemap та внутрішніх посилань врятувала ситуацію

Платформа для публікацій провела консolidування доменів: чотири субдомени в один канонічний хост.
Вони реалізували 301-редиректи і чекали турбулентності. Родзинка: вони поставилися до цього як до операційної зміни, а не до SEO-бажання.

Перед запуском вони згенерували мапу відповідностей (старий → новий), прогнали автоматичні тести редиректів і оновили внутрішні посилання в шаблонах.
Не лише навігацію. Футер, блоки «пов’язані статті», RSS-фіди — усе. Вони також перегенерували карти сайту, щоб включити лише канонічні URL,
і опублікували їх разом із деплоєм.

Після запуску Search Console наповнився «Сторінка з переадресацією» для старих хостів (очікувано), але новий хост швидко індексувався.
Статистика краулінгу показала, що Googlebot швидко перемістився від старих URL.
Моніторинг логів показав різке зниження звернень до редиректів за кілька тижнів — джерела дискавері були чисті.

Урок не був гламурним: нудна робота запобігає холодній аварії.
Редиректи — це міст, але потрібно ще перемістити трафік, посилання і сигнали на новий бік.

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

1) Симптом: сплеск «Сторінка з переадресацією» після деплою

Коренева причина: Нове правило ввело мультихоп-редиректи або цикли (часто слеш + локаль + нормалізація хоста).
Виправлення: Проганяйте тести ланцюгів редиректів на вибірці URL; зводьте до одного хопа; додавайте регресійні тести в CI для канонічних URL.

2) Симптом: Старі URL показують «Сторінка з переадресацією», але нові — «Crawled — currently not indexed»

Коренева причина: Цільові сторінки низької якості/тонкі, заблоковані, повільні або мають конфлікти canonical/noindex.
Виправлення: Переконайтеся, що ціль повертає 200, індексується, має self-canonical і суттєвий контент. Виправте продуктивність і шаблони.

3) Симптом: GSC показує «Сторінка з переадресацією» для URL, які мають бути фінальними

Коренева причина: Внутрішні посилання або карта сайту вказують на неканонічні варіанти, тому Google продовжує знаходити неправильну версію першою.
Виправлення: Оновіть внутрішні посилання, карту сайту, hreflang і структуровані дані, щоб вони посилалися лише на канонічні цілі.

4) Симптом: Редиректи працюють у браузері, але не для Googlebot

Коренева причина: Умовна логіка на основі user-agent, cookies, гео або бот-мітігація на рівні CDN/WAF.
Виправлення: Тестуйте з UA Googlebot, порівнюйте заголовки, видаліть умовні редиректи і забезпечте однакову нормалізацію.

5) Симптом: Сторінки зникають після «очищення» параметрів

Коренева причина: Правило редиректу згорнуло значимі URL у загальні сторінки, втративши довгохвосту релевантність.
Виправлення: Редиректьте/видаляйте лише трекінгові параметри; обробляйте функціональні фільтри за допомогою canonical або noindex, або дозволяйте індексацію вибірково.

6) Симптом: Редиректні URL залишаються в індексі місяцями

Коренева причина: Тимчасові редиректи (302/307) використовуються для постійних переміщень або непослідовні сигнали canonical.
Виправлення: Використовуйте 301/308 для постійних переміщень; переконайтеся, що ціль канонічна; оновіть внутрішні посилання і карти сайту на ціль.

7) Симптом: Редиректи викликають періодичні 5xx та падіння краулінгу

Коренева причина: Обробка редиректів на рівні додатку викликає дорогі обчислення; overload origin; промахи кеша; TLS-рукопотиск на кожному хопі.
Виправлення: Перенесіть редиректи на edge/web-сервер де можливо; кешуйте редиректи; зменште кількість хопів; моніторьте p95 latency на кінцях редиректів.

Жарт №2: Найшвидший спосіб знайти невідоме правило редиректу — видалити його і почекати, поки хтось важливий помітить.

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

Чеклист A: Ви бачите «Сторінка з переадресацією» і хочете зрозуміти, чи варто хвилюватися

  1. Візьміть 20 URL зі звіту (мікс важливих і випадкових).
  2. Запустіть curl з лічильником редиректів. Якщо багато випадків >1 хопа — хвилюйтеся.
  3. Підтвердіть, що фінальні URL повертають 200 і індексуються (noindex/robots/canonical).
  4. Перевірте, чи фінальні URL індексуються і отримують покази.
  5. Якщо фінальні URL здорові — розглядайте «Сторінка з переадресацією» як інформаційний сигнал.

Чеклист B: Очищення редиректів, яке не викличе новий інцидент

  1. Зробіть інвентар поточних правил редиректів у всіх шарах: CDN/WAF, load balancer, web server, app.
  2. Визначте політику канонічних URL: протокол, хост, слеш, нижній регістр, патерни локалі.
  3. Забезпечте одношаговий шлях до канонічного URL де можливо.
  4. Оновіть внутрішні посилання і шаблони, щоб використовували канонічні URL.
  5. Перегенеруйте карти сайту, щоб містили лише канонічні URL.
  6. Деплойте з моніторингом: частота редиректів, 4xx/5xx на цілі, латентність.
  7. Після деплою робіть лог-семпл Googlebot і перевіряйте, що він дістається до сторінок з 200.

Чеклист C: План для міграцій (доменів або структури URL)

  1. Створіть файл мапінгу (старий → новий) для всіх високовартісних URL; не покладайтеся лише на regex.
  2. Реалізуйте 301/308 редиректи і протестуйте на цикли та ланцюги.
  3. Збережіть паритет контенту: тайтли, заголовки, структуровані дані де доречно.
  4. Переконайтеся, що нові сторінки мають self-referencing canonical.
  5. Переключіть карти сайту на нові URL під час запуску.
  6. Моніторьте індексацію: нові сторінки мають зростати, в той час як старі стають «Сторінкою з переадресацією».
  7. Тримайте редиректи довго (місяці — роки в залежності від екосистеми), а не два тижні тому, що хтось хоче «чисту конфігурацію».

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

  • Факт 1: HTTP 301 і 302 датуються ранніми специфікаціями HTTP; веб пересував сторінки ще майже з початку існування.
  • Факт 2: 307 і 308 були введені пізніше для уточнення збереження методів; вони важливіші для API, але з’являються в сучасних стеках.
  • Факт 3: Пошукові системи історично трактували 302 як «не передавати сигнали», але з часом ставлення стало гнучкішим, якщо редирект тримається довго.
  • Факт 4: HSTS може зробити HTTP→HTTPS редиректи невидимими при тестуванні в браузері, бо браузер піднімає з’єднання до HTTPS ще до запиту.
  • Факт 5: CDN часто реалізують редиректи на edge; це швидше, але може створювати приховані взаємодії з редиректами origin.
  • Факт 6: Раніше канонізацію часто робили через редиректи, бо rel=canonical спочатку не існував; згодом canonical став стандартним інструментом.
  • Факт 7: Ланцюги редиректів стали частішими, коли стек наростав: CMS + фреймворк + CDN + WAF + load balancer — кожен «допомагає» нормалізувати.
  • Факт 8: Боти не поводяться як користувачі: вони можуть сканувати в масштабі, повторювати запити агресивно і підсилювати дрібні неефективності у великі інфраструктурні витрати.

FAQ

1) Чи варто намагатися позбутися «Сторінки з переадресацією» у Search Console?

Не заради самого факту. Ваша мета — щоб цільові URL індексувалися і показували результати. Те, що перенаправлені URL «не індексуються», — очікувано.
Очищайте тільки коли поведінка редиректів неефективна або непослідовна.

2) Чи є «Сторінка з переадресацією» покаранням?

Ні. Це категоризація. Покарання — це те, що ви робите далі: зберігати ланцюги, редиректити на тонкі сторінки або надсилати суперечливі сигнали canonical.

3) Скільки редиректів — занадто багато?

Практично: орієнтуйтеся на один хоп. Два ще життєздатні. Більше — запах надійності, і це може уповільнити краулінг і марнувати бюджет.
Якщо бачите 3+, виправляйте, якщо немає вагомої причини.

4) Чи 302 шкодить SEO у порівнянні з 301?

Іноді. Якщо переміщення постійне — використовуйте 301 або 308. Довгоіснуючий 302 може спрацювати, але він передає невизначеність і може затримати консолідацію.
Не будьте впевнені, що Google «рано чи пізно» трактуватиме це як 301.

5) Чому моя карта сайту показує URL, які GSC визначає як «Сторінка з переадресацією»?

Тому що генератор карти сайтів використовує неправильну базу (HTTP vs HTTPS, неправильний хост) або виводить застарілі шляхи.
Виправте генератор, щоб карти сайту містили лише канонічні, фінальні URL. Це один із найпростіших виграшів у цій темі.

6) Що робити, якщо мені потрібні обидві версії доступними (наприклад, відфільтровані сторінки), але я не хочу їх індексувати?

Не редиректте їх, якщо вони функціонально потрібні. Залиште доступними, а потім свідомо використовуйте rel=canonical або правила noindex.
Редиректи призначені для «цого не має існувати як лендингову сторінку».

7) Чи може «Сторінка з переадресацією» бути спричинена JavaScript-редиректами?

Так, але це складніший режим. JS-редиректи можуть бути повільнішими, менш надійними для ботів і виглядати підозріло при зловживанні.
Віддавайте перевагу серверним редиректам, якщо немає сильної причини інакше.

8) Скільки часу зберігати редиректи після міграції?

Довше, ніж здається. Місяці як мінімум; часто рік або більше для значних сайтів, особливо якщо старі URL активно лінкуються.
Раннє видалення редиректів — як перетворити вашу міграцію на постійний жнивар 404.

9) Чому редиректні URL все ще багато сканується?

Google продовжує знаходити їх через внутрішні посилання, карти сайту або зовнішні посилання. Внутрішні джерела під вашим контролем; виправляйте їх першими.
Зовнішні посилання потребують часу, щоб зникнути. Мета — припинити живити проблему.

10) Чи може «Сторінка з переадресацією» приховувати проблеми з безпекою або WAF?

Абсолютно. WAF іноді редиректить підозрілий трафік, трафік з лімітом або певні user-agent-и. Якщо Googlebot отримує таке ставлення,
ви отримаєте нестабільність індексації. Підтвердіть поведінку за допомогою тестів з UA і логів edge.

Висновок: практичні наступні кроки

«Сторінка з переадресацією» не ваш ворог. Це ліхтарик. Інколи він світить на URL, які ви навмисно вивели з обігу. Чудово.
Інколи він показує борг редиректів: ланцюги, цикли, суперечливі canonical і «тимчасові» редиректи, що стали постійними із лінощів.

Наступні кроки, що швидко окуповуються:

  1. Візьміть 20–50 URL зі звіту і виміряйте кількість хопів через curl.
  2. Підтвердіть, що цілі індексуються (200, без noindex, не заблоковані, self-canonical).
  3. Виправте внутрішні посилання і карти сайту, щоб вказували на фінальні канонічні URL.
  4. Зведіть редиректи до одного хопа і стандартизуйтесь на 301/308 для постійних переміщень.
  5. Спостерігайте логи: Googlebot має витрачати менше часу на редиректи і більше — на справжні сторінки.

Якщо ви поставитесь до редиректів як до продакшн-інфраструктури — спостережуваної, придатної для тестування і нудної —
ви отримаєте кращий SEO-результат: Google витрачатиме свій час на ваш контент, а не на вашу «водопровідну мережу».

MariaDB проти SQLite при пікових записах: хто витримує сплески без драми

Пікові записи не з’являються ввічливо. Вони приходять гуртом: виконавці завдань пробуджуються одночасно, черга очищується після розгортання, мобільні клієнти перепідключаються після тунелю в поїзді, або “ой” backfill, який ви пообіцяли запустити «повільно». Запитання не в тому, чи може ваша база даних записувати. Запитання в тому, чи може вона записувати багато, зараз, не перетворивши ваш on-call на хобі.

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

Справжнє питання: що означає «сплеск» для вашої системи

«Піковий запис» — це розмите словосполучення, яке породжує дорогі непорозуміння. Щонайменше чотири різні тварини, які люди називають сплеском:

  1. Короткий піковий стрибок, висока конкуренція: 500 запитів приходять одночасно, кожен робить невеликий вставлений запис.
  2. Тривалий сплеск: 10× від звичайної швидкості записів протягом 10–30 хвилин (пакетні задачі, backfill).
  3. Вибух латентності в довгому хвості: середній пропуск виглядає нормально, але кожні 20 секунд коміти затримуються на 300–2000 мс.
  4. I/O-скеля: диск або система зберігання вдаряє по стіні скидання (fsync/flush cache), і все вишиковується в чергу за нею.

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

Але й там є пастки. Пастка SQLite — це блокування. Пастка MariaDB — це думати, що сервер бази даних є вузьким місцем, коли насправді проблеми зі сховищем (або ваша політика комітів).

Кілька фактів і історія, які справді важливі

Декілька контекстних моментів, які короткі, конкретні і дивно передбачувані для поведінки під сплесками:

  • SQLite — це бібліотека, не сервер. Немає окремого демона; ваш додаток лінкує її і безпосередньо читає/записує файл БД. Це і суперсила продуктивності, і операційне обмеження.
  • Початковий дизайн SQLite оптимізовано під вбудовані системи. Він став популярним на десктопі/мобільних пристроях, бо це «просто файл» і не потребує DBA.
  • WAL-режим у SQLite введено для покращення конкуренції. Він розділяє читання та записи додаванням до журналу перед записом, дозволяючи читачам працювати під час записів — до певної межі.
  • SQLite усе ще має правило одного записувача на рівні бази даних. WAL допомагає читачам, але кілька одночасних записувачів усе одно серіалізуються на write lock.
  • MariaDB — це форк MySQL. Форк відбувся після придбання Sun Oracle; MariaDB стала «дружнім спільноті» продовженням для багатьох організацій.
  • InnoDB став дефолтним рушієм MySQL/MariaDB не випадково. Він побудований навколо MVCC, redo журналів, фонового скидання і відновлення після аварій — фіч, які важливі під час сплесків.
  • Продуктивність MariaDB під сплесками сильно залежить від поведінки fsync. Ваша політика скидання redo журналу може перенести біль від «кожен коміт затримується» до «деякі коміти затримуються, але загальна пропускна здатність зростає». Це винагорода за компроміс.
  • Більшість інцидентів «база даних повільна» під час пікових записів — насправді «сховище повільне». База даних просто перша визнає проблему, блокуючись на fsync.

Анатомія шляху запису: MariaDB/InnoDB проти SQLite

SQLite: один файл, один записувач, мінімум церемоній

SQLite пише в один файл бази даних (плюс, у WAL-режимі, файл WAL та файл спільної пам’яті-shm). Ваш процес виконує SQL; SQLite перетворює його на оновлення сторінок. Під час коміту транзакції SQLite має забезпечити надійність згідно з вашими pragma налаштуваннями. Це зазвичай означає примусове записування на стійке сховище за допомогою викликів типу fsync, залежно від платформи і файлової системи.

Під час сплесків критичний момент SQLite — наскільки швидко вона може пройти через «отримати write lock → запис сторінок/WAL → політика синхронізації → звільнити lock». Якщо комітів багато і вони маленькі, накладні витрати домінують через виклики синхронізації і передачу блокувань. Якщо коміти пакетуються, SQLite може летіти.

WAL-режим змінює форму: записувачі додають у WAL, а читачі можуть продовжувати читати головну знімок БД. Але все одно лише один записувач одночасно, і чекпоїнти можуть стати другим видом сплеску (більше про це нижче).

MariaDB/InnoDB: конкуренція, буферизація і фоновий I/O

MariaDB — це серверний процес з кількома робочими потоками. InnoDB підтримує buffer pool (кеш) для сторінок, redo лог (write-ahead) і часто undo лог для MVCC. Коли ви комітите, InnoDB записує redo записи і — залежно від налаштувань — скидає їх на диск. Забруднені сторінки скидаються у фоні.

Під час сплесків суперсила InnoDB у тому, що він може приймати багато одночасних записувачів, ставити роботу в чергу і згладжувати її фонового скидання — якщо ви правильно підібрали розміри і ваш I/O витримує навантаження. Його слабкість у тому, що він все ще може вдаритись у жорстку стіну, коли redo лог або скидання dirty сторінок стає критичним, і тоді латентність стрибкоподібно зростає.

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

Хто краще витримує сплески (і коли)

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

SQLite перемагає, коли

  • Один процес або контрольовані записувачі: один потік запису, черга або виділений процес запису.
  • Короткі транзакції, пакетні коміти: ви можете комітити кожні N записів або кожні T мілісекунд.
  • Локальний диск, низька латентність fsync: NVMe, а не хиткий мережевий файловий шар.
  • Ви хочете простоти: немає сервера, менше рухомих частин, менше приводів прокидатися о 3 ранку.
  • Читано-важке навантаження з періодичними сплесками: WAL-режим може зберегти читання швидкими під час записів.

SQLite програє (голосно), коли

  • Багато одночасних записувачів: вони серіалізуються, і ваші потоки додатка накопичуються за «database is locked».
  • Багато процесів одночасно пишуть: особливо на зайнятих хостах або контейнерах без координації.
  • Чекпоїнтинг стає сплеском: WAL зростає, тригериться checkpoint, і вмить ви отримуєте ще один write-шторм у середині існуючого.
  • Сховище має дивну поведінку fsync: деякі віртуалізовані чи мережеві диски роблять надійність надзвичайно дорогою або непослідовною.

MariaDB перемагає, коли

  • У вас реальна конкуренція: багато інстансів додатка, що одночасно пишуть.
  • Потрібні операційні інструменти: реплікація, бекапи, online schema changes, hooks для спостереження.
  • Потрібно ізолювати навантаження: buffer pool поглинає сплески, thread pool і черги можуть запобігти повному колапсу.
  • Потрібні передбачувані семантики ізоляції: MVCC з консистентними читаннями під навантаженням записів.

MariaDB програє, коли

  • Ваш диск не встигає скидувати: flush redo журналу зупиняє світ; латентність вибухає.
  • Ви неправильно розмірили buffer pool: занадто малий — буде трясіння кешу; надто великий — з’явиться драма з кешем ОС і свапом.
  • Ви «налаштовуєте» надійність всліпу: ви купуєте пропускну здатність, продаючи майбутньому собі інцидент втрати даних.
  • Ваша схема створює гарячі точки: лічильники на одному рядку, погані індекси чи монотонні вставки, що борються за одні структури.

Жарт №1: SQLite — друг, який завжди вчасно — поки не покличете ще трьох друзів говорити одночасно, тоді він просто зачиняє двері.

Налаштування надійності: за що ви насправді платите fsync

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

Ричаги надійності в SQLite

SQLite відкриває налаштування надійності через pragmas. Основні для сплесків:

  • journal_mode=WAL: зазвичай рекомендований варіант для конкурентних читань і стабільної продуктивності записів.
  • synchronous: контролює, наскільки агресивно SQLite синхронізує дані на диск. Вища надійність зазвичай означає більші витрати на fsync.
  • busy_timeout: не підвищує пропускну здатність, але запобігає марним відмовам, чекаючи на блокування.
  • wal_autocheckpoint: контролює, коли SQLite намагається робити checkpoint (переносити вміст WAL у головний файл БД).

Тут тонкість: у WAL-режимі система може здаватися чудовою, поки WAL не виросте і checkpoint не стане неминучим. Цей «checkpoint-податок» часто проявляється як періодичні стрибки латентності, що виглядають як «хрипіння» бази даних. Якщо ви логируєте або вставляєте часорядні дані, це може сильно вдарити.

Налаштування надійності в MariaDB/InnoDB

У InnoDB критичні для сплесків налаштування пов’язані зі скиданням redo журналу та тим, як швидко можна записати dirty сторінки:

  • innodb_flush_log_at_trx_commit: класичний компроміс надійність/продуктивність. Значення 1 — найбезпечніше (скидання при кожному коміті), 2 — трохи швидше з невеликою втратою надійності, 0 — швидше, але ризиковано.
  • sync_binlog: якщо ви використовуєте бінлог для реплікації, це може додатково коштувати fsync при записі.
  • innodb_redo_log_capacity (або старі параметри розміру log file): занадто мало — часті checkpoint; занадто багато — змінюється час відновлення. Піки часто виявляють недо-розміровані логи.
  • innodb_io_capacity / innodb_io_capacity_max: кажуть InnoDB, наскільки агресивно виконувати фонове скидання.

Для толерантності до сплесків ви хочете, щоб база поглинала сплеск і скидувала рівномірно, а не панічно. Панічне скидання — це коли латентність стає «цікавою».

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

Патерн: маленькі транзакції при високому QPS

Це класична петля «insert одного рядка і commit», помножена на конкуренцію. Це комітний шторм.

  • SQLite: контенція на блокування + витрати на fsync. Ви побачите «database is locked» або великі затримки, якщо не чергуєте записи і не пакетируєте коміти.
  • MariaDB: може витримати конкуренцію, але fsync на кожен коміт може домінувати над латентністю. Ви побачите багато commit’ів, очікування скидання журналу і насичення I/O.

Патерн: backfill з важкими індексами

Ви додаєте колонки, робите backfill і оновлюєте вторинні індекси. Тепер кожен запис розповсюджується на кілька оновлень B-дерева.

  • SQLite: один записувач робить його передбачуваним, але повільним; вікно блокування довше, тому всі інші чекають довше.
  • MariaDB: пропускна здатність залежить від buffer pool і I/O. Гарячі індекси можуть викликати contention на затискачах; занадто багато потоків може погіршити ситуацію.

Патерн: сплеск співпадає з циклом checkpoint/flush

Це сценарій «усе нормально, усе нормально… чому кожні 30 секунд все горить?».

  • SQLite WAL checkpoint: довгі цикли checkpoint можуть блокувати або уповільнювати записи, залежно від режиму і умов.
  • InnoDB checkpoint: redo лог заповнюється, dirty сторінки треба скинути, і основна робота починає чекати на фонового I/O.

Патерн: джиттер латентності сховища

Усе норм, поки диск не затримається. Хмарні томи, скидання кешу RAID, шумні сусіди, коміти файлової системи — обирайте свого винуватця.

  • SQLite: ваш поток додатка — це база даних; він блокується. Піки латентності безпосередньо впливають на час відповіді запитів.
  • MariaDB: може ставити в чергу і паралелізувати, але рано чи пізно серверні потоки теж блокуються. Різниця в тому, що ви можете побачити це всередині рушія через лічильники і логи.

Жарт №2: «Ми просто зробимо це синхронним і швидким» — це еквівалент фрази «Я просто залишусь спокійним і вчасно пройду контроль в аеропорту».

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

Інцидент через хибне припущення: «SQLite витримає кількох записувачів, правда?»

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

Потім прийшов продакшн. Автоскейл підняв кілька контейнерів на тому ж хості, всі писали в один файл SQLite через спільний hostPath. Як тільки трафік підскочив, записувачі зіштовхнулись. SQLite зробила те, для чого вона створена: серіалізувала записи. Додаток зробив те, для чого він створений: запанікував.

Симптоми були брудні: тайм-аути запитів, помилки «database is locked» і петля повторних спроб, яка множила сплеск. Хост виглядав недовикористаним по CPU, що підштовхнуло до неправильного діагнозу: «це не може бути база; CPU вільний.»

Виправлення було простим і дорослим: один записувач на файл БД. Вони перейшли на per-container файли SQLite і ввели явну чергу записів в процесі. Коли потрібні були записі між контейнерами, вони перемістили буферний шар в MariaDB з коректним connection pooling і пакетуванням транзакцій.

Висновок: SQLite неймовірна, коли ви навмисно контролюєте серіалізацію записів. Це хаос, коли серіалізація виявляється випадковою.

Оптимізація, яка повернулась бумерангом: «Послабимо fsync і додамо потоки»

Внутрішня адмін-платформа працювала на MariaDB. Під час квартального імпорту вони бачили сплески латентності комітів. Хтось (доброзичливий, втомлений) змінив innodb_flush_log_at_trx_commit з 1 на 2 і збільшив конкуренцію в імпортері з 16 до 128 потоків. Вони хотіли «продавити пакет швидше» і зменшити вікно болю.

Пропускна здатність покращилась десь на п’ять хвилин. Потім система вдарилась у іншу стіну: трясіння buffer pool та write amplification від вторинних індексів. Dirty сторінки накопичувались швидше, ніж їх можна було скинути. InnoDB почав агресивно скидати. Латентність перетворилась зі сплесків на постійно жахливу, а primary почав відставати в реплікації через зміну fsync шаблону бінлога під навантаженням.

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

Кінцеве рішення не було «більше налаштувань». Це було дисципліноване формування навантаження: обмежити імпортер, пакетувати коміти і планувати задачу з передбачуваним rate-limit. Вони залишили налаштування надійності консервативними і вирішили реальну проблему: імпортер не мав поводитись як DDoS-тест.

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

Скучно, але правильно, що врятувало день: «Вимірювати fsync, тримати запас, репетиції відновлення»

Сервіс, суміжний із платежами (де ви не можете креативити з надійністю), використовував MariaDB з InnoDB. Кожні кілька тижнів вони мали сплеск: задачі звірки плюс підвищення трафіку. Це ніколи не приводило до аварії, і ніхто не святкував. Це було головною метою.

У них була нудна рутина. Вони постійно вимірювали латентність диска (включаючи fsync), а не лише IOPS. Вони тримали запас у ємності redo логу і підібрали buffer pool, щоб система не тряслась під час сплесків. Вони також регулярно репетирували відновлення, щоб ніхто не вчився робити бекап під час інциденту.

Одного дня джиттер латентності сховища подвоївся через шумного сусіда на обладнанні. Сервіс не впав. Він став повільнішим, спрацювали алерти, і команда застосувала відоме пом’якшення: тимчасово обмежила пакетні задачі і призупинила некритичних записувачів. Трафік користувачів залишився в SLO.

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

Висновок: «нудна» практика вимірювань і тримання запасу — найдешевша страховка від сплесків.

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

Коли сплеск записів вдаряє і все поводиться дивно, часу на філософствування немає. Потрібне швидке дерево рішень: чи ми зв’язані блокуванням, CPU чи I/O?

Спочатку: підтвердьте форму болю (латентність проти пропускної здатності)

  • Якщо пропускна здатність залишається високою, але p95/p99 латентність вибухає: шукайте fsync/journal/checkpoint затримки.
  • Якщо пропускна здатність падає: шукайте контенцію блокувань, виснаження потоків або насичення сховища.

Друге: визначте, чи специфічно це для SQLite або MariaDB

  • SQLite: помилки «database is locked», довгі очікування, ріст WAL, або зупинки через checkpoint.
  • MariaDB: потоки чекають на log flush, фонове скидання dirty сторінок, очікування row lock або відставання реплікації, що підсилює тиск.

Третє: доведіть або виключіть сховище як вузьке місце

  • Перевірте латентність диска, глибину черги та поведінку fsync під навантаженням.
  • Якщо сховище «стрибуче», майже будь-яка база виглядатиме винною.

Четверте: перестаньте робити гірше

  • Тротлінгуйте джерело сплеску (пакетна задача, імпортер, петля повторних спроб).
  • Пакетуйте коміти. Зменшіть конкуренцію. Вимкніть «безкінечні повтори без джиттера».
  • Збирайте докази перед перезапуском сервісів. Рестарти стирають підказки і рідко вирішують фізику проблеми.

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

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

1) Перевірити, чи система насичена I/O (Linux)

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db01)  12/30/2025  _x86_64_ (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.1    0.0    4.3    31.7     0.0    51.9

Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %util  await  svctm
nvme0n1          20.0  900.0   640.0 42000.0    0.0    50.0   98.7   22.4   0.8

Значення: %util близько 100% плюс високе await вказує, що пристрій є обмежувачем; iowait теж підвищений.

Рішення: Не починайте з «налаштування SQL». Тротлінгуйте записувачів, зменшіть частоту комітів або перемістіть на швидше сховище. Якщо том спільний, перевірте шум сусідів.

2) Визначити, який процес робить найбільше записів

cr0x@server:~$ pidstat -d 1 3
Linux 6.5.0 (db01)  12/30/2025  _x86_64_  (8 CPU)

# Time   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:01:01  999     2145      0.00  52000.00     0.00  mariadbd
12:01:01 1000     8812      0.00   1200.00     0.00  importer

Значення: MariaDB робить основні записи; importer також робить вклад, але не домінує.

Рішення: Сфокусуйтесь на InnoDB flushing/redo поведінці і латентності сховища, не лише на пакетуванні додатка.

3) Перевірити файлову систему і опції mount (сюди приходять сюрпризи з надійності)

cr0x@server:~$ mount | grep -E " /var/lib/mysql | /data "
/dev/nvme0n1p2 on /var/lib/mysql type ext4 (rw,relatime,commit=5)
/dev/nvme1n1p1 on /data type xfs (rw,relatime,attr2,inode64,logbufs=8)

Значення: інтервал commit ext4 і поведінка журналювання можуть впливати на періодичні латентні стрибки.

Рішення: Якщо бачите періодичні зупинки узгоджені з commits журналу, розгляньте налаштування файлової системи або перемістіть файли БД на том з кращими характеристиками латентності.

4) Виміряти сиру латентність fsync-подібних операцій простим тестом sync

cr0x@server:~$ dd if=/dev/zero of=/var/lib/mysql/sync-test.bin bs=4k count=4096 oflag=dsync
4096+0 records in
4096+0 records out
16777216 bytes (17 MB, 16 MiB) copied, 3.91 s, 4.3 MB/s

Значення: oflag=dsync примушує синхронізацію на блок; низька пропускна здатність вказує на високу вартість синхронізацій. Це не ідеальна модель, але вона показує, що «сховище брешуче».

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

5) MariaDB: підтвердити політику скидання InnoDB і розміри redo

cr0x@server:~$ mariadb -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_redo_log_capacity','innodb_io_capacity','innodb_io_capacity_max');"
+------------------------------+-----------+
| Variable_name                | Value     |
+------------------------------+-----------+
| innodb_flush_log_at_trx_commit | 1       |
| sync_binlog                   | 1        |
| innodb_redo_log_capacity      | 1073741824|
| innodb_io_capacity            | 200      |
| innodb_io_capacity_max        | 2000     |
+------------------------------+-----------+

Значення: Повна надійність і для redo, і для бінлога (дорого при сплесках). Ємність redo може бути замалою залежно від навантаження.

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

6) MariaDB: перевірити, чи чекаєте ви на скидання журналу

cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_log_waits'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';"
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_log_waits | 1834  |
+------------------+-------+
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_os_log_fsyncs | 920044 |
+----------------------+--------+

Значення: Log waits означає, що транзакції мусили чекати на скидання redo журналу. Сплески + латентність fsync = біль.

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

7) MariaDB: перевірити тиск dirty сторінок (заборгованість скидання)

cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_total';"
+--------------------------------+--------+
| Variable_name                  | Value  |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_dirty | 412345 |
+--------------------------------+--------+
+--------------------------------+--------+
| Variable_name                  | Value  |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_total | 524288 |
+--------------------------------+--------+

Значення: Дуже високе відношення dirty сторінок вказує, що система відстає зі скиданням; чекпоїнти можуть змусити затримки.

Рішення: Обережно збільшіть налаштування I/O capacity, переконайтеся, що сховище витримує записи, і зменшіть вхідний потік записів, поки dirty сторінки не стабілізуються.

8) MariaDB: виявити очікування блокувань і гарячі таблиці

cr0x@server:~$ mariadb -e "SELECT * FROM information_schema.innodb_lock_waits\G"
*************************** 1. row ***************************
requesting_trx_id: 123456
blocking_trx_id: 123455
blocked_table: `app`.`events`
blocked_lock_type: RECORD
blocking_lock_type: RECORD

Значення: Є контенція на конкретній таблиці/індексі.

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

9) MariaDB: переглянути поточні стани потоків (на що вони чекають?)

cr0x@server:~$ mariadb -e "SHOW PROCESSLIST;"
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| Id  | User | Host      | db   | Command | Time | State                  | Info                         |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| 101 | app  | 10.0.0.12 | app  | Query   |   12 | Waiting for handler commit | INSERT INTO events ...     |
| 102 | app  | 10.0.0.13 | app  | Query   |   11 | Waiting for handler commit | INSERT INTO events ...     |
| 103 | app  | 10.0.0.14 | app  | Sleep   |    0 |                        | NULL                         |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+

Значення: «Waiting for handler commit» зазвичай корелює з тиском на commit/fsync.

Рішення: Дослідіть налаштування sкидання redo/binlog і латентність диска; розгляньте пакетування записів.

10) SQLite: перевірити режим журналу і synchronous

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA journal_mode; PRAGMA synchronous; PRAGMA wal_autocheckpoint;"
wal
2
1000

Значення: WAL-режим увімкнено; synchronous=2 — FULL (дужe надійно, повільніше); autocheckpoint на 1000 сторінок.

Рішення: Якщо у вас сплески і ви бачите затримки, обміркуйте, чи дійсно потрібен FULL. Також плануйте стратегію checkpoint (ручну/контрольовану), замість того, щоб дозволяти autocheckpoint вас здивувати.

11) SQLite: виявити контенцію блокувань контрольованим тестом запису

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA busy_timeout=2000; BEGIN IMMEDIATE; INSERT INTO events(ts, payload) VALUES(strftime('%s','now'),'x'); COMMIT;"

Значення: Якщо це періодично завершується помилкою «database is locked», у вас є суперечливі записувачі або довгі транзакції.

Рішення: Введіть single-writer чергу, скоротіть транзакції і переконайтесь, що читачі не тримають блокування довше, ніж очікується (наприклад, довгі SELECT у транзакції).

12) SQLite: стежити за ростом WAL і здоров’ям checkpoint

cr0x@server:~$ ls -lh /data/app.db /data/app.db-wal /data/app.db-shm
-rw-r--r-- 1 app app 1.2G Dec 30 12:05 /data/app.db
-rw-r--r-- 1 app app 3.8G Dec 30 12:05 /data/app.db-wal
-rw-r--r-- 1 app app  32K Dec 30 12:05 /data/app.db-shm

Значення: WAL більший за основну БД. Це не автоматично фатально, але знак, що checkpoint не встигає.

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

13) SQLite: перевірити, чи читачі блокують checkpoint (busy database)

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0

Значення: Три числа — (busy, log, checkpointed). Нулі після TRUNCATE означають, що checkpoint пройшов швидко і WAL усічено.

Рішення: Якщо «busy» не нуль або WAL не усічеться, шукайте довгі транзакції читачів і виправляйте їх (скорочуйте читання, уникайте відкритих транзакцій).

14) MariaDB: підтвердити розмір buffer pool і тиск

cr0x@server:~$ mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';"
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 8589934592 |
+-------------------------+------------+
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 18403921 |
+-------------------------+----------+

Значення: Якщо buffer pool reads швидко зростають під час сплеску, ви втрачаєте кеш і робите більше фізичного I/O, ніж планували.

Рішення: Збільшіть buffer pool (якщо RAM дозволяє), зменшіть робочий набір (індекси, патерни запитів) або шардуйте навантаження. Не ігноруйте ОС; свап зіпсує все.

15) Підозра на мережеве сховище: швидко перевірити розподіл латентності

cr0x@server:~$ ioping -c 10 -W 2000 /var/lib/mysql
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=1 time=0.8 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=2 time=1.1 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=3 time=47.9 ms
...
--- /var/lib/mysql ioping statistics ---
10 requests completed in 12.3 s, min/avg/max = 0.7/6.4/47.9 ms

Значення: Цей максимальний сплеск латентності — саме те, як виглядає латентність коміту, коли диск підстрибнув.

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

16) Знайти retry-шторм у логах застосунку (самопідсилюваний сплеск)

cr0x@server:~$ journalctl -u app-ingester --since "10 min ago" | grep -E "database is locked|retrying" | tail -n 5
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=7
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=8
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=9
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=10
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=11

Значення: Ви не просто маєте контенцію; ви множите її повторними спробами.

Рішення: Додайте експоненціальний backoff з джиттером, обмежте кількість повторів і розгляньте single-writer чергу. Агресивні повтори — це те, як сплеск перетворюється в авралію.

Поширені помилки (симптом → корінна причина → виправлення)

1) Симптом: помилки «database is locked» під час сплесків (SQLite)

Корінна причина: Багато одночасних записувачів або довготривалі транзакції, що утримують блокування; реальність одного записувача стикається з багатопроцесним навантаженням.

Виправлення: Серіалізуйте записи явно (один потік/процес запису), використовуйте WAL-режим, встановіть розумний busy_timeout і пакетні коміти. Уникайте тривалих читальних транзакцій під час записів.

2) Симптом: періодичні затримки 200–2000 мс кожні N секунд (SQLite)

Корінна причина: Цикли WAL checkpoint або коміти журналу файлової системи, що створюють сплески синхронізації.

Виправлення: Контролюйте checkpoint (ручне під час тихих вікон), налаштуйте wal_autocheckpoint, знижуйте synchronous лише з чіткими вимогами на надійність і перевіряйте джиттер латентності сховища.

3) Симптом: MariaDB p99 сплески при низькому CPU

Корінна причина: I/O-залежні коміти: редо/бінлог fsync латентність домінує; потоки чекають на log flush або handler commit.

Виправлення: Пакетуйте транзакції, зменшіть конкуренцію, перегляньте innodb_flush_log_at_trx_commit і sync_binlog з бізнес-погодженням, і покращіть латентність сховища.

4) Симптом: пропускна здатність падає, коли ви «додаєте більше воркерів» (MariaDB)

Корінна причина: Контенція блокувань/затискачів або тиск на скидання, підсилений трясінням потоків; більше конкуренції збільшує контекстні переключення і контенцію.

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

5) Симптом: WAL файл росте вічно (SQLite)

Корінна причина: Довгоживучі читачі не дозволяють checkpoint завершитись; або налаштування автоперевірки не відповідають навантаженню.

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

6) Симптом: відставання реплікації MariaDB під час імпортів

Корінна причина: fsync бінлога і скидання redo під важким записовим навантаженням; однопотокове застосування (залежно від налаштувань) не встигає.

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

7) Симптом: «На моєму ноутбуці швидко, у проді повільно» (обидві)

Корінна причина: Семантика сховища відрізняється: NVMe ноутбука проти спільного хмарного тому; fsync і джиттер латентності — різні світи.

Виправлення: Бенчмарк на схожому до продакшену сховищі, виміряйте розподіл латентності і встановіть SLO навколо p99 латентності коміту — не лише середнього throughput.

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

Якщо ви обираєте між MariaDB і SQLite для навантажень з піковими записами

  1. Рахуйте записувачів, а не запити. Скільки процесів/хостів може писати одночасно?
  2. Визначте, чи можете забезпечити одного записувача. Якщо так — SQLite залишається варіантом.
  3. Чітко визначте вимоги на надійність. «Ми можемо втратити 1 секунду даних» — реальна вимога; «повинно бути надійно» — ні.
  4. Виміряйте латентність fsync сховища. Якщо вона стрибуча, обидві БД виглядатимуть ненадійними під сплесками.
  5. Плануйте backfill-и. Якщо ви регулярно імпортуєте або перепроцесуєте дані, з самого початку проектуйте throttle і пакетування.

План загартування SQLite під сплески (практично)

  1. Увімкніть WAL-режим і підтвердьте, що він залишається увімкненим.
  2. Встановіть busy_timeout на значення, що має значення (сотні до тисяч мс), і обробляйте SQLITE_BUSY з backoff + джиттером.
  3. Пакетуйте коміти: робіть коміт кожні N рядків або кожні T мс.
  4. Впровадьте чергу записів з одним потоковим записувачем. Якщо є багато процесів, введіть один процес-записувач.
  5. Контролюйте checkpoint: запускайте wal_checkpoint у періоди низького трафіку; налаштуйте wal_autocheckpoint.
  6. Слідкуйте за розміром WAL і успішністю checkpoint як за метриками першої черги.

План загартування MariaDB під сплески (практично)

  1. Підтвердьте, що ви використовуєте InnoDB для таблиць з інтенсивними записами.
  2. Розмір buffer pool так, щоб робочий набір поміщався настільки, наскільки дозволяє RAM, без свапу.
  3. Перевірте ємність redo логу; уникайте надто малих redo, що примушують часті checkpoint.
  4. Вирівняйте innodb_io_capacity з реальною здатністю сховища (не за побажаннями).
  5. Обмежте конкуренцію додатка; використовуйте connection pooling; уникайте штормів потоків.
  6. Пакетуйте записи і використовуйте multi-row inserts там, де це безпечно.
  7. Вимірюйте і ставте алерти на log waits, індикатори латентності fsync і відношення dirty сторінок.

Коли мігрувати з SQLite до MariaDB (або навпаки)

  • Мігрувати SQLite → MariaDB коли ви не можете забезпечити одного записувача, потрібні записи з декількох хостів або важливі операційні інструменти (реплікація/онлайн бекапи).
  • Мігрувати MariaDB → SQLite коли навантаження локальне, один записувач і ви платите зайві операційні накладні за невелику вбудовану базу даних.

Часті питання

1) Чи може SQLite витримати високу швидкість запису?

Так — якщо ви пакетируєте транзакції і тримаєте записувачів серіалізованими. SQLite може бути дуже швидкою на одному ядрі, бо уникає мережевих витрат і серверного оверхеду.

2) Чому SQLite каже «database is locked» замість того, щоб ставити записувачів у чергу?

Модель блокувань SQLite проста і свідома. Вона очікує, що додаток контролюватиме конкуренцію (busy_timeout, повтори і, бажано, один записувач). Якщо ви хочете, щоб база сама керувала важкою багатозаписовістю — вам потрібна серверна БД.

3) Чи завжди WAL-режим — правильний вибір для SQLite під сплесками?

Часто — але не завжди. WAL допомагає при конкурентних читаннях під час записів і може згладжувати стабільне навантаження записів. Він також вводить поведінку checkpoint, яку треба керувати. Ігнорування checkpoint призведе до періодичних затримок і гігантських WAL-файлів.

4) Для MariaDB, яке налаштування найсильніше впливає на поведінку під сплесками?

innodb_flush_log_at_trx_commit і (якщо використовується binlog) sync_binlog. Вони безпосередньо визначають, як часто ви платите вартість fsync. Зміна цих значень змінює надійність, тож ставтесь до цього як до бізнес-рішення.

5) Чому сплески записів іноді виглядають гірше після додавання індексів?

Індекси збільшують write amplification. Одна вставка перетворюється на кілька оновлень B-дерева і більше dirty сторінок. Під сплесками різниця між «одним записом» і «п’ятьма записами» — це ваш p99.

6) Чи варто розміщувати SQLite на мережевому сховищі?

Зазвичай ні. SQLite залежить від коректної, низьколатентної семантики блокувань і синхронізації. Мережеві файлові системи та деякі розподілені томи можуть зробити блокування непередбачуваним, а fsync — надзвичайно повільним. Якщо мусите — тестуйте конкретну реалізацію сховища під навантаженням.

7) Якщо MariaDB повільна під сплесками, чи треба просто масштабувати CPU?

Тільки якщо ви довели, що вузьке місце — CPU. Більшість проблем сплесків — це латентність I/O або контенція. Додавання CPU до fsync-вузького місця — як додати касирів, коли в магазині лише одна каса.

8) Який найпростіший спосіб змусити будь-яку базу витримувати сплески краще?

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

9) Хто безпечніший щодо надійності під сплесками?

Обидва можуть бути безпечними; обидва можуть бути налаштовані небезпечно. За замовчуванням MariaDB має консервативніші налаштування для серверних навантажень. SQLite теж може бути повністю надійною, але вартість продуктивності під сплесками більш помітна, бо вона сидить у вашому запитному шляху.

10) Як зрозуміти, чи ви заблоковані чекпоїнтами?

SQLite: WAL росте і checkpoint повідомляє «busy» або не усічається. MariaDB: log waits зростають, dirty сторінки наростають, і ви бачите затримки, пов’язані зі скиданням. У обох випадках корелюйте з пиками латентності диска.

Висновок: практичні кроки

Якщо у вас є пікові записи і ви вирішуєте між MariaDB та SQLite, не починайте з ідеології. Почніть з моделі записів.

  • Якщо ви можете забезпечити одного записувача, пакетувати коміти і тримати базу на низьколатентному локальному сховищі, SQLite витримає сплески тихо і дешево.
  • Якщо у вас багато записувачів між процесами/хостами і вам потрібні операційні інструменти як реплікація та спостережуваність, MariaDB — безпечніший вибір — за умови, що ви поважаєте фізику fsync і налаштовуєтесь обачно.

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

Dovecot: Maildir проти mdbox — оберіть сховище, яке не підведе вас пізніше

Ви не помічаєте формат поштової скриньки, коли все працює тихо. Помічаєте, коли iPhone CEO каже «Не вдається отримати пошту», диски 70% простою, а кожен IMAP-вхід нагадує переговори з шафою, повною конфетті.

Зберігання пошти — це одне з тих інфраструктурних рішень, що залишаються нудними — поки не стануть єдиною темою розмов. Тримаймо це нудним навмисно.

Рішення, яке справді має значення

«Maildir vs mdbox» звучить як дебат про формат. Це не так. Це дебат про філософію експлуатації:

  • Maildir ставить на карту семантику файлової системи: кожне повідомлення — окремий файл; атомарні перейменування — ваші друзі; корупція зазвичай локалізована; і ви отримуєте велику кількість інодів.
  • mdbox ставить на карту агрегацію під управлінням Dovecot: повідомлення живуть у більших контейнерних файлах з метаданими Dovecot; ви зменшуєте тиск на іноди; операції можуть бути швидшими при певних IO-шаблонах; але помилка може зачепити значно більше.

Якщо ви керуєте невеликим сервером з адекватною файловою системою і хочете простого налагодження та легкого часткового відновлення, Maildir — це за замовчуванням, який старіє добре. Якщо ви працюєте в масштабі, де кількість інодів, сканування каталогів і накладні витрати на дрібні файли вас вбивають, mdbox може бути правильною болючою мірою — але лише якщо ви дисципліновано підходите до резервного копіювання, обслуговування та оперативних інструментів.

Цитата, яку варто тримати на стікері, бо вона прямо застосовується до форматів поштових скриньок: «Надія — не стратегія.» — Gene Kranz.

Maildir і mdbox в одному огляді

Maildir: що це таке

Maildir зберігає кожне повідомлення як окремий файл у структурі директорій — зазвичай cur/, new/ та tmp/ для кожної скриньки. Прапорці часто кодуються у імені файлу. Доставка та переміщення покладаються на атомарність операцій перейменування.

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

mdbox: що це таке

mdbox зберігає повідомлення всередині «box» файлів, якими керує Dovecot (поряд з індексними та map-метаданими). Думайте про це як про те, що Dovecot бере на себе більшу частину шару зберігання: менше файлів, більше структури і більша залежність від гарантій консистентності Dovecot.

Операційний відчуттєвий ефект: Коли це швидко — приємно. Коли потрібно ремонтувати — хочете мати інструменти під рукою і перевірені резервні копії.

Що вам слід обрати (думка)

  • Оберіть Maildir, якщо: ви невелика або середня організація, цінуєте простоту відновлення, маєте пристойні SSD, використовуєте снапшоти/резервні копії і хочете передбачувані домени відмов.
  • Оберіть mdbox, якщо: у вас багато користувачів, багато повідомлень, реальний тиск на іноди, сканування директорій шкодить, або вам потрібні функції сховища, як-от ефективне зменшення кількості файлів — і ви готові експлуатувати обслуговування Dovecot і практику відновлення.
  • Уникайте «не має значення» як рішення. Воно має значення в той день, коли вам потрібно відновити одну скриньку о 3:00 ранку, а ваш процес відновлення — «відновити все і молитися».

Жарт #1: Рішення про зберігання пошти як татуювання: здаються веселими, поки не спробуєте їх видалити під час квартального огляду відмов.

Факти та історія, що пояснюють сучасні компроміси

Трохи контексту робить компроміси менш довільними. Ось конкретні пункти, що пояснюють, чому ці формати існують і чому вони ведуть себе так, як ведуться:

  1. Maildir створили, щоб уникнути проблем блокувань mbox. Традиційний mbox зберігав цілу скриньку в одному файлі; паралельний доступ історично викликав проблеми з блокуванням і ризиком корупції.
  2. «Атомарний трюк перейменування» Maildir залежить від гарантій файлової системи. Шаблон tmp→new/cur опирається на атомарність в межах однієї файлової системи.
  3. IMAP вибухнув у потребі метаданих для скриньок. Індексування, прапорці і відстеження UID стали критичними для продуктивності; індексні файли Dovecot — відповідь на цю реальність.
  4. Накладні витрати дрібних файлів стали більшою проблемою зі зростанням зберігання пошти. Мільйони дрібних файлів виснажують іноди, пошуки в каталогах і інструменти резервного копіювання; цей тиск — одна з причин появи агрегованих форматів.
  5. Dovecot ввів «box» формати, щоб зменшити навантаження на файлову систему. mdbox та подібні дизайни переносять частину роботи з файлової системи у структури під управлінням Dovecot.
  6. Еволюція файлових систем має значення. Ext4, XFS, ZFS і btrfs по-різному працюють з каталогами та метаданими; один і той же формат може бути «ок» на одній файловій системі і болісним на іншій.
  7. Снапшоти copy-on-write змінили очікування щодо резервного копіювання. З ZFS/btrfs зробити «консистентну точку в часі» легше — але тільки якщо ваші індекси та модель блокувань поводяться добре під снапшотом.
  8. Клієнти пошти стали агресивнішими. Мобільні клієнти роблять часті синхронізації; очікують серверний пошук/FTS; формати поштових скриньок, що посилюють IO метаданих, можуть виглядати гіршими сьогодні, ніж у 2008 році.

Як кожен формат відмовляє в продакшні

Режими відмов Maildir

1) «Забагато файлів» стає справжнім інцидентом. Ви досягаєте виснаження інодів, резервні копії повільніють або сканування каталогів стає вашим підлогою затримок. Maildir не попереджає ввічливо; він просто стає повільним, а потім раптово неможливим.

2) Часткова корупція виживає — але не без витрат. Декілька файлів повідомлень можуть бути пошкоджені через проблеми диска або збої передач. Зазвичай решту можна врятувати. Але якщо індексні файли збиваються, клієнти бачать відсутні або дубльовані повідомлення, поки ви не перебудуєте індекси.

3) Резервні копії брешуть, якщо ви не використовуєте снапшоти. Резервне копіювання по файлах під час доставки може зафіксувати несумісний стан (повідомлення в tmp/, часткові перейменування). Це може працювати, але ви маєте розуміти, що «консистентно» означає для maildir.

Режими відмов mdbox

1) Консистентність метаданих стає вашим життям. mdbox покладається на метадані Dovecot (індекси, map-файли). Якщо вони несинхронні або пошкоджені, скринька може виглядати порожньою або переплутаною, навіть якщо базові box-файли існують.

2) Більша зона ураження на файл. Пошкоджений контейнерний файл може зачепити більше повідомлень. Інструменти Dovecot можуть ремонтувати в багатьох випадках, але ізоляція «один файл — одне повідомлення» у Maildir тут не працює за замовчуванням.

3) Складність відновлення зростає. Відновлення однієї скриньки може бути простим, якщо у вас є директорії на користувача та хороші інструменти. Але може стати хаосом, якщо у вас «один гігантський том і надія». Дизайн має значення.

Жарт #2: Найкращий формат поштової скриньки — той, який ви можете відновити, поки ваша кава ще придатна для пиття.

Модель продуктивності: за що ви насправді платите

Прихований податок у Maildir: метадані та операції з каталогами

Продуктивність Maildir визначається метаданими файлової системи: створення, перейменування, stat, перелік каталогів та оновлення часових міток. Якщо у вас SSD і файлова система, яка добре працює з каталогами, maildir може бути дуже швидким. Якщо у вас обертові диски або перевантажені шляхи метаданих, maildir може відчуватися так, ніби він робить усе, окрім доставки пошти.

Коли в користувачів сотні тисяч повідомлень в одній папці, maildir може різко деградувати, бо сервер робить багато операцій з каталогами, просто щоб відповісти «що нового?». Індекси Dovecot допомагають, але фактична кількість файлів все одно переслідує вас у резервних копіях, часі fsck і використанні інодів.

Прихований податок у mdbox: структури під управлінням Dovecot і процес ремонту

mdbox зменшує тиск на кількість файлів, що може зменшити накладні витрати на каталоги та іноди. Але ви платите іншим ресурсом: довірою і підтримкою метаданих Dovecot. Це означає, що ви піклуєтеся про цілісність індексів, здоров’я map-файлів і про те, як ваші резервні копії/відновлення взаємодіють з цими файлами.

На завантажених системах mdbox може бути дружнішим до файлової системи, але також може посилити наслідки «хитрого» тюнінгу чи небезпечних практик резервного копіювання.

Затримка проти пропускної здатності: обирайте те, що відчувають користувачі

Більшість поштових відмов — це не «сервер не витримує пропускну здатність». Це «вхід повільний», «відкриття INBOX повільне», «пошук повільний», «оновлення прапорців повільні». Це затримка. Затримка походить від кругових запитів до сховища і конкуренції за метадані.

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

Резервні копії, відновлення і чому «це просто файли» — пастка

Резервні копії Maildir: оманливо просто

Maildir виглядає як «просто файли», що змушує людей розслабитися. Не робіть цього. Жива maildir має транзитні стани (tmp/), перейменування і оновлення індексів. Якщо ви робите резервні копії без снапшотів, ви можете зафіксувати скриньку в середині операції.

Що працює добре:

  • Снапшоти файлової системи (ZFS, btrfs, LVM thin snapshots) з читанням резервних копій зі снапшоту.
  • Резервні копії, які зберігають права доступу, власника і часові мітки (доставка пошти і Dovecot чутливі до цього).
  • Регулярні практики перебудови індексів під час тестів відновлення.

Резервні копії mdbox: вам потрібна консистентність, не просто копії

mdbox вимагає захоплення як box-файлів, так і метаданих/індексів в одній консистентній точці. Снапшот-орієнтовані резервні копії — розумна відправна база. Якщо ви покладаєтеся на копіювання файлу за файлом без снапшотів, ризикуєте зафіксувати невідповідні стани — box-файл каже одне, індекс — інше, map — третє.

Відновлення: плануйте «один користувач, одна папка, одне повідомлення»

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

Якщо ви не можете відновити одну скриньку без відновлення всього, ви не обрали формат зберігання — ви обрали майбутній інцидент.

Реальність реплікації/високої доступності

Формат скриньки не замінює стратегію реплікації. Він лише змінює режими відмов і зручність експлуатації.

Реплікація Dovecot і вибір формату

Dovecot може реплікувати поштові скриньки на рівні застосунку. Це може згладити деякі відмінності файлової системи. Але реплікація не виправить:

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

Снапшоти — не реплікація, реплікація — не резервне копіювання

Снапшоти дають відновлення до точки в часі; реплікація дає доступність. Вам потрібні обидва, якщо система пошти важлива. Якщо ви можете дозволити собі лише одне, обирайте перевірені резервні копії. Доступність без можливості відновлення — просто швидший шлях до продовження простою.

Практичні завдання: команди, виводи і що ви вирішуєте

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

1) Підтвердити поточний формат скриньки

cr0x@server:~$ doveconf -n | egrep '^(mail_location|mail_attachment_dir|mail_plugins|namespace|mail_fsync)'
mail_location = maildir:~/Maildir
mail_plugins = $mail_plugins quota
mail_fsync = optimized

Значення: Цей сервер використовує maildir в домашній директорії кожного користувача. Якщо ви очікували mdbox, ваші «припущення щодо продуктивності» вже неправильні.

Рішення: Підлаштуйте налагодження під формат: тиск інодів і операції з каталогами важливіші для maildir; цілісність метаданих і map/index важливіші для mdbox.

2) Перевірити версію Dovecot (функції та баги мають значення)

cr0x@server:~$ dovecot --version
2.3.19.1 (9b53102964)

Значення: У вас сучасна 2.3.x. Поведінка відрізняється між мажорними/мінорними релізами, особливо щодо обробки індексів і налаштувань fsync за замовчуванням.

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

3) Виміряти використання інодів (мовчазний вбивця Maildir)

cr0x@server:~$ df -ih /var/vmail
Filesystem      Inodes IUsed   IFree IUse% Mounted on
/dev/sdb1         50M   41M     9M   83% /var/vmail

Значення: 83% використання інодів. Це не «гарно». Це «один неправильний імпорт від простою».

Рішення: Якщо використання інодів зростає швидко, або (a) перейти на файлову систему з більшою кількістю інодів / іншою стратегією розподілу, (b) впровадити політики зберігання, (c) перемістити об’ємних користувачів в mdbox, або (d) переразробити структуру папок/архівування.

4) Порахувати файли повідомлень у «гарячій» папці

cr0x@server:~$ find /var/vmail/acme.example/jane/Maildir/cur -type f | wc -l
287641

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

Рішення: Розглянути розбиття папок (архіви за роками), зміну політик клієнта або переміщення цього користувача в mdbox, якщо експлуатаційні витрати повторюються.

5) Визначити, чи Dovecot проводить час у IO wait

cr0x@server:~$ iostat -x 1 3
Linux 6.1.0-18-amd64 (server) 	01/03/2026 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           3.21    0.00    1.10   24.50    0.00   71.19

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  r_await  w_await  svctm  %util
nvme0n1         85.00  220.00  7400.0 14800.0     95.0     3.20   10.50     5.10    12.60   0.35  10.70

Значення: CPU iowait високий (24.5%). Сховище не завантажене (%util ~10%), але затримка (await) не надто добра. Часто це вказує на шаблони із синхронними записами або конкуренцію метаданих, а не на лінійний дефіцит пропускної здатності.

Рішення: Перевірте налаштування fsync, процеси Dovecot, що викликають «шторм» синхронізацій, і опції монтування файлової системи. Для maildir шаблони записів метаданих можуть створювати це навіть на SSD.

6) Подивитися, які процеси створюють IO-тиск

cr0x@server:~$ pidstat -d 1 5
Linux 6.1.0-18-amd64 (server) 	01/03/2026 	_x86_64_	(8 CPU)

# Time        UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:10:01     1001     23142      0.00   9200.00      0.00  dovecot
12:10:01     1001     23188      0.00   5100.00      0.00  dovecot
12:10:01        0     11422      0.00   1400.00      0.00  rsync

Значення: Dovecot багато пише (ймовірно, оновлення індексів, зміни прапорців, доставки). Є також rsync — класичний «резервне копіювання конкурує з живим IO».

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

7) Перевірити стан сервісів Dovecot і конкуренцію

cr0x@server:~$ doveadm service status
auth: client connections: 12, server connections: 12
imap: client connections: 380, server connections: 380
lmtp: client connections: 0, server connections: 0
indexer-worker: client connections: 8, server connections: 8

Значення: IMAP має 380 активних підключень. Працюють indexer worker-и. Якщо у вас недостатньо ресурсів для індексації, пошук і відкриття скриньок можуть тягнутися.

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

8) Виміряти затримку відкриття скриньки та статус (з перспективи Dovecot)

cr0x@server:~$ doveadm -v mailbox status -u jane@example.com messages recent uidnext unseen INBOX
INBOX messages=142003 recent=0 uidnext=412887 unseen=12

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

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

9) Виявити і безпечно перебудувати пошкоджені індекси

cr0x@server:~$ doveadm -Dv index -u jane@example.com INBOX
doveadm(jane@example.com): Debug: Loading modules from directory: /usr/lib/dovecot/modules
doveadm(jane@example.com): Debug: Added plugin: quota
doveadm(jane@example.com): Debug: Finished indexing INBOX

Значення: Dovecot може перебудувати індекси. Вивід debug підтверджує завантаження модулів і завершення індексації.

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

10) Шукати помилки на рівні файлової системи (бюрократична правда)

cr0x@server:~$ dmesg -T | egrep -i 'ext4|xfs|btrfs|zfs|nvme|i/o error|reset|abort' | tail -n 10
[Fri Jan  3 11:58:41 2026] nvme nvme0: I/O 123 QID 6 timeout, completion polled
[Fri Jan  3 11:58:41 2026] nvme nvme0: resetting controller
[Fri Jan  3 11:58:43 2026] EXT4-fs warning (device sdb1): ext4_dx_add_entry: Directory index full, reach max htree level

Значення: NVMe таймаути і попередження індексації директорій ext4. Це не проблема Dovecot. Це поведінка зберігання і файлової системи під навантаженням.

Рішення: Виправте апаратне/прошивкове забезпечення, перевірте здоров’я NVMe і розгляньте налаштування файлової системи або перенесення поштового сховища на FS, краще придатну для великих каталогів.

11) Підтвердити фактичне здоров’я диска (перед тим як звинувачувати формат)

cr0x@server:~$ smartctl -a /dev/nvme0 | egrep -i 'critical_warning|media_errors|num_err_log_entries|temperature'
Critical Warning:                   0x00
Temperature:                       41 Celsius
Media and Data Integrity Errors:    0
Error Information Log Entries:      2

Значення: Немає помилок медіа, але є записи журналу помилок. У поєднанні з NVMe скиданнями це може означати періодичні проблеми контролера/прошивки.

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

12) Перевірити розподіл директорій і файлів (виявити патологічні макети)

cr0x@server:~$ du -sh /var/vmail/acme.example/jane/Maildir
96G	/var/vmail/acme.example/jane/Maildir

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

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

13) Перевірити опції монтування файлової системи (тут ховаються вбивці латентності)

cr0x@server:~$ findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /var/vmail
/var/vmail /dev/sdb1 ext4 rw,relatime,errors=remount-ro

Значення: Стандартні опції. Якщо ви бачите sync або надто агресивні налаштування журналювання, ви могли самі собі створити латентність.

Рішення: Уникайте сліпого копіювання опцій монтування. Вносьте зміни тільки з виміряними покращеннями латентності і планом відкату.

14) Для mdbox: знайти ключові метадані і перевірити ознаки базової цілісності

cr0x@server:~$ ls -la /var/vmail/acme.example/jane/mdbox/ | head
total 64
drwx------  5 vmail vmail 4096 Jan  3 12:01 .
drwx------ 12 vmail vmail 4096 Jan  3 12:01 ..
-rw-------  1 vmail vmail 8192 Jan  3 11:59 dovecot.index
-rw-------  1 vmail vmail 4096 Jan  3 11:59 dovecot.index.log
-rw-------  1 vmail vmail 2048 Jan  3 12:00 dovecot.map.index
-rw-------  1 vmail vmail 4096 Jan  3 11:58 storage

Значення: Присутність індексних і map-файлів очікувана. Відсутні або нульового розміру файли під час нормальної роботи можуть індикувати корупцію або проблеми з правами.

Рішення: Якщо вони відсутні або недоступні для читання — спочатку виправте права/власність; якщо підозрюєте корупцію — переходьте до відновлення зі снапшоту або робочих процесів ремонту Dovecot.

15) Спостерігати активну конкуренцію блокувань файлів скриньки

cr0x@server:~$ lsof +D /var/vmail/acme.example/jane/Maildir 2>/dev/null | head -n 10
COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
dovecot 23142 vmail   15r  REG  8,17     12456 918273 /var/vmail/acme.example/jane/Maildir/dovecot.index
dovecot 23142 vmail   16u  REG  8,17     40960 918274 /var/vmail/acme.example/jane/Maildir/dovecot.index.log
dovecot 23188 vmail   18r  REG  8,17     53248 918275 /var/vmail/acme.example/jane/Maildir/cur/1735891023.M1234P23188.server,S=53248:2,S

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

Рішення: Якщо конкуренція постійна, оцініть налаштування індексів, латентність сховища і поведінку клієнтів (наприклад, клієнти, що постійно проганяють повну синхронізацію).

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

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

Перш за все: доведіть, чи це латентність сховища, CPU або конкуренція Dovecot

  • IO wait і латентність: iostat -x 1 3 і pidstat -d 1 5. Високий iowait або await вказує на сховище або шаблони синхронізації.
  • Навантаження CPU: top або pidstat -u 1 5. Якщо CPU завантажений, ви не обираєте між maildir і mdbox — ви обираєте між масштабуванням і переписуванням.
  • Тиск підключень: doveadm service status. Якщо IMAP-з’єднання стрибають і процеси голодують, налаштуйте ліміти процесів і поведінку клієнтів.

По-друге: визначте, чи проблема — «метадані файлової системи», чи «метадані Dovecot»

  • Підозри Maildir: використання інодів (df -ih), величезні підрахунки файлів у папці (find ... | wc -l), попередження ext4 в dmesg.
  • Підозри mdbox: відсутні/некоректні dovecot.map.index і інтенсивний лог індексації, повільні запити статусу попри адекватні метрики сховища.

По-третє: перевірте, чи проблема локалізована або системна

  • Запустіть doveadm mailbox status для одного «гарячого» і одного «нормального» користувача.
  • Якщо лише кілька користувачів повільні — обробляйте їх як особливі випадки (архівування, розбиття скриньки, інший формат, інший том).
  • Якщо всі повільні — підозрюйте сховище, глобальні індексери, резервні копії або недавні зміни в монтуванні/ядрі/прошивці.

По-четверте: оберіть найменш ризикову корекцію

  • Перебудова індексів (безпечно і відкатно).
  • Зупинити конкуренцію IO (резервні копії, антивірусні сканування, агресивне відпилювання логів).
  • Виправити помилки файлової системи/апаратні проблеми перед тюнінгом Dovecot.
  • Тільки потім розглядати міграцію між форматами.

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

1) «IMAP-вхід повільний, але завантаження диска низьке»

Симптоми: Користувачі повідомляють про затримки при відкритті папок; моніторинг показує низьке %util на дисках.

Корінь: Висока затримка на операцію (метадані IO, синхронні записи, пошуки в каталогах). Низьке використання не означає низьку затримку.

Виправлення: Виміряйте await за допомогою iostat -x. Якщо затримка висока, зменшіть синхронно-важкі поведінки, перемістіть резервні копії з живої FS і розгляньте mdbox, якщо домінує тиск інодів/каталогів.

2) «Резервні копії консистентні, бо ми використовуємо rsync вночі»

Симптоми: Відновлення дає дивні скриньки: відсутні недавні повідомлення, дублікати або клієнти запускають шалені синхронізації.

Корінь: Копіювання файл за файлом захопило maildir/mdbox під час оновлення; індекси і повідомлення з різних точок в часі.

Виправлення: Резервне копіювання через снапшоти. Відновлюйте зі снапшоту. Перебудуйте індекси після відновлення за допомогою doveadm index або обережно видаляючи застарілі індексні файли.

3) «Ми поклали все в одну масивну INBOX»

Симптоми: Один або два користувачі постійно повільні; вікна резервних копій вибухають; з’являються попередження файлової системи.

Корінь: Великі папки підсилюють витрати на перелік і метадані (maildir) або накладні витрати на індексацію (будь-який формат).

Виправлення: Впровадіть політики архівування і структурування папок. Розгляньте серверні правила Sieve. Розділіть «гарячі» скриньки по шарах зберігання.

4) «Ми мігрували формати і не спланували перехід індексів»

Симптоми: Після міграції клієнти бачать відсутні повідомлення поки не синхронізують; навантаження сервера зростає.

Корінь: Індекси не були чисто перебудовані або відновлені неконсистентно; клієнти викликають інтенсивну синхронізацію.

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

5) «Ми відключили fsync заради продуктивності»

Симптоми: Продуктивність покращилась… поки не стався збій або відключення живлення; тоді користувачі втрачають оновлення прапорців, недавні доставки або бачать корумпований стан.

Корінь: Небезпечні налаштування стійкості. Зберігання пошти інтенсивно пише метадані; втрата кількох секунд може створити заплутані невідповідності.

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

6) «Антивірус сканує весь поштовий магазин кожну годину»

Симптоми: Періодичні сплески латентності; багато пропусків кешу; IO-сплески; користувачі скаржаться хвилеподібно.

Корінь: Багато файлів Maildir карає за повні дерева сканування; навіть mdbox страждає, якщо сканування витрашає кеші.

Виправлення: Сканувати при прийомі (LMTP/SMTP pipeline) або використовувати цілеспрямоване сканування. Виключайте індекси і тимчасові папки з широких сканів, де це доречно.

7) «Ми вважали, що файлова система не має значення»

Симптоми: Одна і та сама конфігурація поводиться по-різному на різних хостах; оновлення «випадково» змінюють продуктивність.

Корінь: Індексація каталогів, поведінка аллокатора і журналювання залежать від файлової системи і версії ядра.

Виправлення: Стандартизуйте вибір файлової системи і опції монтування для поштових томів. Бенчмаркуйте операції зі скриньками, а не лише послідовну пропускну здатність.

Три корпоративні міні-історії (анонімізовані, правдоподібні та повчальні)

Міні-історія 1: Аварія, спричинена хибним припущенням

Вони керували середньою платформою пошти. Нічого екзотичного: Dovecot, Postfix, кластер віртуальних машин і «тимчасовий» том сховища, який став постійним. Новий член команди запитав, який формат скриньки вони використовують. Відповідь була впевненою і хибною: «Це Maildir, тож це просто файли. Резервні копії прості».

Резервне завдання було нічним копіюванням по дереву файлів. Без снапшотів. Завдання працювало під час доставок і іноді в піковий час мобільної синхронізації. Воно «працювало» у тому сенсі, що створювало купу файлів. Також воно захоплювало транзитні стани: повідомлення в tmp, перейменування напівзавершені, індексні логи в середині оновлення.

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

Корінь проблеми не був у Maildir. Корінь був у припущенні, що файлове резервне копіювання рівнозначне консистентному. Вони перейшли на снапшот-орієнтовані резервні копії і додали тренування відновлення, яке включало перебудову індексів і поетапне підключення клієнтів. Наступне відновлення було нудним. Всім стало менше боляче. Оце і показник успіху.

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

Інша компанія мала серйозний тиск інодів. Вони перейшли на mdbox, щоб зменшити кількість файлів і скоротити час сканування резервних копій. Добре обґрунтоване рішення. Потім вони вирішили додатково підтиснути продуктивність, зменшивши налаштування стійкості: знизили синхронізацію і посилили кешування записів. Виглядало чудово в бенчмарках. Графіки стали гарніші.

Потім сталася подія живлення в одному стійці. Не катастрофа, просто кілька хвилин хаосу. Системи перезавантажилися. Більшість сервісів відновилися. Пошта — ні. Користувачі могли входити, але нові листи з’являлися неконсистентно, а прапорці поводилися як рекомендації. Деяка пошта існувала в box-файлах, але не відображалася в IMAP, поки індекси не були відремонтовані. Деякі ремонти спрацювали; інші вимагали відновлення метаданих зі снапшотів.

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

Вони відкотили ризикові налаштування, інвестували в захист від втрати живлення для сховища і стандартизували підхід snapshot+replication. Продуктивність стала трохи гірша за фантазію бенчмарків. Доступність була кращою за реальність відмов. Обирайте реальність.

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

Регульована організація експлуатувала Dovecot в масштабі. Їхнє сховище було настільки велике, що «відновити все» не було планом; це було прощальним листом. Вони використовували Maildir для більшості користувачів і mdbox для підмножини облікових записів з великим об’ємом. Ключем були не формати — ключем була дисципліна.

Вони практикували відновлення щоквартально. Не «ми тестували резервні копії». Справжні відновлення: вибрати випадкову скриньку, відновити її на ізольований хост, перебудувати індекси, перевірити доступ по IMAP і звірити кількість повідомлень і недавні доставки. Вони також мали політику: робити снапшоти кожні кілька хвилин, тримати короткий локальний ретеншн, реплікувати снапшоти поза вузлом і тестувати шлях відновлення наскрізь.

Коли контролер сховища почав давати збої, вони побачили це рано в dmesg і SMART. Перш ніж сталося відновлення даних, вони відключили поштовий сервіс на репліку і продовжили обслуговувати користувачів. Потім вони відновили декілька уражених скриньок з останнього відомо-робочого снапшоту і перевірили їх інструментами Dovecot перед поверненням у виробничу роботу.

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

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

Вибір формату: контрольний список рішень

  1. Скільки повідомлень на користувача? Якщо багато користувачів мають понад 200k повідомлень у папці, Maildir покарає вашу файлову систему, якщо ви не керуєте розбиттям папок.
  2. Чи обмежені ви інодами? Якщо df -ih показує тенденцію вище 70% і зростає, сприймайте це як проблему масштабування, а не попереджувальний знак.
  3. Чи є у вас резервні копії на основі снапшотів? Якщо ні — виправте це перед змінюваністю формату. Інакше ви просто змінимо характер невідповідностей резервних копій.
  4. Чи потрібне вам просте часткове відновлення? Maildir зазвичай дружніший для хірургічного відновлення. mdbox може бути придатний, але вам потрібні відпрацьовані інструменти і процеси.
  5. Яка у вас файлова система? Бенчмаркуйте операції зі скриньками на реальній файловій системі і ядрі, на якому ви працюватимете. Поштові навантаження важко передбачувані і метадані-важкі.
  6. Чи є у вас час персоналу на обслуговування? Якщо ні — оберіть шлях з найпростішими day-2 операціями: Maildir плюс хороше снапшотування.

План міграції: Maildir → mdbox (відносно безпечна послідовність)

  1. Проведіть інвентар користувачів і визначте «гарячі» скриньки (великі папки, висока текучість).
  2. Впровадьте резервні копії на основі снапшотів і проведіть відновлення до тестового зразка перед міграцією.
  3. Налаштуйте стендовий сервер з цільовою версією Dovecot і конфігурацією.
  4. Мігруйте маленьку пілотну групу першою. Моніторте затримку IMAP, час перебудови індексів і видимі проблеми для користувачів.
  5. Плануйте міграції у непіковий час. Обмежуйте конкурентність. Не мігруйте всіх одразу, якщо вам не до вподоби ризик.
  6. Після кожної партії: перебудуйте індекси, звірте кількість повідомлень і стежте за хвилями клієнтських синхронізацій.
  7. Майте можливість відкотитися через снапшоти і чіткий маркер cutover.

Операційний базовий набір (незалежно від формату)

  • Резервні копії на основі снапшотів з перевіреними відновленнями.
  • Моніторинг використання інодів, росту каталогів і обсягу пошти.
  • Моніторинг латентності сховища (await) і помилок ядра щодо зберігання.
  • Визначені процедури для перебудови індексів і ремонту скриньок.
  • Контроль поведінки клієнтів, де це можливо (агресивні схеми синхронізації можуть вас DOS-ити).

Часті запитання

1) Чи Maildir завжди безпечніший за mdbox?

Ні. Maildir часто має меншу зону ураження при пошкодженні окремого файлу, але може драматично падати через виснаження інодів і накладні витрати на метадані. «Безпечніший» залежить від того, що саме ви, ймовірно, зіпсуєте.

2) Чи mdbox завжди швидший?

Ні. mdbox може зменшити накладні витрати дрібних файлів, але якщо ваш вузький горлечко — індексний шторм, налаштування синхронізації або повільна латентність сховища, це не вирішить проблему самостійно. Він також додає складності під час ремонту та відновлення.

3) Яка основна причина, чому Maildir-системи з часом повільнішають?

Зростання кількості файлів плюс операції метаданих. Система не сповільнюється лінійно; вона починає страждати, коли каталоги стають величезними, резервні копії повзуть, а використання інодів наближається до обриву.

4) Яка основна причина, чому mdbox-системи стають болючими?

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

5) Чи варто зберігати пошту на NFS?

Тільки якщо ви розумієте семантику блокувань NFS сервер/клієнт, характеристики латентності і поведінку при відмовах під навантаженням. Зберігання пошти важко навантаження і чутливе до стрибків латентності. Багато «таємничих» проблем з поштою — це просто мережеве сховище, що поводиться по-своєму.

6) Чи можна змішувати формати на одному хості?

Так, і інколи це прагматично: тримайте більшість користувачів на Maildir і перемістіть облікові записи з великим об’ємом в mdbox. Просто переконайтеся, що ваші оперативні інструменти покривають обидва варіанти.

7) Чи замінюють снапшоти реплікацію Dovecot?

Ні. Снапшоти допомагають повернутися назад і відновити стан. Реплікація допомагає залишатися доступним. Вони вирішують різні проблеми і відмовляють по-різному.

8) Як зрозуміти, чи це корупція індексів чи реальна втрата повідомлень?

Порівняйте реальність файлової системи з видом IMAP. Якщо повідомлення існують на диску (файли maildir або storage у mdbox), але не відображаються — перебудуйте індекси. Якщо їх нема на диску — це втрата, і потрібні відновлення.

9) Яке «мінімально життєздатне» обслуговування для здорового шару зберігання Dovecot?

Резервні копії на основі снапшотів, періодичні відновлення, моніторинг використання інодів і латентності сховища, а також рунбук для перебудови індексів/ремонту. Все інше — оптимізація.

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

  1. Запустіть базові перевірки: doveconf -n, df -ih, iostat -x і doveadm mailbox status для «гарячого» користувача. Запишіть, що насправді правда.
  2. Перевірте консистентність резервних копій: Якщо ви не робите снапшоти, вважайте свої резервні копії «кращими зусиллями», а не відновленням, на яке можна покластися.
  3. Зробіть одне відновлення-тренування: Виберіть одну скриньку, відновіть її в ізольованому середовищі, перебудуйте індекси, перевірте IMAP. Заміряйте час. Задокументуйте процес.
  4. Визначте «кліф зростання»: Іноди, величезні папки і латентність сховища — три великі проблеми. Встановіть оповіщення для них.
  5. Приймайте рішення свідомо: Якщо ваша біль — тиск інодів і накладні витрати метаданих, mdbox може бути рішенням. Якщо ваша біль — відновлюваність і простота операцій, лишайтеся на Maildir і масштабируйте файлову систему та підхід до резервного копіювання правильно.

Оберіть формат, що відповідає вашому бюджету відмов і вашій реальності відновлення. Ваше майбутнє «я» буде тим, хто на виклику. Не жартуйте над ним.