rename()原子性和NFS?

16

关于:rename()是否原子化?

我的问题与此类似,但又不完全相同,因为我想知道的是,在使用NFS时,是否可以依赖于rename()的原子性。

这是我正在处理的情况——我必须始终存在一个“索引”文件。

所以:

  • 客户端创建一个新文件
  • 客户端将新文件重命名为“旧”的索引文件。

另一个客户端:

  • 读取索引文件
  • 基于索引引用磁盘结构。

这样做的前提是rename()是原子的,这意味着始终会有一个“索引”文件(尽管可能是过时的版本,因为缓存和时间因素)。

然而,我遇到的问题是这个——它是在NFS上运作的,但是我的几个NFS客户端偶尔会报告“ENOENT”-没有这样的文件或目录。(例如,在每5分钟进行的数百次操作中,我们每隔几天就会出现这个错误)。

所以,我希望有人能给我指点——在这种情况下,是否不可能出现'ENOENT'?

我之所以问这个问题,是因为RFC 3530中的这篇文章:

重命名操作对于客户端必须是原子性的。

我想知道这是否仅适用于发出重命名命令的客户端,而不适用于查看目录的客户端?(我可以接受缓存的/过期的目录结构,但这个操作的重点是这个文件将始终以某种形式“存在”)

操作序列(来自执行写操作的客户端)如下:

21401 14:58:11 open("fleeg.ext", O_RDWR|O_CREAT|O_EXCL, 0666) = -1 EEXIST (File exists) <0.000443>
21401 14:58:11 open("fleeg.ext", O_RDWR) = 3 <0.000547>
21401 14:58:11 fstat(3, {st_mode=S_IFREG|0600, st_size=572, ...}) = 0 <0.000012>
21401 14:58:11 fadvise64(3, 0, 572, POSIX_FADV_RANDOM) = 0 <0.000008>
21401 14:58:11 fcntl(3, F_SETLKW, {type=F_WRLCK, whence=SEEK_SET, start=1, len=1}) = 0 <0.001994>
21401 14:58:11 open("fleeg.ext.i", O_RDWR|O_CREAT, 0666) = 4 <0.000538>
21401 14:58:11 fstat(4, {st_mode=S_IFREG|0600, st_size=42, ...}) = 0 <0.000008>
21401 14:58:11 fadvise64(4, 0, 42, POSIX_FADV_RANDOM) = 0 <0.000006>
21401 14:58:11 close(4)                 = 0 <0.000011>
21401 14:58:11 fstat(3, {st_mode=S_IFREG|0600, st_size=572, ...}) = 0 <0.000007>
21401 14:58:11 open("fleeg.ext.i", O_RDONLY) = 4 <0.000577>
21401 14:58:11 fstat(4, {st_mode=S_IFREG|0600, st_size=42, ...}) = 0 <0.000007>
21401 14:58:11 fadvise64(4, 0, 42, POSIX_FADV_RANDOM) = 0 <0.000006>
21401 14:58:11 fstat(4, {st_mode=S_IFREG|0600, st_size=42, ...}) = 0 <0.000007>
21401 14:58:11 fstat(4, {st_mode=S_IFREG|0600, st_size=42, ...}) = 0 <0.000007>
21401 14:58:11 read(4, "\3PAX\1\0\0O}\270\370\206\20\225\24\22\t\2\0\203RD\0\0\0\0\17\r\0\2\0\n"..., 42) = 42 <0.000552>
21401 14:58:11 close(4)                 = 0 <0.000013>
21401 14:58:11 fcntl(3, F_SETLKW, {type=F_RDLCK, whence=SEEK_SET, start=466, len=68}) = 0 <0.001418>
21401 14:58:11 pread(3, "\21@\203\244I\240\333\272\252d\316\261\3770\361#\222\200\313\224&J\253\5\354\217-\256LA\345\253"..., 38, 534) = 38 <0.000010>
21401 14:58:11 pread(3, "\21@\203\244I\240\333\272\252d\316\261\3770\361#\222\200\313\224&J\253\5\354\217-\256LA\345\253"..., 38, 534) = 38 <0.000010>
21401 14:58:11 pread(3, "\21\"\30\361\241\223\271\256\317\302\363\262F\276]\260\241-x\227b\377\205\356\252\236\211\37\17.\216\364"..., 68, 466) = 68 <0.000010>
21401 14:58:11 pread(3, "\21\302d\344\327O\207C]M\10xxM\377\2340\0319\206k\201N\372\332\265R\242\313S\24H"..., 62, 300) = 62 <0.000011>
21401 14:58:11 pread(3, "\21\362cv'\37\204]\377q\362N\302/\212\255\255\370\200\236\350\2237>7i`\346\271Cy\370"..., 104, 362) = 104 <0.000010>
21401 14:58:11 pwrite(3, "\21\302\3174\252\273.\17\v\247\313\324\267C\222P\303\n~\341F\24oh/\300a\315\n\321\31\256"..., 127, 572) = 127 <0.000012>
21401 14:58:11 pwrite(3, "\21\212Q\325\371\223\235\256\245\247\\WT$\4\227\375[\\\3263\222\0305\0\34\2049A;2U"..., 68, 699) = 68 <0.000009>
21401 14:58:11 pwrite(3, "\21\262\20Kc(!.\350\367i\253hkl~\254\335H\250.d\0036\r\342\v\242\7\255\214\31"..., 38, 767) = 38 <0.000009>
21401 14:58:11 fsync(3)                 = 0 <0.001007>
21401 14:58:11 fstat(3, {st_mode=S_IFREG|0600, st_size=805, ...}) = 0 <0.000009>
21401 14:58:11 open("fleeg.ext.i.tmp", O_RDWR|O_CREAT|O_TRUNC, 0666) = 4 <0.001813>
21401 14:58:11 fstat(4, {st_mode=S_IFREG|0600, st_size=0, ...}) = 0 <0.000007>
21401 14:58:11 fadvise64(4, 0, 0, POSIX_FADV_RANDOM) = 0 <0.000007>
21401 14:58:11 write(4, "\3PAX\1\0\0qT2\225\226\20\225\24\22\t\2\0\205;D\0\0\0\0\17\r\0\2\0\n"..., 42) = 42 <0.000012>
21401 14:58:11 stat("fleeg.ext.i", {st_mode=S_IFREG|0600, st_size=42, ...}) = 0 <0.000011>
21401 14:58:11 fchmod(4, 0100600)       = 0 <0.002517>
21401 14:58:11 fstat(4, {st_mode=S_IFREG|0600, st_size=42, ...}) = 0 <0.000008>
21401 14:58:11 close(4)                 = 0 <0.000011>
21401 14:58:11 rename("fleeg.ext.i.tmp", "fleeg.pax.i") = 0 <0.001201>
21401 14:58:11 close(3)                 = 0 <0.000795>
21401 14:58:11 munmap(0x7f1475cce000, 4198400) = 0 <0.000177>
21401 14:58:11 munmap(0x7f14760cf000, 4198400) = 0 <0.000173>
21401 14:58:11 futex(0x7f147cbcb908, FUTEX_WAKE_PRIVATE, 2147483647) = 0 <0.000010>
21401 14:58:11 exit_group(0)            = ?
21401 14:58:11 +++ exited with 0 +++

