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

时间:2018-02-13 15:46:10

标签: python linux multiprocessing locking nfs

我正在尝试实施一个"记录管理员" python 3x和linux / macOS中的类。这门课程相对容易和直接,是唯一的" hard"我想要的是能够在多个进程上访问相同的文件(保存结果)。

这在概念上看起来很简单:保存时,获取文件的独占锁定。更新您的信息,保存新信息,释放文件的独占锁定。很容易。

我正在使用fcnt.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上,相同的代码失败了。

3 个答案:

答案 0 :(得分:5)

我没有看到文件锁和os.replace()的组合如何有意义。当替换文件(即替换目录条目)时,所有现有的文件锁(可能包括等待锁定成功的文件锁,我不确定这里的语义)和文件描述符将对抗旧文件,而不是新文件。我怀疑这是导致你在测试中失去一些记录的竞争条件背后的原因。

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

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

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

  

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

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

附录:使用锁定文件时要处理的一个棘手问题是当进程因任何原因而死亡时要做什么,并保留锁定文件。如果要在无人看管的情况下运行,您可能需要合并某种超时并删除锁定文件(例如,检查stat()时间戳)。

答案 1 :(得分:3)

使用随机命名的硬链接和这些文件的链接计数作为锁定文件是一种常见的策略(例如this),并且比使用lockd更有争议,但有关限制的更多信息NFS上的各种锁读取:http://0pointer.de/blog/projects/locking.html

您还会发现,对于使用NFS上的Mbox文件的MTA软件来说,这是一个长期存在的标准问题。可能最好的答案是使用Maildir而不是Mbox,但如果你在源代码中寻找像postfix这样的例子,那么它将接近最佳实践。如果他们根本不解决这个问题,那也可能就是你的答案。

答案 2 :(得分:2)

NFS非常适合文件共享。它很糟糕,作为传输"平台。

我多次在NFS-for-data-transmission道路上走下坡路。在每个实例中,解决方案都涉及远离NFS。

获得可靠的锁定是问题的一部分。另一部分是服务器上的文件更新,并期望客户端在某个特定时间点(例如在他们可以获取锁定之前)接收该数据。

NFS并非设计为数据传输解决方案。有缓存和时间。更不用说文件内容的分页和文件元数据(例如atime属性)。并且客户端O / S在本地跟踪状态(例如"其中"在写入文件末尾时附加客户端的数据)。

对于分布式同步存储,我建议您查看一个可以实现此功能的工具。如Cassandra,甚至是通用数据库。

如果我正确阅读用例,您还可以使用简单的基于服务器的解决方案。让服务器侦听TCP连接,从连接读取消息,然后将每个写入文件,在服务器本身内序列化写入。拥有自己的协议(知道消息的开始和停止位置)会增加一些复杂性,但除此之外,它还相当直接。