ZFS fio для баз даних: тестування синхронних записів без самообману

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

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

Коли ви запускаєте ZFS під базою даних, саме синхронні записи показують правду. Саме там більшість тестів fio випадково шахраює: неправильні прапори, хибні припущення, інший шлях I/O, інші гарантії стійкості. Це польовий посібник з запуску fio проти ZFS так, щоби вам дійсно було важливо, чи переживуть дані вимкнення живлення.

Що насправді означають «синхронні записи» в ZFS (і чому це важливо для баз даних)

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

Ця обіцянка реалізується невеликим набором поведінок: fsync(), fdatasync(), O_DSYNC, O_SYNC і іноді семантика FUA у шарі зберігання. Більшість гарантій стійкості баз даних зводиться до одного жорсткого вимогу: запис журналу має опинитися на стабільному сховищі до того, як «OK» повернеться клієнту.

ZFS ускладнює це в хорошому сенсі (сильна модель узгодженості) і в заплутаному (copy-on-write + групи транзакцій + intent log). ZFS може приймати записи в пам’ять і виконувати їх пізніше в TXG (transaction group). Це нормальний буферизований I/O. Але коли додаток просить синхронні семантики, ZFS мусить переконатися, що запис є стійким до повернення.

Тут з’являється ZIL (ZFS Intent Log). ZIL — це не «кеш записів» у звичному розумінні; це механізм, щоб швидко задовольнити синхронні запити, не змушуючи негайного повного коміту пулу. Якщо система зламається, записи ZIL відтворюються під час імпорту, щоб привести файлову систему до узгодженого стану, який включає ці синхронні операції.

ZIL зазвичай живе в самому пулі. SLOG (окремий лог-пристрій) — це опціональний виділений пристрій для збереження записів ZIL. Правильно зроблений SLOG перетворює «затримку синхронного запису» на «затримку пристрою SLOG». Неправильно зроблений — перетворює «надійну базу даних» на «сподіваюся, UPS сьогодні в порядку».

Ваше завдання при тестуванні — виміряти:
затримки та IOPS під синхронними семантиками, що відповідають базі даних,
і робити це, переконавшись, що ви випадково не тестуєте ОЗП, page cache або налаштування стійкості, яких у проді немає.

Факти та контекст: як ми дійшли до цього

  • Факт 1: ZFS виникла в Sun Microsystems у середині 2000-х з акцентом на цілісність даних від краю до краю (контрольні суми скрізь), а не на «максимальні показники fio».
  • Факт 2: Традиційні файлові системи часто покладалися на write-back кеш і журнал; ZFS використовує copy-on-write плюс транзакційні коміти (TXG), що змінює поведінку «затримки запису».
  • Факт 3: ZIL існує спеціально для того, щоб зробити синхронні операції швидкими без примусу повного TXG sync на кожен fsync-подібний запит.
  • Факт 4: Ранні підприємницькі системи зберігання часто постачалися з батарейно-захищеним write cache; сьогодні NVMe-диски з захистом від втрати живлення (PLP) є найближчим споживчим аналогом для безпечного низьколатентного логування.
  • Факт 5: Історично бази даних оптимізувалися під обертальні диски, де послідовне логування було вирішальним; на SSD/NVMe варіація затримок (tail latency) стає реальним вбивцею.
  • Факт 6: АPI асинхронного I/O в Linux розвивалися окремо (libaio, io_uring), і fio може пройти багато шляхів коду — деякі з них не відповідають тому, як ваша база записує WAL/redo.
  • Факт 7: «Write cache enabled» на диску — з часів SATA — це підступна річ; воно може покращити бенчмарки й одночасно тихо знищувати гарантії стійкості при втраті живлення.
  • Факт 8: Властивості датасету ZFS як sync, recordsize, logbias і primarycache можуть суттєво змінити продуктивність без змін у коді застосунку — велика сила, і велика можливість прострілу в ногу.

Ментальна модель ZFS для людей з баз даних: TXG, ZIL, SLOG і самообмани

TXG: метроном для вашого навантаження записів

ZFS групує зміни в транзакційні групи. Кожні кілька секунд (зазвичай близько 5с, налаштовувано) TXG синхронізується на диск. Якщо ваше навантаження переважно асинхронні буферизовані записи, пропускна здатність сильно залежить від поведінки TXG: брудні дані накопичуються, а потім скидаються. Це може давати «зубчасту» картину затримок — вибухи при flush-ах, потім затишшя.

Для синхронних записів ZFS не може просто чекати наступного TXG sync. Воно має підтвердити лише після того, як запис стає стійким. Саме тому існує ZIL.

ZIL: не другий журнал, не журнал бази даних і не магія

ZIL реєструє наміри синхронних операцій. Він пишеться послідовно-ish, у невеликих записах. Коли TXG нарешті комітить реальні блоки даних, відповідні записи ZIL стають непотрібними і видаляються.

Важливий висновок: у сталому стані ZIL про затримку, а не про ємність. Пристрій SLOG не має бути великим.
Йому потрібно бути низьколатентним, надійним і стабільним під навантаженням.