NB - 以上路径和文件已重命名以保持一致性。 fleeg.ext 是数据文件,fleeg.ext.i 是索引文件。在此过程中,fleeg.ext.i 文件正在被覆盖(由.tmp文件),这就是为什么人们认为该路径上应始终存在一个文件(旧文件或刚刚被覆盖的新文件)。在读取客户端上,PCAP看起来像LOOKUP NFS调用失败了:
124   1.375777  10.10.41.35 -> 10.10.41.9   NFS 226   LOOKUP    fleeg.ext.i V3 LOOKUP Call, DH: 0x6fbbff3a/fleeg.ext.i
125   1.375951   10.10.41.9 -> 10.10.41.35  NFS 186 5347  LOOKUP  0775 Directory  V3 LOOKUP Reply (Call In 124) Error: NFS3ERR_NOENT
126   1.375975  10.10.41.35 -> 10.10.41.9   NFS 226   LOOKUP    fleeg.ext.i V3 LOOKUP Call, DH: 0x6fbbff3a/fleeg.ext.i
127   1.376142   10.10.41.9 -> 10.10.41.35  NFS 186 5347  LOOKUP  0775 Directory  V3 LOOKUP Reply (Call In 126) Error: NFS3ERR_NOENT

3
重命名文件前,你是否关闭了文件?确保先关闭文件非常重要。 - Maxim Egorushkin
看了一下 strace,似乎不是这样的 - 'new' 文件是由 open 调用创建的,然后在打开的同时进行了 rename。我会再跟踪一次以确认。 - Sobrique
@MaximEgorushkin - 已经在追踪中重新运行并将其添加到问题中 - 文件在重命名之前已关闭。 - Sobrique
哪个系统调用返回 ENOENT,是 open 还是 read - Maxim Egorushkin
我收到的错误信息表明它是“open” - 尽管我不太确定,因为我没有失败的strace(它不经常发生,所以我没有捕捉到),只有应用程序层记录。 - Sobrique
4个回答

