【问题标题】:Improve the speed to process many many small files提高处理许多小文件的速度
【发布时间】:2020-10-19 06:07:39
【问题描述】:

我们有一个应用程序需要在启动时处理许多小文件(1000 万~1 亿个文件,大小均为 1KB~2KB)。

这些文件位于一个根目录下的多个嵌套目录下。

我们需要做的很简单:读取文件的前128字节,并检查文件的有效性(检查本身很便宜)。

目前我们只是递归地读取目录并一一读取文件,这几乎导致我们完成该过程。

我们尝试了多个进程,当我们使用所有内核时,时间减少到 4 小时。

我们尝试了linux原生的Aio,时间缩短到3~4小时。

还有其他方法可以减少处理时间吗?

【问题讨论】:

  • 您可能受到文件系统 IO 操作的限制 - 使用所有内核实际上可能会减慢您的速度,我会使用不同数量的内核进行测试,看看什么是最佳的。有没有影响这些文件的生成方式?它们可以分块转储到更少的文件中吗?你有时间连接一些而其他的正在生成吗?提供完整的过程将使答案更加灵活。并确保这不是 XY 问题。
  • Linux 系统将任何一个进程可以打开的文件描述符的数量限制为每个进程 1024 个。在目录服务器超过每个进程 1024 的文件描述符限制后,任何新进程和工作线程都将被阻止。使用ulimit 命令将文件描述符限制设置为unlimited
  • @JoopEggen 你在说rsync吗?
  • 磁盘分区有多大?如果它非常小,则将数据复制到 RAM 中可能会快得多。或者,如果可能的话,将所有文件打包成一个大文件也可能更有效。考虑将数据移动到(NVMe)SSD,这种操作速度要快得多(请参阅IOPS)。文件系统可能也很重要。
  • 这听起来像是一个你需要分解和剖析的问题......流程的每个部分花费了多少时间(枚举文件,读取 128 个字节,“检查有效性”等...)。你能定义“非常便宜”吗?使用较小的数据集,操作时间约为 15-30 秒。

标签: c linux performance file


【解决方案1】:

您介意测试以下verifier.c吗?

/* SPDX-License-Identifier: CC0-1.0 */
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <pthread.h>
#include <limits.h>
#include <fcntl.h>
#include <ftw.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

/* Singly-linked list of paths. */
struct path_list {
    struct path_list *next;
    int               err;
    char              path[];
};

/* File content verifier.  Returns 0 if OK, errno error code otherwise. */
static int verify_data(const unsigned char *data, const size_t size)
{
    /*
     * TODO!
    */

    (void)data; (void)size; /* Remove; these just silence warnings about unused parameters. */

    return 0;
}


static inline void free_path_list(struct path_list *list)
{
    while (list) {
        struct path_list *curr = list;

        list = list->next;

        curr->next = NULL;
        curr->err  = -1;
        free(curr);
    }
}

/* Number of descriptors excluded in the calculations */
#ifndef  RESERVE_DESCRIPTORS
#define  RESERVE_DESCRIPTORS  10
#endif

/* Minimum number of descriptors for nftw() */
#ifndef  MINIMUM_NFTW_DESCRIPTORS
#define  MINIMUM_NFTW_DESCRIPTORS  16
#endif

/* Maximum number of descriptors for nftw()
   - recommended number is twice the maximum depth of the tree. */
#ifndef  MAXIMUM_NFTW_DESCRIPTORS
#define  MAXIMUM_NFTW_DESCRIPTORS  256
#endif

static int   verify_tree_add(const char *, const struct stat *, int);
static void *verify_tree_worker(void *);

static pthread_t        *verify_worker  = NULL;
static size_t            verify_workers = 0;
static unsigned long     verify_count_ok = 0;
static unsigned long     verify_count = 0;
static pthread_mutex_t   verify_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t    verify_more = PTHREAD_COND_INITIALIZER;
static struct path_list *verify_list = NULL;    /* Paths to be verified */
static struct path_list *verify_fail = NULL;    /* Paths that failed the check */
static int               verify_done = 0;      /* Set when path buffer completed */

/* Verify all regular files in the specified tree using 'workers' threads.
   Returns the list of files that failed verification.
   Always sets errno: to zero if success, to nonzero if error.
*/
static struct path_list *verify_tree(const char *root, const size_t workers)
{
    struct path_list *retval;
    pthread_attr_t    attrs;
    struct rlimit     limit;
    long              maxfd;
    size_t            i;
    int               result;

