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

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

Усе чудово. QPS стабільний. Латентність нудна. Потім приходить деплой, черга очищується або на один день запускають пакетну задачу — і ваш MySQL p99 перетворюється на кошмар. Графіки не стрімко стрибають; вони падають, а потім застоюються на «timeout».

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

Ментальна модель: де MySQL зустрічається з ZFS і починаються проблеми

Почнімо з неприємної істини: для MySQL важливіша послідовність латентності, аніж пік пропускної здатності. Ваш додаток не повідомляє нікого через втрату 10% пропускної спроможності. Він сповіщає вас, коли p99 з 5 мс піднімається до 800 мс і з’єднання починають таймаутитись.

ZFS — це не тонкий шар над дисками. Це транзакційна файловa система з копіюванням при записі (copy-on-write), власним кешем (ARC), групуванням записів, контрольними сумами і окремим механізмом журналу намірів (ZIL) для синхронної семантики. Вона чудово робить записи безпечними та перевірюваними. Водночас вона може перетворити плавний потік дрібних синхронних записів у проблему чергування — особливо коли пул зайнятий, фрагментований або неправильно спроєктований для малої латентності.

Звідки в MySQL беруться записи

InnoDB виконує кілька типів записів. Під навантаженням зі сплесками найбільше болять:

  • Записи redo журналу (послідовні, часто синхронізуються залежно від innodb_flush_log_at_trx_commit).
  • Записи doublewrite buffer (запис-наближення за дизайном для безпеки при краші).
  • Скидання сторінок даних (фонова операція, але сплески відбуваються, коли накопичується тиск на брудні сторінки).
  • Записи binlog (можуть синхронізуватись, особливо в налаштуваннях реплікації/GTID).
  • Тимчасові таблиці / файли сортування (при поганих налаштуваннях можуть сильно навантажувати сховище під час сплесків).

Куди ZFS кладе ці записи

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

Синхронність — ключове слово. Коли MySQL виконує fsync() або відкриває файли з O_SYNC, він вимагає, щоб дані були на стабільному сховищі перед продовженням. На ZFS синхронні записи обробляються через ZIL. Якщо ви додаєте окремий логовий пристрій (SLOG), ви змінюєте місце збереження цих синхронних записів.

ZIL — це не «кеш записів» для усього пулу. Це механізм для задоволення синхронних семантик безпечно. Більшість часу записи ZIL відтворюються лише після крашу; інакше вони «впалюються» під час наступного коміту TXG. Для латентності MySQL шлях через ZIL — це те місце, де секунди перетворюються на мілісекунди, або мілісекунди в секунди.

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

Парафразована ідея (приписано): Werner Vogels наполягає, що варто проєктувати систему для відмов як нормальний стан, а не виняток.

Факти та історичний контекст, що пояснюють сучасні режими відмов

  • ZFS народився в Sun як файлова система з наскрізною цілісністю даних: контрольні суми скрізь, самовідновлення з надмірністю. Цей «пріоритет цілісності» впливає на торгові‑задачі продуктивності й сьогодні.
  • Copy-on-write не опціональний в ZFS. Перезаписи перетворюються на виділення нових блоків і оновлення метаданих. Чудово для снапшотів; потенційно болісно для баз даних з інтенсивними випадковими записами, коли простору замало.
  • ZIL існує тому, що POSIX вимагає синхронної семантики. Це не особлива функція для баз даних; це інфраструктура для коректної роботи fsync().
  • SLOG — це пристрій, а не режим. Люди говорять «увімкнути SLOG»; насправді ви додаєте окремий log vdev, щоб зберігати ZIL записи швидше і передбачуваніше.
  • ARC (Adaptive Replacement Cache) створено, щоб перегравати класичний LRU, балансуючи новизну та частоту. Він може зробити читання дивовижно швидким — доки не забере надто багато пам’яті у InnoDB buffer pool та ОС.
  • L2ARC з’явився пізніше, щоб розширити ARC на швидкі пристрої. Він допомагає читанням, але коштує пам’яті й пропускної здатності для підтримки, що не безкоштовно під час сплесків записів.
  • Doublewrite buffer MySQL — це відповідь на часткові сторінкові записи при краші. На файлових системах з атомарністю на рівні сторінки він часто зайвий; на більшості систем — захисний механізм. На ZFS він часто допомагає експлуатаційній безпеці, але дає додаткове IO.
  • Властивості датасетів еволюціонували як запобіжні заходи, бо адміністратори постійно робили помилки. Такі налаштування як atime=off, compression і recordsize з’явилися тому, що поведінка файлової системи за замовчуванням не «розумна для БД».
  • Сучасні SSD додали власні сюрпризи латентності: вичерпання SLC‑кешу, фірмове GC і змінний write amplification. Ваш «швидкий» SLOG може перетворитися на гарбуз під тривалими синхронними записами.

Як сплески записів перетворюються на катастрофи латентності на ZFS

1) Ампліфікація синхронних записів: fsync‑шторм зустрічає обмежені IOPS

Коли MySQL налаштований на надійність — наприклад, innodb_flush_log_at_trx_commit=1 і синхронізація binlog увімкнена — кожний коміт може вимагати fsync(). Груповий коміт допомагає, але під сплесками трафіку ви все одно можете побачити гуркіт синхронних запитів.

