Comment faire pour verrouiller correctement les fichiers sur NFS?

J’essaie d’implémenter une classe “record manager” dans python 3x et linux / macOS. La classe est relativement simple et facile, la seule chose “difficile” que je souhaite est de pouvoir accéder au même fichier (où les résultats sont enregistrés) sur plusieurs processus.

Cela semblait assez facile, sur le plan conceptuel: lors de la sauvegarde, obtenez un verrou exclusif sur le fichier. Mettez à jour vos informations, enregistrez les nouvelles informations, libérez le verrou exclusif sur le fichier. Assez facile.

J’utilise fcnt.lockf(file, fcntl.LOCK_EX) pour acquérir le verrou exclusif. Le problème est que, en regardant sur Internet, je trouve que beaucoup de sites Web disent que ce n’est pas fiable, que cela ne fonctionnera pas sur Windows, que le support sur NFS est instable et que les choses peuvent changer entre macOS et Linux.

J’ai accepté que le code ne fonctionne pas sur Windows, mais j’espérais pouvoir le faire fonctionner sur macOS (machine unique) et sur Linux (sur plusieurs serveurs avec NFS).

Le problème est que je ne peux pas sembler faire ce travail; et après un certain temps de débogage et après les tests passés sur macOS, ils ont échoué une fois que je les ai essayés sur NFS avec Linux (Ubuntu 16.04). Le problème est une incohérence entre les informations sauvegardées par plusieurs processus – certains processus ont leurs modifications manquantes, ce qui signifie que quelque chose s’est mal passé dans la procédure de locking et de sauvegarde.

Je suis sûr que je fais quelque chose de mal, et je soupçonne que cela peut être lié aux problèmes que je lis en ligne. Alors, quelle est la bonne façon de traiter plusieurs access au même fichier qui fonctionne sur MacOS et Linux sur NFS?

modifier

Voici à quoi ressemble la méthode typique qui écrit de nouvelles informations sur le disque:

 sf = open(self._save_file_path, 'rb+') try: fcntl.lockf(sf, fcntl.LOCK_EX) # acquire an exclusive lock - only one writer self._raw_update(sf) #updates the records from file (other processes may have modified it) self._saved_records[name] = new_info self._raw_save() #does not check for locks (but does *not* release the lock on self._save_file_path) finally: sf.flush() os.fsync(sf.fileno()) #forcing the OS to write to disk sf.close() #release the lock and close 

Bien que ce soit comme une méthode typique qui ne lit que les informations du disque :

 sf = open(self._save_file_path, 'rb') try: fcntl.lockf(sf, fcntl.LOCK_SH) # acquire shared lock - multiple writers self._raw_update(sf) #updates the records from file (other processes may have modified it) return self._saved_records finally: sf.close() #release the lock and close 

En outre, voici à quoi ressemble _raw_save:

 def _raw_save(self): #write to temp file first to avoid accidental corruption of information. #os.replace is guaranteed to be an atomic operation in POSIX with open('temp_file', 'wb') as p: p.write(self._saved_records) os.replace('temp_file', self._save_file_path) #pretty sure this does not release the lock 

Message d’erreur

J’ai écrit un test unitaire où je crée 100 processus différents, 50 qui lisent et 50 qui écrivent dans le même fichier. Chaque processus effectue une attente aléatoire pour éviter d’accéder aux fichiers de manière séquentielle.

Le problème est que certains des dossiers ne sont pas conservés; à la fin, il y a quelques 3-4 enregistrements aléatoires manquants, donc je finis seulement avec 46-47 enregistrements plutôt que 50.

Modifier 2

J’ai modifié le code ci-dessus et j’obtiens le verrou non sur le fichier lui-même, mais sur un fichier de locking distinct. Cela empêche le problème que la fermeture du fichier libère le verrou (comme suggéré par @janneb), et rend le code fonctionne correctement sur mac. Le même code échoue sur Linux avec NFS cependant.

Utiliser des liens durs nommés de manière aléatoire et le lien compte sur ces fichiers en tant que fichiers de locking est une stratégie courante (par ex. Ceci ) et discutable mieux que d’utiliser lockd mais pour plus d’informations sur les limites de toutes sortes de verrous sur NFS //0pointer.de/blog/projects/locking.html

