この問題の主な原因は数百万のファイルを使用した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
...
さらに、このディレクトリで費やされた時間はかなりのものでした。
$ time ls -1 | wc -l
2616044
real 0m20.609s
user 0m16.241s
sys 0m3.639s
これをより効率的なプロセスにする方法は、はるかに大きなバッファーで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);
}
これは、根本的な基本的な問題(多くのファイル、パフォーマンスが低いファイルシステム内)に対抗するものではありません。投稿されている多くの代替案よりもはるかに高速です。
予見されるように、影響を受けるディレクトリを削除してから再作成する必要があります。ディレクトリのサイズは大きくなるだけで、ディレクトリのサイズが原因で内部にいくつかのファイルがあってもパフォーマンスが低下する可能性があります。
編集:私はこれをかなりきれいにしました。実行時にコマンドラインで削除できるようにするオプションを追加し、正直に振り返ってみると疑わしいツリーウォークの束を削除しました。また、メモリ破損を引き起こすことが示されました。
できるようになりました 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
これがまだうまく機能していることにちょっと驚きました!