Comment nous avons chassé pendant deux semaines le bogue NFS dans le noyau Linux

Une description détaillée des recherches de bogues de la tâche GitLab qui ont conduit au correctif pour le noyau Linux


Le 14 septembre, le support GitLab a signalé un problème critique survenu à l'un de nos clients: tout d'abord, GitLab fonctionne correctement, puis les utilisateurs obtiennent une erreur. Ils ont essayé de cloner certains référentiels via Git, et tout à coup un message incompréhensible est apparu à propos d'un fichier obsolète: Stale file error périmé. L'erreur a persisté pendant longtemps et n'a pas fonctionné jusqu'à ce que l'administrateur système démarre manuellement ls dans le répertoire lui-même.


J'ai dû étudier les mécanismes internes de Git et le système de fichiers réseau NFS. En conséquence, nous avons trouvé un bogue dans le client NFS Linux v4.0, Trond Myklebust a écrit un correctif pour le noyau , et depuis le 26 octobre ce correctif est inclus dans le noyau Linux principal .


Dans cet article, je vais vous expliquer comment nous avons étudié le problème, dans quelle direction nous avons pensé et quels outils nous avons utilisés pour suivre le bug. Nous avons été inspirés par l'excellent travail de détective d'Oleg Dashevsky décrit dans le post «Comment j'ai chassé une fuite de mémoire en Ruby pendant deux semaines» .



C'est également un excellent exemple de la façon dont le débogage open source est un sport d'équipe impliquant de nombreuses personnes, entreprises et pays. La devise de GitLab, « Tout le monde peut contribuer », est vraie non seulement pour GitLab lui-même, mais aussi pour d'autres projets open source, tels que le noyau Linux.


Reproduction d'insectes


Nous avons conservé NFS sur GitLab.com pendant de nombreuses années, mais avons ensuite cessé de l'utiliser pour accéder aux données du référentiel sur les machines avec des applications. Nous avons transféré tous les appels Git vers Gitaly . Nous prenons en charge NFS pour les clients qui gèrent leurs installations sur GitLab mais n'ont jamais rencontré le même problème que le client susmentionné.


Le client a donné quelques conseils utiles :


  1. Texte d'erreur complet: fatal: Couldn't read ./packed-refs: Stale file handle .
  2. Apparemment, le problème est survenu lorsque le client a démarré manuellement la récupération de place dans Git avec la commande git gc .
  3. L'erreur a disparu lorsque l'administrateur système a démarré l'utilitaire ls dans le répertoire.
  4. L'erreur a disparu à la fin du processus git gc .

Il est clair que les deux premiers points sont liés. Lorsque vous soumettez des modifications à la branche Git, Git crée un lien faible - un nom de fichier long qui indique le nom de la branche pour la validation. Par exemple, lors de l'envoi au master , un fichier appelé refs/heads/master sera créé dans le référentiel:


 $ cat refs/heads/master 2e33a554576d06d9e71bfd6814ee9ba3a7838963 

La commande git gc effectue plusieurs tâches. Par exemple, il collecte ces liens faibles (refs) et les regroupe dans un seul fichier appelé packed-refs . Cela accélère un peu le travail, car la lecture d'un gros fichier est plus facile que de nombreux petits. Par exemple, après avoir exécuté la commande git gc , le fichier packed-refs pourrait ressembler à ceci:


 # pack-refs with: peeled fully-peeled sorted 564c3424d6f9175cf5f2d522e10d20d781511bf1 refs/heads/10-8-stable edb037cbc85225261e8ede5455be4aad771ba3bb refs/heads/11-0-stable 94b9323033693af247128c8648023fe5b53e80f9 refs/heads/11-1-stable 2e33a554576d06d9e71bfd6814ee9ba3a7838963 refs/heads/master 

Comment le fichier packed-refs créé? Pour le savoir, nous avons exécuté la commande strace git gc où nous avions un maillon faible. Voici les lignes pertinentes:


 28705 open("/tmp/libgit2/.git/packed-refs.lock", O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC, 0666) = 3 28705 open(".git/packed-refs", O_RDONLY) = 3 28705 open("/tmp/libgit2/.git/packed-refs.new", O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC, 0666) = 4 28705 rename("/tmp/libgit2/.git/packed-refs.new", "/tmp/libgit2/.git/packed-refs") = 0 28705 unlink("/tmp/libgit2/.git/packed-refs.lock") = 0 

