PostgreSQL est franc : quand il est lent, il explique généralement pourquoi. ZFS est lui aussi franc — mais pas toujours dans le même langage.
Ensemble, ils peuvent donner une base de données remarquablement fiable… ou un système qui paraît correct sur les tableaux de bord jusqu’au moment où il ne l’est plus.
Le point douloureux est toujours le même : les écritures synchrones. La durabilité du WAL. Les pics de latence qui n’apparaissent que sous forte charge de commit.
Quelqu’un suggère « il suffit de mettre sync=disabled » et soudain vous vous retrouvez à une réunion imprévue sur la « posture d’intégrité des données ».
La stratégie : que faire, que ne pas faire
Si vous voulez que PostgreSQL sur ZFS fonctionne en production, cessez de raisonner en conseils génériques pour systèmes de fichiers et commencez à penser
en frontières de durabilité. La frontière de durabilité de PostgreSQL, c’est le WAL + le comportement de fsync (plus les checkpoints et la réplication).
La frontière de durabilité de ZFS, ce sont le ZIL (et le SLOG s’il existe), les transaction groups et l’endroit où vont les écritures synchrones.
La stratégie de dataset et de sync qui fonctionne n’est pas exotique :
- Séparez les données PostgreSQL et le WAL sur des datasets distincts (et idéalement sur des vdevs séparés si possible).
- Laissez les fonctions de sécurité de PostgreSQL activées :
fsync=on,full_page_writes=onet des choix raisonnables poursynchronous_commit. - Utilisez
sync=standardpour la plupart des datasets ZFS. Cela respecte la sémantique POSIX de sync, et Postgres l’utilise pour une raison. - Si la latence des commits est le goulot, corrigez le chemin de sync correctement : fournissez un vrai périphérique SLOG (protégé contre perte d’alimentation) ou changez intentionnellement l’exigence de durabilité de l’application (par ex.
synchronous_commit=offpour des charges spécifiques). - Utilisez la compression. Elle aide généralement la latence et le débit sur les pages de base de données, et c’est l’un des rares « repas gratuits » qui fonctionne en production.
- Ne mettez pas
sync=disabledsur le dataset de la base à moins d’avoir documenté le rayon d’impact et obtenu l’accord explicite des personnes qui seront appelées à 3h du matin.
La manière la plus simple de perdre son poste est « d’optimiser la durabilité » sans prévenir. La deuxième manière la plus simple est d’acheter des périphériques rapides
puis de faire attendre ZFS sur des E/S de sync lentes de toute façon.
Blague n°1 : Si vous mettez sync=disabled sur une base de données et appelez ça « finalement durable », vous avez inventé un nouveau niveau de stockage : l’espoir.
Faits et contexte qui changent les décisions
Ce ne sont pas des anecdotes pour faire joli. Chacun devrait influer sur un vrai choix de conception.
- ZFS est copy-on-write (héritage du travail original de Sun Microsystems). PostgreSQL s’attend déjà à réécrire des pages de 8 Ko ; ZFS transforme cela en nouvelles allocations de blocs. Cela affecte l’amplification d’écriture et la fragmentation.
- Le ZIL existe toujours, même sans SLOG. Un SLOG ne « met pas le ZIL en marche », il déplace le périphérique de log vers quelque chose de plus rapide et sûr pour les écritures synchrones.
- La taille de page par défaut de PostgreSQL est 8 Ko. Le
recordsizepar défaut de ZFS est souvent 128 Ko. Ce décalage n’est pas forcément mauvais, mais il détermine le comportement read-modify-write sur les mises à jour et la quantité de données modifiées par écriture. - Historiquement, beaucoup d’affirmations « ZFS est lent pour les bases » venaient de pools mal configurés :
ashiftincorrect sur disques 4K, aucun SLOG pour des charges sync-intensives, ou ARC affamé en RAM. - Le WAL de PostgreSQL est plutôt séquentiel mais pas purement séquentiel. C’est append-heavy, mais le comportement de fsync peut créer des rafales, et l’archivage/la réplication peuvent provoquer des lectures à des moments inopportuns.
- ZFS vérifie l’intégralité de chaque bloc avec des checksums. Cela permet de détecter des corruptions silencieuses que d’autres piles laisseraient passer jusqu’à une défaillance ultérieure. Pour les bases, ce n’est pas un luxe optionnel ; c’est de l’intégrité.
- La compression est devenue « sûre par défaut » dans beaucoup d’équipes ZFS après la maturation de LZ4 (et l’accélération des CPU). Aujourd’hui, laisser la compression désactivée, c’est souvent laisser des performances sur la table.
- La durabilité PostgreSQL n’est pas un interrupteur binaire. Vous pouvez choisir la durabilité au niveau de la transaction (
synchronous_commit) ou de la réplication (replication synchrone), plutôt que de saboter le système de fichiers.
Agencement des datasets qui tient la charge réelle
L’erreur la plus fréquente est de tout mettre sous un seul dataset, de le régler une fois, puis de penser que c’est fini.
PostgreSQL a au moins trois personnalités d’I/O : fichiers de données, WAL, et « autres choses importantes mais pas à optimiser au prix de la durabilité »
(logs, dumps, staging de basebackup, etc.).
Arborescence pragmatique de datasets
Voici un agencement qui correspond au comportement de Postgres et permet de régler ZFS sans se saboter :
tank/pg: dataset parent pour tout le stockage PostgreSQL.tank/pg/data: répertoire principal des données (PGDATA) à l’exclusion du WAL si vous le déplacez.tank/pg/wal: répertoire WAL (lierpg_walici ou utiliserpostgresql.conflà où c’est supporté).tank/pg/tmp: tables temporaires / sorts si vous placeztemp_tablespacesici (optionnel ; souvent mieux sur NVMe local et acceptable à perdre).tank/pg/backups: sauvegardes et staging de basebackup (cibles de snapshot, spool d’archivage WAL, etc.).
Pourquoi séparer les datasets ? Parce que recordsize, logbias, compression, et parfois les décisions de primarycache diffèrent.
Le WAL veut faible latence et comportement de sync prévisible. Les fichiers de données veulent efficacité en lecture, mise en cache et contrôle de la fragmentation.
Où placer physiquement le WAL
Si vous le pouvez, le WAL mérite un stockage rapide et un chemin de sync propre. Options :
- Même pool, dataset séparé : le plus simple, permet toujours de régler des propriétés ; les performances dépendent de la disposition des vdevs.
- Vdevs miroir séparés pour le WAL : bon quand le pool principal est en RAIDZ et que la latence du WAL souffre.
- Pool séparé pour le WAL : parfois fait, mais opérationnellement plus lourd (plus de choses à surveiller, plus de domaines de panne).
Ne déplacez pas le WAL vers un stockage « rapide mais douteux » sans protection contre perte d’alimentation si vous comptez sur la durabilité synchrone. Le WAL est le journal de ce qui s’est passé ; on n’écrit pas son journal avec de l’encre qui disparaît.
Sémantiques de sync : PostgreSQL vs ZFS (et la place du SLOG)
PostgreSQL écrit des enregistrements WAL et utilise fsync() (ou fdatasync()) pour garantir la durabilité lors d’un commit, selon les réglages.
Quand Postgres dit « sync », il entend : « Si on perd l’alimentation maintenant, la transaction commitée doit rester commitée après la récupération après crash. »
ZFS avec sync=standard respecte les requêtes synchrones en utilisant le ZIL. Le ZIL n’est pas un cache d’écriture pour tout ; c’est un journal des écritures synchrones afin qu’elles puissent être rejouées après un crash. Sans périphérique SLOG dédié, le ZIL réside sur le pool principal, ce qui signifie que les écritures synchrones atterrissent sur vos disques de données. Avec un SLOG, elles atterrissent sur le SLOG (toujours écrites de manière sûre), puis sont engagées plus tard sur le pool principal via le comportement normal des transaction groups.
Ce que sync signifie vraiment sur un dataset
sync=standard: honorer les requêtes sync. C’est le défaut souhaité pour les bases de données.sync=always: traiter toutes les écritures comme synchrones, même si l’application ne l’a pas demandé. Généralement pire pour Postgres ; cela transforme des écritures non critiques en taxe de latence.sync=disabled: mentir à l’application sur la durabilité. Rapide. Aussi une décision risquée quand l’alimentation flanche.
SLOG : ce que c’est, ce que ce n’est pas
Un SLOG est un périphérique dédié pour le ZIL. Il améliore la latence des écritures synchrones en fournissant un endroit rapide pour les engager.
Mais il doit être du bon type : faible latence, forte endurance en écriture et — non négociable — protection contre perte d’alimentation.
Si le SLOG ment sur la durabilité (SSD grand public avec cache volatile sans PLP), vous avez essentiellement déplacé le pari de sync=disabled dans le matériel.
Il peut « fonctionner » jusqu’au jour où il ne fonctionne plus, ce qui est le pire modèle de fiabilité en exploitation.
Une citation pour fonder une revue d’incident
idée paraphrasée — John Allspaw : la fiabilité vient de rendre les pannes visibles et survivables, pas de croire qu’elles n’arriveront pas.
Propriétés ZFS recommandées pour Postgres (avec justification)
Ce sont des valeurs par défaut assumées qui fonctionnent dans beaucoup d’installations réelles. Vous pouvez dévier, mais faites-le délibérément et testez avec votre charge.
Prérequis au niveau du pool (avant les datasets)
ashiftcorrect pour vos disques (typiquement 12 pour secteurs 4K). Se tromper ici, c’est acheter une amplification d’écriture permanente.- Miroirs pour charges sensibles à la latence (y compris Postgres lourd en WAL). RAIDZ peut convenir pour la capacité et le débit, mais la latence d’I/O sync est généralement meilleure sur des miroirs.
- Assez de RAM pour l’ARC, mais ne dévorez pas shared_buffers de Postgres. L’ARC s’étendra volontiers ; votre OOM killer, lui, sera moins indulgent.
Dataset : tank/pg/data
recordsize=16Kourecordsize=8K: Commencez par 16K pour beaucoup de charges OLTP ; 8K peut réduire le read-modify-write sur les mises à jour mais nuire aux lectures séquentielles larges. Mesurez.compression=lz4: Améliore généralement les IOPS effectifs et réduit l’amplification d’écriture. C’est un des rares réglages à la fois sûr et plus rapide.atime=off: Postgres n’a pas besoin d’atime. Cessez d’en payer le coût.xattr=sa(si supporté) : garde les xattrs dans les inodes ; réduit les I/O métadonnées.logbias=latency(par défaut) : favoriser la faible latence pour les opérations synchrones. Pour le dataset de données, laisser le défaut est correct ; on affinera le WAL séparément.
Dataset : tank/pg/wal
recordsize=16K: Les écritures WAL sont append-ish ; pas besoin d’un recordsize énorme. Évitez 128K ici.compression=lz4: Le WAL se compresse parfois bien, mais le gain est souvent modeste. Néanmoins, cela réduit généralement les écritures physiques.logbias=throughput:
Ceci nécessite une explication. logbias=throughput dit à ZFS de privilégier l’écriture des données sync vers le pool principal plutôt que le SLOG dans certains schémas.
Sur un système avec un bon SLOG, vous voudrez souvent logbias=latency. Sans SLOG, le WAL vit déjà sur le ZIL du pool principal ; le bias compte moins.
En pratique : si vous avez un SLOG approprié et que la latence de commit est un goulot, gardez logbias=latency pour le WAL. Si vous satuez le SLOG ou qu’il n’est pas le goulot, envisagez throughput.
Ne l’appliquez pas par mimétisme. Mesurez la latence des commits et le comportement du ZIL.
sync=standard: Oui, toujours. Votre « mode rapide » doit être dans les réglages de transaction Postgres, pas dans des mensonges au niveau du système de fichiers.primarycache=metadata(parfois) : Si les lectures du WAL poussent hors du cache des données utiles, limitez le cache du WAL à la metadata. Cela dépend de votre RAM et de la charge.
Dataset : tank/pg/tmp (optionnel)
sync=disabledpeut être acceptable ici si c’est réellement de l’espace temporaire jetable et que vous acceptez de le perdre en cas de crash. Traitez-le comme un disque scratch.compression=lz4,atime=off.
Blague n°2 : La seule chose plus « temporaire » que tank/pg/tmp est la confiance de quelqu’un qui n’a pas testé le basculement.
Tâches pratiques : commandes, sorties et décisions
Voici les exercices de terrain : ce que vous lancez, ce que la sortie signifie et quelle décision vous prenez. L’objectif est d’arrêter de débattre sur des impressions.
Tâche 1 : Confirmer la santé du pool (parce que tout le reste est vain si c’est cassé)
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
Ce que ça signifie : le pool est ONLINE, le scrub est propre, le périphérique SLOG est présent et en ligne.
Décision : poursuivre l’optimisation de performance. Si vous voyez DEGRADED, erreurs de checksum ou périphérique de log mort, arrêtez et corrigez la fiabilité d’abord.
Tâche 2 : Vérifier ashift (tueur silencieux de performance)
cr0x@server:~$ sudo zdb -C tank | grep -E "ashift|vdev_tree" -n | head
56: vdev_tree:
74: ashift: 12
Ce que ça signifie : ashift: 12 indique des secteurs 4K. Bon pour les SSD/HDD modernes.
Décision : si ashift est 9 sur des disques 4K, vous paierez l’amplification d’écriture pour toujours. La migration/reconstruction est la solution, pas un réglage.
Tâche 3 : Inspecter les propriétés courantes des datasets
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
Ce que ça signifie : vous voyez ce qui est hérité et ce qui est explicitement défini.
Décision : appliquez les propriétés sur le dataset qui correspond au comportement (données vs WAL), pas sur le parent « par commodité ».
Tâche 4 : Créer des datasets avec des valeurs par défaut sensées
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
Ce que ça signifie : les datasets existent et se montent là où Postgres s’attend (ajustez selon votre distro/version).
Décision : alignez vos frontières de système de fichiers avec les besoins opérationnels : snapshots séparés, propriétés séparées, surveillance séparée.
Tâche 5 : Appliquer les propriétés (données)
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
Ce que ça signifie : le dataset de données est réglé pour le comportement page-ish de Postgres et pour réduire le churn des métadonnées.
Décision : commencez à 16K. Si les mises à jour sont lourdes et que vous observez une amplification d’écriture, testez 8K. Ne devinez pas.
Tâche 6 : Appliquer les propriétés (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
Ce que ça signifie : le WAL est préparé pour un comportement de sync à faible latence.
Décision : si la latence des commits reste élevée, cela pointe vers la latence du SLOG/périphérique, pas vers « plus de réglages ».
Tâche 7 : Confirmer où Postgres écrit le 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
Ce que ça signifie : le répertoire WAL est redirigé correctement.
Décision : si le WAL est toujours à l’intérieur du dataset de données, vous perdez l’isolation de réglage et les snapshots deviennent plus contraignants qu’ils ne devraient l’être.
Tâche 8 : Vérifier si vous êtes limité par les commits synchrones
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
Ce que ça signifie : Postgres se comporte de manière sûre : fsync activé, commits synchrones.
Décision : si la latence est inacceptable, résolvez-le avec un SLOG ou des compromis ciblés au niveau Postgres — pas en désactivant le sync sur ZFS.
Tâche 9 : Mesurer l’activité ZIL/SLOG et repérer la pression de sync
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
-------------------------- ----- ----- ----- ----- ----- -----
Ce que ça signifie : le vdev logs effectue des écritures. C’est le trafic sync qui frappe le SLOG.
Décision : si les opérations d’écriture du SLOG sont élevées et que la latence des commits l’est aussi, votre périphérique SLOG peut être trop lent, saturé ou mal comporté.
Tâche 10 : Surveiller la latence directement avec iostat (vérité au niveau périphérique)
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
Ce que ça signifie : faible await sur le périphérique SLOG suggère qu’il n’est pas le goulot.
Décision : si l’await sur le périphérique de log grimpe en millisecondes sous charge de commit, corrigez le SLOG (périphérique, topologie PCIe, firmware, PLP) ou n’en utilisez pas.
Tâche 11 : Vérifier que la compression aide vraiment (et ne grille pas le 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
Ce que ça signifie : les données se compriment bien, le WAL moins. C’est normal.
Décision : laissez la compression activée ; le gain sur les données compense généralement tout. Si le CPU est saturé, mesurez avant de changer.
Tâche 12 : Vérifier la pression sur l’ARC et si ZFS se bat avec Postgres pour la RAM
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
Ce que ça signifie : l’ARC est grand mais pas plein, miss% est raisonnable. Si miss% est élevé et que Postgres met aussi beaucoup en cache, vous pourriez avoir un double cache.
Décision : ajustez l’ARC max si l’OS swappe ou si Postgres est affamé ; ou réduisez shared_buffers de Postgres si l’ARC fait mieux le travail pour votre pattern d’accès.
Tâche 13 : Vérifier le timing des transaction groups (source de latence périodique)
cr0x@server:~$ sudo sysctl vfs.zfs.txg.timeout
vfs.zfs.txg.timeout: 5
Ce que ça signifie : l’intervalle TXG est de 5 secondes (valeur courante). Les charges en rafales peuvent observer un comportement de flush périodique.
Décision : ne le changez pas juste parce que vous le pouvez. Si vous observez des pics de latence réguliers alignés sur les commits TXG, investiguez la pression d’écriture et la latence des périphériques avant d’y toucher.
Tâche 14 : Confirmer l’état d’autotrim (performance SSD dans le temps)
cr0x@server:~$ sudo zpool get autotrim tank
NAME PROPERTY VALUE SOURCE
tank autotrim on local
Ce que ça signifie : TRIM est activé ; les SSD restent plus sains sous écritures soutenues.
Décision : activez-le pour les pools SSD sauf raison spécifique contraire.
Tâche 15 : Vérification de la politique de snapshot (les sauvegardes sont une fonctionnalité opérationnelle)
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
Ce que ça signifie : cadence de snapshots cohérente pour les deux datasets.
Décision : les snapshots ne sont pas des sauvegardes en soi, mais ils permettent rollback rapide et réplication. Assurez-vous que la rétention/archivage WAL correspond à la cadence des snapshots.
Tâche 16 : Tester la latence d’écriture sync avec pgbench et corréler
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)
Ce que ça signifie : c’est orienté lecture (-N) et ne devrait pas stresser beaucoup les commits sync.
Décision : lancez aussi un test orienté écriture ; si seuls les tests d’écriture/commit sont lents, concentrez-vous sur le chemin WAL + sync (SLOG, latence périphérique, profondeur de file d’attente).
Playbook de diagnostic rapide
Quand la production est lente, vous n’avez pas le temps pour la philosophie. Vous avez besoin d’un chemin court jusqu’au goulot.
Voici l’ordre qui trouve rapidement les vrais problèmes.
Premier : prouvez que c’est la latence de sync (ou pas)
- Vérifiez Postgres : les commits sont-ils lents ou les requêtes sont lentes pour d’autres raisons ?
- Consultez
pg_stat_statementset la distribution de latence des transactions, pas seulement le TPS moyen. - Si vous voyez des pics de latence d’écriture alignés avec des commits/checkpoints, suspectez le chemin de 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
Décision : un nombre élevé de checkpoints_req par rapport aux timed checkpoints suggère une pression et des stalls liés aux checkpoints ; investiguez le réglage WAL/checkpoint et la latence du stockage.
Deuxième : vérifier que ZFS est sain et ne se bride pas lui-même
zpool statuspour erreurs et activité resilver/scrub.zpool iostat -v 1pour voir où vont les écritures (vdevs principaux vs logs).
Troisième : trouver le périphérique réellement lent
iostat -x 1et rechercher un await élevé et un %util élevé.- Si un SLOG existe, vérifiez-le explicitement.
- Confirmez le placement PCIe et si le « périphérique rapide » est sur un bus partagé ou derrière un contrôleur atypique.
Quatrième : vérifier le caching et la pression mémoire
- Taille et misses de l’ARC.
- Swapping Linux ou tempêtes de reclaim mémoire.
- Interactions shared_buffers de Postgres vs cache OS vs ARC.
Cinquième : vérifier la fragmentation et les signaux d’amplification d’écriture
zpool listpour la capacité ; une forte occupation nuit aux performances.- Recordsize du dataset vs pattern effectif d’I/O.
- Autotrim, tendances du ratio de compression.
Erreurs courantes : symptôme → cause → correctif
Ce sont celles qui arrivent en rotation d’astreinte parce qu’elles sont subtiles, et survivables… jusqu’au moment où elles ne le sont plus.
1) « Les commits sont lents et saccadés » → pas de SLOG (ou un mauvais) → ajouter un SLOG correct
Symptôme : le TPS semble correct jusqu’à ce que la concurrence augmente ; la latence des commits bondit. Les utilisateurs voient des pauses aléatoires.
Cause : les écritures WAL synchrones atterrissent sur un vdev RAIDZ occupé ou sur des disques lents ; ou le SLOG est de catégorie grand public sans PLP et montre des pics de latence.
Correctif : utilisez des SLOG mirroirs, à faible latence, protégés contre perte d’alimentation ; vérifiez avec zpool iostat -v et iostat -x.
2) « Les performances ont empiré après avoir réglé recordsize » → mauvais choix de recordsize → revenir en arrière et tester
Symptôme : après avoir mis recordsize=8K partout, les scans séquentiels et sauvegardes ont ralenti ; la charge CPU a augmenté.
Cause : un recordsize trop petit augmente la surcharge des métadonnées et réduit l’efficacité du préfetch pour les lectures larges.
Correctif : utilisez 16K (parfois 32K) pour les données, gardez le WAL plus petit ; faites des benchmarks avec vos requêtes réelles.
3) « Nous avons perdu des données commités après un événement électrique » → sync=disabled (ou SLOG non sûr) → restaurer la durabilité et révalider
Symptôme : la base redémarre proprement mais il manque des écritures récentes ; les réplicas divergent ; les journaux d’audit ne correspondent pas.
Cause : ZFS a reconnu des écritures sync sans stockage stable. Cela peut être un sync=disabled explicite ou un périphérique qui ment.
Correctif : remettre sync=standard ; utiliser un SLOG approprié ; exécuter des vérifications de cohérence et valider les hypothèses applicatives sur la durabilité.
4) « Stalls périodiques de 5–10 secondes » → pool presque plein ou rafales de checkpoint → libérer de l’espace et lisser le pattern d’écriture
Symptôme : stalls à intervalles réguliers ; l’I/O semble normale le reste du temps.
Cause : le pool est trop plein (allocations coûteuses), ou les checkpoints forcent de gros flushs.
Correctif : gardez les pools confortablement en dessous d’une forte utilisation ; ajustez les paramètres de checkpoint/WAL de Postgres ; assurez-vous que WAL et datasets de données sont réglés séparément.
5) « Le CPU est élevé, l’I/O semble bas, les requêtes sont lentes » → compression ou checksumming incriminés à tort → vérifier mémoire et contention
Symptôme : les graphiques de stockage paraissent calmes, mais la latence est élevée et les CPUs sont occupés.
Cause : vous n’êtes pas lié par l’I/O ; peut-être contention de buffers, pression de vacuum, ou mauvais dimensionnement shared_buffers/ARC.
Correctif : utilisez les vues Postgres (pg_stat_activity, pg_stat_bgwriter, pg_stat_io sur versions récentes) pour trouver le vrai goulot. Ne faites pas de ZFS le bouc émissaire.
Trois mini-récits tirés du monde de l’entreprise
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Une entreprise SaaS de taille moyenne a migré son cluster Postgres primaire d’ext4 sur RAID matériel vers ZFS sur une paire de miroirs NVMe flambant neufs.
Ils ont préparé la migration : répétitions, mesures, même une coupure le weekend. Les graphiques étaient excellents. Tout le monde est rentré chez soi.
Une semaine plus tard, une opération de maintenance d’alimentation ne s’est pas passée comme prévu. Les racks sont revenus, Postgres a redémarré, la réplication a rattrapé son retard et tout avait l’air « bien ».
Puis les tickets du support client ont commencé : des mises à jour manquantes des dernières minutes avant la panne. Pas de corruption. Pas de cluster planté. Juste… des transactions absentes.
L’hypothèse erronée était subtile et très humaine : ils croyaient « NVMe est rapide, et ZFS est sûr, donc on peut désactiver le sync pour retrouver les performances d’ext4 ».
Ils avaient mis sync=disabled sur le dataset, pensant que Postgres resterait sûr parce que fsync=on dans Postgres. Mais ZFS rendait un succès
sans forcer le stockage stable. Postgres avait fait le travail correctement ; le système de fichiers avait simplement refusé de participer.
Le correctif post-incident a été ennuyeux : remettre sync=standard, ajouter un SLOG mirroir avec PLP, et documenter que le « mode rapide » pour un sous-ensemble de charges est
synchronous_commit=off sur des sessions spécifiques — pas un mensonge au niveau du système de fichiers.
La vraie leçon n’était pas « ne jamais optimiser ». C’était : n’optimisez pas en changeant la signification de « committed ». Votre application a déjà un réglage pour ça.
Mini-récit 2 : L’optimisation qui a échoué
Une autre organisation — grande, réglementée et adepte des comités — avait un système Postgres de reporting qui exécutait de grands scans séquentiels et des écritures batch la nuit.
Ils ont lu un billet sur l’ajustement du recordsize à la taille de page de la base et ont décidé d’imposer recordsize=8K sur tout le pool « pour la cohérence ».
Une demande de changement a été déposée. Tout le monde a hoché la tête. La cohérence rassure.
En une semaine, les jobs nocturnes ont commencé à durer plus longtemps. Puis la fenêtre de « rattrapage du matin » a commencé à empiéter sur les heures de pointe.
Le système batch n’était pas plus écrit qu’avant ; il faisait juste plus de travail par octet stocké.
La raison : la charge avait de grandes lectures séquentielles (requêtes analytiques), et le recordsize plus petit a accru la surcharge métadonnée et réduit l’efficacité de lecture.
Le préfetch et l’agrégation de blocs de ZFS ne pouvaient plus faire leur travail aussi bien. La compression était toujours activée, mais elle compressait beaucoup de petits enregistrements
et les suivait individuellement. La charge CPU a augmenté. Les disques n’étaient pas saturés ; le système passait plus de temps sur la gestion.
Ils ont corrigé cela en séparant les datasets par comportement : tablespaces d’entrepôt de données sur un dataset avec recordsize=128K (parfois 256K pour de très larges scans),
et datasets OLTP sur 16K. Le WAL est resté à 16K. Tout le monde a conservé sa « cohérence », simplement pas au niveau du pool entier.
L’enseignement discret : un seul nombre ne peut pas décrire « une charge de base de données ». Si vous ne pouvez pas nommer le pattern I/O, vous ne pouvez pas le régler.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une équipe de services financiers exploitait Postgres sur ZFS avec une configuration conservative : miroirs, compression activée, sync=standard, et un SLOG mirroir.
Rien de spectaculaire. Leur meilleur ingénieur décrivait cela comme « résolument sans intérêt ». C’était un compliment.
Ils avaient aussi une habitude que les autres équipes se moquaient : scrub hebdomadaire, et un tableau de bord qui affichait les erreurs de checksum même quand « tout fonctionnait ».
Sur le papier, les scrubs étaient « du I/O en plus ». En réunion, les scrubs étaient « pourquoi on se fait ça ». En réalité, les scrubs étaient le canari.
Un jour, le scrub a signalé un petit nombre d’erreurs de checksum sur un seul périphérique. Aucune erreur applicative pour l’instant. Aucun symptôme client.
Ils ont remplacé le périphérique comme prévu, résilvergé proprement, et ont passé leur chemin. Des semaines plus tard, un modèle SSD similaire présentait un bug de firmware
sous une transition d’état d’alimentation spécifique. Les équipes sans détection proactive l’ont découvert de manière plus excitante.
Leur résultat était peu spectaculaire : pas d’incident, pas de panne, pas de perte de données. C’est le meilleur type d’histoire en exploitation : celle qu’on raconte presque pas.
Mais quand votre travail est de tenir vos promesses à l’entreprise, « ennuyeux » est une caractéristique.
Listes de contrôle / plan étape par étape
Étape par étape : construire un hôte Postgres-sur-ZFS de manière sensée
- Concevez le pool pour la latence en priorité : miroirs pour l’OLTP primaire si la latence de commit compte. RAIDZ pour capacité et débit.
- Choisissez l’alignement de secteur correct : confirmez
ashift=12lors de la création du pool. - Ajoutez un SLOG seulement si nécessaire (charges sync-intensives) et seulement s’il est PLP-protégé. Miroirez-le.
- Créez des datasets séparés :
tank/pg/data,tank/pg/wal, optionnellementtank/pg/tmpettank/pg/backups. - Définissez les propriétés par dataset : compression, recordsize, logbias, atime, xattr.
- Branchez correctement les répertoires Postgres et vérifiez l’emplacement du WAL.
- Benchmarquez avec
pgbenchet un jeu de requêtes proche de la production. Mesurez les percentiles de latence de commit. - Activez les scrubs et surveillez les erreurs. Traitez les erreurs de checksum comme des alarmes, pas des suggestions.
- Planifiez les snapshots : coordonnez avec l’archivage/retention WAL et vos objectifs de récupération.
- Documentez le contrat de durabilité : quels réglages sont autorisés (
synchronous_commit), et ce qui est interdit (sync=disabledsur datasets critiques).
Checklist opérationnelle : avant de changer une propriété ZFS en production
- Pouvez-vous revenir en arrière ? (Les changements de propriété sont faciles ; les régressions de performance ne le sont pas.)
- Avez-vous une reproduction de la charge (profil pgbench, replay, ou au moins un ensemble de requêtes de test connu) ?
- Cela changera-t-il la sémantique de durabilité ? Si oui, avez-vous l’accord explicite ?
- Avez-vous capturé avant/après : latence commit p95/p99, await des périphériques,
zpool iostat? - Le pool est-il sain, scrubbé et non en cours de resilver ?
FAQ
1) Dois-je exécuter PostgreSQL sur ZFS du tout ?
Oui, si vous valorisez l’intégrité des données, les snapshots et les outils opérationnels, et si vous êtes prêt à comprendre le comportement de sync.
Si votre équipe traite le stockage comme une boîte noire, vous apprendrez — juste pendant un incident.
2) Ai-je besoin d’un SLOG pour PostgreSQL ?
Uniquement si la latence des écritures synchrones est votre goulot. Beaucoup de charges orientées lecture ou asynchrones n’en bénéficieront pas beaucoup.
Si vous en avez besoin, utilisez des périphériques PLP et miroirez-les.
3) sync=disabled est-il jamais acceptable ?
Sur des datasets Postgres critiques : non. Sur des datasets temporaires véritablement jetables : peut-être, si vous êtes explicite sur la perte en cas de crash.
Si vous voulez réduire la durabilité pour un sous-ensemble d’opérations, utilisez les réglages Postgres comme synchronous_commit.
4) Quel recordsize devrais-je utiliser pour les données Postgres ?
Commencez à 16K. Envisagez 8K pour de l’OLTP très orienté écriture si vous mesurez une réduction de l’amplification d’écriture. Envisagez des recordsize plus larges (64K–128K)
pour des tables d’analytics avec gros scans séquentiels. Les datasets séparés rendent cela simple.
5) Le WAL doit-il être sur un dataset séparé ?
Oui. Cela vous donne un réglage ciblé et des frontières opérationnelles plus claires. L’isolation des performances est un bonus ; la clarté est le gain principal.
6) La compression ZFS aide-t-elle les bases de données ?
En général, oui. LZ4 est le choix courant car il est rapide et peu risqué. Il réduit I/O physique, ce que la plupart des bases attendent réellement.
7) Comment les snapshots interagissent-ils avec la cohérence PostgreSQL ?
Les snapshots ZFS sont cohérents au niveau crash du système de fichiers. Pour des sauvegardes cohérentes applicativement, coordonnez avec Postgres : utilisez des base backups,
l’archivage WAL, ou quiescez correctement. Les snapshots sont un excellent outil ; ce n’est pas une sauce magique de cohérence.
8) Dois-je désactiver atime ?
Oui pour les datasets Postgres. Les mises à jour atime génèrent des écritures inutiles. Laissez-le désactivé sauf si une exigence de conformité dépend réellement de atime.
9) Miroirs ou RAIDZ pour Postgres ?
Miroirs quand la latence compte (OLTP, chargé en commits). RAIDZ quand l’efficacité de capacité compte et que votre workload est plus séquentiel ou tolérant à la latence.
Vous pouvez aussi mixer : garder un vdev miroir « voie rapide » pour le WAL ou les données chaudes et un vdev RAIDZ pour les données plus froides, mais attention à la complexité.
10) Quelle est la configuration la plus simple et sûre qui donne de bonnes performances ?
Pool en miroir, compression=lz4, atime=off, dataset data en 16K recordsize, dataset WAL en 16K recordsize, sync=standard.
Ajouter un SLOG mirroir seulement si la latence de commit l’exige.
Conclusion : prochaines étapes à exécuter cette semaine
Si vous exécutez déjà Postgres sur ZFS, pas besoin d’une refonte héroïque. Il vous faut deux choses : des frontières de dataset qui reflètent la réalité, et un chemin de sync qui
correspond à vos promesses de durabilité.
- Séparez les datasets en au moins data et WAL. Appliquez les propriétés de manière intentionnelle.
- Mesurez la latence des commits sous charge d’écriture et corrélez avec
zpool iostatetiostat -x. - Si le sync est le goulot, corrigez-le correctement : SLOG mirroir PLP ou compromis ciblés côté Postgres — pas de mensonges filesystem.
- Laissez la compression activée et cessez de payer pour atime.
- Faites des scrubs et la surveillance d’erreurs une habitude. « Pas d’incidents » n’est pas de la chance ; c’est détection plus discipline ennuyeuse.
L’objectif est un système qui ne nécessite pas un expert stockage pour l’exploitation quotidienne. ZFS peut faire cela pour PostgreSQL.
Mais il faut traiter la sémantique de sync comme un contrat, pas comme une suggestion.