SLOG: пристрій «щоб синхронні записи не були жахливими»

З SLOG ZFS пише записи ZIL на цей пристрій замість основного пулу. Якщо SLOG швидкий і має захист від втрати живлення, затримка синхронних записів може впасти радикально.

Якщо SLOG швидкий, але не захищений від втрати живлення, ви збудували апарат для корупції бази даних. Так, можна пощастити. Удача — не проєкт.

Де люди обманюють себе

Більшість «ZFS бенчмарків для баз даних» йдуть не так в одному з трьох випадків:

  1. Вони фактично не виконують синхронного I/O. fio пише буферизовані дані й віддає чудові показники. База даних так не поводиться.
  2. Вони виконують синхронний I/O, але ZFS не дотримує його так, як у проді. Властивість датасету sync=disabled або віртуалізаційні шари змінюють семантику.
  3. Вимірюють пропускну здатність і ігнорують розподіл затримок. Бази даних «вмирають» через p99 і p999, а не через середні MB/s.

«Все швидко в середньому» — це те, як вас викличуть о 02:13. Базу даних не цікавить середнє. Її турбують викиди.

Одна цитата, яку варто тримати на видноті:
«Надія — не стратегія.»
— General Gordon R. Sullivan

Жарт №1: Якщо ваш бенчмарк закінчується за 30 секунд, а ваші висновки живуть три роки, ви займаєтесь інженерією продуктивності як гороскоп.

Правила fio: як бенчмарки випадково шахраюють

Правило 1: «sync» має означати одне й те саме для fio, ОС і ZFS

fio може імітувати синхронну поведінку кількома способами:
fsync=1 (виклик fsync після кожного запису),
fdatasync=1,
sync=1 (використовувати O_SYNC),
dsync=1 (використовувати O_DSYNC),
direct=1 (O_DIRECT, обхід page cache),
і движок I/O може ще змінити семантику (psync, libaio, io_uring).

Бази даних зазвичай роблять буферизовані записи в файл WAL/redo і викликають fsync на commit (або груповий commit). Деякі використовують O_DSYNC / O_DIRECT залежно від конфігурації. Отже ваш тест fio слід підбирати відповідно до конфігурації бази, а не до того, що дає найгарніший графік.

Правило 2: буферизований I/O + малі файли = тест page cache

Якщо ви запускаєте fio проти малого файлу без direct=1, ви можете вимірювати швидкість Linux page cache. Це все ще корисно, але це не те, про що ви думаєте.

Для тестування синхронних записів найгірша брехня —
ви думаєте, що вимірюєте затримку стійкості, а насправді вимірюєте швидкість ОЗП плюс випадковий flush.

Правило 3: «fsync після кожного запису» не завжди відповідає тому, що робить ваша база

Параметр fsync=1 у fio змушує викликати fsync після кожного виклику write. Це моделює базу даних без групового commit. Багато баз роблять group commit: кілька транзакцій ділять один fsync. Якщо у проді у вас груповий commit, «fsync на кожні 4k запис» сильно недооцінить пропускну здатність і завищить затримки.

Виправлення — не шахрайство. Виправлення — навмисно змоделювати груповий commit (кілька потоків запису, частота fsync або суміш синхронних та асинхронних операцій).

Правило 4: продукт — це процентилі затримок

Коли синхронні записи повільні, ваша база чекає. Це видно як сплески затримок і накопичення черги. Завжди фіксуйте:
p50, p95, p99 і за можливості p99.9, плюс IOPS під задаваною затримкою.

Правило 5: перевірте, що ваш тест дійсно синхронний

Не довіряйте конфігураційному файлу тільки тому, що він «виглядає правильно». Змушуйте систему довести це: трасуйте системні виклики, спостерігайте поведінку ZIL, підтвердьте властивості датасету й упевніться, що пристрої виконують flush.

Проєктування чесного тесту fio для синхронних записів бази даних

Почніть з шляху стійкості бази даних

Оберіть одну конфігурацію бази даних і зіставте її з fio:

  • За замовчуванням PostgreSQL: буферизовані WAL-записи + fsync. fio-аналoг: буферизовані записи з fsync=1 або періодичним fsync залежно від групового commit.
  • MySQL/InnoDB: залежить від innodb_flush_method і innodb_flush_log_at_trx_commit. fio-аналoг: мікс fsync і O_DSYNC патернів.
  • SQLite FULL: часті fsync-бар’єри. fio-аналoг: малі синхронні записи з fsync або O_DSYNC.

Обирайте розмір блоку відповідно до журналу, а не таблиці

WAL/redo зазвичай пишеться частинами (часто 8k, 16k, 32k, іноді 4k). Використовуйте bs=8k або bs=16k для тестів, що нагадують WAL. Не застосовуйте bs=1m і не називайте це «базою даних».

Використовуйте кілька job-ів для моделювання конкуренції та групового commit

