ZFS і Kubernetes: дизайн PV, який не підведе під час відмови вузлів

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

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

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

Що насправді відмовляє під час «відмови вузла»

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

Режим відмови A: вузол мертвий, сховище ціле

Подумайте: kernel panic, помер NIC, вийшов з ладу блок живлення або гіпервізор перезавантажив вузол. Диски в порядку, пул в порядку, але Kubernetes
не може розмістити поди на цьому вузлі — принаймні не одразу. Якщо ваш PV тільки локальний, под застрягає, поки вузол не повернеться. Це може бути
прийнятно для кешу. Це неприйнятно для платіжного журналу.

Режим відмови B: вузол живий, сховище хворе

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

Режим відмови C: вузол напів-мертвий (найдорожчий варіант)

Вузол «Ready» настільки, щоб вводити в оману логіку контрольної площини, але не може виконувати надійні IO. Тут з’являються тайм-аути, завислі монтування,
pod-и в стані Terminating і каскадні повторні спроби.

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

Факти та контекст, які варто знати

Це не дрібниці. Це ті маленькі реалії, які визначають, чому певні дизайни ZFS+Kubernetes працюють, а інші рано чи пізно з’їдять ваш вихідний вікенд.

  1. ZFS народився в світі, який ненавидів тишу корупції. Його end-to-end контрольні суми і самовідновлення були створені, щоб помічати bit rot, який RAID сам по собі не виявить.
  2. Copy-on-write — основна причина, чому знімки дешеві. ZFS не «заморожує» том; він зберігає вказівники на блоки, поки нові записі йдуть в інше місце.
  3. Раннє впровадження ZFS було пов’язане з Solaris та тісною інтеграцією. Та спадщина все ще впливає на припущення щодо стабільних імен пристроїв та стабільних стеків зберігання.
  4. ZVOLs і datasets — різні тварини. ZVOLs поводяться як блочні пристрої; datasets — як файлові системи. Вони мають різні налаштування та поведінку під час відмов.
  5. ZFS завжди трохи брешуть щодо «вільного місця». Фрагментація, metaslabs і поведінка reservation означають, що 80% заповнення може відчуватися як 95% з точки зору затримок.
  6. Ashift має більше значення, ніж здається. Невідповідний розмір сектора може назавжди вдарити по продуктивності; ви не «виправите» це пізніше без перебудови.
  7. Scrub-и не опціональні для довготривалих флотів. Це спосіб ZFS знайти латентні помилки до того, як вам знадобиться блок під час відновлення.
  8. Абстракції зберігання Kubernetes були спроєктовані для мереж перш за все. Локальні PV існують, але оркестраційна історія свідомо обмежена: Kubernetes не телепортує ваші байти.
  9. «Single writer» семантика — базове обмеження безпеки. Якщо два вузли можуть монтувати й записувати ту саму файлову систему без fencing — ви проєктуєте машину для корупції.

Три архетипи PV з ZFS (і коли їх використовувати)

1) Локальний ZFS PV (зв’язаний з вузлом): швидкий, простий, безжалісний

Це паттерн «ZFS на кожному вузлі, PV прив’язується до вузла». Ви створюєте dataset або zvol на вузлі, експортуєте його через CSI або LocalPV,
і планувальник ставить под, що споживає, на цей вузол.

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

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

2) Мережеве сховище на ZFS (iSCSI/NFS поверх ZFS): портативне, централізоване, інший домен відмов

Розмістіть ZFS на виділених storage-вузлах і експортуйте томи по мережі. Kubernetes бачить це як мережеве сховище, що узгоджується з його моделлю розміщення.
Домен відмов стає сервісом сховища, а не обчислювальним вузлом.

Використовуйте, коли: потрібно, щоб поди вільно переміщувалися, і ви готові інженерити HA сховища як дорослі.

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

3) Репліковане локальне сховище ZFS з промоцією (ZFS send/receive, DRBD-подібне або оркестратор): стійке, складне

Це паттерн «маєш торт і платиш за нього»: зберігайте дані локально для продуктивності, але реплікуйте на піди-пір, щоб можна було промотувати репліку під час відмови вузла.
Це можна реалізувати через механізми реплікації ZFS (знімки + send/receive), координовані контролером, або через storage-систему, що лежить на ZFS.

