在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.