Більшість систем підтверджують кілька транзакцій одночасно. Навіть якщо кожна транзакція «синхронна», система може конвеєрувати роботу. Запустіть кілька job-ів fio з розділеним файлом або окремими файлами, щоб змоделювати конкуренцію. Але розумійте компроміс:
більше потоків може приховати латентність одного запису, але збільшити хвостову затримку.

Тримайте файли достатньо великими, щоб уникнути фальшивої локальності

Для тестів журналу можна використовувати файл розміром як WAL-сегменти (наприклад, кілька ГБ), але переконайтесь, що ви не перезаписуєте одні й ті ж блоки, які гарячі в ARC та кешах метаданих. Також: не запускайте все на крихітному датасеті з primarycache=all і потім дивуйтеся, чому «швидко».

Розділяйте тести «журнал» і «скидання даних»

У бази даних щонайменше два стилі записів:

  1. Записи журналу (синхронні): малі, чутливі до затримки, критичні щодо стійкості.
  2. Записи даних (помірно асинхронні): більші, орієнтовані на пропускну здатність, керовані чекпоінтами.

Якщо ви змішаєте їх в одному fio job, ви нічого не зможете діагностувати. Створіть щонайменше два профілі тесту.

Запускайте довго, щоб потрапити в сталий стан і побачити цикли TXG

Якщо ваш runtime коротший за кілька інтервалів TXG sync, ви можете бути введені в оману теплими кешами, початковою алокаційною поведінкою та короткостроковими «підйомами» пристроїв. Для затримок синхронних записів мінімум 2–5 хвилин; 10–20 хвилин краще, якщо ви гонитесь за p99.9.

Жарт №2: Єдина річ швидша за NVMe SLOG — це бенчмарк, який забув увімкнути sync.

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

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

Завдання 1: Підтвердити налаштування датасету sync (найбільше «ой»)

cr0x@server:~$ zfs get -o name,property,value,source sync tank/db
NAME      PROPERTY  VALUE     SOURCE
tank/db   sync      standard  local

Значення: standard поважає запити додатка на синхронність. Якщо тут стоїть disabled, ваш «бенчмарк синхронних записів» ймовірно тестує фантазійний режим.

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

Завдання 2: Перевірити, чи існує SLOG і чи він реально використовується

cr0x@server:~$ zpool status -v tank
  pool: tank
 state: ONLINE
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        ONLINE       0     0     0
          mirror-0                  ONLINE       0     0     0
            nvme0n1p2               ONLINE       0     0     0
            nvme1n1p2               ONLINE       0     0     0
        logs
          mirror-1                  ONLINE       0     0     0
            nvme2n1p1               ONLINE       0     0     0
            nvme3n1p1               ONLINE       0     0     0

Значення: Присутній дзеркальний лог-пристрій. Добре: записи ZIL йдуть на SLOG, а дзеркало зменшує ризик втрати журналу через один пристрій.

Рішення: Якщо розділу logs немає і затримка sync висока, розгляньте PLP-спроможний SLOG. Якщо SLOG є, але це один SSD зі сумнівною безпекою при втраті живлення, вважайте його ризиковим доти, доки не отримаєте підтвердження.

Завдання 3: Підтвердити, що SLOG повідомляє про write cache і flush-поведінку

cr0x@server:~$ sudo hdparm -W /dev/nvme2n1

/dev/nvme2n1:
 write-caching =  1 (on)

Значення: Write caching увімкнено. На NVMe це нормально й не автоматично небезпечно; головне питання — чи має диск PLP.

Рішення: Підтвердіть PLP через специфікацію вендора або nvme id-ctrl. Якщо не можете довести безпеку при відключенні живлення, не використовуйте його як SLOG для бази, що вимагає стійкості.

Завдання 4: Виявити індикатори захисту від втрати живлення (найкраще з ОС)

cr0x@server:~$ sudo nvme id-ctrl /dev/nvme2n1 | egrep -i 'oncs|vwc|frmw'
oncs    : 0x001f
vwc     : 0x0001
frmw    : 0x0016

Значення: vwc вказує на наявність волатильного write cache. Це не підтверджує PLP; це підтверджує, що вам слід хвилюватися. Біти oncs показують підтримку опціональних команд, як-от flush. Вам усе одно потрібна правда від виробника щодо PLP.

Рішення: Якщо не можете підтвердити PLP, не продавайте «стійкі коміти». Ви все ще можете використовувати швидкий пристрій, але чесно вказуйте ризик.

Завдання 5: Підтвердити ashift і вирівнювання (тиха вбивця продуктивності)

cr0x@server:~$ zdb -C tank | egrep -n 'ashift|vdev_tree' -A2
42:        ashift: 12
43:        asize: 1999844147200
44:        is_log: 0

Значення: ashift=12 означає 4K сектори. На сучасних SSD/NVMe це зазвичай правильно. Неправильний ashift може спричиняти write amplification і затримки.

Рішення: Якщо ashift занадто малий (наприклад, 9) на 4K‑нативних пристроях, виправлення вимагає перебудови пулу. Не «налаштовуйте» навколо цього; це структурне питання.