Використовуйте, коли: хочете локальну продуктивність з виживаністю при відмові вузла, і можете забезпечити single-writer семантику за допомогою fencing.

Уникайте, коли: не можете гарантувати fencing або вам потрібна справжня синхронна реплікація, але ви не приймаєте витрати на затримку.

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

Принципи проєктування, що запобігають неприємним сюрпризам

Принцип 1: Явно визначайте домен відмови

Для кожного StatefulSet запишіть: «Якщо вузол X зник, чи приймаємо простій? На який час? Чи можемо відновити? Чи потрібен автоматичний фейловер?»
Якщо ви не можете відповісти на ці питання, ваш StorageClass — просто молитва в YAML.

Принцип 2: Забезпечуйте single-writer через fencing, а не відчуття

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

  • жорсткого fencing (STONITH, відключення живлення, fencing на рівні гіпервізора), або
  • системи зберігання, що гарантує ексклюзивне підключення, або
  • додатка, який сам по собі безпечний для multi-writer (рідко; зазвичай потребує кластерної файлової системи або рівня реплікації в БД).

Жарт #1: Split-brain — це як дати двом стажерам root у продакшні — усі багато чому навчаться, а компанія — жалкуватиме.

Принцип 3: Обирайте dataset vs zvol залежно від того, як пише ваш додаток

Багато CSI ZFS рішень дають вибір: dataset (файлова система) або zvol (блок). Не вибирайте за естетикою.

  • Datasets добре працюють з POSIX, квотами й легко інспектуються. Чудові для загальних файлових навантажень.
  • ZVOLs поводяться як блочні томи і часто використовуються для баз даних через ext4/xfs зверху або як raw block. Вони потребують ретельного вибору volblocksize.

Принцип 4: Налаштовуйте властивості ZFS під навантаження, а не під весь кластер

«Одна конфігурація ZFS, що керує всім» — це шлях до сумних баз даних або сумних конвеєрів логів. Використовуйте властивості на рівні dataset:
recordsize, compression, atime, xattr, logbias, primarycache/secondarycache і reservation там, де потрібно.

Принцип 5: Ємність — це налаштування продуктивності

У ZFS «майже повний» — це не тільки ризик по ємності; це ризик по затримкам. Плануйте алерти навколо фрагментації пулу та розподілу алокацій, а не лише «df показує 10% вільно».

Принцип 6: Наблюваність має включати ZFS, а не тільки Kubernetes

Kubernetes скаже вам, що под Pending. Він не скаже, що один NVMe повторює команди і ваш пул дроселюється.
Створіть дашборди й алерти на zpool status, кількість помилок, результати scrub та метрики затримки.

CSI реалії: що Kubernetes зробить і не зробить за вас

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

Kubernetes зробить:

  • підключення/монтування томів згідно з драйвером,
  • поважатиме node affinity для локальних PV,
  • перезапустить поди в іншому місці, якщо PV портативний і планувальник може його розмістити.

Kubernetes не зробить:

  • не реплікує ваші локальні ZFS datasets,
  • не забезпечить fencing вузла, щоб запобігти подвійним записам,
  • не вилікує деградований пул магічно,
  • не зрозуміє концепцію «pool health» ZFS, якщо ви його не навчите (taints, node conditions або зовнішні контролери).

Надійний дизайн приймає ці межі й явно будує відсутні компоненти: оркестрацію реплікації, правила промоції та планування на основі здоров’я.

Одна операційна цитата, що добре постаріла: «Hope is not a strategy.» — Gene Kranz

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

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

Завдання 1: Підтвердити, до якого вузла фактично прив’язаний PVC (реальність локального PV)

cr0x@server:~$ kubectl get pvc -n prod app-db -o wide
NAME     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    AGE   VOLUMEMODE
app-db   Bound    pvc-7b3b3b9a-1a2b-4f31-9bd8-3c1f9d3b2d1a   200Gi      RWO            zfs-local      91d   Filesystem
cr0x@server:~$ kubectl get pv pvc-7b3b3b9a-1a2b-4f31-9bd8-3c1f9d3b2d1a -o jsonpath='{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}{"\n"}'
worker-07