8

我认为问题不在于RENAME操作不具有原子性,而在于通过NFS打开文件不具有原子性。

NFS使用Filehandles(文件句柄);要对一个文件执行操作,客户端首先需要通过LOOKUP获取一个Filehandle,然后使用所获取的Filehandle执行其他请求。至少需要两个数据报文,它们之间的时间可能相当“长”。

我猜测你遇到的情况是,一个客户端(client1)执行了一个LOOKUP操作;就在这之后,被LOOKUP的文件因为RENAME操作(由client2执行)而被删除了;客户端client1拥有的Filehandle已经无效,因为它指向inode而非命名路径。

所有这一切的原因在于,NFS旨在实现无状态(stateless)。更多信息请参见此PDF:http://pages.cs.wisc.edu/~remzi/OSTEP/dist-nfs.pdf

该行为在第6页和第8页中有详细解释。


5
在这种情况下出现ENOENT是完全可能的。根据RFC 3530的规定:“操作对客户端必须是原子的。” 这很可能意味着它必须对调用此操作的客户端是原子的,而不是所有客户端。此外,文件中还提到:“如果目标目录已包含具有该名称的条目……则在重命名发生之前删除现有目标。这就是其他客户端有时会收到ENOENT的原因。换句话说,在NFS上,rename不是原子性的。

2
作为开发者,我对如何正确更新我的应用程序中存储在NFS上的配置文件很感兴趣。这个文件经常被读取,但在应用程序更新时,由于方案更新,它会被重新写入。重要的是,在更新时,应该保留现有内容,同时,如果不存在,应该创建一个“默认”配置文件。虽然使用真正的原子重命名很简单,但是在NFS上存在一个小的时间段,文件在这段时间内不存在。因此,读者不能简单地创建“默认”配置文件,只因为找不到它。然而,似乎在NFS上可以使用下面的脚本来解决这个问题。基本的步骤如下:
- 更新者原子性地创建一个锁目录,进行重命名、同步和删除锁 - 读者准备处理不存在的文件和过时的读取,在这种情况下,他们自己成为更新者。一旦他们获得了锁,他们尝试再次读取配置文件,以区分正在更新的文件和不存在的文件。

你可以在这里找到我对这个概念的C++实现here,如果想要一个独立的Python脚本,请参见下方。

使用方法:

# start writer with
$ echo abc > foo; rm tmp*; rmdir foo_LOCK/; ./renametest.py foo 1
# On another machine, start reader with
$ ./renametest.py foo 0

很快,你会看到类似的消息
iter 481 stale file handle
iter 16811 file not found
iter 16811 failed to obtain lock. Giving up.

这表明一些进程在尝试获取锁时饥饿太久了。然而,配置文件要么成功读取/更新,要么没有。没有损坏。很好。
这个脚本:
#!/usr/bin/env python3

import os
import sys
import tempfile
import errno
import time


def eprint(*args, **kwargs):
    print('iter', g_iter, *args, file=sys.stderr, **kwargs)


def lock_file_name(filename):
    return filename + '_LOCK'

def try_lock(filename):
    try:
        os.mkdir(lock_file_name(filename))
        return True
    except FileExistsError:
        return False


def abc_or_die(filename):
    with open(filename, 'r') as f:
        content = f.read()
    if content != "abc\n":
        eprint("ERROR - bad content:", content)
        exit(1)

def update_it(filename):
    cwd = os.getcwd()
    for i in range(10):
        if not try_lock(filename):
            time.sleep(1)
            continue

        # 'Updating' a cfg file usually means to read it first,
        # which should now be safe:
        abc_or_die(filename)

        tmp_file = tempfile.NamedTemporaryFile(delete=False, dir=cwd).name
        with open(tmp_file, 'w') as f:
            f.write("abc\n")

        # almost-atomic-replace on NFS
        os.rename(tmp_file, filename)
        # sync, before releasing the lock. Otherwise, there is still a small slot,
        # where the lockdir is removed, while the config-file rename is still in progress
        os.sync()
        os.rmdir(lock_file_name(filename))
        return True

    eprint('failed to obtain lock. Giving up.')


