fio ZFS pour bases de données : tester les écritures synchrones sans se mentir

Cet article vous a aidé ?

Votre base de données fonctionnait « correctement » en staging. Puis la production est arrivée avec de vrais utilisateurs, de la vraie latence,
et le type d’amplification d’écriture qui transforme des benchmarks confiants en fan fiction de performance.

Si vous exécutez ZFS sous une base de données, les écritures synchrones sont le lieu de la vérité. C’est aussi là que la plupart des tests fio
trichent accidentellement : mauvais flags, mauvaises hypothèses, mauvais chemin I/O, garanties de durabilité mal comprise. C’est un guide de terrain
pour exécuter fio contre ZFS comme si vous vous souciez vraiment que les données survivent à une coupure de courant.

Ce que signifient réellement les « écritures synchrones » sur ZFS (et pourquoi les bases de données s’en soucient)

Les bases de données ne veulent pas des « écritures rapides ». Elles veulent des écritures committées. Quand une base de données dit « transaction commise »,
elle fait une promesse : si la machine perd l’alimentation immédiatement après, les données commises sont toujours là au redémarrage.

Cette promesse s’implémente par un petit ensemble de comportements :
fsync(), fdatasync(), O_DSYNC, O_SYNC, et parfois la sémantique FUA
dans la pile de stockage. La plupart de la durabilité des bases de données revient à une exigence : l’enregistrement du journal est sur un stockage stable
avant que l’« OK » ne retourne au client.

ZFS complique cela de façon positive (modèle de cohérence fort) et de façon déroutante (copy-on-write + groupes de transactions + journal d’intention).
ZFS peut accepter les écritures en mémoire et les commettre plus tard dans un TXG (transaction group). C’est l’I/O tamponnée normale. Mais quand une
application demande des sémantiques synchrones, ZFS doit s’assurer que l’écriture est durable avant de répondre.

Entrez le ZIL (ZFS Intent Log). Le ZIL n’est pas un « cache d’écriture » au sens usuel ; c’est un mécanisme pour satisfaire rapidement les requêtes synchrones
sans forcer un commit complet du pool immédiatement. Si le système plante, les enregistrements ZIL sont rejoués à l’import pour ramener le système de fichiers
à un état cohérent incluant ces opérations synchrones.

Le ZIL vit normalement sur le pool lui-même. Un SLOG (dispositif de log séparé) est un dispositif dédié optionnel qui contient les enregistrements ZIL.
Bien fait, un SLOG transforme la « latence d’écriture synchrone » en « latence du dispositif SLOG ». Mal fait, il transforme « base durable » en « j’espère que l’onduleur fait son travail ».

Votre travail lors des tests est de mesurer :
la latence et les IOPS sous les sémantiques synchrones qui correspondent à la base,
et de le faire en vous assurant que vous ne testez pas accidentellement la RAM, le cache de page, ou un réglage de durabilité différent de la prod.

Faits & contexte : comment on en est arrivé là

  • Fait 1 : ZFS est né chez Sun Microsystems au milieu des années 2000 avec un accent sur l’intégrité des données de bout en bout (sommation de contrôle partout), pas sur les « chiffres fio maximaux ».
  • Fait 2 : Les systèmes de fichiers traditionnels comptaient souvent sur le cache write-back et un journal ; ZFS utilise le copy-on-write plus des commits transactionnels (TXG), ce qui change le comportement de la « latence d’écriture ».
  • Fait 3 : Le ZIL existe spécifiquement pour rendre les opérations synchrones rapides sans forcer un sync TXG complet à chaque requête de type fsync.
  • Fait 4 : Le stockage d’entreprise ancien livrait souvent des caches d’écriture avec batterie ; aujourd’hui, les disques NVMe avec protection contre la perte de puissance (PLP) sont l’analogue grand public le plus proche pour un journal de sync sûr et bas-latence.
  • Fait 5 : Les bases de données ont historiquement optimisé autour des disques tournants, où la journalisation séquentielle régnait ; sur SSD/NVMe, la variance de latence (tail latency) devient le vrai tueur.
  • Fait 6 : Les API I/O asynchrones de Linux ont évolué séparément (libaio, io_uring), et fio peut exercer de nombreux chemins code—dont certains ne correspondent pas à la façon dont votre base écrit son WAL/redo log.
  • Fait 7 : « Cache d’écriture activé » sur un disque est un piège depuis l’avènement de SATA ; ça peut améliorer les benchmarks tout en détruisant silencieusement les garanties de durabilité en cas de coupure de courant.
  • Fait 8 : Les propriétés de dataset ZFS comme sync, recordsize, logbias, et primarycache peuvent modifier matériellement la performance sans changer le code applicatif—grand pouvoir, grande propension à se tirer une balle dans le pied.

Modèle mental ZFS pour les personnes DBA : TXG, ZIL, SLOG et mensonges

TXG : le métronome derrière votre charge d’écriture

