向现有的Zip文件中添加文件 - 性能问题

19

我有一个保存文件到文件夹(大约有 20 万个小文件)的 WCF Web 服务。之后,我需要将它们移动到另一台服务器上。

我找到的解决方案是将它们压缩然后再移动。

当我采用这个解决方案时,我进行了 (20,000 个文件) 的测试,压缩 20,000 个文件只需要大约 2 分钟,移动 zip 文件非常快。但在生产环境中,压缩 200,000 个文件需要超过 2 小时。

以下是我的压缩文件夹代码:

using (ZipFile zipFile = new ZipFile())
{
    zipFile.UseZip64WhenSaving = Zip64Option.Always;
    zipFile.CompressionLevel = CompressionLevel.None;
    zipFile.AddDirectory(this.SourceDirectory.FullName, string.Empty);

    zipFile.Save(DestinationCurrentFileInfo.FullName);
}

我想修改WCF Web服务,使其不再保存到文件夹,而是保存到zip文件中。

我使用以下代码进行测试:

var listAes = Directory.EnumerateFiles(myFolder, "*.*", SearchOption.AllDirectories).Where(s => s.EndsWith(".aes")).Select(f => new FileInfo(f));

foreach (var additionFile in listAes)
{
    using (var zip = ZipFile.Read(nameOfExistingZip))
    {
        zip.CompressionLevel = Ionic.Zlib.CompressionLevel.None;
        zip.AddFile(additionFile.FullName);

        zip.Save();
    }

    file.WriteLine("Delay for adding a file  : " + sw.Elapsed.TotalMilliseconds);
    sw.Restart();
}

第一个文件添加到压缩包只需要5毫秒,但第10000个文件需要800毫秒。

有没有什么方法可以优化这个过程?或者您有其他建议吗?

编辑

上面显示的示例仅用于测试,在WCF webservice中,我将有不同的请求发送文件,需要将其添加到Zip文件中。 由于WCF是无状态的,每次调用都会有一个新的类实例,那么如何保持Zip文件打开以添加更多文件?


你尝试过调整创建压缩文件的设置吗?如果压缩时间太长,可能使用了过于强力的压缩方式。或者您是否需要将所有小文件都写出来,还是可以定义一种格式,使您可以将它们写成一个文件?虽然会失去压缩功能,但这样做会更容易些。 - Guvante
2
为什么每次添加文件都要打开、添加、保存和关闭压缩文件?你可以多次调用“AddFile”。 - Paul Abbott
2
反复打开和保存文件的目的是为了增加鲁棒性,以防进程在中途失败时导致所有文件丢失。随着文件变得越来越大,很可能是这种重复的打开/保存过程不断消耗更多时间。您可以通过减少保存频率(例如每100个文件保存一次)来减少开销,同时仍然保持一定的鲁棒性。 - Dan Bryant
@Anas:你的问题开始时说你保存了200,000个文件,然后将它们压缩并移动,但最后你又表示你正在上传文件到一个zip文件中。为什么不直接将文件添加到磁盘上的目录中,直到达到特定的阈值,然后一次性将它们全部压缩并发送? - StriplingWarrior
1
@StriplingWarrior 在一开始,我做出了这样的假设:如果压缩 20,000 个文件需要 2 分钟,那么压缩 200,000 个文件需要 20 分钟,但事实并非如此,压缩 200,000 个文件需要超过 2 小时。因此,我想到了一个方法,不再将文件先保存到磁盘上,而是直接保存到 zip 文件中,这可能会节省时间。 - Anas
显示剩余7条评论
5个回答

11

我看了你的代码并立即发现了问题。现在很多软件开发人员的问题是他们不理解东西是如何工作的,这使得无法“推理”。在这种特殊情况下,你似乎不知道ZIP文件是如何工作的;因此,我建议你首先阅读“Zip文件格式”并尝试分解其背后的“发生”的过程。

推理

现在我们都了解了它们的工作方式,让我们通过使用你的源代码来分解它们的工作方式,并从那里继续推理:

var listAes = Directory.EnumerateFiles(myFolder, "*.*", SearchOption.AllDirectories).Where(s => s.EndsWith(".aes")).Select(f => new FileInfo(f));

foreach (var additionFile in listAes)
{
    // (1)
    using (var zip = ZipFile.Read(nameOfExistingZip))
    {
        zip.CompressionLevel = Ionic.Zlib.CompressionLevel.None;
        // (2)
        zip.AddFile(additionFile.FullName);

        // (3)
        zip.Save();
    }

    file.WriteLine("Delay for adding a file  : " + sw.Elapsed.TotalMilliseconds);
    sw.Restart();
}
  • (1) 打开一个ZIP文件。你每次尝试添加文件时都要这样做
  • (2) 向ZIP文件中添加单个文件
  • (3) 保存完整的ZIP文件

