모든 요청에 ​​대해 새로운 단일 HttpClient 인스턴스를 만들어야합니까?


57

최근 에 다음과 같은 방식 으로 사용하는 문제에 대해 이야기하는 asp.net 괴물 의이 블로그 게시물 을 보았습니다 HttpClient.

using(var client = new HttpClient())
{
}

블로그 게시물에 따라 HttpClient모든 요청을 처리 한 후에는 TCP 연결을 열어 둘 수 있습니다. 이로 인해 잠재적으로 System.Net.Sockets.SocketException.

게시물에 따라 올바른 방법 HttpClient은 소켓 낭비를 줄이는 데 도움이 되는 단일 인스턴스를 만드는 것 입니다.

게시물에서 :

HttpClient의 단일 인스턴스를 공유하면 소켓을 재사용하여 소켓 낭비를 줄일 수 있습니다.

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = Client.GetAsync("http://aspnetmonsters.com").Result;
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

이것이 HttpClient사용하는 것이 가장 좋은 방법이라고 생각했기 때문에 사용 후 항상 물건을 폐기 했습니다. 그러나이 블로그 게시물은 이제 내가 오랫동안 그렇게 잘못하고 있다고 생각합니다.

HttpClient모든 요청 에 대해 새 단일 인스턴스를 만들어야 합니까? 정적 인스턴스를 사용할 때의 함정이 있습니까?


사용 방식에 따른 문제가 있습니까?
whatsisname

어쩌면 확인 이 대답 또한 .
John Wu

@ whatsisname 아니오 나는 블로그를보고 있지만이 문제를 항상 잘못 사용하고 있다고 생각했습니다. 따라서 두 가지 접근 방식에서 문제가 발생하면 동료 개발자로부터 이해하고 싶었습니다.
Ankit Vijay

3
나는 그것을 직접 시도하지 않았으므로 (응답으로 제공하지는 않지만) .NET Core 2.1의 Microsoft에 따르면 docs.microsoft.com/en-us/dotnet/standard/에
Joeri Sebrechts

(내 대답에서 언급했듯이, 더 잘 보이게하고 싶었으므로 짧은 의견을 작성하고 있습니다.) 정적 인스턴스는 일단 Close()새로운 작업을 시작하거나 시작 하면 tcp 연결 종료 핸드 셰이크를 올바르게 처리합니다 Get(). 클라이언트가 끝나면 클라이언트를 폐기하면 닫는 핸드 셰이크를 처리 할 사람이 없으며 포트 때문에 포트가 모두 TIME_WAIT 상태가됩니다.
Mladen B.

답변:


39

매력적인 블로그 게시물처럼 보입니다. 그러나 결정을 내리기 전에 먼저 블로그 작성자가 실행 한 것과 동일한 테스트를 자체 코드로 실행했습니다. 또한 HttpClient와 그 동작에 대해 조금 더 알아 보려고합니다.

이 게시물 은 다음과 같이 말합니다.

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

따라서 HttpClient가 공유 될 때 발생할 수있는 일은 연결이 재사용되고 있기 때문에 지속적인 연결이 필요하지 않은 경우에 좋습니다. 이것이 상황에 중요한지 확실하게 알 수있는 유일한 방법은 자체 성능 테스트를 실행하는 것입니다.

발굴하면이 문제를 해결하는 몇 가지 다른 리소스 (Microsoft Best Practices 기사 포함)를 찾을 수 있으므로 어쨌든 (예방 조치를 취하여) 구현하는 것이 좋습니다.

참고 문헌

Httpclient를 잘못 사용하고 있으며 소프트웨어를 불안정하게 만들고
있습니까? Singleton HttpClient? 이 심각한 동작과 그 문제를 해결하는 방법
Microsoft 패턴 및 실습-성능 최적화 : 부적절한 인스턴스화
코드 검토시 재사용 가능한 HttpClient의 단일 인스턴스
Singleton HttpClient는 DNS 변경 (CoreFX)을 존중하지 않음
HttpClient 사용에 대한 일반적인 조언


1
좋은 목록입니다. 이것은 나의 주말 읽었다.
Ankit Vijay

"발굴하면이 문제를 해결하는 몇 가지 다른 리소스를 찾을 수 있습니다 ..."TCP 연결 열기 문제를 말하는 것입니까?
Ankit Vijay