ZFS regroupe les modifications en groupes de transactions. Toutes les quelques secondes (souvent autour de 5s, réglable), un TXG est synchronisé sur disque.
Si votre charge est majoritairement des écritures tamponnées asynchrones, le débit que vous voyez est fortement lié au comportement des TXG : les données sales s’accumulent, puis flushent.
Cela peut produire des motifs en « dents de scie » de latence—pics lors des flushs, puis calme.

Pour les écritures synchrones, ZFS ne peut pas juste attendre le prochain sync TXG. Il doit accuser réception seulement après que l’écriture soit durable. C’est pourquoi le ZIL existe.

ZIL : pas un deuxième journal, pas un journal de base de données, pas magique

Le ZIL enregistre l’intention des opérations synchrones. Il est écrit de manière séquentielle-ish, en petits enregistrements. Quand le TXG finit par commettre les blocs réels,
les enregistrements ZIL correspondants deviennent inutiles et peuvent être abandonnés.

Conséquence importante : à l’état stable, le ZIL concerne la latence, pas la capacité. Un dispositif SLOG n’a pas besoin d’être énorme.
Il doit être faible latence, durable, et consistant sous pression.

SLOG : le dispositif « ne rendez pas les écritures synchrones horribles »

Avec un SLOG, ZFS écrit les enregistrements ZIL sur ce dispositif au lieu du pool principal. Si le SLOG est rapide et possède une protection contre la perte d’alimentation,
la latence des écritures synchrones peut chuter drastiquement.

Si le SLOG est rapide mais pas sûr en cas de perte de puissance, vous avez construit un appareil de corruption de base de données. Oui, vous pouvez avoir de la chance. La chance n’est pas une conception.

Où les gens se mentent à eux-mêmes

La plupart des « benchmarks ZFS pour bases » se plantent de trois façons :

  1. Ils ne font pas réellement d’I/O synchrone. fio écrit des données tamponnées et rapporte des chiffres glorieux. La base ne se comporte pas ainsi.
  2. Ils font des I/O synchrones, mais ZFS ne les honore pas comme en production. Dataset sync=disabled ou des couches de virtualisation changent la sémantique.
  3. Ils mesurent le débit et ignorent la distribution de latence. Les bases meurent à cause du p99 et p999, pas à cause de la moyenne MB/s.

« Tout est rapide en moyenne » est la façon dont vous recevez un appel à 02:13. La base ne s’inquiète pas de la moyenne. Elle s’énerve à cause des valeurs extrêmes.

Une citation à garder collée à votre écran :
« L’espoir n’est pas une stratégie. »
—General Gordon R. Sullivan

Blague #1 : Si votre benchmark finit en 30 secondes et que vos conclusions tiennent trois ans, vous faites de l’ingénierie de performance comme un horoscope.

Règles fio : comment les benchmarks trichent accidentellement

Règle 1 : « sync » doit signifier la même chose pour fio, l’OS et ZFS

fio peut effectuer un comportement de type sync de plusieurs manières :
fsync=1 (appel fsync après chaque écriture),
fdatasync=1,
sync=1 (utiliser O_SYNC),
dsync=1 (utiliser O_DSYNC),
direct=1 (O_DIRECT, contourner le page cache),
et le moteur I/O peut encore modifier les sémantiques (psync, libaio, io_uring).

Les bases de données font souvent des écritures tamponnées sur leur fichier WAL/redo et appellent fsync au commit (ou group commit). Certaines utilisent O_DSYNC / O_DIRECT
selon la configuration. Cela signifie que votre test fio doit être choisi en fonction de la configuration de la base, pas en fonction de ce qui rend le plus joli sur un graphique.

Règle 2 : I/O tamponnée + petits fichiers = benchmark du page cache

Si vous exécutez fio contre un petit fichier sans direct=1, vous pouvez mesurer le page cache de Linux. Cela peut être utile, mais ce n’est pas ce que vous croyez.

Pour tester les écritures synchrones, le pire mensonge est :
vous pensez mesurer la latence de durabilité, mais vous mesurez la vitesse de la RAM plus un flush occasionnel.

Règle 3 : « fsync par écriture » n’est pas toujours ce que fait votre base

Mettre fsync=1 dans fio force un fsync après chaque appel write. Cela modèle une base sans group commit. Beaucoup de bases font du group commit :
plusieurs transactions partagent un fsync. Si votre base de prod regroupe les commits, « fsync par write 4k » peut sous-estimer drastiquement le débit et surestimer la latence.

La solution n’est pas de tricher. La solution est de modéliser intentionnellement le group commit (plusieurs threads écrivant, cadence de fsync, ou un mélange de sync et async).

Règle 4 : les percentiles de latence sont le produit

Quand les écritures synchrones sont lentes, votre base attend. Cela se voit comme des pics de latence et une accumulation de files d’attente. Capturez toujours :
p50, p95, p99, et idéalement p99.9, plus IOPS sous une latence cible.

Règle 5 : vérifiez que votre test est réellement synchrone