Якщо ZFS має записувати ці синхронні записи у головний пул, ваша латентність стає латентністю пулу. А пул також зайнятий TXG‑комітами, оновленням метаданих і, можливо, resilver/scrub. Отже, ви отримуєте систему, де пропускна здатність виглядає «нормальною», але кожна транзакція чекає в черзі на стійке збереження.

2) Тиск комітів TXG: «гладкі» записи стають періодичним болем

ZFS накопичує брудні дані в пам’яті і скидає їх у TXG. Під сплесками записів ви можете досягти лімітів брудних даних, і ZFS почне гальмувати вхідні записи, щоб уникнути безконтрольного використання пам’яті. Це коректна поведінка. Це також момент, коли ваші потоки бази припиняють робити корисну роботу і починають чекати на сховище.

Коли flush TXG великий, він конкурує з синхронним IO. Навіть з SLOG, пул все одно має виконати реальну роботу запису даних і метаданих. SLOG допомагає вам швидко підтвердити синхронні записи, але пізніше ви все одно можете згоріти, якщо пул не встигає і ZIL починає заповнюватись і чекати комітів.

3) Простір і фрагментація: тихий множник латентності

ZFS потребує вільного простору. Не «трохи вільного». РЕАЛЬНО вільного простору. Коли пули заповнюються, алокація ускладнюється. Блоки фрагментуються. Оновлення метаданих стають більш розсіяними. Кожен запис починає виглядати як проблема маленького випадкового IO.

Бази даних відмінно перетворюють вільний простір у «не вільний». Якщо ваш пул на рівні 80–90% використання, ви фактично тестуєте алгоритми алокації ZFS під час піків. Не робіть цього в продакшені, якщо вам подобається дзвінок on‑call о 3:00.

4) Непідходящий recordsize і надто багато роботи з метаданими

Сторінки InnoDB зазвичай 16K. За замовчуванням ZFS recordsize — 128K для загальних навантажень. Якщо ви залишите 128K для датасету з tablespace InnoDB, ви можете збільшити write amplification: змінювані 16K можуть вимагати перезапису більших блоків залежно від патернів доступу і стиснення.

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

5) Міф про SLOG: «додати SSD і вся синхронна латентність зникне»

SLOG корисний лише настільки, наскільки його durable write latency надійна. Споживчі SSD можуть бути швидкими, доки не впадуть у write cliff. Вони також можуть брехати про flush‑поведінку. Для SLOG вам потрібна передбачувана латентність під стійкими записами і захист від втрати живлення (або еквівалентні enterprise‑гарантії).

Жарт №1: Купити дешевий SSD для SLOG — це як найняти стажера тримати фундамент будівлі: ентузіазм є, але від фізики будуть питання.

6) ARC vs InnoDB buffer pool: пам’ять — спільне поле бою

ARC агресивний і ефективний. InnoDB buffer pool теж агресивний і ефективний. Якщо дозволити обом боротися за RAM, ядро врешті‑решт обере переможця, і це не буде ваша безвідмовність. Ви побачите свопінг, хвилі повернення пам’яті й IO‑ампліфікацію, коли кешовані сторінки будуть інтенсивно змінюватись.

Для MySQL зазвичай краще явно задати розмір InnoDB buffer pool і обмежити ARC, щоб ZFS не з’їдав решту. ZFS може вижити з меншим ARC; MySQL при випадкових читаннях — ні.

Швидка діагностика: перше/друге/третє

Це потік «у вас п’ять хвилин до того, як керівництво зайде в канал інциденту». Це не елегантно. Це ефективно.

Перше: підтвердьте, що симптом — це латентність сховища, а не CPU або блокування

  1. Перевірте MySQL на очікування блокувань і тиск flush: якщо потоки застрягли на mutex/locks, налаштування сховища не допоможуть.
  2. Перевірте завантаження ОС і IO wait: високий %wa і зростаюче навантаження при низькому використанні CPU вказують на чергу IO.
  3. Перевірте латентність пулу ZFS і глибину черги: визначте, чи обмежує вузол пул чи SLOG.

Друге: вирішіть, чи синхронні записи — вузьке місце

  1. Подивіться на високі швидкості синхронних записів (redo/binlog) і поведінку fsync().
  2. Перевірте, чи є у вас SLOG і чи він насичений або повільний.
  3. Підтвердіть налаштування sync у датасеті (зазвичай має бути standard для надійності; не «виправляйте» інциденти брехнею).

Третє: перевірте стан пулу і «повільні» фактори

  1. Заповненість пулу: якщо ви вище ~80% використання, ви знайшли причину.
  2. Фрагментація: висока фрагментація корелює з проблемами випадкових записів.
  3. Scrub/resilver: якщо запущені, вони можуть перетворити керований сплеск у катастрофу.
  4. Торможення через брудні дані: ZFS може свідомо уповільнювати вас, щоб не загинути.

Рішення: у випадку інциденту три найпоширеніші винуватці — (1) шлях синхронних записів, (2) насиченість/латентність пулу, (3) тиск пам’яті, що викликає турбулентність кешів. Не починайте змінювати recordsize під час інциденту, якщо ви не любите грати в рулетку з даними.

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

