Назад | Перейти на главную страницу

rm в каталоге с миллионами файлов

Справочная информация: физический сервер, возраст около двух лет, диски SATA со скоростью вращения 7200 об / мин, подключенные к карте RAID 3Ware, ext3 FS смонтированы noatime и данные = заказаны, без сумасшедшей нагрузки, ядро ​​2.6.18-92.1.22.el5, время безотказной работы 545 дней . Каталог не содержит подкаталогов, только миллионы маленьких (~ 100 байт) файлов с некоторыми более крупными (несколько КБ).

У нас есть сервер, который в течение последних нескольких месяцев немного кукушка, но мы заметили это только на днях, когда он не мог писать в каталог из-за того, что он содержит слишком много файлов. В частности, он начал выдавать эту ошибку в / var / log / messages:

ext3_dx_add_entry: Directory index full!

На рассматриваемом диске осталось много inodes:

Filesystem            Inodes   IUsed   IFree IUse% Mounted on
/dev/sda3            60719104 3465660 57253444    6% /

Итак, я предполагаю, что это означает, что мы достигли предела того, сколько записей может быть в самом файле каталога. Не знаю, сколько это будет файлов, но, как видите, не может быть больше трех миллионов или около того. Не то чтобы это хорошо, заметьте! Но это первая часть моего вопроса: каков именно этот верхний предел? Это настраивается? Прежде, чем на меня будут кричать - я хочу настроить это вниз; этот огромный каталог вызывал всевозможные проблемы.

Как бы то ни было, мы обнаружили проблему в коде, который генерировал все эти файлы, и исправили ее. Теперь я застрял с удалением каталога.

Здесь несколько вариантов:

  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 )

    Фактически это сокращенная версия; реальный, который я использую, который просто добавляет некоторые отчеты о прогрессе и чистую остановку, когда у нас заканчиваются файлы для удаления, это:

    export i=0;
    time ( while [ true ]; do
      ls -Uf | head -n 3 | grep -qF '.png' || break;
      ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null;
      export i=$(($i+10000));
      echo "$i...";
    done )

    Кажется, это работает довольно хорошо. Пока я пишу это, он удалил 260 000 файлов за последние тридцать минут или около того.

Now, for the questions:
  1. Как упоминалось выше, настраивается ли ограничение на количество записей в каталоге?
  2. Почему потребовалось "реальные 7m9.561s / user 0m0.001s / sys 0m0.001s", чтобы удалить один файл, который был первым в списке, возвращенном ls -U, и, возможно, потребовалось десять минут, чтобы удалить первые 10 000 записей с помощью команды в № 3, но теперь она довольно успешно продвигается? Если на то пошло, он удалил 260 000 примерно за тридцать минут, но теперь потребовалось еще пятнадцать минут, чтобы удалить еще 60 000. Почему огромные колебания скорости?
  3. Есть ли лучший способ сделать это? Не хранить миллионы файлов в каталоге; Я знаю, что это глупо, и в мои часы этого бы не случилось. Поиск в Google проблемы и просмотр SF и SO предлагает множество вариантов find которые не будут значительно быстрее моего подхода по нескольким очевидным причинам. Но есть ли у идеи delete-via-fsck какие-либо основания? Или что-то совсем другое? Мне не терпится услышать нестандартное мышление (или мышление изнутри малоизвестного).
Thanks for reading the small novel; feel free to ask questions and I'll be sure to respond. I'll also update the question with the final number of files and how long the delete script ran once I have that.

Окончательный вывод скрипта !:

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

real    253m59.331s
user    0m6.061s
sys     5m4.019s

Итак, три миллиона файлов были удалены чуть более чем за четыре часа.

Хотя основной причиной этой проблемы является производительность ext3 с миллионами файлов, реальная основная причина этой проблемы иная.

Когда каталог должен быть внесен в список, для каталога вызывается readdir (), который выдает список файлов. readdir - это вызов posix, но настоящий системный вызов Linux, который здесь используется, называется getdent. Getdent перечисляет записи каталога, заполняя буфер записями.