Завдання 6: Спостерігайте за поведінкою TXG sync і затримками

cr0x@server:~$ sudo dmesg -T | egrep -i 'txg|spa_sync|zil' | tail -n 5
[Mon Dec 25 10:11:03 2025] ZFS: spa_sync: tank txg 93421 took 2112ms
[Mon Dec 25 10:11:08 2025] ZFS: spa_sync: tank txg 93422 took 1897ms

Значення: TXG sync, що триває ~2 секунди, не є автоматично фатальним, але може корелювати зі сплесками затримок, особливо коли система близька до лімітів брудних даних.

Рішення: Якщо TXG sync часто займає багатосекундний час під навантаженням, досліджуйте латентність пристроїв пулу, налаштування dirty data і чи не накопичуються синхронні записи через затори.

Завдання 7: Перевірити властивості ZFS, що важливі для датасету бази даних

cr0x@server:~$ zfs get -o name,property,value -s local,default recordsize,logbias,atime,compression,primarycache tank/db
NAME     PROPERTY      VALUE     SOURCE
tank/db  recordsize    16K       local
tank/db  logbias       latency   local
tank/db  atime         off       local
tank/db  compression   lz4       local
tank/db  primarycache  all       default

Значення: recordsize впливає на блоки даних (не на записи ZIL), але має значення для I/O шаблонів таблиць. logbias=latency сприяє патернам використання ZIL, що корисні для низької затримки. atime=off уникає зайвих записів. lz4 зазвичай дає виграш.

Рішення: Для датасетів WAL розгляньте менший recordsize (наприклад, 16K) і logbias=latency. Для даних вирівнюйте recordsize з типовим розміром сторінки та шаблонами читання. Не застосовуйте recordsize=8K сліпо скрізь.

Завдання 8: Запустити fio «як WAL» з fsync після кожного запису (худший випадок)

cr0x@server:~$ fio --name=wal-fsync --filename=/tank/db/fio-wal.log --size=8g --bs=8k --rw=write --ioengine=psync --direct=0 --fsync=1 --numjobs=1 --runtime=120 --time_based=1 --group_reporting=1
wal-fsync: (groupid=0, jobs=1): err= 0: pid=18421: Mon Dec 25 10:22:11 2025
  write: IOPS=3400, BW=26.6MiB/s (27.9MB/s)(3192MiB/120001msec)
    slat (usec): min=6, max=310, avg=11.2, stdev=5.4
    clat (usec): min=120, max=8820, avg=280.4, stdev=210.1
     lat (usec): min=135, max=8840, avg=292.1, stdev=210.5
    clat percentiles (usec):
     | 50.00th=[  240], 95.00th=[  520], 99.00th=[ 1200], 99.90th=[ 4100]

Значення: Це моделює «запис 8K, fsync, повтор». Процентилі затримок показують шлях стійкості. p99.9 ≈ 4ms може бути прийнятним або ні, в залежності від SLA.

Рішення: Якщо p99/p99.9 занадто високі, полюйте на латентність SLOG, поведінку flush‑ів пристрою або на контенцію в пулі. Не міняйте налаштування бази даних спочатку; доведіть, що це сховище.

Завдання 9: Запустити тест з O_DSYNC (ближче до деяких режимів БД)

cr0x@server:~$ fio --name=wal-dsync --filename=/tank/db/fio-wal-dsync.log --size=8g --bs=8k --rw=write --ioengine=psync --direct=1 --dsync=1 --numjobs=1 --runtime=120 --time_based=1 --group_reporting=1
wal-dsync: (groupid=0, jobs=1): err= 0: pid=18502: Mon Dec 25 10:25:10 2025
  write: IOPS=5200, BW=40.6MiB/s (42.6MB/s)(4872MiB/120001msec)
    clat (usec): min=95, max=6210, avg=190.7, stdev=160.2
    clat percentiles (usec):
     | 50.00th=[  160], 95.00th=[  380], 99.00th=[  900], 99.90th=[ 2700]

Значення: direct=1 обходить page cache; dsync=1 просить только синхронізацію даних на запис. Це часто краще відображає «надійне додавання в журнал», ніж fsync=1.

Рішення: Якщо це значно краще за fsync-по-запису, це може вказувати на те, що ваше навантаження виграє від групового commit або інших синхронних примітивів. Перевірте відповідність з реальним режимом бази.

Завдання 10: Додати конкуренцію для моделювання навантаження групового commit

cr0x@server:~$ fio --name=wal-dsync-8jobs --filename=/tank/db/fio-wal-8jobs.log --size=16g --bs=8k --rw=write --ioengine=psync --direct=1 --dsync=1 --numjobs=8 --runtime=180 --time_based=1 --group_reporting=1
wal-dsync-8jobs: (groupid=0, jobs=8): err= 0: pid=18614: Mon Dec 25 10:29:40 2025
  write: IOPS=24000, BW=187MiB/s (196MB/s)(33660MiB/180001msec)
    clat (usec): min=110, max=20210, avg=285.9, stdev=540.3
    clat percentiles (usec):
     | 50.00th=[  210], 95.00th=[  650], 99.00th=[ 2400], 99.90th=[12000]

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

