Commit initial : réplication bidirectionnelle ZFS avec NFS HA
This commit is contained in:
commit
996b5c6c8e
4 changed files with 441 additions and 0 deletions
209
README.md
Normal file
209
README.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# Réplication Bidirectionnelle ZFS avec Serveur NFS Hautement Disponible
|
||||
|
||||
Une implémentation prête pour la production d'un stockage NFS hautement disponible utilisant Proxmox HA, ZFS, et Sanoid/Syncoid pour la réplication bidirectionnelle automatique.
|
||||
|
||||
## Contexte du Projet
|
||||
|
||||
Ce projet répond au défi de créer une solution de stockage redondante et hautement disponible en utilisant des pools ZFS indépendants sur du matériel standard, spécifiquement conçue pour du stockage de données froides avec des disques durs 3.5" connectés en USB.
|
||||
|
||||
### Le Défi
|
||||
|
||||
- **Contraintes matérielles** : Deux disques durs SATA 3.5" dans des boîtiers USB, sur des nœuds physiques différents
|
||||
- **Caractéristiques des données** : Stockage froid (fichiers média, archives) avec écritures peu fréquentes et lectures importantes
|
||||
- **Besoins de disponibilité** : Nécessité d'un basculement automatique avec un temps d'arrêt minimal
|
||||
- **Infrastructure** : Cluster Proxmox HA existant avec plusieurs nœuds
|
||||
|
||||
### La Solution
|
||||
|
||||
Les disques étant connectés en USB sur des nœuds physiques séparés, les solutions classiques (miroir ZFS local, DRBD bloc par bloc, ou systèmes de fichiers distribués lourds comme Ceph/GlusterFS) sont soit impossibles, soit disproportionnées pour ce cas d'usage.
|
||||
|
||||
Cette architecture implémente une approche plus simple et efficace :
|
||||
|
||||
1. **Pools ZFS indépendants** sur des nœuds Proxmox séparés (un disque par nœud)
|
||||
2. **Réplication bidirectionnelle au niveau ZFS** utilisant Sanoid/Syncoid avec détection automatique de la direction
|
||||
3. **Modèle actif-passif** où le nœud hébergeant le conteneur LXC NFS devient le maître de réplication
|
||||
4. **Basculement automatique** exploitant Proxmox HA pour une migration transparente
|
||||
|
||||
Cette approche fournit une redondance adaptée aux données froides tout en restant simple à maintenir et optimisée pour du stockage USB.
|
||||
|
||||
## Vue d'Ensemble de l'Architecture
|
||||
|
||||
### Topologie du Cluster
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Cluster Proxmox HA │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ acemagician │ │ elitedesk │ │
|
||||
│ │ (192.168.100.10)│◄────────────►│ (192.168.100.20) │ │
|
||||
│ │ │ Réplication │ │ │
|
||||
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
|
||||
│ │ │ zpool1 │ │ Syncoid │ │ zpool1 │ │ │
|
||||
│ │ │ (HDD USB) │ │ │ │ (HDD USB) │ │ │
|
||||
│ │ └────────────┘ │ │ └────────────┘ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ ┌────────────┐ │ │
|
||||
│ │ │ │ │ LXC 103 │ │ │
|
||||
│ │ │ │ │ NFS Server │ │ │
|
||||
│ │ │ │ └────────────┘ │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ thinkpad │ │
|
||||
│ │ (192.168.100.30) │ │
|
||||
│ │ Nœud témoin │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Composants Clés
|
||||
|
||||
- **Cluster Proxmox HA** : 2 nœuds de production + 1 nœud témoin pour le quorum
|
||||
- **Pools ZFS indépendants** : `zpool1` sur chaque nœud de production (un seul HDD 3.5" connecté en USB)
|
||||
- **Conteneur LXC (CTID 103)** : Serveur NFS avec rootfs sur LINSTOR/DRBD pour le basculement HA
|
||||
- **Sanoid** : Gestion automatisée des snapshots avec politiques de rétention configurables
|
||||
- **Syncoid** : Réplication ZFS efficace avec support de reprise
|
||||
- **Automatisation Systemd** : Exécution basée sur un timer toutes les 10 minutes
|
||||
|
||||
### Pourquoi Cette Architecture ?
|
||||
|
||||
**Contraintes de Déploiement** :
|
||||
- Les disques sont connectés en USB sur des nœuds physiques distincts, empêchant un miroir ZFS local (qui nécessite les disques sur le même nœud)
|
||||
- Les solutions de stockage distribué (Ceph, GlusterFS) sont surdimensionnées pour ce cas d'usage et consomment trop de ressources
|
||||
- DRBD réplique au niveau bloc, moins efficace que la réplication ZFS incrémentale basée sur les snapshots
|
||||
- La réplication ZFS via Syncoid offre le meilleur compromis simplicité/efficacité
|
||||
|
||||
**Optimisation pour Données Froides** :
|
||||
- Les fichiers média et archives ont des schémas de lecture élevée / écriture faible
|
||||
- Un intervalle de réplication de 10 minutes est acceptable (faibles exigences RPO)
|
||||
- La réplication asynchrone n'impacte pas les performances de lecture
|
||||
- La bande passante USB 3.0 est suffisante pour les transferts delta de réplication incrémentale
|
||||
|
||||
**Rentabilité** :
|
||||
- Réutilise des disques durs 3.5" existants dans des boîtiers externes
|
||||
- Pas besoin de contrôleurs SAS coûteux ou de baies hot-swap
|
||||
- Solution légère comparée aux systèmes de fichiers distribués complexes
|
||||
|
||||
## Fonctionnement
|
||||
|
||||
### Réplication Actif-Passif
|
||||
|
||||
1. **Détection du maître** : Le script de réplication (`zfs-nfs-replica.sh`) effectue une triple vérification de sécurité pour confirmer que le conteneur LXC NFS fonctionne localement
|
||||
2. **Direction automatique** : Le nœud hébergeant le conteneur actif devient le maître de réplication et pousse les snapshots vers le nœud passif
|
||||
3. **Réplication complète du pool** : Tous les datasets sous `zpool1` sont répliqués récursivement en utilisant `syncoid --recursive`
|
||||
4. **Adaptation au basculement** : Lorsque Proxmox HA migre le LXC, la direction de réplication s'inverse automatiquement
|
||||
|
||||
### Triple Vérification de Sécurité
|
||||
|
||||
Avant d'initier la réplication, le script vérifie trois fois (avec des délais de 2 secondes) :
|
||||
- Le conteneur existe (`pct status 103`)
|
||||
- Le statut du conteneur est "running"
|
||||
- Le conteneur est réactif (test de santé `pct exec`)
|
||||
|
||||
Cela évite les scénarios de split-brain et garantit que seul le nœud actif réplique.
|
||||
|
||||
### Gestion des Snapshots
|
||||
|
||||
Sanoid crée des snapshots selon un calendrier défini :
|
||||
- **Horaire** : 24 snapshots (rétention de 1 jour)
|
||||
- **Quotidien** : 7 snapshots (rétention de 1 semaine)
|
||||
- **Mensuel** : 3 snapshots (rétention de 3 mois)
|
||||
- **Annuel** : 1 snapshot (rétention de 1 an)
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Réplication bidirectionnelle automatique** : S'adapte aux migrations Proxmox HA sans intervention manuelle
|
||||
- **Synchronisation récursive du pool** : Tous les datasets sous `zpool1` sont automatiquement inclus
|
||||
- **Contrôle de concurrence par verrou** : Empêche les tâches de réplication simultanées
|
||||
- **Gestion d'erreurs complète** : Valide la connectivité SSH, l'existence du pool et les opérations ZFS
|
||||
- **Journalisation détaillée** : Toutes les opérations sont journalisées dans syslog (facility: local0)
|
||||
- **Authentification SSH dédiée** : Paire de clés SSH isolée pour la sécurité de la réplication
|
||||
- **Connectivité basée sur IP** : Utilise des IPs statiques pour une communication inter-nœuds fiable
|
||||
|
||||
## Structure du Dépôt
|
||||
|
||||
```
|
||||
.
|
||||
├── README.md # Ce fichier
|
||||
├── zfs-nfs-replica.sh # Script principal de réplication
|
||||
├── zfs-nfs-replica.service # Définition du service systemd
|
||||
└── zfs-nfs-replica.timer # Configuration du timer systemd
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Surveillance
|
||||
|
||||
```bash
|
||||
# Vérifier l'état de la réplication
|
||||
systemctl status zfs-nfs-replica.timer
|
||||
journalctl -u zfs-nfs-replica.service
|
||||
|
||||
# Voir les snapshots sur tous les datasets
|
||||
zfs list -t snapshot -r zpool1
|
||||
|
||||
# Comparer les snapshots entre les nœuds
|
||||
diff <(ssh root@192.168.100.10 "zfs list -t snapshot -r zpool1 -o name") \
|
||||
<(ssh root@192.168.100.20 "zfs list -t snapshot -r zpool1 -o name")
|
||||
|
||||
# Vérifier quel nœud est actif
|
||||
ha-manager status
|
||||
pct status 103
|
||||
```
|
||||
|
||||
### Réplication Manuelle
|
||||
|
||||
```bash
|
||||
# Déclencher la réplication manuellement
|
||||
/usr/local/sbin/zfs-nfs-replica.sh
|
||||
|
||||
# Tester le comportement de basculement
|
||||
ha-manager migrate ct:103 elitedesk
|
||||
```
|
||||
|
||||
## Principes de Conception
|
||||
|
||||
- **Source de vérité** : Le nœud exécutant le conteneur LXC est toujours le maître
|
||||
- **Sécurité d'abord** : Triple vérification empêche la réplication depuis le mauvais nœud
|
||||
- **Portée complète du pool** : L'intégralité de zpool1 est répliquée récursivement, pas les datasets individuels
|
||||
- **Opération asynchrone** : Réplication indépendante des E/S NFS (intervalles de 10 minutes)
|
||||
- **Adaptation automatique** : Aucune intervention manuelle nécessaire lors des migrations HA
|
||||
- **Pools indépendants** : Chaque nœud maintient son propre pool non-mirroré
|
||||
|
||||
## Spécifications Techniques
|
||||
|
||||
- **Intervalle de réplication** : 10 minutes (configurable via le timer systemd)
|
||||
- **Délai initial** : 5 minutes après le démarrage
|
||||
- **Timeout de verrou** : Réplication concurrente empêchée via flock
|
||||
- **Timeout SSH** : 5 secondes pour les vérifications de connectivité
|
||||
- **Pool** : `zpool1` (codé en dur, doit exister sur les deux nœuds)
|
||||
- **Conteneur** : CTID 103, nom `nfs-server`
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Cluster Proxmox VE (testé sur 8.x)
|
||||
- Pools ZFS nommés `zpool1` sur les nœuds de production
|
||||
- Sanoid/Syncoid installés depuis le dépôt officiel Sanoid
|
||||
- Paire de clés SSH dédiée pour la réplication
|
||||
- Conteneur LXC avec rootfs sur LINSTOR/DRBD
|
||||
- Configuration Proxmox HA avec paramètres de priorité appropriés
|
||||
|
||||
## Limitations et Considérations
|
||||
|
||||
- **RPO** : Un intervalle de réplication de 10 minutes signifie une perte de données potentielle jusqu'à 10 minutes dans des scénarios catastrophiques
|
||||
- **Bande passante USB** : Vitesse de réplication limitée par le débit USB 3.0 (adapté aux données froides)
|
||||
- **Synchronisation initiale manuelle** : La première réplication depuis le nœud rempli doit être initiée manuellement (voir INSTALLATION.md)
|
||||
- **Point unique de défaillance** : Une panne du nœud actif nécessite une migration HA avant que les données ne soient accessibles
|
||||
- **Dépendance réseau** : La réplication nécessite une connectivité réseau stable entre les nœuds
|
||||
|
||||
## Licence
|
||||
|
||||
Ce projet est fourni tel quel pour un usage éducatif et en production. N'hésitez pas à l'adapter à vos besoins d'infrastructure.
|
||||
|
||||
## Auteur
|
||||
|
||||
BENE Maël
|
||||
|
||||
Développé pour une infrastructure NFS hautement disponible de homelab utilisant du matériel standard et des logiciels open-source.
|
||||
35
zfs-nfs-replica.service
Normal file
35
zfs-nfs-replica.service
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[Unit]
|
||||
Description=ZFS NFS HA Replication Service
|
||||
After=zfs-import.target zfs-mount.service pve-ha-lrm.service
|
||||
Requires=zfs-import.target zfs-mount.service
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/zfs-nfs-replica.sh
|
||||
|
||||
# Sécurité
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
# Ne pas redémarrer automatiquement en cas d'échec
|
||||
# (le timer s'en chargera à la prochaine exécution)
|
||||
Restart=no
|
||||
|
||||
# Timeout de 30 minutes (réplication peut être longue)
|
||||
TimeoutStartSec=1800
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=zfs-nfs-replica
|
||||
|
||||
# Ressources
|
||||
# Limiter l'utilisation CPU pour ne pas impacter les services
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
179
zfs-nfs-replica.sh
Normal file
179
zfs-nfs-replica.sh
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Script de réplication ZFS automatique pour NFS HA
|
||||
# À déployer sur acemagician et elitedesk
|
||||
#
|
||||
# Ce script :
|
||||
# - Vérifie 3 fois que le LXC nfs-server est actif localement
|
||||
# - Détermine le nœud distant automatiquement
|
||||
# - Réplique le dataset ZFS vers le nœud passif
|
||||
# - Utilise un verrou pour éviter les réplications concurrentes
|
||||
#
|
||||
# Auteur : BENE Maël
|
||||
# Version : 1.0
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
CTID=103
|
||||
CONTAINER_NAME="nfs-server"
|
||||
ZPOOL="zpool1" # Pool entier à répliquer (tous les datasets)
|
||||
CHECK_DELAY=2 # Délai entre chaque vérification (secondes)
|
||||
LOG_FACILITY="local0"
|
||||
SSH_KEY="/root/.ssh/id_ed25519_zfs_replication"
|
||||
|
||||
# Fonction de logging
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
logger -t "zfs-nfs-replica" -p "${LOG_FACILITY}.${level}" "$@"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $@" >&2
|
||||
}
|
||||
|
||||
# Fonction de vérification du statut du LXC
|
||||
check_lxc_running() {
|
||||
local attempt="$1"
|
||||
|
||||
log "info" "Vérification #${attempt}/3 du statut du LXC ${CTID} (${CONTAINER_NAME})"
|
||||
|
||||
# Vérifier que le CT existe
|
||||
if ! pct status "$CTID" &>/dev/null; then
|
||||
log "warning" "Le conteneur ${CTID} n'existe pas sur ce nœud"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Vérifier le statut
|
||||
local status
|
||||
status=$(pct status "$CTID" 2>/dev/null | awk '{print $2}')
|
||||
|
||||
if [[ "$status" != "running" ]]; then
|
||||
log "info" "Le conteneur ${CTID} n'est pas en cours d'exécution (statut: ${status})"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Vérification supplémentaire: le processus existe-t-il vraiment?
|
||||
if ! pct exec "$CTID" -- test -f /proc/1/cmdline 2>/dev/null; then
|
||||
log "warning" "Le conteneur ${CTID} semble running mais n'est pas responsive"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "info" "Vérification #${attempt}/3 réussie: LXC ${CTID} est actif"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Triple vérification de sécurité
|
||||
verify_lxc_is_active() {
|
||||
local check_count=0
|
||||
local success_count=0
|
||||
|
||||
for i in 1 2 3; do
|
||||
check_count=$((check_count + 1))
|
||||
|
||||
if check_lxc_running "$i"; then
|
||||
success_count=$((success_count + 1))
|
||||
else
|
||||
log "error" "Échec de la vérification #${i}/3"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Délai entre les vérifications (sauf après la dernière)
|
||||
if [[ $i -lt 3 ]]; then
|
||||
sleep "$CHECK_DELAY"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $success_count -eq 3 ]]; then
|
||||
log "info" "✓ Triple vérification réussie: le LXC ${CTID} est définitivement actif sur ce nœud"
|
||||
return 0
|
||||
else
|
||||
log "error" "✗ Triple vérification échouée: ${success_count}/3 vérifications réussies"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Détermination du nœud local et distant
|
||||
LOCAL_NODE=$(hostname)
|
||||
log "info" "Démarrage du script sur le nœud: ${LOCAL_NODE}"
|
||||
|
||||
# Déterminer le nœud distant et son IP
|
||||
case "$LOCAL_NODE" in
|
||||
"acemagician")
|
||||
REMOTE_NODE_NAME="elitedesk"
|
||||
REMOTE_NODE_IP="192.168.100.20"
|
||||
;;
|
||||
"elitedesk")
|
||||
REMOTE_NODE_NAME="acemagician"
|
||||
REMOTE_NODE_IP="192.168.100.10"
|
||||
;;
|
||||
*)
|
||||
log "error" "Nœud inconnu: ${LOCAL_NODE}. Ce script doit s'exécuter sur acemagician ou elitedesk."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
log "info" "Nœud distant configuré: ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP})"
|
||||
|
||||
# Triple vérification de sécurité
|
||||
if ! verify_lxc_is_active; then
|
||||
log "info" "Le LXC ${CTID} n'est pas actif sur ce nœud. Pas de réplication nécessaire."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Vérification de l'existence du pool
|
||||
if ! zpool list "$ZPOOL" &>/dev/null; then
|
||||
log "error" "Le pool ${ZPOOL} n'existe pas sur ce nœud"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verrou pour éviter les réplications concurrentes
|
||||
LOCKFILE="/var/run/zfs-replica-${ZPOOL}.lock"
|
||||
LOCKFD=200
|
||||
|
||||
# Fonction de nettoyage
|
||||
cleanup() {
|
||||
if [[ -n "${LOCK_ACQUIRED:-}" ]]; then
|
||||
log "info" "Libération du verrou"
|
||||
flock -u $LOCKFD
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Tentative d'acquisition du verrou (non-bloquant)
|
||||
eval "exec $LOCKFD>$LOCKFILE"
|
||||
if ! flock -n $LOCKFD; then
|
||||
log "info" "Une réplication est déjà en cours. Abandon."
|
||||
exit 0
|
||||
fi
|
||||
LOCK_ACQUIRED=1
|
||||
|
||||
log "info" "Verrou acquis. Début de la réplication."
|
||||
|
||||
# Vérification de la connectivité SSH vers le nœud distant
|
||||
if ! ssh -i "$SSH_KEY" -o ConnectTimeout=5 -o BatchMode=yes "root@${REMOTE_NODE_IP}" "echo OK" &>/dev/null; then
|
||||
log "error" "Impossible de se connecter à ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP}) via SSH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "info" "Connexion SSH vers ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP}) vérifiée"
|
||||
|
||||
# Vérification que le pool existe sur le nœud distant
|
||||
if ! ssh -i "$SSH_KEY" "root@${REMOTE_NODE_IP}" "zpool list ${ZPOOL}" &>/dev/null; then
|
||||
log "error" "Le pool ${ZPOOL} n'existe pas sur ${REMOTE_NODE_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Réplication avec syncoid (récursive pour tous les datasets)
|
||||
log "info" "Début de la réplication récursive: ${ZPOOL} → ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP}):${ZPOOL}"
|
||||
|
||||
# Syncoid utilise les options SSH via la variable d'environnement SSH
|
||||
export SSH="ssh -i ${SSH_KEY}"
|
||||
|
||||
if syncoid --recursive --no-sync-snap --quiet "$ZPOOL" "root@${REMOTE_NODE_IP}:$ZPOOL"; then
|
||||
log "info" "✓ Réplication récursive réussie vers ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP})"
|
||||
log "info" " Tous les datasets de ${ZPOOL} ont été synchronisés"
|
||||
exit 0
|
||||
else
|
||||
log "error" "✗ Échec de la réplication vers ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP})"
|
||||
exit 1
|
||||
fi
|
||||
18
zfs-nfs-replica.timer
Normal file
18
zfs-nfs-replica.timer
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[Unit]
|
||||
Description=ZFS NFS HA Replication Timer
|
||||
Requires=zfs-nfs-replica.service
|
||||
|
||||
[Timer]
|
||||
# Démarrage initial : 5 minutes après le boot
|
||||
OnBootSec=5min
|
||||
|
||||
# Exécution périodique : toutes les 10 minutes
|
||||
OnUnitActiveSec=10min
|
||||
|
||||
# Précision du timer (par défaut 1 minute)
|
||||
AccuracySec=1min
|
||||
|
||||
Unit=zfs-nfs-replica.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Loading…
Add table
Add a link
Reference in a new issue