Ne faites pas confiance à un fichier de configuration parce qu’il « a l’air correct ». Faites en sorte que le système le prouve : tracez les appels système, observez le comportement du ZIL,
confirmez les propriétés du dataset, et confirmez que les dispositifs honorent les flushes.

Conception d’un test fio honnête pour les écritures synchrones des bases

Partir du chemin de durabilité de la base

Choisissez une configuration de base et mappez-la à fio :

  • PostgreSQL par défaut : écritures WAL tamponnées + fsync. Analogue fio : écritures tamponnées avec fsync=1 ou fsync périodique, selon le group commit.
  • MySQL/InnoDB : dépend de innodb_flush_method et innodb_flush_log_at_trx_commit. Analogue fio : mélange de patrons fsync et O_DSYNC.
  • SQLite FULL : barrières fsync fréquentes. Analogue fio : petites écritures synchrones avec fsync ou O_DSYNC.

Choisir une taille de bloc qui correspond au journal, pas à la table

Le WAL/redo est généralement écrit en morceaux (souvent 8k, 16k, 32k, parfois 4k). Utilisez bs=8k ou bs=16k pour les tests type WAL.
N’utilisez pas bs=1m et appelez ça « base de données ».

Utiliser plusieurs jobs pour modéliser la concurrence et le group commit

La plupart des systèmes valident plusieurs transactions en concurrence. Même si chaque transaction est « synchrone », le système peut pipeliner le travail.
Exécutez plusieurs jobs fio avec un fichier partagé ou des fichiers séparés pour modéliser la contention. Mais comprenez le compromis :
plus de threads peuvent masquer la latence d’une seule écriture tout en augmentant la latence de queue.

Garder les fichiers assez grands pour éviter la fausse localité

Pour les tests de journal, vous pouvez utiliser un fichier dimensionné comme les segments WAL (par ex., quelques Go), mais assurez-vous de ne pas réécrire sans cesse
les mêmes blocs qui restent chauds dans l’ARC et les caches de métadonnées. Aussi : ne faites pas tout tourner contre un dataset minuscule avec primarycache=all
puis vous étonnez que ce soit « rapide ».

Séparer les tests « journal synchrone » des tests « flush des données »

Une base a au moins deux personnalités d’écriture :

  1. Écritures de journal (sync) : petites, sensibles à la latence, critiques pour la durabilité.
  2. Écritures de données (asynchrone-ish) : plus grandes, orientées débit, pilotées par des checkpoints.

Si vous les mélangez dans un seul job fio, vous ne pouvez rien diagnostiquer. Créez au moins deux profils de test.

Exécuter assez longtemps pour atteindre l’état stable et observer les cycles TXG

Si votre durée est plus courte que quelques intervalles de sync TXG, vous pouvez être induit en erreur par des caches chauds, un comportement d’allocation initial,
et des boosts à court terme du dispositif. Pour la latence d’écriture synchrone, 2–5 minutes est un minimum décent ; 10–20 minutes est mieux si vous chassez le p99.9.

Blague #2 : La seule chose plus rapide qu’un SLOG NVMe est un benchmark qui a oublié d’activer le sync.

Tâches pratiques (commandes, sorties, décisions)

Voici les tâches que j’exécute réellement quand quelqu’un dit « ZFS est lent pour notre base » et me donne un seul chiffre moyen d’IOPS.
Chaque tâche inclut : commande, ce que signifie la sortie, et quelle décision en tirer.

Tâche 1 : Confirmer le paramètre sync du dataset (le plus gros « oups »)

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

Signification : standard respecte les requêtes de synchronisation de l’application. Si ceci indique disabled, votre « benchmark d’écriture synchrone »
teste probablement un mode fantaisiste.

Décision : Si la production doit être durable, gardez standard (ou always si vous devez forcer le sync). Si vous voyez
disabled près d’une base de données, traitez ça comme un sev-1 en attente d’un événement d’alimentation.

Tâche 2 : Vérifier s’il existe un SLOG et s’il est réellement utilisé

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

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

Signification : Un dispositif de log en miroir est présent. Bien : les écritures ZIL vont vers le SLOG, et le miroir réduit le risque de perte du log sur un seul dispositif.

Décision : S’il n’y a pas de section logs et que la latence sync est élevée, évaluez un SLOG capable de PLP. S’il y a un SLOG mais que c’est un SSD unique
avec sécurité d’alimentation inconnue, supposez un risque de durabilité tant que ce n’est pas prouvé.

Tâche 3 : Vérifier que le dispositif SLOG rapporte le cache d’écriture et le comportement de flush

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

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

Signification : Le cache d’écriture est « activé ». Sur NVMe c’est normal et pas automatiquement dangereux ; la vraie question est si le disque a PLP.

Décision : Confirmez le PLP via les specs du vendeur ou nvme id-ctrl. Si vous ne pouvez pas établir la sécurité contre la perte d’alimentation,
ne l’utilisez pas comme SLOG pour une base qui revendique la durabilité.

Tâche 4 : Identifier les indicateurs de protection contre la perte d’alimentation (au mieux depuis l’OS)

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