Значення: Якщо є nodeAffinity, PV прив’язаний до вузла. Ваш под не перезапуститься в іншому місці без міграції тома.

Рішення: Якщо це база Tier-1, припиніть видавати її за HA. Або прийміть прив’язане простою, або переходьте на репліковане/портативне сховище.

Завдання 2: Дізнатися, чому под Pending (планувальник підкаже)

cr0x@server:~$ kubectl describe pod -n prod app-db-0
...
Events:
  Type     Reason            Age    From               Message
  ----     ------            ----   ----               -------
  Warning  FailedScheduling  2m30s  default-scheduler  0/12 nodes are available: 11 node(s) didn't match Pod's node affinity, 1 node(s) had taint {node.kubernetes.io/unreachable: }, that the pod didn't tolerate.

Значення: Це не «Kubernetes поводиться дивно». Він робить саме те, що ви йому сказали: ставити под там, де живе PV, але той вузол недоступний.

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

Завдання 3: Перевірити стан пулу ZFS на вузлі (починайте з фактів)

cr0x@server:~$ sudo zpool status -x
all pools are healthy

Значення: Немає відомих помилок і деградованих vdev. Це не означає «продуктивність в нормі», але означає «не очевидно палає».

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

Завдання 4: Знайти накопичення прихованих помилок (страшний варіант)

cr0x@server:~$ sudo zpool status
  pool: tank
 state: DEGRADED
status: One or more devices has experienced an error resulting in data corruption.
action: Restore the file in question if possible. Otherwise restore the entire pool from backup.
  scan: scrub repaired 0B in 05:12:44 with 2 errors on Sun Dec 22 03:10:24 2025
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        DEGRADED     0     0     0
          mirror-0                  DEGRADED     0     0     0
            nvme-SAMSUNG_MZVLB1T0   ONLINE       0     0     2
            nvme-SAMSUNG_MZVLB1T0   ONLINE       0     0     0

errors: Permanent errors have been detected in the following files:
        tank/k8s/pv/pvc-...:<0x123>

Значення: Помилки контрольних сум і постійні помилки — не «подивимось пізніше». Це вказує на корупцію, яку ZFS не зміг відновити.

Рішення: Трактуйте як інцидент з втратою даних: ідентифікуйте уражений PV, відновіть з бекапу або репліки, заплануйте заміну диска й перевірку scrub.

Завдання 5: Перевірити ashift та вирівнювання фізичних секторів (рішення назавжди)

cr0x@server:~$ sudo zdb -C tank | grep -E 'ashift|vdev_tree' -n | head
72:        ashift: 12

Значення: ashift=12 означає 4K сектори. Якщо ваші диски 4K, а ashift = 9 — ви платите постійну ціну за записи.

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

Завдання 6: Перевірити вільне місце пулу і фрагментацію (ємність ≠ використана продуктивність)

cr0x@server:~$ zpool list -o name,size,alloc,free,frag,health
NAME  SIZE   ALLOC  FREE  FRAG  HEALTH
tank  3.62T  3.02T  614G  61%   ONLINE

Значення: 61% фрагментації при високій алокації — це множник затримки. Записи стають дорогими, і синхронні записи погіршуються.

Рішення: Якщо фраг збільшується і затримки важливі — розширюйте пул, зменшуйте churn або мігруйте «гарячі» навантаження. Також перегляньте recordsize/volblocksize і політику утримання знімків.

Завдання 7: Переглянути властивості dataset для PV (виловити випадкове «один розмір для всіх»)

cr0x@server:~$ sudo zfs get -o name,property,value -s local,default recordsize,compression,atime,logbias,primarycache tank/k8s/pv/pvc-7b3b3b9a
NAME                      PROPERTY      VALUE
tank/k8s/pv/pvc-7b3b3b9a   recordsize    128K
tank/k8s/pv/pvc-7b3b3b9a   compression   lz4
tank/k8s/pv/pvc-7b3b3b9a   atime         on
tank/k8s/pv/pvc-7b3b3b9a   logbias       latency
tank/k8s/pv/pvc-7b3b3b9a   primarycache  all

Значення: atime=on часто — марний підсилювач записів для БД. recordsize=128K може бути неправильним для дрібного випадкового IO.