Рішення: Якщо для вашого SLA важлива затримка, оптимізуйте хвости затримок, а не пікові IOPS. Можливо, вам краще менше конкурентних коммітерів або кращий SLOG замість більшої кількості потоків.

Завдання 11: Підтвердити, що fio виконує очікувані системні виклики (strace)

cr0x@server:~$ sudo strace -f -e trace=pwrite64,write,fdatasync,fsync,openat fio --name=wal-fsync --filename=/tank/db/trace.log --size=256m --bs=8k --rw=write --ioengine=psync --direct=0 --fsync=1 --numjobs=1 --runtime=10 --time_based=1 --group_reporting=1 2>&1 | tail -n 12
openat(AT_FDCWD, "/tank/db/trace.log", O_RDWR|O_CREAT, 0644) = 3
pwrite64(3, "\0\0\0\0\0\0\0\0"..., 8192, 0) = 8192
fsync(3)                                = 0
pwrite64(3, "\0\0\0\0\0\0\0\0"..., 8192, 8192) = 8192
fsync(3)                                = 0

Значення: Ви бачите реальний патерн: pwrite, fsync, повтор. Якщо ви очікували O_DSYNC і його немає, ваші опції fio не роблять те, що ви думаєте.

Рішення: Не вірте просто «fio каже dsync=1». Підтвердіть системні виклики, особливо при зміні ioengine або прапорів direct I/O.

Завдання 12: Спостерігати активність ZIL/SLOG під час тесту (iostat на лог vdev)

cr0x@server:~$ iostat -x 1 /dev/nvme2n1 /dev/nvme3n1
Linux 6.8.0 (server)  12/25/2025  _x86_64_  (32 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           5.22    0.00    2.10    7.11    0.00   85.57

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz  aqu-sz  %util
nvme2n1           0.0      0.0     0.0    0.0    0.00     0.0   18500.0  150000.0     0.0    0.0    0.18     8.1     3.2   91.0
nvme3n1           0.0      0.0     0.0    0.0    0.00     0.0   18480.0  149800.0     0.0    0.0    0.19     8.1     3.1   90.4

Значення: Інтенсивні записи на пристрої журналу під час запуску fio синхронних записів вказують, що ZIL потрапляє на SLOG. Низький w_await — те, що вам потрібно.

Рішення: Якщо лог-пристрої показують мало активності під час «sync» тестів, ви ймовірно не робите синхронний I/O або датасет/пул налаштовані не так, як ви очікували.

Завдання 13: Перевірити латентність усього пулу та черги (питання «це main vdevs?»)

cr0x@server:~$ iostat -x 1 /dev/nvme0n1 /dev/nvme1n1
Device            r/s     rkB/s   r_await     w/s     wkB/s   w_await  aqu-sz  %util
nvme0n1          50.0   12000.0     0.45   1200.0  98000.0     3.90     6.1   99.0
nvme1n1          45.0   11000.0     0.50   1180.0  97000.0     4.10     6.2   99.0

Значення: Головні пристрої пулу завантажені (%util близько 99) з багатомілісекундними очікуваннями записів. Навіть з хорошим SLOG, TXG sync і загальна контенція можуть підвищувати хвостову затримку.

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

Завдання 14: Переконатися, що ви не тестуєте ARC (кеш) для читань

cr0x@server:~$ arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  arcsz     c
10:35:01   412    20      4     0    0    20  100     0    0   28.1G  30.0G
10:35:02   398    17      4     0    0    17  100     0    0   28.1G  30.0G
10:35:03   420    18      4     0    0    18  100     0    0   28.1G  30.0G

Значення: Низький miss% означає, що читання переважно обслуговуються з ARC. Це нормально для кеш-хітів бази даних, але не репрезентативно для продуктивності диска.

Рішення: Для тестів читання зробіть розмір тесту більшим за ARC або використайте primarycache=metadata на тестовому датасеті, щоб уникнути випадково лише кешових показників.

Завдання 15: Тимчасово протестувати з sync=always, щоб виявити «async, замаскований під sync»

cr0x@server:~$ sudo zfs set sync=always tank/db
cr0x@server:~$ zfs get -o name,property,value sync tank/db
NAME     PROPERTY  VALUE
tank/db  sync      always

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

Рішення: Використовуйте це лише в контрольованому тестуванні. Якщо sync=always руйнує пропускну здатність, це знак, що додаток покладався на асинхронні записи і вам треба перевірити припущення щодо стійкості.

Завдання 16: Підтвердити, що ZFS не загортає записи через ліміти брудних даних

cr0x@server:~$ cat /proc/spl/kstat/zfs/arcstats | egrep 'dirty_data|dirty_over'
dirty_data                            4    1328619520
dirty_over_target                     4             0

Значення: Якщо dirty_over_target часто ненульове, ZFS чинить тиск на письменників, щоб контролювати пам’ять і поведінку sync. Це проявляється у затримках запису і сплесках затримок.

Рішення: Якщо ви досягаєте лімітів dirty під нормальним навантаженням, досліджуйте розмір пам’яті, фонові політики запису і ZFS-налаштування — обережно. Не просто підвищуйте ліміти й забувайте про це.

Завдання 17: Переконатися, що ваш fio job не перезаписує одні й ті ж блоки (помилкова ілюзія)

cr0x@server:~$ fio --name=wal-dsync-rand --filename=/tank/db/fio-wal-rand.log --size=8g --bs=8k --rw=randwrite --ioengine=psync --direct=1 --dsync=1 --numjobs=1 --runtime=60 --time_based=1 --group_reporting=1
wal-dsync-rand: (groupid=0, jobs=1): err= 0: pid=19311: Mon Dec 25 10:41:07 2025
  write: IOPS=2100, BW=16.4MiB/s (17.2MB/s)(984MiB/60001msec)
    clat percentiles (usec):
     | 50.00th=[  330], 95.00th=[ 1200], 99.00th=[ 4800], 99.90th=[21000]

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

Рішення: Для тестів WAL використовуйте послідовний режим, якщо ваша база фактично не пише блоки журналу невпорядковано (рідко). Тримайте цей тест як стресор, а не як основну модель.

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

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

Перше: доведіть, що це дійсно синхронно і дійсно стійко

  1. Перевірте властивість датасету sync і будь-яке успадкування від батьків. Якщо знайдете sync=disabled, припиніть обговорення продуктивності і почніть обговорення ризику.
  2. Підтвердіть налаштування стійкості самої бази (наприклад, чи увімкнено fsync для WAL, режим flush для InnoDB). Якщо БД налаштована на «слабшу стійкість», не тестуйте «сувору стійкість» і навпаки.
  3. Прослідкуйте один репрезентативний запис за допомогою strace або трасування на рівні БД, щоб підтвердити системні виклики: fsync? fdatasync? O_DSYNC?

Друге: ізолюйте, чи проблема в латентності ZIL/SLOG або в заторі пулу

  1. Під час запуску синхронного fio job дивіться iostat -x на пристроях SLOG. Якщо вони зайняті і w_await високий, проблема в лог-пристрої.
  2. Якщо SLOG виглядає добре, дивіться головні vdev. Якщо вони перевантажені або мають високі затримки, TXG sync і контенція пулу тягнуть усе вниз.
  3. Шукайте сплески часу TXG sync у логах ядра. Багатосекундні TXG sync тісно корелюють з хвостовими проблемами затримок.

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

  1. Перевірте ashift, конфігурацію RAID і наскільки заповнений пул. Пул вище ~80–85% часто бачить гіршу фрагментацію та поведінку алокації.
  2. Перевірте наявність конкурентних навантажень: snapshots, реплікація, scrubs, бекапи, компактації, інші датасети на тому ж пулі.
  3. Підтвердіть, чи SLOG захищений від втрати живлення. Якщо ні, ви не можете покладатися на нього для стійкості бази — продуктивність тут не те питання.

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

1) Інцидент через хибне припущення: «sync=disabled просто швидше»

