来自.Net 4.5的asynchronousHttpClient是密集加载应用程序的不好select吗?

我最近创build了一个简单的应用程序来testingHTTP调用吞吐量,这个吞吐量可以用一种经典的multithreading方法以asynchronous方式生成。

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

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

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(); } } } 

下面列出了multithreading实现中最重要的部分:

 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(); } } 

运行testing显示multithreading版本更快。 完成10k个请求需要大约0.6秒,而asynchronous完成相同的负载大约需要2秒。 这有点令人惊讶,因为我期待asynchronous的更快。 也许是因为我的HTTP调用非常快。 在现实世界的情况下,服务器应该执行更有意义的操作,并且还应该存在一些networking延迟,结果可能会被颠倒过来。

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

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

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

相比之下,生成HTTP调用的multithreading方法performance得非常可预测。 我花了大约0.6秒10k的消息,大约5.5万秒的100k消息,预计大约55秒100万消息。 没有消息失败。 此外,它运行时,从来没有使用超过55 MB的RAM(根据Windows任务pipe理器)。 asynchronous发送消息时使用的内存与负载成比例增长。 在200k邮件testing中,它使用了大约500 MB的RAM。

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

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

我设法获得更好的结果,内存和执行时间明智的,通过一个简单的,但原始的延迟机制限制asynchronous请求的最大数量:

 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线程池)时,通过限制并发线程数自动实现限制。

为了一个完整的概述,我还创build了一个基于HttpWebRequest而不是HttpClient的asynchronoustesting版本,并设法获得更好的结果。 首先,它允许设置并发连接的数量(使用ServicePointManager.DefaultConnectionLimit或通过configuration)的限制,这意味着它永远不会跑出端口,从不失败的任何请求(HttpClient,默认情况下,基于HttpWebRequest ,但似乎忽略了连接限制设置)。

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

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

更新(谢谢@Stephen Cleary)

事实certificate,HttpClient就像HttpWebRequest(默认情况下是HttpWebRequest)一样,可以在同一个主机上使用ServicePointManager.DefaultConnectionLimit来限制它的并发连接数。 奇怪的是,根据MSDN ,连接限制的默认值是2.我也检查了在我身边使用debugging器,指出确实2是默认值。 但是,似乎除非明确地将值设置为ServicePointManager.DefaultConnectionLimit,否则默认值将被忽略。 由于在我的HttpClienttesting中我没有明确地为它设置一个值,所以我认为它被忽略了。

将ServicePointManager.DefaultConnectionLimit设置为100后,HttpClient变得可靠和可预测(netstat确认只使用了100个端口)。 它仍然比asynchronousHttpWebRequest(约40%)慢,但奇怪的是,它使用较less的内存。 对于涉及一百万个请求的testing,最多使用550 MB,而asynchronousHttpWebRequest中使用1.6 GB。

所以,虽然组合ServicePointManager.DefaultConnectionLimit的HttpClient似乎可以确保可靠性(至less在所有调用都是针对同一个主机的情况下),但是由于缺乏适当的调节机制,它的性能仍然受到负面影响。 一些将请求的并发数量限制为可configuration值并将其余部分放在队列中的东西将使其更适合于高可伸缩性场景。

除了问题中提到的testing之外,我最近创build了一些涉及less得多的HTTP调用的新debugging(5000个与之前的100万个调用相比),但是执行时间要长得多(500毫秒,相比之前大约1毫秒)。 testing应用程序,同步multithreading(基于HttpWebRequest)和asynchronousI / O(基于HTTP客户端)产生了类似的结果:大约10秒钟执行使用大约3%的CPU和30 MB的内存。 两个testing者之间的唯一区别是multithreading使用了310个线程来执行,而asynchronous的只有22个。所以在一个将I / O绑定和CPU绑定操作相结合的应用程序中,asynchronous版本会产生更好的结果因为执行CPU操作的线程可能会有更多的CPU时间,这是实际需要的线程(等待I / O操作完成的线程只是浪费)。

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

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

所以,asynchronousHttpClient不是密集加载应用程序的瓶颈。 就其本质而言,它并不适合于非常快速的HTTP请求,而是适用于很长或很长的HTTP请求,特别是在只有有限数量的可用线程的应用程序中。 此外,通过ServicePointManager.DefaultConnectionLimit限制并发性是一个很好的做法,其值足够高,以确保良好的并行性,但又足够低以防止短暂的端口耗尽。 你可以在这里find关于这个问题的testing和结论的更多细节。

有一件事要考虑,可能会影响你的结果是,与HttpWebRequest你没有得到的ResponseStream和使用该stream。 使用HttpClient,默认情况下,它会将networkingstream复制到内存stream中。 为了使用HttpClient的方式,你正在使用HttpWebRquest你需要做的

 var requestMessage = new HttpRequestMessage() {RequestUri = URL}; Task<HttpResponseMessage> getTask = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); 

另一件事是,我不确定真正的差异,从线程的angular度来看,你实际上正在testing。 如果你深入到HttpClientHandler的深处,它只需执行Task.Factory.StartNew就可以执行一个asynchronous请求。 线程行为被委托给同步上下文,就像你的HttpWebRequest例子完成一样。

毫无疑问,HttpClient添加一些开销,因为它默认使用HttpWebRequest作为它的传输库。 所以,当使用HttpClientHandler的时候,你总是可以直接用HttpWebRequest获得更好的性能。 HttpClient带来的好处是像HttpResponseMessage,HttpRequestMessage,HttpContent和所有强types的标头。 本身并不是一个性能优化。

虽然这不直接回答OP问题的“asynchronous”部分,但是这解决了他正在使用的实现中的错误。

如果您希望扩展应用程序,请避免使用基于实例的HttpClient。 差异是巨大的! 根据负载的不同,您将看到非常不同的性能数字。 HttpClient被devise为跨请求重用。 BCL团队的编写人员证实了这一点。

我最近的一个项目是帮助一个非常大的知名在线电脑零售商扩大黑色星期五/假日交通的一些新系统。 我们遇到了一些使用HttpClient的性能问题。 由于它实现了IDisposable ,因此开发人员通过创build一个实例并将其放在using()语句中,来完成通常要做的事情。 一旦我们开始加载testing的应用程序带来了服务器的膝盖 – 是的,服务器不只是应用程序。 原因是HttpClient的每个实例都会在服务器上打开一个I / O完成端口。 由于GC的非确定性终结以及您正在使用跨越多个OSI层的计算机资源的事实,closuresnetworking端口可能需要一段时间。 实际上Windows操作系统本身可能需要长达20秒才能closures一个端口(每个微软)。 我们打开端口的速度比它们可能被closures的速度快 – 服务器端口耗尽,导致CPU达到100%。 我的修复是将HttpClient更改为解决问题的静态实例。 是的,这是可以随意使用的资源,但任何开销都远远超过了性能的差异。 我鼓励你做一些负载testing,看看你的应用程序的行为。

也在下面的链接回答:

在WebAPI客户端中每次调用创build一个新的HttpClient的开销是多less?

https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client