C# 对于高负载应用程序,来自.Net 4.5的异步HttpClient是一个错误的选择吗?
我最近创建了一个简单的应用程序,用于测试HTTP调用吞吐量,与传统的多线程方法相比,它可以以异步方式生成 应用程序能够执行预定义数量的HTTP调用,并在最后显示执行这些调用所需的总时间。在我的测试过程中,所有HTTP调用都是对本地IIS服务器进行的,它们检索到一个小文本文件(大小为12字节) 异步实现代码中最重要的部分如下所示:C# 对于高负载应用程序,来自.Net 4.5的异步HttpClient是一个错误的选择吗?,c#,asynchronous,.net-4.5,async-await,dotnet-httpclient,C#,Asynchronous,.net 4.5,Async Await,Dotnet Httpclient,我最近创建了一个简单的应用程序,用于测试HTTP调用吞吐量,与传统的多线程方法相比,它可以以异步方式生成 应用程序能够执行预定义数量的HTTP调用,并在最后显示执行这些调用所需的总时间。在我的测试过程中,所有HTTP调用都是对本地IIS服务器进行的,它们检索到一个小文本文件(大小为12字节) 异步实现代码中最重要的部分如下所示: public async void TestAsync() { this.TestInit(); HttpClient httpClient = new
public async void TestAsync()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
ProcessUrlAsync(httpClient);
}
}
private async void ProcessUrlAsync(HttpClient httpClient)
{
HttpResponseMessage httpResponse = null;
try
{
Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
httpResponse = await getTask;
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
finally
{
if(httpResponse != null) httpResponse.Dispose();
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
}
public void TestParallel2()
{
this.TestInit();
ServicePointManager.DefaultConnectionLimit = 100;
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
Task.Run(() =>
{
try
{
this.PerformWebRequestGet();
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
});
}
}
private void PerformWebRequestGet()
{
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
request = (HttpWebRequest)WebRequest.Create(URL);
request.Method = "GET";
request.KeepAlive = true;
response = (HttpWebResponse)request.GetResponse();
}
finally
{
if (response != null) response.Close();
}
}
public async void TestAsync()
{
this.TestInit();
HttpClient HttpClient=新HttpClient();
for(int i=0;i<请求数;i++)
{
ProcessUrlAsync(httpClient);
}
}
专用异步void ProcessUrlAsync(HttpClient HttpClient)
{
httpResponse消息httpResponse=null;
尝试
{
任务getTask=httpClient.GetAsync(URL);
httpResponse=等待getTask;
联锁增量(参考成功调用);
}
捕获(例外情况除外)
{
联锁增量(参考失败调用);
}
最后
{
if(httpResponse!=null)httpResponse.Dispose();
}
锁(同步锁)
{
_itemsLeft--;
如果(_itemsLeft==0)
{
_utcEndTime=DateTime.UtcNow;
这是DisplayTestResults();
}
}
}
多线程实现的最重要部分如下所示:
public async void TestAsync()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
ProcessUrlAsync(httpClient);
}
}
private async void ProcessUrlAsync(HttpClient httpClient)
{
HttpResponseMessage httpResponse = null;
try
{
Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
httpResponse = await getTask;
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
finally
{
if(httpResponse != null) httpResponse.Dispose();
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
}
public void TestParallel2()
{
this.TestInit();
ServicePointManager.DefaultConnectionLimit = 100;
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
Task.Run(() =>
{
try
{
this.PerformWebRequestGet();
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
});
}
}
private void PerformWebRequestGet()
{
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
request = (HttpWebRequest)WebRequest.Create(URL);
request.Method = "GET";
request.KeepAlive = true;
response = (HttpWebResponse)request.GetResponse();
}
finally
{
if (response != null) response.Close();
}
}
public void TestParallel2()
{
this.TestInit();
ServicePointManager.DefaultConnectionLimit=100;
for(int i=0;i<请求数;i++)
{
Task.Run(()=>
{
尝试
{
这个。PerformWebRequestGet();
联锁增量(参考成功调用);
}
捕获(例外情况除外)
{
联锁增量(参考失败调用);
}
锁(同步锁)
{
_itemsLeft--;
如果(_itemsLeft==0)
{
_utcEndTime=DateTime.UtcNow;
这是DisplayTestResults();
}
}
});
}
}
私有void PerformWebRequestGet()
{
HttpWebRequest请求=null;
HttpWebResponse响应=null;
尝试
{
request=(HttpWebRequest)WebRequest.Create(URL);
request.Method=“GET”;
request.KeepAlive=true;
response=(HttpWebResponse)request.GetResponse();
}
最后
{
if(response!=null)response.Close();
}
}
运行测试表明多线程版本更快。对于10k请求,大约需要0.6秒才能完成,而对于相同的负载量,异步请求大约需要2秒才能完成。这有点令人惊讶,因为我希望异步的速度更快。可能是因为我的HTTP调用非常快。在真实场景中,服务器应该执行更有意义的操作,并且还应该存在一些网络延迟,结果可能会相反
然而,我真正关心的是HttpClient在负载增加时的行为方式。由于发送10万条消息大约需要2秒,因此我认为发送10倍的消息大约需要20秒,但运行测试表明,发送10万条消息大约需要50秒。此外,通常需要2分钟以上的时间才能发送20万条消息,并且通常数千条(3-4k)消息会失败,但以下情况除外:
无法对套接字执行操作,因为系统缺少足够的缓冲区空间或队列已满
我检查了IIS日志,失败的操作从未到达服务器。他们在客户端中失败了。我在Windows7机器上运行了测试,默认临时端口范围为49152到65535。运行netstat表明测试期间使用了大约5-6k端口,因此理论上应该有更多可用端口。如果缺少端口确实是导致异常的原因,那么这意味着netstat没有正确地报告这种情况,或者HttClient只使用了最大数量的端口,然后开始抛出异常
相比之下,生成HTTP调用的多线程方法表现得非常可预测。我用了大约0.6秒来处理10k条消息,用了大约5.5秒来处理100k条消息,正如预期的那样,用55秒来处理一百万条消息。没有一条消息失败。此外,在运行时,它从未使用超过55MB的RAM(根据Windows任务管理器)。异步发送消息时使用的内存随着负载成比例增长。在200k消息测试期间,它使用了大约500MB的RAM
我认为上述结果有两个主要原因。首先,HttpClient似乎非常贪婪地创建与服务器的新连接。netstat报告的大量已用端口意味着它可能不会从HTTP保持活动中获得太多好处
第二个原因是HttpClient似乎没有节流机制。事实上,这似乎是与异步操作相关的一个普遍问题。如果您需要执行大量操作,它们将立即启动,然后在可用时继续执行。理论上,这应该是可以的,因为在异步操作中,负载在外部系统上,但如上所述,情况并非完全如此。一次启动大量请求将增加内存使用并降低整个执行速度
通过使用简单但原始的延迟机制限制异步请求的最大数量,我在内存和执行时间方面获得了更好的结果:
public async void TestAsyncWithDelay()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
if (_activeRequestsCount >= MAX_CONCURENT_REQUESTS)
await Task.Delay(DELAY_TIME);
ProcessUrlAsyncWithReqCount(httpClient);
}
}
public异步void testasynchwithdelay()
{