Signification : vwc indique la présence d’un cache d’écriture volatile. Cela ne confirme pas le PLP ; cela confirme que vous devez vous en soucier.
Les bits oncs montrent le support de commandes optionnelles comme flush. Vous avez toujours besoin de la vérité du vendeur pour le PLP.

Décision : Si vous ne pouvez pas confirmer le PLP, ne vendez pas des « commits durables ». Vous pouvez toujours utiliser un dispositif rapide, mais soyez honnête sur le risque.

Tâche 5 : Confirmer ashift et alignement (tueur silencieux de performance)

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

Signification : ashift=12 signifie secteurs 4K. Sur SSD/NVMe modernes, c’est généralement correct. Un ashift incorrect peut causer une amplification d’écriture et de la latence.

Décision : Si ashift est trop petit (par ex., 9) sur des dispositifs natifs 4K, la correction nécessite de reconstruire le pool. Ne « tunez » pas autour ; c’est structurel.

Tâche 6 : Surveiller le comportement et les blocages de sync TXG

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

Signification : Des sync TXG de ~2 secondes ne sont pas intrinsèquement fatals, mais ils peuvent corréler avec des pics de latence, surtout quand le système approche des limites de données sales.

Décision : Si les temps de sync TXG sont fréquemment de plusieurs secondes sous charge, investiguez la latence des dispositifs du pool, les réglages des données sales, et si les écritures synchrones s’accumulent derrière la congestion.

Tâche 7 : Vérifier les propriétés ZFS qui comptent pour les datasets de base

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

Signification : recordsize affecte les blocs de données (pas les enregistrements ZIL), mais cela compte pour les patterns I/O des tables.
logbias=latency encourage des patterns d’utilisation du ZIL favorables à la faible latence. atime=off évite des écritures supplémentaires. lz4 est généralement un gain net.

Décision : Pour les datasets WAL, envisagez un recordsize plus petit (ex. 16K) et logbias=latency. Pour les données, alignez le recordsize sur la taille de page et les patterns de lecture typiques. Ne copiez-collez pas recordsize=8K partout.

Tâche 8 : Exécuter un test fio « type WAL » avec fsync par écriture (pire cas sync)

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

Signification : Cela modélise « écrire 8K, fsync, répéter ». Les percentiles de latence montrent le chemin de durabilité. p99.9 à 4ms peut être acceptable ou non selon votre SLA.

Décision : Si p99/p99.9 sont trop élevés, vous chassez la latence du SLOG, le comportement des flushs du dispositif, ou la contention du pool. Ne touchez pas encore aux réglages de la base ; prouvez d’abord le stockage.

Tâche 9 : Exécuter un test O_DSYNC (plus proche de certains modes DB)

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

Signification : direct=1 contourne le page cache ; dsync=1 demande une sémantique de sync des données par écriture. Cela mappe souvent mieux
aux patterns d’append durable du log qu’un fsync=1.

Décision : Si c’est nettement mieux que fsync-par-écriture, cela peut indiquer que votre charge bénéficie du group commit ou de primitives de sync différentes.
Validez par rapport au mode réel de votre base.

Tâche 10 : Ajouter de la concurrence pour modéliser la pression de group commit

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

Signification : Le débit a augmenté, mais p99.9 a explosé. C’est typique quand le dispositif de log ou le pool ne peut pas garder la latence tail serrée sous concurrence.

Décision : Si votre SLA est sensible à la latence, optimisez pour la latence tail, pas pour les IOPS maximales. Vous préférerez peut-être moins de committers concurrents
ou un meilleur SLOG plutôt que plus de threads.

Tâche 11 : Confirmer que fio émet les appels système attendus (strace)

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

Signification : Vous pouvez voir le pattern réel : pwrite, fsync, répéter. Si vous attendiez O_DSYNC et que vous ne le voyez pas, vos options fio ne font pas ce que vous pensez.

Décision : N’acceptez pas « fio dit dsync=1 » comme preuve. Vérifiez les appels système, surtout en changeant ioengine ou les flags direct I/O.

Tâche 12 : Observer l’activité ZIL/SLOG pendant le test (iostat sur le vdev de log)

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

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

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

Signification : Des écritures lourdes sur les disques de log pendant l’exécution du fio sync suggèrent que le ZIL touche le SLOG. Un w_await faible est ce que vous voulez.

Décision : Si les dispositifs de log montrent peu d’activité durant les tests « sync », vous ne faites probablement pas d’I/O synchrone, ou le dataset/pool est configuré différemment de ce que vous pensiez.

Tâche 13 : Vérifier la latence et la mise en file d’attente pool-wide (question « est-ce les vdevs principaux ? »)

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

Signification : Les dispositifs du pool principal sont saturés (%util proche de 99) avec des attentes d’écriture de plusieurs millisecondes.
Même avec un bon SLOG, le sync TXG et la contention générale peuvent pousser la latence tail vers le haut.

Décision : Si les vdevs principaux sont saturés, vous devez réduire la pression d’écriture de fond (checkpoints, compactions, autres locataires), ajouter des vdevs,
ou déplacer des workloads. Un SLOG ne peut pas réparer un pool qui se noie.

