ZFS для PostgreSQL: стратегія dataset-ів і синхронізації, яка працює

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

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

Біль завжди однакова: синхронні записі. Надійність WAL. Стрибки латентності, що проявляються лише під навантаженням із великою кількістю commit-ів.
Хтось радить «просто поставити sync=disabled» — і раптом ви на небажаному зібранні про «положення щодо цілісності даних».

Стратегія: що робити, а чого не робити

Якщо ви хочете, щоб PostgreSQL на ZFS працював у продакшні, перестаньте думати в категоріях загальних порад для файлових систем і почніть мислити
у термінах меж довговічності (durability boundaries). Межа довговічності PostgreSQL — це WAL + поведінка fsync (плюс контрольні точки і реплікація).
Межа довговічності ZFS — це ZIL (і SLOG, якщо присутній), transaction groups і місце, куди потрапляють синхронні записи.

Стратегія розкладу dataset-ів і синхронізації, яка працює, не екзотична:

  • Тримайте дані PostgreSQL і WAL на окремих dataset-ах (а ідеально — на окремих vdev-ах, якщо можете).
  • Не вимикайте засоби безпеки PostgreSQL: fsync=on, full_page_writes=on і розумні налаштування synchronous_commit.
  • Для більшості випадків використовуйте ZFS sync=standard. Воно відповідає POSIX-семантиці sync, і Postgres її використовує недарма.
  • Якщо латентність коміту — вузьке місце, виправляйте шлях синхронізації правильно: забезпечте належний SLOG-пристрій (з захистом від втрати живлення) або свідомо змініть вимоги додатка до довговічності (наприклад, synchronous_commit=off для конкретних робочих навантажень).
  • Використовуйте стиснення. Воно часто покращує латентність і пропускну здатність для сторінок бази даних і — рідкісний «безкоштовний» прийом, що справді працює в полі.
  • Не ставте sync=disabled на dataset бази даних, якщо ви не зафіксували межі ураження та не отримали явну згоду від тих, кого будуть дзвонити о 3:00 ночі.

Найпростіший спосіб втратити роботу — «оптимізувати довговічність», не сказавши нікому. Другий за простотою — купити швидкі диски і змусити ZFS чекати повільні синхронні I/O.

Жарт №1: Якщо ви ставите sync=disabled на базу і називаєте це «eventually durable», ви винайшли новий рівень зберігання: надія.

Факти й контекст, що впливають на рішення

Це не тривіалії заради тривіалій. Кожен із цих пунктів має зрушити справжній проєктний вибір.

  1. ZFS — copy-on-write (з роботи оригінальної Sun Microsystems). PostgreSQL очікує перезаписувати 8 KB сторінки; ZFS перетворює це на нові алокації блоків. Це впливає на write amplification і фрагментацію.
  2. ZIL завжди присутній, навіть без SLOG. SLOG — це не «увімкнення ZIL», це переміщення лог-пристрою на щось швидше і безпечніше для синхронних записів.
  3. Стандартний розмір сторінки PostgreSQL — 8 KB. За замовчуванням recordsize у ZFS часто 128 KB. Це несумісність не є фатальною, але визначає поведінку read-modify-write при оновленнях і скільки даних «забруднюється» за запис.
  4. Історично багато звинувачень «ZFS повільний для баз даних» походили від неправильно сконфігурованих пулів: неправильний ashift для 4K-дисків, відсутній SLOG для синхронно-важких навантажень або ARC, позбавлений оперативної пам’яті.
  5. WAL PostgreSQL послідовний, але не суто послідовний. Він багато додає записи, але fsync може створювати сплески, а архівування/реплікація — читальне навантаження у невдалий час.
  6. ZFS перевіряє контрольні суми кожного блоку. Це дозволяє виявляти мовчазну корупцію, яку інші стек-и б підгодовували вашій базі даних, доки вона «таємниче» не впаде пізніше. Для баз даних це не розкіш — це цілісність.
  7. Компресія стала «безпечним за замовчуванням» в багатьох ZFS-інсталяціях лише після зрілості LZ4 (і прискорення CPU). Нині залишати компресію вимкненою часто означає викидати продуктивність.
  8. Довговічність PostgreSQL — не бінарний перемикач. Ви можете обирати довговічність на рівні транзакції (synchronous_commit) або реплікації (sync replication), а не шляхом саботажу файлової системи.

