.Net 4.5의 비동기 HttpClient가 집중로드 애플리케이션에 적합하지 않습니까? for (int i = 0;

최근에 고전적인 다중 스레드 방식에 비해 비동기 방식으로 생성 될 수있는 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는 안정성을 보장하는 것처럼 보이지만 (적어도 모든 호스트가 동일한 호스트를 향한 시나리오의 경우) 적절한 조절 메커니즘이 없기 때문에 성능에 부정적인 영향을 미치는 것으로 보입니다. 동시 요청 수를 구성 가능한 값으로 제한하고 나머지를 대기열에 넣는 것은 확장 성이 높은 시나리오에 훨씬 적합합니다.



답변

이 질문에 언급 된 테스트 외에도 최근에는 훨씬 적은 수의 HTTP 호출 (이전에 1 백만에 비해 5000 건)이 걸리지 만 실행하는 데 훨씬 오래 걸리는 요청 (이전에는 1 밀리 초에 비해 500 밀리 초)이 새로 추가되었습니다. 동기식 멀티 스레드 애플리케이션 (HttpWebRequest 기반)과 비동기 I / O 애플리케이션 (HTTP 클라이언트 기반) 둘 다 테스터 애플리케이션은 CPU의 약 3 %와 30MB의 메모리를 사용하여 실행하는 데 약 10 초가 소요됩니다. 두 테스터의 유일한 차이점은 멀티 스레드가 310 스레드를 사용하고 비동기 스레드는 22를 사용한다는 것입니다.

내 테스트의 결론으로, 매우 빠른 요청을 처리 할 때 비동기 HTTP 호출이 최선의 옵션이 아닙니다. 그 이유는 비동기 I / O 호출이 포함 된 작업을 실행할 때 작업이 시작되는 스레드가 비동기 호출이 수행되고 나머지 작업이 콜백으로 등록되는 즉시 종료되기 때문입니다. 그런 다음 I / O 작업이 완료되면 사용 가능한 첫 번째 스레드에서 콜백이 큐에 대기됩니다. 이 모든 것이 오버 헤드를 생성하므로, 시작한 스레드에서 실행될 때 빠른 I / O 작업의 효율성이 높아집니다.

비동기 HTTP 호출은 I / O 작업이 완료 될 때까지 대기중인 스레드를 유지하지 않기 때문에 길거나 잠재적으로 긴 I / O 작업을 처리 할 때 좋은 옵션입니다. 이렇게하면 응용 프로그램에서 사용하는 전체 스레드 수가 줄어 CPU 바인딩 작업에 더 많은 CPU 시간을 소비 할 수 있습니다. 또한 제한된 수의 스레드 만 할당하는 응용 프로그램 (웹 응용 프로그램의 경우와 같이)에서 비동기 I / O는 스레드 풀 스레드 고갈을 방지하여 I / O 호출을 동 기적으로 수행 할 경우 발생할 수 있습니다.

따라서 비동기 HttpClient는 집중적 인로드 응용 프로그램의 병목 현상이 아닙니다. 그것은 본질적으로 매우 빠른 HTTP 요청에 적합하지 않으며, 길거나 잠재적으로 긴 요청, 특히 제한된 수의 스레드 만 사용할 수있는 응용 프로그램에 이상적입니다. 또한 ServicePointManager.DefaultConnectionLimit를 통해 동시성을 제한하는 것은 좋은 수준의 병렬 처리를 보장하기에 충분히 높지만 임시 포트 고갈을 방지하기에 충분히 낮은 값으로하는 것입니다. 이 질문에 대한 테스트 및 결론에 대한 자세한 내용은 여기를 참조하십시오 .


답변

결과에 영향을 줄 수있는 한 가지 고려해야 할 사항은 HttpWebRequest를 사용하면 ResponseStream을 얻지 않고 해당 스트림을 소비한다는 것입니다. HttpClient를 사용하면 기본적으로 네트워크 스트림을 메모리 스트림에 복사합니다. 현재 HttpWebRquest를 사용하는 것과 같은 방식으로 HttpClient를 사용하려면 다음을 수행해야합니다.

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 및 모든 강력한 형식의 헤더와 같은 표준 클래스입니다. 그 자체로는 성능 최적화가 아닙니다.


답변

이것은 OP 질문의 ‘비동기’부분에 직접 대답하지는 않지만, 사용중인 구현의 오류를 해결합니다.

애플리케이션을 확장하려면 인스턴스 기반 HttpClient를 사용하지 마십시오. 차이점은 거대합니다! 부하에 따라 성능 수치가 매우 다릅니다. HttpClient는 여러 요청에서 재사용되도록 설계되었습니다. 이것은 BCL 팀의 직원들에 의해 확인되었습니다.

내가 최근에 수행 한 프로젝트는 매우 유명한 대형 온라인 컴퓨터 소매 업체가 일부 새로운 시스템의 Black Friday / holiday 트래픽으로 확장 할 수 있도록 돕는 것이 었습니다. HttpClient 사용과 관련된 성능 문제가 발생했습니다. 그것은 구현하기 때문에 개발자 IDisposable는 인스턴스를 생성하고 using()명령문 안에 배치하여 일반적으로하는 일을했습니다 . 우리가로드 테스트를 시작하면 앱이 서버를 무릎 꿇게했습니다. 그렇습니다. 앱뿐만 아니라 서버도 마찬가지입니다. 그 이유는 모든 HttpClient 인스턴스가 서버에서 I / O 완료 포트를 열기 때문입니다. GC의 결정적이지 않은 마무리와 여러 OSI 계층에 걸쳐있는 컴퓨터 리소스를 사용하고 있기 때문에 네트워크 포트를 닫는 데 시간이 오래 걸릴 수 있습니다. 실제로 Windows OS 자체포트를 닫는 데 최대 20 초가 소요될 수 있습니다 (Microsoft 당). 우리는 닫힐 수있는 것보다 빨리 포트를 열었습니다. 서버 포트 소진으로 인해 CPU가 100 %로 떨어졌습니다. 내 문제는 HttpClient를 정적 인스턴스로 변경하여 문제를 해결하는 것이 었습니다. 예, 일회용 자원이지만 성능의 차이로 인해 오버 헤드가 훨씬 더 큽니다. 앱의 작동 방식을 확인하기 위해로드 테스트를 수행하는 것이 좋습니다.

또한 아래 링크에서 답변했습니다.

WebAPI 클라이언트에서 호출 당 새로운 HttpClient를 작성하는 오버 헤드는 무엇입니까?

https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client


답변