¿Cuál es la sobrecarga de crear un nuevo HttpClient por llamada en un cliente WebAPI?

162

¿Cuál debería ser la HttpClientvida útil de un cliente WebAPI?
¿Es mejor tener una instancia de HttpClientpara múltiples llamadas?

¿Cuál es la sobrecarga de crear y eliminar una HttpClientsolicitud por solicitud, como en el ejemplo a continuación (tomado de http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from- a-net-client ):

using (var client = new HttpClient())
{
    client.BaseAddress = new Uri("http://localhost:9000/");
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    // New code:
    HttpResponseMessage response = await client.GetAsync("api/products/1");
    if (response.IsSuccessStatusCode)
    {
        Product product = await response.Content.ReadAsAsync<Product>();
        Console.WriteLine("{0}\t${1}\t{2}", product.Name, product.Price, product.Category);
    }
}
Bruno Pessanha
fuente
StopwatchSin embargo, no estoy seguro, podría usar la clase para compararla. Mi estimación sería que tiene más sentido tener una sola HttpClient, suponiendo que todas esas instancias se usen en el mismo contexto.
Matthew

Respuestas:

215

HttpClientha sido diseñado para ser reutilizado para múltiples llamadas . Incluso a través de múltiples hilos. El HttpClientHandlertiene credenciales y las galletas que están destinados a ser reutilizada a través de las llamadas. Tener una nueva HttpClientinstancia requiere volver a configurar todo eso. Además, la DefaultRequestHeaderspropiedad contiene propiedades destinadas a múltiples llamadas. Tener que restablecer esos valores en cada solicitud derrota el punto.

Otro beneficio importante de HttpClientes la capacidad de agregar HttpMessageHandlersa la tubería de solicitud / respuesta para aplicar preocupaciones transversales. Estos podrían ser para el registro, la auditoría, la limitación, el manejo de redireccionamiento, el manejo fuera de línea, la captura de métricas. Todo tipo de cosas diferentes. Si se crea un nuevo HttpClient en cada solicitud, entonces todos estos controladores de mensajes deben configurarse en cada solicitud y de alguna manera también se debe proporcionar cualquier estado de nivel de aplicación que se comparta entre las solicitudes de estos controladores.

Cuanto más utilice las funciones HttpClient, más verá que reutilizar una instancia existente tiene sentido.

Sin embargo, el mayor problema, en mi opinión, es que cuando HttpClientse elimina una clase, se elimina HttpClientHandler, lo que luego cierra por la fuerza la TCP/IPconexión en el conjunto de conexiones que administra ServicePointManager. Esto significa que cada solicitud con una nueva HttpClientrequiere restablecer una nueva TCP/IPconexión.

Según mis pruebas, usando HTTP simple en una LAN, el impacto en el rendimiento es bastante insignificante. Sospecho que esto se debe a que hay un keepalive TCP subyacente que mantiene la conexión abierta incluso cuando HttpClientHandlerintenta cerrarla.

En las solicitudes que pasan por Internet, he visto una historia diferente. He visto un impacto de rendimiento del 40% debido a que tengo que volver a abrir la solicitud cada vez.

Sospecho que el golpe en una HTTPSconexión sería aún peor.

Mi consejo es mantener una instancia de HttpClient durante toda la vida útil de su aplicación para cada API distinta a la que se conecte.

Darrel Miller
fuente
55
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManager¿Qué tan seguro está sobre esta declaración? Eso es difícil de creer. HttpClientme parece una unidad de trabajo que se supone que se instancia a menudo.
usr
2
@vkelman Sí, aún puede reutilizar una instancia de HttpClient incluso si la creó con un nuevo HttpClientHandler. También tenga en cuenta que hay un constructor especial para HttpClient que le permite reutilizar un HttpClientHandler y eliminar el HttpClient sin interrumpir la conexión.
Darrel Miller
2
@vkelman Prefiero mantener el HttpClient alrededor, pero si prefiere mantener el HttpClientHandler cerca, mantendrá la conexión abierta cuando el segundo parámetro sea falso.
Darrel Miller
2
@DarrelMiller Entonces parece que la conexión está vinculada al HttpClientHandler. Sé que para escalar no quiero destruir la conexión, así que necesito mantener un HttpClientHandler alrededor y crear todas mis instancias HttpClient a partir de eso O crear una instancia estática HttpClient. Sin embargo, si el CookieContainer está vinculado al HttpClientHandler, y mis cookies deben diferir según la solicitud, ¿qué recomienda? Me gustaría evitar la sincronización de subprocesos en un HttpClientHandler estático modificando su CookieContainer para cada solicitud.
Dave Black
2
@ Sana.91 Podrías. Sería mejor registrarlo como un singleton en la colección de servicios y acceder a él de esa manera.
Darrel Miller el
69