Рішення: Для dataset-ів баз даних розгляньте atime=off і recordsize, налаштований під типовий розмір блоків (часто 16K для Postgres). Перевіряйте за допомогою бенчмарків, а не фольклору.

Завдання 8: Для ZVOL-backed PV перевірте volblocksize (прихований важіль продуктивності)

cr0x@server:~$ sudo zfs get -o name,property,value volblocksize tank/k8s/zvol/pvc-1f2e3d4c
NAME                         PROPERTY     VALUE
tank/k8s/zvol/pvc-1f2e3d4c    volblocksize 8K

Значення: volblocksize фіксований при створенні. Якщо ваша БД використовує 16K сторінки, а ви вибрали 8K — може збільшитись IO у два рази.

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

Завдання 9: Підтвердити, чи має dataset reservation (щоб запобігти каскаду «пул заповнений»)

cr0x@server:~$ sudo zfs get -o name,property,value refreservation,reservation tank/k8s/pv/pvc-7b3b3b9a
NAME                      PROPERTY        VALUE
tank/k8s/pv/pvc-7b3b3b9a   reservation     none
tank/k8s/pv/pvc-7b3b3b9a   refreservation  none

Значення: Резервованого місця немає. Шумний сусід може заповнити пул, і ваш «важливий» PV втратить можливість записувати.

Рішення: Для критичних PV на спільних пулах виділіть reservation або виділіть окремі пули. Краще інженерне розділення, ніж суперечки під час інциденту.

Завдання 10: Перевірити графік scrub і останній результат (повільні катастрофи починаються звідси)

cr0x@server:~$ sudo zpool status tank | sed -n '1,12p'
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 04:01:55 with 0 errors on Sun Dec 15 03:00:11 2025
config:
...

Значення: Scrub запустився і не знайшов помилок. Чудово. Якщо ваш scrub не запускався місяцями — ви відкладаєте погані новини.

Рішення: Встановіть частоту scrub відповідно до розміру дисків і churn. Алертуйте, якщо scrub припинився або почав знаходити помилки.

Завдання 11: Перевірити свіжість реплікації (RPO — це метрика, а не обіцянка)

cr0x@server:~$ sudo zfs list -t snapshot -o name,creation -s creation | tail -5
tank/k8s/pv/pvc-7b3b3b9a@replica-20251225-0010  Thu Dec 25 00:10 2025
tank/k8s/pv/pvc-7b3b3b9a@replica-20251225-0020  Thu Dec 25 00:20 2025
tank/k8s/pv/pvc-7b3b3b9a@replica-20251225-0030  Thu Dec 25 00:30 2025
tank/k8s/pv/pvc-7b3b3b9a@replica-20251225-0040  Thu Dec 25 00:40 2025
tank/k8s/pv/pvc-7b3b3b9a@replica-20251225-0050  Thu Dec 25 00:50 2025

Значення: Знімки існують на джерелі. Це ще не реплікація. Потрібно підтвердити, що вони дісталися до цілі і що вони свіжі.

Рішення: Якщо знімки відстають більше за ваш RPO — обмежте інший трафік, налагодьте send/recv помилки і припиніть продавати «HA» внутрішньо.

Завдання 12: Прогнати суху репетицію zfs send/receive (знати, що сталося б)

cr0x@server:~$ sudo zfs send -nPv tank/k8s/pv/pvc-7b3b3b9a@replica-20251225-0050 | head
send from @ to tank/k8s/pv/pvc-7b3b3b9a@replica-20251225-0050 estimated size is 3.14G
total estimated size is 3.14G
TIME        SENT   SNAPSHOT

Значення: Оцінений розмір send розумний. Якщо він величезний для малої зміни — можлива невідповідність recordsize або надмірний churn.

Рішення: Якщо інкрементальні send-и несподівано великі — перегляньте частоту знімків, recordsize/volblocksize, і чи додаток перезаписує великі файли постійно.

Завдання 13: Виявити зависання mount/IO на рівні вузла (коли поди «зависають»)