Це реальні оперативні кроки: запустіть команду, інтерпретуйте вивід, вирішіть, що робити далі. Припустимо Linux‑хост з OpenZFS, пул tank, датасет tank/mysql і дані MySQL у /var/lib/mysql.

Завдання 1: перевірити, чи пул явно перевантажений прямо зараз

cr0x@server:~$ zpool iostat -v tank 1 5
              capacity     operations     bandwidth
pool        alloc   free   read  write   read  write
----------  -----  -----  -----  -----  -----  -----
tank        6.20T  1.10T    120   3800   18M   420M
  raidz2    6.20T  1.10T    120   3700   18M   410M
    sda         -      -     15    520  2.2M    58M
    sdb         -      -     14    515  2.1M    57M
    sdc         -      -     15    540  2.2M    60M
    sdd         -      -     15    530  2.2M    59M
logs            -      -      -     100     -    12M
  nvme0n1       -      -      -     100     -    12M

Що це означає: записи інтенсивні (3800 ops/s). Пропускна здатність пулу — 420 MB/s при відносно невеликих читаннях. Якщо латентність висока, потрібна наступна команда: латентність IO, а не лише операції.

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

Завдання 2: перевірити латентність по vdev, щоб знайти вузьке місце

cr0x@server:~$ zpool iostat -v -l tank 1 5
                               operations         bandwidth            total_wait         disk_wait
pool                         read  write        read  write        read  write        read  write
--------------------------  -----  -----  ---------  ---------  ---------  ---------  ---------  ---------
tank                           90   4200        12M       480M        3ms     120ms        1ms     110ms
  raidz2                       90   4100        12M       470M        3ms     125ms        1ms     115ms
    sda                         9    580       1.2M        68M        2ms     140ms        1ms     130ms
    sdb                         9    570       1.2M        67M        2ms     138ms        1ms     128ms
    sdc                         9    600       1.3M        69M        2ms     142ms        1ms     132ms
    sdd                         9    590       1.3M        69M        2ms     141ms        1ms     131ms
logs                            0    120         0        14M        0ms       2ms        0ms       1ms
  nvme0n1                       0    120         0        14M        0ms       2ms        0ms       1ms

Що це означає: загальне total_wait для записів пулу ~120 ms. SLOG у порядку (2 ms). Вузьке місце — робота головного пулу з flush/commit, а не лог‑пристрій.

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

Завдання 3: перевірити заповненість пулу і фрагментацію (передбачити проблеми)

cr0x@server:~$ zpool list -o name,size,alloc,free,capacity,frag,health tank
NAME  SIZE  ALLOC  FREE  CAPACITY  FRAG  HEALTH
tank  7.28T  6.20T  1.08T      85%   62%  ONLINE

Що це означає: 85% заповнення і 62% фрагментація. Це податковий коефіцієнт латентності, особливо для випадкових записів і навантажень, що важкі на метадані.

Рішення: пріоритет — додати ємність (додати vdev або мігрувати дані). Розглядайте «вільний простір» як функцію продуктивності, а не бухгалтерську деталь.

Завдання 4: перевірити, чи виконується scrub або resilver

cr0x@server:~$ zpool status tank
  pool: tank
 state: ONLINE
  scan: scrub in progress since Mon Dec 23 02:11:44 2025
        3.12T scanned at 1.2G/s, 1.55T issued at 600M/s, 6.20T total
        0B repaired, 25.00% done, 02:30:12 to go
config:

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          raidz2-0  ONLINE       0     0     0
            sda     ONLINE       0     0     0
            sdb     ONLINE       0     0     0
            sdc     ONLINE       0     0     0
            sdd     ONLINE       0     0     0
        logs
          nvme0n1   ONLINE       0     0     0

errors: No known data errors

Що це означає: scrub активно виконує IO. На пулі, що вже має проблеми з латентністю записів, scrub може підштовхнути вас до катастрофи.

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

Завдання 5: призупинити/відновити scrub під час інциденту (якщо політика дозволяє)

cr0x@server:~$ sudo zpool scrub -p tank
cr0x@server:~$ zpool status tank
  pool: tank
 state: ONLINE
  scan: scrub paused since Mon Dec 23 03:01:02 2025
        3.12T scanned at 1.2G/s, 1.55T issued at 600M/s, 6.20T total
        0B repaired, 25.00% done, 02:30:12 to go
errors: No known data errors

Що це означає: IO‑тиск має знизитись. Ви жертвуєте часом виявлення латентних помилок заради стабільності продакшену.

Рішення: робіть це лише якщо організація приймає ризик; задокументуйте та відновіть пізніше.

Завдання 6: переглянути властивості датасету, що впливають на поведінку MySQL

cr0x@server:~$ zfs get -o name,property,value -s local,default recordsize,compression,atime,sync,logbias,primarycache,secondarycache tank/mysql
NAME       PROPERTY        VALUE
tank/mysql recordsize      128K
tank/mysql compression     off
tank/mysql atime           on
tank/mysql sync            standard
tank/mysql logbias         latency
tank/mysql primarycache    all
tank/mysql secondarycache  all

Що це означає: recordsize за замовчуванням 128K, compression вимкнено, atime увімкнено. Для InnoDB recordsize часто має бути меншим; atime зазвичай вимикають; compression часто дає виграш на сучасних CPU.

Рішення: плануйте зміни свідомо (особливо recordsize). Швидко вимкніть atime; розгляньте ввімкнення стиснення; оцініть recordsize з урахуванням робочого навантаження.

Завдання 7: вимкнути atime для датасету MySQL

cr0x@server:~$ sudo zfs set atime=off tank/mysql
cr0x@server:~$ zfs get -o name,property,value atime tank/mysql
NAME       PROPERTY  VALUE
tank/mysql atime     off

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

Рішення: робіть це, якщо у вас немає специфічних потреб у atime (рідко для директорії даних MySQL).

Завдання 8: увімкнути стиснення (зазвичай lz4) щоб зменшити IO під час сплесків

cr0x@server:~$ sudo zfs set compression=lz4 tank/mysql
cr0x@server:~$ zfs get -o name,property,value compression tank/mysql
NAME       PROPERTY     VALUE
tank/mysql compression  lz4

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

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

Завдання 9: перевірити розмір ARC і тиск (щоб уникнути боротьби за пам’ять)

cr0x@server:~$ arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  arcsz     c  avail
12:10:01   980   120     10    40   33    60   50    20   17   58G   60G    3G
12:10:02  1020   140     12    55   39    70   50    15   11   59G   60G    2G
12:10:03  1005   160     14    70   44    75   47    15    9   60G   60G    1G

Що це означає: ARC націлений на свій максимум (c), і доступна пам’ять тане. Якщо хост почне свопитись — MySQL постраждає серйозно.

Рішення: обмежте ARC (zfs_arc_max), щоб MySQL buffer pool мав стабільну RAM. Не намагайтеся «виграти», дозволяючи обом кешам рости без контролю.

Завдання 10: перевірити свопінг та IO wait на рівні ОС

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
12  3  524288  31200  10240  88400  150  220   120  9800 2100 6400 12  8 62 18  0
10  2  524288  28000  10080  87000  180  260   110 10500 2200 6500 11  9 60 20  0
14  4  524288  25000   9980  86500  210  300    90 12000 2300 6700 10 10 58 22  0
11  3  524288  24000   9950  86000  190  280   100 11000 2250 6600 11  9 59 21  0
13  4  524288  23000   9900  85500  220  310    95 12500 2350 6800 10 10 56 24  0

Що це означає: активний свопінг (si/so) і високий wa. Класичний сценарій «латентність погіршилася, бо пам’ять погіршилася».

Рішення: зменшіть ARC, знизьте споживання пам’яті MySQL або додайте RAM. Короткотерміново зупиніть найгірші джерела записів і стабілізуйтеся.

Завдання 11: підтвердити пристрої ZIL/SLOG та їхній стан

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

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          raidz2-0  ONLINE       0     0     0
            sda     ONLINE       0     0     0
            sdb     ONLINE       0     0     0
            sdc     ONLINE       0     0     0
            sdd     ONLINE       0     0     0
        logs
          nvme0n1   ONLINE       0     0     0

errors: No known data errors

Що це означає: є SLOG (nvme0n1), і він здоровий.

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

Завдання 12: переконатися, що датасет змонтований там, де MySQL справді зберігає дані

cr0x@server:~$ findmnt /var/lib/mysql
TARGET         SOURCE      FSTYPE OPTIONS
/var/lib/mysql tank/mysql  zfs    rw,xattr,noacl

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

Рішення: якщо ні — припиніть «налаштовувати» не той датасет і спочатку виправте схему монтування/макет.

Завдання 13: переглянути налаштування надійності MySQL, що породжують синхронні записи

cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_doublewrite','innodb_flush_method');"
+--------------------------------+------------+
| Variable_name                  | Value      |
+--------------------------------+------------+
| innodb_doublewrite             | ON         |
| innodb_flush_log_at_trx_commit | 1          |
| innodb_flush_method            | O_DIRECT   |
| sync_binlog                    | 1          |
+--------------------------------+------------+

Що це означає: це режим повної надійності: redo скидається при кожному коміті і binlog синхронізується при кожному коміті. Чудово для коректності. Складніше для сховища під сплесками.

Рішення: не змінюйте ці параметри легковажно. Якщо бізнес погоджується на ризик (деякі погоджуються для кешів, але не для грошей), розгляньте sync_binlog > 1 або innodb_flush_log_at_trx_commit=2, але лише з явним погодженням.

Завдання 14: перевірити сигнали чекпоінтів/тиску брудних сторінок MySQL

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs'; SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';"
+-------------------------------+--------+
| Variable_name                 | Value  |
+-------------------------------+--------+
| Innodb_buffer_pool_pages_dirty| 245000 |
+-------------------------------+--------+
+---------------------+--------+
| Variable_name       | Value  |
+---------------------+--------+
| Innodb_os_log_fsyncs| 185000 |
+---------------------+--------+
+-----------------+------+
| Variable_name   | Value|
+-----------------+------+
| Innodb_log_waits| 420  |
+-----------------+------+

Що це означає: багато брудних сторінок і ненульові Innodb_log_waits вказують на вузьке місце з flush журналу. Під сплесками це узгоджується з тиском на шлях синхронізації сховища.