짧은 대답 : 정적 HttpClient 사용하십시오 . 웹 서버 나 다른 서버의 DNS 변경을 지원해야하는 경우 시간 초과 설정에 대해 걱정해야합니다.
Jess

3
HttpClient가 엉망이 된 방법에 대한 증거는 @AnkitVijay가 주석을 쓴 것처럼 "주말 읽기"라는 것입니다.
usr

DNS 변경 외에 @Jess-단일 소켓을 통해 모든 클라이언트 트래픽을 처리하면로드 밸런싱도 망가질 수 있습니까?
Iain

16

파티에 늦었지만이 까다로운 주제에 대한 학습 여정이 있습니다.

1. HttpClient 재사용에 대한 공식 옹호자는 어디에서 찾을 수 있습니까?

나는 경우 의미 HttpClient를 재사용하는 것은 의도일을 매우 중요 , 그러한 옹호 더 나은 오히려 "고급 항목", "성능 (안티) 패턴"또는 거기에 다른 블로그 게시물의 많은에 숨겨진되지 않고, 자신의 API 문서에 설명되어 있습니다 . 그렇지 않으면 새로운 학습자가 너무 늦기 전에 그것을 어떻게 알아야합니까?

현재 (2018 년 5 월), "c # httpclient"를 인터넷 검색 할 때의 첫 번째 검색 결과 는 MSDN의이 API 참조 페이지를 가리키며 ,이 의도는 전혀 언급되지 않았습니다. 글쎄, 초보자를위한 여기 1 강의는 항상 MSDN 도움말 페이지 제목 바로 다음에 "기타 버전"링크를 클릭하면 아마도 "현재 버전"에 대한 링크를 찾을 수 있습니다. 이 HttpClient의 경우, 그 의도 설명이 포함 된 최신 문서로 이동 합니다 .

나도 올바른 문서 페이지를 찾을 수 없습니다이 주제에 새이었다 많은 개발자를 의심,이 기술이 널리 확산되지 않은 이유, 그리고 그들이 그것을 발견 할 때 사람들은 놀랐다 나중에 아마도, 하드 방식 .

2. (mis?) 개념 using IDisposable

이것은 약간 주제에서 벗어나 있지만, 어떻게 비난하는 언급 한 블로그 게시물에있는 사람들을 볼 수있는 우연이 아니다, 지적 여전히 가치가있다 HttpClient'의 IDisposable그들이 사용하는 경향이 있습니다 인터페이스 using (var client = new HttpClient()) {...}패턴을 다음 문제로 이어질는.

나는 그것이 "IDisposable 객체는 수명이 짧을 것으로 예상된다"는 무의미한 개념으로 귀결된다고 생각한다 .

그러나이 스타일로 코드를 작성할 때 확실히 수명이 짧은 것처럼 보입니다.

using (var foo = new SomeDisposableObject())
{
    ...
}

IDisposable에 대한 공식 문서IDisposable개체의 수명이 짧아야 한다고 언급하지 않습니다 . 정의에 따르면 IDisposable은 관리되지 않는 리소스를 해제 할 수있는 메커니즘 일뿐입니다. 더 이상 없습니다. 그런 의미에서 결국 폐기를 촉발 할 것으로 예상되지만 단기적으로 그렇게 할 필요는 없습니다.

따라서 실제 물체의 수명주기 요구 사항에 따라 폐기를 트리거 할시기를 올바르게 선택하는 것이 작업입니다. IDisposable을 오래 사용하는 것을 막을 수있는 것은 없습니다 :

using System;
namespace HelloWorld
{
    class Hello
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");

            using (var client = new HttpClient())
            {
                for (...) { ... }  // A really long loop

                // Or you may even somehow start a daemon here

            }

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
}

이 새로운 이해를 통해 이제 블로그 게시물 을 다시 방문 하면 "수정"이 HttpClient한 번 초기화 되지만 절대 폐기하지 않는다는 것을 분명히 알 수 있습니다. 따라서 netstat 출력에서 ​​연결이 ESTABLISHED 상태로 유지된다는 것을 알 수 있습니다. 제대로 닫히지 않았습니다. 닫히면 상태는 TIME_WAIT입니다. 실제로 전체 프로그램이 종료 된 후 하나의 연결 만 열면 크게 문제가되지 않으며 블로그 포스터는 여전히 수정 후에도 성능이 향상됩니다. 그러나 여전히 IDisposable을 비난하고 처분하지 않기로 선택하는 것은 개념적으로 올바르지 않습니다.

