6

我正在尝试使用 HTTP 将大文件 (>1GB) 从一台服务器下载到另一台服务器。为此,我正在并行发出 HTTP 范围请求。这让我可以并行下载文件。

保存到磁盘时,我正在获取每个响应流,打开与文件流相同的文件,寻找我想要的范围,然后写入。

但是,我发现除了一个响应流之外的所有响应流都超时了。看起来磁盘 I / O 跟不上网络 I/O。但是,如果我做同样的事情,但让每个线程写入一个单独的文件,它就可以正常工作。

作为参考,这是我写入同一文件的代码:

int numberOfStreams = 4;
List<Tuple<int, int>> ranges = new List<Tuple<int, int>>();
string fileName = @"C:\MyCoolFile.txt";
//List populated here
Parallel.For(0, numberOfStreams, (index, state) =>
{
    try
    {
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("Some URL");
        using(Stream responseStream = webRequest.GetResponse().GetResponseStream())
        {
            using (FileStream fileStream = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
            {
                fileStream.Seek(ranges[index].Item1, SeekOrigin.Begin);
                byte[] buffer = new byte[64 * 1024];
                int bytesRead;
                while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    if (state.IsStopped)
                    {
                        return;
                    }
                    fileStream.Write(buffer, 0, bytesRead);
                }
            }
        };
    }
    catch (Exception e)
    {
        exception = e;
        state.Stop();
    }
});

这是写入多个文件的代码:

int numberOfStreams = 4;
List<Tuple<int, int>> ranges = new List<Tuple<int, int>>();
string fileName = @"C:\MyCoolFile.txt";
//List populated here
Parallel.For(0, numberOfStreams, (index, state) =>
{
    try
    {
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("Some URL");
        using(Stream responseStream = webRequest.GetResponse().GetResponseStream())
        {
            using (FileStream fileStream = File.Open(fileName + "." + index + ".tmp", FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
            {
                fileStream.Seek(ranges[index].Item1, SeekOrigin.Begin);
                byte[] buffer = new byte[64 * 1024];
                int bytesRead;
                while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    if (state.IsStopped)
                    {
                        return;
                    }
                    fileStream.Write(buffer, 0, bytesRead);
                }
            }
        };
    }
    catch (Exception e)
    {
        exception = e;
        state.Stop();
    }
});

我的问题是,C#/Windows 在从多个线程写入单个文件时是否会采取一些额外的检查/操作,这会导致文件 I/O 比写入多个文件时慢?所有磁盘操作都应该受磁盘速度的约束吗?谁能解释这种行为?

提前致谢!

更新:这是源服务器抛出的错误:

“无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者连接失败,因为连接的主机没有响应。” [System.IO.IOException]:“无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者连接失败,因为连接的主机没有响应。” InnerException:“连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立连接失败,因为连接的主机没有响应”消息:“无法将数据写入传输连接:

4

5 回答 5

4

除非您正在写入条带 RAID,否则您不太可能通过同时从多个线程写入文件来体验性能优势。事实上,更有可能是相反的——并发写入会交错并导致随机访问,从而导致磁盘查找延迟,这使得它们比大型顺序写入慢几个数量级。

要获得一个透视感,请查看一些延迟比较。从磁盘连续读取 1 MB 需要 20 毫秒;写入大约需要相同的时间。另一方面,每次磁盘寻道大约需要 10 毫秒。如果您的写入以 4 KB 块交错,那么您的 1 MB 写入将需要额外的 2560 毫秒的寻道时间,使其比顺序慢 100 倍。

我建议在任何时候只允许一个线程写入文件,并仅将并行性用于网络传输。您可以使用生产者-消费者模式,将下载的块写入有界并发集合(例如BlockingCollection<T>),然后由专用线程拾取并写入磁盘。

于 2015-07-31T17:50:05.883 回答
2
    fileStream.Seek(ranges[index].Item1, SeekOrigin.Begin);

该 Seek() 调用是一个问题,您将寻找与当前文件结尾相距很远的文件部分。您的下一个 fileStream.Write() 调用会强制文件系统扩展磁盘上的文件,用零填充文件中未写入的部分。

这可能需要一段时间,您的线程将被阻塞,直到文件系统完成扩展文件。可能足够长以触发超时。您会在转移开始时看到这很早就出错了。

一种解决方法是在开始写入真实数据之前创建填充整个文件。否则,下载者使用的一种非常常见的策略,您可能以前见过 .part 文件。另一个不错的好处是您可以很好地保证传输不会因为磁盘空间不足而失败。请注意,只有当机器有足够的 RAM 时,用零填充文件才便宜。1 GB 在现代机器上应该不是问题。

复制代码:

using System;
using System.IO;
using System.Diagnostics;

class Program {
    static void Main(string[] args) {
        string path = @"c:\temp\test.bin";
        var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write);
        fs.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);
        var buf = new byte[4096];
        var sw = Stopwatch.StartNew();
        fs.Write(buf, 0, buf.Length);
        sw.Stop();
        Console.WriteLine("Writing 4096 bytes took {0} milliseconds", sw.ElapsedMilliseconds);
        Console.ReadKey();
        fs.Close();
        File.Delete(path);
    }
}

输出:

Writing 4096 bytes took 1491 milliseconds

那是在快速 SSD 上,主轴驱动器需要更长的时间。

于 2015-07-31T18:11:57.373 回答
1

这是我从目前提供的信息中的猜测:

在 Windows 上,当您写入扩展文件大小的位置时,Windows 需要将其之前的所有内容初始化为零。这可以防止旧磁盘数据泄漏,这将是一个安全问题。

可能,除了您的第一个线程之外,所有线程都需要将这么多的数据归零,以至于下载超时。这不再是真正的流式传输,因为第一次写入需要很长时间。

如果您有 LPIM 权限,您可以避免零初始化。否则,出于安全原因,您不能。免费下载管理器显示一条消息,它在每次下载开始时开始零初始化。

于 2015-07-31T17:57:41.737 回答
1

因此,在尝试了所有建议之后,我最终使用了 aMemoryMappedFile并打开了一个流以写入MemoryMappedFile每个线程:

int numberOfStreams = 4;
List<Tuple<int, int>> ranges = new List<Tuple<int, int>>();
string fileName = @"C:\MyCoolFile.txt";
//Ranges list populated here
using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileName, FileMode.OpenOrCreate, null, fileSize.Value, MemoryMappedFileAccess.ReadWrite))
{
    Parallel.For(0, numberOfStreams, index =>
    {
        try
        {
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("Some URL");
            using(Stream responseStream = webRequest.GetResponse().GetResponseStream())
            {
                using (MemoryMappedViewStream fileStream = mmf.CreateViewStream(ranges[index].Item1, ranges[index].Item2 - ranges[index].Item1 + 1, MemoryMappedFileAccess.Write))
                {
                    responseStream.CopyTo(fileStream);
                }
            };
        }
        catch (Exception e)
        {
            exception = e;
        }
    });
}
于 2015-08-05T18:54:35.143 回答
0

System.Net.Sockets.NetworkStream.Write

堆栈跟踪显示错误发生在写入服务器时。这是一个超时。这可能是因为

  1. 网络故障/过载
  2. 无响应的服务器。

这不是写入文件的问题。分析网络和服务器。也许服务器还没有准备好并发使用。

通过禁止写入文件来证明这一理论。错误应该仍然存在。

于 2015-07-31T19:49:08.067 回答