Рішення: корелюйте з латентністю ZFS. Якщо час запису ZFS високий — виправляйте шлях збереження; якщо ZFS в нормі — перевіряйте конфігурацію журналу MySQL і планування CPU.

Завдання 15: знайти процеси, що застрягли в IO (швидко і грубо)

cr0x@server:~$ ps -eo pid,comm,state,wchan:30 | egrep 'mysqld|z_wr_iss|z_wr_int|z_wr|txg|sync' | head
  2141 mysqld           D io_schedule
  2147 mysqld           D io_schedule
  2153 mysqld           D io_schedule
  1103 z_wr_iss         D cv_wait
  1104 z_wr_int         D cv_wait

Що це означає: потоки MySQL у стані D, що чекають у планувальнику IO, вказують на застої сховища. Письмові потоки ZFS, що чекають, також можуть вказувати на внутрішнє гальмування/тиск при комітах.

Рішення: перевірте zpool iostat -l і статистику дисків ОС. Якщо підтверджено — зменшіть навантаження записів і вирішіть обмеження пулу.

Завдання 16: перевірити латентність і насичення по пристроях з Linux (доповнення до погляду ZFS)

cr0x@server:~$ iostat -x 1 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.10    0.00    8.20   18.40    0.00   61.30

Device            r/s     w/s   rKB/s   wKB/s  avgrq-sz avgqu-sz   await  r_await  w_await  svctm  %util
sda               9.0   580.0   1200   68000     228.0     45.0   135.0     3.0    137.0   1.6  99.0
sdb               9.0   570.0   1180   67000     228.0     44.0   132.0     3.0    134.0   1.6  98.5
nvme0n1           0.0   120.0      0   14000     233.0      0.3     2.0     0.0      2.0   0.2   2.4

Що це означає: HDDs завантажені ~99% з очікуванням запису ~130 ms. NVMe (SLOG) у порядку. Це відповідає картині ZFS: проблема латентності запису в головному пулі.

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

Налаштування ZFS, що важливі для MySQL (і які — пастки)

Recordsize: підлаштовуйте під робоче навантаження, а не під ідеологію

Для InnoDB звично використовують recordsize=16K для датасету з tablespace. Аргумент: InnoDB змінює 16K сторінки, і менші записи можуть знизити ампліфікацію записів і покращити латентність під випадковими оновленнями.

Але не копіюйте на автопілот. Якщо ваше навантаження — здебільшого великі послідовні сканування, бекапи або аналітика з великими діапазонами, більший recordsize може допомогти. Якщо ви зберігаєте binlog або бекапи на тому ж датасеті (не робіть так), потреби конфліктуватимуть.

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

  • Помістіть дані MySQL у власний датасет.
  • Для OLTP‑InnoDB починайте з recordsize=16K.
  • Для змішаного навантаження протестуйте 16K vs 32K.

Стиснення: lz4 зазвичай — безкоштовні гроші

Стиснення зменшує байти записів і читань. Для сплесків записів це важливо, бо ви намагаєтесь проштовхнути менше фізичного IO через той самий вузький корок. На сучасних CPU lz4 зазвичай дає чистий позитивний ефект.

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

sync і logbias: будьте чесні, але обирайте отруту

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

logbias=latency vs logbias=throughput часто неправильно розуміють. Для MySQL зазвичай важлива латентність. Якщо у вас є SLOG, logbias=latency — розумний вибір. Якщо SLOG відсутній і пул повільний — зміна logbias не створить обладнання. Однак вона може змінити, як і скільки йде в ZIL vs головний пул у певних випадках.

Жарт №2: Встановити sync=disabled, щоб «вирішити проблему латентності» — це як вимкнути пожежну сигналізацію, бо вона пищить під час пожежі.

SLOG: що він реально робить і коли допомагає

SLOG допомагає, коли:

  • Ваше навантаження робить багато синхронних записів.
  • Головний пул має гіршу латентність, ніж якісний SSD/NVMe.
  • Пристрій SLOG має захист від втрати живлення і низьку стабільну латентність запису.

SLOG не допомагає, коли:

  • Ви обмежені комітами TXG у головному пулі (пул не може швидко скинути дані).
  • Ваше навантаження здебільшого асинхронне (немає fsync‑тиску).
  • Ваш «SLOG» — споживчий SSD, що руйнується під тривалими записами або бреше про flush.

Операційна порада: дзеркальте SLOG, якщо ваш профіль ризику це вимагає. Відмова SLOG може змусити пул опинитися в ненадійному стані залежно від платформи; навіть якщо не робить цього, ви тільки що створили тригер інциденту. Для критичного MySQL дзеркальний SLOG — це нудно, але правильно.

Спеціальний vdev: метадані і дрібні блоки можуть стати прихованим виграшем

Якщо ваш пул на HDD, подумайте про спеціальний vdev (на SSD) для метаданих і дрібних блоків. Бази даних створюють метаданичний шум: непрямі блоки, структури каталогів, мапи простору. Поміщення цього на швидке медіа може знизити латентність і покращити стабільність.

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

Primarycache/secondarycache: припиніть подвійне кешування до свопу