Середня SaaS‑компанія мігрувала з керованої бази до self-hosted Postgres, щоб зекономити й отримати контроль. Зберігання було на ZFS на парі SSD у дзеркалі. Доброзичливий інженер встановив sync=disabled на датасеті, бо коміти «повільні в бенчмарках».

Тижнями все виглядало чудово. Затримки впали. Графіки були зелені. Команда влаштувала переможний круг і перейшла до помітніших задач, як-от фічі продукту й дашборди для дашбордів.

Потім стався випадок з відключенням живлення — не драматичний, просто планове обслуговування UPS і спрацювання автоматичного вимикача, яке не відпрацювало так, як всі думали. Хости перезавантажилися. Postgres піднявся. Він навіть приймав підключення.

Тонка жахливість була в логічній корупції. Невеликий набір недавно підтверджених транзакцій зник, а інший набір опинився частково застосованим. Додаток бачив дивні foreign key проблеми, і процеси з відновлення почали видаляти «дублікати», які не були дублікати.
Розв’язувати це довелося днями, бо корупція рідко робить бузіки і відразу падає.

Постмортем був простим. Хибне припущення: «ZFS безпечно, отже вимкнення sync безпечне». ZFS безпечний, коли ви дотримуєтеся його контракту безпеки. Вони попросили ZFS обманути базу даних. ZFS підкорився. Комп’ютери такі слухняні.

2) Оптимізація, що дала зворотний ефект: «Додамо дешевий швидкий SLOG»

Фінансова платформа мала пул ZFS на enterprise SSD, але навантаження з великою кількістю sync (аудит‑логинг плюс транзакційна база) показували сплески p99 commit затримок. Хтось запропонував додати SLOG. Гарна інтуїція.

Закупівля знайшла «дуже швидкі» споживчі NVMe за частку вартості enterprise. Специфікація була веселкою IOPS‑чисел, що змушує керівництво короткочасно вірити, що фізика — опціональна.