Tâche 14 : S’assurer que vous ne mesurez pas l’ARC (cache) pour les lectures

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

Signification : Un miss% faible signifie que les lectures sont majoritairement servies depuis l’ARC. C’est correct pour des hits cache applicatifs,
mais pas représentatif des performances de lecture disque.

Décision : Pour les benchmarks de lecture, dimensionnez le test pour dépasser l’ARC ou utilisez primarycache=metadata sur un dataset de test
pour éviter des chiffres uniquement issus du cache.

Tâche 15 : Tester temporairement avec sync=always pour attraper « async déguisé en sync »

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

Signification : ZFS traitera toutes les écritures comme synchrones sur ce dataset. C’est un levier diagnostique : si la performance change dramatiquement,
votre workload n’émettait pas vraiment d’écritures synchrones comme vous le pensiez, ou elles étaient tamponnées de manière inattendue.

Décision : Utilisez cela seulement en test contrôlé. Si « sync always » écrase votre débit, c’est un signe que votre application dépend d’un comportement d’écriture asynchrone
et que vous devez valider les hypothèses de durabilité.

Tâche 16 : Confirmer que ZFS ne bride pas silencieusement à cause des limites de données sales

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

Signification : Si dirty_over_target est fréquemment non nul, ZFS pousse les écrivains pour contrôler la mémoire et le comportement de sync.
Cela se manifeste par des stalls d’écriture et des pics de latence.

Décision : Si vous atteignez les limites de dirty sous charge normale, investiguez le dimensionnement mémoire, les patterns d’écriture de fond,
et les réglages ZFS—avec précaution. Ne montez pas juste les limites et appelez cela réglé.

Tâche 17 : Valider que votre job fio n’écrase pas les mêmes blocs (illusion de trimmed)

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

Signification : Les écritures synchrones aléatoires sont plus dures que les append séquentiels du journal. Si votre « test WAL » est aléatoire,
vous modélisez autre chose (comme le sync des fichiers de données).

Décision : Utilisez le séquentiel pour les tests type WAL sauf si votre base écrit vraiment des blocs de log de façon non séquentielle (rare).
Gardez ce test comme facteur de stress, pas comme modèle principal.

Manuel de diagnostic rapide

Vous êtes en réunion. Quelqu’un dit « les commits sont lents » et colle un seul graphique. Voici le chemin le plus court pour identifier le goulot sans
transformer cela en projet d’archéologie.

Premier : prouver que c’est vraiment synchrone et vraiment durable

  1. Vérifiez le sync du dataset et toute héritage depuis le parent.
    Si vous trouvez sync=disabled, arrêtez la discussion performance et commencez la discussion risque.
  2. Confirmez les réglages de durabilité de la base (ex. WAL fsync activé, mode de flush InnoDB). Si la DB est en mode « durabilité relâchée »,
    ne benchmarkez pas « durabilité stricte » et vice versa.
  3. Tracez une écriture représentative avec strace ou le tracing au niveau DB pour confirmer les appels système : fsync ? fdatasync ? O_DSYNC ?

Second : isoler si la douleur vient de la latence ZIL/SLOG ou de la congestion du pool

  1. Pendant un job fio sync, regardez iostat -x sur les dispositifs SLOG. S’ils sont occupés et que w_await est élevé, le dispositif de log est en faute.
  2. Si le SLOG semble correct, observez les vdevs principaux. S’ils sont saturés ou ont une forte latence, le sync TXG et la contention du pool ralentissent tout.
  3. Cherchez des pics de temps de sync TXG dans les logs kernel. Des temps TXG de plusieurs secondes corrèlent fortement avec des problèmes de latence tail.

Troisième : poursuivre les amplificateurs habituels (ils comptent plus que vous ne voulez)

  1. Vérifiez ashift, le layout RAID, et si le pool est presque plein. Un pool au-delà de ~80–85% d’utilisation voit souvent une fragmentation et un comportement d’allocation pire.
  2. Vérifiez les workloads concurrents : snapshots, réplication, scrubs, backups, compactions, autres datasets sur le même pool.
  3. Vérifiez que le SLOG est sécurisé en cas de perte d’alimentation. S’il ne l’est pas, vous ne pouvez pas vous y fier pour la durabilité — la performance n’est pas l’argument utile.

Trois mini-récits d’entreprise depuis les tranchées de fiabilité

1) Incident causé par une mauvaise hypothèse : « sync=disabled c’est juste plus rapide »

Une entreprise SaaS de taille moyenne a migré d’une base gérée vers Postgres auto-hébergée pour réduire les coûts et gagner du contrôle. Le stockage était ZFS sur une paire
de miroirs SSD. Un ingénieur bien intentionné a défini sync=disabled sur le dataset parce que les commits étaient « lents dans les benchmarks ».

Pendant des semaines, tout avait l’air bien. La latence a chuté. Les graphiques étaient verts. L’équipe a fait un tour de victoire et est passée à des travaux plus visibles,
comme les fonctionnalités produit et la création de dashboards sur les dashboards.

