如何在C#中产生大量同时发出的HTTP HEAD请求?

时间:2019-04-26 14:48:58

标签: c# multithreading webrequest

我们有一个服务器,该服务器根据查询字符串进行图像处理,然后呈现结果。结果也将缓存90天。由于复杂,某些操作可能需要6到7秒钟。

我们列出了一些产品的市场最近将获取图像时的超时降低到一个较低的值,这导致任何给定Feed中的大多数商品第一次由于(其错误消息)“图像超时”而失败。当我们重新提交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分钟,这不是我们可以接受的等待,然后再发送Feed。

由于我们实际上不需要来自图像服务器的答复,因此我尝试使用对图像服务器的异步请求,该请求在记录时间内通过了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 { }
    }
}

1 个答案:

答案 0 :(得分: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()中的最后一批排队之后,仍在运行的进程有机会完成。

希望这对其他人有帮助。