Проблема в основном заключается в том, что readdir () использует фиксированный размер буфера 32 КБ для выборки файлов. По мере того, как каталог становится все больше и больше (размер увеличивается по мере добавления файлов) ext3 становится все медленнее и медленнее для получения записей, а дополнительного размера буфера readdir 32 КБ достаточно только для включения части записей в каталоге. Это заставляет readdir повторять цикл снова и снова и снова и снова вызывать дорогостоящий системный вызов.

Например, в тестовом каталоге, который я создал с более чем 2,6 миллиона файлов внутри, запуск "ls -1 | wc-l" показывает большой вывод strace многих системных вызовов getdent.

$ 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
...

Вдобавок время, проведенное в этом каталоге, было значительным.

$ time ls -1 | wc -l
2616044

real    0m20.609s
user    0m16.241s
sys 0m3.639s

Чтобы сделать этот процесс более эффективным, можно вручную вызвать getdent с гораздо большим буфером. Это значительно улучшает производительность.

Теперь вы не должны вызывать getdent вручную вручную, поэтому не существует интерфейса для его обычного использования (проверьте страницу руководства, чтобы увидеть getdent!), Однако вы жестяная банка вызовите его вручную и сделайте вызов системного вызова более эффективным.

Это значительно сокращает время, необходимое для получения этих файлов. Я написал программу, которая это делает.