Розміщення dataset-ів, що переживе реальні навантаження

Найпоширеніша помилка — помістити все в один dataset, один раз налаштувати і думати, що робота зроблена.
PostgreSQL має щонайменше три I/O-персонажі: файли даних, WAL і «речі, які важливі, але не варті оптимізації за рахунок довговічності»
(логи, дампи, тимчасові місця для бекапів тощо).

Прагматичне дерево dataset-ів

Ось розклад, що відображає поведінку Postgres і дозволяє тонко налаштувати ZFS без самосаботажу:

  • tank/pg: батьківський dataset для всього сховища PostgreSQL.
  • tank/pg/data: основний каталог даних (PGDATA) без WAL, якщо ви його переміщуєте.
  • tank/pg/wal: каталог WAL (створіть символічне посилання pg_wal сюди або використовуйте postgresql.conf, де це підтримується).
  • tank/pg/tmp: тимчасові таблиці / сорти, якщо ви розміщуєте temp_tablespaces тут (необов’язково; часто краще на локальному NVMe і прийнятно втратити при падінні).
  • tank/pg/backups: бекапи та проміжне сховище basebackup (цілі для snapshot-ів, черга архіву WAL тощо).

Чому окремі dataset-и? Тому що recordsize, logbias, compression і іноді рішення про primarycache відрізняються.
WAL потребує низької латентності й передбачуваної поведінки синхронізації. Файли даних потребують ефективності читання, кешування та контролю фрагментації.

Куди фізично помістити WAL

Якщо можете собі дозволити — WAL заслуговує на швидке сховище і чистий шлях синхронізації. Варіанти:

  • Той самий пул, окремий dataset: найпростіше, дає можливість налаштовувати властивості; продуктивність залежить від розміщення vdev-ів.
  • Окремі mirror vdev-и для WAL: добре, коли головний пул на RAIDZ і затримка WAL страждає.
  • Окремий пул для WAL: іноді робиться, але операційно важче (більше речей для моніторингу, більше доменів відмов).

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

Семантика sync: PostgreSQL vs ZFS (і де підходить SLOG)

PostgreSQL записує WAL-записи і використовує fsync() (або fdatasync()) для гарантії довговічності при коміті транзакції, залежно від налаштувань.
Коли Postgres каже «sync», це означає: «Якщо зараз втратити живлення, закомічена транзакція має лишитися закоміченою після відновлення».

ZFS з sync=standard поважає синхронні запити, використовуючи ZIL. ZIL — це не кеш записів для всього; це журнал синхронних записів, щоб їх можна було відтворити після збою.
Без виділеного SLOG ZIL живе в основному пулі, отже синхронні записи потрапляють на ваші диски даних. З SLOG вони потрапляють на SLOG (по-прежньому записані безпечно), а потім пізніше фіксуються в основному пулі звичайними transaction group-ами.

Що справді означає sync для dataset-а

  • sync=standard: поважати sync-запити. Це за замовчуванням, яке вам потрібно для баз даних.
  • sync=always: трактувати всі записи як синхронні, навіть якщо додаток не просив. Зазвичай гірше для Postgres; це перетворює неважливі записи на податок латентності.
  • sync=disabled: брехати додатку щодо довговічності. Швидко. Але кар’єрно обмежує, коли спрацьовує відключення живлення.

SLOG: що це таке і чим не є

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

Якщо SLOG бреше щодо довговічності (споживчий SSD з летким кешем і без PLP), ви фактично перемістили ризик sync=disabled в апаратний рівень.
Це може «працювати» доти, доки не перестане — найменш корисний шаблон надійності в операційній роботі.

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

перефразована ідея — John Allspaw: надійність походить від того, що робиш відмови видимими й переживаними, а не від віри, що вони не трапляться.

Рекомендовані властивості ZFS для Postgres (з обґрунтуванням)

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

Передумови на рівні пулу (перед dataset-ами)

  • Правильний ashift для ваших дисків (зазвичай 12 для 4K секторів). Помилка тут — постійна плата за write amplification.
  • Міри для затримкочутливих навантажень (включно з WAL-важкими Postgres). RAIDZ підходить для ємності та пропускної здатності, але невеликі sync I/O зазвичай краще на mirror-ах.
  • Достатньо RAM для ARC, але не позбавляйте Postgres shared_buffers. ARC із задоволенням розростається; ваш OOM killer буде не в захваті.