Si desea que su aplicación se amplíe, ¡la diferencia es ENORME! Dependiendo de la carga, verá números de rendimiento muy diferentes. Como menciona Darrel Miller, el HttpClient fue diseñado para ser reutilizado en todas las solicitudes. Esto fue confirmado por los chicos del equipo de BCL que lo escribieron.

Un proyecto reciente que tuve fue ayudar a un minorista informático en línea muy grande y conocido a escalar el tráfico del Viernes Negro / vacaciones para algunos sistemas nuevos. Nos encontramos con algunos problemas de rendimiento relacionados con el uso de HttpClient. Como se implementa IDisposable, los desarrolladores hicieron lo que normalmente harían al crear una instancia y colocarla dentro de una using()declaración. Una vez que comenzamos las pruebas de carga, la aplicación puso de rodillas al servidor; sí, el servidor no solo la aplicación. La razón es que cada instancia de HttpClient abre un puerto en el servidor. Debido a la finalización no determinista de GC y al hecho de que está trabajando con recursos informáticos que abarcan múltiples capas OSI , el cierre de puertos de red puede llevar un tiempo. De hecho, el propio sistema operativo WindowsPuede tomar hasta 20 segundos para cerrar un puerto (por Microsoft). Estábamos abriendo puertos más rápido de lo que podrían cerrarse: agotamiento del puerto del servidor que perjudicó la CPU al 100%. Mi solución fue cambiar el HttpClient a una instancia estática que resolvió el problema. Sí, es un recurso desechable, pero cualquier sobrecarga se ve ampliamente compensada por la diferencia en el rendimiento. Te animo a que hagas algunas pruebas de carga para ver cómo se comporta tu aplicación.

También puede consultar la página de guía de WebAPI para obtener documentación y ejemplos en https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

Presta especial atención a esta llamada:

HttpClient está destinado a ser instanciado una vez y reutilizado a lo largo de la vida de una aplicación. Especialmente en aplicaciones de servidor, crear una nueva instancia de HttpClient para cada solicitud agotará la cantidad de sockets disponibles bajo cargas pesadas. Esto provocará errores de SocketException.

Si encuentra que necesita usar una estática HttpClientcon diferentes encabezados, direcciones base, etc., lo que tendrá que hacer es crear el HttpRequestMessagemanual y establecer esos valores en el HttpRequestMessage. Luego, use elHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

ACTUALIZACIÓN para .NET Core : debe usar la IHttpClientFactoryinyección de dependencia vía para crear HttpClientinstancias. Administrará la vida útil por usted y no necesita deshacerse de él explícitamente. Consulte Realizar solicitudes HTTP utilizando IHttpClientFactory en ASP.NET Core

Dave Black
fuente
1
¡Esta publicación contiene información útil para aquellos que harán pruebas de estrés ...!
Sana.91
9

Como dicen las otras respuestas, HttpClientestá destinado a la reutilización. Sin embargo, la reutilización de una sola HttpClientinstancia en una aplicación multiproceso significa que no puede cambiar los valores de sus propiedades con estado, como BaseAddressy DefaultRequestHeaders(por lo que solo puede usarlas si son constantes en su aplicación).

Un enfoque para sortear esta limitación es envolver HttpClientcon una clase que duplica todos los HttpClientmétodos que necesita ( GetAsync, PostAsyncetc.) y los delega en un singleton HttpClient. Sin embargo, eso es bastante tedioso (también necesitará ajustar los métodos de extensión ), y afortunadamente hay otra forma : seguir creando nuevas HttpClientinstancias, pero reutilizando el subyacente HttpClientHandler. Solo asegúrese de no desechar el controlador:

HttpClientHandler _sharedHandler = new HttpClientHandler(); //never dispose this
HttpClient GetClient(string token)
{
    //client code can dispose these HttpClient instances
    return new HttpClient(_sharedHandler, disposeHandler: false)         
    {
       DefaultRequestHeaders = 
       {
            Authorization = new AuthenticationHeaderValue("Bearer", token) 
       } 
    };
}
Ohad Schneider
fuente
2
La mejor manera de hacerlo es mantener una instancia de HttpClient, y luego crear sus propias instancias locales de HttpRequestMessage y luego usar el método .SendAsync () en el HttpClient. De esta manera, seguirá siendo seguro para subprocesos. Cada HttpRequestMessage tendrá sus propios valores de autenticación / URL.
Tim P.
@TimP. por que es mejor SendAsynces mucho menos conveniente que los métodos dedicados como PutAsync, PostAsJsonAsyncetc.
Ohad Schneider
2
SendAsync le permite cambiar la URL y otras propiedades, como los encabezados y seguir siendo seguro para subprocesos.
Tim P.
2
Sí, el controlador es la clave. Mientras eso se comparta entre las instancias de HttpClient, está bien. Leí mal tu comentario anterior.
Dave Black el
1
Si mantenemos un controlador compartido, ¿aún tenemos que ocuparnos de un problema de DNS obsoleto?
shanti
5