Команда встановила диски як дзеркальний SLOG і знову запустила fio. Числа виглядали феноменально. Потім пішов продакшн-трафік. Хвостова затримка погіршилася. Не постійно — достатньо, щоб ламаються SLA періодично.

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

Виправлення було нудним: замінити SLOG на PLP‑здатні пристрої, підібрані для низьколатентних записів зі збалансованими flush‑ами, а не для максимального маркетингового throughput. Продуктивність покращилася, і історія ризиків перестала бути соромною.

3) Нудна, але правильна практика, що врятувала ситуацію: «Тестуйте реальний шлях fsync»

Велика компанія запускала multi-tenant MySQL на ZFS. У них було внутрішнє правило: будь‑яка зміна зберігання вимагала «тест шляху стійкості». Не загальний бенчмарк. Тест, що моделює реальний метод flush і поведінку commit.

Команда планувала оновлення платформи: нові сервери, новий NVMe‑пул, новий SLOG. На папері це було апгрейдом. Під час pre-prod тестів стандартні throughput бенчмарки виглядали чудово. Тест шляху стійкості fio виглядав… підозріло. p99.9 латентності були несподівано високі.

Завдяки правилу у них були й інструменти: fio job‑и, що відповідали MySQL flush‑патернам, верифікація через strace і скрипт iostat для підтвердження, що SLOG дійсно використовується. Вони швидко відслідкували проблему до налаштування прошивки на лог-пристроях, яке змінювало поведінку flush під певною глибиною черги.

Вендор виправив прошивку/налаштування. Платформа була відправлена. Ніхто не святкував — це правильний рівень святкування за «ми не запустили прихований ризик у продакшн».

Заощадження не було рядком у бюджеті; воно було у відсутності постмортему. Це найкраще.

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

1) «fio показує 200k IOPS, але база комітить по 2k/s»

Симптоми: числа fio величезні; затримка коміту в БД все ще погана; графіки не корелюють.

Причина: fio тест буферизований і не змушує стійкість (direct=0 + без fsync/dsync), або датасет має sync=disabled у тесті але не у проді.

Виправлення: Перезапустіть fio з fsync=1 або dsync=1 (згідно режиму БД), верифікуйте через strace і підтвердіть zfs get sync на цільовому датасеті.

2) «Додавання SLOG погіршило ситуацію»

Симптоми: середня затримка покращилась, але p99/p99.9 погіршились; періодичні зависання; іноді SLOG завантажений.

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

Виправлення: Використовуйте PLP‑здатні низьколатентні пристрої; дзеркальте SLOG; валідуйте під конкуренцією fio; дивіться iostat -x на w_await та поведінку черги.

3) «Синхронні записи повільні навіть з хорошим SLOG»

Симптоми: SLOG виглядає нормально; головні пристрої пулу мають високу завантаженість; час TXG sync довгий.

Причина: затор пулу (чекпоінти, інші орендарі, scrubs, реплікація), throttle через брудні дані або майже повний/зфрагментований пул.

Виправлення: Зменшіть конкурентні записи, плануйте scrubs/реплікацію, додайте vdevs, тримайте простір у пулі, і валідуйте часи TXG sync.

4) «Затримки нормальні у коротких тестах, жахливі за години»

Симптоми: 1–2 хв тести виглядають чудово; стійкі навантаження деградують; періодичні сплески.

Причина: виснаження SLC кешу пристрою, термальне дроселювання, фонові процеси (GC), або цикли TXG/dirty data не покриті короткими запусками.

Виправлення: Запускайте довші тести (10–20 хв), моніторте температуру і тротлінг пристрою, відстежуйте p99.9, і тестуйте на реальних рівнях заповнення.

5) «Ми переключили sync=always і продуктивність впала»

Симптоми: пропускна здатність різко впала; затримки зросли; додаток почав таймаути.

Причина: навантаження покладалося на асинхронні записи; припущення додатка про стійкість були слабшими, ніж думали; або лог-пристрій/пул не можуть витримати примусового sync.

Виправлення: Узгодьте очікування стійкості з бізнес-вимогами, підтвердіть налаштування БД, впровадьте справжній SLOG або прийміть нижчу швидкість комітів з правильною стійкістю.

6) «Випадкові синхронні записи катастрофічні»

Симптоми: randwrite + dsync показує жахливі хвостові затримки; послідовні тести в порядку.

Причина: ви тестуєте патерни синхронізації файлів даних (важко) замість апенду журналу (легше). Також можлива причина: RAIDZ write amplification і фрагментація.

Виправлення: Розділяйте тести WAL (послідовні) і дані; розгляньте дзеркала для чутливих до затримки БД; тримайте простір пулу і розумний recordsize.

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

