C# 对于高负载应用程序,来自.Net 4.5的异步HttpClient是一个错误的选择吗?

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

我最近创建了一个简单的应用程序,用于测试HTTP调用吞吐量,与传统的多线程方法相比,它可以以异步方式生成

应用程序能够执行预定义数量的HTTP调用,并在最后显示执行这些调用所需的总时间。在我的测试过程中,所有HTTP调用都是对本地IIS服务器进行的,它们检索到一个小文本文件(大小为12字节)

异步实现代码中最重要的部分如下所示:

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()
{