Relacionado con sitios web de gran volumen pero no directamente con HttpClient. Tenemos el fragmento de código a continuación en todos nuestros servicios.

        // number of milliseconds after which an active System.Net.ServicePoint connection is closed.
        const int DefaultConnectionLeaseTimeout = 60000;

        ServicePoint sp =
                ServicePointManager.FindServicePoint(new Uri("http://<yourServiceUrlHere>"));
        sp.ConnectionLeaseTimeout = DefaultConnectionLeaseTimeout;

Desde https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2); k (DevLang-csharp) & rd = verdadero

"Puede usar esta propiedad para asegurarse de que las conexiones activas de un objeto de ServicePoint no permanezcan abiertas indefinidamente. Esta propiedad está destinada a escenarios en los que las conexiones deben interrumpirse y restablecerse periódicamente, como los escenarios de equilibrio de carga.

De manera predeterminada, cuando KeepAlive es verdadero para una solicitud, la propiedad MaxIdleTime establece el tiempo de espera para cerrar las conexiones de ServicePoint debido a la inactividad. Si ServicePoint tiene conexiones activas, MaxIdleTime no tiene efecto y las conexiones permanecen abiertas indefinidamente.

Cuando la propiedad ConnectionLeaseTimeout se establece en un valor distinto de -1, y después de que transcurre el tiempo especificado, se cierra una conexión de ServicePoint activa después de atender una solicitud estableciendo KeepAlive en falso en esa solicitud. Establecer este valor afecta a todas las conexiones administradas por el objeto ServicePoint ".

Cuando tiene servicios detrás de un CDN u otro punto final que desea conmutar por error, esta configuración ayuda a las personas que llaman a seguirlo a su nuevo destino. En este ejemplo, 60 segundos después de una conmutación por error, todas las personas que llaman deben volver a conectarse al nuevo punto final. Requiere que conozca sus servicios dependientes (aquellos servicios que USTED llama) y sus puntos finales.

Sin reembolsos Sin devoluciones
fuente
Todavía pone mucha carga en el servidor abriendo y cerrando conexiones. Si utiliza HttpClients basados ​​en instancias con HttpClientHandlers basados ​​en instancias, aún se encontrará con agotamiento de puertos si no tiene cuidado.
Dave Black
No estoy en desacuerdo. Todo es una compensación. Para nosotros, seguir un CDN o DNS redirigido es dinero en el banco versus pérdida de ingresos.
No hay reembolsos No hay devoluciones
1

También puede consultar esta publicación de blog de Simon Timms: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Pero HttpClientes diferente. Aunque implementa la IDisposableinterfaz, en realidad es un objeto compartido. Esto significa que debajo de las cubiertas es reentrante) e hilo seguro. En lugar de crear una nueva instancia de HttpClientpara cada ejecución, debe compartir una sola instancia de HttpClientdurante toda la vida útil de la aplicación. Veamos por qué.

SvenAelterman
fuente
1

Una cosa para señalar, que ninguno de los blogs de "no usar usando" nota es que no es solo la BaseAddress y DefaultHeader lo que debe tener en cuenta. Una vez que hace que HttpClient sea estático, hay estados internos que se llevarán a través de las solicitudes. Un ejemplo: se está autenticando a un tercero con HttpClient para obtener un token FedAuth (ignore por qué no usa OAuth / OWIN / etc.), ese mensaje de Respuesta tiene un encabezado Set-Cookie para FedAuth, esto se agrega a su estado HttpClient. El siguiente usuario que inicie sesión en su API enviará la cookie FedAuth de la última persona a menos que esté administrando estas cookies en cada solicitud.

escapismoc
fuente
0

Como primer problema, aunque esta clase es desechable, usarla con la usinginstrucción no es la mejor opción porque incluso cuando desecha el HttpClientobjeto, el socket subyacente no se libera de inmediato y puede causar un problema grave llamado 'agotamiento de sockets'.

Pero hay un segundo problema HttpClientque puede tener cuando lo usa como objeto único o estático. En este caso, un singleton o static HttpClientno respeta los DNScambios.

en .net core puedes hacer lo mismo con HttpClientFactory algo como esto:

public interface IBuyService
{
    Task<Buy> GetBuyItems();
}
public class BuyService: IBuyService
{
    private readonly HttpClient _httpClient;

    public BuyService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Buy> GetBuyItems()
    {
        var uri = "Uri";

        var responseString = await _httpClient.GetStringAsync(uri);

        var buy = JsonConvert.DeserializeObject<Buy>(responseString);
        return buy;
    }
}

Configurar servicios

services.AddHttpClient<IBuyService, BuyService>(client =>
{
     client.BaseAddress = new Uri(Configuration["BaseUrl"]);
});

documentación y ejemplo aquí

Reza Jenabi
fuente