Puis un événement d’alimentation est arrivé — pas un incendie de datacenter dramatique, juste une maintenance UPS et un déclenchement de disjoncteur qui n’a pas basculé comme tout le monde le pensait.
Les hôtes ont redémarré. Postgres est revenu. Il a même accepté des connexions.

L’horreur subtile fut la corruption logique. Un petit ensemble de transactions récemment commises manquaient, et un autre ensemble était partiellement appliqué.
L’application a vu des incohérences de clefs étrangères et des jobs de réconciliation ont commencé à supprimer des « doublons » qui n’étaient pas des doublons.
Il a fallu des jours pour démêler, principalement parce que la corruption n’est rarement polie au point de planter immédiatement.

Le postmortem n’était pas compliqué. La mauvaise hypothèse était : « ZFS est sûr, donc désactiver sync est sûr. » ZFS est sûr quand vous respectez son contrat de sécurité.
Ils avaient demandé à ZFS de mentir à la base. ZFS a obéi. Les ordinateurs obéissent comme ça.

2) Optimisation qui a mal tourné : « ajoutons un SLOG consumer rapide et pas cher »

Une plateforme de services financiers avait un pool ZFS sur des SSD entreprise, mais des charges sync-intensives (audit logging plus base transactionnelle) montraient des pics
de latence p99. Quelqu’un a suggéré d’ajouter un SLOG. Bonne instinct.

Les achats se sont impliqués. Ils ont trouvé des NVMe consumer « très rapides » à une fraction du prix des modèles entreprise. La fiche technique était un arc-en-ciel de chiffres IOPS,
du genre qui fait croire momentanément aux dirigeants que la physique est optionnelle.

L’équipe a installé les disques en SLOG miroir et relancé fio. Les chiffres semblaient phénoménaux. Puis le trafic de production a frappé. La latence tail s’est aggravée.
Pas de façon constante—juste assez pour ruiner les SLA de façon intermittente.

Le retour de bâton était double. D’abord, ces disques avaient un comportement de cache SLC agressif : les rafales étaient excellentes, l’écriture synchronisée soutenue sous concurrence ne l’était pas.
Ensuite, ils manquaient d’une protection significative contre la perte d’alimentation, donc l’équipe ne pouvait honnêtement pas prétendre à la durabilité. Ils avaient amélioré la moyenne et
empoisonné le p99, tout en augmentant le risque.

La correction fut ennuyeuse : remplacer le SLOG par des dispositifs PLP-capables choisis pour les écritures durables à faible latence sous flush, pas pour le débit maximal.
La performance s’est améliorée, et l’histoire de risque a cessé d’être embarrassante.

3) Pratique ennuyeuse mais correcte qui a sauvé la mise : « tester le chemin fsync réel »

Une grande entreprise exploitait une flotte MySQL multi-tenant sur ZFS. Ils avaient une règle interne : tout changement de stockage nécessitait un « test du chemin de durabilité ».
Pas un benchmark générique. Un test qui modélisait leur méthode réelle de flush et de commit.

Une équipe prévoyait un renouvellement de plateforme : nouveaux serveurs, nouveau pool NVMe, nouveau SLOG. Sur le papier c’était une montée en gamme. Durant les tests pré-prod,
les benchmarks de débit standard semblaient excellents. Le test du chemin de durabilité fio avait l’air… bizarre. Les p99.9 étaient inattendument élevés.

Parce qu’ils avaient la règle, ils avaient aussi les outils : jobs fio qui correspondaient au pattern de flush MySQL, vérification par strace, et un script iostat pour confirmer que le SLOG était utilisé.
Ils ont rapidement tracé le problème à un réglage de firmware sur les dispositifs de log qui modifiait le comportement de flush sous profondeur de file.

Le vendeur a corrigé le firmware/réglage. La plateforme a été livrée. Personne n’a fait la fête, ce qui est le niveau de célébration approprié pour « nous n’avons pas livré un risque latent de données en production ».

L’économie n’était pas un poste de dépense ; c’était l’absence d’un postmortem. Ce sont les meilleures économies.

Erreurs courantes : symptômes → cause racine → correctif

1) « fio affiche 200k IOPS, mais la base commit à 2k/s »

Symptômes : les chiffres fio sont énormes ; la latence de commit DB est toujours mauvaise ; les graphiques ne corrèlent pas.

Cause racine : le test fio est tamponné et ne force pas la durabilité (direct=0 + pas de fsync/dsync), ou dataset sync=disabled en test mais pas en prod.

Fix : Re-lancez fio avec fsync=1 ou dsync=1 (en correspondance avec le mode DB), vérifiez avec strace, et confirmez zfs get sync sur le dataset cible.

2) « Ajouter un SLOG a empiré les choses »

Symptômes : la latence moyenne s’améliore, mais p99/p99.9 se dégradent ; stalls intermittents ; parfois le SLOG est saturé.

