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를 사용하고 있습니다.
답변
두 가지가 떠 오릅니다. 대부분의 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