최근에 고전적인 다중 스레드 방식에 비해 비동기 방식으로 생성 될 수있는 HTTP 호출 처리량을 테스트하기위한 간단한 응용 프로그램을 만들었습니다.
애플리케이션은 사전 정의 된 수의 HTTP 호출을 수행 할 수 있으며 결국에는이를 수행하는 데 필요한 총 시간을 표시합니다. 테스트하는 동안 모든 HTTP 호출이 로컬 IIS 서버에 이루어졌으며 작은 텍스트 파일 (12 바이트 크기)을 검색했습니다.
비동기 구현을위한 코드의 가장 중요한 부분은 다음과 같습니다.
public async void TestAsync()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
ProcessUrlAsync(httpClient);
}
}
private async void ProcessUrlAsync(HttpClient httpClient)
{
HttpResponseMessage httpResponse = null;
try
{
Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
httpResponse = await getTask;
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
finally
{
if(httpResponse != null) httpResponse.Dispose();
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
}
멀티 스레딩 구현의 가장 중요한 부분은 다음과 같습니다.
public void TestParallel2()
{
this.TestInit();
ServicePointManager.DefaultConnectionLimit = 100;
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
Task.Run(() =>
{
try
{
this.PerformWebRequestGet();
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
});
}
}
private void PerformWebRequestGet()
{
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
request = (HttpWebRequest)WebRequest.Create(URL);
request.Method = "GET";
request.KeepAlive = true;
response = (HttpWebResponse)request.GetResponse();
}
finally
{
if (response != null) response.Close();
}
}
테스트를 실행하면 멀티 스레드 버전이 더 빠릅니다. 10k 요청에 대해 완료하는 데 약 0.6 초가 소요되었으며 비동기 요청은 동일한 양의로드에 대해 완료하는 데 약 2 초가 걸렸습니다. 비동기식이 더 빠를 것으로 기대했기 때문에 이것은 약간의 놀라움이었습니다. 내 HTTP 호출이 매우 빠르기 때문일 수 있습니다. 실제 시나리오에서는 서버가보다 의미있는 작업을 수행해야하고 네트워크 대기 시간도 있어야하는 경우 결과가 반전 될 수 있습니다.
그러나 실제로 관심이있는 것은로드가 증가 할 때 HttpClient가 작동하는 방식입니다. 10k 메시지를 전달하는 데 약 2 초가 걸리기 때문에 10 배의 메시지 수를 전달하는 데 약 20 초가 소요될 것이라고 생각했지만 테스트를 실행하면 100k 메시지를 전달하는 데 약 50 초가 소요되는 것으로 나타났습니다. 또한 일반적으로 200k 메시지를 배달하는 데 2 분 이상이 걸리며 다음과 같은 경우를 제외하고 수천 개 (3-4k)가 실패하는 경우가 많습니다.
시스템에 충분한 버퍼 공간이 없거나 큐가 가득 찼기 때문에 소켓에서 작업을 수행 할 수 없습니다.
IIS 로그와 실패한 작업이 서버에 도착하지 않았 음을 확인했습니다. 그들은 클라이언트 내에서 실패했습니다. 임시 포트의 기본 범위 49152 ~ 65535를 사용하여 Windows 7 시스템에서 테스트를 실행했습니다. netstat를 실행하면 테스트 중에 약 5-6k 포트가 사용되는 것으로 나타 났으므로 이론적으로는 더 많은 것이 가능했을 것입니다. 포트가 실제로 예외의 원인 인 경우 netstat가 상황을 제대로보고하지 않았거나 HttClient가 최대 포트 수만 사용하고 예외가 발생하기 시작합니다.
대조적으로, HTTP 호출을 생성하는 멀티 스레드 접근 방식은 매우 예측 가능합니다. 10k 메시지의 경우 약 0.6 초, 100k 메시지의 경우 약 5.5 초, 1 백만 메시지의 경우 약 55 초가 걸렸습니다. 실패한 메시지가 없습니다. 또한 실행하는 동안 Windows 작업 관리자에 따라 55MB 이상의 RAM을 사용하지 않았습니다. 메시지를 비동기 적으로 보낼 때 사용되는 메모리는로드에 비례하여 증가했습니다. 200k 메시지 테스트 중에 약 500MB의 RAM을 사용했습니다.
위의 결과에는 두 가지 주요 이유가 있다고 생각합니다. 첫 번째는 HttpClient가 서버와의 새로운 연결을 만드는 데 매우 탐욕스러워 보인다는 것입니다. netstat에 의해보고 된 사용 된 포트 수가 많으면 HTTP 연결 유지 기능의 이점이 크지 않을 수 있습니다.
두 번째는 HttpClient에 조절 메커니즘이없는 것입니다. 실제로 이것은 비동기 작업과 관련된 일반적인 문제인 것 같습니다. 매우 많은 수의 작업을 수행해야하는 경우 모두 한 번에 시작되고 사용 가능한대로 연속이 실행됩니다. 이론적으로 이것은 비동기 작업에서 부하가 외부 시스템에 있기 때문에 이상이 없지만 위에서 입증 된 것처럼 이것은 전부는 아닙니다. 한 번에 많은 수의 요청을 시작하면 메모리 사용량이 증가하고 전체 실행 속도가 느려집니다.
간단하지만 기본 지연 메커니즘으로 최대 비동기 요청 수를 제한하여 더 나은 결과, 메모리 및 실행 시간을 현명하게 얻을 수있었습니다.
public async void TestAsyncWithDelay()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
if (_activeRequestsCount >= MAX_CONCURENT_REQUESTS)
await Task.Delay(DELAY_TIME);
ProcessUrlAsyncWithReqCount(httpClient);
}
}
HttpClient에 동시 요청 수를 제한하는 메커니즘이 포함되어 있으면 정말 유용합니다. .Net 스레드 풀을 기반으로하는 Task 클래스를 사용하는 경우 동시 스레드 수를 제한하여 제한을 자동으로 수행합니다.
완전한 개요를 위해 HttpClient가 아닌 HttpWebRequest를 기반으로 한 비동기 테스트 버전을 만들었으며 훨씬 더 나은 결과를 얻을 수있었습니다. 시작을 위해 동시 연결 수에 대한 제한을 설정할 수 있습니다 (ServicePointManager.DefaultConnectionLimit 또는 구성을 통해). 포트가 부족하지 않고 모든 요청에서 실패하지 않았 음을 의미합니다 (기본적으로 HttpClient는 HttpWebRequest를 기반으로 함) 연결 제한 설정을 무시하는 것 같습니다).
비동기 HttpWebRequest 접근 방식은 여전히 멀티 스레딩 접근 방식보다 약 50-60 % 느리지 만 예측 가능하고 신뢰할 수 있습니다. 그것의 유일한 단점은 큰로드에서 엄청난 양의 메모리를 사용한다는 것입니다. 예를 들어 백만 건의 요청을 보내려면 약 1.6GB가 필요했습니다. 동시 요청 수를 제한함으로써 (HttpClient에서 위에서했던 것처럼) 사용 된 메모리를 20MB로 줄이고 멀티 스레딩 방식보다 10 % 느린 실행 시간을 얻었습니다.
이 긴 프리젠 테이션 후에, 나의 질문은 다음과 같습니다. .Net 4.5의 HttpClient 클래스가 집중적 인로드 응용 프로그램에 적합하지 않습니까? 내가 언급 한 문제를 해결 해야하는 방법이 있습니까? HttpWebRequest의 비동기 풍미는 어떻습니까?
업데이트 (@Stephen Cleary 덕분에)
알 수 있듯이 HttpClient는 HttpWebRequest (기본적으로 기반으로 함)와 마찬가지로 ServicePointManager.DefaultConnectionLimit로 제한된 동일한 호스트에서 동시 연결 수를 가질 수 있습니다. 이상한 점은 MSDN 에 따르면 연결 제한의 기본값은 2입니다. 디버거를 사용하여 내면에서 실제로 2가 기본값임을 확인했습니다. 그러나 값을 ServicePointManager.DefaultConnectionLimit으로 명시 적으로 설정하지 않으면 기본값이 무시됩니다. HttpClient 테스트 중에 명시 적으로 값을 설정하지 않았으므로 무시되었다고 생각했습니다.
ServicePointManager.DefaultConnectionLimit를 100으로 설정 한 후 HttpClient는 신뢰할 수 있고 예측 가능해졌습니다 (netstat는 100 개의 포트만 사용됨을 확인합니다). 여전히 비동기 HttpWebRequest (약 40 %)보다 느리지 만 이상하게도 더 적은 메모리를 사용합니다. 백만 건의 요청이 포함 된 테스트의 경우 비동기 HttpWebRequest의 1.6GB와 비교하여 최대 550MB를 사용했습니다.
따라서 ServicePointManager.DefaultConnectionLimit 조합의 HttpClient는 안정성을 보장하는 것처럼 보이지만 (적어도 모든 호스트가 동일한 호스트를 향한 시나리오의 경우) 적절한 조절 메커니즘이 없기 때문에 성능에 부정적인 영향을 미치는 것으로 보입니다. 동시 요청 수를 구성 가능한 값으로 제한하고 나머지를 대기열에 넣는 것은 확장 성이 높은 시나리오에 훨씬 적합합니다.
SemaphoreSlim
이미 언급했듯이 또는 ActionBlock<T>
TPL Dataflow 에서을 사용할 수 있습니다 .
HttpClient
존중해야합니다ServicePointManager.DefaultConnectionLimit
.