【问题标题】:HttpClient concurrent behavior different when running in Powershell than in Visual Studio在 Powershell 中运行时与在 Visual Studio 中运行时的 HttpClient 并发行为不同
【发布时间】:2020-06-27 16:13:27
【问题描述】:

我正在使用 MS Graph API 将数百万用户从本地 AD 迁移到 Azure AD B2C,以在 B2C 中创建用户。我编写了一个 .Net Core 3.1 控制台应用程序来执行此迁移。为了加快速度,我正在对 Graph API 进行并发调用。这工作得很好 - 有点。

在开发过程中,我从 Visual Studio 2019 运行时体验到了可接受的性能,但对于测试,我是从 Powershell 7 中的命令行运行的。从 Powershell 并发调用 HttpClient 的性能非常糟糕。从 Powershell 运行时,HttpClient 允许的并发调用数量似乎存在限制,因此大于 40 到 50 个请求的并发批次中的调用开始堆积。它似乎正在运行 40 到 50 个并发请求,同时阻止其余请求。

我不是在寻求异步编程方面的帮助。我正在寻找一种方法来解决 Visual Studio 运行时行为和 Powershell 命令行运行时行为之间的差异。从 Visual Studio 的绿色箭头按钮在发布模式下运行的行为符合预期。从命令行运行不会。

我用异步调用填充任务列表,然后等待 Task.WhenAll(tasks)。每次调用需要 300 到 400 毫秒。从 Visual Studio 运行时,它按预期工作。我同时进行了 1000 个呼叫,每个呼叫都在预期时间内单独完成。整个任务块只比最长的单个调用长几毫秒。

当我从 Powershell 命令行运行相同的构建时,行为会发生变化。前 40 到 50 次调用预计需要 300 到 400 毫秒,但随后各个调用时间会增加到 20 秒。我认为这些调用正在序列化,因此一次只执行 40 到 50 个调用,而其他调用则在等待。

经过数小时的反复试验,我能够将其范围缩小到 HttpClient。为了隔离问题,我使用一个执行 Task.Delay(300) 并返回模拟结果的方法模拟了对 HttpClient.SendAsync 的调用。在这种情况下,从控制台运行与从 Visual Studio 运行的行为相同。

我正在使用 IHttpClientFactory,我什至尝试调整 ServicePointManager 上的连接限制。

这是我的注册码。

    public static IServiceCollection RegisterHttpClient(this IServiceCollection services, int batchSize)
    {
        ServicePointManager.DefaultConnectionLimit = batchSize;
        ServicePointManager.MaxServicePoints = batchSize;
        ServicePointManager.SetTcpKeepAlive(true, 1000, 5000);

        services.AddHttpClient(MSGraphRequestManager.HttpClientName, c =>
        {
            c.Timeout = TimeSpan.FromSeconds(360);
            c.DefaultRequestHeaders.Add("User-Agent", "xxxxxxxxxxxx");
        })
        .ConfigurePrimaryHttpMessageHandler(() => new DefaultHttpClientHandler(batchSize));

        return services;
    }

这是 DefaultHttpClientHandler。

internal class DefaultHttpClientHandler : HttpClientHandler
{
    public DefaultHttpClientHandler(int maxConnections)
    {
        this.MaxConnectionsPerServer = maxConnections;
        this.UseProxy = false;
        this.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate;
    }
}

这是设置任务的代码。

        var timer = Stopwatch.StartNew();
        var tasks = new Task<(UpsertUserResult, TimeSpan)>[users.Length];
        for (var i = 0; i < users.Length; ++i)
        {
            tasks[i] = this.CreateUserAsync(users[i]);
        }

        var results = await Task.WhenAll(tasks);
        timer.Stop();

这是我模拟 HttpClient 的方式。

        var httpClient = this.httpClientFactory.CreateClient(HttpClientName);
        #if use_http
            using var response = await httpClient.SendAsync(request);
        #else
            await Task.Delay(300);
            var graphUser = new User { Id = "mockid" };
            using var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonConvert.SerializeObject(graphUser)) };
        #endif
        var responseContent = await response.Content.ReadAsStringAsync();

以下是通过 GraphAPI 使用 500 个并发请求创建的 10k B2C 用户的指标。前 500 个请求比正常时间长,因为正在创建 TCP 连接。

这是console run metrics的链接。

这是Visual Studio run metrics的链接。

VS 运行指标中的阻塞时间与我在这篇文章中所说的不同,因为我将所有同步文件访问移至进程末尾,以尽可能隔离有问题的代码以进行测试运行.

项目使用 .Net Core 3.1 编译。我正在使用 Visual Studio 2019 16.4.5。

