对于密集负载应用程序来说,.Net 4.5 中的 async HttpClient 是一个糟糕的选择吗?

2024-03-10

我最近创建了一个简单的应用程序,用于测试可以以异步方式生成的 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();
    }
}

运行测试表明多线程版本速度更快。完成 10k 请求大约需要 0.6 秒,而对于相同的负载量,异步请求大约需要 2 秒才能完成。这有点令人惊讶,因为我预计异步速度会更快。也许是因为我的 HTTP 调用速度非常快。在现实场景中,服务器应该执行更有意义的操作,并且还应该存在一些网络延迟,结果可能会相反。

然而,真正让我担心的是 HttpClient 在负载增加时的行为方式。由于传送 10k 条消息大约需要 2 秒,因此我认为传送 10 倍数量的消息需要大约 20 秒,但运行测试表明传送 100k 消息大约需要 50 秒。此外,传递 200k 条消息通常需要超过 2 分钟的时间,并且通常有几千条(3-4k)条消息会失败,但会出现以下异常:

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

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

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

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

第二是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);
    }
}

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

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

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

在这个冗长的演示之后,我的问题是:.Net 4.5 中的 HttpClient 类对于密集负载应用程序来说是一个糟糕的选择吗?有什么办法可以限制它,从而解决我提到的问题吗? HttpWebRequest 的异步风格怎么样?

更新(感谢@Stephen Cleary)

事实证明,HttpClient 就像 HttpWebRequest(默认情况下基于它)一样,可以通过 ServicePointManager.DefaultConnectionLimit 限制同一主机上的并发连接数。奇怪的是,根据MSDN http://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.defaultconnectionlimit.aspx,连接限制的默认值为 2。我还使用调试器检查了这一点,它指出 2 确实是默认值。但是,似乎除非明确为 ServicePointManager.DefaultConnectionLimit 设置值,否则默认值将被忽略。由于我在 HttpClient 测试期间没有明确设置它的值,因此我认为它被忽略了。

将 ServicePointManager.DefaultConnectionLimit 设置为 100 HttpClient 变得可靠且可预测(netstat 确认仅使用 100 个端口)。它仍然比异步 HttpWebRequest 慢(大约 40%),但奇怪的是,它使用的内存更少。对于涉及 100 万个请求的测试,它最多使用 550 MB,而异步 HttpWebRequest 则使用 1.6 GB。

因此,虽然 HttpClient 与 ServicePointManager.DefaultConnectionLimit 的组合似乎可以确保可靠性(至少对于所有调用都针对同一主机的情况),但它的性能看起来仍然因缺乏适当的限制机制而受到负面影响。将并发请求数限制为可配置值并将其余请求放入队列中的方法将使其更适合高可扩展性场景。


除了问题中提到的测试之外,我最近还创建了一些新的测试,涉及更少的 HTTP 调用(5000 次与之前的 100 万次相比),但执行时间更长的请求(500 毫秒与之前的约 1 毫秒相比)。两种测试应用程序(同步多线程应用程序(基于 HttpWebRequest)和异步 I/O 应用程序(基于 HTTP 客户端))都产生了相似的结果:使用大约 3% 的 CPU 和 30 MB 内存执行大约 10 秒。两个测试器之间的唯一区别是多线程测试器使用 310 个线程来执行,而异步测试器仅使用 22 个线程。因此,在结合了 I/O 绑定和 CPU 绑定操作的应用程序中,异步版本会产生更好的结果因为会有更多的 CPU 时间可用于执行 CPU 操作的线程,而这些线程才是真正需要它的线程(等待 I/O 操作完成的线程只是浪费)。

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

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

因此,async HttpClient 对于密集负载应用程序来说并不是瓶颈。只是从本质上来说,它不太适合非常快的 HTTP 请求,相反,它非常适合长请求或可能长请求,特别是在只有有限数量可用线程的应用程序内。此外,最好通过 ServicePointManager.DefaultConnectionLimit 限制并发性,其值足够高以确保良好的并行性,但又足够低以防止临时端口耗尽。您可以找到有关此问题的测试和结论的更多详细信息here http://www.ducons.com/blog/tests-and-thoughts-on-asynchronous-io-vs-multithreading.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

