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 からでももとファイルシステムの機能が使えるんだなぁと.