수백만 개의 파일이있는 디렉토리의 rm 없기 때문에 내부 FS 함수를 호출해야

배경 : 실제 서버, 약 2 년 된 3Ware RAID 카드에 연결된 7200RPM SATA 드라이브, ext3 FS 탑재 noatime 및 데이터 . 디렉토리에는 수백 개의 작은 (~ 100 바이트) 파일과 더 큰 (몇 KB) 파일이 포함 된 하위 디렉토리가 없습니다.

우리는 지난 몇 달 동안 약간의 뻐꾸기가 된 서버를 가지고 있지만, 너무 많은 파일을 포함하고 있기 때문에 디렉토리에 쓸 수 없었던 다른 날에만 서버를 발견했습니다. 특히, / var / log / messages에서이 오류가 발생하기 시작했습니다.

ext3_dx_add_entry: Directory index full!

문제의 디스크에는 많은 inode가 남아 있습니다.

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

그래서 그것은 우리가 디렉토리 파일 자체에있을 수있는 항목 수의 한계에 도달했음을 의미합니다. 얼마나 많은 파일이 될지 모릅니다. 그러나 3 백만 개 이상이 될 수 있습니다. 그게 좋은 게 아니에요 그러나 그것은 내 질문 중 하나입니다. 정확히 그 상한은 무엇입니까? 조정 가능합니까? 나는 외쳤다하기 전에에서-I는 조정을 원하는 아래로 ; 이 거대한 디렉토리는 모든 종류의 문제를 일으켰습니다.

어쨌든, 우리는 모든 파일을 생성하는 코드에서 문제를 추적하고 수정했습니다. 이제 디렉토리 삭제에 붙어 있습니다.

몇 가지 옵션은 다음과 같습니다.

  1. rm -rf (dir)

    나는 이것을 먼저 시도했다. 나는 눈에 띄는 영향없이 하루 반 동안 도망 간 후에 그것을 포기하고 죽였습니다.

  2. 디렉토리에서 unlink (2) : 고려할 가치가 있지만, unlink (2)를 통해 삭제하는 것보다 fsck를 통해 디렉토리 내의 파일을 삭제하는 것이 더 빠른지 여부가 문제입니다. 즉, 어떤 식 으로든, 나는 그 inode를 사용되지 않은 것으로 표시해야합니다. 이것은 물론 fsck가 / lost + found에있는 파일에 항목을 삭제하지 않도록 지시 할 수 있다고 가정합니다. 그렇지 않으면 방금 문제를 옮겼습니다. 다른 모든 관심사에 더하여, 이것에 대해 조금 더 읽은 후에, 내가 찾을 수있는 unlink (2) 변형 중 어느 것도 내가 거칠게 삭제할 수 없기 때문에 내부 FS 함수를 호출해야 할 것입니다 항목이있는 디렉토리 푸우
  3. while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )

    이것은 실제로 단축 버전입니다. 내가 실행중인 실제 파일은 진행률 보고서를 추가하고 삭제할 파일이 부족할 때 깨끗하게 중지하는 것입니다.

    수출 i = 0;
    시간 (while [true]; do
      LS -Uf | 헤드 -n 3 | grep -qF '.png'|| 단절;
      LS -Uf | 헤드 -n 10000 | xargs rm -f 2> / dev / null;
      수출 i = $ (($ i + 10000));
      에코 "$ i ...";
    완료)

    이것은 오히려 잘 작동하는 것 같습니다. 이 글을 쓰면서 지난 30 분 동안 26 만 개의 파일이 삭제되었습니다.

이제 질문이 있습니다.

  1. 위에서 언급했듯이 디렉토리 별 항목 제한은 조정 가능합니까?
  2. 왜에 의해 반환 된 목록의 첫 번째 하나 하나의 파일을 삭제 “실제 7m9.561s / 사용자 0m0.001s / SYS의 0m0.001s”을 했는가 ls -U, 그리고 그것은으로 처음 10,000 개 항목을 삭제 아마도 십분했다 3 번 명령을했지만 이제는 아주 행복하게 지내고 있습니까? 그 문제로 약 30 분 만에 260,000을 삭제했지만 이제 60,000 개를 더 삭제하는 데 15 분이 더 걸립니다. 왜 큰 속도로 스윙합니까?
  3. 이런 종류의 일을하는 더 좋은 방법이 있습니까? 디렉토리에 수백만 개의 파일을 저장하지 마십시오. 나는 그것이 어리 석다는 것을 안다. 그리고 그것은 내 시계에서 일어나지 않았을 것이다. 문제를 탐색하고 SF와 SO를 살펴보면 find몇 가지 자명 한 이유로 내 접근 방식보다 훨씬 빠르지 않은 많은 변형이 제공 됩니다. 그러나 Delete-via-fsck 아이디어에는 다리가 있습니까? 아니면 다른 것이 있습니까? 즉시 사용 가능한 (또는 잘 알려지지 않은 내부에서) 생각을 듣고 싶습니다.

작은 소설을 읽어 주셔서 감사합니다. 궁금한 점이 있으시면 언제든지 답변 드리겠습니다. 또한 최종 파일 수와 일단 삭제 스크립트가 실행 된 시간으로 질문을 업데이트합니다.

최종 스크립트 출력! :

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

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

따라서 3 시간 동안 3 백만 개의 파일이 약간 삭제되었습니다.



답변

data=writeback마운트 옵션은 파일 시스템의 저널링을 방지하기 위해 시도 할 가치가있다. 삭제 시간에만 수행해야하지만 삭제 작업 중에 서버가 종료되거나 재부팅되는 경우 위험이 있습니다.

에 따르면 이 페이지 ,

일부 응용 프로그램은 사용될 때 속도가 크게 향상됩니다. 예를 들어, 응용 프로그램이 많은 양의 작은 파일을 작성하고 삭제할 때 속도 향상을 볼 수 있습니다 (…).

이 옵션은 fstab마운트 작업 중 또는 마운트 작업 중으로 설정되며로 대체 data=ordered됩니다 data=writeback. 삭제할 파일이 포함 된 파일 시스템을 다시 마운트해야합니다.


답변

이 문제의 주요 원인은 수백만 파일의 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에 대한 man 페이지를 확인!) 그러나 당신이 할 수 수동으로 전화하여 시스템 콜 호출 방법을보다 효율적으로.

이렇게하면 이러한 파일을 가져 오는 데 걸리는 시간이 크게 줄어 듭니다. 나는 이것을하는 프로그램을 썼다.

/* 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

이것이 여전히 잘 작동한다는 것에 놀랐습니다!


답변

이 파일 시스템에서 다른 모든 파일을 임시 저장 위치로 백업하고 파티션을 다시 포맷 한 다음 파일을 복원 할 수 있습니까?


답변

ext3에는 디렉토리 당 파일 제한이 없으며 파일 시스템 inode 제한 만 있습니다 (하위 디렉토리의 수에는 제한이 있다고 생각합니다).

파일을 제거한 후에도 여전히 문제가있을 수 있습니다.

디렉토리에 수백만 개의 파일이 있으면 디렉토리 항목 자체가 매우 커집니다. 모든 제거 작업에 대해 디렉토리 항목을 스캔해야하며 항목 위치에 따라 각 파일에 대해 다양한 시간이 걸립니다. 불행히도 모든 파일을 제거한 후에도 디렉토리 항목의 크기가 유지됩니다. 따라서 디렉토리가 비어있는 경우에도 디렉토리 항목을 스캔해야하는 추가 조작은 여전히 ​​오랜 시간이 걸립니다. 이 문제를 해결하는 유일한 방법은 디렉토리의 이름을 바꾸고 이전 이름으로 새 디렉토리를 작성하고 나머지 파일을 새 디렉토리로 전송하는 것입니다. 그런 다음 이름을 바꾼 것을 삭제하십시오.


답변

나는 그것을 벤치마킹 하지 않았지만 , 이 사람은 :

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

답변

위의 사용자가 제안한대로 ext3 fs의 매개 변수를 변경 한 후에도 간단하게 작동하지 않습니다. 너무 많은 메모리를 소비했습니다. 이 PHP 스크립트는 빠르고 중요하지 않은 CPU 사용량, 중요하지 않은 메모리 사용량을 트릭했습니다.

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


답변

나는 최근 비슷한 문제에 직면하여 ring0의 data=writeback제안 을 얻을 수 없었습니다 (아마도 파일이 내 주 파티션에 있다는 사실 때문에). 해결 방법을 조사하는 동안 나는 이것을 우연히 발견했습니다.

tune2fs -O ^has_journal <device>

data옵션 은 옵션에 관계없이 저널링을 완전히 해제 합니다 mount. 나는 이것을 결합 noatime하고 볼륨을 dir_index설정했으며 꽤 잘 작동하는 것 같습니다. 삭제하지 않아도 실제로 삭제가 완료되고 시스템이 응답 상태를 유지하며 문제없이 백업 및 실행 (저널링을 다시 사용)합니다.