Visual Studio에서와 Powershell에서 실행될 때 HttpClient 동시 동작


10

MS Graph API를 사용하여 B2C에서 사용자를 만들기 위해 수백만 명의 사용자를 온-프레미스 AD에서 Azure AD 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 사용자에 대한 메트릭입니다. TCP 연결이 작성되고 있기 때문에 처음 500 개의 요청이 평소보다 오래 걸립니다.

다음은 콘솔 실행 메트릭에 대한 링크 입니다.

다음은 Visual Studio 실행 메트릭에 대한 링크 입니다.

VS 실행 메트릭의 차단 시간은 테스트 실행을 위해 가능한 한 문제가있는 코드를 격리하기 위해 모든 동기 파일 액세스를 프로세스의 끝으로 이동했기 때문에이 게시물에서 언급 한 것과 다릅니다.

프로젝트는 .Net Core 3.1을 사용하여 컴파일됩니다. Visual Studio 2019 16.4.5를 사용하고 있습니다.


2
첫 번째 배치 후 netstat 유틸리티를 사용한 연결 상태를 검토 했습니까? 처음 몇 가지 작업이 완료된 후 진행 상황에 대한 통찰력을 제공 할 수 있습니다.
Pranav Negandhi

이 방법으로 해결하지 않으면 (HTTP 요청 비동기) ConcurrentQueue [개체] 소비자 / 프로듀서 병렬 처리에서 각 사용자에 대해 항상 동기화 HTTP 호출을 사용할 수 있습니다. 최근 PowerShell에서 약 2 억 개의 파일에 대해이 작업을 수행했습니다.
thepip3r

1
@ thepip3r 나는 단지 당신의 칭찬을 다시 읽고 이번에는 그것을 이해했습니다. 나는 그것을 명심할 것이다.
Mark Lauter

1
: 대신 C #을의 PowerShell을 가고 싶어한다면 아니, 난, 말하고 leeholmes.com/blog/2018/09/05/...을 .
thepip3r

1
@ thepip3r Stephen Cleary의 블로그 항목을 읽으십시오. 나는 잘해야합니다.
마크 라우터

답변:


3

두 가지가 떠 오릅니다. 대부분의 Microsoft powershell은 버전 1과 2로 작성되었습니다. 버전 1과 2에는 System.Threading.Thread.ApartmentState of MTA가 있습니다. 버전 3 ~ 5에서는 아파트 상태가 기본적으로 STA으로 변경되었습니다.

두 번째 생각은 System.Threading.ThreadPool을 사용하여 스레드를 관리하는 것처럼 들립니다. 스레드 풀이 얼마나 큽니까?

그래도 문제가 해결되지 않으면 System.Threading에서 파기를 시작하십시오.

귀하의 질문을 읽을 때 나는이 블로그를 생각했습니다. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455

한 동료가 수천 개의 작업 항목을 작성하는 샘플 프로그램으로 시연했으며, 각 항목은 완료하는 데 500ms가 걸리는 네트워크 호출을 시뮬레이션합니다. 첫 번째 데모에서 네트워크 호출은 동기 호출을 차단하고 있으며 샘플 프로그램은 효과를보다 명확하게하기 위해 스레드 풀을 10 개의 스레드로 제한했습니다. 이 구성에서 처음 몇 개의 작업 항목이 스레드로 신속하게 발송되었지만 새 작업 항목을 서비스하는 데 사용할 수있는 스레드가 더 이상 없어 대기 시간이 길어지기 시작했습니다. 서비스 할 수있게됩니다. 작업 항목 시작까지의 평균 대기 시간은 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/en-us/uwp/api/windows.web.http.httpclient

또한 모든 HttpClient 인스턴스는 자체 연결 풀을 사용하여 다른 HttpClient 인스턴스에서 실행 된 요청과 해당 요청을 분리합니다.

Windows.Web.Http 네임 스페이스에서 HttpClient 및 관련 클래스를 사용하는 앱이 많은 양의 데이터 (50MB 이상)를 다운로드하는 경우 앱은 해당 다운로드를 스트리밍하고 기본 버퍼링을 사용하지 않아야합니다. 기본 버퍼링을 사용하면 클라이언트 메모리 사용량이 매우 커져 잠재적으로 성능이 저하 될 수 있습니다.

두 환경을 계속 비교하면 문제가 두드러집니다.

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)로 늘리려 고했습니다. 이것은 행동에 영향을 미치지 않았습니다.
마크 라우터

두 환경에서 몇 개의 스레드가 생성됩니까?
Aaron

'HttpClient'가 모든 작업을 수행하고 있기 때문에 얼마나 많은 스레드가 있는지 궁금합니다.
Aaron

두 버전의 아파트 상태는 무엇입니까?
Aaron
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.