Ви прокидаєтеся від повідомлення про заповнений кореневий файловий простір. Docker «використовує лише 40GB», згідно з docker system df,
але df -h кричить. CI-зборки повільні. Prune не допомагає. Хтось пропонує «просто додати диск»,
і ви відчуваєте, як ваш майбутній on-call буде подавати скаргу.
На ZFS різниця між хостом, який спокійно працює роками з контейнерами, і хостом, який вибухає в тисячі дрібних шарів,
снапшотів і клонів — здебільшого в одному: розмітка датасетів. Зробіть це правильно — і використання диска стане зрозумілим.
Зробіть помилково — і вам доведеться займатися археологією з zdb о 2 ранку.
Як виглядає «експлозія шарів» на ZFS
Образи Docker — це стек шарів. З драйвером збереження ZFS кожен шар може відображатися на датасет ZFS
(або клон снапшота) залежно від реалізації Docker і версії. Це не обов’язково погано.
Клони ZFS дешеві при створенні. Рахунок приходить пізніше, з відсотками:
- Шторм клонів: кожен запуск контейнера породжує записуваний клон, і ваш пул починає
виглядати як генеалогічна діаграма. - Розростання снапшотів: шари супроводжуються снапшотами; снапшоти зберігають блоки; «видалені» дані
все ще рахуються, бо на них є посилання. - Хаос метаданих: багато дрібних датасетів означає багато метаданих датасетів, операцій монтування
та несподіваних спадкувань властивостей. - Облік простору брешеться (для людей):
dfбачить монтування, Docker бачить логічні шари,
ZFS бачить посилені байти, і ваш мозок не бачить нічого чіткого.
Експлозія шарів — це не лише «занадто багато образів». Це занадто багато об’єктів файлової системи, термін життя яких не відповідає вашому операційному циклу.
Виправлення не в «щоб більше прати»; виправлення — у вирівнюванні меж датасетів ZFS з тим, що ви реально управляєте: станом engine, кешем образів,
записуваними шарами контейнерів і персистентними даними додатків.
Цікаві факти та історичний контекст
- Клони ZFS створювалися для миттєвого провізіонування (думаємо про середовища розробки та шаблони віртуальних машин).
Модель шарів Docker іноді точно відповідає цій формі — іноді надто. - Docker спочатку сильно просував AUFS, бо union-файлові системи були найпростіше зрозумілою моделлю для шарів.
ZFS з’явився пізніше як драйвер з іншими семантиками й гострими кутами. - OverlayFS став стандартом для більшості дистрибутивів Linux, здебільшого тому, що він «достатньо хороший»
і знаходиться в ядрі, без окремої історії модуля ZFS. - ZFS відстежує «referenced» vs «logical» простір. Ось чому «я видалив» не те саме, що «пул звільнився», коли є снапшоти й клони.
- Значення recordsize за замовчуванням (128K) походить від задач, орієнтованих на пропускну здатність.
Бази даних, навантаження з дрібними файлами та шари контейнерів іноді вимагають інших значень, і «один розмір для всіх» — міф. - Компресія LZ4 стала очевидним вибором у багатьох розгортаннях ZFS, бо вона швидка й часто значно зменшує записи — особливо для шарів образів, повних тексту й бінарників.
- Dedup часто виявляється попереджувальною історією у світі ZFS: приваблива на слайдах, безжальна у вимогах до RAM і складності експлуатації.
Образи контейнерів спокушають на її використання. - «ZFS on Linux» еволюціонував у OpenZFS як кросплатформений проєкт. Ця зрілість — причина, чому багато людей тепер впевнено запускають ZFS у production для хостів контейнерів.
Цілі проєктування: що дає розумна розмітка
Якщо ви запускаєте Docker на ZFS у production, ви хочете, щоб хост поводився як пристрій:
передбачувані оновлення, нудні відкати, легке планування ємності й відмови, які сигналізують рано, а не тихо пізно.
1) Розділення життєвих циклів
Стан Docker engine, кеші образів, записувані шари й персистентні томи не мають одного життєвого циклу.
Трактування їх як одне дерево під одним датасетом — найкоротший шлях до «ми не можемо прибрати це, не ризикуючи production».
2) Зробіть облік простору читабельним
ZFS може точно сказати, де байти — якщо ви дасте йому межі, що відповідають вашій ментальній моделі.
Датасети дають вам used, usedbydataset, usedbysnapshots, usedbychildren, квоти та резерви.
Монолітний датасет дає одну велику цифру і головний біль.
3) Припиніть «сміття виживає»
Docker-хаос породжує сміття, яке залежить від снапшотів і клонів. Розмітка має зробити можливим безпечне (і швидке) знищення цілих піддерев, коли ви вирішите, що кеш або шари — одноразові.
4) Зберігайте можливість тонкого налаштування продуктивності
Записувані шари контейнерів поводяться як випадкові дрібні записи. Звантаження образів схоже на послідовні записи. Бази даних у томах мають власні потреби.
Вам потрібні властивості на рівні датасету, щоб налаштовувати без перетворення всього пулу на науковий експеримент.
Парафразуючи ідею Вернера Вінглайса: «Все ламається, весь час — проектуйте так, щоб відмови були ізольованими і відновлюваними.»
Саме це дають межі датасетів у випадку збоїв зберігання: вони обмежують радіус ураження.
Рекомендована розмітка датасетів (робіть так)
Шаблон простий: один пул, кілька топ-рівневих датасетів з чіткою відповідальністю, і рівно одне місце, де Docker має право робити свої дивні операції зі снапшотами/клонами.
Пул і топ-рівневі датасети
tank/ROOT/<os>— ваші датасети кореня ОС, керовані інструментами ОСtank/var— загальний/var, не для Dockertank/var/lib/docker— внутрішній світ Docker (образи, шари, метадані)tank/var/lib/docker/volumes— опційно окремо, але зазвичай я віддаю перевагу томам поза деревом Docker (див. нижче)tank/containers— персистентні дані додатків (bind-монти, compose томи через шляхи хоста)tank/containers/<app>— датасети на додаток з квотами/refquotastank/backup— цілі реплікації (receive-only), не живі навантаження
Опінонізоване правило: тримайте персистентні дані поза датасетом драйвера Docker
Драйвер ZFS для Docker оптимізований під поведінку образів і шарів, а не під «вашу базу даних, яку ніколи не можна видалити».
Розміщуйте персистентні дані в виділених датасетах, змонтованих у стабільному місці, як-от /containers, і bind-монтуйте їх у контейнери.
Це дає чисту реплікацію і зрозумілі політики збереження.
Властивості, що зазвичай працюють
Це за замовчуванням, не догма. Але це «нудні» значення, які витримують контакт з CI,
піковими логами й сонними адміністраторами.
- Docker датасет:
compression=lz4,atime=off,xattr=sa,acltype=posixacl(якщо дистрибутив цього очікує),recordsize=16Kабо32K(часто краще для хаосу шарів, ніж 128K). - Бази даних у персистентних датасетах: налаштовуйте під движок; поширена відправна точка —
recordsize=16Kдля PostgreSQL, інколи8K, і розгляньтеlogbias=latencyдля синхронно-важких навантажень. - Датасет логів: часто
compression=lz4іrecordsize=128Kпідходить; більша боротьба — це збереження логів, не розмір блоків. - Бекапи:
readonly=onна receive-цілях; запобігає випадковим редагуванням.
Жарт №1: ZFS снапшоти як офісна пошта — видалення елемента не означає, що його немає, це означає «хтось інший заархівував назавжди».
Де насправді зупиняється експлозія шарів
Експлозія шарів стає керованою, коли:
- Датасет драйвера Docker — одноразовий і обмежений квотами (щоб невдалий збір не з’їв весь хост).
- Персистентні дані живуть деінде, тож «вимести стан Docker» — реальний варіант відновлення.
- Снапшоти на датасеті Docker або уникнуті, або строго контролюються (бо Docker уже використовує снапшоти/клони внутрішньо).
Чому це працює: механіка ZFS, що має значення
Клони зберігають блоки
Драйвер ZFS покладається на снапшоти й клони: шари образів стають снапшотами, записувані шари — клонами.
Це ефективно — поки ви не намагаєтесь звільнити простір. Видалений шар може все ще посилатися на блоки через ланцюги клонів.
Пул бачить «referenced bytes», і ці байти не зникають лише тому, що Docker про них «забув».
Межі датасетів — це операційні межі
Якщо стан Docker — один датасет, ви можете задати властивості для всього набору поведінок. Ви також можете
знищити його як одиницю. Якщо персистентні томи живуть всередині цього датасету, ви приробили свої коштовності
до купи сміття.
Quota і refquota — різні інструменти
quota обмежує датасет плюс його дітей. refquota обмежує лише сам датасет.
Для «кожному додатку по 200G, але може створювати дочірні датасети всередині» — корисний quota.
Для «цей датасет не повинен рости, незалежно від снапшотів в інших місцях» — refquota дає пряміший контроль.
Поведіка монтувань має значення для надійності Docker
Docker очікує, що /var/lib/docker присутній рано і залишається стабільним. Датасети ZFS монтуються через zfs mount
(часто керується systemd-сервісами). Якщо ви поховаєте Docker всередині авто-монтуємого ієрархічного шляху з краєвими залежностями,
рано чи пізно ви отримаєте гоночну умову при завантаженні й дуже спантеличений демон Docker.
ARC-тиск реальний на хостах контейнерів
ZFS любить RAM. Хости контейнерів теж люблять RAM. Якщо ви не обмежите ARC на завантаженому вузлі, ви можете позбавити ресурсу
робочі навантаження контейнерів тонкими способами: підвищений reclaim, стрибки латентності й багато «повільно, але нічого не завантажено».
Жарт №2: Dedup виглядає як безкоштовне зберігання, поки ваш RAM не дізнається, що таке «змушені понаднормові години».
Практичні завдання (команди, виводи та рішення)
Це реальні операційні завдання, які ви можете виконати на Docker-хості з ZFS. Кожне включає: команду, приклад виводу,
що означає вивід, і яке рішення з нього випливає. Виконуйте їх у цьому порядку, коли нарощуєте впевненість, і в більш тісному циклі під час гасіння пожеж.
Завдання 1: Підтвердити, що Docker справді використовує драйвер збереження ZFS
cr0x@server:~$ docker info --format '{{.Driver}}'
zfs
Значення: Сторор образів/шарів Docker є ZFS-aware. Якщо там overlay2, ця стаття все ще корисна для томів, але не для механіки шарів.
Рішення: Якщо не zfs, зупиніться й вирішіть, чи ви мігруєте драйвери, чи просто організовуєте томи.
Завдання 2: Визначити датасет, що підкріплює /var/lib/docker
cr0x@server:~$ findmnt -no SOURCE,TARGET /var/lib/docker
tank/var/lib/docker /var/lib/docker
Значення: Ваш Docker root — це датасет, а не просто директорія. Добре — тепер ви можете встановлювати властивості й квоти чисто.
Рішення: Якщо це не датасет (наприклад, показує /dev/sda2), заплануйте міграцію перед тим, як торкатися тонкого налаштування.
Завдання 3: Перелічити датасет Docker і негайних дітей
cr0x@server:~$ zfs list -r -o name,used,refer,avail,mountpoint tank/var/lib/docker | head
NAME USED REFER AVAIL MOUNTPOINT
tank/var/lib/docker 78.4G 1.20G 420G /var/lib/docker
tank/var/lib/docker/zfs 77.1G 77.1G 420G /var/lib/docker/zfs
Значення: Драйвер Docker часто створює дочірній датасет (зазвичай з ім’ям zfs), що містить датасети шарів.
Рішення: Якщо ви бачите тисячі дітей у цьому дереві, експлозія шарів вже відбувається; ви керуватимете цим за допомогою квот і ритму очищення.
Завдання 4: Порахуйте, скільки датасетів створив Docker
cr0x@server:~$ zfs list -r tank/var/lib/docker/zfs | wc -l
3427
Значення: Це кількість датасетів, а не кількість образів. Тисячі не завжди фатальні, але корелюють з повільними монтуваннями, повільними знищеннями і повільним завантаженням.
Рішення: Якщо це росте без меж, вам потрібна суворіша політика збереження образів, прибирання CI або окремий build-вузол, який можна скинути.
Завдання 5: Побачити, де простір: датасет vs снапшоти vs діти
cr0x@server:~$ zfs list -o name,used,usedbydataset,usedbysnapshots,usedbychildren -r tank/var/lib/docker | head
NAME USED USEDDS USEDSNAP USEDCHILD
tank/var/lib/docker 78.4G 1.20G 9.30G 67.9G
tank/var/lib/docker/zfs 77.1G 2.80G 8.90G 65.4G
Значення: Якщо usedbysnapshots великий — «видалені» дані утримуються снапшотами. Якщо домінує usedbychildren, простір займають датасети шарів.
Рішення: Велике використання снапшотів: зменшіть знімкування датасетів Docker і очистіть старі снапшоти. Велике використання дітей: очистіть образи/контейнери й розгляньте скидання датасету Docker, якщо це безпечно.
Завдання 6: Знайти найстаріші снапшоти, пов’язані з Docker (якщо є)
cr0x@server:~$ zfs list -t snapshot -o name,creation,used -s creation | grep '^tank/var/lib/docker' | head
tank/var/lib/docker@weekly-2024-11-01 Fri Nov 1 02:00 1.12G
tank/var/lib/docker@weekly-2024-11-08 Fri Nov 8 02:00 1.08G
Значення: Політики знімкування на рівні хоста іноді випадково включають датасети Docker. Це зазвичай контрпродуктивно з драйвером ZFS.
Рішення: Виключіть датасети Docker з загальних розкладів знімкування; натомість знімайте знімки персистентних датасетів додатків.
Завдання 7: Перевірити критичні властивості ZFS на датасеті Docker
cr0x@server:~$ zfs get -o name,property,value -s local,inherited compression,atime,xattr,recordsize,acltype tank/var/lib/docker
NAME PROPERTY VALUE
tank/var/lib/docker compression lz4
tank/var/lib/docker atime off
tank/var/lib/docker xattr sa
tank/var/lib/docker recordsize 16K
tank/var/lib/docker acltype posixacl
Значення: Ці властивості сильно впливають на продуктивність при дрібних файлах і накладні витрати на метадані.
Рішення: Якщо atime=on, вимкніть його для датасетів Docker. Якщо компресія вимкнена, увімкніть lz4, якщо у вас немає дуже конкретної причини не робити цього.
Завдання 8: Застосувати квоту, щоб обмежити радіус ураження Docker
cr0x@server:~$ sudo zfs set quota=250G tank/var/lib/docker
cr0x@server:~$ zfs get -o name,property,value quota tank/var/lib/docker
NAME PROPERTY VALUE
tank/var/lib/docker quota 250G
Значення: Docker більше не зможе спожити весь пул і звалити хост.
Рішення: Виберіть квоту, що підтримує очікуваний обіг образів плюс запас. Якщо ви регулярно досягаєте квоти, виправте політику збереження; не підвищуйте її одразу.
Завдання 9: Підтвердити стан пулу і чи ви обмежені ємністю
cr0x@server:~$ zpool status -x
all pools are healthy
cr0x@server:~$ zpool list
NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
tank 928G 721G 207G - - 41% 77% 1.00x ONLINE -
Значення: Пул здоровий, 77% заповнення, помірна фрагментація. По мірі наближення до 85–90% поведінка ZFS та продуктивність погіршуються.
Рішення: Якщо CAP понад ~85%, пріоритезуйте звільнення простору або додавання vdev перед тим, як ганяти мікрооптимізації.
Завдання 10: Визначити write amplification і латентність на око
cr0x@server:~$ iostat -x 1 3
Linux 6.8.0 (server) 12/25/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.1 0.0 6.2 9.8 0.0 71.9
Device r/s w/s rKB/s wKB/s avgrq-sz avgqu-sz await svctm %util
nvme0n1 210.0 980.0 9800.0 42000.0 72.1 8.90 7.40 0.52 62.0
Значення: Підвищений %iowait і await вказують, що затримки зберігання впливають на систему.
%util нижче 100% означає, що можливо ви обмежені чергуванням або синхронною поведінкою, а не сирим пропускним здатністю.
Рішення: Якщо await високий під час штормів збірки контейнерів, розгляньте виділення build-вузлів, налаштування синк-важких датасетів і перевірку ефективності SLOG (якщо він є).
Завдання 11: Перевірити розмір ARC і сигнали тиску пам’яті
cr0x@server:~$ cat /proc/spl/kstat/zfs/arcstats | egrep '^(size|c|c_min|c_max) '
size 4 8589934592
c 4 10737418240
c_min 4 1073741824
c_max 4 17179869184
Значення: ARC наразі ~8G, може зрости до ~16G. На хості контейнерів зростання ARC без обмеження може позбавити ресурсу робочі процеси.
Рішення: Якщо вузол викликає OOM контейнерам, поки ARC росте, обмежте ARC через параметри модуля і залиште пам’ять для застосунків.
Завдання 12: Знайти, які датасети мають найбільше снапшотів (показник churn)
cr0x@server:~$ zfs list -H -t snapshot -o name | awk -F@ '{print $1}' | sort | uniq -c | sort -nr | head
914 tank/var/lib/docker/zfs/graph/3f0c2b3d2a0e
842 tank/var/lib/docker/zfs/graph/9a1d11c7e6f4
Значення: Якщо датасети, пов’язані з Docker, накопичують снапшоти поза внутрішнім управлінням Docker, хтось робить знімки занадто агресивно.
Рішення: Аудитуйте ваші інструменти знімкування; виключіть дерева шарів Docker.
Завдання 13: Виявити простір, утримуваний видаленими але референсованими блоками (снапшоти/клони)
cr0x@server:~$ zfs get -o name,property,value used,referenced,logicalused,logicalreferenced tank/var/lib/docker
NAME PROPERTY VALUE
tank/var/lib/docker used 78.4G
tank/var/lib/docker referenced 1.20G
tank/var/lib/docker logicalused 144G
tank/var/lib/docker logicalreferenced 3.10G
Значення: Логічний простір вищий за фізично використаний: компресія працює і/або є спільні блоки. Головне: used включає дітей і снапшоти; referenced — це те, що цей датасет звільнить, якщо його знищити.
Рішення: Якщо used величезний, а referenced малий, знищення датасету може звільнити багато місця (бо воно забирає дітей і снапшоти). Це допустима стратегія скидання стану Docker — якщо персистентні дані знаходяться в іншому місці.
Завдання 14: Створити персистентний датасет додатку з жорсткою межею
cr0x@server:~$ sudo zfs create -o mountpoint=/containers tank/containers
cr0x@server:~$ sudo zfs create -o mountpoint=/containers/payments -o compression=lz4 -o atime=off tank/containers/payments
cr0x@server:~$ sudo zfs set refquota=200G tank/containers/payments
cr0x@server:~$ zfs get -o name,property,value mountpoint,refquota tank/containers/payments
NAME PROPERTY VALUE
tank/containers/payments mountpoint /containers/payments
tank/containers/payments refquota 200G
Значення: Персистентні дані мають власний mountpoint і суворий ліміт розміру.
Рішення: Bind-монтуйте /containers/payments у контейнери. Якщо додаток досягне refquota, він впаде контрольовано, а не поглине хост.
Завдання 15: Реплікувати персистентні датасети безпечно (send/receive)
cr0x@server:~$ sudo zfs snapshot tank/containers/payments@replica-001
cr0x@server:~$ sudo zfs send -c tank/containers/payments@replica-001 | sudo zfs receive -u backup/containers/payments
cr0x@server:~$ zfs get -o name,property,value readonly backup/containers/payments
NAME PROPERTY VALUE
backup/containers/payments readonly off
Значення: Ви передали консистентний снапшот. Датасет-приемник не стає readonly автоматично, якщо ви цього не встановите.
Рішення: Встановіть readonly=on на цільових бекапах, щоб уникнути випадкових записів.
cr0x@server:~$ sudo zfs set readonly=on backup/containers/payments
cr0x@server:~$ zfs get -o name,property,value readonly backup/containers/payments
NAME PROPERTY VALUE
backup/containers/payments readonly on
Завдання 16: Порівняти використання диска Docker з використанням ZFS (виявити невідповідність)
cr0x@server:~$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 44 12 38.7GB 22.3GB (57%)
Containers 61 9 4.1GB 3.5GB (85%)
Local Volumes 16 10 9.8GB 1.2GB (12%)
Build Cache 93 0 21.4GB 21.4GB
cr0x@server:~$ zfs list -o name,used,avail tank/var/lib/docker
NAME USED AVAIL
tank/var/lib/docker 78.4G 420G
Значення: Docker відображає логічні розміри, які він вважає своїми. ZFS показує фактичне використання, включно зі снапшотами і зв’язками клонів. Якщо ZFS used значно більший за уявлення Docker, значить є посилання на блоки поза обліком Docker (часто снапшоти).
Рішення: Шукайте снапшоти й клони, що утримують простір; розгляньте виключення датасетів Docker з політики знімкування і скинення стану Docker, якщо потрібно.
Швидкий плейбук діагностики
Коли хост повільний або заповнений, не починайте з випадкового prune. Почніть з вузького циклу, що покаже,
яка підсистема винна: ємність пулу, утримання простору снапшотами ZFS, кеш Docker або чиста I/O-латентність.
Перше: ємність і «простір, що утримується заручником»
- Ємність пулу:
zpool list. Якщо CAP > ~85%, очікуйте проблем. - Де простір:
zfs list -o used,usedbysnapshots,usedbychildren -r tank/var/lib/docker. - Снапшоти:
zfs list -t snapshot | grep docker. Якщо снапшоти існують на датасетах Docker — це підозріло.
Інтерпретація: Якщо домінують снапшоти — обріжте їх. Якщо діти домінують — очищайте образи/контейнери або розгляньте скидання датасету Docker.
Друге: тип вузького місця (латентність vs CPU vs тиск пам’яті)
- Дискова латентність:
iostat -x 1 3і слідкуйте заawait,%util,%iowait. - Зростання ARC: перевірте
/proc/spl/kstat/zfs/arcstatsі системну пам’ять. - CPU steal / конкуренція планувальника: якщо віртуалізовано, перевірте
%stealу виводіiostat.
Інтерпретація: Тиск ARC і I/O-латентність часто маскуються під «Docker повільний». Це не однакова операція виправлення.
Третє: джерело churn Docker
- Вибух кешу збірки:
docker system dfі стратегіяdocker builder prune. - Утримання образів: перелік старих образів і тегів; впровадьте TTL у CI і реєстрах.
- Тренд кількості датасетів: кількість датасетів у
tank/var/lib/docker/zfsтиждень до тижня.
Інтерпретація: Якщо кількість датасетів невблаганно росте, ваша робота зі збірок/завантажень фактично є фабрикою шарів. Обмежте її квотами й ізоляцією.
Три корпоративні міні-історії (як команди помиляються)
Інцидент: неправильне припущення («Docker prune звільнить простір»)
Середня компанія запускала CI на потужному ZFS-backed Docker-хості. Було нічне завдання: prune образів,
prune контейнерів, prune кешу збірки. Воно проходило успішно, з логами, що виглядали відповідально. Тим часом пул повільно повз
з 60% до 90% протягом місяця, а потім обрушився під час завантаженого релізного тижня.
On-call зробив звичне: запустив prune вручну, перезапустив Docker, навіть перезавантажив хост. Нічого не змінилося.
docker system df стверджував, що можна звільнити багато. ZFS казав інакше. zpool list показував 94% заповнення,
і I/O-латентність зросла, бо ZFS виділяв з найгірших залишкових сегментів.
Неправильне припущення було підступним: вони вважали, що уявлення Docker про «неактивне» відповідає здатності ZFS звільнити блоки.
Але хост також мав загальну політику знімкування на tank/var, яка включала /var/lib/docker.
Щоночі вони робили снапшоти датасету, повного клонів і хаосу. Це означало, що «видалені шари» все ще були референсовані снапшотами, тож простір застряг.
Виправлення не вимагало героїки. Вони виключили датасет Docker з політики знімкування, знищили старі снапшоти
і перемістили персистентні дані поза датасетом Docker, щоб мати можливість знищити стан Docker при потребі.
Після цього prune почали працювати знову, бо ZFS нарешті дозволили реально звільнити блоки.
Оптимізація, що відкотилася («Увімкнемо dedup для образів»)
Інша команда мала добрий намір: образи контейнерів мають багато ідентичних файлів. Чому б не увімкнути dedup на
датасеті Docker і заощадити місце? Пілотували на вузлі і святкували початкові цифри. Використаний простір впав. Оплески.
Потім вузол почав гальмувати під навантаженням. Збірки стали непередбачуваними. З’явилися стрибки латентності під час пікової активності,
не лише під час CI. Команда додала CPU. Додали швидші диски. Вони виконували ритуальний танець налагодження продуктивності, поки справжня проблема мовчки сиділа.
Dedup значно підвищує кількість звернень до метаданих і вимагає багато RAM для DDT (dedup table). Вузол тепер робив додаткові операції
на кожному шляху запису та читання, особливо з хаосом створення і видалення шарів. Гірше — збої продуктивності були періодичні,
залежали від хіт-рейту кешу і робочого набору DDT.
Відкат був болючим, бо відключення dedup не «роздедупує» вже існуючі блоки; воно лише припиняє dedup для нових записів.
Вони врешті мігрували стан Docker на свіжий датасет з dedup off і залишили компресію увімкненою.
Більшість економії простору вони отримали від lz4 і розумних політик збереження, без операційного податку.
Нудна, але правильна практика, що врятувала день (окремі датасети + квоти)
Команда платформи платежів запускала Docker на ZFS з розміткою, що здавалася занадто охайною: Docker жив у датасеті
з твердою квотою. Кожна станова служба мала свій датасет під /containers з refquota і простим розкладом снапшотів.
Ціль бекапу була receive-only. Нічого надзвичайного. Ніяких хитрих скриптів.
Одного дня в CI була помилка конфігурації: збірка в циклі підвантажувала базові образи й створювала нові теги кожен запускач.
На більшості систем це просто об’їло б диск, поки хост не впав. Тут датасет Docker досягнув квоти і Docker почав падати при pull.
Голосно. Вузол залишився живим. Бази даних продовжували працювати.
On-call отримав алерт про невдалі збірки, а не про мертвий production-хост. Вони виправили конфіг CI, потім очистили датасет Docker.
Ніякого відновлення даних. Ніяких термінових закупівель ємності. Квота не запобігла помилці — вона не допустила, щоб помилка стала інцидентом.
Це практика, яка рідко має гучні урочистості. Але вона має бути. Нудні межі зберігання — те, що перетворює «ой» у «тікет», а не «ой» у «інцидент».
Поширені помилки: симптом → корінь → виправлення
1) «Docker prune запустився, але простір ZFS не повернувся»
Симптом: Docker відображає можливість звільнення простору; ZFS used залишається високим.
Корінь: Снапшоти на датасетах Docker утримують референсовані блоки; або ланцюги клонів тримають блоки живими.
Виправлення: Припиніть знімкування датасетів шарів Docker; знищіть снапшоти; розгляньте скидання tank/var/lib/docker після переміщення персистентних даних.
2) «На хості тисячі монтувань і повільний завантаження»
Симптом: Довгі завантаження, юніти монтування systemd виконуються довго, Docker стартує пізно або падає.
Корінь: Docker ZFS driver створив велику кількість датасетів; обробка монтувань стала дорогою.
Виправлення: Обмежте датасет квотою; зменшіть обіг образів; періодично перебудовуйте датасет Docker на CI-вузлах; відокремте build-вузли від довготривалих prod-вузлів.
3) «Контейнери випадково повільні; CPU не завантажений»
Симптом: Стрибки латентності, таймаути, непередбачувана продуктивність збірок.
Корінь: Пул майже повний, фрагментація і уповільнення алокацій; або ARC віджирає пам’ять у додатків.
Виправлення: Тримайте пул під ~80–85%; обмежте ARC; додайте vdevs (не просто більші диски in-place, якщо хочете реальне покращення продуктивності).
4) «ZFS usedbysnapshots великий під /var/lib/docker»
Симптом: Простір снапшотів домінує у використанні.
Корінь: Загальна політика знімкування застосована до датасету Docker; Docker вже має власну внутрішню модель снапшот/клон.
Виправлення: Виключіть датасет Docker з розкладів знімкування хоста; знімайте снапшоти /containers/<app> натомість.
5) «Ми налаштували recordsize для Docker і база даних стала гірше»
Симптом: Збільшення латентності БД після «тонкого налаштування зберігання для контейнерів».
Корінь: Дані БД зберігаються всередині датасету Docker або в драйвер-управляємому шляху; recordsize обрано під хаос шарів, а не під патерни БД.
Виправлення: Перемістіть БД у власний датасет; налаштуйте recordsize і logbias там; зберігайте налаштування Docker для Docker.
6) «Реплікація каша і відновлення страшні»
Симптом: Бекапи включають шари Docker, кеші й стан, що робить send/receive великими і повільними.
Корінь: Персистентні дані змішані зі станом Docker під одним деревом датасетів.
Виправлення: Розділіть персистентні датасети під /containers; реплікуйте лише їх. Розглядайте датасет Docker як кеш/стан, а не як матеріал для бекапу.
7) «Ми увімкнули dedup і тепер все непередбачувано»
Симптом: Варіабельність продуктивності, тиск пам’яті, дивні стрибки латентності.
Корінь: Робочий набір таблиці dedup занадто великий; додаткове навантаження на метадані через хаос шарів.
Виправлення: Не використовуйте dedup для сховищ шарів Docker. Використовуйте компресію і політики збереження; якщо dedup вже ввімкнено — мігруйте на новий датасет.
Контрольні списки / покроковий план
План A: Новий хост (чисте розгортання)
- Створіть пул з адекватним ashift і дизайном vdev під ваші диски (mirror/RAIDZ залежно від моделі відмов).
- Створіть датасети:
tank/var/lib/dockerзмонтований у/var/lib/dockertank/containersзмонтований у/containers- Опційно датасети на додаток:
tank/containers/<app>
- Встановіть властивості для датасету Docker:
compression=lz4,atime=off,xattr=sa, і розгляньтеrecordsize=16Kабо32K. - Задайте квоту на
tank/var/lib/docker, підібрану під очікуваний обіг. - Для кожної станної служби створіть датасет під
/containersі встановітьrefquota. - Сконфігуруйте Docker на використання драйвера ZFS і правильного zpool/dataset (через конфіг daemon), потім запустіть Docker.
- Виключіть датасети Docker з будь-якої загальної автоматизації знімкування; знімайте лише персистентні датасети.
- Визначте реплікацію піддерева
tank/containersна receive-only backup pool.
План B: Існуючий хост (міграція без драми)
- Інвентар персистентного:
- Перелічіть compose-стеки і їхні томи.
- Визначте, які томи справді бази даних або станні сервіси.
- Створіть датасети
/containersна додаток і перемістіть дані туди (rsync або міграція на рівні застосунку). - Оновіть compose/k8s маніфести, щоб bind-монтувати шляхи хоста з
/containers/<app>. - Тільки після переміщення персистентних даних: застосуйте квоту до датасету Docker.
- Аудитуйте снапшоти: якщо у вас є снапшоти датасетів Docker, видаліть їх обережно після перевірки, що вони не потрібні для відкату.
- Встановіть властивості на датасеті Docker і перезапустіть Docker у контрольоване вікно.
- Якщо дерево датасетів вже патологічне (десятки тисяч датасетів), розгляньте перебудову стану Docker:
- Зупиніть Docker
- Знищіть і створіть заново
tank/var/lib/docker - Запустіть Docker і повторно підтягніть образи
План C: CI-вузли (ставте їх як «cattle»)
- Розміщуйте стан Docker CI на окремому датасеті з суворою квотою.
- Не знімайте снапшоти CI Docker датасетів.
- Заплануйте агресивне очищення кешу збірок.
- Періодично перебудовуйте CI-вузли замість спроби «тримати їх чистими назавжди».
- Тримайте артефакти в зовнішньому сховищі; Docker використовуйте як кеш.
FAQ
1) Використовувати драйвер ZFS Docker або overlay2 на ZFS?
Якщо у вас вже є ZFS і ви хочете ZFS-native снапшоти/клони для шарів, використовуйте драйвер ZFS. Якщо ви хочете більш mainstream шлях
і простіші day-2 операції, overlay2 поверх ZFS може бути прийнятним — але ви втрачаєте деякі ZFS-native семантики і можете зіткнутися з дивними взаємодіями.
У будь-якому випадку тримайте персистентні дані в окремих датасетах.
2) Чи можна знімкувати /var/lib/docker для бекапів?
Можна. Але не варто. Стан Docker відтворюваний; ваші бази даних — ні. Знімайте та реплікуйте /containers/<app>.
Розглядайте образи і шари Docker як кеш і матеріал для відтворення.
3) Чому важлива кількість датасетів?
Кожен датасет має метадані і може вимагати управління монтуванням. Тисячі можуть бути прийнятні; десятки тисяч стають операційним тертям:
повільні переліки, повільні знищення, повільні mount/unmount і більший радіус помилок.
4) Які властивості найважливіші для датасетів Docker?
compression=lz4, atime=off, xattr=sa — звичайні виграші. recordsize залежить від навантаження;
16K–32K часто поводяться краще для хаосу шарів, ніж 128K.
5) Чи ставити Docker томи під датасет Docker?
Для епемерних томів — ок. Для станних навантажень — не ставте. Використовуйте bind-монти хост-шляхів, які підкріплені датасетами під /containers.
Так ви робите бекапи і квоти менш страшними.
6) Чи корисний додатковий SLOG для Docker?
Лише якщо у вас синк-важкі навантаження на датасетах з sync=standard і ваші застосунки реально виконують sync-записи.
Багато контейнерних навантажень не є sync-bound. Тестуйте з метриками; не купуйте SLOG як марновірство.
7) Чому я бачу великий usedbysnapshots, хоча не робив снапшоти вручну?
Інструменти знімкування хоста часто таргетять цілі дерева (наприклад, tank/var). Або продукт бекапу знімає рекурсивно.
Docker також використовує ZFS снапшоти внутрішньо, але зазвичай вони керуються в дереві драйвера Docker.
Виправлення — чітко обмежити автоматизацію знімкування.
8) Чи можна «дефрагментувати» ZFS пул для покращення продуктивності?
Ні в класичному сенсі файлової системи. Практичне виправлення — дисципліна ємності (не тримайте пул «гарячим»),
хороша архітектура vdev і, іноді, перезапис даних шляхом міграції датасетів (send/receive) на свіжий пул.
9) Який найбезпечніший спосіб скинути стан Docker на ZFS-хості?
Зупиніть Docker, переконайтеся, що ніякі персистентні дані не знаходяться в /var/lib/docker, потім знищіть і створіть заново датасет Docker.
Ось чому ми розділяємо /containers — щоб цей крок був безпечним, коли він потрібен.
10) Як запобігти одному runaway-додатку заповнити пул?
Розміщуйте кожний станний додаток у власному датасеті і встановлюйте refquota. Розміщуйте Docker state під квотою.
Потім налаштуйте алерти по використанню квот до того, як воно вдарить у стіну.
Висновок: наступні кроки, які можна зробити сьогодні
Якщо ви запам’ятаєте одне: стан Docker не є святим, і ваша розмітка ZFS має це відображати. Дайте Docker власний датасет,
обмежте його квотою і перестаньте знімкувати його, ніби це сімейний альбом. Розміщайте персистентні дані в окремих датасетах під
/containers, з refquota і планом реплікації, який ви зможете пояснити стомленому колезі о 3 ранку.
Практичні наступні кроки:
- Запустіть
findmntі підтвердіть, що/var/lib/docker— виділений датасет. - Запустіть
zfs list -o usedbydataset,usedbysnapshots,usedbychildrenі з’ясуйте, що насправді утримує простір. - Виключіть датасети Docker з автоматизації знімкування.
- Створіть датасети
/containersдля станних сервісів і перемістіть туди дані. - Встановіть квоти/refquotas, щоб помилки закінчувалися малим і голосним фейлом.
Експлозія шарів не припиниться тому, що ви попросили. Вона припиниться, коли ви проведете межу і її забезпечите.