MySQL вже має buffer pool. ZFS має ARC і, можливо, L2ARC. Якщо ви кешуєте все двічі, ви марнуєте RAM і підвищуєте хвилі витіснення під сплесками.

Багато команд ставлять primarycache=metadata для датасету MySQL, щоб уникнути кешування даних в ARC, які вже кешує InnoDB, залишаючи метадані гарячими. Це може допомогти стабілізувати пам’ять. Це не універсальне рішення; вимірюйте патерни читань.

Ashift і вейв лейаут vdev: «ви не витягнете геометрію»

Якщо пул збудований з неправильною вирівнюваністю секторів (ashift занадто малий), ви можете страждати від ампліфікації записів завжди. Якщо лейаут vdev спроєктований під ємність, а не під IOPS (наприклад, широка RAIDZ для бази OLTP), ви отримаєте передбачуваний дискомфорт.

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

Налаштування MySQL/InnoDB, що взаємодіють із ZFS

Налаштування надійності: зробіть ризик явним

Найбільший важіль латентності при сплесках у MySQL — як часто ви примушуєте стійкі скидання:

  • innodb_flush_log_at_trx_commit=1: стійкий redo при кожному коміті. Найбезпечніше, найвищий тиск на синхронні операції.
  • innodb_flush_log_at_trx_commit=2: скидання в ОС при кожному коміті, fsync щосекунди. Менше тиску; ризик втрати до 1с даних при краші.
  • sync_binlog=1: fsync binlog при кожному коміті. Сильна консистентність реплікації; тиск на синхронні операції.

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

Метод скидання: уникайте подвоєння буферизації

innodb_flush_method=O_DIRECT часто використовують, щоб уникнути кешу сторінок ОС для файлів даних InnoDB, зменшуючи подвійне буферування. На ZFS взаємодія тонша, бо ZFS має ARC, не такий як OS page cache, але O_DIRECT часто успішно використовується.

Що ви намагаєтесь уникнути: MySQL записує дані, ОС кешує їх, ZFS кешує знову, пам’ять тане, і ядро починає свопити. Це не стратегія продуктивності; це крик про допомогу.

Тюнінг брудних сторінок: сплески підсилюються чергою

Коли buffer pool накопичує надто багато брудних сторінок, MySQL змушений активно скидати їх. Це може перетворити помірний сплеск записів на штурм сховища. Налаштуйте:

  • innodb_max_dirty_pages_pct і innodb_max_dirty_pages_pct_lwm
  • innodb_io_capacity і innodb_io_capacity_max (встановлюйте відповідно до реальної здатності сховища)
  • innodb_flush_neighbors (часто 0 для SSD пулів; більш тонко для HDD)

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

Binlog і тимчасові файли: не колокуйте болючі речі

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

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

Інцидент через хибне припущення: «Ми додали SLOG, тож синхронна латентність вирішена»

Компанія мала MySQL primary на пулі ZFS, що базувався на HDD. Вони періодично бачили p99‑сплески під час трафікових бур — здебільшого під розсилками маркетингу й кінцем місяця. Хтось зробив півправильний крок: додали швидкий NVMe як SLOG. Латентність покращилась у стабільному стані, тож зміна була визнана «виправленою».

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

Коли вони нарешті подивились zpool iostat -l, історія була відверта: записи в SLOG мали низьку латентність, але час запису в головний пул був величезний. TXG‑коміти були вузьким місцем. ZIL міг швидко підтвердити записи, але не міг змусити пул швидше скидати брудні дані.

Виправлення не було додаванням ще одного лог‑пристрою. Воно було банальним: додати vdev для підвищення IOPS, знизити заповненість пулу та сегментувати навантаження, щоб binlog і тимчасові файли не билися з tablespaces. Хибне припущення — вважати, що синхронна латентність = латентність SLOG; насправді стійкість синхронності залежить від того, чи пул встигає за накопиченням брудної роботи.

Оптимізація, що відвернулася: «Давайте піднімемо recordsize заради throughput»

Інша команда мала здебільшого SSD‑пул і хотіла кращу продуктивність при масовому завантаженні. Вони змінили recordsize датасету MySQL на 1M, бо бачили рекомендації для великих послідовних навантажень. Масові завантаження стали швидшими. Вони оголосили перемогу і пішли далі.

Потім OLTP‑латентність почала коливатись. Не завжди, але під сплесками. Малі оновлення гарячих рядків почали викликати непропорційний IO. ZFS перезаписував великі рекорди частіше, і метаданичні операції зросли. Пул міг витримати ширину смуги, але розподіл латентності став неприємним. Бізнесу було неважливо, що нічні завантаження завершуються швидше; їх хвилювало, що іноді оформлення замовлення займало 900 мс.

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

Зрештою вирішили сегментувати: окремі датасети для масових таблиць з більшим recordsize, OLTP tablespaces з 16K або 32K, і перестали вважати «MySQL» одним монолітним IO‑патерном. Повернення не означало, що великий recordsize завжди поганий; помилка була в застосуванні одного вузла налаштувань до кількох конфліктних навантажень.

Нудна, але правильна практика, що врятувала день: «Запас і поступове обмеження»

