【问题标题】:How can I spawn huge number simultaneous HTTP HEAD requests in C#?如何在 C# 中同时生成大量 HTTP HEAD 请求?
【发布时间】:2019-04-26 14:48:57
【问题描述】:

我们有一个基于查询字符串进行图像处理然后呈现结果的服务器。结果也会被缓存 90 天。由于复杂性,一些操作可能需要 6-7 秒。

我们列出了我们的一些产品的市场最近减少了它们在获取图像时的超时时间到一个较低的值,导致任何给定 Feed 中的大多数商品由于(他们的错误消息)“图像超时”而第一次失败。当我们重新提交提要时,就没有这样的问题了,因为我们的图像服务器现在已经缓存了图像。

不要建议要求市场更改他们的超时时间。他们非常不灵活和不合作。另外,请不要建议使用更强大的图像服务器。它实际上是一个巨大的农场,不受我的团队控制。

这让我只有一个选择。在将提要发送到市场之前,我需要“准备好缓存”。问题是一个提要最多可以包含 5000 个项目,每个项目至少有 2 张图像。这意味着 10,000 张图片。

我正在使用HEAD 调用,因为我们不需要返回给我们的图像。我曾尝试在.Net Framework 中使用WebRequest 甚至Socket,在异步Task 中调用(使用Task.Run()`),但CLR 一次只能启动大约20 个任务。由于平均而言,每张图像大约需要 4 秒(有些长达 6-7 秒,有些只需要 1 秒),因此您需要 10,000 / 20 = 500 * 4 秒 = 2000 秒 = 33 1/3 分钟,这不是在我们发送提要之前,我们可以等待。

由于我们实际上并不需要来自图像服务器的回复,因此我尝试对图像服务器使用异步请求,并在创纪录的时间内通过foreach,但我发现,使用该异步请求我不能保证调用会在启动所有任务的代码完成时触发,所以这无济于事。

我们使用 AWS,所以我考虑使用 Lambda,但这会增加额外的复杂性和费用,但那里的大规模并行能力听起来可以解决问题。

我该如何解决这个问题?

测试服务器

public class HomeController : Controller {
    private Random random;
    public HomeController() {
        random = new Random(DateTime.UtcNow.Millisecond);
    }
    public ActionResult Index(string url) {
        var wait = random.Next(1, 70);
        Thread.Sleep(wait * 100);
        return Content(wait + " : " + url);
    }
}

测试客户端

class Program {
    static void Main(string[] args) {
        var tasks = new List<Task>();
        for (var i = 0; i < 200; i++) {
            Console.WriteLine(i.ToString());
            var task = SendRequest("http://test.local.com/Home/Index?url=" + i);
            tasks.Add(task);
        }
        Task.WaitAll(tasks.ToArray());
    }
    private static async Task SendRequest(string url) {
        try {
            var myWebRequest = WebRequest.Create(url);
            myWebRequest.Method = "HEAD";
            var foo = await myWebRequest.GetResponseAsync();
            //var foo = myWebRequest.GetResponseAsync();
            //var foo = myWebRequest.GetResponse();
            foo.Dispose();
        }
        catch { }
    }
}

【问题讨论】:

  • 连接池限制可能吗? (适用于 WebRequest,但不适用于 Socket。)CLR 肯定很乐意启动更多任务。
  • 哇!乔恩斯基特。好吧,如果有人可以帮助我,我知道你可以。我跑了ThreadPool.GetMaxThreads,得到了 32K 和 1000,所以似乎有很多可用的工人和 IO 连接。如何让 CLR 使用它们?
  • 如果不知道您当前的代码是什么样的,很难说要更改什么。请提供minimal reproducible example。 (理想情况下也有一个小型 Web 服务器,只使用 HttpListener - 可以模拟延迟,并计算并发请求。)
  • 试试ThreadPool.SetMinThreads(100, 100); 看看是否有什么不同。
  • 从长远来看,图像服务器获得接受 URL 列表并缓存它们的功能可能会更有效。作为一项低优先级的工作,它可以以最小的开销完成它,并且希望足够快以满足您团队的需求。用大量并发请求轰炸服务器可能与 DOS 攻击具有相同的效果。 ??????

标签: c# multithreading webrequest


【解决方案1】:

我讨厌回答自己的问题,但我想分享一下我最终做了什么,以防其他人遇到同样的问题。基本上,我将调用图像服务的代码封装到它自己的微型可执行文件中,然后我使用Process.Start() 来运行可执行文件。我当然希望看到性能的提升,但我对我看到的提升幅度感到惊讶。提升大约是 20 倍,机器上的 CPU 使用率仅上升 20-40%,具体取决于我运行了多少并发批次以及批次有多大。

在下面的代码中,请记住我已经删除了try{}...catch{} 块以保持代码紧凑。

单独的可执行文件(项目名称是ImageCachePrimer

class Program {
    static void Main(string[] args) {
        var tasks = new List<Task>(args.Length);
        foreach (var url in args) {
            tasks.Add(Task.Run(async () => await SendRequest(url)));
        }
        Task.WaitAll(tasks.ToArray());
    }
    private static async Task SendRequest(string url) {
        var myWebRequest = WebRequest.Create(url);
        myWebRequest.Method = "HEAD";
        var foo = await myWebRequest.GetResponseAsync();
        foo.Dispose();
    }
}

调用可执行文件的方法。

private static Process CreateProcess(IEnumerable<string> urls)
{
    var args = urls.Aggregate("", (current, url) => current + url + " ");
    var start = new ProcessStartInfo();
    start.Arguments = args;
    start.FileName = "ImageCachePrimer.exe";
    start.WindowStyle = ProcessWindowStyle.Hidden;
    start.CreateNoWindow = false;
    start.UseShellExecute = true;
    return Process.Start(start);
}

调用上述方法的方法

private static void PrimeImageCache(IReadOnlyCollection<string> urls) {
    var distinctUrls = urls.Distinct().ToList();
    const int concurrentBatches = 20;
    const int batchSize = 15;
    var processes = new List<Process>(concurrentBatches);
    foreach (var batch in distinctUrls.FormIntoBatches(batchSize)) {
        processes.Add(CreateProcess(batch));
        while (processes.Count >= concurrentBatches) {
            Thread.Sleep(500);
            for (var i = 0; i < processes.Count; i++) {
                var process = processes[i];
                if (process.HasExited) {
                    processes.Remove(process);
                }
            }
        }
    }
    while (processes.Count > 0) {
        Thread.Sleep(500);
        for (var i = 0; i < processes.Count; i++) {
            var process = processes[i];
            if (process.HasExited) {
                processes.Remove(process);
            }
        }
    }
}

单独的可执行文件和调用它的方法非常简单。我想解释一下最终方法中的一些细微差别。首先,我最初尝试使用foreach(var process in processes){process.WaitForExit();},但这使得批处理中的每个进程都必须完成,然后我才能启动一个新进程。它还导致 CPU 飙升至 100%(我猜在内部它会执行一个近乎空的循环来查看进程是否完成)。因此,如第一个 while 循环中所示,我“自己动手”。 其次,我必须添加最后的while 循环,以确保在我将最后一批在上一个foreach() 中排队之后仍在运行的进程有机会完成。

希望这对其他人有所帮助。

【讨论】:

  • 这似乎相当可靠,尽管对我来说,寻求多进程解决方案似乎总是让步(因为增加了复杂性)。在理想的世界中,使用单个进程应该是可行的(并且性能相同)。
  • @TheodorZoulias 同意。我不愿意走这条路,但最后我找不到让单个进程产生足够多线程的方法。我很想看到其他人对此的解决方案,不涉及产生外部进程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-20
  • 1970-01-01
  • 2010-09-11
  • 2011-09-09
相关资源
最近更新 更多