Cause racine : le dispositif SLOG a une mauvaise latence d’écriture synchrone soutenue, manque de PLP, ou souffre sous concurrence à cause du cache interne.

Fix : Utilisez des dispositifs PLP-capables à faible latence ; mettez le SLOG en miroir ; validez avec des tests fio en concurrence ; surveillez iostat -x pour w_await et la profondeur de file.

3) « Les écritures synchrones sont lentes même avec un bon SLOG »

Symptômes : le SLOG semble bien ; les vdevs principaux montrent une forte utilisation ; les temps de sync TXG sont longs.

Cause racine : congestion du pool (flushs de checkpoint, autres locataires, scrubs, réplication), throttling pour données sales, ou pool presque plein/fragmenté.

Fix : Réduisez les écritures concurrentes, planifiez correctement scrubs/réplication, ajoutez des vdevs, gardez de la marge dans les pools, et validez les temps de sync TXG.

4) « La latence est correcte sur de courts tests, terrible sur des heures »

Symptômes : tests 1–2 minutes excellents ; charges soutenues se dégradent ; pics périodiques.

Cause racine : épuisement du cache SLC du dispositif, throttling thermique, maintenance de fond (GC), ou cycles TXG/données sales non capturés par des runs courts.

Fix : Exécutez des tests plus longs (10–20 minutes), surveillez la température et le throttling des dispositifs, suivez p99.9, et testez en conditions d’état stable et niveaux de remplissage réels.

5) « On a mis sync=always et la performance a chuté »

Symptômes : le débit chute massivement ; la latence augmente ; l’app commence à timeouter.

Cause racine : le workload dépendait d’écritures asynchrones ; les hypothèses de durabilité de l’application étaient plus faibles que prévu ; ou le dispositif de log/pool ne peut pas soutenir le comportement sync forcé.

Fix : Alignez les attentes de durabilité avec les besoins métier, confirmez les réglages DB, implémentez un SLOG approprié ou acceptez des taux de commit plus bas avec la bonne durabilité.

6) « Les écritures synchrones aléatoires sont catastrophiques »

Symptômes : randwrite + dsync montre une latence tail terrible ; les tests séquentiels sont corrects.

Cause racine : vous testez des patterns de sync de fichiers de données (difficiles) plutôt que des append de log (plus faciles). Possible aussi : amplification d’écriture RAIDZ et fragmentation.

Fix : Séparez les tests WAL (séquentiel) des tests de données ; considérez des miroirs pour les workloads DB sensibles à la latence ; gardez du headroom dans le pool et un recordsize raisonnable.

Checklists / plan pas à pas

Étapes : construire un benchmark d’écriture synchrone défendable en postmortem

  1. Choisir le mode de durabilité de la base.
    Écrivez exactement comment les commits sont faits (fsync par commit ? group commit ? O_DSYNC ?).
  2. Verrouiller les propriétés ZFS.
    Confirmez sync, logbias, compression, atime, et toute héritage depuis les parents.
  3. Confirmer la présence et le type de SLOG.
    zpool status, identifier les vdevs de log, confirmer le miroir, confirmer l’histoire PLP.
  4. Construire deux profils fio :
    un pour les écritures de log sync (séquentiel 8–16K), un pour les données/checkpoint (plus grand, mixte, éventuellement aléatoire).
  5. Vérifier les appels système.
    Utilisez strace sur un court run. Si ce n’est pas fsync/dsync comme prévu, arrêtez et corrigez le job.
  6. Exécuter assez longtemps.
    Au moins 2–5 minutes ; plus long si vous tenez au p99.9.
  7. Collecter les bonnes métriques pendant l’exécution :
    iostat -x pour SLOG et vdevs principaux, temps de sync TXG dans les logs, iowait CPU, et percentiles de latence fio.
  8. Prendre une décision à partir du p99/p99.9.
    Si la latence tail est inacceptable, traitez cela comme un problème d’architecture de stockage, pas comme un « tune la base jusqu’à ce qu’elle arrête de se plaindre ».

Checklist : santé du SLOG pour bases de données

  • Dispositifs PLP-capables (ou acceptation documentée du risque).
  • Mettre le SLOG en miroir pour disponibilité et réduire le risque de perte de log sur un seul dispositif.
  • Optimiser pour la constance de latence, pas pour les chiffres marketing de débit maximal.
  • Surveiller la latence d’écriture du SLOG sous concurrence.
  • Rester simple : un bon SLOG vaut mieux que trois douteux.

Checklist : quand suspecter que vous vous mentez

  • Votre job fio tourne « trop vite », et les dispositifs de log ne montrent pas d’écritures.
  • Les tests courts sont incroyables ; les tests longs se dégradent fortement.
  • La latence moyenne semble correcte, mais la base timeoute (latence tail).
  • Vous avez changé sync ou les réglages de flush DB « pour la performance » et n’avez pas validé un scénario de perte d’alimentation.
  • Vous ne pouvez pas expliquer quel pattern d’appels système votre benchmark utilise.

FAQ