Vous constaterez également qu’il s’agit d’un problème standard de longue date pour les logiciels MTA utilisant des fichiers Mbox via NFS. La meilleure solution était probablement d’utiliser Maildir au lieu de Mbox , mais si vous recherchez des exemples dans le code source de quelque chose comme postfix, cela sera proche des meilleures pratiques. Et si elles ne résolvent tout simplement pas ce problème, cela pourrait aussi être votre réponse.

Je ne vois pas comment la combinaison des verrous de fichiers et os.replace () peut avoir un sens. Lorsque le fichier est remplacé (c’est-à-dire que l’entrée de répertoire est remplacée), tous les verrous de fichiers existants (y compris les verrous de fichiers en attente du locking, je ne suis pas sûr de la sémantique) et les descripteurs de fichiers seront contre ancien fichier, pas le nouveau. Je soupçonne que c’est la raison pour laquelle les conditions de course vous ont fait perdre certains des records lors de vos tests.

os.replace () est une bonne technique pour s’assurer qu’un lecteur ne lit pas une mise à jour partielle. Mais il ne fonctionne pas correctement face à de multiples mises à jour (à moins que la perte de certaines mises à jour ne soit acceptable).

Un autre problème est que fcntl est une API vraiment vraiment stupide. En particulier, les verrous sont liés au processus, pas au descripteur de fichier. Ce qui signifie que, par exemple, un descripteur de fichier close () sur n’importe quel fichier pointant vers le fichier libère le verrou.

Une façon serait d’utiliser un “fichier de locking”, par exemple en tirant parti de l’atomicité de link (). De http://man7.org/linux/man-pages/man2/open.2.html :

Les programmes portables qui souhaitent effectuer un locking de fichier atomique à l’aide d’un fichier de locking et qui doivent éviter d’utiliser O_EXCL comme support NFS peuvent créer un fichier unique sur le même système de fichiers (par exemple, nom d’hôte et PID) et utiliser le lien (2) un lien vers le fichier de locking. Si le lien (2) renvoie 0, le verrou a réussi. Sinon, utilisez stat (2) sur le fichier unique pour vérifier si son nombre de liens est passé à 2, auquel cas le verrou est également réussi.

Si c’est ok pour lire des données légèrement obsolètes alors vous pouvez utiliser cette danse link () uniquement pour un fichier temporaire que vous utilisez lors de la mise à jour du fichier et ensuite os.replace () le fichier “principal” que vous utilisez pour lire sans serrure). Sinon, vous devez faire le truc link () pour le fichier “principal” et oublier le locking partagé / exclusif, tous les verrous sont alors exclusifs.

Addendum : Une chose délicate à utiliser lors de l’utilisation de fichiers de locking est ce qu’il faut faire lorsqu’un processus meurt pour une raison quelconque et laisse le fichier de locking. Si cela doit s’exécuter sans surveillance, vous pouvez vouloir intégrer un certain délai et supprimer les fichiers de locking (par exemple, vérifier les horodatages stat ()).

NFS est idéal pour le partage de fichiers. Il craint comme un moyen de transmission.

J’ai été plusieurs fois sur la route des transmissions de données NFS. Dans tous les cas, la solution impliquait de s’éloigner de NFS.

Obtenir un locking fiable est une partie du problème. L’autre partie est la mise à jour du fichier sur le serveur et l’attente que les clients reçoivent ces données à un moment donné (par exemple, avant de pouvoir attraper le verrou).

NFS n’est pas conçu pour être une solution de transmission de données. Il y a des caches et du timing impliqués. Sans parler de la pagination du contenu du fichier et des métadonnées du fichier (par exemple, l’atsortingbut atime). Et le client O / S’assure de suivre l’état localement (tel que “where” pour append les données du client lors de l’écriture à la fin du fichier).

Pour un magasin dissortingbué et synchronisé, je vous recommande de consulter un outil qui vous convient parfaitement. Comme Cassandra, ou même une firebase database polyvalente.

Si je lis correctement le cas d’utilisation, vous pouvez également utiliser une solution simple basée sur un serveur. Demandez à un serveur d’écouter les connexions TCP, de lire les messages à partir des connexions, puis écrivez-les dans un fichier, en sérialisant les écritures dans le serveur lui-même. Avoir votre propre protocole (pour savoir où un message commence et s’arrête) est un peu plus complexe, mais sinon, il est assez simple.