Одна організація запускала MySQL на ZFS для внутрішньої критичної системи. Нічого гламурного: бізнес‑процеси, деякі сплески вдень, інколи бекстепи. У них була сувора політика: пул ніколи не перевищує консервативний поріг заповнення, і кожна write‑важка задача має повзунок обмеження у планувальнику.

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

Чому? Дві причини. По‑перше, був вільний простір і низька фрагментація, тож алокація не деградувала під тиском. По‑друге, on‑call швидко зменшив backfill і чергу пакетів без зміни налаштувань надійності. Система мала «гальма» операційного управління, що не вимагали брехні файловій системі.

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

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

1) p99‑сплески під час сплесків, SLOG виглядає нормально

Симптом: zpool iostat -l показує низьку латентність логу, але високий час запису пулу; MySQL показує log waits і таймаути.

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

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

2) Латентність раптово погіршилась після того, як пул перевищив ~80% використання

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

Корінь проблеми: штрафи алокації та фрагментації на майже заповненій файловій системі з copy-on-write; більше розсіяних записів і оновлень метаданих.

Виправлення: додати ємність (краще — додаванням vdev, а не по одному диску), ребалансувати міграціями датасетів, встановити SLO щодо вільного простору.

3) «Виправлено» встановленням sync=disabled, потім крах спричинив втрату даних

Симптом: латентність була хороша до непередбаченого ребуту; після перезапуску таблиці MySQL пошкоджені або останні коміти відсутні.

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

Виправлення: встановити sync=standard (або always якщо потрібно), використати належний SLOG і вирішити реальне вузьке місце продуктивності.

4) Свопінг починається під час сплесків і не відновлюється

Симптом: vmstat показує активний своп; MySQL гальмує навіть після завершення сплеску; ARC залишається великим.

Корінь проблеми: ARC і буфер пул MySQL конкурують; тиск пам’яті спричиняє reclaim і своп, що підвищує IO і латентність.

Виправлення: обмежити ARC, правильно налаштувати буфер пул MySQL, уникати L2ARC без достатнього RAM, додати пам’ять якщо потрібно.

5) Записи швидкі до запуску scrub/resilver, потім p99 вибухає

Симптом: кореляція з zpool status що показує скан.

Корінь проблеми: підтримка IO конкурує з виробничими записами; пул не має запасу IOPS.

Виправлення: плануйте scrubs поза піком, забезпечте IOPS‑маржу пулу, використайте контролі пріоритету IO де можливо, призупиняйте scrubs під час інцидентів згідно з політикою.

6) Після «додавання SSD кешу» продуктивність погіршилась під записами

Симптом: L2ARC увімкнено; під сплесками латентність записів зростає; використання пам’яті підскакує.

Корінь проблеми: витрати на підтримку L2ARC (метадані, записи в L2ARC) підвищують навантаження; накладні витрати пам’яті знижують запас.

Виправлення: відключити L2ARC для write‑важких OLTP систем, якщо немає виміреної проблеми з read‑miss і достатнього RAM.

7) Випадкові зависання кожні кілька секунд як по годинах

Симптом: періодичні сплески латентності вирівняні з інтервалами комітів TXG.

Корінь проблеми: flush TXG спричиняє хвилі чергування; пул не може стабільно підтримувати роботу без сплесків.

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

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

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

  1. Оберіть лейаут vdev для IOPS перш за все: дзеркальні vdev як базис для OLTP. Ємність приходить від більшої кількості дзеркал, а не від тонких RAIDZ.
  2. Тримайте пул під SLO по заповненості: встановіть внутрішнє правило (наприклад, алерт на 70%, дія при 80%). Точне число залежить від навантаження, але «довести до 95%» — не серйозна інженерія.
  3. Використовуйте реальний SLOG, якщо потрібна синхронна продуктивність: низька латентність, захист від втрати живлення і бажано дзеркалювання.
  4. Розгляньте спеціальний vdev для HDD‑пулів: дзеркалений, розмір під метадані та дрібні блоки.
  5. Створіть окремі датасети:
    • tank/mysql для tablespaces InnoDB
    • tank/mysql-binlog для binlogs
    • tank/mysql-tmp для tmpdir при потребі
  6. Задайте властивості датасетів навмисно:
    • atime=off
    • compression=lz4
    • recordsize=16K (стартова точка для OLTP tablespaces)
    • logbias=latency (типово для DB датасетів)
  7. Вирішіть політику кешування: часто primarycache=metadata на tablespaces, лишайте дефолт на binlogs якщо їх часто читають для реплікації/бекапів.
  8. Обмежте ARC відповідно до загальної RAM і buffer pool MySQL: залиште запас для ОС, таблиць сторінок і пікових навантажень.
  9. Тестуйте навантаження зі сплесками: не лише середнє навантаження. Симулюйте патерн сплесків і дивіться p99, а не лише пропускну здатність.

Операційний чеклист: коли ви розгортаєте зміну, що багато пише

  • Перевірте ємність пулу і фрагментацію. Якщо ви вже біля порогу — відкладіть write‑важку задачу або додайте ємність спочатку.
  • Підтвердіть, що scrub/resilver не заплановано перекриватись.
  • Переконайтеся, що пакетні задачі мають регулятори і їх можна призупинити без зміни коду.
  • Зробіть базовий замір zpool iostat -l і метрик MySQL fsync перед зміною.
  • Налаштуйте оповіщення по p99 латентності, Innodb_log_waits і ZFS write wait разом. Одна метрика обманює.

