更好的C#设计,可以异步遍历文件系统以每天处理大量文件

时间:2019-07-28 21:22:21

标签: c# rabbitmq filesystems

我正在创建一个C#控制台应用程序,该应用程序将遍历给定的文件夹(和子文件夹)以加密所有文件(二进制或文本)并更新sqlserver数据库中的IsEncrypted标志。客户端盒上将有数百万个文件需要加密。我们计划每天在下班时间按计划任务运行该应用程序(例如从晚上10点开始每天运行8个小时)。

我有两个选择:

选项1

使用Parallel.ForEach进行文件处理。

public void Process(ProcessorOptions options, ProcessorParameter parameter)
{
    int counter = 0;
    CancellationTokenSource cts = new CancellationTokenSource();
    ParallelOptions parallelOptions = new ParallelOptions();
    parallelOptions.CancellationToken = cts.Token;

    try
    {
        parallelOptions.MaxDegreeOfParallelism = Environment.ProcessorCount;
        if (options.NumberOfThreads > 0)
        {
            parallelOptions.MaxDegreeOfParallelism = options.NumberOfThreads;
        }

        if (options.StopTime != 0)
        {
            Timer timer = new Timer(callback => { cts.Cancel(); }, null, options.StopTime * 60000, Timeout.Infinite);
        }

        List<string> storagePaths = parameter.StoragePaths;
        Log("Process Started...");

        foreach (var path in storagePaths)
        {
            Parallel.ForEach(TraverseDirectory(path, f => f.Extension != ".enc"), parallelOptions, file =>
            {
                if (file.Name.IndexOf("SRSCreate.dir") < 0)
                {
                    ProcessFile(parameter, file.FullName, file.Directory.Name, file.Name);
                    counter++;
                }
            });
        }
        Log(string.Format("Process Files Ended... Total File Count = {0}", counter));
    }
    catch (OperationCanceledException ex)
    {
        log.WriteWarningEntry(string.Format("Reached stop time = {0} min, explicit cancellation triggered. Total number of files processed = {1}", options.StopTime, counter.ToString()), ex);
    }
    catch (Exception ex)
    {                
        log.WriteErrorEntry(ex);
    }
    finally
    {
        cts.Dispose();
    }
}

我对它进行了基准测试,发现处理2000个文件几乎需要7-8分钟。我可以做些什么来改善性能吗?另外,确定下一次运行(第二天)从哪里开始的最佳方法是什么?

选项2

使用RabbitMQ的现有设计来推送带有文件路径的消息,以处理文件,以实现可伸缩性和维护列表。

public void Process(ProcessorOptions options, ProcessorParameter parameter)
{
    try
    {
        using (IConnection connection = parameter.ConnectionFactory.CreateConnection())
        {
            using (IModel channel = connection.CreateModel())
            {
                var queueName = parameter.TopicSubscription.DeriveQueueName();
                var queueDeclareResponse = channel.QueueDeclare(queueName, true, false, false, null);
                EventingBasicConsumer consumer = new EventingBasicConsumer(channel);

                consumer.Received += (o, e) =>
                {
                    string messageContent = Encoding.UTF8.GetString(e.Body);
                    FileData message = JsonConvert.DeserializeObject(messageContent, typeof(FileData)) as FileData;
                    ProcessFile(parameter, message.EntityId, message.Attributes["Id"], message.Attributes["filename"]);
                };

                string consumerTag = channel.BasicConsume(queueName, true, consumer);
            }
        }
    }
    catch (Exception ex)
    {
        log.WriteErrorEntry(ex);
    }
    finally
    {
        Trace.Exit(method);
    }
}

在配置了StopTime之后,我仍然必须弄清楚如何停止阅读消息。性能不是很好,我看到它需要大约25-30分钟来处理2000个文件。我们认为我们可以在一台或多台计算机上运行应用程序的多个副本,以处理单个队列以进行扩展。您认为吗,我可以更改此代码以使其更优化?