Dataset: tank/pg/data

  • recordsize=16K або recordsize=8K: почніть з 16K для багатьох OLTP-навантажень; 8K може зменшити read-modify-write при оновленнях, але погіршити великі послідовні читання. Вимірюйте.
  • compression=lz4: зазвичай покращує ефективні IOPS і знижує write amplification. Це одне з небагатьох налаштувань, що і безпечне, і швидке.
  • atime=off: Postgres не потребує atime. Припиніть платити за це.
  • xattr=sa (якщо підтримується): тримає xattr в інодах; знижує I/O метаданих.
  • logbias=latency (за замовчуванням): віддавати перевагу низькій латентності для синхронних операцій. Для data-dataset залишайте за замовчуванням; WAL налаштовуватимемо окремо.

Dataset: tank/pg/wal

  • recordsize=16K: записи WAL — «append-ish»; вам не потрібен великий recordsize. Уникайте 128K тут.
  • compression=lz4: WAL інколи добре стиснутий, але виграш зазвичай помірний. Все ж частіше не зашкодить і зменшує записи на пристрій.
  • logbias=throughput:

Тут потрібне пояснення. logbias=throughput каже ZFS схилятися до запису sync-даних у головний пул, а не в SLOG у деяких патернах.
На системі з хорошим SLOG часто хочете logbias=latency. На системі без SLOG WAL вже живе в основному пулі ZIL; bias має менше значення.
Практично: якщо у вас є належний SLOG і латентність коміту — вузьке місце, тримайте logbias=latency для WAL. Якщо ви насичуєте SLOG або він не є вузьким місцем, розгляньте throughput.
Не робіть карго-культу. Вимірюйте латентність комітів і поведінку ZIL.

  • sync=standard: так, іще раз. Ваш «швидкий режим» повинен бути на рівні налаштувань Postgres, а не брехні файлової системи.
  • primarycache=metadata (іноді): якщо читання WAL витісняє корисний кеш даних, обмежте кеш до метаданих для WAL. Залежить від RAM і навантаження.

Dataset: tank/pg/tmp (необов’язково)

  • sync=disabled може бути прийнятним тут якщо це справді відкиданий тимчасовий простір і ви готові втратити його при падінні. Трактуйте як scratch-диск.
  • compression=lz4, atime=off.

Жарт №2: Єдине, що «тимчасовіше» за tank/pg/tmp, — це впевненість того, хто не тестував failover.

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

Це польові вправи: що ви запускаєте, що означає вивід і яке рішення ви приймаєте. Мета — припинити сперечатися на рівні відчуттів.

Завдання 1: Підтвердіть стан пулу (бо все інше марно, якщо він зламаний)

cr0x@server:~$ sudo zpool status -v tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 02:11:03 with 0 errors on Sun Dec 22 03:00:12 2025
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
          nvme2n1p1                 ONLINE       0     0     0

errors: No known data errors

Що це означає: пул ONLINE, scrub чистий, SLOG-пристрій присутній і online.
Рішення: можна рухатися до тюнінгу продуктивності. Якщо бачите DEGRADED, checksum помилки або мертвий лог-пристрій — зупиніться й виправляйте надійність першочергово.

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

cr0x@server:~$ sudo zdb -C tank | grep -E "ashift|vdev_tree" -n | head
56:        vdev_tree:
74:            ashift: 12

Що це означає: ashift: 12 вказує на 4K сектори. Добре для сучасних SSD/HDD.
Рішення: якщо ashift 9 на 4K-дисках — ви платите за write amplification вічно. Фікс — міграція/перебудова, а не регулювання.

Завдання 3: Перегляньте властивості dataset-ів

cr0x@server:~$ sudo zfs get -o name,property,value,source -r recordsize,compression,atime,sync,logbias,primarycache,xattr tank/pg
NAME          PROPERTY      VALUE     SOURCE
tank/pg       atime         off       local
tank/pg       compression   lz4       local
tank/pg       logbias       latency   default
tank/pg       primarycache  all       default
tank/pg       recordsize    128K      default
tank/pg       sync          standard  default
tank/pg       xattr         sa        local
tank/pg/data  recordsize    16K       local
tank/pg/wal   recordsize    16K       local
tank/pg/wal   logbias       latency   local

Що це означає: видно, що успадковане, а що задане явно.
Рішення: встановлюйте властивості на dataset, що відповідає поведінці (data vs WAL), а не на батька «бо зручно».

