繁体   English   中英

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

[英]HttpClient concurrent behavior different when running in Powershell than in Visual Studio

我正在使用 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();

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM