rm 在包含数百万个文件的目录上

BMD*_*Dan 118 ext3 directory kernel rm

背景:物理服务器,大约两年,7200-RPM SATA 驱动器连接到 3Ware RAID 卡,ext3 FS 挂载 noatime 和 data=ordered,不在疯狂负载下,内核 2.6.18-92.1.22.el5,正常运行时间 545 天. 目录不包含任何子目录,只有数百万个小(~100 字节)文件,还有一些更大的(几 KB)文件。

我们有一台服务器在过去几个月中出现了一些问题,但我们直到前几天才注意到它,因为它开始无法写入目录,因为它包含太多文件。具体来说,它开始在 /var/log/messages 中抛出这个错误:

ext3_dx_add_entry: Directory index full!
Run Code Online (Sandbox Code Playgroud)

有问题的磁盘有大量剩余的 inode:

Filesystem            Inodes   IUsed   IFree IUse% Mounted on
/dev/sda3            60719104 3465660 57253444    6% /
Run Code Online (Sandbox Code Playgroud)

所以我猜这意味着我们达到了目录文件本身可以有多少条目的限制。不知道会有多少文件,但正如您所见,不能超过三百万左右。不是那么好,请注意!但这是我的问题之一:上限到底是多少?是可调的吗?在我被骂之前——我想把它调低;这个庞大的目录导致了各种各样的问题。

无论如何,我们在生成所有这些文件的代码中找到了问题,我们已经更正了它。现在我坚持删除目录。

这里有几个选项:

  1. rm -rf (dir)

    我先试过这个。在它运行了一天半没有任何明显影响后,我放弃并杀死了它。

  2. 目录上的 unlink(2):绝对值得考虑,但问题是通过 fsck 删除目录中的文件是否比通过 unlink(2) 删除更快。也就是说,以某种方式,我必须将这些 inode 标记为未使用。当然,这假设我可以告诉 fsck 不要删除 /lost+found 中文件的条目;否则,我只是转移了我的问题。除了所有其他问题之外,在阅读更多内容之后,结果证明我可能不得不调用一些内部 FS 函数,因为我能找到的 unlink(2) 变体都不允许我随意删除一个包含条目的目录。呸。
  3. while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )

    这实际上是缩短的版本;我正在运行的真正的一个,它只是添加了一些进度报告和一个干净的停止,当我们用完要删除的文件时,是:

    出口 i=0;
    时间 ( while [ true ]; 做
      ls -Uf | 头-n 3 | grep -qF '.png' || 休息;
      ls -Uf | 头-n 10000 | xargs rm -f 2>/dev/null;
      出口 i=$(($i+10000));
      echo "$i...";
    完毕 )

    这似乎工作得很好。在我写这篇文章时,它在过去三十分钟左右删除了 260,000 个文件。

现在,对于问题:
  1. 如上所述,每个目录的条目限制是否可调?
  2. 为什么“real 7m9.561s / user 0m0.001s / sys 0m0.001s”删除单个文件,它是 返回的列表中的第一个文件,而ls -U删除前 10,000 个条目可能需要十分钟#3 中的命令,但现在它很愉快地进行了?就此而言,它在大约 30 分钟内删除了 260,000 个,但现在又花了 15 分钟才删除了 60,000 个。为什么速度波动很大?
  3. 有没有更好的方法来做这种事情?不在一个目录中存储数百万个文件;我知道这很愚蠢,而且它不会发生在我的手表上。谷歌搜索问题并查看 SF 和 SO 提供了很多变化find,由于几个不言而喻的原因,这些变化不会比我的方法快得多。但是通过 fsck 删除的想法有什么用吗?或者完全是别的什么?我渴望听到开箱即用(或在不知名的盒子内)的想法。
感谢阅读小小说;随时提出问题,我一定会回答。我还将使用最终文件数以及删除脚本运行多长时间来更新问题。

最终脚本输出!:

2970000...
2980000...
2990000...
3000000...
3010000...

real    253m59.331s
user    0m6.061s
sys     5m4.019s
Run Code Online (Sandbox Code Playgroud)

因此,在四个多小时内删除了 300 万个文件。

Mat*_*Ife 89

2021 年 8 月更新

这个答案继续吸引了很多关注,我觉得它似乎已经过时了,现在有点多余了。

执行 afind ... -delete最有可能在性能方面产生可接受的结果。

我认为可能会导致更高性能的一个方面是解决问题的“删除”部分而不是“列表”部分。

我试过了,没有用。但我觉得解释我做了什么以及为什么这样做很有用。

在当今较新的内核中,通过使用内核中的 IO uring 子系统(请参阅 参考资料man 2 io_uring_setup),实际上可以尝试异步执行取消链接 —— 这意味着我们可以提交取消链接请求,而无需等待或阻塞以查看结果。

该程序基本上读取一个目录,提交数百个unlinks而不等待结果,然后在系统完成处理请求后获取结果。

它试图做它dentls所做的但使用 IO uring。可以用gcc -o dentls2 dentls2.c -luring.

#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <sched.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>

#include <linux/io_uring.h>
#include <liburing.h>

/* Try to keep the queue size to under two pages as internally its stored in
 * the kernel as contiguously ordered pages. Basically the bigger you make it
 * the higher order it becomes and the less likely you'll have the contiguous
 * pages to support it, despite not hitting any user limits.
 * This reduces an ENOMEM here by keeping the queue size as order 1
 * Ring size internally is rougly 24 bytes per entry plus overheads I haven't
 * accounted for.
 */
#define QUEUE_SIZE 256

/* Globals to manage the queue */
static volatile int pending = 0;
static volatile int total_files = 0;

/* Probes kernel uring implementation and checks if action is 
 * supported inside the kernel */
static void probe_uring(
    struct io_uring *ring)
{
  struct io_uring_probe *pb = {0};

  pb = io_uring_get_probe_ring(ring);

  /* Can we perform IO uring unlink in this kernel ? */
  if (!io_uring_opcode_supported(pb, IORING_OP_UNLINKAT)) {
    free(pb);
    errno = ENOTSUP;
    err(EXIT_FAILURE, "Unable to configure uring");
  }

  free(pb);
}


/* Place a unlink call for the specified file/directory on the ring */
static int submit_unlink_request(
    int dfd,
    const char *fname,
    struct io_uring *ring)
{
  char *fname_cpy = strdup(fname);
  struct io_uring_sqe *sqe = NULL;

  /* Fetch a free submission entry off the ring */
  sqe = io_uring_get_sqe(ring);
  if (!sqe)
    /* Submission queue full */
    return 0;

  pending++;
  /* Format the unlink call for submission */
  io_uring_prep_rw(IORING_OP_UNLINKAT, sqe, dfd, fname_cpy, 0, 0);
  sqe->unlink_flags = 0;

  /* Set the data to just be the filename. Useful for debugging
   * at a later point */
  io_uring_sqe_set_data(sqe, fname_cpy);

  return 1;
}


/* Submit the pending queue, then reap the queue
 * clearing up room on the completion queue */
static void consume_queue(
    struct io_uring *ring)
{
  char *fn;
  int i = 0, bad = 0;
  int rc;
  struct io_uring_cqe **cqes = NULL;

  if (pending < 0)
    abort();

  cqes = calloc(pending, sizeof(struct io_uring_cqe *));
  if (!cqes)
    err(EXIT_FAILURE, "Cannot find memory for CQE pointers");

  /* Notify about submitted entries from the queue (this is a async call) */
  io_uring_submit(ring);

  /* We can immediately take a peek to see if we've anything completed */
  rc = io_uring_peek_batch_cqe(ring, cqes, pending);

  /* Iterate the list of completed entries. Check nothing crazy happened */
  for (i=0; i < rc; i++) {
    /* This returns the filename we set earlier */
    fn = io_uring_cqe_get_data(cqes[i]);

    /* Check the error code of the unlink calls */
    if (cqes[i]->res < 0) {
      errno = -cqes[i]->res;
      warn("Unlinking entry %s failed", fn);
      bad++;
    }

    /* Clear up our CQE */
    free(fn);
    io_uring_cqe_seen(ring, cqes[i]);
  }

  pending -= rc + bad;
  total_files += rc - bad;
  free(cqes);
}



/* Main start */
int main(
    const int argc,
    const char **argv)
{
  struct io_uring ring = {0};
  struct stat st = {0};
  DIR *target = NULL;
  int dfd;
  struct dirent *fn;

  /* Check initial arguments passed make sense */
  if (argc < 2)
    errx(EXIT_FAILURE, "Must pass a directory to remove files from.");

  /* Check path validity */
  if (lstat(argv[1], &st) < 0)
    err(EXIT_FAILURE, "Cannot access target directory");

  if (!S_ISDIR(st.st_mode)) 
    errx(EXIT_FAILURE, "Path specified must be a directory");

  /* Open the directory */
  target = opendir(argv[1]);
  if (!target)
    err(EXIT_FAILURE, "Opening the directory failed");
  dfd = dirfd(target);

  /* Create the initial uring for handling the file removals */
  if (io_uring_queue_init(QUEUE_SIZE, &ring, 0) < 0)
    err(EXIT_FAILURE, "Cannot initialize URING");

  /* Check the unlink action is supported */
  probe_uring(&ring);

  /* So as of writing this code, GETDENTS doesn't have URING support.
   * but checking the kernel mailing list indicates its in progress.
   * For now, we'll just do laymans readdir(). These days theres no 
   * actual difference between it and making the getdents() call ourselves.
   */
  while (fn = readdir(target)) {
    if (fn->d_type != DT_REG)
      /* Pay no attention to non-files */
      continue;

    /* Add to the queue until its full, try to consume it
     * once its full. 
     */
    while (!submit_unlink_request(dfd, fn->d_name, &ring)) {
      /* When the queue becomes full, consume queued entries */
      consume_queue(&ring);
      /* This yield is here to give the uring a chance to 
       * complete pending requests */
      sched_yield();
      continue;
    }
  }

  /* Out of files in directory to list. Just clear the queue */
  while (pending) {
    consume_queue(&ring);
    sched_yield();
  }

  printf("Total files: %d\n", total_files);

  io_uring_queue_exit(&ring);
  closedir(target);
  exit(0);
}
Run Code Online (Sandbox Code Playgroud)

具有讽刺意味的是,结果与我的猜测相反,但为什么呢?

具有 400 万个文件的 TMPFS

$ time ./dentls2 /tmp/many
Total files: 4000000

real    0m6.459s
user    0m0.360s
sys 0m24.224s
Run Code Online (Sandbox Code Playgroud)

使用查找:

$ time find /tmp/many -type f -delete

real    0m9.978s
user    0m1.872s
sys 0m6.617s
Run Code Online (Sandbox Code Playgroud)

具有 1000 万个文件的 BTRFS

$ time ./dentls2 ./many
Total files: 10000000

real    10m25.749s
user    0m2.214s
sys 16m30.865s
Run Code Online (Sandbox Code Playgroud)

使用查找:

time find ./many -type f -delete

real    7m1.328s
user    0m9.209s
sys 4m42.000s
Run Code Online (Sandbox Code Playgroud)

所以看起来批处理系统调用并没有实时改进。新的工作dentls2花费更多的时间(四倍)只会导致更差的性能。因此,整体效率的净损失和更糟糕的延迟。dentls2更差。

造成这种情况的原因是因为 io_uring 产生内核调度程序线程来在内部执行取消链接工作,但是正在处理的目录 inode 一次只能由单个编写器修改。

基本上使用uring我们创建了许多小线程,但只允许从目录中删除一个线程。我们刚刚创建了一堆争用并消除了进行批量 IO 的优势。

使用 eBPF,您可以测量 unlink 频率并观察导致延迟的原因。

在 BTRFS 的情况下,它是内核函数调用btrfs_commit_inode_delayed_inode,它在unlink被调用时获取锁。

dentls2

# /usr/share/bcc/tools/funclatency btrfs_commit_inode_delayed_inode
    Tracing 1 functions for "btrfs_commit_inode_delayed_inode"... Hit Ctrl-C to end.

     nsecs               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 0        |                                        |
       128 -> 255        : 0        |                                        |
       256 -> 511        : 18       |                                        |
       512 -> 1023       : 120      |                                        |
      1024 -> 2047       : 50982    |                                        |
      2048 -> 4095       : 2569467  |********************                    |
      4096 -> 8191       : 4936402  |****************************************|
      8192 -> 16383      : 1662380  |*************                           |
     16384 -> 32767      : 656883   |*****                                   |
     32768 -> 65535      : 85409    |                                        |
     65536 -> 131071     : 21715    |                                        |
    131072 -> 262143     : 9719     |                                        |
    262144 -> 524287     : 5981     |                                        |
    524288 -> 1048575    : 857      |                                        |
   1048576 -> 2097151    : 293      |                                        |
   2097152 -> 4194303    : 220      |                                        |
   4194304 -> 8388607    : 255      |                                        |
   8388608 -> 16777215   : 153      |                                        |
  16777216 -> 33554431   : 56       |                                        |
  33554432 -> 67108863   : 6        |                                        |
  67108864 -> 134217727  : 1        |                                        |