Чеклист інциденту: правила «не погіршувати»

  • Не ставте sync=disabled на датасет даних як мітод пом’якшення інциденту, якщо ви явно не приймаєте втрату даних.
  • Не змінюйте recordsize під час інциденту в очікуванні миттєвого покращення; воно вплине на нові записи, а корінь проблеми зазвичай інший.
  • Якщо політика дозволяє — призупиніть scrubs/resilvers під час танення пулу.
  • Зменшіть навантаження записів: обмежте задачі, зменшіть повтори, відкиньте некритичні записи і зупиніть backfills.
  • Збирайте докази: zpool iostat -l, iostat -x, лічильники MySQL. Ваше майбутнє «я» подякує за чеки.

FAQ

1) Чи варто взагалі запускати MySQL на ZFS?

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

2) Чи обов’язковий SLOG для MySQL?

Не завжди. Якщо ваше навантаження здебільшого асинхронне або ви приймаєте розслаблену надійність, він може не знадобитися. Якщо ви працюєте з innodb_flush_log_at_trx_commit=1 і sync_binlog=1 під сплесками, якісний SLOG часто є різницею між «нормально» і «інцидентом».

3) Чи вирішить затримку сплесків встановлення sync=disabled?

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

4) Який recordsize слід використовувати для InnoDB?

Стартова точка — recordsize=16K для датасету tablespace. Протестуйте 16K vs 32K при змішаних патернах. Розділяйте датасети для різних IO‑патернів (binlogs, бекапи).

5) Чи допомагає стиснення ZFS базам даних?

Часто так. lz4 може зменшити фізичні записи і читання, що допомагає під сплесками і зменшує знос. Перевіряйте запас CPU і вимірюйте p99 латентність, а не лише throughput.

6) Чи варто вмикати L2ARC для MySQL?

Зазвичай ні для write‑важкого OLTP. L2ARC має накладні витрати пам’яті і артистичні витрати на запис. Якщо у вас виміряна проблема з read‑miss і достатньо RAM — може допомогти, але це не налаштування за замовчуванням.

7) Чому продуктивність погіршується, коли пул заповнюється?

Copy-on-write алокація ускладнюється при зменшенні вільного простору; фрагментація і накладні операції метаданих зростають. Ваш «простий запис» стає більш випадковим IO і більше метаданичної роботи. Тримайте запас простору.

8) RAIDZ чи дзеркала для MySQL?

Для латентнісно чутливого OLTP дзеркала — безпечний вибір за замовчуванням, бо вони дають більше IOPS і передбачувану латентність. RAIDZ може працювати, але легше натрапити на латентні стрибки при малих випадкових записах і високій зайнятості.

9) Як зрозуміти, чи вузьке місце — синхронні записи чи фонове скидання?

Корелюйте Innodb_log_waits і fsync‑лічильники MySQL з zpool iostat -l (часи очікування для логу і пулу). Якщо латентність логу висока — дивіться SLOG і шлях синхронізації. Якщо час пулу високий — дивіться на тиск TXG, заповненість пулу і лейаут vdev.

10) Чи можуть спеціальні vdev допомогти латентності MySQL?

Так, особливо на HDD‑пулі, прискорюючи метадати і дрібні блоки IO. Дзеркаліть їх і ставтесь до них як до критичних; втрата спеціального vdev може призвести до втрати пулу.

Висновок: наступні кроки, що дійсно знижують ризик

Якщо ви запускаєте MySQL на ZFS і боїтесь сплесків записів, вам не потрібні містичні рішення. Потрібні три речі: правильна ментальна модель, чесні рішення щодо надійності і достатньо IOPS/запасу, щоб ZFS міг робити свою роботу без гальмування бази даних.

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

  1. Запустіть швидку діагностику в тихий день. Зафіксуйте базові zpool iostat -l, iostat -x і ключові лічильники MySQL.
  2. Аудитуйте заповненість пулу і фрагментацію. Якщо ви вище вашого безпечного порогу — розглядайте ємність як критичне виправлення продуктивності.
  3. Підтвердіть шлях синхронних записів: датасет sync чесний, SLOG (якщо є) enterprise‑класу і здоровий, і зрозумійте, чи саме flush пулу — реальний вузький момент.
  4. Розділіть датасети за IO‑патернами і встановіть властивості навмисно (atime off, lz4 on, recordsize відповідний).
  5. Обмежте ARC, щоб пам’ять не стала прихованим тригером інцидентів.
  6. Побудуйте операційні регулятори для write‑важких задач. Ваше найкраще швидке виправлення під час сплеску — часто «зупинити зайві записи», а не «міняти файлову систему в бою».

Зробіть це, і наступний сплеск записів стане контрольованим уповільненням, а не опівнічним крахом. Мета — не героїчне відновлення. Мета — нудні графіки.

← Попередня
Міграція електронної пошти: перемістіть пошту на новий сервер з мінімальним простоєм (реальні кроки)
Наступна →
Ubuntu 24.04: Виправлення «Too many open files» в Nginx — підвищення лімітів правильно (systemd)

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