cr0x@server:~$ dmesg -T | tail -20
[Thu Dec 25 01:12:11 2025] INFO: task kworker/u32:4:12345 blocked for more than 120 seconds.
[Thu Dec 25 01:12:11 2025] zio pool=tank vdev=/dev/nvme0n1 error=5 type=1 offset=123456 size=131072 flags=1809
[Thu Dec 25 01:12:12 2025] blk_update_request: I/O error, dev nvme0n1, sector 987654

Значення: Ядро повідомляє про заблоковані задачі та помилки IO. Це не проблема Kubernetes; це інцидент на вузлі зі сховищем.

Рішення: Cordon/дренуйте вузол, перемістіть нестейтфул навантаження та почніть заміну/ремонт. Не «просто перезапускайте pod» на зламаному IO-шляху.

Завдання 14: Подивитися, чи Kubernetes застряг із відключенням тому (симптом контрольної площини, причина — сховище)

cr0x@server:~$ kubectl get volumeattachment
NAME                                                                   ATTACHER                 PV                                         NODE        ATTACHED   AGE
csi-9a2d7c5f-1d20-4c6a-a0a8-1c0f67c9a111                                 zfs.csi.example.com    pvc-7b3b3b9a-1a2b-4f31-9bd8-3c1f9d3b2d1a  worker-07    true       3d

Значення: Kubernetes вважає, що том підключений до worker-07. Якщо worker-07 мертвий, це підвішене підключення може блокувати фейловер.

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

Завдання 15: Перевірити тиск ARC (промахи кешу виглядають як «сховище повільне»)

cr0x@server:~$ sudo arcstat 1 5
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  arcsz     c
01:20:11   812   401     49   122   15   249   31    30    3   28.1G  31.9G
01:20:12   790   388     49   110   14   256   32    22    2   28.1G  31.9G
01:20:13   840   430     51   141   17   260   31    29    3   28.1G  31.9G
01:20:14   799   420     53   150   18   244   30    26    3   28.1G  31.9G
01:20:15   820   415     51   135   16   255   31    25    3   28.1G  31.9G

Значення: Високий miss% означає, що читання падають на диск. Якщо диски в порядку, але затримки стрибнули — тиск ARC може бути винуватцем.

Рішення: Якщо ARC замалий або пам’ять обмежена подами, розгляньте розмір вузла, політики cgroup пам’яті і налаштування primarycache для dataset-ів.

Завдання 16: Підтвердити фактичне вільне місце файлової системи vs вільне місце пулу (квотні пастки)

cr0x@server:~$ sudo zfs get -o name,property,value used,avail,quota,refquota tank/k8s/pv/pvc-7b3b3b9a
NAME                      PROPERTY  VALUE
tank/k8s/pv/pvc-7b3b3b9a   used      187G
tank/k8s/pv/pvc-7b3b3b9a   avail     13.0G
tank/k8s/pv/pvc-7b3b3b9a   quota     200G
tank/k8s/pv/pvc-7b3b3b9a   refquota  none

Значення: Dataset досягнув своєї квоти; додаток отримає ENOSPC, навіть якщо пул ще має вільне місце.

Рішення: Збільшіть квоту (через change control), очистіть дані або масштабуйтесь горизонтально. Не просто «додавайте диски», якщо квота — обмежувач.

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

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

Перше: визначте, чи це проблема планування/розміщення або IO

  1. Под Pending чи Running?

    • Якщо Pending: перевірте kubectl describe pod події щодо node affinity/taints.
    • Якщо Running але завис: підозрюйте mount/IO шлях.
  2. Чи PV прив’язаний до вузла? Перевірте PV nodeAffinity. Якщо так — відмова вузла = недоступність тому, якщо немає реплікації/промоції.

Друге: перевірте статус підключення та монтування (контрольна площина vs реальність вузла)

  1. Перевірте kubectl get volumeattachment на предмет застряглих підключень.
  2. На вузлі перевірте зависання монтувань і помилки IO через dmesg -T.
  3. Якщо у вашого CSI драйвера є логи — дивіться тайм-аути, помилки доступу або цикли «already mounted».

Третє: перевірте стан пулу і dataset (не лише «він змонтований?»)

  1. zpool status -x і повний zpool status на предмет помилок.
  2. zpool list для фрагментації і вільного місця.
  3. zfs get на ураженому dataset/zvol для властивостей, що відповідають очікуванням навантаження.

