Skip to content

Zfs supervision snapshots

Supervision ZFS et Automatisation des Clichés Sanoid

Gestion du stockage ZFS en environnement de production

Lorsqu'un serveur Linux héberge l'ensemble des données volumineuses et persistantes des conteneurs applicatifs (Nextcloud, bibliothèque photos Immich, téléchargements, bases de données PostgreSQL) sur un pool ZFS (ex: data-1).

ZFS intègre des mécanismes d'intégrité natifs (checksums systématiques, détection du bit rot). En production, trois réglages sont utiles : 1. Régulation de la mémoire vive (ZFS ARC) : ZFS a tendance par défaut à occuper jusqu'à 50% de la RAM disponible pour son cache de lecture, ce qui peut provoquer des évictions (OOM) de conteneurs gourmands en mémoire comme Ollama ou Immich en fonction des config. 2. Politique de clichés instantanés (snapshots) : Permet de restaurer des données en cas de suppression accidentelle ou d'anomalie sans impacter les performances d'écriture (Copy-on-Write). 3. Surveillance automatisée de la santé du pool : Pour être alerté dès le premier secteur défectueux ou la première erreur de checksum.


1. Régulation du Cache Mémoire ZFS ARC (zfs_arc_max)

Sur les machines disposant de moins de 64 Go de RAM, il est recommandé d'appliquer une limitation stricte du cache ARC à 2 Go (2147483648 octets).

Le paramètre est inscrit dans /etc/modprobe.d/zfs.conf et l'initramfs est reconstruit :

# /etc/modprobe.d/zfs.conf
# Limite l'Adaptive Replacement Cache (ARC) à 2 Go pour préserver la RAM des conteneurs
options zfs zfs_arc_max=2147483648

Cette limite garantit que le système d'exploitation et la couche applicative Docker conservent toujours une marge confortable de mémoire vive sans dépendre excessivement du swap.


2. Automatisation des Clichés avec Sanoid

Pour automatiser la création et la purge des instantanés sans recourir à des scripts crontab personnalisés fragiles, nous utilisons l'outil standard Sanoid.

Configuration : /etc/sanoid/sanoid.conf

La politique de production s'applique récursivement sur tout le pool cible (ex: data-1) :

# Configuration Sanoid pour le pool de données
[data-1]
use_template = production
recursive = yes

[template_production]
# Pas de snapshots à la minute
frequently = 0

# Rétention de 8 snapshots horaires (1 cliché par heure pendant 8h)
hourly = 8

# Pas de snapshots quotidiens redondants
daily = 0

# Rétention d'1 snapshot hebdomadaire (pour l'historique moyen terme)
weekly = 1

# Pas de rétention mensuelle ou annuelle sur ce pool
monthly = 0
yearly = 0

# Activation de la création automatique et du nettoyage des clichés expirés
autosnap = yes
autoprune = yes

Ordonnancement Systemd

Sanoid est orchestré par son timer natif /lib/systemd/system/sanoid.timer : * S'exécute automatiquement toutes les heures. * Crée le nouvel instantané sous la nomenclature standard data-1@autosnap_AAAA-MM-JJ_HH:MM:SS_hourly. * Supprime automatiquement les snapshots plus anciens que la fenêtre de rétention définie (autoprune = yes).


3. Contrôle d'Intégrité Automatisé : ansible-script-check_snapshot.sh

Afin de vérifier que le mécanisme de clichés ne s'est pas bloqué ou arrêté silencieusement, un script d'audit dédié valide périodiquement deux critères stricts :

  1. Le volume minimal de clichés : Au moins 9 snapshots récents doivent être présents (COUNT >= 9).
  2. L'âge maximal autorisé : Aucun snapshot conservé ne doit être orphelin ou âgé de plus de 7 jours (MAX_AGE_DAYS=7).
# Exemple d'appel manuel ou Ansible
./ansible-script-check_snapshot.sh data-1 9 7

Algorithme de vérification du script :

# 1. Extraction et comptage des clichés autosnap
SNAPSHOTS=$(zfs list -H -t snapshot -o name 2>/dev/null | grep "^${DATASET}@" | grep "autosnap" || true)
COUNT=$(echo "$SNAPSHOTS" | grep -v '^$' | wc -l)

if [ "$COUNT" -lt "$EXPECTED_COUNT" ]; then
    echo "✗ ERREUR : Nombre de snapshots insuffisant ($COUNT trouvé(s), $EXPECTED_COUNT attendu(s))."
    exit 1
fi

# 2. Analyse de l'horodatage de chaque instantané
NOW=$(date +%s)
MAX_AGE_SECONDS=$(( MAX_AGE_DAYS * 86400 ))
THRESHOLD=$(( NOW - MAX_AGE_SECONDS ))

while read -r snap; do
    datetime_str=$(echo "$snap" | sed -E 's/.*autosnap_([0-9]{4}-[0-9]{2}-[0-9]{2})_([0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1 \2/')
    snap_epoch=$(date -d "$datetime_str" +%s 2>/dev/null)
    if [ "$snap_epoch" -lt "$THRESHOLD" ]; then
        echo "✗ ERREUR : Le snapshot '$snap' est trop ancien (> $MAX_AGE_DAYS jours)."
        exit 1
    fi
done <<< "$SNAPSHOTS"

Si le script détecte un écart de conformité, Ansible remonte immédiatement une erreur critique qui déclenche l'envoi d'un courriel d'alerte par le plugin de notification SMTP (script-notify_failure.py).


4. Surveillance de la Santé du Pool (zpool status)

La surveillance matérielle du stockage peut être exécutée de manière programmée via Ansible ou un script de supervision :

  1. Exécution de zpool status data-1.
  2. Vérification que l'état global est strictement ONLINE.
  3. Recherche d'états critiques de dégradation (DEGRADED, FAULTED, OFFLINE, UNAVAIL).
  4. Parsing des compteurs d'erreurs d'E/S physiques :
  5. Erreurs de lecture (READ)
  6. Erreurs d'écriture (WRITE)
  7. Erreurs de checksum (CKSUM)
  8. En cas de compteur supérieur à zéro ou d'anomalie signalée sur un disque, le playbook capture le rapport complet et transmet immédiatement une alerte d'urgence par SMTP à l'administrateur.

5. Commandes Sysadmin pour la Restauration

Grâce aux snapshots ZFS Copy-on-Write, restaurer des données corrompues ou supprimées par erreur est quasi instantané :

# 1. Parcourir les fichiers d'un snapshot sans rien démonter (accès en lecture seule)
ls -la /mnt/data/.zfs/snapshot/autosnap_2026-09-26_14:00:00_hourly/Nextcloud/

# 2. Restaurer un fichier spécifique
cp /mnt/data/.zfs/snapshot/autosnap_2026-09-26_14:00:00_hourly/Nextcloud/document_perdu.pdf /mnt/data/Nextcloud/

# 3. Rollback complet d'un dataset (attention : écrase les modifications postérieures)
zfs rollback data-1@autosnap_2026-09-26_14:00:00_hourly
Cette architecture assure une résilience maximale aux pannes logiques et matérielles tout en préservant les performances globales du serveur d'applications.


6. Playbook Ansible Prêt à l'Emploi (playbook-zfs-sanoid.yaml)

Ce playbook autonome installe Sanoid, configure la limitation du cache mémoire ARC, déploie la politique de snapshots horaires/hebdomadaires, active le timer systemd et déploie le script de contrôle d'intégrité :