Крок за кроком: створити синхронний бенчмарк, який ви можете захистити в постмортемі

  1. Оберіть режим стійкості бази.
    Запишіть точно, як виконуються коміти (fsync на commit? груповий commit? O_DSYNC?).
  2. Заблокуйте властивості ZFS.
    Підтвердьте sync, logbias, compression, atime і успадкування від батьків.
  3. Підтвердіть наявність і тип SLOG.
    zpool status, ідентифікуйте log vdevs, підтвердіть дзеркалення, підтвердіть історію PLP.
  4. Побудуйте два профілі fio:
    один для log sync записів (послідовний 8–16K), один для даних/чекпоінту (більше, змішане, можливо випадкове).
  5. Підтвердіть системні виклики.
    Використайте strace на короткому прогоні. Якщо він не робить fsync/dsync так, як ви очікуєте — зупиніться і виправте job.
  6. Запускайте довго.
    Принаймні 2–5 хв; довше, якщо вас цікавить p99.9.
  7. Збирайте правильні метрики під час тесту:
    iostat -x для SLOG і main vdevs, логи часу TXG sync, CPU iowait і процентилі затримок fio.
  8. Приймайте рішення на основі p99/p99.9.
    Якщо хвостова затримка неприйнятна, розглядайте це як питання про дизайн сховища, а не «поправте БД, щоб вона перестала скаржитися».

Контрольний список: здоровий SLOG для баз даних

  • Пристрої з PLP (або документоване прийняття ризику).
  • Дзеркалення SLOG для доступності і зменшення ризику втрати журналу одного диска.
  • Оптимізація для консистентності латентності, а не для маркетингових пікових чисел throughput.
  • Моніторинг записів SLOG під конкуренцією.
  • Простота: один хороший SLOG краще трьох сумнівних.

Контрольний список: коли підозрювати, що ви себе обманюєте

  • Ваш fio job «надто швидкий», а лог-пристрої взагалі не показують записів.
  • Короткі тести виглядають чудово; довгі тести різко деградують.
  • Середня затримка нормальна, але база таймаутиться (хвостова затримка).
  • Ви змінили sync або налаштування flush БД «для продуктивності» і не виконали тест на випадок відключення живлення.
  • Ви не можете пояснити, які саме системні виклики використовує ваш бенчмарк.

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

1) Чи слід використовувати fsync=1 чи dsync=1 у fio для тестів журналу бази даних?

Використовуйте те, що використовує ваша база. PostgreSQL зазвичай нагадує буферизовані записи плюс fsync. Деякі режими MySQL наближені до O_DSYNC або O_DIRECT. Якщо не знаєте — трасуйте базу через strace під час commit і зіставте.

2) Чи коли-небудь sync=disabled прийнятний для баз даних?

Тільки якщо ви навмисно обираєте слабшу стійкість (і бізнес згоден). Це може бути прийнятно для епhemeral кешів або відновлюваних даних. Для транзакційних систем це зниження стійкості, подане як налаштування продуктивності.

3) Чи покращує SLOG звичайну асинхронну пропускну здатність?

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

4) Який розмір має бути у SLOG?

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

5) Чому конкуренція збільшує IOPS, але погіршує p99.9?

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

6) Чи впливає recordsize на продуктивність синхронних записів?

Не безпосередньо для записів ZIL, але воно впливає на те, як пишуться блоки даних і може змінювати поведінку чекпоінтів та read-modify-write. Для датасетів журналу ключові ручки — sync, якість SLOG і загальне здоров’я пулу.

7) Чи слід використовувати direct=1 для WAL‑тестів?

Це залежить. Якщо ваша база пише WAL буферизовано і fsync-ить, то direct=0 з fsync краще відображає реальність. Якщо база використовує direct I/O або O_DSYNC, то direct=1 підходить. Правильна відповідь: відтворіть реальність, а потім вимірюйте.

8) Чи необхідне дзеркалення SLOG?

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

9) Чому мої цифри fio змінюються після перезавантаження?

Прогрів кешу, інший температурний стан пристрою, фонова GC на SSD і інша фрагментація/локальність метаданих — все це має значення. Ось чому довгі тести у сталому стані чесніші, ніж «один прогін після завантаження».

10) Чи може RAIDZ бути хорошим для баз даних на ZFS?

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

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

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

Наступні кроки, що дійсно дають результат:

  1. Оберіть режим стійкості бази, який ви використовуєте в проді, і зафіксуйте його (частота fsync, груповий commit, метод flush).
  2. Перевірте властивість датасету ZFS sync і конфігурацію SLOG, і доведіть патерн системних викликів через strace.
  3. Запустіть два профілі fio (WAL sync і data/checkpoint) достатньо довго, щоб захопити хвостову латентність і цикли TXG.
  4. Коли результати погані, використайте швидкий план діагностики: підтвердіть синхронні семантики, ізолюйте SLOG vs пул, потім шукайте контенцію і підсилювачі.
  5. Якщо вам потрібен SLOG — купуйте з орієнтацією на PLP і стабільність латентності. Якщо ви не можете це обґрунтувати, чесно документуйте компроміс стійкості.

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

← Попередня
Чому монстр кешу може перемогти «швидший» CPU
Наступна →
Ubuntu 24.04: IPv6 ламає випадкові сайти — виправте двостек правильно (не просто відключайте IPv6)

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