linux rename()原子性和NFS?

yks3o0rb  于 2023-10-16  发布在  Linux
关注(0)|答案(4)|浏览(131)

关于:Is rename() atomic?
我问的是类似的问题,但不完全相同,因为我想知道的是,在使用NFS时,依赖rename()的原子性是否安全?
这是我正在处理的一个场景-我有一个必须始终存在的“索引”文件。
所以:

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

独立客户端:

  • 读取索引文件
  • 是指基于索引的磁盘结构。

这是假设rename()是原子的,意味着总是会有一个“索引”文件(尽管它可能是一个过时的版本,因为缓存和计时)。
然而,我遇到的问题是-这是发生在NFS -和工作-但我的几个NFS客户端 * 偶尔 * 报告“ENOENT”-没有这样的文件或目录。(例如,在以5 m间隔发生的数百次操作中,我们每隔几天就会得到这个错误)。
所以我希望有人能给我一些启发--在这种情况下,真的不可能得到“足够”吗?
我问这个问题的原因是RFC 3530中的这个条目:
RENAME操作对于客户端来说必须是原子操作。
我想知道这是否意味着 * 只是 * 客户端发出重命名,而不是客户端查看目录?(我可以使用缓存/过时的目录结构,但此操作的重点是该文件将始终以某种形式“存在”)
操作顺序(来自执行写操作的客户端)为:

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
oxiaedzo

oxiaedzo1#

我认为问题不在于RENAME不是原子的,而在于通过NFS打开文件不是原子的。
NFS使用文件句柄;客户端要对文件做些什么,首先通过LOOKUP获取文件句柄,然后使用获取的文件句柄执行其他请求。最少需要两个数据报,在特定情况下,它们之间的时间可能相当“长”。
我想,在您身上发生的是客户端(client 1)执行LOOKUP;在那之后,LOOKUP文件作为RENAME的结果被删除(由client 2); Filehandle client 1 has不再有效,因为它引用的是inode,而不是命名路径。
这一切的原因是NFS的目标是无状态。此PDF中的更多信息:http://pages.cs.wisc.edu/~remzi/OSTEP/dist-nfs.pdf
在第6页和第8页中,对这种行为进行了很好的解释。

ua4mk5z4

ua4mk5z42#

在这种情况下,是否真的不可能获得ENOENT
很有可能。RFC 3530说道:
该操作对于客户端是原子的。
这很可能意味着它必须对 * 调用此操作的客户端 * 是原子的,而不是所有客户端。
进一步说:
如果目标目录已包含名称为.在重命名发生之前移除现有目标。
这就是为什么 * 其他 * 客户端有时会得到ENOENT
换句话说,rename在NFS上不是原子的。

8cdiaqws

8cdiaqws3#

作为开发人员,我对如何正确更新应用程序的NFS驻留配置文件感兴趣。此文件经常被读取,但是,在应用程序更新时,由于方案更新,它会被重写。重要的是,在更新时,应该保留现有的内容,同时应该创建一个“默认”配置文件(如果不存在)。而真正的 * 原子重命名 * 这是简单的,on NFS there is a small time slot,其中文件不存在。因此,读者不能仅仅因为找不到“默认”配置文件就简单地创建它。然而,在NFS上,这个问题似乎可以使用下面的脚本解决。基本程序是:

  • 更新atomically create a lock_dir,重命名,同步,并删除锁定
  • 读取器为不存在的文件和陈旧的读取做好准备,因此它们本身成为更新器。一旦他们得到了锁,他们会尝试再次读取配置文件,以区分file-in-update和file-not-exist。

这个概念的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://stackoverflow.com/a/26738736
    sys.stderr.close()

作为旁注,首先,我在库调用flock中使用了建议锁--一个用于阅读的共享锁,一个用于写的排他锁。这样,我根本没有使用rename,一切都工作正常(代码也很简单)。但是,当有很多其他流量正在进行时,通过NFS进行锁定可能会很慢,所以我寻找一种没有锁的“安全”重命名实现。

irlmq6kh

irlmq6kh4#

我想我现在知道发生了什么。我在这里添加它,因为虽然其他人在到达那里时非常有帮助,但问题的实际根源是这样的:
阅读主机:

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;
}

这会导致我的测试用例很快失败(几分钟),而不是看起来根本没有失败。这取决于'.nfsXXX'文件,该文件是在打开文件句柄时创建的,然后被删除(或被RENAME覆盖)。
因为NFS是无状态的,它必须为客户端提供一些持久性,所以它仍然可以像在本地文件系统上执行打开/取消链接一样读取/写入该文件。要做到这一点-我们得到一个双RENAME和一个非常短的(亚毫秒)的间隔,我们的目标文件 * 不 * 存在的LOOKUP NFS RPC找到。

相关问题