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

标签 python linux multiprocessing locking nfs

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

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

我正在使用 fcntl.lockf(file, fcntl.LOCK_EX) 获取独占锁。问题是,在互联网上,我发现 很多 不同的网站都在说这不可靠,它不能在 Windows 上运行,对 NFS 的支持不稳定,并且macOS 和 linux 之间可能会发生变化。

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

问题是我似乎无法完成这项工作;经过一段时间的调试并在 macOS 上通过测试后,一旦我在带有 linux (ubuntu 16.04) 的 NFS 上尝试它们,它们就失败了。问题是多个进程保存的信息不一致 - 一些进程缺少修改,这意味着锁定和保存过程出了问题。

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

编辑

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

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 上失败了。

最佳答案

我不明白文件锁和 os.replace() 的组合有何意义。当文件被替换(也就是目录项被替换)时,所有已经存在的文件锁(可能包括等待加锁成功的文件锁,这里的语义不太清楚)和文件描述符都会对旧文件,不是新文件。我怀疑这是导致您在测试中丢失一些记录的竞争条件背后的原因。

os.replace() 是一种很好的技术,可以确保读者不会阅读部分更新。但它在面对多个更新程序时并不能很好地工作(除非丢失一些更新是可以的)。

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

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

Portable programs that want to perform atomic file locking using a lockfile, and need to avoid reliance on NFS support for O_EXCL, can create a unique file on the same filesystem (e.g., incorporating hostname and PID), and use link(2) to make a link to the lockfile. If link(2) returns 0, the lock is successful. Otherwise, use stat(2) on the unique file to check if its link count has increased to 2, in which case the lock is also successful.

如果可以读取稍微陈旧的数据,那么您可以仅对更新文件时使用的临时文件使用此 link() 舞蹈,然后使用 os.replace() 用于读取的“主”文件(阅读然后可以是无锁的)。如果不是,那么您需要为“主”文件执行 link() 技巧,而忘记共享/独占锁定,所有锁都是独占的。

附录:使用锁定文件时要处理的一件棘手的事情是当进程由于某种原因终止并留下锁定文件时该怎么办。如果这是在无人值守的情况下运行,您可能需要合并某种超时和删除锁定文件(例如检查 stat() 时间戳)。

关于python - 如何在 NFS 上进行正确的文件锁定?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48770531/

相关文章:

python - 使用 Selenium 查找具有相同类的两个不同 div 中的不同元素

python - Monkeypatching:将类上的方法替换为函数

linux - shell_exec 返回 NULL 但是当从控制台尝试时,它有效

linux - 如何在 Windows 7 上模仿我的 OS X 工作流程?

没有 "if __name__ == ' __main_ _':"的 python3.x 多处理循环

multithreading - 在Linux/UNIX上的多处理情况下是否可以使用互斥锁?

python - 多线程与多处理 : Which one to select?

python - 有没有类似于分布式Ruby的Python模块

python - 来自堆叠数据帧的 fill_between

linux - 如何在操作系统启动时执行 Bash 脚本 (Kali Linux)