שאלה rm על ספרייה עם מיליוני קבצים


רקע: שרת פיזי, כבן שנתיים, 7200-RPM SATA כוננים המחוברים כרטיס RAID 3Ware, ext3 FS רכוב noatime ונתונים = הורה, לא תחת עומס מטורף, ליבה 2.6.18-92.1.22.el5, uptime 545 ימים . המדריך אינו מכיל כל תיקיות משנה, רק מיליוני קבצים קטנים (~ 100 בתים), עם כמה גדולים יותר (כמה KB).

יש לנו שרת זה הלך קצת קוקייה במהלך החודשים האחרונים, אבל שמנו לב רק את זה יום אחר, כאשר זה התחיל להיות מסוגל לכתוב לספרייה בשל זה המכיל יותר מדי קבצים. באופן ספציפי, זה התחיל לזרוק את השגיאה ב / var / log / הודעות:

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)

ניסיתי את זה קודם. ויתרתי והרגתי אותה אחרי שהיא רצה במשך יום וחצי בלי שום השפעה ניכרת.

  • Unlink (2) על הספרייה: בהחלט שווה שיקול, אבל השאלה היא אם זה יהיה מהר יותר למחוק את הקבצים בתוך הספרייה באמצעות fsck מאשר למחוק באמצעות קישור (2). כלומר, כך או אחרת, אני צריך לסמן את אלה inodes כמו בשימוש. זה מניח, כמובן, כי אני יכול להגיד fsck לא להפיל את הערכים לקבצים / איבדו + נמצא; אחרת, העברתי את הבעיה שלי. בנוסף לכל הדאגות האחרות, אחרי שקראתי על זה קצת יותר, מתברר שהייתי כנראה צריך לקרוא כמה פונקציות FS פנימי, כמו אף אחד unlink (2) וריאנטים אני יכול למצוא יאפשר לי רק למחוק בשמחה ספרייה עם ערכים בו. פו.
  • while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
  • זוהי למעשה הגרסה המקוצרת; את אחד אמיתי אני רץ, אשר רק מוסיף קצת התקדמות הדיווח להפסיק נקי כאשר אנו נגמרים של קבצים למחוק, היא:

    יצוא i = 0;
    בזמן (בזמן אמת)
      ls-uf | head -n 3 | grep-qF '.png' || לשבור;
      ls-uf | head -n 10000 | xargs rm-f 2> / dev / null;
      יצוא i = $ (($ i + 10000));
      הד "$ i ...";
    בוצע )

    זה נראה עובד טוב למדי. כשאני כותב את זה, זה נמחק 260,000 קבצים בשלושים הדקות האחרונות או כך.


    97
    2017-09-22 23:57




    rm (GNU coreutils) 8.4 יש אפשרות זו: "-v, --verbose להסביר מה נעשה". הוא יציג את כל הקבצים שנמחקו. - Cristian Ciupitu
    למעשה, זה יהיה דרך מסודרת לעשות סרגל התקדמות: מכיוון שכל קובץ יהיה שלושים ושבעה תווים (36 + a \ n '), אני יכול בקלות לכתוב מנתח עבור זה, ומאחר printf () הוא זול הפקודה rm כבר יש את שם הקובץ נטען, אין עונש ביצועים מיוחדים. ללא שם: נראה כמו לא Starter עבור עושה את כל shebang, שכן מעולם לא יכולתי לקבל rm "לעשות משהו כזה, בכל מקרה. אבל זה יכול לעבוד די טוב כמו התקדמות 10,000 בתוך התקדמות; אולי "." עבור כל מאה קבצים? - BMDan
    rm -rfv | pv -l >/dev/null. pv צריך להיות זמין EPEL מאגר. - Cristian Ciupitu
    pv הוא מדהים באופן מדהים. אני משאיר אחריו שובל של מתקני pv. - BMDan
    היתה לי בעיה זו בדיוק לאחרונה. תודה! - richo


    תשובות:


    ה data=writeback אפשרות הר ראוי לשפוט, כדי למנוע journaling של מערכת הקבצים. זה צריך להיעשות רק במהלך זמן המחיקה, קיים סיכון אבל אם השרת הוא כיבוי או אתחול מחדש במהלך פעולת המחיקה.

    לפי הדף הזה,

    יישומים מסוימים מראים שיפור משמעותי משמעותי כאשר הוא משמש. לדוגמה, שיפורים מהירות ניתן לראות (...) כאשר יישומים ליצור ולמחוק כמויות גדולות של קבצים קטנים.

    האפשרות מוגדרת fstab או במהלך פעולת הר, החלפה data=ordered עם data=writeback. יש לעדכן מחדש את מערכת הקבצים המכילה את הקבצים שיש למחוק.


    30
    2017-09-26 05:49



    הוא יכול גם להגדיל את הזמן מ commit  אפשרות: "ערך ברירת המחדל (או כל ערך נמוך) יפגע בביצועים, אבל זה טוב לבטיחות הנתונים.הגדרתו ל 0 תהיה בעלת אותה השפעה כמו להשאיר אותו בברירת מחדל (5 שניות) .הגדרתו לערכים גדולים מאוד שיפור ביצועים". - Cristian Ciupitu
    Writeback נראה כוכבים, למעט תיעוד הסתכלתי (gentoo.org/doc/en/articles/l-afig-p8.xml#doc_chap4) מציינת במפורש שהיא עדיין מטא נתונים של כתבי עת, ואני מניחה שכל הנתונים שאני משנים (אני בהחלט לא משנה את הנתונים בקבצים עצמם). האם ההבנה שלי לא נכונה? - BMDan
    לבסוף, FYI, לא מוזכר בקישור זה היא העובדה כי נתונים = writeback יכול להיות חור אבטחה ענק, שכן הנתונים הצביעו על ידי ערך נתון לא יכול לקבל את הנתונים שנכתב שם על ידי האפליקציה, כלומר התרסקות עלולה לגרום בנתונים הישנים, אולי רגישים / פרטיים שנחשפו. לא חשש כאן, כי אנחנו רק מפנה את זה באופן זמני, אבל אני רוצה להזהיר את כולם כי אזהרה אם אתה או אחרים אשר רצים על פני הצעה זו לא היו מודעים. - BMDan
    להתחייב: זה די חלקלק! תודה על המצביע. - BMDan
    data=writeback עדיין מטא נתונים לפני כתיבתו למערכת הקבצים הראשית. כפי שאני מבין את זה, זה פשוט לא לאכוף סדר בין דברים כמו כתיבת מפת מידה וכתיבת נתונים לתוך אותם מרחקים. אולי יש אילוצים אחרים ההזמנה היא מרגיעה, יותר מדי, אם אתה רואה רווח perf מזה. כמובן, הרכבה בלי היומן בכלל יכול להיות ביצועים גבוהים יותר. (זה עלול לאפשר את השינויים metadata רק לקרות ב- RAM, ללא צורך שיהיה משהו על הדיסק לפני op הקישור משלים). - Peter Cordes


    בעוד הגורם העיקרי לבעיה זו היא ביצועים ext3 עם מיליוני קבצים, הגורם השורש בפועל לבעיה זו שונה.

    כאשר ספריה צריך להיות רשום readdir () נקרא על ספריה אשר מניב רשימה של קבצים. readir היא שיחת טלפון, אבל הקריאה האמיתית במערכת לינוקס בשימוש כאן נקראת 'getdents'. Getdents רשימה במדריך ערכים על ידי מילוי מאגר עם ערכים.

    הבעיה היא בעיקר את העובדה כי readdir () משתמש בגודל מאגר קבוע של 32Kb כדי לאחזר קבצים. כמו ספרייה מקבל גדול יותר ויותר (גודל מגדיל כמו קבצים מתווספים) ext3 מקבל לאט יותר לאט יותר לאחזר ערכים ועוד readdir של 32Kb גודל חיץ מספיק רק כדי לכלול חלק קטן של ערכים בספריה. זה גורם readdir לולאה שוב ושוב ולעורר את שיחת המערכת יקר שוב ושוב.

    לדוגמה, על המדריך בדיקה יצרתי עם מעל 2.6 מיליון קבצים בפנים, פועל "ls -1 | wc-l" מראה פלט סטרייס גדול של שיחות מערכת רבות 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
    

    השיטה כדי להפוך את זה תהליך יעיל יותר הוא לקרוא 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

    תוצאות חדשות. מבוסס על הנחה של ספרייה עם 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
    

    היה קצת מופתע זה עדיין עובד כל כך טוב!


    73
    2017-11-06 19:06



    שני חששות קטנים: האחד, [256] כנראה צריך להיות [FILENAME_MAX], ושתיים, לינוקס שלי (2.6.18 == CentOS 5.x) לא נראה לכלול ערך d_type ב dirent (לפחות לפי getdents (2)). - BMDan
    אתה יכול בבקשה לפרט קצת על btree rebalancing ומדוע מחיקת סדר מסייע במניעת זה? ניסיתי Googling על זה, למרבה הצער ללא הועיל. - ovgolovin
    כי עכשיו נראה לי שאם אנחנו מוחקים לפי הסדר, אנחנו מאלצים את האיזון, כשאנחנו מסירים עלים בצד אחד ומשאירים בצד השני: en.wikipedia.org/wiki/B-tree#Rebalancing_after_deletion - ovgolovin
    אני מקווה שאני לא מפריע לך בעניינים האלה. אבל עדיין התחלתי שאלה לגבי מחיקת קבצים לפי הסדר stackoverflow.com/q/17955459/862380, אשר נראה כי לא לקבל תשובה אשר יסביר את הבעיה עם הדוגמה, אשר יהיה מובן עבור מתכנתים רגילים. אם יש לך זמן להרגיש כך, אתה יכול להסתכל לתוך זה? אולי אתה יכול לכתוב הסבר טוב יותר. - ovgolovin
    זהו קטע מדהים של קוד. זה היה הכלי היחיד שאני יכול למצוא מסוגל למחוק ולמחוק כמה 11,000,000 (אחד עשר מיליון) קבצי הפעלה אשר נבנה בספרייה, כנראה על פני כמה שנים. תהליך פלסק שהיה אמור לשמור אותם תחת שליטה באמצעות למצוא טריקים אחרים בתשובות אחרות כאן, לא הצליח להשלים ריצה, כך הקבצים פשוט המשיך לבנות. זה מחווה לעץ בינארי כי מערכת הקבצים משתמשת כדי לאחסן את המדריך, כי הפעלות היו מסוגלים לעבוד בכלל - אתה יכול ליצור קובץ לאחזר אותו ללא עיכוב. רק רישומים היו בלתי שמישים. - Jason


    האם ניתן יהיה לגבות את כל הקבצים האחרים ממערכת קבצים זו למיקום אחסון זמני, לאתחל מחדש את המחיצה ולאחר מכן לשחזר את הקבצים?


    31
    2017-09-23 00:27



    אני באמת אוהב את התשובה הזאת, למעשה. כעניין מעשי, במקרה זה, לא, אבל זה לא אחד שהייתי מעלה על דעתי. בראוו! - BMDan
    בדיוק מה חשבתי גם. זוהי תשובה לשאלה 3. אידיאלי אם אתה שואל אותי :) - Joshua


    אין להגביל את כל קובץ קובץ ext3 רק את הגודל של הקבצים inode inode (אני חושב שיש גבול על מספר תיקיות משנה).

    ייתכן שתיתקל בבעיות לאחר הסרת הקבצים.

    כאשר בספרייה יש מיליוני קבצים, ערך הספריה עצמה הופך להיות גדול מאוד. יש לסרוק את ערך הספריה עבור כל פעולת הסרה, וזה לוקח כמות זמן שונה עבור כל קובץ, בהתאם למקום שבו נמצא הערך שלו. למרבה הצער גם לאחר כל הקבצים הוסרו ערך הספריה שומרת על גודלו. כך פעולות נוספות הדורשות סריקה את ערך המדריך עדיין ייקח הרבה זמן גם אם הספרייה ריקה עכשיו. הדרך היחידה לפתור את הבעיה היא לשנות את שם המדריך, ליצור אחד חדש עם השם הישן, ולהעביר את כל הקבצים שנותרו אחד חדש. לאחר מכן מחק את השם ששמו שונה.


    11
    2017-09-23 05:45



    אכן, הבחנתי רק בהתנהגות זו לאחר מחיקת הכל. למזלנו, כבר הוצאנו את הספרייה מ"קו האש ", כביכול, כדי שאוכל פשוט לזוז. - BMDan
    עם זאת, אם אין הגבלה קובץ בספרייה, למה אני מקבל "ext3_dx_add_entry: אינדקס מלא!" כאשר היו עדיין inodes זמין על המחיצה? לא היו תיקיות משנה בתוך ספריה זו. - BMDan
    הממ עשיתי מחקר קצת יותר, נראה שיש גבול של מספר בלוקים במדריך יכול לקחת. מספר הקבצים המדויק תלוי בכמה דברים, למשל אורך שם הקובץ. זה gossamer-threads.com/lists/linux/kernel/921942 נראה עולה כי עם 4k בלוקים אתה אמור להיות מסוגל לקבל יותר מ 8 מיליון קבצים בספרייה. האם היו שם קבצים ארוכים במיוחד? - Alex J. Roberts
    כל שם קובץ היה באורך 36 תווים. - BMDan
    גם זה אני מתוך רעיונות :) - Alex J. Roberts


    אני לא benchmarked זה, אבל הבחור הזה עשהYou

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

    5
    2018-06-04 11:52





    למצוא פשוט לא עובד בשבילי, גם לאחר שינוי הפרמטרים ext3 FS כפי שהוצע על ידי המשתמשים לעיל. הצריכה יותר מדי זיכרון. זה סקריפט PHP עשה את הטריק - מהיר, שימוש משמעותי במעבד, שימוש בזיכרון לא משמעותי:

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

    פרסמתי דוח באגים על בעיה זו עם מציאת: http://savannah.gnu.org/bugs/?31961


    4
    2017-12-23 19:54



    זה הציל אותי! - jestro


    אני לאחרונה התמודד עם בעיה דומה ולא היה מסוגל לקבל ring0's data=writeback הצעה לעבודה (אולי בשל העובדה כי הקבצים נמצאים על המחיצה העיקרית שלי). בעוד אני חוקר דרכים לעקיפת הבעיה נתקלתי בזה:

    tune2fs -O ^has_journal <device>
    

    זה יכבה את היומן לחלוטין, ללא קשר data אפשרות לתת mount. שילבתי את זה עם noatime ואת נפח היה dir_index להגדיר, וזה נראה עובד די טוב. המחיקה למעשה הסתיימה בלי שאני צריך להרוג את זה, המערכת שלי נשאר מגיב, וזה עכשיו לחזור ולהפעיל (עם היומן בחזרה) ללא בעיות.


    3
    2018-04-23 22:29



    אני הולך להציע הרכבה כמו ext2 במקום ext3, כדי למנוע יומן כתבי עת metadata. זה צריך לעשות את אותו הדבר. - Peter Cordes


    ודא שאתה מבצע את הפעולות הבאות:

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

    אשר צריך להאיץ את הדברים קצת גם כן.


    3
    2017-09-27 02:03



    שיחה טובה, אבל זה כבר רכוב noatime, כפי שהזכרתי את הכותרת לשאלה. ו nodiratime הוא מיותר; ראה lwn.net/Articles/245002 . - BMDan
    ppl לחזור על המנטרה "noatime, nodiratime, nodevatime, noreadingdocsatime" - poige


    הפקודה מאוד איטית. נסה:

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

    2
    2017-09-23 04:04



    רם רץ יום-יום וחצי, ולבסוף הרגתי אותו, בלי לדעת אם הוא באמת השיג משהו. הייתי זקוק לבר התקדמות. - BMDan
    באשר RM להיות איטי מאוד, "זמן למצוא. -Delete" על 30k קבצים: 0m0.357s / 0m0.019s / 0m0.337s אמיתי / משתמש / sys. "time (ls -1U | xargs rm -f)" באותם קבצים: 0m0.366s / 0m0.025s / 0m0.340s. וזה בעצם טריטוריה של טעות השגיאה. - BMDan
    אתה יכול פשוט לרוץ strace -r -p <pid of rm> כדי לצרף את התהליך כבר פועל RM. אז אתה יכול לראות כמה מהר unlink שיחות המערכת הן גלילה בעבר. (-r מעביר את הזמן מאז השיחה הקודמת המערכת בתחילת כל שורה.) - Peter Cordes