やるきなし

2022/09/01 15:22 / Ruby の FileUtils.mv がなんか遅い (NFS と EXDEV と btrfs)

Ruby の FileUtils.mv がなんか遅い,というか /usr/bin/mv が異様に早い,という話.

ファイルサーバの /home/<username>/ (/home 自体が btrfs subvolume)を NFS+autofs でクライアントがマウントしていて,/home/myn/ から /home/default/ へのファイル移動することを考える(/home/myn//home/default/ をそれぞれ NFS mount している状態).

FileUtils.mv の場合 rename を試みて Errno::EXDEV あるいは Errno::EPERM だったら copy を行う(ファイルサイズが大きいと時間がかかる).

https://github.com/ruby/fileutils/blob/master/lib/fileutils.rb

  def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil)
    fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose
    return if noop
    fu_each_src_dest(src, dest) do |s, d|
      destent = Entry_.new(d, nil, true)
      begin
        if destent.exist?
          if destent.directory?
            raise Errno::EEXIST, d
          end
        end
        begin
          File.rename s, d
        rescue Errno::EXDEV,
               Errno::EPERM # move from unencrypted to encrypted dir (ext4)
          copy_entry s, d, true

/usr/bin/mv の場合は rename しようとして失敗すると clone operation (下の箇所)を試みる.成功すればそれで移動(一瞬で終わる).

https://github.com/coreutils/coreutils/blob/master/src/copy.c

/* Perform the O(1) btrfs clone operation, if possible.
   Upon success, return 0.  Otherwise, return -1 and set errno.  */
static inline int
clone_file (int dest_fd, int src_fd)
{
#ifdef FICLONE
  return ioctl (dest_fd, FICLONE, src_fd);
#else
  (void) dest_fd;
  (void) src_fd;
  errno = ENOTSUP;
  return -1;
#endif
}

strace で追った感じ,以下 EXDEV の行以降抜粋.

FileUtils.mv の場合

copy_file_range で copy.

rename("test", "/home/default/test")    = -1 EXDEV (Invalid cross-device link)
newfstatat(AT_FDCWD, "test", {st_mode=S_IFREG|0644, st_size=192658, ...}, AT_SYMLINK_NOFOLLOW) = 0
openat(AT_FDCWD, "test", O_RDONLY|O_CLOEXEC) = 5
ioctl(5, TCGETS, 0x7ffc0c5c7490)        = -1 ENOTTY (Inappropriate ioctl for device)
newfstatat(5, "", {st_mode=S_IFREG|0644, st_size=192658, ...}, AT_EMPTY_PATH) = 0
openat(AT_FDCWD, "/home/default/test", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0100644) = 6
ioctl(6, TCGETS, 0x7ffc0c5c7000)        = -1 ENOTTY (Inappropriate ioctl for device)
newfstatat(5, "", {st_mode=S_IFREG|0644, st_size=192658, ...}, AT_EMPTY_PATH) = 0
newfstatat(6, "", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_EMPTY_PATH) = 0
lseek(5, 0, SEEK_CUR)                   = 0
copy_file_range(5, NULL, 6, NULL, 192658, 0) = 192658
close(6)                                = 0
close(5)                                = 0

/usr/bin/mv の場合

ioctl で clone を試みる

renameat2(AT_FDCWD, "test", AT_FDCWD, "/home/default/test", RENAME_NOREPLACE) = -1 EXDEV (Invalid cross-device link)
stat("/home/default/test", 0x7ffebbebd0a0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "test", {st_mode=S_IFREG|0644, st_size=192658, ...}, AT_SYMLINK_NOFOLLOW) = 0
newfstatat(AT_FDCWD, "/home/default/test", 0x7ffebbebcd60, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
unlink("/home/default/test")            = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "test", O_RDONLY|O_NOFOLLOW) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=192658, ...}) = 0
openat(AT_FDCWD, "/home/default/test", O_WRONLY|O_CREAT|O_EXCL, 0600) = 4
fstat(4, {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
ioctl(4, BTRFS_IOC_CLONE or FICLONE, 3) = 0
utimensat(4, NULL, [{tv_sec=1662011348, tv_nsec=268642097} /* 2022-09-01T14:49:08.268642097+0900 */, {tv_sec=1662011426, tv_nsec=47929040} /* 2022-09-01T14:50:26.047929040+0900 */], 0) = 0

NFS からでももとファイルシステムの機能が使えるんだなぁと.