在 Powershell 中运行时与在 Visual Studio 中运行时的 HttpClient 并发行为不同

Mar*_*ter 10 c# powershell httpclient visual-studio .net-core

我正在使用 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;
    }
Run Code Online (Sandbox Code Playgroud)

这是 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是设置任务的代码。

        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();
Run Code Online (Sandbox Code Playgroud)

这是我模拟 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();
Run Code Online (Sandbox Code Playgroud)

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

这是控制台运行指标的链接。

这是Visual Studio 运行指标的链接。

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

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

Aar*_*ron 3

我想到两件事。大多数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

一位同事演示了一个示例程序,该程序创建了 1000 个工作项,每个工作项都模拟一个需要 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
Run Code Online (Sandbox Code Playgroud)

更新 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            
Run Code Online (Sandbox Code Playgroud)

更新3:

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

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

如果使用 Windows.Web.Http 命名空间中的 HttpClient 和相关类的应用下载大量数据(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
Run Code Online (Sandbox Code Playgroud)