在我的电脑上,这需要大约一个小时。

现在,并非所有的文件格式细节都相关。我们正在寻找会在程序中变得越来越糟糕的东西。

浏览文件格式规范,您会注意到压缩基于Deflate,它不需要关于被压缩的其他文件的信息。然后,我们将注意到“文件表”如何存储在ZIP文件中:

Zip file structure

您会注意到这里有一个“中央目录”,其中存储了ZIP文件中的文件。它基本上被存储为“列表”。因此,使用此信息,我们可以推断实现按照这种顺序执行步骤(1-3)的平凡方式是什么:

  • 打开zip文件,读取中央目录
  • 追加(新)压缩文件的数据,在新中央目录中存储指针和文件名。
  • 重新编写中央目录。

想一想,对于文件#1,您需要进行1个写操作;对于文件#2,您需要读取(1个项目),附加(内存中)并写入(2个项目);对于文件#3,您需要读取(2项),附加(内存中)并写入(3项)。依此类推。这基本上意味着,如果您添加更多文件,性能将会变得非常糟糕。您已经观察到了这一点,现在您知道原因了。

一个可能的解决方案

在以前的解决方案中,我一次性添加了所有文件。在您的用例中,这可能行不通。另一种解决方案是实现合并,该合并基本上每次将2个文件合并在一起。如果在开始压缩过程时没有所有文件可用,则这更为方便。

基本上算法如下:

  1. 添加一些(例如16个)文件。您可以玩弄这个数字。将其存储在“file16.zip”中。
  2. 添加更多文件。当您达到16个文件时,必须将16个项目的两个文件合并为32个项目的单个文件。
  3. 合并文件,直到无法再合并。基本上,每当您有两个N项的文件时,就会创建一个新的2*N项的文件。
  4. 转到(2)。

同样,我们可以推理。前16个文件不是问题,我们已经确定了这一点。

我们还可以推断出我们的程序会发生什么。因为我们将2个文件合并成1个文件,所以我们不必执行太多的读取和写入操作。实际上,如果您考虑一下,您会发现您拥有32个条目的文件需要进行2次合并,64个条目需要进行4次合并,128个条目需要进行8次合并,256个条目需要进行16次合并……嘿,等等,我们知道这个序列是 2^N 。同样,通过推理,我们会发现我们需要大约500次合并-这比我们开始的200,000个操作要好得多。

在ZIP

static void Main()
{
    try { File.Delete(@"c:\tmp\test.zip"); }
    catch { }

    var sw = Stopwatch.StartNew();

    using (var zip = new ZipFile(@"c:\tmp\test.zip"))
    {
        zip.UseZip64WhenSaving = Zip64Option.Always;
        for (int i = 0; i < 200000; ++i)
        {
            string filename = "foo" + i.ToString() + ".txt";
            byte[] contents = Encoding.UTF8.GetBytes("Hello world!");
            zip.CompressionLevel = Ionic.Zlib.CompressionLevel.None;
            zip.AddEntry(filename, contents);
        }

        zip.Save();
    }

    Console.WriteLine("Elapsed: {0:0.00}s", sw.Elapsed.TotalSeconds);
    Console.ReadLine();
}

哇,这只用了4.5秒就完成了。好多了。


@Anas 你没说那个 :-) 在这种情况下,我会先将它们存储在临时位置,或者两两合并ZIP文件。如果所有这些都不起作用,您可以尝试过度分配ZIP目录表,这也应该能解决问题;但这需要在ionic库中进行黑客攻击。 - atlaste
@Anas 仍然,我会尝试一次添加所有文件。你可以将单例模式与计时器结合使用。当向ZIP文件添加文件时锁定以避免并发问题。如果计时器达到零(例如10秒后)或者如果达到200K文件,则将其刷新到磁盘。我可能也会实现IDisposable和未捕获异常处理程序,以确保在几乎所有情况下数据都被刷新。无论哪种方式,临时位置都比较“安全”,以防停电等情况。 - atlaste
1
可能更容易选择tar路线或自己的简单追加文件格式,而不是坚持使用zip,特别是因为干预格式会在需要确保归档未损坏的任何类型的CRC时引起麻烦。不确定合并文件是否会产生实质性的CPU效益(肯定没有I/O效益)。 - Erwin Mayer
@ErwinMayer 抱歉,但我不同意。 如果您阅读 OP 的问题,他指出文件是批量移动的。 他还明确问到 ZIP 文件,这将完美地发挥作用。 至于合并,我不确定您的目标是什么,但 IO 收益肯定存在:它是 O(n log n) 而不是 O(n²),并且只有顺序访问。 我确实同意 TAR 文件也可以起作用-但这意味着您不应该压缩它(否则会丢失必要的信息)。 - atlaste
1
@atlaste 是的,他问到了Zip文件,但由于在他的代码示例中它们没有被压缩,我认为考虑这实际上是(非)要求的一部分是明智的。当然,如果远程服务器绝对需要ZIP文件,那么就无法避免。您可以将TAR压缩文件以仍然具有某些压缩优势。对于您建议的合并解决方案,它如何成为I/O O(n log n)?每次创建一个新的带有附加文件的归档文件时,您都必须重新读取整个未压缩的-相同大小-存档文件。 - Erwin Mayer
显示剩余2条评论