Четверте: вирішіть, відновлювати через перезапуск, фейловер чи відновлення

  • Перезапуск — лише якщо вузол і пул здорові, а проблема на рівні софту (app, kubelet, CSI тимчасово).
  • Фейловер — лише якщо ви можете гарантувати single-writer і маєте відому добру репліку.
  • Відновлення — якщо є помилки контрольних сум, постійні помилки або скомпрометований пул.

Жарт #2: Kubernetes перезапустить ваш под за секунди; ваші дані перемістяться самі приблизно ніколи.

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

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

Середній SaaS збудував «високопродуктивну» Kubernetes платформу на локальному ZFS. Кожен вузол мав NVMe-дзеркала.
Їх StatefulSet-и використовували CSI драйвер, що провізував datasets локально, і у всіх були приємні бенчмарки.

Неправильне припущення було тонким: «Якщо вузол помре, Kubernetes перемістить pod в інше місце». Правда для безстанних.
Неправда для локальних PV. Вони прочитали слова «PersistentVolume» і витлумачили їх як «persistent across nodes».

Коли вийшов з ладу комутатор у стійці, частина вузлів стала недоступною. Pod-и баз даних стали Pending, бо їх PV були node-affined.
On-call людина намагалася «force delete» pod-и, потім «recreate PVC», потім «просто scale to zero і назад». Нічого з цього не перемістило дані.

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

Виправлення було нудним: вони перекваліфікували, які навантаження дозволені на node-bound PV, перемістили Tier-1 стан до портативного сховища,
і написали рукопис, що починається з «Чи PV node-affined?». Це одне питання скоротило години при наступних інцидентах.

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

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

На папері зміни виглядали добре. На практиці менший recordsize підвищив навантаження на метадані й фрагментацію на зайнятих dataset-ах.
Частіші знімки збільшили churn і розміри send-ів для навантажень, що перезаписували великі файли. Реплікація не стала швидшою; вона стала голосніша.

Гірше — зміни запустили глобально, включно з томами, які цього не потребували: агрегації логів, кеші збірки та БД з іншою IO-поведінкою.
Хвостові латенції підповзли перші. Потім черги. Потім тікети в сапорт.

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

Вони відкотили зміни до профілів за StorageClass і визначили профілі: «db-postgres», «db-mysql», «logs», «cache», кожен мапиться на шаблон dataset.
Команда платформи перестала бути культом тюнінгу і стала сервісом.

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

Фінансова команда запускала ZFS на виділених storage-вузлах, експортувала томи в Kubernetes. Нічого фантастичного. «Інновація» була в дисципліні:
щотижневі scrub-и, алерти на будь-які помилки контрольних сум і політика, що деградований пул тригерить інцидент, навіть якщо додатки виглядають нормально.

Якось scrub-и почали повідомляти невелику кількість коректованих помилок на дзеркалі. Додатки були здорові. Хоча спокуса була відкласти заміну до наступного кварталу — вони цього не зробили.
Вони замінили підозрілий пристрій і прогнали scrub знову до чистого стану.

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

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

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

Цей розділ навмисно конкретний. Якщо ви впізнаєте симптом — діяти потрібно без винаходу інженерії сховища під стресом.

1) Pod застряг у Pending після відмови вузла

Симптом: Pod StatefulSet не планується; події згадують node affinity або «volume node affinity conflict».
Корінна причина: Локальний PV прив’язаний до вузла; вузол недоступний або має taint.
Виправлення: Якщо простій прийнятний — відновіть вузол. Якщо ні — переробіть архітектуру: портативне сховище або репліковане локальне сховище з промоцією і fencing.

2) Pod Running але додаток таймаути на диску

Симптом: Контейнер запущений; логи додатка показують IO timeouts; kubectl exec іноді зависає.
Корінна причина: Помилки пристрою або заблокований IO; ZFS може повторювати; вузол «напів-мертвий».
Виправлення: Перевірте dmesg, zpool status. Cordon вузол. Замініть збійні пристрої. Не перезапускайте поди на тому самому зламаному шляху IO.

3) «No space left on device», але пул має вільне місце