avg = 8533 nsecs, total: 85345432173 nsecs, count: 10000918
Run Code Online (Sandbox Code Playgroud)

使用find ... -delete

# /usr/share/bcc/tools/funclatency btrfs_commit_inode_delayed_inode
Tracing 1 functions for "btrfs_commit_inode_delayed_inode"... Hit Ctrl-C to end.
     nsecs               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 0        |                                        |
       128 -> 255        : 0        |                                        |
       256 -> 511        : 34       |                                        |
       512 -> 1023       : 95       |                                        |
      1024 -> 2047       : 1005784  |****                                    |
      2048 -> 4095       : 8110338  |****************************************|
      4096 -> 8191       : 672119   |***                                     |
      8192 -> 16383      : 158329   |                                        |
     16384 -> 32767      : 42338    |                                        |
     32768 -> 65535      : 4667     |                                        |
     65536 -> 131071     : 3597     |                                        |
    131072 -> 262143     : 2860     |                                        |
    262144 -> 524287     : 216      |                                        |
    524288 -> 1048575    : 22       |                                        |
   1048576 -> 2097151    : 6        |                                        |
   2097152 -> 4194303    : 3        |                                        |
   4194304 -> 8388607    : 5        |                                        |
   8388608 -> 16777215   : 3        |                                        |

avg = 3258 nsecs, total: 32585481993 nsecs, count: 10000416
Run Code Online (Sandbox Code Playgroud)

您可以从直方图中看到find平均花费 3258 纳秒,btrfs_commit_inode_delayed_inodedentls2在函数中花费 8533 纳秒。

此外,直方图显示,整体 io_uring 线程花费至少两倍的时间等待锁定,大多数调用需要 4096-8091 纳秒,而大多数调用find需要 2048-4095 纳秒。