Les appels système ont montré que la commande git gc :


  1. Ouvert packed-refs.lock . Cela indique aux autres processus que le fichier packed-refs est verrouillé et ne peut pas changer.
  2. Ouvert packed-refs.new .
  3. J'ai packed-refs.new maillons faibles dans packed-refs.new .
  4. Renommé packed-refs.new en packed-refs .
  5. Suppression de packed-refs.lock .
  6. Suppression des maillons faibles.

Le point clé ici est le quatrième, c'est-à-dire le changement de nom, où Git introduit le fichier packed-refs . git gc collecte non seulement les liens faibles, mais effectue également une tâche beaucoup plus gourmande en ressources - il recherche et supprime les objets inutilisés. Dans les grands référentiels, cela peut durer plus d'une heure.


Et nous nous sommes demandé: dans les grands référentiels, git gc garde-t-il le fichier ouvert pendant le nettoyage? Nous avons étudié les journaux strace , lancé l'utilitaire lsof , et voici ce que nous avons appris sur le processus git gc :


image


Comme vous pouvez le voir, le fichier packed-refs ferme à la toute fin, après le processus potentiellement long des Garbage collect objects de Garbage collect objects .


La question suivante s'est donc posée: comment se comporte NFS lorsque le fichier packed-refs est ouvert sur un nœud, et que l'autre le renomme à ce moment-là?


«À des fins scientifiques», nous avons demandé au client de mener une expérience sur deux machines différentes (Alice et Bob):
1) Dans le volume partagé NFS, créez deux fichiers: test1.txt et test2.txt avec des contenus différents, afin qu'ils soient plus faciles à distinguer:


 alice $ echo "1 - Old file" > /path/to/nfs/test1.txt alice $ echo "2 - New file" > /path/to/nfs/test2.txt 

2) Sur la machine d'Alice, le fichier test1.txt doit être ouvert:


 alice $ irb irb(main):001:0> File.open('/path/to/nfs/test1.txt') 

3) Sur la machine d'Alice, affichez en continu le contenu de test1.txt :


 alice $ while true; do cat test1.txt; done 

4) Ensuite, sur la machine de Bob, exécutez la commande:


 bob $ mv -f test2.txt test1.txt 

La dernière étape reproduit ce que git gc fait avec le fichier packed-refs lors du remplacement d'un fichier existant.
Sur la machine du client, le résultat ressemblait à ceci:


 1 - Old file 1 - Old file 1 - Old file cat: test1.txt: Stale file handle 

Voilà! Nous semblons avoir contrôlé le problème de manière contrôlée. Mais dans la même expérience sur un serveur Linux NFS, ce problème ne s'est pas produit. Le résultat était attendu - après avoir renommé le nouveau contenu a été accepté:


 1 - Old file 1 - Old file 1 - Old file 2 - New file <--- RENAME HAPPENED 2 - New file 2 - New file 

D'où vient cette différence de comportement? Il s'avère que le client a utilisé le stockage Isilon NFS , qui ne supportait que NFS v4.0. Lorsque nous avons changé les paramètres de connexion en v4.0 en utilisant le paramètre vers=4.0 dans /etc/fstab , le test a montré un résultat différent pour le serveur NFS Linux:


 1 - Old file 1 - Old file 1 - Old file 1 - Old file <--- RENAME HAPPENED 1 - Old file 1 - Old file 

Au lieu du descripteur de Stale file handle obsolète Stale file handle périmé Stale file handle serveur Linux NFS v4.0 affiche un contenu obsolète. Il s'avère que la différence de comportement peut être expliquée par les spécifications NFS. De RFC 3010 :


Le descripteur de fichier peut devenir obsolète ou expirer lorsqu'il est renommé, mais pas toujours. Les implémenteurs de serveur sont invités à prendre des mesures pour garantir que les descripteurs de fichiers n'expirent pas et n'expirent pas de cette manière.

En d'autres termes, les serveurs NFS peuvent choisir comment se comporter lorsqu'un fichier est renommé, et le serveur NFS renvoie assez raisonnablement une Stale file error dans de tels cas. Nous avons suggéré que la cause du problème est la même, bien que les résultats soient différents. Nous pensions que c'était une vérification du cache, car l'utilitaire ls dans le répertoire a supprimé l'erreur. Nous avions maintenant un scénario de test reproductible, et nous nous sommes tournés vers des experts - les responsables Linux NFS.


False Trace: délégation sur un serveur NFS


Lorsque nous avons réussi à reproduire l'erreur étape par étape, j'ai écrit aux contacts Linux NFS à propos de ce que nous avions appris. J'ai correspondu avec Bruce Fields, le mainteneur du serveur NFS Linux pendant une semaine, et il a suggéré que le bogue était dans NFS et que j'avais besoin d'étudier le trafic réseau. Il pensait que le problème était la délégation de tâches sur le serveur NFS.


