surveillance de fichiers Go

Surveillance de fichiers Go : implémenter fsnotify

Tutoriel pas-à-pas GoIntermédiaire

Surveillance de fichiers Go : implémenter fsnotify

Le polling de fichiers avec os.Stat dans une boucle infinie est une aberration de performance. Cette méthode sature le CPU et multiplie les appels système inutiles.

La surveillance de fichiers Go via fsnotify permet de s’appuyer sur les mécanismes natifs du noyau, comme inotify sur Linux. Cela transforme une attente passive en un système réactif, essentiel pour des services comme un hub de modèles IA devant réagir instantanément à l’ajout de nouveaux poids.

Après cette lecture, vous saurez configurer un watcher, gérer la récursivité manuellement et éviter les fuites de descripteurs de fichiers.

surveillance de fichiers Go

🛠️ Prérequis

Pour tester ce guide, vous aurez besoin de l’environnement suivant :

  • Go 1.22 ou supérieur pour profiter des dernières optimisations de runtime.
  • Un système Linux (pour tester l’interaction avec inotify).
  • La bibliothèque github.com/fsnotify/fsnotify installée via go get.

📚 Comprendre surveillance de fichiers Go

Le noyau Linux utilise inotify pour notifier les processus des changements sur un inode. Chaque surveillance consomme un descripteur de fichier. La surveillance de fichiers Go avec fsnotify n’est qu’un wrapper autour de inotify (Linux), kqueue (BSD/macOS) et fsevents (macOS).

Schéma du flux d’événements :
Fichier (Write) -> Kernel (inotify) -> FD (File Descriptor) -> Go Channel (fsnotify) -> Votre application.

Contrairement à un simple scan, le coût CPU est quasi nul tant qu’aucun événement ne survient. Cependant, attention : fsnotify ne gère pas la récursivité nativement. Si vous surveillez /models, la création de /models/resnet50 ne sera pas détectée automatiquement sans ajout explicite du nouveau sous-répertoire.

🐹 Le code — surveillance de fichiers Go

Go
package main

import (
	"fmt"
	"log"

	"github.com/fsnotify/fsnotify"
)

func main() {
	// Création du watcher
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	// Ajout du répertoire à surveiller
	err = watcher.Add(".")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Surveillance active sur le répertoire courant...")

	// Boucle d'écoute des événements
	for {
		events, err := watcher.Events.
		if err != nil {
			log.Println("Erreur lecture events:", err)
			continue
		}

		for _, event := range events {
			// On ne traite que les écritures et les créations
			if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
				fmt.Printf("Événement détecté: %s (%s)\n", event.Name, event)
			}
		}
	}
}

📖 Explication

Dans le code_source, l’utilisation de event.Has(fsnotify.Write) est cruciale. Depuis les versions récentes de la bibliothèque, cette méthode bitwise est plus lisible que de comparer des masques manuellement.

Le code_source_2 utilise filepath.WalkDir introduit en Go 1.16. C’est plus performant que l’ancien filepath.Walk car il évite des appels os.Lstat inutiles. Lors de la surveillance de fichiers Go, c’est l’élément clé pour la performance lors du scan initial.

Un piège fréquent : lors d’une suppression de dossier, le watcher perd le suivi de ce dossier. Si vous recréez un dossier avec le même nom, vous devez ré-ajouter explicitement le chemin au watcher.

Documentation officielle Go

🔄 Second exemple

Go
package main

import (
	"os"
	"path/filepath"

	"github.com/fsnotify/fsnotify"
)

// WatchRecursive permet de surveiller un dossier et tous ses sous-dossiers
func WatchRecursive(path string) (*fsnotify.Watcher, error) {
	watcher, err := fsname.NewWatcher()
	if err != nil {
		return nil, err
	}

	// Parcours de l'arborescence pour ajouter chaque dossier
	err = filepath.WalkDir(path, func(subPath string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			// On ajoute uniquement les dossiers au watcher
			return watcher.Add(subPath)
		}
		return nil
	})

	if err != nil {
		return nil, err
	}

	return watcher, nil
}

Tutoriel pas-à-pas

La mise en place d’une surveillance de fichiers Go efficace suit un processus rigoureux pour éviter de saturer le système.

Étape 1 : Initialisation du watcher. Utilisez fsnotify.NewWatcher(). Cette fonction alloue les structures nécessaires et prépare le canal d’événements. Ne négligez jamais le defer watcher.Close(), car chaque instance ouverte consomme un descripron de fichier système.

Étape 2 : Configuration de la cible. Appelez watcher.Add(path). Si vous travaillez sur un projet de hub de modèles, ciblez le dossier ./weights. Si vous avez besoin de la récursivité, vous devez implémenter une fonction de parcours comme montré dans code_source_2. La doc officielle ne garantit pas la propagation automatique aux sous-dossiers.

Étape 3 : La boucle de traitement. Vous devez lire le canal watcher.Events. Attention, il existe aussi un canal watcher.Errors. Un piège classique est de ne lire que les événements. Si vous ignorez le canal d’erreurs, votre application peut continuer de tourner alors que le watcher est mort à cause d’un dépassement de limite système.

