首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >来自.NET 4.5的异步HttpClient是密集加载应用程序的不好选择?

来自.NET 4.5的异步HttpClient是密集加载应用程序的不好选择?
EN

Stack Overflow用户
提问于 2018-03-06 00:01:15
回答 2查看 0关注 0票数 0

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

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

下面列出了异步实现代码中最重要的部分:

代码语言:txt
复制
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();
        }
    }
}

多线程实现的最重要部分如下:

代码语言:txt
复制
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();
    }
}

运行测试显示多线程版本更快。大约需要0.6秒才能完成10k个请求,而异步方式需要大约2秒才能完成相同数量的加载。这有点令人惊讶,因为我期待异步程序更快。也许这是因为我的HTTP调用非常快。在现实世界的场景中,服务器应该执行更有意义的操作,并且还应该存在一些网络延迟,结果可能会颠倒过来。

然而,真正令我担忧的是当负载增加时HttpClient的行为方式。由于大约需要2秒才能发送10k条消息,我认为需要大约20秒才能发送10倍的消息数,但运行测试表明,它需要大约50秒才能发送100k条消息。此外,通常需要2分钟才能发送200k条消息,并且通常会有数千条消息(3-4k)失败,并有以下例外情况:

由于系统缺少足够的缓冲空间或队列已满,因此无法执行套接字上的操作。

我检查了IIS日志和失败的操作从未到达服务器。他们在客户端失败了。我在Windows 7计算机上运行了测试,默认范围是临时端口49152到65535.运行netstat显示测试中使用了大约5-6k个端口,理论上应该有更多的端口可用。如果缺少端口确实是异常的原因,则意味着netstat没有正确报告情况,或者HttClient只使用最大数量的端口,然后开始抛出异常。

相比之下,生成HTTP调用的多线程方法表现得非常可预测。我为10k消息花了大约0.6秒,对于100k消息花了大约5.5秒,预计大约55秒花了100万消息。没有消息失败。此外,它运行时,从未使用超过55 MB的RAM(根据Windows任务管理器)。异步发送消息时使用的内存与负载成比例增长。在200k消息测试中,它使用了大约500 MB的RAM。

我认为上述结果有两个主要原因。第一个是HttpClient在与服务器建立新连接时似乎非常贪婪。netstat报告的大量使用端口意味着它可能不会从HTTP keep-alive中获益太多。

第二个是HttpClient似乎没有限制机制。事实上,这似乎是与异步操作相关的一般问题。如果您需要执行大量操作,它们将立即启动,然后在可用时继续执行。从理论上讲,这应该是可以的,因为在异步操作中,负载在外部系统上,但正如上面证明的那样,情况并非完全如此。同时启动大量请求会增加内存使用量并减慢整个执行速度。

我设法通过一个简单但基本的延迟机制来限制异步请求的最大数量,从而获得更好的结果,内存和执行时间。

代码语言:txt
复制
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);
    }
}

如果HttpClient包含限制并发请求数的机制,那将非常有用。使用Task类(基于.Net线程池)时,限制是通过限制并发线程数自动实现的。

对于一个完整的概述,我还创建了一个基于HttpWebRequest而不是HttpClient的异步测试版本,并设法获得更好的结果。首先,它允许设置并发连接数量的限制(使用ServicePointManager.DefaultConnectionLimit或通过config),这意味着它永远不会耗尽端口,并且永远不会失败(HttpClient默认基于HttpWebRequest ,但它似乎忽略了连接限制设置)。

异步HttpWebRequest方法仍然比多线程慢大约50-60%,但它是可预测和可靠的。唯一的缺点是它在大负载下使用了大量的内存。例如,它需要大约1.6 GB用于发送100万个请求。通过限制并发请求的数量(就像我上面为HttpClient所做的那样),我设法将使用的内存减少到20 MB,并且获得的执行时间仅比多线程方法慢10%。

经过这么长时间的演示之后,我的问题是:.Net 4.5的HttpClient类是密集加载应用程序的不好选择吗?有没有什么办法来遏制它,这应该解决我提到的问题?HttpWebRequest的异步风格如何?

EN

回答 2

Stack Overflow用户

发布于 2018-03-06 08:38:25

除了问题中提到的测试之外,我最近还创建了一些涉及少量HTTP调用的新调试(5000与前一百万次相比),但是执行时间要长得多(500毫秒与之前大约1毫秒相比)。两个测试应用程序,同步多线程(基于HttpWebRequest)和异步I / O(基于HTTP客户端)产生了类似的结果:大约需要10秒钟使用大约3%的CPU和30 MB的内存。两位测试人员唯一的区别是多线程使用310个线程执行,而异步执行只有22个。

作为我的测试的结论,在处理非常快速的请求时,异步HTTP调用并不是最好的选择。其原因在于,当运行包含异步I / O调用的任务时,任务启动的线程将在异步调用完成时立即退出,并将任务的其余部分注册为回调。然后,当I / O操作完成时,回调将排队等待在第一个可用线程上执行。所有这些都会产生开销,这使得快速I / O操作在启动它们的线程上执行时效率更高。

异步HTTP调用在处理长或可能长的I / O操作时是一个不错的选择,因为它不会使任何线程忙于等待I / O操作完成。这减少了应用程序使用的线程总数,从而允许CPU绑定操作花费更多的CPU时间。此外,在仅分配有限数量的线程的应用程序上(例如Web应用程序就是如此),异步I / O可以防止线程池线程耗尽,这可能会在同步执行I / O调用时发生。

因此,异步HttpClient不是密集加载应用程序的瓶颈。就其本质而言,它不太适合于非常快速的HTTP请求,相反它适用于很长或很长的HTTP请求,尤其是内部只有有限数量的可用线程的应用程序。此外,通过ServicePointManager.DefaultConnectionLimit限制并发性是一个很好的做法,其值足够高以确保良好的并行性水平,但又足够低以防止短暂的端口耗尽。你可以找到提出了这个问题,测试和结论,更多的细节在这里

票数 0
EN

Stack Overflow用户

发布于 2018-03-06 09:55:33

有一件事要考虑,可能会影响你的结果,就是HttpWebRequest你没有得到ResponseStream并且使用那个流。使用HttpClient,默认情况下它会将网络流复制到内存流中。为了使用HttpClient的方式与您当前使用HttpWebRquest相同,需要这样做

代码语言:javascript
复制
var requestMessage = new HttpRequestMessage() {RequestUri = URL};
Task<HttpResponseMessage> getTask = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);

另一件事是,我不确定真正的差异,从线程的角度来看,你实际上正在测试。如果您深入了解HttpClientHandler的深处,它只需执行Task.Factory.StartNew即可执行异步请求。线程行为以完全相同的方式委托给同步上下文,就像使用HttpWebRequest示例完成的示例一样。

毫无疑问,HttpClient添加了一些开销,因为它默认使用HttpWebRequest作为它的传输库。因此,使用HttpClientHandler时,将始终能够直接使用HttpWebRequest获得更好的性能。HttpClient带来的好处是可以使用像HttpResponseMessage,HttpRequestMessage,HttpContent和所有强类型标题的标准类。它本身并不是一个性能优化。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/-100004200

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档