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


10

我正在使用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个并发请求创建的1万B2C用户的指标。前500个请求比正常的更长,因为正在创建TCP连接。

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

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

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

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


2
在第一批处理之后,您是否已使用netstat实用程序检查了连接状态?它可能提供一些有关前几项任务完成后发生情况的信息。
Pranav Negandhi

如果您最终没有以这种方式解决问题(异步HTTP请求),则可以始终对ConcurrentQueue [object]使用者/生产者并行处理中的每个用户使用同步HTTP调用。我最近在PowerShell中处理了大约2亿个文件。
–thepip3r

1
@ thepip3r这次我只是重新阅读了您的表扬并理解了它。我会记住这一点。
Mark Lauter

1
不,我是说,如果您想使用PowerShell而不是c#: leeholmes.com/blog/2018/09/05/…
thepip3r

1
@ thepip3r只需阅读Stephen Cleary的博客条目。我应该很好。
Mark Lauter

Answers:


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毫秒才能完成的网络调用。在第一个演示中,网络调用阻止了同步调用,并且示例程序将线程池限制为十个线程,以使效果更加明显。在这种配置下,前几个工作项被迅速分派给线程,但是随后开始建立延迟,因为没有更多的线程可用于服务新工作项,因此其余工作项必须等待更长的时间才能线程化。可以提供服务。到工作项目开始的平均等待时间超过2分钟。

更新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/zh-cn/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
Mark Lauter,

默认的最小线程池为12,我尝试将最小池大小增加到我的批处理大小(用于测试的为500)。这对行为没有影响。
Mark Lauter

两种环境中生成多少个线程?
亚伦

我想知道“ HttpClient”有多少个线程,因为它正在完成所有工作。
亚伦

您的两个版本中的公寓状态如何?
亚伦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.