3
我可以看出你只想把这20万个文件分组成一个大文件,不需要压缩(就像一个tar归档文件)。 探索的两个想法:
  1. 尝试使用比 Zip 更快的其他文件格式。我想到了 Tar(磁带归档)(由于其简单性而具有自然的速度优势),它甚至有一个追加模式,这正是您需要确保 O(1) 操作的内容。SharpCompress 是一个允许您使用此格式(以及其他格式)进行操作的库。

  2. 如果您可以控制远程服务器,您可以实现自己的文件格式。我能想到的最简单的方法是将每个新文件分别压缩为 zip 文件(将文件元数据(如名称、日期等)存储在文件内容本身中),然后将每个这样的压缩文件附加到单个原始字节文件中。您只需要存储字节偏移量(在另一个 txt 文件中用列分隔)即可让远程服务器将大型文件拆分为 200,000 个压缩文件,然后解压缩每个文件以获取元数据。我猜这也大致是 tar 在幕后所做的 :)

  3. 尝试将压缩结果保存到 MemoryStream 而不是文件中,在一天结束时再将其刷新到文件中。当然,为了备份目的,您的 WCF 服务必须保留接收到的单个文件的副本,直到您确定它们已被提交到远程服务器为止。

  4. 如果确实需要压缩,7-Zip(并调整选项)是值得一试的。


嗨,Erwin,我一定会尝试你的建议并保持更新,谢谢!! - Anas

0

如果您对处理100*20000个文件的运行效果感到满意,那么您是否可以将大型ZIP文件分为100个“小”ZIP文件呢?为了简单起见,每分钟创建一个新的ZIP文件,并在名称中放置时间戳。


0
你一直在重复打开文件,为什么不通过循环将它们全部添加到一个zip文件中,然后保存呢?
var listAes = Directory.EnumerateFiles(myFolder, "*.*", SearchOption.AllDirectories)
    .Where(s => s.EndsWith(".aes"))
    .Select(f => new FileInfo(f));

using (var zip = ZipFile.Read(nameOfExistingZip))
{
    foreach (var additionFile in listAes)
    {
        zip.CompressionLevel = Ionic.Zlib.CompressionLevel.None;
        zip.AddFile(additionFile.FullName);
    }
    zip.Save();
}

如果文件不是一下子全部都可用的,你至少可以把它们分批处理。所以,如果你预计会有20万个文件,但目前只收到了10个,不要打开压缩文件,添加一个文件,然后关闭它。等待更多的文件到达后,再将它们分批添加。


1
他在评论和编辑中回答了这个问题。他正在异步上传文件,希望保持一个运行的存档将有助于避免在所有文件上传后出现2小时的大量访问。 - StriplingWarrior

-1

您可以使用 .Net TPL(任务并行库)压缩所有文件,方法如下:

    while(0 != (read = sourceStream.Read(bufferRead, 0, sliceBytes)))
{
   tasks[taskCounter] = Task.Factory.StartNew(() => 
     CompressStreamP(bufferRead, read, taskCounter, ref listOfMemStream, eventSignal)); // Line 1
   eventSignal.WaitOne(-1);           // Line 2
   taskCounter++;                     // Line 3
   bufferRead = new byte[sliceBytes]; // Line 4
}

Task.WaitAll(tasks);                  // Line 6

这里有一个已编译的库和源代码:

http://www.codeproject.com/Articles/49264/Parallel-fast-compression-unleashing-the-power-of


谢谢您的回答,但是这个库似乎只能压缩文件,不能压缩文件夹? - Anas
1
TPL几乎从来不是加速的解决方案。在这种情况下也不是,因为问题是I/O优化不良,类似于字符串连接抖动。 - Aron

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接