Симптом: Додаток отримує ENOSPC; zpool list показує вільне місце.
Корінна причина: Достигла квота dataset або refquota, або snapshot-резервування займає місце.
Виправлення: Інспектуйте zfs get quota,refquota,used,avail. Збільшіть квоту, очистіть дані або змініть політику знімків.

4) Реплікація є, але фейловер втрачає «свіжі записи»

Симптом: Після промоції дані застарілі; останні кілька хвилин відсутні.
Корінна причина: Асинхронна реплікація і невиконання RPO (відставання знімків, backlog send).
Виправлення: Заміряйте лаг реплікації; відрегулюйте частоту знімків і конкуруючі send-и; забезпечте пропускну здатність; розгляньте синхронну реплікацію, якщо бізнес вимагає (і прийміть затримки).

5) Несподівано великі інкрементальні send-и

Симптом: Інкрементальна реплікація роздувається навіть при малих змінах.
Корінна причина: Додаток перезаписує великі файли; поганий recordsize матч; фрагментація; занадто багато дрібних знімків, що створюють churn.
Виправлення: Налаштуйте recordsize/volblocksize під навантаження; зменшіть churn; відрегулюйте частоту знімків; розгляньте реплікацію на рівні додатку для навантажень з частими перезаписами.

6) Помилки монтування: «already mounted» або pod-и в стані Terminating

Симптом: Логи CSI показують конфлікти монтування; pod-и зависають у Terminating; підключення лишається true.
Корінна причина: Вузол помер без чистого unmount; стале підключення; драйвер не обробляє коректно відновлення після аварії.
Виправлення: Використовуйте процедурy force detach, які підтримує драйвер; забезпечте fencing; налаштуйте kubelet таймаути; перевірте поведінку після аварій в staging.

7) Пул ONLINE, але затримки жахливі

Симптом: Немає помилок; додатки повільні; p99 латенція стрибає; scrubs в порядку.
Корінна причина: Пул майже повний, висока фрагментація, ARC промахи, підсилення синхронних записів або дрібні випадкові записи по HDD vdev-ам.
Виправлення: Перевірте фрагментацію та алокацію; налаштуйте властивості dataset; додайте ємність; перемістіть синхронно важкі навантаження на vdev-и з правильним SLOG (тільки якщо ви розумієте його).

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

Крок за кроком: оберіть правильну стратегію PV для кожного навантаження

  1. Класифікуйте навантаження.

    • Tier-0: втрата даних неприпустима; простій — хвилини, не години.
    • Tier-1: простій прийнятний, але має бути передбачуваним; відновлення протестоване.
    • Tier-2: кеші та похідні дані, які можна перебудувати.
  2. Оберіть домен відмови, з яким можна жити.

    • Якщо Tier-0: не використовуйте node-bound local PV без реплікації + fencing.
    • Якщо Tier-1: локальний PV може бути прийнятним, якщо відновлення вузла швидке і відпрацьоване.
    • Якщо Tier-2: локальний PV зазвичай підходить; оптимізуйте для простоти.
  3. Оберіть dataset vs zvol.

    • Dataset для загальних файлів, логів і додатків, які виграють від простої інспекції.
    • Zvol для блочної семантики або коли ваш CSI стек очікує block; встановіть volblocksize свідомо.
  4. Визначте профілі зберігання як код.

    • Для кожного профілю: recordsize/volblocksize, compression, atime, logbias, квоти/reservation.
    • Змапіруйте профілі на StorageClasses, а не на плутані знання в головах людей.
  5. Плануйте і тестуйте відмову вузла.

    • Вбивайте вузол в staging поки БД пише.
    • Заміряйте час до відновлення сервісу.
    • Перевірте, що неможливі умови split-brain.

Операційний чекліст: як виглядає «готово для продакшну»

  • Scrub-и заплановані і є алерти, якщо їх пропускають або вони знаходять помилки.
  • Стан пулу моніториться (degraded, checksum errors, події видалення пристроїв).
  • Пороги ємності базуються на алокації пулу та фрагментації, а не лише на вільних ГБ.
  • Репліки перевірені (якщо використовується реплікація): час останнього receive, лаг та процедура промоції).
  • Fencing задокументовано і протестовано (якщо промотуються репліки): хто/що запобігає dual-writer).
  • Рунбуки існують для: втрати вузла, відмови диска, застряглого підключення, відновлення із знімку та відкотів.
  • Бекапи протестовані як відновлення, а не як приємне відчуття безпеки.

FAQ

1) Чи можу я отримати «реальний HA» з локальними ZFS PV?

Не за замовчуванням. Локальні PV прив’язані до вузла. Для HA потрібна реплікація на інший вузол і безпечний механізм промоції з fencing,
або мережеве/портативне сховище.

2) Чи достатньо ZFS реплікації (send/receive) для фейловера?

Це необхідно, але не достатньо. Потрібна ще оркестрація (коли робити знімок, send, receive, промоцію) і строгий single-writer fencing,
щоб не зіпсувати дані під час часткових відмов.

3) Використовувати datasets чи zvols для Kubernetes PV?

Datasets простіше інспектувати й налаштовувати для файлових навантажень. Zvols кращі, коли потрібні блочні семантики або коли CSI драйвер очікує block.
Для баз даних обидва варіанти можуть підходити — просто налаштуйте recordsize/volblocksize свідомо.

4) Який найбільший «підводний камінь» під час відмов вузлів?

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

5) Чи Kubernetes обробляє fencing за мене?

Ні. Kubernetes може видалити pod і перепланувати його, але не може гарантувати, що мертвий вузол вже не пише в сховище, якщо сама система зберігання
не забезпечує ексклюзивність або ви не реалізуєте fencing зовні.

6) Якщо я запускаю ZFS на виділених storage-вузлах і експортую NFS — це «погано»?

Не обов’язково. Це компроміс: простіша портативність для подів, але потрібно інженерити HA сховища і надійність мережі. Це може бути дуже розумно,
особливо для змішаних навантажень, якщо ви розглядаєте storage-вузли як production-системи першого класу.

7) Які властивості ZFS найважливіші для PV?

Для dataset-ів: recordsize, compression=lz4 (зазвичай), atime=off для багатьох write-heavy навантажень, primarycache, logbias для sync-heavy додатків.
Для zvol-ів: volblocksize і compression. Також не ігноруйте quotas/reservations.

8) Як запобігти шумному сусіду, що заповнить пул і виведе з ладу критичні PV?

Використовуйте квоти dataset для справедливості і reservation/refreservation для критичних томів на спільних пулах. Краще: розділяйте критичні навантаження
на окремі пули або вузли, коли ставки високі.

9) Чи завжди добре додавати SLOG для баз даних?

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

10) Який найчистіший спосіб обробляти «вузол зникає» для stateful додатків?

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

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

ZFS охоче захистить ваші дані від космічних променів і недбалих дисків. Kubernetes охоче видалить ваші pod-и і перезапустить їх в іншому місці.
Пастка — припускати, що ці два «охоче» будуть узгоджені під час відмови вузла. Вони не будуть, якщо ви цього не спроєктуєте.

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

  1. Перелічіть StatefulSet-и і помітьте їх за толерантністю до відмов (Tier-0/1/2). Зробіть це явним.
  2. Знайдіть PV, прив’язані до вузлів і рішення, чи прийнятний такий простій. Якщо ні — переробіть зараз, а не під час інциденту.
  3. Стандартизуйте профілі зберігання (властивості dataset/zvol) під класи навантажень. Припиніть глобальні експерименти з тюнінгом у продакшні.
  4. Додайте алерти на здоров’я ZFS і scrub-и поряд з метриками Kubernetes. Сховище може бути зламане, поки pod-и виглядають «добре».
  5. Проведіть game day відмови вузла для одного stateful навантаження. Заміряйте час. Задокументуйте. Виправіть частини, що вимагають героїзму.

Якщо ви зробите лише одну річ: зробіть домен відмови первинним рішенням при проєктуванні. Локальний ZFS швидкий. Але швидке сховище,
яке не може фейловеритись — це лише дуже ефективний спосіб бути недоступним.

← Попередня
Заголовки про 0-day: чому одна вразливість викликає миттєву паніку
Наступна →
Debian 13 «Занадто багато відкритих файлів»: правильне виправлення через systemd (не лише ulimit)

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