最后一个问题:您是否认为还有其他选择比上述选择更有效,更可扩展?

注意:

1)方法ProcessFile调用加密逻辑和用于更新数据库的逻辑。

2)我们遍历文件夹而不是从数据库开始,因为有可能文件系统中的文件在数据库中尚不存在。

2 个答案:

答案 0 :(得分:0)

  

我不确定生产中将涉及多少个物理驱动器。但是如果需要,客户可以添加更多。未加密的文件将替换为同一台服务器上的已加密文件,由于未加密的文件存在安全风险,因此需要对100%的文件进行加密,并且每天的计数都会减少。是的,加密要求文件在内存中以运行算法。文件的平均大小约为3 mb。我知道文件大小没有限制,但是通常我们会得到巨大的图像文件,Word和Excel文档,然后是一些小文本文件。

我可以看到问题有很多未知数,这表明单个配置不能在所有情况下都能解决。所以我的建议是使系统灵活。我将从为所涉及的物理驱动器创建配置开始。每个物理驱动器应具有并发设置。一个SSD驱动器可以在同时读写2-3个线程的情况下达到最佳工作状态,而一个硬盘驱动器可能会遇到多个线程。下一个重要的设置是加密器线程的数量。理想情况下,当正在运行的线程数等于计算机的可用处理器/核心数时,系统应能最好地工作。执行流程应如下所示:

  • IO线程正在从其关联的物理驱动器读取文件或向其关联的物理驱动器写入文件。
  • 当IO线程完成读取未加密的文件时,会将其排队在全局队列中进行处理。
  • 当IO线程完成写入加密文件时,它还会更新数据库。
  • 加密器线程一直在池全局队列中处理文件。
  • 加密线程完成文件处理后,会将其排队到文件物理驱动器的专用队列中。
  • 当IO线程空闲时,它将查看其关联的物理驱动器的专用队列(如果有任何已处理的文件)。如果存在,它将出队并将其写入磁盘。如果不是,则继续从磁盘读取另一个文件。

所有这些都可以通过线程或任务以及BlockingCollection类来实现。无需Parallel.ForEach或第三方库。

答案 1 :(得分:-1)

这涉及到绩效问题,因此我将首先链接绩效问题:https://ericlippert.com/2012/12/17/performance-rant/

从本质上讲,此操作应该是磁盘绑定的,而不是CPU绑定的。进程可以快速完成文件的速度以及它可以快速读取,加密和写入文件的速度-显然都与磁盘绑定。在磁盘上同时执行更多操作将使速度变慢而不是变快。除非您当然有一些极端的设置,例如SSD的Raid 0。

如果可以从多任务处理中受益,则应该是数据库访问。通常,这些操作会通过网络堆栈进行,尤其是如果数据库在另一台计算机上,则很有可能会比磁盘慢。同时,您不想向数据库发送查询垃圾邮件。所有查询都有开销,而1 200行查询比200 1行查询要快。因此,以某种形式的Enuemration或Streaming方法获取DB数据,然后在文件上执行操作。但最慢的情况取决于每次运行时有多少个新/未加密文件。

将整个内容移到数据库中是可行的。有两种方法可以将BLOBS与DB一起存储,听起来您正在使用“在磁盘上存储,仅在DB中链接”。如果是这样,Filestream之类的属性可能会帮助您:https://www.red-gate.com/simple-talk/sql/learn-sql-server/an-introduction-to-sql-server-filestream/

稍微偏离主题,但是我的Pet-Peeve是异常处理,您的示例代码有一个主要的缺点:

catch (Exception ex)
{
    log.WriteErrorEntry(ex);
}

您捕获了Exception,但不让它继续,这意味着您在致命的异常之后继续。这只会给您带来更多-而又难以理解-跟进异常。因此,您绝对不要那样做。我有两篇关于异常处理的文章确实链接很多,我认为它们可以在这里为您提供帮助: