Surveillance de fichiers Go : implémenter fsnotify
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.
🛠️ 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/fsnotifyinstallée viago 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
📖 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.
🔄 Second exemple
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.
watcher.Add("./data")
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.
for event := range watcher.Events { process(event) }
for event := range watcher.Events { go process(event) }
⚠️ Fuite de descripteurs
L’absence de fermeture du watcher sature les ressources du système.
w, _ := fsnotify.NewWatcher(); // sans defer close
w, _ := fsnotify.NewWatcher(); defer w.Close()
⚠️ Dépassement inotify
Trop de dossiers surveillés dépassent la limite du noyau Linux.
Surveiller 100 000 dossiers sans configurer le kernel.
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.Contextpour orchestrer l’arrêt propre du watcher avec vos workers. - Implémentez un mécanisme de debounce : si 50 événements
Writearrivent 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
Createvolumineux.
- 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.