对于密集负载应用程序来说,.Net 4.5 中的 async HttpClient 是一个糟糕的选择吗? 的相关文章

  • 在c中用以下结构填充矩阵

    我有以下结构 typedef struct arr integer int size int arr arr arr integer arr arr integer alloc arr integer int len arr arr int
  • 即使定义了其他主键,实体框架 6 也会创建 Id 列

    我将 DataObject 定义为 public class SensorType EntityData PKs public string CompanyId get set public string ServiceId get set
  • 除了第一列之外,Gridview 行可点击?

    我使用以下代码使 gridview 的整行可单击 protected void gridMSDS RowDataBound object sender GridViewRowEventArgs e if e Row RowType Data
  • 模拟 EF core dbcontext 和 dbset

    我正在使用 ASP NET Core 2 2 EF Core 和 MOQ 当我运行测试时 我收到此错误 消息 System NotSupportedException 非虚拟 可在 VB 中重写 成员上的设置无效 x gt x Movies
  • 使用 std::string 导致 Windows“找不到入口点”[重复]

    这个问题在这里已经有答案了 当我用 G C C 编译它时 include
  • 如何使用 ASP.NET MVC 4.0 DonutOutputCache VaryByCustom 使缓存失效

    我正在为我的 ASP NET 应用程序使用 DevTrends MvcDonutCaching 包 它工作得很好 我目前遇到的一个问题是使我为子操作设置的 VaryByCustom 缓存无效 这是我用于 VaryByCustom 设置的一些
  • 使用 microsoft word.interop 删除 Word 文档中的空白页

    我创建了一个Word文档 它使用以下命令生成动态内容词互操作 它有一些分页符之间使用 我面临的问题是 此分页符会创建我不想向用户显示的空白页面 在某些情况下 我需要在那里添加这些分页符以维护页面布局 因此我无法考虑删除这些分页符 但我想要的
  • C# 列表框 ObservableCollection

    我正在尝试使用 ListBox DataSource ObservableCollection 但是我不知道如何在 OC 更新时让列表框自动更新 我可以在 OC 上挂接 CollectionChanged 事件 但是我需要对列表框执行什么操
  • 当应用程序未聚焦时监听按键

    我有一个应用程序 C 4 0 WPF 它是隐藏的 可以通过单击系统托盘图标或我创建的其他框架 停靠在左侧和最上面的小框架 来显示 My customer wants to add a new way to display the appli
  • 那里有更好的 DateTime.Parse 吗? [关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 有谁知道有一个库 付费或免费 能够处理比 DateTime Parse 使用的更常见的日期时间格式 能够
  • 在 C# 4.0 中,是否可以从泛型类型参数派生类?

    我一直在尝试这个 但我似乎无法弄清楚 我想做这个 public abstract class SingletonType
  • CS0246 找不到类型或命名空间名称“ErrorViewModel”(您是否缺少 using 指令或程序集引用?)

    我收到 CS0246 错误代码 我正在做一个 MVC net core 项目 我正在将 Razor 合并到我的 C 代码中 我在进行构建时收到此错误 我在最后一行收到错误 有人能帮我解决这个问题吗 global Microsoft AspN
  • XPath 选择具有特定属性值的元素?

    我在使用 XPath 选择节点时遇到问题 我将展示一个示例 由于实际数据量很大 xml 文件被缩短了 这是 XML 的子集
  • C++ 静态工厂构造函数

    我正在进行模拟 它需要创建多个相当相似的模型 我的想法是有一个名为 Model 的类并使用静态工厂方法来构造模型 例如 模型 createTriangle or 模型 createFromFile 我从以前的 java 代码中汲取了这个想法
  • 如何定义 Swagger UI 参数的默认值?

    我已将 Swagger Swashbuckle 集成到 NET Core 2 2 API 项目中 一切都很好 我的要求纯粹是为了方便 考虑以下 API 方法 public Model SomeEstimate SomeRequest req
  • 正则表达式基于组的不同替换?

    所以我对正则表达式比较陌生 并且做了一些练习 我正在玩一个简单的 混淆器 它只是寻找 dot or dot or at or at 不区分大小写 并且在匹配项之前或之后有或没有任意数量的空格 这是针对通常情况的 someemail AT d
  • STL 向量、迭代器和插入 (C++)

    我有一个将向量的迭代器传递到的方法 在这个方法中 我想向向量中添加一些元素 但我不确定当只有迭代器时这是否可行 void GUIComponentText AddAttributes vector
  • 为 C++ 类播种 rand()

    我正在开发一个 C 类 它使用rand 在构造函数中 我真的希望这个班级在几乎所有方面都能照顾好自己 但我不知道在哪里播种rand 如果我播种rand 在构造函数中 每次构造我的对象类型的新实例时都会对其进行播种 因此 如果我按顺序创建 3
  • Python 中的 C 指针算术

    我正在尝试将一个简单的 C 程序转换为 Python 但由于我对 C 和 Python 都一无所知 这对我来说很困难 我被 C 指针困住了 有一个函数采用 unsigned long int 指针并将其值添加到 while 循环中的某些变量
  • 访问 Visual Studio 扩展中的当前代码窗格

    我正在编写一个 Visual Studio 2010 扩展 在代码视图中带有右键单击菜单 我希望能够从菜单项事件处理程序检查当前代码 但无法在对象模型中找到执行此操作的位置 如何在 Visual Studio 扩展中访问当前窗口中的代码 E

随机推荐