如何在NFS上正确进行文件锁定?

16

我正在尝试在python 3x和Linux / macOS中实现“记录管理器”类。该类相对简单明了,我唯一想要的“困难”是能够在多个进程中访问保存结果的同一个文件。

从概念上看,这似乎很容易:在保存时,在文件上获取独占锁。更新您的信息,保存新信息,释放文件上的独占锁。很容易。

我使用fcntl.lockf(file, fcntl.LOCK_EX)来获取独占锁。问题是,通过在互联网上查找,我发现有很多不同的网站都说这不可靠,在Windows上无法工作,在NFS上的支持也不稳定,并且在macOS和Linux之间的情况可能会发生变化。

我已经接受了代码在Windows上无法工作,但是我希望能够使其在macOS(单台机器)和Linux(在具有NFS的多个服务器上)上工作。

问题是我似乎无法使此功能正常工作;并且在调试了一段时间之后,在macOS上测试通过之后,当我在具有linux(ubuntu 16.04)的NFS上尝试它们时,它们失败了。问题是多个进程保存的信息之间存在不一致 - 有些进程的修改丢失了,这意味着在锁定和保存过程中出现了问题。

我确定我做错了某件事情,并且我怀疑这可能与我在网上阅读的问题有关。那么,在macOS和Linux上通过NFS处理对同一文件的多次访问的正确方法是什么?

编辑

下面是将新信息写入磁盘的典型方法:

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

虽然这是一个典型的仅从磁盘读取信息的方法:

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

此外,这就是_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

错误信息

我编写了一个单元测试,创建了100个不同的进程,其中50个读取并且50个写入同一个文件。每个进程都会进行一些随机等待以避免按顺序访问文件。

问题在于某些记录没有被保留;最终会有3-4个随机记录丢失,因此我最后只得到46-47条记录而不是50条。

编辑2

我已修改上述代码,现在我在一个单独的锁文件上获取锁,而不是在文件本身上获取锁。这可以防止关闭文件会释放锁(如@janneb所建议的那样),并使代码在mac上正确运行。然而,在带有NFS的linux上,相同的代码会失败。


1
您需要在问题中包含一个最小,完整和可验证的示例 - Marcin Orlowski
3
@MarcinOrlowski您好,由于我不知道问题的具体情况,很难重新创建一个最小示例。我可以将整个代码倾泻在问题中,但这可能没有什么帮助。不过,我已经添加了一个典型方法保存到文件的示例 - 希望这足够了。 - Ant
1
@MarcinOrlowski 但无论如何,我更关心的是“如何在nfs上进行适当的文件锁定”,而不是我的具体情况。由于测试在Mac上通过而在Linux上未通过,我认为问题必须在nfs上的锁定特性而不是我的代码中。为了处理这些特殊情况,我应该遵循一些指南/最佳实践。所以我的问题是,人们是如何做到的?他们遵循哪些指南? - Ant
@MarcinOrlowski 当然,我同意 :) 我之所以提出这个问题,是因为我读到了很多关于锁定和nfs的问题(但必须承认这些文章相当老)。无论如何,我不确定要包括代码的哪个部分。我包括了写入/读取磁盘的方法遵循的模式。 - Ant
看起来你没有有效地放置锁,因为你的结果。在我阅读的文档中,“在至少一些系统上,只有在文件描述符引用打开写入的文件时才能使用LOCK_EX。”。我想知道这是否可能意味着它必须是“wb”模式(仅写入),并且在你的情况下以'rb+'静默失败,无法首先获得此独占锁,而只是继续进行而不被阻塞等待。 - Darkonaut
显示剩余6条评论
3个回答

9
我不明白文件锁和os.replace()的组合怎么能有意义。当文件被替换(也就是说,目录条目被替换)时,所有现有的文件锁(可能包括等待锁定成功的文件锁,我不确定这里的语义)和文件描述符都将与旧文件关联,而不是新文件。我怀疑这就是导致您在测试中丢失一些记录的竞争条件背后的原因。

os.replace()是一种确保读者不会读取部分更新的好技术。但是在多个更新器面前,它的工作不稳健(除非丢失一些更新是可以接受的)。

另一个问题是fcntl是一个非常愚蠢的API。特别是,锁定绑定到进程而不是文件描述符。这意味着对文件指向的任何文件描述符的close()都会释放锁定。

一个方法是使用“锁文件”,例如利用link()的原子性。来自http://man7.org/linux/man-pages/man2/open.2.html:

便携式程序希望使用lockfile执行原子文件锁定,并需要避免依赖于NFS支持的O_EXCL,可以在同一文件系统上创建一个唯一文件(例如,包括主机名和PID),并使用link(2)将链接到lockfile。如果link(2)返回0,则锁定成功。否则,对唯一文件使用stat(2)以检查其链接计数是否增加到2,在这种情况下,锁定也成功。