Завдання 4: Створіть dataset-и з розумними значеннями за замовчуванням

cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql tank/pg
cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql/16/main tank/pg/data
cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql/16/wal tank/pg/wal
cr0x@server:~$ sudo zfs create -o mountpoint=/var/lib/postgresql/tmp tank/pg/tmp

Що це означає: dataset-и існують і монтуються там, де Postgres очікує (підлаштуйте під вашу дистрибуцію/версію).
Рішення: тримайте межі файлової системи узгодженими з операційними потребами: окремі snapshot-и, окремі властивості, окремий моніторинг.

Завдання 5: Застосуйте властивості (data)

cr0x@server:~$ sudo zfs set atime=off compression=lz4 xattr=sa recordsize=16K sync=standard tank/pg/data
cr0x@server:~$ sudo zfs get -o property,value -H atime,compression,xattr,recordsize,sync tank/pg/data
atime	off
compression	lz4
xattr	sa
recordsize	16K
sync	standard

Що це означає: dataset data налаштований під сторінкову поведінку Postgres і зменшення метаданичного шуму.
Рішення: почніть з 16K. Якщо оновлень багато і бачите write amplification — тестуйте 8K. Не вгадуйте.

Завдання 6: Застосуйте властивості (WAL)

cr0x@server:~$ sudo zfs set atime=off compression=lz4 recordsize=16K sync=standard logbias=latency tank/pg/wal
cr0x@server:~$ sudo zfs get -o property,value -H atime,compression,recordsize,sync,logbias tank/pg/wal
atime	off
compression	lz4
recordsize	16K
sync	standard
logbias	latency

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

Завдання 7: Підтвердьте, куди Postgres пише WAL

cr0x@server:~$ sudo -u postgres psql -XAtc "show data_directory; show hba_file; show config_file;"
/var/lib/postgresql/16/main
/var/lib/postgresql/16/main/pg_hba.conf
/etc/postgresql/16/main/postgresql.conf
cr0x@server:~$ sudo -u postgres psql -XAtc "select pg_walfile_name(pg_current_wal_lsn());"
00000001000000020000003A
cr0x@server:~$ sudo ls -ld /var/lib/postgresql/16/main/pg_wal
lrwxrwxrwx 1 postgres postgres 26 Dec 25 10:44 /var/lib/postgresql/16/main/pg_wal -> /var/lib/postgresql/16/wal

Що це означає: каталог WAL перенаправлено правильно.
Рішення: якщо WAL усе ще всередині data-dataset, втрачаєте ізоляцію налаштувань, а snapshot-и стають складнішими, ніж треба.

Завдання 8: Перевірте, чи ви обмежені на sync-commit

cr0x@server:~$ sudo -u postgres psql -XAtc "select name, setting from pg_settings where name in ('fsync','synchronous_commit','wal_sync_method');"
fsync|on
synchronous_commit|on
wal_sync_method|fdatasync

Що це означає: Postgres поводиться безпечно: fsync увімкнено, синхронні коміти.
Рішення: якщо латентність неприйнятна, вирішуйте це через SLOG або за рахунок цілеспрямованих компромісів на рівні Postgres — не вимикайте sync у ZFS.

Завдання 9: Виміряйте активність ZIL/SLOG і знайдіть тиск синхронізації

cr0x@server:~$ sudo zpool iostat -v tank 1 5
                              capacity     operations     bandwidth
pool                        alloc   free   read  write   read  write
--------------------------  -----  -----  -----  -----  -----  -----
tank                         980G  2.65T    120   1800  9.2M  145M
  mirror-0                   980G  2.65T    120   1700  9.2M  132M
    nvme0n1p2                   -      -     60    850  4.6M   66M
    nvme1n1p2                   -      -     60    850  4.6M   66M
logs                             -      -      0    420    0  4.1M
  nvme2n1p1                      -      -      0    420    0  4.1M
--------------------------  -----  -----  -----  -----  -----  -----

Що це означає: vdev logs виконує записи. Це sync-трафік, що потрапляє на SLOG.
Рішення: якщо операцій запису на SLOG багато і латентність комітів висока, ваш SLOG може бути занадто повільним, насиченим або збоїти.

Завдання 10: Слідкуйте за латентністю прямо через iostat (фактичний рівень пристроїв)

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