Étape 4 : Gestion de la concurrence. Dans un environnement de production, ne traitez pas la logique métier directement dans la boucle de lecture. Si le traitement d’un fichier prend 2 secondes, vous bloquez la lecture des événements suivants. Le canal va saturer, et vous perdrez des notifications. Envoyez les événements vers un pool de workers via un canal tamponné.

▶️ Exemple d’utilisation

Exécutez le code suivant dans un dossier contenant des fichiers de test :

# Création d'un fichier de test
touch test_model.bin
# Le programme affichera :
# Événement détecté: test_model.bin (create)
# Événement détecté: test_model.bin (write)

🚀 Cas d’usage avancés

1. Hot-reloading de configuration : Surveillez un fichier config.yaml. Dès qu’une modification est détectée, déclenchez un rechargement de la structure interne de votre application sans redémarrage.

2. Pipeline d’ingestion de modèles : Dans un hub d’IA, surveillez un dossier /incoming. L’événement Create déclenche l’extraction, la validation du checksum et le déplacement vers le stockage permanent.

3. Système de logs réactif : Un agent de monitoring qui analyse les logs en temps réel. En utilisant la surveillance de fichiers Go, l’agent ne consomme rien tant que le service loggé n’écrit pas de nouvelles lignes.

🐛 Erreurs courantes

⚠️ Oubli de la récursivité

Le watcher ne détecte pas les fichiers dans les sous-dossiers créés après le démarrage.

✗ Mauvais

watcher.Add("./data")
✓ Correct

filepath.WalkDir("./data", func(...) { watcher.Add(path) })

⚠️ Saturation du canal d'événements

Le traitement trop lent bloque le canal, provoquant la perte d’événements.

✗ Mauvais

for event := range watcher.Events { process(event) }
✓ Correct

for event := range watcher.Events { go process(event) }

⚠️ Fuite de descripteurs

L’absence de fermeture du watcher sature les ressources du système.

✗ Mauvais

w, _ := fsnotify.NewWatcher(); // sans defer close
✓ Correct

w, _ := fsnotify.NewWatcher(); defer w.Close()

⚠️ Dépassement inotify

Trop de dossiers surveillés dépassent la limite du noyau Linux.

✗ Mauvais

Surveiller 100 000 dossiers sans configurer le kernel.
✓ Correct

echo 100000 > /proc/sys/fs/inotify/max_user_watches

✅ Bonnes pratiques

Pour une surveillance de fichiers Go professionnelle, suivez ces règles :

  • Utilisez toujours un context.Context pour orchestrer l’arrêt propre du watcher avec vos workers.
  • Implémentez un mécanisme de debounce : si 50 événements Write arrivent en 10ms, ne déclenchez votre logique qu’une seule fois.
  • Ne surveillez jamais la racine du système de fichiers /.
  • Utilisez des canaux tamponnés pour séparer la lecture des événements et leur traitement.
  • Vérifiez toujours la disponibilité de l’espace disque avant de traiter un événement Create volumineux.
Points clés

  • fsnotify utilise les primitives natives du noyau (inotify/kqueue).
  • La récursivité n'est pas gérée par défaut, il faut parcourir l'arbre.
  • Le traitement des événements doit être asynchrone pour éviter les pertes.
  • La gestion des erreurs du canal <code>watcher.Errors</code> est obligatoire.
  • Le polling est à bannir au profit de l'approche événementielle.
  • La limite de <code>max_user_watches</code> est un goulot d'étranglement Linux classique.
  • Le <code>defer watcher.Close()</code> est vital pour la stabilité système.
  • Le pattern <em>debounce</em> est indispensable pour les fichiers lourds.

❓ Questions fréquentes

Est-ce que fsnotify fonctionne sur Windows ?

Oui, il utilise les notifications de changement de fichier de l’API Windows, mais le comportement peut différer légèrement de inotify.

Pourquoi mon application ne voit pas les nouveaux fichiers dans les sous-dossiers ?

Comme mentionné, fsnotify ne suit pas les arborescences de manière récursive. Vous devez ajouter chaque nouveau dossier manuellement.

Peut-on surveiller des fichiers réseau (NFS/SMB) ?

C’est risqué. Les protocoles réseau ne garantissent pas toujours la propagation des événements au noyau client.

Comment gérer les fichiers très lourds lors d'une écriture ?

Utilisez un timer de debounce. Attendez que l’événement CloseWrite (si disponible) ou une absence d’activité de 500ms survienne.

📚 Sur le même blog

🔗 Le même sujet sur nos autres blogs

📝 Conclusion

La surveillance de fichiers Go est un levier de performance majeur pour les architectures réactives. En maîtrisant fsnotify, vous évitez le gaspillage de cycles CPU et garantissez une latence minimale pour vos services. Pour approfondir les mécanismes de synchronisation, consultez la documentation Go officielle. Un dernier conseil : surveillez toujours la consommation de mémoire de vos workers si vous lancez une goroutine par événement.

Publications similaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *