J'essaie d'implémenter une classe "record manager" dans python 3x et linux/macOS. La classe est relativement simple et directe, la seule chose "difficile" que je veux est de pouvoir accéder à le même fichier (où les résultats sont enregistrés) sur plusieurs processus.
Cela semblait assez facile, conceptuellement: lors de l'enregistrement, acquérez 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 fcntl.lockf(file, fcntl.LOCK_EX)
pour acquérir le verrou exclusif. Le problème est que, en regardant sur Internet, je trouve beaucoup de différents sites Web disant que ce n'est pas fiable, que cela ne fonctionnera pas Windows, que le support sur NFS est fragile, et que les choses pourraient changer entre macOS et linux.
J'ai accepté que le code ne fonctionnera pas sur Windows, mais j'espérais pouvoir le faire fonctionner sur macOS (une seule machine) et sur linux (sur plusieurs serveurs avec NFS).
Le problème est que je n'arrive pas à faire fonctionner cela; et après un certain temps de débogage et après que les tests aient réussi sur macOS, ils ont échoué une fois que je les ai essayés sur le NFS avec linux (ubuntu 16.04). Le problème est une incohérence entre les informations enregistré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 verrouillage et d'enregistrement.
Je suis sûr qu'il y a quelque chose que je fais mal, et je soupçonne que cela peut être lié aux problèmes que j'ai lus sur Internet. Alors, quelle est la bonne façon de gérer plusieurs accès 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 cela ressemble à une méthode typique qui ne fait que lire 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 fait une attente aléatoire pour éviter d'accéder séquentiellement aux fichiers.
Le problème est que certains des enregistrements ne sont pas conservés; à la fin, il manque 3-4 enregistrements aléatoires, donc je ne me retrouve qu'avec 46-47 enregistrements plutôt que 50.
Modifier 2
J'ai modifié le code ci-dessus et j'acquiert le verrou non pas sur le fichier lui-même, mais sur un fichier de verrouillage séparé. Cela évite le problème de fermeture du fichier qui libérerait le verrou (comme suggéré par @janneb) et rend le code fonctionne correctement sur mac. Le même code échoue cependant sur Linux avec NFS.
Je ne vois pas comment la combinaison de 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 probablement les verrous de fichiers en attente de verrouillage, je ne suis pas sûr de la sémantique ici) et les descripteurs de fichiers seront contre ancien fichier, pas le nouveau. Je soupçonne que c'est la raison derrière les conditions de course vous faisant perdre certains records dans vos tests.
os.replace () est une bonne technique pour s'assurer qu'un lecteur ne lit pas une mise à jour partielle. Mais cela ne fonctionne pas de manière robuste face à plusieurs mises à jour (à moins que la perte de certaines mises à jour soit correcte).
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 ex. une fermeture () sur TOUT descripteur de fichier pointant vers le fichier libérera le verrou.
Une façon serait d'utiliser un "fichier de verrouillage", par ex. profitant de l'atomicité de link (). De http://man7.org/linux/man-pages/man2/open.2.html :
Les programmes portables qui souhaitent effectuer un verrouillage de fichier atomique à l'aide d'un fichier de verrouillage et doivent éviter de dépendre de la prise en charge NFS pour O_EXCL, peuvent créer un fichier unique sur le même système de fichiers (par exemple, incorporer le nom d'hôte et le PID) et utiliser le lien (2) pour créer un lien vers le fichier de verrouillage. Si le lien (2) renvoie 0, le verrouillage est réussi. Sinon, utilisez stat (2) sur le fichier unique pour vérifier si son nombre de liens est passé à 2, auquel cas le verrouillage réussit également.
Si c'est Ok pour lire des données légèrement périmées, vous pouvez utiliser ce lien () danser uniquement pour un fichier temporaire que vous utilisez lors de la mise à jour du fichier, puis os.replace () le fichier "principal" vous utilisez pour la lecture (la lecture peut alors être sans verrou). Sinon, vous devez faire l'astuce link () pour le fichier "principal" et oublier le verrouillage partagé/exclusif, tous les verrous sont alors exclusifs.
Addendum : Une chose délicate à gérer lors de l'utilisation de fichiers de verrouillage est de savoir quoi faire lorsqu'un processus meurt pour une raison quelconque, et laisse le fichier de verrouillage autour. Si cela doit s'exécuter sans surveillance, vous souhaiterez peut-être incorporer une sorte de délai d'expiration et de suppression des fichiers de verrouillage (par exemple, vérifiez les horodatages stat ()).
L'utilisation de liens durs nommés de manière aléatoire et le lien compte sur ces fichiers en tant que fichiers de verrouillage est une stratégie courante (par exemple this ), et peut-être mieux que d'utiliser lockd
mais pour beaucoup plus d'informations sur les limites de toutes sortes de verrous sur NFS lire ceci: http://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
sur NFS. La meilleure réponse était probablement d'utiliser Maildir
au lieu de Mbox
, mais si vous recherchez des exemples dans le code source de quelque chose comme postfix, ce sera proche de la meilleure pratique. Et s'ils ne résolvent tout simplement pas ce problème, cela pourrait aussi être votre réponse.
NFS est idéal pour le partage de fichiers. Il aspire comme support de "transmission".
J'ai parcouru la route NFS pour la transmission de données à plusieurs reprises. Dans tous les cas, la solution consistait à s'éloigner de NFS.
L'obtention d'un verrouillage 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 précis (comme avant de pouvoir saisir 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'attribut atime). Et le client O/S'es garde une trace de l'état local (comme "où" pour ajouter les données du client lors de l'écriture à la fin du fichier).
Pour un magasin distribué et synchronisé, je recommande de regarder un outil qui fait exactement cela. Tels que Cassandra, ou même une base de données à usage général.
Si je lis correctement le cas d'utilisation, vous pouvez également opter pour une solution simple basée sur un serveur. Demandez à un serveur d'écouter les connexions TCP, lisez les messages des connexions, puis écrivez-les dans un fichier, en sérialisant les écritures au sein du serveur lui-même. Il y a une complexité supplémentaire à avoir votre propre protocole (à savoir où un message commence et s'arrête), mais sinon, c'est assez simple.