Device            r/s   w/s  rkB/s  wkB/s  await  svctm  %util
nvme0n1          80.0  600.0  5120  65536   2.10   0.20  14.0
nvme1n1          78.0  590.0  5000  64500   2.05   0.19  13.5
nvme2n1           0.0  420.0     0   4200   0.35   0.05   2.5

Що це означає: низький await на SLOG-пристрої свідчить, що він не є вузьким місцем.
Рішення: якщо await на лог-пристрої стрибає в мілісекунди під навантаженням commit-ів, виправляйте SLOG (пристрій, PCIe-топологію, прошивку, PLP) або не використовуйте його.

Завдання 11: Підтвердіть, що компресія справді допомагає (і не спалює CPU)

cr0x@server:~$ sudo zfs get -o name,property,value -H compressratio,compression tank/pg/data tank/pg/wal
tank/pg/data	compressratio	1.62x
tank/pg/data	compression	lz4
tank/pg/wal	compressratio	1.10x
tank/pg/wal	compression	lz4

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

Завдання 12: Перевірте тиск ARC і чи не змагається ZFS за RAM з Postgres

cr0x@server:~$ sudo arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  arcsz     c
10:52:01   520    40      7    30   75    10   25     0    0   64G   96G
10:52:02   610    55      9    45   82    10   18     0    0   64G   96G
10:52:03   590    50      8    41   82     9   18     0    0   64G   96G

Що це означає: ARC великий, але не заповнений до межі, miss% — розумний. Якщо miss% високий і Postgres теж сильно кешує, можливо, є подвійне кешування.
Рішення: налаштуйте max ARC, якщо ОС починає свопити або Postgres терпить нестачу; або зменшіть shared_buffers, якщо ARC виконує роботу краще для вашого патерну доступу.

Завдання 13: Перевірте таймінг transaction group (джерело періодичних затримок)

cr0x@server:~$ sudo sysctl vfs.zfs.txg.timeout
vfs.zfs.txg.timeout: 5

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

Завдання 14: Підтвердіть autotrim (поведінка SSD з часом)

cr0x@server:~$ sudo zpool get autotrim tank
NAME  PROPERTY  VALUE     SOURCE
tank  autotrim  on        local

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

Завдання 15: Санітарна перевірка політики snapshot-ів (бо бекапи — це операційна функція)

cr0x@server:~$ sudo zfs list -t snapshot -o name,creation -S creation | head
NAME                               CREATION
tank/pg/data@hourly-2025-12-25-10   Thu Dec 25 10:00 2025
tank/pg/wal@hourly-2025-12-25-10    Thu Dec 25 10:00 2025
tank/pg/data@hourly-2025-12-25-09   Thu Dec 25 09:00 2025
tank/pg/wal@hourly-2025-12-25-09    Thu Dec 25 09:00 2025

Що це означає: існує узгоджений cadence snapshot-ів для обох dataset-ів.
Рішення: snapshot-и не є бекапами самі по собі, але дають швидкий rollback і реплікацію. Переконайтеся, що утримання WAL/архівування узгоджене з cadence snapshot-ів.

Завдання 16: Тестуйте латентність sync-записів з pgbench і корелюйте

cr0x@server:~$ sudo -u postgres pgbench -i -s 50 benchdb
dropping old tables...
creating tables...
generating data...
vacuuming...
creating primary keys...
done.
cr0x@server:~$ sudo -u postgres pgbench -c 16 -j 16 -T 60 -N benchdb
transaction type: 
scaling factor: 50
query mode: simple
number of clients: 16
number of threads: 16
duration: 60 s
number of transactions actually processed: 920000
latency average = 1.043 ms
tps = 15333.201 (without initial connection time)

Що це означає: це переважно читальне тестування (-N) і не повинно сильно навантажувати sync-commit.
Рішення: проведіть також write-heavy тест; якщо тільки write/commit тести повільні — фокусуйтеся на WAL + шляху sync (SLOG, латентність пристрою, queue depth).

Швидкий сценарій діагностики

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

По-перше: доведіть, що це саме latency sync (або ні)

  • Перевірте Postgres: чи повільні коміти, чи запити повільні з інших причин?
  • Дивіться на pg_stat_statements і розподіл латентності транзакцій, а не лише середній TPS.
  • Якщо бачите сплески write-латентності, збіглі з commit-ами/чекпоінтами — підозрюйте шлях sync.