def handle_read_fail(filename):
    for i in range(10):
        if not try_lock(filename):
            time.sleep(1)
            continue
        # got the lock
        if not os.path.exists(filename):
            # TODO: in the real world, we would create the config file now.
            # Here we require it to exist
            eprint('ERROR: got lock but file does not exist')
            exit(1)
        abc_or_die(filename)
        os.rmdir(lock_file_name(filename))
        return True

    eprint('failed to obtain lock. Giving up.')




def read_it(filename):
    try:
        with open(filename, 'r') as f:
            content = f.read()
            if len(content) == 0:
                eprint('file is empty')
                handle_read_fail(filename)
                return

            if content != "abc\n":
                eprint("ERROR - bad content:", content)
                exit(1)
            # eprint('red success on first try!')
            return True
    except OSError as e:
        if e.errno == errno.ENOENT:
            eprint('file not found')
        elif e.errno == errno.ESTALE:
            eprint('stale file handle')
        else:
            eprint("unhandled error", e)
            exit(1)
        handle_read_fail(filename)


def main():
    global g_iter
    filename=sys.argv[1]
    do_update=int(sys.argv[2])

    g_iter = 0
    if do_update == 1:
        while True:
            update_it(filename)
            g_iter += 1
    else:
        while True:
            read_it(filename)
            g_iter += 1

if __name__ == '__main__':
    try:
        main()
    except (BrokenPipeError, KeyboardInterrupt):
        pass
    # avoid additional broken pipe error. s. https://dev59.com/dF8d5IYBdhLWcg3wkSwV#26738736
    sys.stderr.close()



首先,我使用了库函数flock进行咨询锁定-读取时使用共享锁,写入时使用独占锁。这样,我根本不使用rename,一切都正常运行(而且代码很简单)。然而,通过NFS进行锁定在有大量其他流量时可能会很慢,所以我寻找了一种没有锁定的“安全”重命名实现。

感谢您的评论,非常感激。这仍然是我们需要注意的问题,但通过“仅仅”阻止代码产生无效的期望,许多问题已经解决了。 - undefined

1
我认为我现在知道正在发生的事情的答案了。我在这里添加它,因为虽然其他人在帮助解决问题方面非常有帮助,但实际问题的根源是这个:
读取主机:
79542  10.643148 10.0.0.52 -> 10.0.0.24 NFS 222  ACCESS allowed   testfile  V3 ACCESS Call, FH: 0x76a9a83d, [Check: RD MD XT XE]
79543  10.643286 10.0.0.24 -> 10.0.0.52 NFS 194 0 ACCESS allowed 0600 Regular File testfile NFS3_OK V3 ACCESS Reply (Call In 79542), [Allowed: RD MD XT XE]
79544  10.643335 10.0.0.52 -> 10.0.0.24 NFS 222  ACCESS allowed     V3 ACCESS Call, FH: 0xe0e7db45, [Check: RD LU MD XT DL]
79545  10.643456 10.0.0.24 -> 10.0.0.52 NFS 194 0 ACCESS allowed 0755 Directory  NFS3_OK V3 ACCESS Reply (Call In 79544), [Allowed: RD LU MD XT DL]
79546  10.643487 10.0.0.52 -> 10.0.0.24 NFS 230  LOOKUP    testfile  V3 LOOKUP Call, DH: 0xe0e7db45/testfile
79547  10.643632 10.0.0.24 -> 10.0.0.52 NFS 190 0 LOOKUP  0755 Directory  NFS3ERR_NOENT V3 LOOKUP Reply (Call In 79546) Error: NFS3ERR_NOENT
79548  10.643662 10.0.0.52 -> 10.0.0.24 NFS 230  LOOKUP    testfile  V3 LOOKUP Call, DH: 0xe0e7db45/testfile
79549  10.643814 10.0.0.24 -> 10.0.0.52 NFS 190 0 LOOKUP  0755 Directory  NFS3ERR_NOENT V3 LOOKUP Reply (Call In 79548) Error: NFS3ERR_NOENT