3. HttpClient를 정적 속성에 넣거나 단일 톤으로 넣어야합니까?

이전 섹션의 이해를 바탕으로 여기에 대한 답변이 "필수 사항은 아님"으로 명확 해 졌다고 생각합니다. HttpClient를 재사용하고 (이상적으로) 결국 폐기하는 한 코드를 구성하는 방법에 달려 있습니다.

유감스럽게도 현재 공식 문서비고 섹션에 나오는 예조차도 엄격하게 올바른 것은 아닙니다. 처리되지 않을 정적 HttpClient 속성을 포함하는 "GoodController"클래스를 정의합니다. 무엇을 거역하는 예 부분에 또 다른 예는 강조한다 : "처분 호출 할 필요가 ... 그래서 응용 프로그램은 자원을 누설하지 않습니다."

마지막으로, 싱글 톤 자체에는 어려움이 없습니다.

"글로벌 변수가 좋은 아이디어라고 생각하는 사람은 몇 명입니까?

얼마나 많은 사람들이 싱글 톤이 좋은 생각이라고 생각합니까? 약간의, 몇개의, 가산 복수 명사 앞에 위치하는 수량 표현.

무엇을 제공합니까? 싱글 톤은 여러 글로벌 변수 일뿐입니다. "

-이 감동적인 연설에서 인용 한 "글로벌 스테이트와 싱글 톤"

PS : SqlConnection

이것은 현재의 Q & A와 관련이 없지만, 아마 알고있을 것입니다. SqlConnection 사용 패턴이 다릅니다. 당신은 도록 SqlConnection을 재사용 할 필요가 없습니다 그것의 연결 풀 나은 그런 식으로 처리 할 수 있기 때문에.

차이점은 구현 방식으로 인해 발생합니다. 각 HttpClient 인스턴스는 자체 연결 풀 ( 여기 에서 인용 )을 사용합니다. 그러나 이것 에 따르면 SqlConnection 자체는 중앙 연결 풀에 의해 관리됩니다 .

그리고 HttpClient에서와 마찬가지로 SqlConnection을 폐기해야합니다.


14

static 테스트를 통해 일부 성능 테스트를 수행했습니다 HttpClient. 테스트를 위해 아래 코드를 사용했습니다.

namespace HttpClientTest
{
    using System;
    using System.Net.Http;

    class Program
    {
        private static readonly int _connections = 10;
        private static readonly HttpClient _httpClient = new HttpClient();

        private static void Main()
        {
            TestHttpClientWithStaticInstance();
            TestHttpClientWithUsing();
        }

        private static void TestHttpClientWithUsing()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    using (var httpClient = new HttpClient())
                    {
                        var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        private static void TestHttpClientWithStaticInstance()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

시험용:

  • 10, 100, 1000 및 1000 연결로 코드를 실행했습니다.
  • 평균을 알아 보려면 각 테스트를 3 회 실행하십시오.
  • 한 번에 한 가지 방법 만 실행

요청 HttpClient을 처리하는 대신 정적 을 사용하여 40 % ~ 60 % uon의 성능 향상을 발견 했습니다 HttpClient. 나는 성능 테스트 결과의 세부 사항을 여기 블로그 포스트에 넣었다 .


1

TCP 연결 을 올바르게 닫으 려면 FIN-FIN + ACK-ACK 패킷 시퀀스를 완료해야합니다 ( TCP 연결을 열 때 SYN-SYN + ACK-ACK와 동일 ). .Close () 메소드를 호출하고 (일반적으로 HttpClient 가 처리 중일 때 발생 ) 원격 측에서 닫기 요청 (FIN + ACK 사용)을 확인하기를 기다리지 않으면 TIME_WAIT 상태가됩니다. 우리는 리스너 (HttpClient)를 폐기하고 원격 피어가 FIN + ACK 패킷을 보내면 포트 상태를 적절한 닫힌 상태로 재설정 할 기회를 얻지 못했기 때문에 로컬 TCP 포트.

TCP 연결을 닫는 올바른 방법은 .Close () 메서드를 호출하고 다른 쪽 (FIN + ACK)의 close 이벤트가 우리쪽에 도착할 때까지 기다리는 것입니다. 그래야만 최종 ACK를 보내고 HttpClient를 폐기 할 수 있습니다.

추가하기 위해, "연결 : 연결 유지"HTTP 헤더로 인해 HTTP 요청을 수행하는 경우 TCP 연결을 열어 두는 것이 좋습니다. 또한 HTTP 헤더 "Connection : Close"를 설정하여 원격 피어에게 연결을 끊도록 요청할 수 있습니다. 이렇게하면 TIME_WAIT 상태가 아닌 로컬 포트가 항상 올바르게 닫힙니다.


1

다음은 HttpClient 및 HttpClientHandler를 효율적으로 사용하는 기본 API 클라이언트입니다. 요청을하기 위해 새로운 HttpClient를 만들 때 많은 오버 헤드가 있습니다. 각 요청에 대해 HttpClient를 다시 작성하지 마십시오. 가능한 한 HttpClient를 재사용하십시오 ...

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

용법:

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}

-5

HttpClient 클래스를 사용하는 방법은 없습니다. 핵심은 환경과 제약 조건에 맞는 방식으로 응용 프로그램을 설계하는 것입니다.

HTTP는 퍼블릭 API를 공개해야 할 때 사용할 수있는 훌륭한 프로토콜입니다. RPC 메시지 대기열 패턴이 종종 내부 서비스에 더 적합한 선택이지만 경량의 짧은 대기 시간 내부 서비스에도 효과적으로 사용될 수 있습니다.

HTTP를 잘 수행하는 데는 많은 복잡성이 있습니다.

다음을 고려하세요:

  1. 소켓을 만들고 TCP 연결을 설정하려면 네트워크 대역폭과 시간이 사용됩니다.
  2. HTTP / 1.1은 동일한 소켓에서 파이프 라인 요청을 지원합니다. 이전 응답을 기다릴 필요없이 여러 요청을 차례로 보냅니다. 이는 블로그 게시물에서보고 한 속도 향상에 대한 책임이 있습니다.
  3. 캐싱 및로드 밸런서-서버 앞에로드 밸런서가있는 경우 요청에 적절한 캐시 헤더가 있는지 확인하면 서버의로드를 줄이고 클라이언트에 대한 응답을 더 빨리 얻을 수 있습니다.
  4. 리소스를 폴링하지 말고 HTTP 청크를 사용하여 주기적 응답을 반환하십시오.

그러나 무엇보다도 테스트, 측정 및 확인하십시오. 의도 한대로 작동하지 않으면 예상 결과를 얻는 방법에 대한 특정 질문에 답변 할 수 있습니다.


4
이것은 실제로 질문에 대답하지 않습니다.
whatsisname

하나의 올바른 방법이 있다고 가정합니다. 나는 생각하지 않습니다. 적절한 방식으로 사용하고 작동 방식을 테스트 및 측정 한 다음 행복해질 때까지 접근 방식을 조정해야합니다.
Michael Shaw

HTTP를 사용할지, 통신하지 않을지를 사용하는 것에 대해 약간 썼습니다. OP는 특정 라이브러리 구성 요소를 사용하는 가장 좋은 방법에 대해 질문했습니다.
whatsisname

1
@MichaelShaw : HttpClient구현 IDisposable합니다. 그러므로 그것을 정리하는 방법을 알고 있으며 using필요할 때마다 문장 을 감싸는 데 적합한 수명이 짧은 개체라고 기대하는 것은 무리 가 아닙니다. 불행히도, 그것은 실제로 작동하는 방식이 아닙니다. OP가 링크 된 블로그 게시물은 using명령문이 범위를 벗어 났고 HttpClient오브젝트가 삭제 된 후 오래 지속되는 자원 (특히 TCP 소켓 연결)이 있음을 분명히 보여줍니다 .
Robert Harvey

1
나는 그 사고 과정을 이해합니다. 아키텍처 관점에서 HTTP에 대해 생각하고 동일한 서비스에 많은 요청을 할 계획이라면 캐싱 및 파이프 라이닝에 대해 생각하고 HttpClient를 수명이 짧은 객체로 만드는 생각입니다. 단순히 잘못 느낀다. 마찬가지로, 다른 서버에 요청을하고 소켓을 활성 상태로 유지해도 아무런 이점이 없으면 HttpClient 객체를 사용한 후에 폐기하는 것이 좋습니다.
Michael Shaw
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.