cr0x@server:~$ sudo -u postgres psql -XAtc "select checkpoints_timed, checkpoints_req, buffers_checkpoint, buffers_clean from pg_stat_bgwriter;"
120|8|981234|44321

Рішення: високі checkpoints_req відносно timed checkpoints свідчать про тиск і зупинки через чекпоінти; досліджуйте тонке налаштування чекпоінтів і WAL, але також і затримку сховища.

По-друге: перевірте, чи ZFS здоровий і не обмежує сам себе

  • zpool status для помилок та активності resilver/scrub.
  • zpool iostat -v 1 щоб побачити, куди йдуть записи (основні vdev-и vs logs).

По-третє: знайдіть пристрій, який фактично повільний

  • iostat -x 1 і дивіться на високий await та високе %util.
  • Якщо є SLOG — перевірте його окремо.
  • Підтвердьте розміщення PCIe і чи не на спільній шині «швидкий» пристрій чи за дивним контролером.

По-четверте: перевірте кешування і тиск пам’яті

  • Розмір ARC і пропуски (misses).
  • Свопінг Linux або сплески reclaim.
  • Взаємодія Postgres shared_buffers vs OS cache vs ARC.

По-п’яте: перевірте фрагментацію і сигнали write amplification

  • zpool list за ємністю; висока заповненість шкодить продуктивності.
  • recordsize dataset-а vs фактичний I/O-патерн.
  • Autotrim, динаміка compressratio.

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

Ось ті, що з’являються в ротаціях на викликах, бо вони тонкі, виживальні… поки не стає гірше.

1) «Коміти повільні й нестабільні» → немає SLOG (або він поганий) → додайте належний SLOG

Симптом: TPS виглядає нормальним до підвищення конкуренції; латентність комітів стрибає. Користувачі бачать випадкові паузи.

Корінна причина: синхронні WAL-записи потрапляють на завантажений RAIDZ vdev або повільні диски; або SLOG — споживчого класу без PLP і має стрибки латентності.

Виправлення: використовуйте змонтовані, низьколатентні, захищені від втрати живлення SLOG-пристрої; перевірте через zpool iostat -v і iostat -x.

2) «Продуктивність погіршилась після зміни recordsize» → невідповідність recordsize і write amplification → відкат і тестування

Симптом: після встановлення recordsize=8K по всьому пулу послідовні сканування і бекапи стали повільніші; зросло навантаження на CPU.

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

Виправлення: використовуйте 16K (іноді 32K) для data, тримайте OLAP-таблиці на великому recordsize; WAL — на 16K; бенчмаркуйте реальними запитами.

3) «Ми втратили закомічені дані після відключення живлення» → sync=disabled (або небезпечний SLOG) → відновіть довговічність і перевірте целостність

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

Корінна причина: ZFS підтвердив sync-записи без стабільного сховища. Це могло бути від sync=disabled або пристрою, що брешуть про довговічність.

Виправлення: встановіть sync=standard; використовуйте належний SLOG; прогоніть перевірки узгодженості і підтвердьте припущення додатка про довговічність.

4) «Перідичні 5–10 секундні зависання» → пул майже повний або сплески checkpoint-ів → звільніть місце і вирівняйте запис

Симптом: паузи приблизно з регулярним інтервалом; інакше I/O виглядає нормально.

Корінна причина: пул занадто заповнений (алокація важчає), або чекпоінти змушують великі flush-и.

Виправлення: тримайте пули з комфортним запасом вільного місця; тюньте Postgres checkpoint/WAL; переконайтеся, що dataset-и WAL і data налаштовані окремо.

5) «CPU високе, I/O низьке, запити все одно повільні» → неправильно звинувачено компресію чи checksumming → перевірте пам’ять і блокування

Симптом: графіки диска спокійні, але латентність висока і CPU зайнятий.

Корінна причина: ви не прив’язані до I/O; можливо, контенція буферів, вакуумні проблеми або невірний розмір shared_buffers/ARC.

Виправлення: використовуйте Postgres-запити (pg_stat_activity, pg_stat_bgwriter, pg_stat_io у новіших версіях) щоб знайти реальне вузьке місце. Не зробіть ZFS цапом відбувайлом.

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

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

Середня SaaS-компанія мігрувала свій primary Postgres кластер з ext4 на апаратний RAID до ZFS на парі блискучих NVMe mirror-ів.
Вони робили міграцію акуратно: репетиції, виміри, навіть нічний cutover. Графіки виглядали чудово. Усі пішли додому.

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