编写主机:
203306  13.805489  10.0.0.6 -> 10.0.0.24 NFS 246  LOOKUP    .nfs00000000d59701e500001030  V3 LOOKUP Call, DH: 0xe0e7db45/.nfs00000000d59701e500001030
203307  13.805687 10.0.0.24 -> 10.0.0.6  NFS 186 0 LOOKUP  0755 Directory  NFS3ERR_NOENT V3 LOOKUP Reply (Call In 203306) Error: NFS3ERR_NOENT
203308  13.805711  10.0.0.6 -> 10.0.0.24 NFS 306  RENAME    testfile,.nfs00000000d59701e500001030  V3 RENAME Call, From DH: 0xe0e7db45/testfile To DH: 0xe0e7db45/.nfs00000000d59701e500001030
203309  13.805982 10.0.0.24 -> 10.0.0.6  NFS 330 0,0 RENAME  0755,0755 Directory,Directory  NFS3_OK V3 RENAME Reply (Call In 203308)
203310  13.806008  10.0.0.6 -> 10.0.0.24 NFS 294  RENAME    testfile_temp,testfile  V3 RENAME Call, From DH: 0xe0e7db45/testfile_temp To DH: 0xe0e7db45/testfile
203311  13.806254 10.0.0.24 -> 10.0.0.6  NFS 330 0,0 RENAME  0755,0755 Directory,Directory  NFS3_OK V3 RENAME Reply (Call In 203310)
203312  13.806297  10.0.0.6 -> 10.0.0.24 NFS 246  CREATE    testfile_temp  V3 CREATE Call, DH: 0xe0e7db45/testfile_temp Mode: EXCLUSIVE
203313  13.806538 10.0.0.24 -> 10.0.0.6  NFS 354 0,0 CREATE  0755,0755 Regular File,Directory testfile_temp NFS3_OK V3 CREATE Reply (Call In 203312)
203314  13.806560  10.0.0.6 -> 10.0.0.24 NFS 246  SETATTR  0600  testfile_temp  V3 SETATTR Call, FH: 0x4b69a46a
203315  13.806767 10.0.0.24 -> 10.0.0.6  NFS 214 0 SETATTR  0600 Regular File testfile_temp NFS3_OK V3 SETATTR Reply (Call In 203314)

只有在打开同一文件进行读取时,才能重现此问题 - 因此除了一个简单的 C 写入重命名循环之外:

#!/usr/bin/env perl

use strict;
use warnings;

while ( 1 ) {
  open ( my $input, '<', 'testfile' ) or warn $!;
  print ".";
  sleep 1;
}

这导致我的测试用例很快(几分钟)失败,而不是根本没有失败。这归因于在打开文件句柄并删除它(或被RENAME覆盖)时创建的“ .nfsXXX”文件。
由于NFS是无状态的,它必须为客户端提供一些持久性,以便它可以像在本地文件系统上执行open/unlink操作一样读/写该文件。为了做到这一点,我们进行了双重RENAME,并且存在一个非常短暂的(亚毫秒级别)时间间隔,在此期间我们正在寻找的文件不会出现在LOOKUP NFS RPC中。

只有在“writer”主机上有一个读文件句柄打开时才会发生这种情况。重命名对于“那个”客户端是原子的,但由于发生了两次重命名,所以“远程”客户端在两个事件之间看到的目录是-非常短暂地-空的。 - Sobrique
1
最后一条评论,我保证。你所说的“something”,应该是NFS文件句柄,通常指一个inode。你得到了一个指向“myfile.txt”的文件句柄,你将该文件重命名,但文件句柄仍然指向同一对象,即使被重命名。因此,这种双重重命名对我来说似乎是错误的。 - linuxfan says Reinstate Monica
我不是在重命名'myfile.txt'。我正在将'myfile.tmp'覆盖到'myfile.txt'上以替换它(从而删除它)。通常,在Unix上这不是问题 - 文件句柄会一直保留,直到引用计数降至零,即使文件已被删除。但是NFS必须处理服务器或客户端重新启动的情况。因此,为了保持'myfile.txt'处于打开状态 - 同时覆盖它 - 需要先重命名'打开'副本(并保留FH)。 - Sobrique
日志“Writing host:”指的不是服务器,而是客户端。这或许更奇怪。 - linuxfan says Reinstate Monica
2
@GuillaumePapin 那只是一个名为“tshark”的程序,它与wireshark捆绑在一起,但只能以文本形式显示。 - Sobrique
显示剩余4条评论

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