/* 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);
}

Хотя это не решает основную фундаментальную проблему (большое количество файлов в файловой системе, которая плохо справляется с этим). Вероятно, это будет намного, намного быстрее, чем многие из публикуемых альтернатив.

Предварительно следует удалить затронутый каталог и после этого переделать его. Каталоги только увеличиваются в размере и могут оставаться плохо работающими даже с несколькими файлами внутри из-за размера каталога.

Редактировать: Я немного почистил это. Добавлена ​​опция, позволяющая удалять в командной строке во время выполнения, и удалена куча вещей, связанных с деревьями, которые, если честно, были в лучшем случае сомнительными. Также было показано, что вызывает повреждение памяти.

Теперь ты можешь сделать dentls --delete /my/path

Новые результаты. На основе каталога с 1,82 миллиона файлов.

## 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

Был немного удивлен, что это все еще так хорошо работает!

В data=writeback Параметр mount заслуживает того, чтобы его испробовали, чтобы предотвратить журналирование файловой системы. Это следует делать только во время удаления, однако существует риск, если сервер выключается или перезагружается во время операции удаления.

В соответствии с эта страница,

Некоторые приложения показывают очень значительное улучшение скорости при его использовании. Например, улучшение скорости можно увидеть (...), когда приложения создают и удаляют большие объемы небольших файлов.

Опция устанавливается либо в fstab или во время операции монтирования, заменив data=ordered с участием data=writeback. Файловая система, содержащая файлы, которые необходимо удалить, должна быть перемонтирована.

Можно ли сделать резервную копию всех других файлов из этой файловой системы во временное хранилище, переформатировать раздел, а затем восстановить файлы?

В ext3 нет ограничения на файл в каталоге, только ограничение inode файловой системы (хотя я думаю, что есть ограничение на количество подкаталогов).

У вас могут возникнуть проблемы после удаления файлов.

Когда в каталоге миллионы файлов, сама запись в каталоге становится очень большой. Запись каталога должна сканироваться для каждой операции удаления, и это занимает разное количество времени для каждого файла, в зависимости от того, где находится его запись. К сожалению, даже после удаления всех файлов запись каталога сохраняет свой размер. Таким образом, дальнейшие операции, требующие сканирования записи в каталоге, все равно займут много времени, даже если каталог сейчас пуст. Единственный способ решить эту проблему - переименовать каталог, создать новый со старым именем и перенести все оставшиеся файлы в новый. Затем удалите переименованный.

Я не тестировал его, но этот парень сделал:

rsync -a --delete ./emptyDirectoty/ ./hugeDirectory/

find просто не работал у меня, даже после изменения параметров ext3 fs, как было предложено вышеупомянутыми пользователями. Слишком много памяти. Этот скрипт PHP сделал свое дело - быстрое, незначительное использование ЦП, незначительное использование памяти:

<?php 
$dir = '/directory/in/question';
$dh = opendir($dir)) { 
while (($file = readdir($dh)) !== false) { 
    unlink($dir . '/' . $file); 
} 
closedir($dh); 
?>

Я опубликовал отчет об этой проблеме с find: http://savannah.gnu.org/bugs/?31961

TL; DR: используйте rsync -a --delete emptyfolder/ x.

У этого вопроса 50 тысяч просмотров и довольно много ответов, но, похоже, никто не сравнивал все разные ответы. Есть одна ссылка на внешний тест, но ему больше 7 лет, и он не смотрел на программу, представленную в этом ответе: https://serverfault.com/a/328305/565293

Частично сложность заключается в том, что время, необходимое для удаления файла, сильно зависит от используемых дисков и файловой системы. В моем случае я тестировал оба с потребительским SSD под управлением BTRFS на Arch Linux (обновлено с 2020-03), но я получил такой же порядок результатов в другом дистрибутиве (Ubuntu 18.04), файловой системе (ZFS) и диске. тип (HDD в конфигурации RAID10).

Настройка теста была идентична для каждого запуска:

# setup
mkdir test && cd test && mkdir empty
# create 800000 files in a folder called x
mkdir x && cd x
seq 800000 | xargs touch
cd ..

Результаты теста:

rm -rf x: 30,43 с

find x/ -type f -delete: 29,79

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

rsync -a --delete empty/ x: 25,11 с

(Ниже приводится программа от этот ответ, но изменено, чтобы ничего не печатать или ждать перед удалением файлов.)

./dentls --delete x: 29,74

В rsync версия оказывалась победителем каждый раз, когда я повторял тест, хотя и с довольно низким отрывом. В perl команда была медленнее, чем любой другой вариант в моих системах.

Несколько шокирует то, что программа из верхнего ответа на этот вопрос оказалась на моих системах не быстрее, чем простой rm -rf. Давайте разберемся, почему это так.

Прежде всего, ответ утверждает, что проблема в том, что rm использует readdir с фиксированным размером буфера 32 КБ с getdents. Это оказалось не так в моей системе Ubuntu 18.04, которая использовала буфер в четыре раза больше. В системе Arch Linux он использовал getdents64.

Кроме того, ответ вводит в заблуждение статистику, показывающую его скорость на листинг файлы в большом каталоге, но не удаляя их (вот о чем и был вопрос). Он сравнивает dentls к ls -u1, но простой strace показывает, что getdents является не причина почему ls -u1 работает медленно, по крайней мере, не в моей системе (Ubuntu 18.04 с 1000000 файлами в каталоге):

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]

это 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]

Это правильно! Даже если dentls делает только 12 звонков вместо 245, это фактически занимает систему дольше для запуска этих вызовов. Таким образом, объяснение, данное в этом ответе, на самом деле неверно - по крайней мере, для двух систем, на которых я смог это проверить.

То же самое касается rm и dentls --delete. В то время как rm занимает 0,42 секунды на вызов getdents, dentls занимает 0,53 с. В либо случае большую часть времени тратится на звонки unlink!

Короче говоря, не ожидайте увидеть массовое ускорение. dentls, если ваша система не похожа на авторскую и не требует больших затрат на отдельные getdents. Возможно, разработчики glibc значительно ускорили его за годы, прошедшие с момента написания ответа, и теперь для ответа на разные размеры буфера требуется линейная величина времени. Или, может быть, время отклика getdents зависит от архитектуры системы каким-то образом, что не очевидно.

Убедитесь, что вы делаете:

mount -o remount,rw,noatime,nodiratime /mountpoint

что также должно немного ускорить процесс.

Недавно я столкнулся с подобной проблемой и не смог получить ring0 data=writeback предложение поработать (возможно из-за того, что файлы находятся на моем основном разделе). Исследуя обходные пути, я наткнулся на это:

tune2fs -O ^has_journal <device>

Это полностью отключит ведение журнала, независимо от data вариант дать mount. Я объединил это с noatime и объем имел dir_index установлен, и, похоже, он работал очень хорошо. Удаление фактически завершилось, и мне не пришлось его убивать, моя система оставалась отзывчивой, и теперь она снова работает (с включенным журналированием) без проблем.

Пару лет назад я нашел каталог с 16 миллионов XML файлы в / файловая система. Из-за критики сервера мы использовали следующую команду, которая занимала около 30 часов заканчивать:

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

Это был старый 7200 об / мин hdd, и, несмотря на узкое место ввода-вывода и скачки производительности ЦП, старый веб-сервер продолжал свою работу.

Является dir_index установить для файловой системы? (tune2fs -l | grep dir_index) Если нет, включите его. Обычно это включено для нового RHEL.

ls очень медленная команда. Пытаться:

find /dir_to_delete ! -iname "*.png" -type f -delete

Из того, что я помню, удаление inodes в файловых системах ext составляет O (n ^ 2), поэтому чем больше файлов вы удалите, тем быстрее пойдут остальные.

Был один раз, когда я столкнулся с подобной проблемой (хотя мои оценки смотрели на время удаления ~ 7 часов), в конце концов пошел предложенный маршрут jftuga в первом комментарии.

Иногда в подобных случаях Perl может творить чудеса. Вы уже пробовали, может ли такой небольшой сценарий, как этот, превзойти bash и основные команды оболочки?

#!/usr/bin/perl 
open(ANNOYINGDIR,"/path/to/your/directory");
@files = grep("/*\.png/", readdir(ANNOYINGDIR));
close(ANNOYINGDIR);

for (@files) {
    printf "Deleting %s\n",$_;
    unlink $_;
}

Или другой, возможно, даже более быстрый подход Perl:

#!/usr/bin/perl
unlink(glob("/path/to/your/directory/*.png")) or die("Could not delete files, this happened: $!");

РЕДАКТИРОВАТЬ: Я просто попробовал свои Perl-скрипты. Более многословный что-то делает правильно. В моем случае я пробовал это с виртуальным сервером с 256 МБ ОЗУ и полумиллионом файлов.

time find /test/directory | xargs rm полученные результаты:

real    2m27.631s
user    0m1.088s
sys     0m13.229s

по сравнению с

time perl -e 'opendir(FOO,"./"); @files = readdir(FOO); closedir(FOO); for (@files) { unlink $_; }'

real    0m59.042s
user    0m0.888s
sys     0m18.737s

Очевидно, здесь не яблоки к яблокам, но я настроил небольшой тест и сделал следующее:

Создано 100000 файлов размером 512 байт в каталоге (dd и /dev/urandom в петле); забыл рассчитать время, но создание этих файлов заняло примерно 15 минут.

Выполните следующее, чтобы удалить указанные файлы:

ls -1 | wc -l && time find . -type f -delete

100000

real    0m4.208s
user    0m0.270s
sys     0m3.930s 

Это коробка Pentium 4 2,8 ГГц (пара сотен Гбайт IDE 7200 об / мин, я думаю; EXT3). Ядро 2.6.27.

Я предпочитаю уже предложенный подход newfs. Основная проблема, как уже отмечалось, заключается в том, что линейное сканирование для обработки удаления проблематично.

rm -rf должен быть почти оптимальным для локальной файловой системы (NFS будет другим). Но в миллионах файлов, 36 байтов на имя файла и 4 на индексный дескриптор (предположение, без проверки значения для ext3), это 40 * миллионов, которые должны храниться в ОЗУ только для каталога.

Предположительно, вы используете кеш-память метаданных файловой системы в Linux, так что блоки для одной страницы файла каталога удаляются, пока вы все еще используете другую часть, только чтобы снова попасть на эту страницу кеша, когда следующий файл удален. Настройка производительности Linux - не моя область, но / proc / sys / {vm, fs} /, вероятно, содержит что-то подходящее.

Если вы можете позволить себе простои, вы можете рассмотреть возможность включения функции dir_index. Он переключает индекс каталога с линейного на что-то гораздо более оптимальное для удаления в больших каталогах (хешированные b-деревья). tune2fs -O dir_index ... с последующим e2fsck -D должно сработать. Однако, хотя я уверен, что это поможет перед есть проблемы, не знаю как преобразование (e2fsck с -D) выполняется при работе с существующим каталогом v.large. Резервные копии + пососать и посмотреть.

Вот как я удаляю миллионы файлов трассировки, которые иногда могут собираться на большом сервере базы данных Oracle:

for i in /u*/app/*/diag/*/*/*/trace/*.tr? ; do rm $i; echo -n . ;  done

Я обнаружил, что это приводит к довольно медленному удалению, которое мало влияет на производительность сервера, обычно около часа на миллион файлов при «типичной» настройке 10 000 IOPS.

Часто проходит несколько минут, прежде чем каталоги будут просканированы, сгенерирован начальный список файлов и первый файл будет удален. С этого момента файл. отображается для каждого удаленного файла.

Задержка, вызванная эхом на терминал, оказалась достаточной задержкой, чтобы предотвратить любую значительную нагрузку во время процесса удаления.

Скорее всего, вы столкнулись с проблемами перезаписи каталога. Попробуйте сначала удалить самые новые файлы. Посмотрите на параметры монтирования, которые откладывают обратную запись на диск.

Для индикатора выполнения попробуйте запустить что-то вроде rm -rv /mystuff 2>&1 | pv -brtl > /dev/null

Я бы, наверное, сделал компилятор C и сделал моральный эквивалент вашего скрипта. То есть использовать opendir(3) чтобы получить дескриптор каталога, затем используйте readdir(3) чтобы получить имена файлов, затем подсчитывать файлы по мере их отключения и время от времени печатать «% d файлов удалено» (и, возможно, прошедшее время или текущая отметка времени).

Я не ожидаю, что это будет заметно быстрее, чем версия сценария оболочки, просто мне приходилось время от времени вырывать компилятор, либо потому что нет чистого способа делать то, что я хочу от оболочки, либо потому что хотя это можно сделать в оболочке, это непродуктивно медленно.

Хорошо, это было описано разными способами в остальной части обсуждения, но я подумал, что добавлю свои два цента. Виновником производительности в вашем случае, вероятно, является readdir. Вы получаете обратно список файлов, которые не обязательно никоим образом находятся на диске последовательно, что приводит к повсеместному доступу к диску при отключении. Файлы достаточно малы, поэтому операция отмены связи, вероятно, не слишком сильно обнуляет пространство. Если вы прочитаете каталог, а затем выполните сортировку по возрастанию inode, вы, вероятно, получите лучшую производительность. Итак, readdir в ram (сортировка по inode) -> unlink -> profit.

Я думаю, Inode - это грубое приближение ... но, исходя из вашего варианта использования, он может быть довольно точным ...

Что ж, это не настоящий ответ, но ...

Можно ли преобразовать файловую систему в ext4 и посмотреть, изменится ли что-нибудь?

Вы можете использовать функции распараллеливания xargs:

ls -1|xargs -P nb_concurrent_jobs -n nb_files_by_job rm -rf
ls|cut -c -4|sort|uniq|awk '{ print "rm -rf " $1 }' | sh -x

на самом деле, этот будет немного лучше, если оболочка, которую вы используете, выполняет расширение командной строки:

ls|cut -c -4|sort|uniq|awk '{ print "echo " $1 ";rm -rf " $1 "*"}' |sh