Неправильне припущення було тонким і людським: вони вірили «NVMe швидке, ZFS безпечний, тож можна вимкнути sync, щоб отримати продуктивність ext4».
Вони встановили sync=disabled на dataset, думаючи, що Postgres все одно буде безпечним, бо fsync=on в Postgres. Але ZFS повертав успіх,
не примушуючи стабільного запису. Postgres робив усе правильно; файлова система просто відмовилася співпрацювати.

Післяінцидентне виправлення було буденним: відновили sync=standard, додали mirror SLOG з PLP і задокументували, що «швидкий режим» для підмножини навантажень —
synchronous_commit=off в конкретних сесіях, а не глобальна брехня файлової системи.

Справжній урок не «ніколи не оптимізуйте». Він такий: не оптимізуйте, змінюючи значення «закомічено». У вашого додатка вже є ручка для цього.

Міні-історія 2: Оптимізація, що відіграла назад

Інша організація — велика, регульована й з комітетами — мала репортингову Postgres систему з великими послідовними скануваннями й пакетними записами вночі.
Вони прочитали допис про підбір recordsize під розмір сторінки бази даних і вирішили встановити recordsize=8K по всьому пулу «для послідовності».
Заява на зміну пройшла. Усі кивнули. Послідовність заспокоює.

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

Причина: навантаження мало великі послідовні читання (аналітика), і менший recordsize збільшив метаданичну накладну і знизив ефективність читання.
ZFS prefetch і агрегація блоків втратили ефективність. Компресія була вмикнена, але тепер стиснення відбувалося по багатьох дрібних записах і відстежувалося окремо.
Навантаження на CPU зросло. Диски не були завантажені; система просто більше часу проводила на обліку.

Вони виправили це, розділивши dataset-и за поведінкою: tablespaces під data-warehouse на dataset з recordsize=128K (іноді 256K для дуже великих сканів),
а OLTP-датасети на 16K. WAL залишився 16K. Усі зберегли «послідовність», але не на рівні всього пулу.

Тиха висновок: одне число не може описати «робоче навантаження бази даних». Якщо ви не можете назвати I/O-патерн — не можете його налаштувати.

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

Команда фінансових послуг запускала Postgres на ZFS з консервативною конфігурацією: mirror-ми, компресія увімкнена, sync standard і mirrored SLOG.
Нічого особливого. Їхній найкращий інженер описав це як «агресивно нецікаве». Це було як схвалення.

Вони також мали звичку, яку інші команди жартома лаяли: щотижневі scrub-и і дашборд, що показував помилки контрольних сум навіть коли «все працює».
На папері scrub-и були «додатковим I/O». На зустрічах scrub-и — «навіщо ми це робимо». Насправді scrub-и були канаркою.

Одного тижня scrub показав невелику кількість checksum помилок на одному пристрої. Жодних помилок додатка поки що. Жодних симптомів у клієнтів.
Вони замінили пристрій за графіком, виконали resilver і продовжили роботу. Через тижні схожий модель SSD знайшли з проблемою прошивки при певному переході стану живлення.
Команди без проактивного виявлення дізналися б про це в менш приємний спосіб.

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

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

Покроково: збудуйте новий хост Postgres на ZFS правильно

  1. Проєктуйте пул з акцентом на латентність: mirror-и для первинного OLTP, якщо важлива латентність комітів. RAIDZ — для ємності та пропускної здатності.
  2. Обирайте правильне вирівнювання секторів: підтвердіть ashift=12 під час створення пулу.
  3. Додавайте SLOG тільки якщо він потрібен (синхронно-важкі робочі навантаження) і тільки на PLP-захищені пристрої. Міроруйте його.
  4. Створіть окремі dataset-и: tank/pg/data, tank/pg/wal, опційно tank/pg/tmp і tank/pg/backups.
  5. Встановіть властивості на рівні dataset-ів: compression, recordsize, logbias, atime, xattr.
  6. Підключіть директорії Postgres правильно і перевірте місце розташування WAL.
  7. Бенчмаркуйте з pgbench і production-подібним набором запитів. Вимірюйте перцентилі латентності комітів.
  8. Увімкніть scrub-и і моніторинг помилок. Справляйтеся з checksum помилками як із пожежною сигналізацією, а не як з порадами.
  9. Робіть snapshot-и осмислено: координуйте з архівуванням/утриманням WAL і вашими цілями відновлення.
  10. Задокументуйте контракт довговічності: які налаштування дозволені (synchronous_commit), і що заборонене (sync=disabled на критичних dataset-ах).

Операційний чекліст: перед тим як змінювати будь-яку властивість ZFS у продакшні

  • Чи можете ви відкотитися? (Змінити властивості легко; відкат по продуктивності — ні.)
  • Чи маєте ви відтворення навантаження (pgbench профіль, replay або принаймні набір тестових запитів)?
  • Чи змінить це семантику довговічності? Якщо так — є явна згода?
  • Чи зняли ви метрики до/після: commit latency p95/p99, device await, zpool iostat?
  • Чи пул здоровий, почищений scrub-ом і не виконує resilver?

FAQ

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

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

2) Чи потрібен мені SLOG для PostgreSQL?

Лише якщо затримка синхронних записів — ваше вузьке місце. Багато читально-важких або асинхронних навантажень не виграють багато.
Якщо потрібен — використовуйте PLP-захищені пристрої і міроруйте їх.

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

На критичних dataset-ах Postgres: ні. На справді відкиданих тимчасових dataset-ах: можливо, якщо ви свідомі втрати при падінні.
Якщо хочете менше довговічності для підмножини операцій — використовуйте ручки Postgres на кшталт synchronous_commit.

4) Який recordsize вибрати для даних Postgres?

Почніть з 16K. Розгляньте 8K для write-heavy OLTP, якщо вимірювання вказують на зменшення write amplification. Для аналітичних tablespaces з великими послідовними сканами — більший recordsize (64K–128K).
Окремі dataset-и полегшують це.

5) Чи повинен WAL бути в окремому dataset-і?

Так. Це дає цілеспрямоване налаштування і чисті операційні межі. Ізоляція продуктивності — бонус; головна вигода — зрозумілість.

6) Чи допомагає ZFS компресія базам даних?

Зазвичай так. LZ4 — загальноприйнятий дефолт, бо швидкий і малоризиковий. Він знижує фізичний I/O, на якому більшість баз фактично чекають.

7) Як snapshot-и взаємодіють із консистентністю PostgreSQL?

ZFS snapshot-и гарантують crash-consistency на рівні файлової системи. Для консистентних бекапів додатку координуйтеся з Postgres: використовуйте base backups,
архівування WAL або коректне призупинення. Snapshot-и — відмінна інфраструктура; вони не роблять магію консистентності самі по собі.

8) Чи варто вимикати atime?

Так для dataset-ів Postgres. atime-оновлення — зайві записи. Тримайте вимкненим, якщо лише у вас немає вимоги відповідності, що дійсно залежить від atime.

9) Mirrors чи RAIDZ для Postgres?

Mirrors коли важлива латентність (OLTP, багато комітів). RAIDZ коли важлива ефективність ємності і навантаження більш послідовне або толерантне до латентності.
Можна змішувати: тримати mirror vdev «fast lane» для WAL або гарячих даних і RAIDZ для холодніших даних, але будьте обережні з ускладненням.

10) Яка найпростіша безпечна конфігурація з гарною продуктивністю?

Mirror pool, compression=lz4, atime=off, data dataset з recordsize 16K, WAL dataset 16K, sync=standard.
Додавайте mirrored SLOG лише якщо затримкість комітів вимагає цього.

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

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

  1. Розділіть dataset-и принаймні на data і WAL. Проставте властивості свідомо.
  2. Виміряйте латентність комітів під навантаженням записів і корелюйте з zpool iostat та iostat -x.
  3. Якщо sync — вузьке місце, виправте його правильно: зеркальний PLP SLOG або цілеспрямовані зміни довговічності в Postgres — жодних файлових брехень.
  4. Тримайте компресію включеною і припиніть платити за atime.
  5. Зробіть scrub-и і моніторинг помилок регулярною практикою. «Немає інцидентів» — це не щастя; це виявлення плюс нудна дисципліна.

Мета — система, що не потребує експерта зі зберігання для щоденної роботи. ZFS може це забезпечити для PostgreSQL.
Але треба ставитись до семантики sync як до контракту, а не як до поради.

← Попередня
Пошкоджені мітки ZFS: правильне виправлення помилок імпорту
Наступна →
Резервні копії електронної пошти: тренування відновлення, яке потрібно провести (інакше ваші бекапи — фікція)

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