Qu'est-ce que la délégation sur un serveur NFS?


En un mot, la version NFS v4 a une fonction de délégation pour accélérer l'accès aux fichiers. Le serveur peut déléguer l'accès en lecture ou en écriture au client afin que le client n'ait pas à demander constamment au serveur si le fichier a été modifié par un autre client. Autrement dit, déléguer un dossier, c'est comme prêter à quelqu'un son cahier et dire: «Vous écrivez ici, et je le prendrai quand je serai prêt.» Et une personne n'a pas besoin de demander un carnet chaque fois que vous devez écrire quelque chose - elle a une totale liberté d'action jusqu'à ce que le carnet soit retiré. Dans NFS, une demande de retour d'un bloc-notes est appelée révocation de délégation.


Un bogue dans la révocation de la délégation NFS pourrait expliquer le problème de Stale file handle . Rappelez-vous comment test1.txt été ouvert dans l' test1.txt d'Alice, puis test2.txt remplacé. Le serveur n'a peut-être pas pu révoquer la délégation pour test1.txt , ce qui a conduit à un état non valide. Pour tester cette théorie, nous avons enregistré le trafic NFC avec l'utilitaire tcpdump et l'avons visualisé à l'aide de Wireshark.


Wireshark est un excellent outil open source pour analyser le trafic réseau, en particulier pour explorer NFS en action. Nous avons enregistré la trace à l'aide de la commande suivante sur un serveur NFS:


 tcpdump -s 0 -w /tmp/nfs.pcap port 2049 

Cette commande enregistre tout le trafic NFS qui passe généralement par le port TCP 2049. Comme notre expérience a réussi avec NFS v4.1, mais pas avec NFS v4.0, nous avons pu comparer le comportement de NFS dans le cas de travail et de non fonctionnement. Avec Wireshark, nous avons constaté le comportement suivant:


NFS v4.0 (fichier obsolète)


image


