Zaawansowana optymalizacja systemu plików ZFS dla relacyjnych baz danych: Architektura, wydajność i integracja dla PostgreSQL oraz MySQL
Opracowanie poświęcone strojeniu stosu ZFS + PostgreSQL oraz ZFS + MySQL/MariaDB na poziomie jądra i konfiguracji silnika bazy danych. Opisujemy architekturę pamięci ARC i L2ARC, dobór urządzeń SLOG, eliminację zbędnych mechanizmów ochronnych (full_page_writes, doublewrite buffer), konfigurację io_uring oraz różnice wdrożeniowe między Linuksem a FreeBSD.
Wprowadzenie do ZFS w kontekście relacyjnych baz danych
Tradycyjne bazy danych, takie jak PostgreSQL czy MySQL (z silnikiem InnoDB), były przez dekady projektowane z myślą o klasycznych systemach plików pokroju ext4 czy XFS. Systemy te opierają się na mechanizmie nadpisywania danych w miejscu (ang. overwrite-in-place). Taka architektura wymuszała implementację dodatkowych mechanizmów chroniących przed uszkodzeniem plików w razie awarii zasilania. Do takich rozwiązań należą m.in. dzienniki transakcyjne (Write-Ahead Logs) oraz bufor podwójnego zapisu (doublewrite buffer).
ZFS (Zettabyte File System, rozwijany jako OpenZFS) opiera się na innym modelu – Copy-on-Write (CoW). Dane nigdy nie są modyfikowane w swoim starym sektorze. ZFS przydziela nowy blok, zapisuje zmodyfikowane informacje, a następnie atomowo aktualizuje wskaźniki w drzewie Merkle. Eliminuje to problem cichego uszkodzenia danych (silent data corruption), a przy okazji daje wbudowaną kompresję w locie i tworzenie migawek (snapshotów) bez obciążania systemu.
Nałożenie losowych operacji I/O typowych dla relacyjnej bazy danych na domyślnie skonfigurowany ZFS prowadzi do kolizji architektonicznych – amplifikacji zapisu, podwójnego buforowania i niekontrolowanego rozrostu metadanych w pamięci RAM. Poniżej opisujemy, jak dostroić ten stos do pracy produkcyjnej na poziomie jądra i aplikacji.
Pamięć podręczna ARC – architektura i samostrojenie
W przeciwieństwie do standardowego bufora stron (page cache) w systemie Linux, opartego na prostym algorytmie LRU (Least Recently Used), ZFS używa zaawansowanej pamięci podręcznej ARC (Adaptive Replacement Cache). Tradycyjne algorytmy są bardzo podatne na zjawisko wypłukiwania (cache thrashing) – np. wykonanie kopii zapasowej za pomocą pg_dump wczytuje całą bazę sekwencyjnie, usuwając z RAM-u krytyczne dla wydajności dane transakcyjne.
ARC rozwiązuje to, śledząc nie tylko czas ostatniego dostępu do danych, ale także jego częstotliwość. Pamięć dzieli się na dwie główne listy robocze i dwie wirtualne "listy widma":
- MRU (Most Recently Used): Przechowuje dane wczytane niedawno, ale rzadko powtarzające się.
- MFU (Most Frequently Used): Przechowuje "gorący" zbiór roboczy, o który baza pyta bez przerwy.
- GMRU (Ghost MRU) i GMFU (Ghost MFU): Przechowują jedynie sygnatury i indeksy usuniętych bloków.
Całkowity docelowy rozmiar pamięci podręcznej ARC to w algorytmie zmienna C. Pamięć ta jest dzielona pomiędzy sekcje MRU i MFU ruchomą granicą oznaczaną jako P. System ZFS stroi się sam: jeśli aplikacja żąda danych, które zostały już usunięte z głównej pamięci, ale ich sygnatura znajduje się na liście GMRU (jest to tzw. trafienie widmo), system wie, że sekcja MRU była za mała. Adaptacyjnie zwiększa więc parametr P, poszerzając MRU kosztem obszaru MFU. Ten ciągły matematyczny balans uodparnia ZFS na duże skany sekwencyjne.
Zarządzanie pamięcią RAM w kontekście baz danych
Domyślnie ZFS dąży do zajęcia niemal całego dostępnego RAM-u serwera. Kiedy silnik bazy danych, posiadający własne bufory (shared_buffers w PostgreSQL, innodb_buffer_pool_size w MySQL), zażąda dużej alokacji pamięci, jądro Linuksa może uruchomić OOM Killer i zabić proces bazy.
Aby tego uniknąć, należy wymusić ścisłe ograniczenia dla pamięci ARC za pomocą parametrów zfs_arc_max oraz zfs_arc_min. Konkretny podział zależy od profilu obciążenia. W środowiskach PostgreSQL, gdzie shared_buffers powinien mieć co najmniej 25% RAM, rozsądnym punktem wyjścia jest przydzielenie ok. 40–50% fizycznej pamięci na zfs_arc_max. W konfiguracjach MySQL z dużym innodb_buffer_pool_size proporcje mogą być odwrotne. Niezależnie od podziału, bezwzględnie trzeba ustawić twardy limit zfs_arc_max i zostawić margines na work_mem, pule połączeń oraz jądro systemu.
Optymalizacja L2ARC i narzut na metadane (wskaźniki)
Gdy rozbudowa pamięci RAM staje się niemożliwa lub nieopłacalna ekonomicznie, ZFS oferuje mechanizm L2ARC. Pozwala on wykorzystać szybkie dyski SSD/NVMe jako przedłużenie pamięci operacyjnej. Ponieważ jest to pamięć cache służąca wyłącznie do odczytu, ewentualna awaria tego dysku w żaden sposób nie grozi utratą bazy danych.
Narzut wskaźników L2ARC na pamięć operacyjną
Dodanie dużego dysku L2ARC bez wcześniejszej analizy kosztów metadanych to częsty błąd wdrożeniowy. ZFS organizuje dane w hierarchiczne drzewa wskaźników blokowych (block pointer trees), weryfikowane sumami kontrolnymi na każdym poziomie (drzewo Merkle). Każdy blok danych zrzucony z pamięci operacyjnej na dysk L2ARC musi zachować swoje odzwierciedlenie w głównej pamięci RAM w postaci nagłówka. Na systemach 64-bitowych nagłówek ten zajmuje orientacyjnie 70–200 bajtów, w zależności od wersji OpenZFS i rozmiaru bloku. Podłączenie wieloterabajtowego dysku SSD do serwera z małą ilością RAM spowoduje, że same nagłówki zajmą całą dostępną pamięć ARC, spowalniając bazę zamiast ją przyspieszać.
Zjawisko to jest silnie sprzężone z ustawieniem geometrii plików (parametrem recordsize). Zależności przetestowane w chmurze AWS (na bazie testowej o wielkości 850 GB) wykazują przybliżone koszty utrzymania drzewa wskaźników w pamięci w zależności od rozmiaru bloku:
- Przy
recordsize=128KB(domyślnie): Narzut na metadane w ARC to zaledwie 0.1% rozmiaru bazy. - Przy
recordsize=64KB: Narzut wynosi 0.2%. - Przy
recordsize=32KB: Narzut rośnie do 0.4%. - Przy
recordsize=16KB: Koszt wzrasta do 0.8% (co przekłada się na blisko 7 GB samego narzutu nagłówków w pamięci RAM dla bazy rzędu 800 GB).
Integracja dysków efemerycznych (Cloud Ephemeral Storage)
Serwery w usługach chmurowych (np. AWS z rodziny i3, Google Cloud, Azure) oferują tzw. dyski efemeryczne (local NVMe). Charakteryzują się one opóźnieniami rzędu ułamków milisekund i brakiem limitów IOPS narzucanych na sieciowe wolumeny EBS. Ponieważ dane na takich dyskach giną bezpowrotnie przy restarcie maszyny, nie nadają się one do trzymania głównej bazy, ale stanowią doskonałe rozwiązanie dla bufora L2ARC.
Ponieważ L2ARC kompresuje bufory (np. algorytmem LZ4) w locie, dysk NVMe o pojemności 75 GB potrafi zbuforować bazę ponad trzykrotnie większą od swojej nominalnej pojemności. Aby zmusić ZFS do szybkiego zapełnienia tego dysku (przełamując domyślne, zachowawcze limity chroniące SSD przed zużyciem), należy zmodyfikować parametry jądra w /sys/module/zfs/parameters/:
l2arc_write_boost=134217728: Umożliwia agresywne zapisywanie danych tuż po rozruchu systemu.l2arc_write_max=67108864: Zwiększa stały górny limit zasilania dysku NVMe w megabajtach na sekundę.
Jeśli przy bardzo intensywnym obciążeniu zapisem (OLTP) wątek ładujący dane (l2arc_feed) przestaje nadążać z ich przerzucaniem na dysk NVMe, zaleca się skryptowe "podgrzewanie" cache'u (np. cat /var/lib/mysql/data/* > /dev/null) w momentach mniejszego ruchu.
Synchroniczność i ZIL/SLOG: Latency kontra Throughput
Silniki relacyjnych baz danych polegają na komendzie fsync, aby zagwarantować spójność transakcyjną (spełnienie założeń ACID). W przypadku ZFS, takie synchroniczne żądania wymuszają natychmiastowy zapis do struktury ZFS Intent Log (ZIL).
Gdy ZIL znajduje się na tych samych dyskach co pliki danych, baza zaczyna konkurować o zasoby I/O z resztą operacji systemowych. Warunkiem koniecznym profesjonalnego wdrożenia jest instalacja dedykowanego urządzenia dla logów, czyli SLOG (Separate Log Device).
Zasada projektowania SLOG: W przypadku urządzenia SLOG liczy się wyłącznie fizyczne opóźnienie (Latency), podczas gdy całkowita przepustowość (Throughput) ma znaczenie marginalne. Przykładowo, konsumencki dysk o dużej przepustowości (np. Samsung 960 Evo) podczas synchronicznych zrzutów może notować skoki opóźnień do 460 mikrosekund. Z kolei dyski korporacyjne na szynie PCIe (np. technologia Intel Optane) utrzymują stabilne opóźnienia na poziomie zaledwie 16 mikrosekund, co bezpośrednio przekłada się na wyższą liczbę transakcji na sekundę (TPS).
Zabezpieczenie sprzętowe: Dysk pełniący rolę SLOG musi kategorycznie posiadać zabezpieczenie przed utratą zasilania (Power-Loss Protection – PLP) oparte o dedykowane kondensatory. Brak PLP przy nagłej awarii serwera doprowadzi do utraty zbuforowanych logów transakcji, co nieuchronnie uszkodzi struktury indeksowe bazy danych.
Atrybut systemowy logbias
Parametr logbias instruuje jądro ZFS, jak należy realizować zapisy wywołane przez fsync:
logbias=latency(ustawienie domyślne): Kieruje strumień zapisów natychmiast na szybki dysk SLOG, minimalizując opóźnienia i błyskawicznie odblokowując gniazdo bazy danych.logbias=throughput: ZFS całkowicie omija użycie dysku SLOG dla danego zestawu danych. Łączy zmiany w wielkie paczki i zapisuje je bezpośrednio na główny wolumen z maksymalną przepustowością. Jest to ustawienie idealne dla procesów hurtowych (np. zrzutów kopii zapasowych) i chroni dyski SSD przed zbędnymi podwójnymi zapisami.
Problem geometrii (Recordsize) i Write Amplification
ZFS posługuje się dużymi blokami alokacji, domyślnie o rozmiarze 128 KB. Dla kontrastu, silnik InnoDB w środowisku MySQL pracuje na stronach o rozmiarze 16 KB, a PostgreSQL modyfikuje jednorazowo zaledwie 8 KB danych.
Modyfikacja 8 KB informacji w pliku zapisanym w rekordzie o rozmiarze 128 KB zmusza system ZFS (w architekturze Copy-on-Write) do odczytania całych 128 KB, przeliczenia sum kontrolnych w drzewie Merkle, zaktualizowania potrzebnych 8 KB i ostatecznego zapisania z powrotem paczki 128 KB w nowym sektorze dysku (proces ten określa się jako read-modify-write). Zjawisko to nosi nazwę amplifikacji zapisu (Write Amplification). Znacznie obniża ono przepustowość transakcyjną i przyspiesza fizyczne zużycie nośników flash (SSD).
Historycznie zalecano bezwzględne wyrównywanie rozmiarów – np. stosowanie recordsize=8k dla PostgreSQL. Taka modyfikacja faktycznie eliminowała amplifikację zapisu, jednak mocno obniżała skuteczność kompresji. Algorytmy kompresujące (takie jak LZ4 czy nowoczesne ZSTD obsługujące instrukcje wektorowe) muszą posiadać odpowiedni bufor danych w pojedynczym bloku, aby znaleźć powtarzające się wzorce. Zmniejszenie rekordu do zaledwie 8 KB praktycznie uniemożliwia efektywną kompresję.
Biorąc pod uwagę rosnący narzut metadanych przy małych blokach (opisany szczegółowo w sekcji dotyczącej ARC), współczesnym inżynieryjnym kompromisem dla systemów transakcyjnych jest ustawienie rekordu rzędu 32K. Pozwala to zachować natywną wydajność kompresji, przy jednoczesnym utrzymaniu narzutu metadanych na bezpiecznym poziomie. Należy pamiętać, że nowa wartość recordsize obowiązuje wyłącznie dla nowo zapisywanych bloków – zastosowanie tej zmiany do istniejącej bazy danych wymaga jej skopiowania lub odtworzenia przestrzeni mechanizmem zfs send/receive.
Kompleksowa optymalizacja implementacji PostgreSQL na ZFS (PoZoL)
Konfigurowanie i utrzymywanie serwerów PostgreSQL na systemie ZFS on Linux stworzyło odłam inżynierii często określany skrótem "PoZoL". Największy przyrost wydajności w tym scenariuszu uzyskuje się poprzez staranną dezaktywację mechanizmów bazy danych, które dublują funkcje realizowane już przez kernel systemu operacyjnego.
Zjawisko Torn Page i kluczowy przełącznik full_page_writes
Aby zapobiec uszkodzeniom typu "rozerwana strona" (ang. torn page – występuje, gdy zasilanie ulega awarii w trakcie zapisu 8-kilobajtowej strony bazy danych na fizyczny nośnik z sektorem 4K), PostgreSQL stosuje własny mechanizm asekuracyjny. Parametr full_page_writes=on nakazuje silnikowi bazy przy pierwszej modyfikacji kopiować cały, 8-kilobajtowy, surowy obraz strony bezpośrednio do logów operacyjnych WAL. Generuje to ogromny, zbędny ruch na dysku i widocznie ogranicza liczbę transakcji na sekundę (TPS).
Architektura Copy-on-Write systemu ZFS rozwiązuje ten problem całkowicie – nowy blok jest uznawany za poprawny dopiero po jego całkowitym i bezbłędnym zapisie. Jeśli zapis zostanie przerwany, ZFS po prostu utrzymuje wskaźnik do idealnej, starszej kopii danych. Zjawisko rozerwanej strony jest na ZFS fizycznie niemożliwe.
W postgresql.conf należy więc ustawić full_page_writes = off. Odciąża to dyski i eliminuje opóźnienia I/O (I/O stalls). Zastrzeżenie: zmiany tej nie należy wprowadzać, jeśli w systemie wymuszono mały rozmiar bloku (recordsize < 8K), lub jeśli instancja bazy mogłaby zostać w przyszłości zreplikowana z użyciem polecenia rsync na standardowy system plików, taki jak ext4.
Kompromisem jest nieco dłuższy czas odbudowy bazy po awarii (crash recovery). Począwszy od PostgreSQL 15 łagodzi to mechanizm recovery_prefetch (eksperymentalnie dostępny od wersji 14). W połączeniu ze zmienną maintenance_io_concurrency wyprzedza on odczyt, wykorzystując posix_fadvise do asynchronicznego pobierania brakujących bloków z wyprzedzeniem.
Rozdzielenie dzienników WAL od tabel
Powszechnie rekomendowaną praktyką jest utworzenie oddzielnych zbiorów danych (zpools/datasets) z odpowiednio dobranymi parametrami:
| Zbiór ZFS | Uzasadnienie techniczne i konfiguracyjne |
|---|---|
| pg_data (Tabele) | Geometria bloku (recordsize) ustawiona na wartość kompromisową, np. 32K (lub 16K). Włączona kompresja sprzętowa (LZ4/ZSTD) i najczęściej zachowane domyślne wsparcie dla opóźnień: logbias=latency. |
| pg_wal (Logi WAL) | Środowisko typu "append-only" (tylko do dopisywania). Rekomenduje się tu zachowanie dużego rekordu np. 128K. Zmienną logbias należy zostawić przypiętą do modułu SLOG (latency). Warto także zlecić całkowitą kompresję systemowi ZFS, zdejmując ten ciężar z natywnych opcji bazy. |
Należy również zredukować inne mechanizmy dublujące funkcje ZFS:
- Sumy kontrolne (Checksumming): Zaleca się ich całkowitą dezaktywację po stronie bazy. Drzewa Merkle w ZFS znacznie szybciej i bez błędów wyliczają kryptograficzną spójność każdego bloku danych.
- Opcje plików WAL: Wyłączenie recyklingu:
wal_init_zero = offiwal_recycle = off. ZFS w locie obsługuje tak zwane zero-bloki (zero-filled blocks), a sztuczne wymuszanie przez PostgreSQL nadpisywania pustych plików degraduje tylko wydajność dysku. - Metoda synchronizacji: Optymalizacją jest zmiana parametru
wal_sync_methodna wywołaniefdatasync(w pełni wspierane i bezpieczne pod warunkiem posiadania sprzętowego dysku SLOG z ochroną PLP). Pulashared_bufferszdefiniowana dla instancji PostgreSQL nie powinna też zbyt agresywnie konkurować o pamięć z cache'em ARC.
Bug #16936 – uszkodzenie danych przy replikacji strumieniowej pg_wal na ZFS
W klastrach wysokiej dostępności (HA) opartych na PostgreSQL 15+ w połączeniu z OpenZFS 2.1.x lub 2.2.x zidentyfikowano usterkę śledzoną w rejestrze projektu jako Bug #16936. Na dzień publikacji tego artykułu problem pozostaje otwarty i nie doczekał się oficjalnej poprawki w OpenZFS.
Błąd objawia się na replice komunikatami w rodzaju invalid magic number 0000 in log segment lub incorrect resource manager data checksum. Prowadzi to do zamknięcia procesu walreceiver i uszkodzenia podrzędnej instancji bazy.
Przyczyną jest wyścig (race condition) między procesem walsender a warstwą ZFS. Choć wywołanie fsync/fdatasync na stronie WAL zwraca sukces, dane nie są jeszcze w pełni spójne do odczytu przez inny proces. walsender czyta stronę WAL zanim ZFS zdąży fizycznie zatwierdzić bloki metadanych na dysku, w efekcie czego replika odbiera uszkodzony strumień. Problem nasila się na nowszych jądrach (Rocky 9, kernel 5.14+) i przy mniejszych wartościach recordsize.
Obejście: Jedynym sprawdzonym sposobem jest tymczasowe przejście z ciągłej replikacji strumieniowej TCP na plikowe dostarczanie archiwów WAL (WAL shipping, np. przez rsync lub pg_receivewal na ext4). Opóźnienie rzędu pojedynczych sekund wystarcza, by zamknięte pliki archiwalne trafiły na replikę wolne od błędu wyścigu.
Optymalizacja strukturalna silnika InnoDB (MySQL / MariaDB)
Konfigurowanie relacyjnych baz z rodziny MySQL (oraz forków, takich jak MariaDB czy Percona XtraDB), wykorzystujących mechanizm InnoDB na systemie ZFS, wymaga rozwiązania analogicznych problemów powielanego I/O.
Bufor podwójnego zapisu (Doublewrite Buffer)
Doublewrite to odpowiedź twórców MySQL na ten sam problem ("torn page"). Kiedy zmodyfikowana w pamięci strona danych ma zostać zapisana do głównego pliku (.ibd), InnoDB najpierw awaryjnie kieruje jej zrzut do specjalnego, oddzielnego obszaru sekwencyjnego na dysku. Dopiero po upewnieniu się, że ta operacja zakończyła się sukcesem, silnik nadpisuje docelową, ostateczną tabelę z danymi. Praca ta zużywa nawet 100% dostępnej przepustowości I/O bez przynoszenia realnych korzyści wydajnościowych.
Ponieważ mechanika Copy-on-Write na ZFS eliminuje ryzyko istnienia w połowie nadpisanej, uszkodzonej strony, inżynierowie powinni stanowczo wyłączyć tę blokadę bezpieczeństwa. W pliku my.cnf należy zdefiniować parametr innodb_doublewrite = 0. Redukuje to nadwyżkowe cykle zapisu, podnosząc u bazy przepustowość TPS blisko dwukrotnie.
Architektura operacyjna Native AIO i io_uring
Asynchroniczne zapytania jądra (tzw. native AIO) są rutynowo polecane dla implementacji baz danych operujących na systemach pokroju ext4. Implementacja AIO w module ZFS na Linuksie opiera się jednak o warstwę kompatybilności (shim layer), która de facto jedynie symuluje asynchroniczność. Stanowi to wąskie gardło prowadzące do dławienia zapytań I/O i blokowania instancji bazy.
W praktyce problem ten rozwiązuje się na kilka sposobów, w zależności od wersji silnika bazy danych i środowiska uruchomieniowego:
- W klasycznych wdrożeniach radzono sobie z tym poprzez wyłączenie natywnego AIO w konfiguracji (
innodb_use_native_aio = 0, a w przypadku MariaDB/Percona równieżinnodb_use_atomic_writes = 0) i zwiększenie liczby wątków roboczych bazy (innodb_read_io_threadsorazinnodb_write_io_threads). - Wdrożenie standardu io_uring (MariaDB 10.11+ / 12.0+): W nowszych wersjach MariaDB wprowadzono nowoczesny wskaźnik flagi jądra
innodb_linux_aio. Jeśli uruchomiony w trybieauto, silnik uaktywnia wsparcie dla wysoce zoptymalizowanych operacji w module Linuksaio_uring, omijając symulację i znacząco podnosząc wydajność asynchroniczną. - Restrykcje środowisk kontenerowych: Uruchamianie nowego silnika
io_uringwewnątrz izolacji Dockera może generować błąd pamięci systemowej, objawiający się komunikatemmariadbd: io_uring_queue_init() failed with ENOMEM. Aby obejść restrykcje pamięciowe nałożone na kontener, należy na poziomie konfiguracji hosta zdefiniować wyższy limit blokowanej pamięci (np. argumentmemlock: "262144"w Docker Compose lub parametrLimitMEMLOCKkonfiguracji systemd). Jeśli rezerwa nie zostanie przydzielona, baza awaryjnie powróci do klasycznego trybu operacji.
Architektura ZFS dla bazy MySQL
| Parametr / Właściwość | /var/lib/mysql/data (Dane Operacyjne) | /var/lib/mysql/log (Dzienniki Redo) |
|---|---|---|
| Geometria (recordsize) | Wyrównana do rozmiaru strony InnoDB (domyślnie 16 KB), zalecane 16K. | Duże domyślne bloki 128K, wspierające szybki sekwencyjny przydział nowych danych. |
| Priorytetyzacja dyskowa (logbias) | Zabezpieczone ustawieniem throughput w celu ochrony dysku SLOG przed przeciążeniem przy masowych aktualizacjach rzędów. |
Ustawienie priorytetu latency, wymuszające użycie sprzętowego dysku SLOG w celu błyskawicznego potwierdzania logów Redo. |
| Konfiguracja Bazy | Ustawienie innodb_flush_neighbors = 0 zapobiega niepotrzebnemu grupowaniu odczytów dla sektorów charakterystycznych dla tradycyjnych dysków HDD. |
Parametr bufora innodb_log_write_ahead_size=16384 oraz metoda zrzutu danych innodb_flush_method=fsync. |
Zależności jądra w systemie Linux (POSIX): Atrybuty rozszerzone (xattr), atime oraz wyrównanie sektorowe (ashift)
Często pomijanym aspektem przy wdrażaniu ZFS w ekosystemie Linuksa jest sposób zarządzania atrybutami rozszerzonymi. ZFS, dbając o kompatybilność ze starszymi standardami, domyślnie używa atrybutów rozszerzonych (Extended Attributes – xattr) w archaiczny i niewydajny sposób przy obsłudze list POSIX ACL.
Przy domyślnym ustawieniu xattr=on, rozszerzone atrybuty (takie jak uprawnienia POSIX ACL) są zapisywane jako zwykłe, ukryte pliki w dodatkowych katalogach, co spowalnia system poprzez wymuszanie wielokrotnych operacji wyszukiwania (seeks) na dysku przy każdym odczycie. Oznacza to, że zaawansowane uprawnienia oraz dodatkowe parametry bazy są przez system odseparowane od samego węzła pliku (inode) i umieszczone w wirtualnych katalogach podrzędnych. Każde zapytanie aplikacji do takiego pliku wymusza na głowicy dysku lub sterowniku NVMe wykonanie niezależnej operacji wyszukiwania. Generuje to zauważalne opóźnienia, niwecząc przewagę, jaką daje szybka pamięć ARC.
Rozwiązaniem jest rekonfiguracja zbiorów danych komendą zfs set xattr=sa (System Attributes). Tryb SA osadza atrybuty POSIX wprost w strukturze inode, eliminując konieczność odpytywania ukrytych plików. Dokumentacja OpenZFS 2.2 wprost zaleca to ustawienie dla obciążeń bazodanowych i udziałów SMB, gdzie operacje sprawdzania list ACL są częste. Wraz z xattr=sa warto ustawić acltype=posixacl dla pełnej kompatybilności. Zmiana xattr dotyczy wyłącznie nowych plików – istniejące dane zaktualizują atrybut dopiero przy operacjach zfs send/recv.
Kolejną kluczową optymalizacją na warstwie metadanych jest wyłączenie automatycznej archiwizacji czasów dostępu poprzez nadanie flagi: zfs set atime=off. Dzięki temu odciążamy dyski SSD, eliminując zbędne cykle zapisu wynikające z samych odczytów i skanowania danych przez system bazodanowy.
W kwestiach sprzętowych najważniejszym parametrem przed utworzeniem puli na nowych nośnikach (np. macierze NVMe), jest właściwe określenie wyrównania alokatora wielkości fizycznego sektora, określanego zmienną ashift. Format używany powszechnie w nowoczesnych SSD (Advanced Format) wymaga dostosowania ZFS do pracy na sektorach 4K, co definiuje się komendą ashift=12. Utworzenie puli z domyślnym, starym przesunięciem dla dysków 512-bajtowych na nowoczesnym nośniku NVMe 4K wygeneruje amplifikację zapisu na poziomie sprzętowym (hardware write amplification). Spowoduje to poważny spadek wydajności, którego nie da się zrekompensować żadnymi późniejszymi zmianami parametrów recordsize czy powiększaniem buforów pamięci.
Specyfika FreeBSD i porównanie wydajności z systemem Linux
ZFS wywodzi się z ekosystemu Solarisa, a FreeBSD był pierwszym wolnym systemem, który go adoptował. Wiele z opisanych wcześniej problemów specyficznych dla Linuksa nie występuje na FreeBSD w ten sam sposób – różnice wynikają z odmiennej architektury jądra i głębszej integracji ZFS z systemem.
Sposób wprowadzania parametrów jądra (Sysctl vs Sysfs)
W systemie Linux parametry sterujące pamięcią ARC czy L2ARC (np. l2arc_write_max, l2arc_write_boost) modyfikuje się poprzez ścieżki w wirtualnym systemie plików /sys/module/zfs/parameters/. Natomiast we FreeBSD zarządza się nimi za pomocą mechanizmu sysctl (lub wpisów w /boot/loader.conf i /etc/sysctl.conf). Przykładowo, limitowanie L2ARC odbywa się przez zmienne takie jak vfs.zfs.l2arc_write_max czy vfs.zfs.l2arc_write_boost, a maksymalny rozmiar bloku kontroluje się poprzez vfs.zfs.max_recordsize.
Architektura list kontroli dostępu (ACL) i atrybutów (xattr)
Cała wcześniejsza sekcja raportu mówiąca o problemach z POSIX ACL i konieczności stosowania komend xattr=sa oraz acltype=posixacl dotyczy wyłącznie specyfiki Linuksa. FreeBSD natywnie nie wspiera list POSIX ACL na ZFS. Zamiast tego domyślnie używa zaawansowanych list w standardzie NFSv4 (acltype=nfsv4), a we współczesnych wersjach OpenZFS na FreeBSD atrybuty te i tak trafiają do przestrzeni System Attributes (SA). Dzięki temu optymalizacja ta rozwiązuje się w dużej mierze automatycznie.
Asynchroniczne I/O (AIO) w MySQL/MariaDB
W kontekście asynchronicznego I/O, ostrzeżenie przed modułem AIO w jądrze (i zalecenie innodb_use_native_aio=0) dotyczy bezpośrednio Linuksa, gdzie sterownik ZFS działa przez problematyczną warstwę kompatybilności (tzw. compatibility shim). We FreeBSD ten problem z architekturą warstwy pośredniej nie występuje w ten sam sposób, a w plikach konfiguracyjnych baz danych często oznacza się dezaktywację AIO z dopiskiem "tylko dla Linuksa". Interfejs io_uring jest z kolei technologią wbudowaną wyłącznie w jądro Linux.
Agresywny Prefetching
W środowiskach FreeBSD wdrażających bazy danych często dodaje się rekomendację całkowitego wyłączenia sprzętowego przewidywania odczytów (ZFS prefetching). Realizuje się to za pomocą zmiennej vfs.zfs.prefetch.disable=1, co poprawia czas odpowiedzi systemu pod obciążeniem bazodanowym.
Podsumowanie wydajności: FreeBSD czy Linux?
Mimo historycznego związku ZFS z FreeBSD, nie można jednoznacznie wskazać go jako lepszego wyboru pod bazy danych. Oba systemy mają swoje mocne strony, a wynik zależy od profilu obciążenia.
FreeBSD często oferuje wyższą wydajność surowych operacji I/O bez dodatkowego strojenia. System ten domyślnie jest zoptymalizowany pod pracę serwerową, podczas gdy Linux wymaga ręcznych zmian (planista I/O, tryby oszczędzania energii procesora), aby osiągnąć porównywalny czas odpowiedzi.
Benchmarki pokazują jednak mieszany obraz w zależności od rodzaju obciążenia:
- O ile FreeBSD wygrywa w surowym I/O, o tyle nierzadko radzi sobie gorzej od Linuksa przy aplikacyjnych operacjach zapisu na wyższym poziomie (np. FWrite).
- W niektórych testach przeprowadzonych w środowiskach chmurowych (takich jak instancje AWS) zaobserwowano, że PostgreSQL działający na FreeBSD potrafił działać wolniej w scenariuszach intensywnego odczytu w porównaniu do serwerów z systemem Ubuntu.
- Linux dysponuje interfejsem
io_uring, który daje mu przewagę w obciążeniach bazodanowych wymagających intensywnego asynchronicznego I/O (co ma duże znaczenie np. dla nowszych wersji MySQL/MariaDB).
Wybór zależy przede wszystkim od doświadczenia zespołu wdrażającego. FreeBSD oferuje natywne wsparcie ZFS i dobrą wydajność I/O od razu po instalacji. Linux po odpowiednim dostrojeniu potrafi działać równie dobrze (lub lepiej w niektórych metrykach), a duża społeczność i szybszy cykl wprowadzania nowych interfejsów (io_uring, eBPF) sprawiają, że pozostaje dominującym wyborem w komercyjnych wdrożeniach.
Zakończenie
Domyślna konfiguracja ZFS nie jest przygotowana na losowe I/O relacyjnych baz danych. Opisane wyżej zmiany – ograniczenie ARC, wydzielenie SLOG, dobór recordsize, eliminacja podwójnych zapisów i prawidłowe ustawienie xattr/ashift – pozwalają zbudować stos, który łączy integralność danych gwarantowaną przez CoW z wydajnością wymaganą w środowisku produkcyjnym.
W WebOptimo specjalizujemy się w administracji serwerami Linux i FreeBSD oraz optymalizacji wydajności baz danych PostgreSQL i MySQL. Jeśli planujesz wdrożenie ZFS w środowisku produkcyjnym lub potrzebujesz audytu istniejącej infrastruktury – skontaktuj się z nami. Sprawdź również nasze usługi administracji serwerem, administracji Linux, administracji FreeBSD, administracji PostgreSQL oraz administracji MySQL.
FAQ – ZFS i bazy danych
Tak, pod warunkiem odpowiedniego dostrojenia. Domyślna konfiguracja ZFS jest zoptymalizowana pod ogólne obciążenia plikowe, a nie pod losowe operacje I/O typowe dla baz danych. Po prawidłowym ustawieniu parametrów recordsize, limitów pamięci ARC, dedykowanego urządzenia SLOG oraz wyłączeniu zbędnych mechanizmów ochronnych (takich jak full_page_writes w PostgreSQL czy innodb_doublewrite w MySQL) – ZFS zapewnia wyższą integralność danych i porównywalną lub lepszą wydajność niż ext4/XFS.
Współczesnym kompromisem inżynieryjnym jest recordsize=32K dla danych transakcyjnych obu silników. Wartość ta zachowuje skuteczność kompresji (LZ4/ZSTD) przy umiarkowanym narzucie metadanych w pamięci ARC. Dla dzienników WAL (PostgreSQL) i Redo Log (MySQL) rekomenduje się zachowanie domyślnego recordsize=128K, ponieważ są to strumienie sekwencyjne. Nowy recordsize obowiązuje wyłącznie dla nowo zapisywanych bloków – zmiana wymaga odtworzenia danych przez zfs send/receive.
Tak. Mechanizm Copy-on-Write w ZFS eliminuje problem rozerwanej strony (torn page), który jest jedynym powodem istnienia parametru full_page_writes. Wyłączenie go (full_page_writes = off w postgresql.conf) odciąża magistralę dyskową i podnosi TPS. Zastrzeżenie: nie należy tego robić, jeśli recordsize jest mniejszy niż 8K lub jeśli instancja może być replikowana za pomocą rsync na system plików typu ext4.
Podział zależy od profilu obciążenia. Dla PostgreSQL rozsądnym punktem wyjścia jest ok. 40–50% RAM na zfs_arc_max, reszta na shared_buffers, work_mem i jądro. Dla MySQL z dużym innodb_buffer_pool_size proporcje mogą być odwrotne. Bezwzględnie trzeba ustawić twardy limit zfs_arc_max – bez niego ZFS będzie dążył do zajęcia całego RAM-u, co może prowadzić do zabijania procesów bazy przez OOM Killer.
Bezwzględnie tak. Dysk pełniący rolę SLOG (Separate Log Device) buforuje logi transakcji przed ich zatwierdzeniem na główny wolumen. Brak dedykowanych kondensatorów zabezpieczających (Power-Loss Protection) oznacza, że nagła awaria zasilania doprowadzi do utraty zbuforowanych transakcji i uszkodzenia struktur indeksowych bazy danych. Dyski konsumenckie (nawet szybkie NVMe) nie nadają się do roli SLOG w środowiskach produkcyjnych.
Główne różnice dotyczą trzech obszarów. Po pierwsze, parametry jądra – na Linuksie modyfikuje się je przez /sys/module/zfs/parameters/, a na FreeBSD przez sysctl i /boot/loader.conf. Po drugie, atrybuty rozszerzone – na Linuksie konieczne jest ręczne ustawienie xattr=sa i acltype=posixacl dla wydajności, podczas gdy FreeBSD domyślnie używa NFSv4 ACL z osadzaniem w System Attributes. Po trzecie, asynchroniczne I/O – interfejs io_uring to technologia wyłącznie Linuksa, a ostrzeżenia przed wadliwym modułem AIO na ZFS dotyczą przede wszystkim implementacji linuxowej.
Tak, i jest to jedno z najbardziej efektywnych kosztowo rozwiązań. L2ARC to pamięć podręczna służąca wyłącznie do odczytu – awaria lub utrata dysku efemerycznego nie powoduje utraty danych. Lokalne dyski NVMe w instancjach chmurowych (np. AWS i3) oferują opóźnienia rzędu ułamków milisekund bez limitów IOPS typowych dla wolumenów sieciowych. Dzięki kompresji LZ4 dysk 75 GB potrafi zbuforować bazę ponad trzykrotnie większą. Należy jednak pamiętać o narzucie metadanych – każdy buforowany blok zajmuje około 80 bajtów w głównej pamięci ARC.