Find 是单线程的,不争用锁,而 `dentls2 是多线程的(由于 uring),它会产生锁争用,并且所经历的延迟反映在分析中。

结论

总而言之,在现代系统上(在撰写本文时),您在软件中可以做的事情越来越少,以使其比预期的速度更快。

它曾经从磁盘读取一个大缓冲区,你可以将一个昂贵的 IO 调用复合到一个大的顺序读取中,而不是小型 getdents() 缓冲区通常最终可能成为的搜索 IO。

此外,由于其他改进,仅调用系统调用的开销较小,并且顺序/随机 IO 访问时间的重大改进消除了我们过去遇到的大 IO 瓶颈。

在我的系统上,此问题已成为内存/CPU 限制。在(至少)BTRFS 上存在一个单一访问器问题,这限制了您一次可以访问每个目录的单个 CPU/程序的取消链接的速度。即使在使用 tmpfs 的理想情况下,尝试对 IO 的产量进行批处理也有轻微的改进,并且在实际文件系统上通常更糟。

最重要的是,我们真的不再有这个问题了——1000 万个文件需要 4 小时才能删除的日子一去不复返了。

只需做一些简单的事情,例如find ... -delete. 与默认的简单设置相比,我尝试的任何优化似乎都无法产生值得编码(或分析)的重大性能改进。


原答案

虽然这个问题的一个主要原因是 ext3 对数百万个文件的性能,但这个问题的实际根本原因是不同的。

当需要列出目录时,在生成文件列表的目录上调用 readdir() 。readdir 是 posix 调用,但这里使用的真正 Linux 系统调用称为“getdents”。Getdents 通过用条目填充缓冲区来列出目录条目。

问题主要在于 readdir() 使用 32Kb 的固定缓冲区大小来获取文件。随着目录变得越来越大(大小随着文件的增加而增加)ext3 获取条目的速度越来越慢,额外的 readdir 的 32Kb 缓冲区大小仅足以包含目录中的一小部分条目。这会导致 readdir 一遍又一遍地循环并一遍又一遍地调用昂贵的系统调用。

例如,在我创建的包含超过 260 万个文件的测试目录中,运行“ls -1|wc-l”会显示许多 getdent 系统调用的大量 strace 输出。

$ strace ls -1 | wc -l
brk(0x4949000)                          = 0x4949000
getdents(3, /* 1025 entries */, 32768)  = 32752
getdents(3, /* 1024 entries */, 32768)  = 32752
getdents(3, /* 1025 entries */, 32768)  = 32760
getdents(3, /* 1025 entries */, 32768)  = 32768
brk(0)                                  = 0x4949000
brk(0x496a000)                          = 0x496a000
getdents(3, /* 1024 entries */, 32768)  = 32752
getdents(3, /* 1026 entries */, 32768)  = 32760
...
Run Code Online (Sandbox Code Playgroud)

此外,在此目录中花费的时间很长。

$ time ls -1 | wc -l
2616044

real    0m20.609s
user    0m16.241s
sys 0m3.639s
Run Code Online (Sandbox Code Playgroud)

使此过程更有效的方法是使用更大的缓冲区手动调用 getdents。这显着提高了性能。

现在,您不应该自己手动调用 getdents,因此不存在正常使用它的接口(请查看手册页以查看 getdents!),但是您可以手动调用它并使您的系统调用调用方式更高效。

这大大减少了获取这些文件所需的时间。我写了一个程序来做到这一点。

/* I can be compiled with the command "gcc -o dentls dentls.c" */

#define _GNU_SOURCE

#include <dirent.h>     /* Defines DT_* constants */
#include <err.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

struct linux_dirent {
        long           d_ino;
        off_t          d_off;
        unsigned short d_reclen;
        char           d_name[256];
        char           d_type;
};

static int delete = 0;
char *path = NULL;

static void parse_config(
        int argc,
        char **argv)
{
    int option_idx = 0;
    static struct option loptions[] = {
      { "delete", no_argument, &delete, 1 },
      { "help", no_argument, NULL, 'h' },
      { 0, 0, 0, 0 }
    };

    while (1) {
        int c = getopt_long(argc, argv, "h", loptions, &option_idx);
        if (c < 0)
            break;

        switch(c) {
          case 0: {
              break;
          }
 
          case 'h': {
              printf("Usage: %s [--delete] DIRECTORY\n"
                     "List/Delete files in DIRECTORY.\n"
                     "Example %s --delete /var/spool/postfix/deferred\n",
                     argv[0], argv[0]);
              exit(0);                      
              break;
          }

          default:
          break;
        }
    }

    if (optind >= argc)
      errx(EXIT_FAILURE, "Must supply a valid directory\n");

    path = argv[optind];
}

int main(
    int argc,
    char** argv)
{

    parse_config(argc, argv);

    int totalfiles = 0;
    int dirfd = -1;
    int offset = 0;
    int bufcount = 0;
    void *buffer = NULL;
    char *d_type;
    struct linux_dirent *dent = NULL;
    struct stat dstat;

    /* Standard sanity checking stuff */
    if (access(path, R_OK) < 0) 
        err(EXIT_FAILURE, "Could not access directory");

    if (lstat(path, &dstat) < 0) 
        err(EXIT_FAILURE, "Unable to lstat path");

    if (!S_ISDIR(dstat.st_mode))
        errx(EXIT_FAILURE, "The path %s is not a directory.\n", path);

    /* Allocate a buffer of equal size to the directory to store dents */
    if ((buffer = calloc(dstat.st_size*3, 1)) == NULL)
        err(EXIT_FAILURE, "Buffer allocation failure");

    /* Open the directory */
    if ((dirfd = open(path, O_RDONLY)) < 0) 
        err(EXIT_FAILURE, "Open error");

    /* Switch directories */
    fchdir(dirfd);

    if (delete) {
        printf("Deleting files in ");
        for (int i=5; i > 0; i--) {
            printf("%u. . . ", i);
            fflush(stdout);
            sleep(1);
        }
        printf("\n");
    }

    while (bufcount = syscall(SYS_getdents, dirfd, buffer, dstat.st_size*3)) {
        offset = 0;
        dent = buffer;
        while (offset < bufcount) {
            /* Don't print thisdir and parent dir */
            if (!((strcmp(".",dent->d_name) == 0) || (strcmp("..",dent->d_name) == 0))) {
                d_type = (char *)dent + dent->d_reclen-1;
                /* Only print files */
                if (*d_type == DT_REG) {
                    printf ("%s\n", dent->d_name);
                    if (delete) {
                        if (unlink(dent->d_name) < 0)
                            warn("Cannot delete file \"%s\"", dent->d_name);
                    }
                    totalfiles++;
                }
            }
            offset += dent->d_reclen;
            dent = buffer + offset;
        }
    }
    fprintf(stderr, "Total files: %d\n", totalfiles);
    close(dirfd);
    free(buffer);

    exit(0);
}
Run Code Online (Sandbox Code Playgroud)

虽然这并不能解决潜在的基本问题(大量文件,在一个文件系统中表现不佳)。它可能比发布的许多替代方案快得多。

作为一个预先考虑,应该删除受影响的目录并在之后重新制作它。由于目录的大小,目录只会增加大小并且即使里面有几个文件也可能保持性能不佳。

编辑:我已经清理了很多。添加了一个选项,允许您在运行时在命令行上删除并删除了一堆 treewalk 的东西,老实说,往后看充其量是有问题的。也被证明会产生内存损坏。

你现在可以做 dentls --delete /my/path

新结果。基于一个包含 182 万个文件的目录。

## Ideal ls Uncached
$ time ls -u1 data >/dev/null

real    0m44.948s
user    0m1.737s
sys 0m22.000s

## Ideal ls Cached
$ time ls -u1 data >/dev/null

real    0m46.012s
user    0m1.746s
sys 0m21.805s


### dentls uncached
$ time ./dentls data >/dev/null
Total files: 1819292

real    0m1.608s
user    0m0.059s
sys 0m0.791s

## dentls cached
$ time ./dentls data >/dev/null
Total files: 1819292

real    0m0.771s
user    0m0.057s
sys 0m0.711s
Run Code Online (Sandbox Code Playgroud)

有点惊讶这仍然如此有效!

  • 这是一段了不起的代码。它是我能找到的唯一能够列出和删除在目录中建立的大约 11,000,000(一千一百万)个会话文件的工具,这些文件可能已经存在了几年。应该使用 find 和其他答案中的其他技巧来控制它们的 Plesk 进程无法完成运行,因此文件一直在堆积。这是对文件系统用来存储目录的二叉树的致敬,会话完全可以工作 - 您可以创建一个文件并立即检索它。只是列表无法使用。 (4认同)

Bre*_*bad 35

data=writeback安装选项值得尝试,以防止文件系统的日志。这应该只在删除期间完成,但是如果在删除操作期间服务器正在关闭或重新启动,则存在风险。

根据这个页面

一些应用程序在使用时表现出非常显着的速度提升。例如,当应用程序创建和删除大量小文件时,可以看到速度的提高 (...)。

该选项fstab在挂载操作中或挂载操作期间设置,替换data=ordereddata=writeback. 包含要删除的文件的文件系统必须重新挂载。

  • `data=writeback` 在将元数据写入主文件系统之前仍然记录它。据我了解,它只是不强制执行诸如编写范围映射和将数据写入这些范围之类的事情之间的排序。如果您从中看到了性能提升,那么它可能还会放宽其他排序限制。当然,完全不使用轴颈进行安装可能会获得更高的性能。(它可能让元数据更改只发生在 RAM 中,而无需在取消链接操作完成之前在磁盘上有任何内容)。 (2认同)

jft*_*uga 32

是否可以将此文件系统中的所有其他文件备份到临时存储位置,重新格式化分区,然后恢复文件?

  • 实际上,我真的很喜欢这个答案。作为一个实际问题,在这种情况下,不,但它不是我会想到的。太棒了! (3认同)

Ale*_*rts 12

ext3 中没有每个目录文件的限制,只有文件系统 inode 限制(我认为子目录的数量是有限制的)。

删除文件后,您可能仍会遇到问题。

当一个目录有数百万个文件时,目录条目本身就变得非常大。每次删除操作都必须扫描目录条目,并且每个文件需要不同的时间,具体取决于其条目所在的位置。不幸的是,即使删除了所有文件,目录条目仍保留其大小。因此,即使目录现在是空的,需要扫描目录条目的进一步操作仍然需要很长时间。解决该问题的唯一方法是重命名目录,使用旧名称创建一个新目录,并将所有剩余文件转移到新目录。然后删除重命名的。

  • 嗯,我做了更多的研究,似乎目录可以占用的块数有限制。文件的确切数量取决于一些因素,例如文件名长度。这个 http://www.gossamer-threads.com/lists/linux/kernel/921942 似乎表明使用 4k 块你应该能够在一个目录中拥有超过 800 万个文件。它们是特别长的文件名吗? (3认同)
  • 也就是说,如果没有每个目录的文件限制,为什么我会收到“ext3_dx_add_entry:目录索引已满!” 当那个分区上还有可用的 inode 时?此目录中没有子目录。 (2认同)

Ali*_*xel 8

我没有对它进行基准测试,但这个人做了

rsync -a --delete ./emptyDirectoty/ ./hugeDirectory/
Run Code Online (Sandbox Code Playgroud)


小智 7

TLDR:使用rsync -a --delete emptyfolder/ x.

这个问题有 5 万次浏览,还有不少答案,但似乎没有人对所有不同的回复进行基准测试。有一个指向外部基准的链接,但该链接已超过 7 年,并且没有查看此答案中提供的程序:https : //serverfault.com/a/328305/565293

这里的部分困难在于删除文件所需的时间在很大程度上取决于正在使用的磁盘和文件系统。就我而言,我使用在 Arch Linux 上运行 BTRFS 的消费者 SSD 进行了测试(更新至 2020-03),但我在不同的发行版(Ubuntu 18.04)、文件系统(ZFS)和驱动器上得到了相同的结果排序类型(RAID10 配置中的 HDD)。

每次运行的测试设置都相同:

# setup
mkdir test && cd test && mkdir empty
# create 800000 files in a folder called x
mkdir x && cd x
seq 800000 | xargs touch
cd ..
Run Code Online (Sandbox Code Playgroud)

检测结果:

rm -rf x: 30.43s

find x/ -type f -delete: 29.79

perl -e 'for(<*>){((stat)[9]<(unlink))}': 37.97s

rsync -a --delete empty/ x: 25.11s

(以下是此答案中的程序,但已修改为不打印任何内容或在删除文件之前等待。)

./dentls --delete x: 29.74

rsync每次我重复测试时,该版本都证明是赢家,尽管利润率很低。该perl命令比我系统上的任何其他选项都慢。

有点令人震惊的是,事实证明,这个问题的最佳答案中的程序在我的系统上并不比简单的rm -rf. 让我们深入研究为什么会这样。

首先,答案声称问题在于rm使用readdir32Kb 的固定缓冲区大小和getdents. 在我的 Ubuntu 18.04 系统上事实证明并非如此,它使用了四倍大的缓冲区。在 Arch Linux 系统上,它使用getdents64.

此外,答案误导性地提供了统计数据,给出了在大目录中列出文件的速度,但没有删除它们(这就是问题所在)。它比较dentlsls -u1,但一个简单的strace显示,getdents不是原因的原因ls -u1是缓慢的,至少不会在我的系统(Ubuntu的18.04与目录百万文件):

strace -c ls -u1 x >/dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 94.00    7.177356           7   1000000           lstat
  5.96    0.454913        1857       245           getdents
[snip]
Run Code Online (Sandbox Code Playgroud)

ls命令对 进行了一百万次调用lstat,这会减慢程序的运行速度。该getdents呼叫最多只能加0.455秒。getdents呼叫在dentls同一个文件夹中占用多长时间?

strace -c ./dentls x >/dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.91    0.489895       40825        12           getdents
[snip]
Run Code Online (Sandbox Code Playgroud)

这是正确的!尽管dentls只进行了 12 次调用而不是 245 次,但实际上系统运行这些调用需要更长的时间。因此,该答案中给出的解释实际上是不正确的 - 至少对于我已经能够对此进行测试的两个系统。

这同样适用于rmdentls --delete。而rm调用 0.42 秒getdents,则dentls需要 0.53 秒。在任何情况下,绝大多数的时间都花在打电话unlink

所以简而言之,不要期望看到大量的加速运行dentls,除非你的系统像作者的一样并且在单个getdents. 也许 glibc 的人在写出答案后的几年里已经大大加快了它的速度,现在需要线性的时间来响应不同的缓冲区大小。或者,响应时间可能getdents以某种不明显的方式取决于系统架构。