我有一个webservice,可以同时接收多个请求。对于每个请求,我需要调用另一个Web服务(身份验证)。问题是,如果多个(> 20)请求同时发生,响应时间突然变得更糟。
我做了一个样本来证明问题:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace CallTest
{
public class Program
{
private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false });
static void Main(string[] args)
{
ServicePointManager.DefaultConnectionLimit = 100;
ServicePointManager.Expect100Continue = false;
// warmup
CallSomeWebsite().GetAwaiter().GetResult();
CallSomeWebsite().GetAwaiter().GetResult();
RunSequentiell().GetAwaiter().GetResult();
RunParallel().GetAwaiter().GetResult();
}
private static async Task RunParallel()
{
var tasks = new List<Task>();
for (var i = 0; i < 300; i++)
{
tasks.Add(CallSomeWebsite());
}
await Task.WhenAll(tasks);
}
private static async Task RunSequentiell()
{
var tasks = new List<Task>();
for (var i = 0; i < 300; i++)
{
await CallSomeWebsite();
}
}
private static async Task CallSomeWebsite()
{
var watch = Stopwatch.StartNew();
using (var result = await _httpClient.GetAsync("http://example.com").ConfigureAwait(false))
{
// more work here, like checking success etc.
Console.WriteLine(watch.ElapsedMilliseconds);
}
}
}
}
顺序通话没问题。它们需要几毫秒才能完成,响应时间大致相同。
但是,并行请求开始越来越长,发送的请求越多。有时甚至需要几秒钟。我在.NET Framework 4.6.1和.NET Core 2.0上测试了它,结果相同。
甚至更奇怪:我使用WireShark跟踪HTTP请求,并且它们总是在同一时间内进行。 但示例程序报告的并行请求值远高于WireShark。
如何为并行请求获得相同的性能?这是一个线程池问题吗?
答案 0 :(得分:1)
在问题的RunParallel()
功能中,在程序运行的第一秒中为所有300个呼叫启动秒表,并在每个http请求完成时结束。
因此,这些时间无法与连续迭代进行比较。
对于较少数量的并行任务,例如50,如果你测量顺序和并行方法所花费的时间,你应该发现并行方法更快,因为它可以尽可能多地GetAsync
任务。
也就是说,当运行300次迭代的代码时,我确实在调试器外部运行时发现了可重复的几秒钟停顿:
在调试器中调试构建:顺序27.6秒,并行0.6秒
调试版本,无需调试器:顺序26.8秒,并行3.2秒
[编辑]
有一个类似的情景描述in this question,它可能与你的问题无关。
这个问题越多,任务运行越多,并在以下情况下消失:
GetAsync
工作等效延迟 watch.ElapsedMilliseconds
诊断停止所有连接,表明所有连接都受到限制的影响。
在主机或网络中似乎是某种(反syn-flood?)限制,一旦一定数量的套接字开始连接就会暂停数据包流。
答案 1 :(得分:1)
听起来无论出于什么原因,你都会在大约20个并发任务中达到收益递减点。所以,你最好的选择可能是扼杀你的并行性。 TPL Dataflow是实现这一目标的绝佳库。要按照您的模式,添加如下方法:
private static Task RunParallelThrottled()
{
var throtter = new ActionBlock<int>(i => CallSomeWebsite(),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 20 });
for (var i = 0; i < 300; i++)
{
throttler.Post(i);
}
throttler.Complete();
return throttler.Completion;
}
您可能需要尝试MaxDegreeOfParallelism
,直到找到最佳位置。请注意,这比批量20更有效。在该方案中,批次中的所有20个都需要在下一批次开始之前完成。使用TPL Dataflow,一旦完成,就允许另一个开始。
答案 2 :(得分:1)
.NET Core 2.1已修复此问题。我认为问题是HttpClient使用的基础Windows WinHTTP处理程序。
在.NET Core 2.1中,他们重写了HttpClientHandler(请参见https://blogs.msdn.microsoft.com/dotnet/2018/04/18/performance-improvements-in-net-core-2-1/#user-content-networking):
在.NET Core 2.1中,HttpClientHandler具有一个新的默认实现,完全从头开始在其他System.Net库的C#顶部完全由C#实现。 System.Net.Sockets,System.Net.Security等。它不仅解决了上述行为问题,而且还显着提高了性能(该实现也公开为SocketsHttpHandler公开,可以直接使用,而不必通过HttpClientHandler来使用。以配置特定于SocketsHttpHandler的属性。
事实证明,这消除了问题中提到的瓶颈。
在.NET Core 2.0上,我得到以下数字(以毫秒为单位):
Fetching URL 500 times...
Sequentiell Total: 4209, Max: 35, Min: 6, Avg: 8.418
Parallel Total: 822, Max: 338, Min: 7, Avg: 69.126
但是在.NET Core 2.1上,单个并行HTTP请求似乎已经改善了很多:
Fetching URL 500 times...
Sequentiell Total: 4020, Max: 40, Min: 6, Avg: 8.040
Parallel Total: 795, Max: 76, Min: 5, Avg: 7.972
答案 3 :(得分:0)
您遇到问题的原因是.NET
未按照等待的顺序恢复Tasks
,等待Task
仅在调用函数无法恢复执行时才会恢复,Task
不适用于Parallel
执行。
如果您进行了一些修改,以便将i
传递给CallSomeWebsite
函数并在将所有任务添加到列表后调用Console.WriteLine("All loaded");
,您将得到类似的内容:( RequestNumber:Time)
All loaded
0: 164
199: 236
299: 312
12: 813
1: 837
9: 870
15: 888
17: 905
5: 912
10: 952
13: 952
16: 961
18: 976
19: 993
3: 1061
2: 1061
在任何时间打印到屏幕之前,您是否注意到每个Task
的创建方式?创建Task
的整个循环在等待网络调用之后的任何Task
恢复执行之前完成。
另外,请看请求1在请求1之前是如何完成的? .NET
将按其认为最佳的顺序恢复Task
(这可以保证更复杂,但我不确定.NET
如何决定继续Task
我认为您可能会感到困惑的一件事是Asynchronous
和Parallel
。它们不相同,Task
用于Asynchronous
执行。这意味着所有这些任务都在同一个线程上运行(可能。.NET
可以根据需要为任务启动新线程),因此它们不会在Parallel
中运行。如果它们是真正的Parallel
,它们都将在不同的线程中运行,并且每次执行的执行时间都不会增加。
更新的功能:
private static async Task RunParallel()
{
var tasks = new List<Task>();
for (var i = 0; i < 300; i++)
{
tasks.Add(CallSomeWebsite(i));
}
Console.WriteLine("All loaded");
await Task.WhenAll(tasks);
}
private static async Task CallSomeWebsite(int i)
{
var watch = Stopwatch.StartNew();
using (var result = await _httpClient.GetAsync("https://www.google.com").ConfigureAwait(false))
{
// more work here, like checking success etc.
Console.WriteLine($"{i}: {watch.ElapsedMilliseconds}");
}
}
至于Asynchronous
执行Synchronous
执行时打印时间较长的原因,您当前的跟踪时间方法没有考虑执行暂停和执行之间花费的时间。延续。这就是为什么所有报告执行时间都超过完成的请求集的原因。如果您想要准确的时间,您需要找到一种方法来减去await
发生和执行继续之间所花费的时间。问题不在于需要更长的时间,而是您的报告方法不准确。如果您将所有Synchronous
次来电的时间相加,那么它实际上远远超过Asynchronous
来电的最长时间:
Sync: 27965
Max Async: 2341