1) Dois-je utiliser fsync=1 ou dsync=1 dans fio pour tester le journal d’une base ?

Utilisez ce que votre base utilise. PostgreSQL ressemble souvent à des écritures tamponnées plus fsync. Certains modes MySQL ressemblent à O_DSYNC ou O_DIRECT.
Si vous ne savez pas, tracez la base avec strace pendant les commits et alignez-vous là-dessus.

2) sync=disabled est-il jamais acceptable pour des bases de données ?

Seulement si vous choisissez explicitement une durabilité plus faible (et que le business est d’accord). Cela peut être acceptable pour des caches éphémères ou des données reconstruites.
Pour des systèmes transactionnels, c’est une dégradation de durabilité déguisée en réglage de performance.

3) Un SLOG améliore-t-il le débit normal asynchrone ?

Non. Un SLOG concerne les opérations synchrones. Si votre workload est majoritairement asynchrone, un SLOG n’aidera pas beaucoup. Il peut aider si votre base fait des fsync fréquents, ce qui est le but.

4) Quelle taille pour mon SLOG ?

Généralement plus petit que vous ne le pensez. Le dimensionnement du SLOG concerne le buffering de quelques secondes d’intention d’écriture synchrone, pas le stockage de la base.
L’exigence réelle est des écritures durables à faible latence sous charge soutenue. Suracheter la capacité ne corrigera pas une mauvaise latence.

5) Pourquoi la concurrence augmente les IOPS mais aggrave la p99.9 ?

Vous échangez la latence par opération contre le débit. Plus de jobs augmentent la profondeur de file, ce qui peut lisser l’utilisation mais amplifier le comportement tail,
surtout si le dispositif a des bizarreries de cache interne ou si le pool est saturé.

6) recordsize affecte-t-il la performance des écritures synchrones ?

Pas directement pour les enregistrements ZIL, mais cela affecte la manière dont les blocs de données sont écrits et peut changer le comportement de checkpoint et de read-modify-write.
Pour les datasets de journal, les réglages clés sont sync, la qualité du SLOG, et la santé générale du pool.

7) Dois-je utiliser direct=1 pour les benchmarks WAL ?

Cela dépend. Si votre base écrit le WAL tamponné et fait fsync, alors direct=0 avec fsync le modélise mieux. Si la base utilise direct I/O ou O_DSYNC,
direct=1 peut être approprié. La bonne réponse : collez-vous à la réalité, puis mesurez.

8) Le miroir du SLOG est-il nécessaire ?

Pour des bases sérieuses, oui, si vous utilisez un SLOG du tout. Une perte de dispositif de log au mauvais moment peut transformer un crash en une récupération longue ou,
dans les pires cas, une perte de données de récentes opérations sync selon le timing et le mode de défaillance. Le miroir est une assurance peu coûteuse comparée à l’explication de transactions perdues.

9) Pourquoi mes chiffres fio changent après un reboot ?

Warm-up du cache, état thermique différent du dispositif, état GC fondamental sur les SSD, et différence de fragmentation/localité de métadonnées importent tous.
Voilà pourquoi les tests longs en état stable sont plus honnêtes qu’« un run après boot ».

10) RAIDZ peut-il convenir pour des bases sur ZFS ?

Cela peut être bien pour des workloads orientés lecture ou débit, mais les bases sensibles à la latence sync préfèrent souvent les miroirs car les petites écritures et la latence tail se comportent mieux.
Si la priorité est la latence de commit, les miroirs sont le choix par défaut pour une raison.

Conclusion : prochaines étapes pratiques

Si vous voulez tester ZFS pour des bases honnêtement, arrêtez de courir après le plus grand chiffre de débit et commencez à rechercher le profil de latence le plus ennuyeux,
répétable et correct pour la durabilité que vous puissiez obtenir.

Prochaines étapes qui font vraiment avancer les choses :

  1. Choisissez le mode de durabilité de la base utilisé en production et notez-le (cadence fsync, group commit, méthode de flush).
  2. Vérifiez le sync du dataset ZFS et la configuration SLOG, et prouvez le comportement des appels système avec strace.
  3. Exécutez deux profils fio (WAL sync et data/checkpoint) assez longtemps pour capturer la latence tail et les cycles TXG.
  4. Quand les résultats sont mauvais, utilisez le playbook de diagnostic rapide : validez les sémantiques sync, isolez SLOG vs pool, puis chassez la contention et les amplificateurs.
  5. Si vous avez besoin d’un SLOG, achetez pour PLP et constance de latence. Si vous ne pouvez pas le justifier, soyez honnête sur les compromis de durabilité.

L’ingénierie de performance, c’est surtout refuser d’être impressionné par vos propres graphiques. ZFS fera ce que vous lui demandez. Assurez-vous de demander la vérité.

← Précédent
Pourquoi un monstre du cache peut battre un CPU « plus rapide »
Suivant →
Ubuntu 24.04 : l’IPv6 casse des sites au hasard — réparer correctement le double empilement (ne désactivez pas IPv6)

Laisser un commentaire