[c#] 닷넷 4.5의 비동기식 HttpClient는 집중적 인 부하 애플리케이션에 나쁜 선택입니까?


0 Answers

결과에 영향을 미칠 수있는 한 가지 사항은 HttpWebRequest를 사용하면 ResponseStream을 가져 오지 않고 해당 스트림을 소비한다는 것입니다. HttpClient를 사용하면 기본적으로 네트워크 스트림을 메모리 스트림으로 복사합니다. HttpClient를 HttpWebRquest를 사용하는 것과 같은 방법으로 사용하려면 다음과 같이하십시오.

var requestMessage = new HttpRequestMessage() {RequestUri = URL};
Task<HttpResponseMessage> getTask = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);

다른 것은 스레딩 관점에서 실제로 차이점이 무엇인지 잘 모르겠다는 것입니다. HttpClientHandler의 깊이를 파고 들면 비동기 요청을 수행하기 위해 Task.Factory.StartNew 만 수행합니다. 스레딩 동작은 HttpWebRequest 예제를 사용한 예제와 완전히 동일한 방식으로 동기화 컨텍스트에 위임됩니다.

의심의 여지없이 HttpClient는 기본적으로 HttpWebRequest를 전송 라이브러리로 사용하여 오버 헤드를 추가합니다. 따라서 HttpClientHandler를 사용하는 동안 항상 HttpWebRequest를 사용하여 더 나은 성능을 얻을 수 있습니다. HttpClient가 가져 오는 이점은 HttpResponseMessage, HttpRequestMessage, HttpContent 및 모든 강력한 형식의 헤더와 같은 표준 클래스를 사용하는 것입니다. 그 자체로 그것은 perf 최적화가 아닙니다.

Question

필자는 최근 비동기식으로 생성 될 수있는 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가 최대 수의 포트만 사용하여 예외를 throw하기 시작한다는 것을 의미합니다.

반대로 HTTP 호출을 생성하는 다중 스레드 접근 방식은 매우 예측 가능합니다. 필자는 10k 메시지의 경우 0.6 초, 100k 메시지의 경우 약 5.5 초, 100 만 메시지의 경우 약 55 초를 예상했습니다. 메시지가 모두 실패했습니다. 더 나아가, 실행 도중 55MB 이상의 RAM을 사용하지 않았습니다 (Windows 작업 관리자에 따르면). 비동기 적으로 메시지를 보낼 때 사용되는 메모리는로드와 비례하여 증가합니다. 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 또는 config를 통해). 이는 포트가 모두 절대로 실행되지 않고 모든 요청에 ​​대해 실패하지 않았 음을 의미합니다 (기본적으로 HttpClient는 HttpWebRequest를 기반으로합니다.) 하지만 연결 제한 설정을 무시하는 것 같습니다.

비동기식 HttpWebRequest 접근법은 멀티 스레딩 방식보다 여전히 약 50-60 % 느리지 만 예측 가능하고 신뢰성이 있습니다. 유일한 단점은 큰 부하가 걸린 상태에서 엄청난 양의 메모리를 사용한다는 것입니다. 예를 들어 1 백만 건의 요청을 전송하려면 약 1.6GB가 필요합니다. 위에서처럼 HttpClient에 대한 동시 요청 수를 제한함으로써 사용 된 메모리를 20MB로 줄이고 다중 스레드 접근 방식보다 10 % 느린 실행 시간을 얻을 수있었습니다.

이 긴 프레젠테이션을 한 후에, 내 질문은 다음과 같습니다. 집중 형 부하 응용 프로그램에 대한 .Net 4.5의 HttpClient 클래스가 나쁜 선택입니까? 그것을 막을 수있는 방법이 있습니까? 내가 언급 한 문제를 해결해야합니까? 어때 HttpWebRequest의 비동기 맛이 어때?

업데이트 (Thanks @ Stephen Cleary)

HttpClient는 HttpWebRequest (기본적으로 기반으로 함)와 마찬가지로 ServicePointManager.DefaultConnectionLimit으로 제한된 동일한 호스트에서 동시 연결 수를 가질 수 있습니다. 이상한 점은 MSDN 에 따르면 연결 제한의 기본값은 2입니다. 디버거를 사용하여 내 측면에서 실제로 2가 기본값임을 나타냅니다. 그러나 명시 적으로 ServicePointManager.DefaultConnectionLimit에 값을 설정하지 않으면 기본값이 무시됩니다. HttpClient 테스트 중에 명시 적으로 값을 설정하지 않았기 때문에 무시되었다고 생각했습니다.

ServicePointManager.DefaultConnectionLimit을 100으로 설정 한 후 HttpClient는 안정적이고 예측 가능 해졌습니다 (netstat는 100 개의 포트 만 사용함을 확인합니다). 그것은 여전히 ​​비동기 HttpWebRequest (약 40 %)보다 느리지 만, 이상하게도 더 적은 메모리를 사용합니다. 1 백만 요청을 포함하는 테스트의 경우 비동기 HttpWebRequest에서 1.6GB와 비교하여 최대 550MB를 사용했습니다.

따라서 ServicePointManager.DefaultConnectionLimit을 함께 사용하는 HttpClient는 안정성을 보장하는 것처럼 보이지만 (적어도 모든 호출이 동일한 호스트에 대해 이루어지는 시나리오의 경우) 적절한 조절 메커니즘이 없어 성능에 부정적인 영향을 미치게됩니다. 동시 요청 수를 구성 가능한 값으로 제한하고 나머지를 대기열에 넣는 것이 높은 확장 성 시나리오에 훨씬 더 적합합니다.






Related