---
- name: Gestion du Stockage ZFS - Régulation ARC, Sanoid et Contrôle de Santé
  hosts: all
  become: true
  vars:
    # Nom du pool ou dataset ZFS à protéger
    zfs_dataset_name: "data-1"
    # Limitation du cache ARC en octets (ex: 2 Go pour laisser la RAM aux conteneurs)
    zfs_arc_limit_bytes: 2147483648
    # Politique de rétention des clichés
    sanoid_hourly: 8
    sanoid_weekly: 1
    # Nombre minimal de snapshots attendus pour validation
    expected_snapshots_min: 9
    # Âge maximum autorisé des clichés en jours
    max_snapshot_age_days: 7

  tasks:
    # -------------------------------------------------------------------------
    # 1. VÉRIFICATION DU MODULE ZFS ET RÉGULATION MÉMOIRE ARC
    # -------------------------------------------------------------------------
    - name: Vérifier la présence de la commande zfs
      ansible.builtin.command: which zfs
      register: zfs_bin_check
      changed_when: false
      failed_when: false

    - name: Interrompre si ZFS n'est pas installé sur la machine
      ansible.builtin.meta: end_host
      when: zfs_bin_check.rc != 0

    - name: Configurer la limite ZFS ARC dans /etc/modprobe.d/zfs.conf
      ansible.builtin.copy:
        dest: /etc/modprobe.d/zfs.conf
        mode: '0644'
        owner: root
        group: root
        content: |
          # Limite maximale de l'Adaptive Replacement Cache (ARC)
          options zfs zfs_arc_max={{ zfs_arc_limit_bytes }}
      register: zfs_arc_conf

    - name: Mettre à jour l'initramfs si la configuration ARC a changé
      ansible.builtin.command: update-initramfs -u
      when: zfs_arc_conf.changed

    # -------------------------------------------------------------------------
    # 2. INSTALLATION ET CONFIGURATION DE SANOID
    # -------------------------------------------------------------------------
    - name: Installer le paquet Sanoid
      ansible.builtin.apt:
        name: sanoid
        state: present
        update_cache: true

    - name: S'assurer de la présence du dossier /etc/sanoid
      ansible.builtin.file:
        path: /etc/sanoid
        state: directory
        mode: '0755'
        owner: root
        group: root

    - name: Déployer la configuration de politique Sanoid (/etc/sanoid/sanoid.conf)
      ansible.builtin.copy:
        dest: /etc/sanoid/sanoid.conf
        mode: '0644'
        owner: root
        group: root
        content: |
          # Configuration Sanoid générée par Ansible
          [{{ zfs_dataset_name }}]
          use_template = production
          recursive = yes

          [template_production]
          frequently = 0
          hourly = {{ sanoid_hourly }}
          daily = 0
          weekly = {{ sanoid_weekly }}
          monthly = 0
          yearly = 0
          autosnap = yes
          autoprune = yes

    - name: Activer et démarrer le timer systemd Sanoid
      ansible.builtin.systemd:
        name: sanoid.timer
        enabled: true
        state: started

    # -------------------------------------------------------------------------
    # 3. DÉPLOIEMENT DU SCRIPT D'AUDIT ET DE VALIDATION D'INTÉGRITÉ
    # -------------------------------------------------------------------------
    - name: Déployer le script d'audit des snapshots dans /usr/local/bin
      ansible.builtin.copy:
        dest: /usr/local/bin/check_zfs_snapshots.sh
        mode: '0755'
        owner: root
        group: root
        content: |
          #!/bin/bash
          # Audit automatisé des snapshots Sanoid
          set -euo pipefail
          DATASET="${1:-{{ zfs_dataset_name }}}"
          EXPECTED_COUNT="${2:-{{ expected_snapshots_min }}}"
          MAX_AGE_DAYS="${3:-{{ max_snapshot_age_days }}}"

          SNAPSHOTS=$(zfs list -H -t snapshot -o name 2>/dev/null | grep "^${DATASET}@" | grep "autosnap" || true)
          COUNT=$(echo "$SNAPSHOTS" | grep -v '^$' | wc -l)

          echo "[ZFS Audit] Snapshots trouvés pour $DATASET : $COUNT (Attendu: >= $EXPECTED_COUNT)"

          if [ "$COUNT" -lt "$EXPECTED_COUNT" ]; then
              echo "[ZFS Audit] Nombre de snapshots insuffisant pour $DATASET ($COUNT < $EXPECTED_COUNT)."
              exit 1
          fi

          NOW=$(date +%s)
          MAX_AGE_SECONDS=$(( MAX_AGE_DAYS * 86400 ))
          THRESHOLD=$(( NOW - MAX_AGE_SECONDS ))

          while read -r snap; do
              [ -z "$snap" ] && continue
              datetime_str=$(echo "$snap" | sed -E 's/.*autosnap_([0-9]{4}-[0-9]{2}-[0-9]{2})_([0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1 \2/')
              snap_epoch=$(date -d "$datetime_str" +%s 2>/dev/null || true)
              if [ -n "$snap_epoch" ] && [ "$snap_epoch" -lt "$THRESHOLD" ]; then
                  echo "[ZFS Audit] Snapshot trop ancien détecté : $snap ($datetime_str)"
                  exit 1
              fi
          done <<< "$SNAPSHOTS"

          echo "[ZFS Audit] Tous les snapshots de $DATASET sont conformes et récents."
          exit 0

    - name: Effectuer une vérification immédiate des snapshots
      ansible.builtin.command: >
        /usr/local/bin/check_zfs_snapshots.sh {{ zfs_dataset_name }} {{ expected_snapshots_min }} {{ max_snapshot_age_days }}
      register: audit_snapshots
      changed_when: false
      failed_when: false

    - name: Afficher le résultat de l'audit
      ansible.builtin.debug:
        msg: "{{ audit_snapshots.stdout_lines }}"

    # -------------------------------------------------------------------------
    # 4. SURVEILLANCE DE L'ÉTAT DU POOL (ZPOOL STATUS)
    # -------------------------------------------------------------------------
    - name: Interroger la santé matérielle du pool ZFS
      ansible.builtin.command: zpool status {{ zfs_dataset_name }}
      register: zpool_status_cmd
      changed_when: false

    - name: Valider que le statut est ONLINE et exempt d'erreurs
      ansible.builtin.assert:
        that:
          - "'ONLINE' in zpool_status_cmd.stdout"
          - "'DEGRADED' not in zpool_status_cmd.stdout"
          - "'FAULTED' not in zpool_status_cmd.stdout"
          - "'errors: No known data errors' in zpool_status_cmd.stdout"
        fail_msg: "Attention : Le pool ZFS {{ zfs_dataset_name }} présente des dégradations ou des erreurs."
        success_msg: "Le pool ZFS {{ zfs_dataset_name }} est sain (ONLINE) et sans erreur."

Comment l'exécuter :

ansible-playbook -i "mon_serveur_zfs," -u root playbook-zfs-sanoid.yaml
Ce playbook est totalement idempotent : il peut être inclus dans vos routines de maintenance ou exécuté lors du provisionnement initial d'un serveur de stockage.