    /* Sanity check. */
    if (!root || *root == '\0' || workers < 1) {
        errno = EINVAL;
        return NULL;
    }

    /* Maximum number of file descriptors */
    if (getrlimit(RLIMIT_NOFILE, &limit) == -1)
        return NULL;

    /* If limited, try to up the soft limit to the hard limit. */
    if (limit.rlim_max != RLIM_INFINITY && limit.rlim_cur < limit.rlim_max) {
        limit.rlim_cur = limit.rlim_max;
        setrlimit(RLIMIT_NOFILE, &limit);
    }

    /* Find out the maximum number of file descriptors. */
    maxfd = sysconf(_SC_OPEN_MAX);
    if (limit.rlim_cur != RLIM_INFINITY && (long)limit.rlim_cur < maxfd)
        maxfd = (long)limit.rlim_cur;

    /* Subtract the number of reserved descriptors and those needed by workers */
    if (maxfd > (long)(workers + RESERVE_DESCRIPTORS))
        maxfd -= workers + RESERVE_DESCRIPTORS;
    /* and limit to the configured range. */
    if (maxfd < MINIMUM_NFTW_DESCRIPTORS)
        maxfd = MINIMUM_NFTW_DESCRIPTORS;
    if (maxfd > MAXIMUM_NFTW_DESCRIPTORS)
        maxfd = MAXIMUM_NFTW_DESCRIPTORS;

    /* Initialize worker information. */
    verify_workers = 0;
    verify_worker  = malloc(workers * sizeof verify_worker[0]);
    if (!verify_worker) {
        errno = ENOMEM;
        return NULL;
    }

    pthread_mutex_init(&verify_lock, NULL);
    pthread_cond_init(&verify_more, NULL);
    verify_list = NULL;
    verify_fail = NULL;
    verify_done = 0;

    /* Clear the counters. */
    verify_count = 0;
    verify_count_ok = 0;

    /* Threads need very little stack. */
    pthread_attr_init(&attrs);
    pthread_attr_setstacksize(&attrs, 2*PTHREAD_STACK_MIN);
    /* Start the worker threads. */
    for (i = 0; i < workers; i++) {
        result = pthread_create(verify_worker + verify_workers, &attrs, verify_tree_worker, NULL);
        if (!result)
            verify_workers++;
    }
    /* Discard the thread creation attribute set. */
    pthread_attr_destroy(&attrs);

    /* No workers started? */
    if (verify_workers < 1) {
        free(verify_worker);
        verify_workers = 0;
        verify_worker = NULL;
        verify_done = -1;
        pthread_cond_destroy(&verify_more);
        pthread_mutex_destroy(&verify_lock);
        /* Insufficient resources */
        errno = EAGAIN;
        return NULL;
    }

    /* Start directory scan. */
    result = ftw(root, verify_tree_add, maxfd);
    if (result) {
        /* Failed; abort. Save errno. */
        if (result == -1)
            result = errno;

        /* Signal threads to cancel work. */
        pthread_mutex_lock(&verify_lock);
        verify_done = -1;
        pthread_cond_broadcast(&verify_more);
        pthread_mutex_unlock(&verify_lock);

        /* Reap worker threads. */
        for (i = 0; i < verify_workers; i++)
            pthread_join(verify_worker[i], NULL);

        /* Grab the failed paths list. */
        retval = verify_fail;
        verify_fail = NULL;

        /* Discard any paths not verified. */
        free_path_list(verify_list);
        verify_list = NULL;

        /* Cleanup. */
        free(verify_worker);
        verify_workers = 0;
        verify_worker = NULL;
        pthread_cond_destroy(&verify_more);
        pthread_mutex_destroy(&verify_lock);

        errno = result;
        return retval;
    }

    /* Success. Signal threads that no more paths will be added. */
    pthread_mutex_lock(&verify_lock);
    verify_done = 1;
    pthread_cond_broadcast(&verify_more);
    pthread_mutex_unlock(&verify_lock);

    /* Reap worker threads. */
    for (i = 0; i < verify_workers; i++)
        pthread_join(verify_worker[i], NULL);

    /* Will return the failed paths list. */
    retval = verify_fail;
    verify_fail = NULL;

    /* Prepend all unprocessed paths -- should be none! -- to failed list. */
    while (verify_list) {
        struct path_list *curr = verify_list;
        verify_list = curr->next;
        curr->next = retval;
        retval = curr;
    }

    /* Cleanup. */
    free(verify_worker);
    verify_workers = 0;
    verify_worker = NULL;
    pthread_cond_destroy(&verify_more);
    pthread_mutex_destroy(&verify_lock);

    /* Success. */
    errno = 0;
    return retval;
}

/* Add regular files to the work pool. */
static int verify_tree_add(const char *path, const struct stat *info, int typeflag)
{
    struct path_list  *item;
    size_t             path_len;

    /* Ignore all but regular files. */
    if (typeflag != FTW_F)
        return 0;

    /* Ignore zero-sized files. */
    if (!info->st_size)
        return 0;

    /* Ignore NULL and empty paths, although they shouldn't happen. */
    if (!path || !*path)
        return 0;

    path_len = strlen(path);
    item = malloc(sizeof (struct path_list) + path_len + 1);
    if (!item)
        return ENOMEM;

    /* Copy the path, including the end-of-string '\0'. */
    memcpy(item->path, path, path_len + 1);

    /* This path item should be OK. */
    item->err = 0;

    /* Prepend to the path list. */
    pthread_mutex_lock(&verify_lock);
    item->next = verify_list;
    verify_list = item;
    /* Signal a waiting worker. */
    pthread_cond_signal(&verify_more);
    /* Add to count. */
    verify_count++;
    pthread_mutex_unlock(&verify_lock);

    return 0;
}

/* Work pool worker thread. */
static void *verify_tree_worker(void *unused __attribute__((unused)))
{
    unsigned char     data[128];
    size_t            have;
    ssize_t           bytes;
    int               fd, result;
    struct path_list *curr;

    pthread_mutex_lock(&verify_lock);
    while (verify_done >= 0) {

        /* No paths in the list? */
        if (!verify_list) {
            /* All done? */
            if (verify_done)
                break;

            /* No, wait for a new path. */
            pthread_cond_wait(&verify_more, &verify_lock);
            continue;
        }

        /* Extract path from the list. */
        curr = verify_list;
        verify_list = curr->next;

        /* Release the mutex for the duration of the verification work. */
        pthread_mutex_unlock(&verify_lock);

        /* Open the file to be verified. */
        fd = open(curr->path, O_RDONLY | O_CLOEXEC);
        if (fd == -1) {
            curr->err = errno;

            /* Append to failed list. */
            pthread_mutex_lock(&verify_lock);
            curr->next = verify_fail;
            verify_fail = curr;
            continue;
        }

        /* Read the initial part of the file, or entire file if it fits in the buffer. */
        have = 0;
        while (have < sizeof data) {
            bytes = read(fd, data + have, sizeof data - have);
            if (bytes > 0) {
                have += bytes;
            } else
            if (bytes == 0) {
                /* All read */
                break;
            } else
            if (bytes != -1) {
                /* Bug. Mark as an I/O error. */
                curr->err = EIO;
                have = 0;
                break;
            } else
            if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
                curr->err = errno;
                have = 0;
                break;
            }
        }

        if (close(fd) == -1) {
            /* This does not normally occur, but let's be thorough. */
            have = 0;
            if (!curr->err)
                curr->err = errno;
        }

        /* Empty file or error? */
        if (!have) {
            /* Append to failed list. */
            pthread_mutex_lock(&verify_lock);
            curr->next = verify_fail;
            verify_fail = curr;
            continue;
        }

        /* Verify file contents. */
        result = verify_data(data, have);
        if (result) {
            /* Append to failed list. */
            pthread_mutex_lock(&verify_lock);
            curr->err = result;
            curr->next = verify_fail;
            verify_fail = curr;
            continue;
        }

        /* Verification successful. */
        free(curr);
        pthread_mutex_lock(&verify_lock);
        verify_count_ok++;
    }
    pthread_mutex_unlock(&verify_lock);

    return NULL;
}

static int parse_size(const char *src, size_t *to)
{
    const char    *end;
    unsigned long  val;

    if (!src)
        return errno = EINVAL;

    errno = 0;
    end = src;
    val = strtoul(src, (char **)(&end), 0);
    if (errno)
        return errno;
    if (end == src)
        return errno = EINVAL;

    while (*end == '\t' || *end == '\n' || *end == '\v' ||
           *end == '\f' || *end == '\r' || *end == ' ')
        end++;

    if (*end)
        return errno = EINVAL;

    if ((unsigned long)(size_t)(val) != val)
        return errno = ERANGE;

    if (to)
        *to = (size_t)val;

    return 0;
}

int main(int argc, char *argv[])
{
    struct path_list *list, *curr;
    size_t            threads;

    if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[2], "--help")) {
        const char *self = (argc > 0 && argv[0]) ? argv[0] : "(this)";
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s DIRECTORY THREADS\n", self);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program scans all regular files in DIRECTORY\n");
        fprintf(stderr, "and all its subdirectories, using THREADS threads\n");
        fprintf(stderr, "in parallel.\n");
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    if (parse_size(argv[2], &threads) || threads < 1) {
        fprintf(stderr, "%s: Invalid number of threads.\n", argv[2]);
        return EXIT_FAILURE;
    }

    list = verify_tree(argv[1], threads);
    if (errno) {
        fprintf(stderr, "Verification failed: %s.\n", strerror(errno));
        for (curr = list; curr; curr = curr->next)
            printf("  %s: (%s)\n", curr->path, strerror(curr->err));
        free_path_list(list);
        return EXIT_FAILURE;
    }

    if (list) {
        fprintf(stderr, "Verification complete: %lu of %lu files ok.\n", verify_count_ok, verify_count);
        for (curr = list; curr; curr = curr->next)
            printf("  %s: (%s)\n", curr->path, strerror(curr->err));
        free_path_list(list);
        return EXIT_FAILURE;
    }

    printf("Verified all %lu files successfully!\n", verify_count_ok);
    return EXIT_SUCCESS;
}

要编译,我推荐使用

gcc -Wall -Wextra -O2 verifier.c -lpthread -o verifier

要运行,请使用类似

./verifier /tree/with/files 64

其中第一个参数是要验证的目录树,第二个参数是要使用的工作线程数。

上面的程序实现了一个简单的基于线程池的单进程多线程验证器。 (您应该在verify_data() 函数中实现实际的数据验证;现在,它假定如果文件可以读取,那就没问题。注意verify_tree_add() 函数忽略空文件;您可能希望删除该检查。以这种方式测试程序对我来说更容易。)

主线程使用ftw() 进行目录树扫描,回调函数提取完整路径,并添加到要打开和检查的路径列表中。

每个工作线程从列表中获取下一个路径,打开文件,读取前 128 个字节(或整个文件,如果文件更小),并验证内容。如果有错误,则将路径添加到失败的路径列表中(返回给调用者);否则释放。

这种方法背后的想法是,大多数工作线程将在 read() 调用中阻塞,从而允许内核优化 I/O 请求顺序并最大限度地增加正在运行的页面数。

这根本没有优化,您可能需要添加额外的保护(不要让路径列表无限增长),但底层模式应该允许您最大化吞吐量。

(在将新邮件保存到 Maildir 邮箱时,例如邮件传输代理可以使用类似的模式,其中每封邮件都是一个单独的文件。为了确保每个文件都已访问存储设备,fsync()(或至少@987654332 @) 需要在描述符上调用,但这可能需要相当长的时间。让一个工作线程在fsync()/fdatasync() 调用上执行保存和阻塞允许邮件传输代理同时处理其他消息。它在吞吐量上有很大的不同。)

为了测试,我比较了

sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ; sync'
time  find /usr -type f -ls > /dev/null

实时耗时约 30 秒;和

sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ; sync'
time  ./verifier /usr 60

这需要大约 50 秒的实时时间。

在这两个命令中,sudo 部分清除页表缓存(详见man 5 proc 部分/proc/sys/vm/drop_caches),因此当计算机尚未缓存任何文件内容并且相对静止时,这两种情况对应wrt。 I/O(sync 部分)。

运行 cache-hot,这两个命令分别需要大约 8 和 30 秒。

(为了测试,我使用了一个验证器来检查第一个字节是否为 0x1c,最后读取的字节是否为 0xfe,以确保编译器没有优化任何内容。换句话说,这些测试确实读取了最初的 128 个字节,或者整个文件(如果较小)。)

这是 HP EliteBook 820 G1 笔记本电脑,配备 Intel i5-4200U 处理器和三星 SSD,/usr 下有 989610(非空)文件。显然,SSD 的性能远胜于旋转磁盘存储,尤其是对于这种工作负载(小文件,即大量小的 I/O 传输块),但对于 100-2 亿个文件来说,三个小时(或更长时间)对我来说听起来有点过分了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-08-15
    • 1970-01-01
    • 1970-01-01
    • 2017-07-25
    • 2022-12-14
    • 2019-10-18
    相关资源
    最近更新 更多