如果可以读取略有过时的数据,则只需为更新文件时使用的临时文件执行此link()操作,然后os.replace()用于用于阅读的“主”文件(然后可以无锁定进行阅读)。否则,您需要针对“主”文件执行link()技巧,并忘记共享/排他锁定,所有锁定都是排他性的。

附录:在使用锁文件时需要处理的一个棘手问题是当进程因任何原因而死亡并留下锁文件时该怎么办。如果要无人值守运行,则可能需要合并某种超时并删除锁文件的机制(例如,检查stat()时间戳)。


1
谢谢您的回答!所以您建议根本不使用fnctl()和flock(),而是使用这个link()技巧来实现独占文件锁定,忘记共享锁定? - Ant
+1 因此,通过避免在os.replace上加锁并使用另一个单独的文件进行锁定,解决了一些问题。在NFS上仍然存在问题,相同的代码无法通过在mac OS上通过的测试。但是,是否有任何方法可以使用该链接/取消链接技巧在NFS上实现共享锁定?我在我的问题中没有提到它,但我还有一个“全局锁定”,即只有一个实例处于活动状态时才允许某些操作。这是通过在创建实例时立即获取共享锁来实现的,并在销毁时释放它。然后,如果您尝试获取独占锁 - Ant
...如果你成功了,这意味着你是唯一的活动实例。为了使这种行为正常工作,我需要共享锁,对吗?(编辑)我刚想到另一个主意:我能不能只使用一些标志文件来表明哪个进程在特定文件上有锁?所以我为每个进程创建一个带有特定名称的文件,并查找它并确定我是否具有共享或独占锁。NFS 文件是否立即被所有进程看到,所以我可以确定它会工作?如果是的话,为什么fcntl 仍然无法提供文件锁定?我正在使用 NFSv3,根据规范应该支持锁定。 - Ant
@Ant:如果你认为link()技巧大致等同于互斥锁,你可以看一下读写锁(rwlocks)是如何在互斥锁之上实现的,以此作为启发。 - janneb
@Ant:例如像这样的内容 https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Using_two_mutexes - janneb
显示剩余3条评论

4
NFS很适合文件共享,但作为“传输”媒介则不怎么好用。我曾多次尝试使用NFS进行数据传输,但每一次都需要采取离开NFS的方案。
获得可靠的锁定是问题的一部分,另一部分是在服务器上更新文件并期望客户端在某个特定时间点(例如在抢占锁之前)接收到该数据。
NFS不是设计为数据传输解决方案。其中涉及缓存和时间控制,还有文件内容的页面化和文件元数据(如atime属性),以及客户端操作系统在本地跟踪状态(例如“何处”附加客户端数据在写入文件末尾时)。
对于分布式同步存储,我建议查看专门完成这项工作的工具,例如Cassandra,甚至是通用数据库。
如果我正确理解了使用情况,您也可以选择一个简单的基于服务器的解决方案。让服务器监听TCP连接,从连接中读取消息,然后将每个消息写入文件,在服务器内部序列化写入。需要自己定义协议(以知道消息的起始和结束位置),但除此之外,它非常直观易懂。

谢谢你的回答!不幸的是,我无法更改文件系统,因为它由服务器所有者设置。我也试图避免运行另一个程序(即基于服务器的解决方案),使其尽可能简单。但你认为这是不可能的吗? - Ant
我认为你会一直与它斗争,直到决策者意识到这不会奏效。这就是为什么我做了同样的事情不止一次 - 我未能说服其他人,这是一个不适合解决问题的解决方案。 - ash
请注意,我预计所有网络文件系统(SMB、APFS等)都将遭受相同的结果。它们解决的根本问题是文件存储和访问,而不是在网络上共享数据流的可靠流式传输。 - ash

4
使用随机命名的硬链接和这些文件上的链接计数作为锁定文件是一种常见策略(例如,此处),并且可以说比使用 lockd 更好,但要了解有关NFS上所有类型锁定的限制的更多信息,请阅读此内容:http://0pointer.de/blog/projects/locking.html 您还会发现,对于在NFS上使用Mbox文件的MTA软件而言,这是一个长期存在的标准问题。最好的答案可能是改用Maildir而不是Mbox。但是,如果在像postfix这样的源代码中寻找示例,则会接近最佳实践。如果他们根本没有解决该问题,那么这也可能是您的答案。

谢谢你的回答。我看到lockf在NFS上不起作用,而Python实现了它。所以我需要做一些其他的事情来在NFS上进行适当的文件锁定,对吗? - Ant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接