Ce diagramme montre qu'à l'étape 1, Alice ouvre test1.txt et reçoit un descripteur de fichier NFS avec l'identifiant stateid 0x3000. Lorsque Bob essaie de renommer le fichier, le serveur NFS demande de réessayer en envoyant le message NFS4ERR_DELAY , et il rappelle la délégation d'Alice via le message CB_RECALL (étape 3). Alice renvoie la délégation (DELEGRETURN à l'étape 4) et Bob essaie d'envoyer à nouveau le message RENAME (étape 5). RENAME est exécuté dans les deux cas, mais Alice continue de lire le fichier avec le même descripteur.


NFS v4.1 (cas de travail)


image


Ici, la différence est visible à l'étape 6. Dans NFS v4.0 (avec un fichier obsolète), Alice essaie à nouveau d'utiliser le même stateid . Dans NFS v4.1 (cas de travail), Alice effectue des opérations LOOKUP et OPEN supplémentaires, de sorte que le serveur renvoie un état différent. Dans la v4.0, il n'envoie aucun message supplémentaire. Cela explique pourquoi Alice voit un contenu obsolète - elle utilise un ancien descripteur.


Pourquoi Alice décide-t-elle soudainement d'une LOOKUP supplémentaire? Apparemment, le rappel de la délégation a réussi, mais il semble qu'un problème subsiste. Par exemple, l'étape d'invalidité est ignorée. Pour vérifier cela, nous avons exclu la délégation NFS sur le serveur NFS lui-même avec cette commande:


 echo 0 > /proc/sys/fs/leases-enable 

Nous avons répété l'expérience, mais le problème n'a pas disparu. Nous nous sommes assurés que le problème n'était pas dans le serveur ou la délégation NFS, et avons décidé de regarder le client NFS dans le noyau.


Creuser plus profondément: client NFS Linux


La première question à laquelle nous avons dû répondre aux responsables NFS était:


Ce problème persiste-t-il dans la dernière version du noyau?


Le problème s'est produit dans les noyaux CentOS 7.2 et Ubuntu 16.04 avec les versions 3.10.0-862.11.6 et 4.4.0-130, respectivement. Mais les deux cœurs étaient à la traîne de la dernière version, qui était à l'époque 4.19-rc2.


Nous avons déployé la nouvelle machine virtuelle Ubuntu 16.04 sur Google Cloud Platform (GCP), cloné le dernier noyau Linux et configuré l'environnement de développement du noyau. Nous avons créé le fichier .config à l'aide de menuconfig et vérifié que:


  1. Le pilote NFS est compilé en tant que module ( CONFIG_NFSD=m ).
  2. Les paramètres corrects du noyau GCP sont correctement spécifiés.

La génétique suit l'évolution en temps réel par la drosophile, et avec le premier élément, nous avons pu rapidement apporter des corrections au client NFS sans redémarrer le noyau. Le deuxième point garantit que le noyau démarrera après l'installation. Heureusement, nous étions satisfaits des paramètres du noyau par défaut.


Nous nous sommes assurés que le problème du fichier obsolète ne disparaissait pas dans la dernière version du noyau. Nous nous sommes demandé:


  1. Où se pose exactement le problème?
  2. Pourquoi cela se produit-il dans NFS v4.0, mais pas dans v4.1?

Pour répondre à ces questions, nous avons exploré le code source NFS. Nous n'avions pas de débogueur de noyau, nous avons donc envoyé deux types d'appels au code source:


  1. pr_info() (c'était printk ).
  2. dump_stack() : il montre la trace de la pile pour l'appel de fonction en cours.

Par exemple, la première chose que nous avons faite a été de nous connecter à la fonction nfs4_file_open() dans fs/nfs/nfs4file.c :


 static int nfs4_file_open(struct inode *inode, struct file *filp) { ... pr_info("nfs4_file_open start\n"); dump_stack(); 

Bien sûr, nous pouvions dprintk avec le débogage dynamique Linux ou utiliser rpcdebug , mais nous voulions ajouter nos propres messages pour vérifier les changements.


Après chaque modification, nous avons recompilé le module et l'avons réinstallé dans le noyau en utilisant les commandes:


 make modules sudo umount /mnt/nfs-test sudo rmmod nfsv4 sudo rmmod nfs sudo insmod fs/nfs/nfs.ko sudo mount -a 

Avec le module NFS, nous avons pu répéter les expériences et recevoir des messages pour comprendre le code NFS. Par exemple, vous pouvez immédiatement voir ce qui se passe lorsque l'application appelle open() :


 Sep 24 20:20:38 test-kernel kernel: [ 1145.233460] Call Trace: Sep 24 20:20:38 test-kernel kernel: [ 1145.233462] dump_stack+0x8e/0xd5 Sep 24 20:20:38 test-kernel kernel: [ 1145.233480] nfs4_file_open+0x56/0x2a0 [nfsv4] Sep 24 20:20:38 test-kernel kernel: [ 1145.233488] ? nfs42_clone_file_range+0x1c0/0x1c0 [nfsv4] Sep 24 20:20:38 test-kernel kernel: [ 1145.233490] do_dentry_open+0x1f6/0x360 Sep 24 20:20:38 test-kernel kernel: [ 1145.233492] vfs_open+0x2f/0x40 Sep 24 20:20:38 test-kernel kernel: [ 1145.233493] path_openat+0x2e8/0x1690 Sep 24 20:20:38 test-kernel kernel: [ 1145.233496] ? mem_cgroup_try_charge+0x8b/0x190 Sep 24 20:20:38 test-kernel kernel: [ 1145.233497] do_filp_open+0x9b/0x110 Sep 24 20:20:38 test-kernel kernel: [ 1145.233499] ? __check_object_size+0xb8/0x1b0 Sep 24 20:20:38 test-kernel kernel: [ 1145.233501] ? __alloc_fd+0x46/0x170 Sep 24 20:20:38 test-kernel kernel: [ 1145.233503] do_sys_open+0x1ba/0x250 Sep 24 20:20:38 test-kernel kernel: [ 1145.233505] ? do_sys_open+0x1ba/0x250 Sep 24 20:20:38 test-kernel kernel: [ 1145.233507] __x64_sys_openat+0x20/0x30 Sep 24 20:20:38 test-kernel kernel: [ 1145.233508] do_syscall_64+0x65/0x130 

Que sont ces do_dentry_open et vfs_open ? Linux possède un système de fichiers virtuel ( VFS ), une couche d'abstraction qui fournit une interface commune à tous les systèmes de fichiers. La documentation VFS indique:


VFS implémente open (2), stat (2), chmod (2) et d'autres appels système. Le système VFS utilise l'argument de nom de chemin qui leur est transmis pour rechercher dans le cache des entrées de répertoire (cache dentry ou dcache). Cela fournit un moteur de recherche très rapide qui convertit le nom du chemin (ou le nom du fichier) en dentisterie spécifique. Dentry réside dans la RAM et n'est jamais enregistré sur le disque - ils n'existent que pour les performances.

Et cela nous est apparu - que se passe-t-il si le problème est dans la cache de la dentisterie?


Nous avons remarqué que le cache dentry est généralement vérifié dans fs/nfs/dir.c Nous étions particulièrement intéressés par la fonction nfs4_lookup_revalidate() , et à titre expérimental, nous l'avons fait fonctionner plus tôt:


 diff --git a/fs/nfs/dir.cb/fs/nfs/dir.c index 8bfaa658b2c1..ad479bfeb669 100644 --- a/fs/nfs/dir.c +++ b/fs/nfs/dir.c @@ -1159,6 +1159,7 @@ static int nfs_lookup_revalidate(struct dentry *dentry, unsigned int flags) trace_nfs_lookup_revalidate_enter(dir, dentry, flags); error = NFS_PROTO(dir)->lookup(dir, &dentry->d_name, fhandle, fattr, label); trace_nfs_lookup_revalidate_exit(dir, dentry, flags, error); + goto out_bad; if (error == -ESTALE || error == -ENOENT) goto out_bad; if (error) 

Et dans cette expérience, un problème de fichier obsolète ne s'est pas produit! Enfin, nous avons attaqué la piste.


Pour découvrir pourquoi le problème ne s'est pas produit dans NFS v4.1, nous avons ajouté des pr_info() à chaque bloc if dans cette fonction. Nous avons expérimenté avec NFS v4.0 et v4.1 et trouvé une condition spéciale dans la version v4.1:


 if (NFS_SB(dentry->d_sb)->caps & NFS_CAP_ATOMIC_OPEN_V1) { goto no_open; } 

Qu'est-ce que NFS_CAP_ATOMIC_OPEN_V1 ? Ce correctif du noyau indique qu'il s'agit d'une fonctionnalité de NFS v4.1, et le code dans fs/nfs/nfs4proc.c confirmé que ce paramètre est dans v4.1 mais pas dans v4.0:


 static const struct nfs4_minor_version_ops nfs_v4_1_minor_ops = { .minor_version = 1, .init_caps = NFS_CAP_READDIRPLUS | NFS_CAP_ATOMIC_OPEN | NFS_CAP_POSIX_LOCK | NFS_CAP_STATEID_NFSV41 | NFS_CAP_ATOMIC_OPEN_V1 

Par conséquent, les versions se sont comportées différemment - dans la version 4.1, goto no_open appelle plus de vérifications dans la fonction nfs_lookup_revalidate() , et dans la version nfs4_lookup_revalidate() fonction nfs4_lookup_revalidate() renvoie plus tôt. Et comment avons-nous résolu le problème?


Solution


J'ai parlé de nos résultats sur la liste de diffusion NFS et suggéré un correctif primitif . Une semaine plus tard, Trond Myklebust a envoyé une série de correctifs avec des corrections de bogues à la liste de diffusion et a trouvé un autre problème connexe dans NFS v4.1 .


Il s'avère que la correction du bogue NFS v4.0 était plus profonde dans la base de code que nous ne le pensions. Trond l'a bien décrit dans le patch :


Il est nécessaire de s'assurer que l'inode et la dentisterie sont correctement revérifiés lorsqu'un fichier déjà ouvert est ouvert. Pour le moment, nous ne vérifions pas non plus NFSv4.0, car le fichier ouvert est mis en cache. Corrigeons cela et mettons en cache les fichiers ouverts uniquement dans des cas spéciaux - pour restaurer les fichiers ouverts et renvoyer la délégation.

Nous nous sommes assurés que ce correctif résolvait le problème de fichier obsolète et envoyions des rapports de bogues aux équipes Ubuntu et RedHat .


Nous avons bien compris que les changements ne seraient pas encore dans la version stable du noyau, nous avons donc ajouté une solution temporaire à ce problème dans Gitaly . Nous avons expérimenté et vérifié que l'appel de stat() dans le fichier packed-refs oblige le noyau à revérifier le fichier renommé dans le cache dentry. Pour plus de simplicité, nous l'avons implémenté dans Gitaly pour tout système de fichiers, pas seulement NFS. La validation n'est effectuée qu'une seule fois avant que Gitaly n'ouvre le référentiel, et pour d'autres fichiers, il existe déjà d'autres appels stat() .


Qu'avons-nous appris


Un bogue peut se cacher dans n'importe quel coin de la pile logicielle, et parfois vous devez le rechercher en dehors de l'application. Si vous avez des connexions utiles dans le monde open source, cela facilitera votre travail.


Un grand merci à Trond Myuklebust pour avoir résolu le problème et à Bruce Fields pour avoir répondu à nos questions et aidé à comprendre NFS. C'est pour une telle réactivité et professionnalisme que nous apprécions la communauté des développeurs open source.

Source: https://habr.com/ru/post/fr432922/


All Articles