【问题讨论】:

  • 您是否查看了第一批后与 netstat 实用程序的连接状态?它可能会提供一些关于前几项任务完成后发生的情况的见解。
  • 如果您最终没有以这种方式解决它(异步 HTTP 请求),您始终可以在 ConcurrentQueue[object] 消费者/生产者并行机制中为每个用户使用同步 HTTP 调用。我最近在 PowerShell 中处理了大约 2 亿个文件。
  • @thepip3r 我刚刚重新阅读了您的推荐并理解了它。我会记住这一点的。
  • 不,如果你想使用 PowerShell 而不是 c#:leeholmes.com/blog/2018/09/05/…
  • @thepip3r 只需阅读 Stephen Cleary 的博客条目。我应该很好。

标签: c# visual-studio powershell .net-core httpclient


【解决方案1】:

我想到了两件事。大多数 microsoft powershell 是在版本 1 和 2 中编写的。版本 1 和 2 具有 MTA 的 System.Threading.Thread.ApartmentState。在版本 3 到 5 中,公寓状态默认更改为 STA。

第二个想法是听起来他们正在使用 System.Threading.ThreadPool 来管理线程。你的线程池有多大?

如果这些都不能解决问题,请开始在 System.Threading 下挖掘。

当我读到你的问题时,我想到了这个博客。 https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455

一位同事演示了一个创建一千个工作项的示例程序,每个工作项都模拟一个需要 500 毫秒才能完成的网络调用。在第一个演示中,网络调用是阻塞同步调用,示例程序将线程池限制为十个线程,以使效果更加明显。在这种配置下,前几个工作项被快速分派到线程,但随后由于没有更多线程可用于服务新工作项,延迟开始增加,因此剩余的工作项必须等待越来越长的时间才能让线程可以为它服务。开始工作项的平均延迟超过两分钟。

更新 1: 我从开始菜单运行 PowerShell 7.0,线程状态为 STA。两个版本的线程状态是否不同?

PS C:\Program Files\PowerShell\7>  [System.Threading.Thread]::CurrentThread

ManagedThreadId    : 12
IsAlive            : True
IsBackground       : False
IsThreadPoolThread : False
Priority           : Normal
ThreadState        : Running
CurrentCulture     : en-US
CurrentUICulture   : en-US
ExecutionContext   : System.Threading.ExecutionContext
Name               : Pipeline Execution Thread
ApartmentState     : STA

更新 2: 我希望得到更好的答案,但是,您将比较这两种环境,直到有什么突出的。

PS C:\Windows\system32> [System.Net.ServicePointManager].GetProperties() | select name

Name                               
----                               
SecurityProtocol                   
MaxServicePoints                   
DefaultConnectionLimit             
MaxServicePointIdleTime            
UseNagleAlgorithm                  
Expect100Continue                  
EnableDnsRoundRobin                
DnsRefreshTimeout                  
CertificatePolicy                  
ServerCertificateValidationCallback
ReusePort                          
CheckCertificateRevocationList     
EncryptionPolicy            

更新 3:

https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient

另外,每个 HttpClient 实例都使用自己的连接池, 将其请求与其他 HttpClient 执行的请求隔离开来 实例。

如果一个应用使用了 HttpClient 和 Windows.Web.Http 中的相关类 命名空间下载大量数据(50 兆字节或更多),然后 该应用程序应该流式传输这些下载,而不是使用默认 缓冲。如果使用默认缓冲,则客户端内存使用情况 会变得非常大,可能会导致性能下降。

只要继续比较两个环境,问题就会很突出

Add-Type -AssemblyName System.Net.Http
$client = New-Object -TypeName System.Net.Http.Httpclient
$client | format-list *

DefaultRequestHeaders        : {}
BaseAddress                  : 
Timeout                      : 00:01:40
MaxResponseContentBufferSize : 2147483647

【讨论】:

  • 在 Powershell 7.0 中运行时 System.Threading.Thread.CurrentThread.GetApartmentState() 从 Program.Main() 中返回 MTA
  • 默认最小线程池为 12,我尝试将最小池大小增加到我的批处理大小(500 用于测试)。这对行为没有影响。
  • 我想知道“HttpClient”有多少线程,因为它正在完成所有工作。
  • 两个版本的公寓状态如何?
  • 在我看来,c# httpclient 调用更接近 tcp 堆栈。为什么要注入 powershell?它为您解决这个问题的任务添加了一个秘密层。很可能不会有明确的答案,因为无法访问代码。
猜你喜欢
  • 1970-01-01
  • 2022-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-20
  • 2018-01-12
  • 1970-01-01
  • 2015-10-14
相关资源
最近更新 更多