Runtime: Singleton HttpClient no respeta los cambios de DNS

Creado en 29 ago. 2016  ·  77Comentarios  ·  Fuente: dotnet/runtime

Como se describe en la siguiente publicación: http://byterot.blogspot.co.uk/2016/07/singleton-httpclient-dns.html , una vez que comience a mantener una instancia HttpClient compartida para mejorar el rendimiento, Encontraré un problema en el que el cliente no respetará las actualizaciones de registros de DNS en caso de escenarios de conmutación por error.

El problema subyacente es que el valor predeterminado de ConnectionLeaseTimeout se establece en -1 , infinito. Solo se cerrará al deshacerse del cliente, lo cual es muy ineficiente.

La solución es actualizar el valor en el punto de servicio de esta manera:

var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar/baz/123?a=ab"));
sp.ConnectionLeaseTimeout = 60*1000; // 1 minute

Desafortunadamente, no hay forma de hacer esto con .NET Core hoy.

ServicePointManager debe transferirse a .NET Core o una funcionalidad equivalente similar debe habilitarse de alguna otra manera.

area-System.Net.Http enhancement

Comentario más útil

Esto es absolutamente un problema. La solución propuesta por @darrelmiller parece demostrar una falta de comprensión del caso de uso.

Usamos Azure Traffic Managers para evaluar el estado de una instancia. Trabajan sobre DNS con TTL cortos. Un host no sabe por arte de magia que no está en buen estado y comienza a emitir encabezados Connection:close . El administrador de tráfico simplemente detecta el estado incorrecto y ajusta la resolución de DNS en consecuencia. Por diseño, esto es COMPLETAMENTE TRANSPARENTE para clientes y hosts... tal como está, una aplicación con un HttpClient estático que resuelve DNS (sin saberlo) por parte de un administrador de tráfico nunca recibirá un nuevo host cuando el host resuelto anteriormente se vuelve inestable.

A partir de netcore 1.1, actualmente estoy buscando una resolución para este problema exacto. No veo uno, nativly de todos modos.

Todos 77 comentarios

No creo que sea la misma propiedad. Ese es el tiempo de espera de la conexión, no el ConnectionLeaseTimeout.

Como se documenta en https://msdn.microsoft.com/en-us/library/system.net.servicepoint.connectionleasetimeout.aspx , debe eliminarse periódicamente en algunos escenarios. Pero no hay forma aparente de cambiar el comportamiento en .NET Core.

@onovotny Sí, eso es algo extraño que parece hacer una conexión explícita

@darrelmiller así que eso está saliendo de mi área, no estoy seguro... pero si hay una solución documentada que aborde el problema original, sería un gran beneficio.

@onovotny , no hay una clase ServicePointManager en .NET Core. Esto suena como un problema de .NET Framework (Escritorio). Abra un problema en UserVoice o Connect.

@onovotny El problema original, según tengo entendido, es que, en el lado del servidor, alguien quiere actualizar una entrada de DNS para que todas las solicitudes futuras de host A vayan a una nueva dirección IP. En mi opinión, la mejor solución es que cuando se cambia esa entrada de DNS, se debe indicar a la aplicación en el host A que devuelva un encabezado Connection:close al cliente en todas las respuestas futuras. Eso obligará a todos los clientes a abandonar la conexión y establecer una nueva. La solución es no tener un valor de tiempo de espera en el cliente, por lo que el cliente envía una conexión: cerrar cuando expire ese tiempo de espera de "arrendamiento".

Esto encaja bien con la pregunta en Stackoverflow Unit que prueba la conmutación por error de DNS, usando un solucionador de DNS personalizado y puede brindar más contexto sobre este problema, por lo que cruzaré el enlace (lo eliminaré aquí si no es apropiado).

Esto es absolutamente un problema. La solución propuesta por @darrelmiller parece demostrar una falta de comprensión del caso de uso.

Usamos Azure Traffic Managers para evaluar el estado de una instancia. Trabajan sobre DNS con TTL cortos. Un host no sabe por arte de magia que no está en buen estado y comienza a emitir encabezados Connection:close . El administrador de tráfico simplemente detecta el estado incorrecto y ajusta la resolución de DNS en consecuencia. Por diseño, esto es COMPLETAMENTE TRANSPARENTE para clientes y hosts... tal como está, una aplicación con un HttpClient estático que resuelve DNS (sin saberlo) por parte de un administrador de tráfico nunca recibirá un nuevo host cuando el host resuelto anteriormente se vuelve inestable.

A partir de netcore 1.1, actualmente estoy buscando una resolución para este problema exacto. No veo uno, nativly de todos modos.

@ kudoz83 Tiene razón, un host no sabe mágicamente cuándo un administrador de tráfico ha ajustado la resolución de DNS, por eso dije

cuando se cambia esa entrada de DNS, se debe informar a la aplicación en el host A

Si no cree que sea práctico notificar a un servidor de origen que debe devolver conexión: cerrar encabezados, solo le quedan algunas opciones:

  • Cierre periódicamente las conexiones de un cliente o middleware y pague el costo de volver a abrir esas conexiones de forma redundante
  • Haga que el cliente sondee un servicio para saber cuándo debe restablecer las conexiones.
  • Use una pieza de middleware que sea lo suficientemente inteligente como para saber cuándo se ha cambiado el DNS.

Y con respecto a mí, no entiendo el caso de uso. El problema original se basó en una publicación de blog en la que el caso de uso estaba relacionado con el cambio entre los espacios de producción y montaje y no tenía nada que ver con la supervisión del estado. Aunque ambos escenarios podrían beneficiarse de poder recibir notificaciones de DNS/slot switches.

AFAIK, ¿no hay una manera de notificar a un host fallido que ha fallado? En general, esto significaría que se encuentra en algún tipo de estado de error. La publicación original habla de escenarios de conmutación por error.

Azure Traffic Manager se explica aquí https://docs.microsoft.com/en-us/azure/traffic-manager/traffic-manager-monitoring

Opera a nivel de DNS y es transparente para clientes y hosts, por diseño. Las instancias de Azure que se comunican entre sí a través de Traffic Manager para la resolución de DNS se encuentran en un aprieto sin poder establecer un TTL en la actualización de DNS para un HttpClient.

La forma en que HttpClient opera actualmente parece estar en desacuerdo con las propias ofertas de servicios de Azure de Microsoft, que es el punto central. Sin mencionar que cualquier servicio similar (por ejemplo, Amazon Route 53) funciona exactamente de la misma manera y tiene exactamente la misma dependencia breve de DNS TTL.

Obviamente encontré este hilo porque estoy afectado por el problema. Simplemente estoy tratando de aclarar que actualmente no parece haber una solución alternativa ideal que no sea simplemente recrear HttpClients arbitrariamente (a costa del rendimiento y la complejidad) para garantizar que la conmutación por error de DNS tenga éxito.

AFAIK, ¿no hay una manera de notificar a un host fallido que ha fallado?

Si un host "fallido" no pudiera ejecutar ningún código, entonces no necesitaríamos el código de estado HTTP 503 porque un servidor de origen nunca sería capaz de devolverlo. Hay escenarios en los que un host fallido puede no ser capaz de procesar una notificación, pero en esos casos probablemente tampoco mantendrá abierta una conexión TCP/IP activa durante mucho tiempo.

Cuando Ali mencionó originalmente este tema en Twitter, antes de escribir la publicación del blog, estaba concentrado en el hecho de que un cliente continuaría haciendo solicitudes y recibiendo respuestas de un servidor que había sido intercambiado.

Entiendo que su situación es diferente y que no puede confiar en que un servidor de origen pueda cerrar una conexión de manera confiable. Lo que no estoy seguro de entender es por qué, para su situación, no puede usar un HttpMessageHandler para detectar respuestas 5XX, cerrar la conexión y volver a intentar la solicitud. O incluso más simple, simplemente convenza a su servidor web para que devuelva la conexión: cierre cada vez que devuelva un código de estado 5XX.

También vale la pena señalar que el comportamiento que le preocupa no está en HttpClient. Está en el HttpHandler subyacente o incluso debajo de eso, y hay varias implementaciones diferentes de HttpHandler en uso. En mi opinión, el comportamiento actual es consistente con la forma en que HTTP está diseñado para funcionar. Estoy de acuerdo en que hay un desafío cuando la configuración de DNS se actualiza mientras las conexiones están abiertas, pero esto no es un problema de .net. Este es un problema de arquitectura HTTP. Eso no significa que .net no pueda hacer algo para que esto sea un problema menor.

Tengo curiosidad por saber cuál crees que debería ser la solución. ¿Cree que se debería volver a implementar algo como ConnectionLeaseTimeout que simplemente interrumpa una conexión periódicamente, independientemente de si ha habido una conmutación por error o no? ¿O tienes una solución mejor?

Lo siento,

No debí parecer tan combativo como soné ayer. Estaba teniendo un mal día.

Honestamente, no sé cómo funciona ConnectionLeaseTimeout debajo de las sábanas. La funcionalidad deseada sería que HttpClient fuera configurable para realizar una nueva búsqueda de DNS, antes de establecer nuevas conexiones en lugar de almacenar en caché la búsqueda de DNS anterior hasta que se elimine el cliente. No creo que esto requiera eliminar una conexión existente...

@kudoz83 No te preocupes, todos tenemos días así :-)

Por lo que entiendo, ConnectionLeaseTimeout es literalmente un temporizador que cierra periódicamente una conexión inactiva desde el lado del cliente. Se implementa en ServicePointManager, que no existe en el núcleo. Esto es diferente al tiempo de espera de "conexión inactiva" que solo cierra una conexión una vez que no se ha utilizado durante un cierto período de tiempo.

Es probable que haya una llamada Win32 que permita vaciar la memoria caché del cliente DNS. Eso aseguraría que las nuevas conexiones realicen una búsqueda de DNS. Lo busqué rápidamente pero no lo encontré, pero estoy seguro de que está allí en alguna parte.

En .net core en Windows, la gestión de las conexiones HTTP en realidad se deja en manos del sistema operativo. Lo más cercano que obtiene es la llamada a Win32 API WinHttpOpen https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs# L718 No hay API públicas que pueda encontrar para hacer algo con _sessionHandle. Para obtener el tipo de cambio de comportamiento que está buscando, sería necesario que la gente del sistema operativo hiciera cambios. Al menos así lo entiendo yo.

¿No sería más fácil obtener Azure Traffic Manager solo para establecer un TTL muy bajo en las entradas de DNS? Si no le preocupan las conexiones de larga duración, entonces un TTL bajo debería minimizar la posibilidad de que se configuren nuevas conexiones en un servidor inactivo.

La ventaja de la forma en que se construye WinHttpHandler es que obtenemos cosas como HTTP/2 gratis con la actualización del sistema operativo. La desventaja es que no tenemos mucho control en el código administrado.

@darrelmiller

[ConnectionLeaseTimeout] se implementa en ServicePointManager, que no existe en el núcleo.

Parece que ServicePoint.ConnectionLeaseTimeout regresará en .Net Standard 2.0/.Net Core 1.2. (Aunque no tengo idea si, por ejemplo, las conexiones WinHttpHandler se adherirán a él).

Creo que aquí hay una falta de comunicación: los TTL de DNS no tienen ninguna relación con la duración de la conexión HTTP/TCP.

Las nuevas conexiones HTTP realizan una búsqueda de DNS y se conectan a la dirección de host devuelta. Se pueden realizar varias solicitudes HTTP a través de la misma conexión única, pero una vez abierta, la conexión HTTP (y la conexión TCP subyacente) no vuelve a realizar una búsqueda de DNS.

Este es el comportamiento correcto, por lo que incluso si realiza miles de solicitudes a través de una conexión que dura días, la resolución de DNS no importa ya que la conexión no ha cambiado. Si realiza una nueva solicitud HTTP a través de una nueva conexión HTTP , verá una búsqueda de DNS para la nueva conexión.

Esta actualización de DNS no tiene nada que ver con HttpClient , que solo usa HttpClientHandler de manera predeterminada (que a su vez usa la pila de sockets HttpWebRequest ) y cualquier resolución de DNS es manejada por esa pila. ServicePointManager administra ServicePoint objetos en esa pila que son conexiones a un host específico. ServicePointManager tiene cierto mínimo de la caché del cliente de las búsquedas de DNS que se pueden manipular.

La configuración predeterminada está destinada a que las conexiones permanezcan abiertas indefinidamente para mayor eficiencia. Si el host conectado deja de estar disponible, la conexión se interrumpirá automáticamente, lo que generará una nueva conexión con una nueva búsqueda de DNS; sin embargo, los TTL de DNS no generarán nuevas conexiones. Si lo necesita, tendrá que codificarlo en su aplicación.

Si desea realizar otra búsqueda de DNS, debe restablecer la conexión en el cliente. Puede forzar una vida útil máxima de la conexión TCP, enviar un encabezado connection: close o simplemente desechar el HttpClient o el ServicePoint subyacente. También puede hacer la resolución de DNS con Dns.GetHostAddresses y usar la dirección IP en su lugar para sus solicitudes HTTP.

Espero que ayude, si estoy equivocado en esto, por favor hágamelo saber.

@manigandham

Creo que aquí hay una falta de comunicación: los TTL de DNS no tienen ninguna relación con la duración de la conexión HTTP/TCP.

Sí, los TTL de DNS no están relacionados, pero no del todo.

Las nuevas conexiones HTTP realizan una búsqueda de DNS y se conectan a la dirección de host devuelta.

Si, eso es correcto. Pero Windows tiene un caché de DNS local que respeta el TTL. Si el TTL es alto y el servidor DNS ha cambiado el registro DNS, la nueva conexión se abrirá con la dirección IP anterior debido al almacenamiento en caché del cliente del registro DNS. Vaciar la memoria caché DNS del cliente es la única solución que conozco.

Se pueden realizar varias solicitudes HTTP a través de la misma conexión única, pero una vez abierta, la conexión HTTP (y la conexión TCP subyacente) no vuelve a realizar una búsqueda de DNS.

Correcto de nuevo. En un intercambio de producción/escenario donde el servidor intercambiado sigue respondiendo, esto hará que muchas solicitudes futuras vayan al servidor antiguo, lo cual es malo. Sin embargo, la conversación más reciente ha sido sobre el escenario en el que falla un servidor y Traffic Manager cambia a un nuevo servidor. Se desconoce qué sucede con la conexión existente al servidor fallido. Si se trata de una falla grave de hardware, es muy probable que se corte la conexión. Eso obligaría al cliente a restablecer una conexión y ahí es donde un TTL bajo es útil. Sin embargo, si la falla del servidor no rompe el servidor y simplemente devuelve respuestas 5XX, entonces sería útil que el servidor devolviera la conexión: cierre para garantizar que las solicitudes futuras se realicen en una nueva conexión, con suerte para el nuevo funcionamiento. servidor

Esta actualización de DNS no tiene nada que ver con HttpClient, que solo usa HttpClientHandler de forma predeterminada (que a su vez usa la pila de sockets HttpWebRequest)

Si y no. En .Net core, HttpClientHandler se ha reescrito y ya no usa HttpWebRequest y, por lo tanto, no usa ServicePointManager. De ahí la razón de este problema.

Eché un vistazo más de cerca a IIS y no parece haber una manera de obtener respuestas 5XX para que vengan con conexión: cerrar encabezados. Sería necesario implementar un HttpModule para hacer esa magia.

Como FYI, aquí hay una publicación relacionada que cubre este problema y una solución provisional. Cuidado con HttpClient

Cualquier ServicePointManager debe transferirse a .NET Core

Si bien trajimos ServicePointManager y HttpWebRequest para la paridad de plataforma, no es compatible con la mayoría de las funciones en .Net Core, ya que la pila subyacente se resuelve en WinHTTP o Curl, que no exponen las perillas de control avanzadas.

El uso de ServicePointManager para controlar HttpClient es en realidad un efecto secundario del uso de HttpWebRequest y la pila HTTP administrada en .Net Framework. Dado que WinHttpHandler/CurlHandler se utilizan en .Net Core, SPM no tiene control sobre las instancias de HttpClient.

o una funcionalidad equivalente similar debe habilitarse de alguna otra manera.

Aquí está la respuesta del equipo de WinHTTP:

  1. Por razones de seguridad, mientras el cliente siga enviando solicitudes y _hay conexiones activas_, WinHTTP seguirá usando la IP de destino. No hay forma de limitar el tiempo que estas conexiones permanecen activas.
  2. Si el servidor cerró todas las conexiones al punto final remoto, se realizarán _nuevas_ conexiones consultando DNS y, por lo tanto, hacia la nueva IP.

Para forzar a los clientes a la siguiente IP, la única solución viable es asegurarse de que el servidor rechace las nuevas conexiones y cierre las existentes (ya mencionado en este hilo).
Esto es lo mismo con lo que entiendo de la documentación del

Teniendo en cuenta otros dispositivos de dispositivos de red que almacenan en caché DNS (por ejemplo, enrutadores domésticos/AP), no recomendaría la conmutación por error de DNS como una solución para la técnica de baja latencia y alta disponibilidad. Si eso es necesario, mi recomendación para una conmutación por error controlada sería tener un LB dedicado (hardware o software) que sepa cómo drenar y detener correctamente.

tal como está, una aplicación con un HttpClient estático que resuelve el DNS (sin saberlo) por un administrador de tráfico nunca recibirá un nuevo host cuando el host resuelto previamente se vuelve defectuoso.

Como se mencionó anteriormente, si por mal estado quiere decir que el servidor devuelve un código de error HTTP y cierra la conexión TCP mientras observa que expiró el TTL de DNS, esto es realmente un error.

Describa su escenario con más detalle:

  1. Durante la conmutación por error, ¿cuál es el caché de DNS y los TTL de la máquina cliente? Puede usar ipconfig /showdns y nslookup -type=soa <domain_name> para comparar lo que la máquina cree que es el TTL y los TTL autorizados del NS.
  2. ¿Hay conexiones activas hacia el servidor remoto? Puede usar netstat /a /n /b para ver las conexiones y los procesos que los poseen.
  3. Crear una reproducción nos ayudaría mucho: adjunte cualquier información disponible, como: versión de .Net Core/Framework, código para cliente/servidor, seguimientos de red, LB de DNS remoto/Administrador de tráfico involucrado, etc.

Si se conecta a una máquina y la conexión TCP/HTTP continúa funcionando, ¿por qué debería suponer en algún momento que el servidor ya no es apropiado?
Creo que hay margen de mejora en el lado de Azure. Por ejemplo, cuando las máquinas no están en buen estado o cambian de entorno, las conexiones existentes se pueden cerrar.

o simplemente desechar el ... subyacente ServicePoint

@manigandham ¿Cómo? No es desechable y el único método relevante que puedo ver es CloseConnectionGroup que realmente no puede usar ya que el nombre del grupo es un detalle de implementación (actualmente un hash del controlador). Además, no estoy seguro de qué sucedería si intentara desechar el ServicePoint mientras el HttpClient estaba usando (especialmente al mismo tiempo).

@ohadschn

.NET Core 2.0 recupera las clases y la funcionalidad de ServicePoint : https://docs.microsoft.com/en-us/dotnet/api/system.net.servicepoint?view=netcore-2.0

Puede volver a usar el tiempo de espera de la concesión de la conexión para establecer un período de tiempo máximo para las conexiones activas antes de que se restablezcan.

No hay nada exótico en cerrar la conexión mientras la solicitud de HttpClient está en curso, sería lo mismo que perder la conectividad al navegar por Internet. O la solicitud nunca lo hace o lo hace y no obtiene la respuesta; ambos deberían generar errores de http devueltos y tendrá que manejarlos en su aplicación.

Con la configuración del tiempo de espera de la concesión de la conexión, puede asegurarse de que esto se haga solo después de que se envíe una solicitud y se restablezca antes de que la siguiente supere el límite de tiempo.

.NET Core 2.0 recupera las clases y la funcionalidad de ServicePoint: https://docs.microsoft.com/en-us/dotnet/api/system.net.servicepoint?view=netcore-2.0
Puede volver a usar el tiempo de espera de la concesión de la conexión para establecer un período de tiempo máximo para las conexiones activas antes de que se restablezcan.

En realidad, no recupera la funcionalidad por completo. La superficie API volvió. Pero básicamente no es operativo ya que las pilas HTTP no usan ese modelo de objeto.

CC: @stephentoub @geoffkizer

Consideraría que muchos de los participantes en este hilo son una especie de "equipo de ensueño" en este tema, pero todavía no estoy seguro de si se ha llegado a un consenso sobre cuáles son las mejores prácticas dadas las que tenemos hoy. Sería genial obtener una respuesta decisiva de este grupo en un par de puntos clave.

Digamos que tengo una biblioteca HTTP de uso general dirigida a muchas plataformas, no todas las cuales admiten ServicePointManager , y estoy intentando proporcionar un comportamiento que administre las instancias de HttpClient inteligente, según las mejores prácticas, de manera predeterminada. No tengo idea de antemano a qué hosts se llamará, y mucho menos si se comportan bien con respecto a los cambios de DNS/ Conexión: cerrar encabezados.

  1. ¿Cómo administro de manera óptima las instancias de HttpClient ? ¿Uno por terminal? ¿Uno por host/esquema/puerto? ¿Literalmente un total de singleton? (Lo dudo, ya que los consumidores pueden querer aprovechar los encabezados predeterminados, etc.)

  2. ¿Cómo manejaría de manera más efectiva este problema de DNS? ¿Eliminar instancias después de 1 minuto? ¿Trátelo como "altamente improbable" y no proporcione ningún comportamiento de eliminación de forma predeterminada?

(Por cierto, no es una situación hipotética; estoy luchando con estas preguntas exactas en este momento).

¡Gracias!

Ni siquiera me invitarían a las pruebas del equipo de ensueño, pero tengo una idea con respecto a tu comentario de singleton HttpClient . Primero, siempre puede usar el método SendAsync para especificar diferentes encabezados y direcciones, por lo que podría envolver eso con su clase que proporciona datos compartidos similares a HttpClient (por ejemplo, encabezados predeterminados, dirección base) todavía termina llamando al mismo cliente único. Pero aún mejor, puede crear varias instancias de HttpClient que compartan el mismo HttpClientHandler : https://github.com/dotnet/corefx/issues/5811.

En cuanto al tema del DNS, el consenso aquí parece ser que:

Para forzar a los clientes a la siguiente IP, la única solución viable es asegurarse de que el servidor rechace las nuevas conexiones y cierre las existentes.

Si esto no es viable para usted, Darrel Miller sugirió algunas opciones más aquí: https://github.com/dotnet/corefx/issues/11224#issuecomment -270498159.

@ohadschn Gracias, y con suerte no me estoy

Con respecto al consejo de DNS, sentí que ahí es donde faltaba más consenso, y es comprensible. Asegurarse de que el servidor se comporte correctamente no es viable si no tiene control de lo que sucede en ese extremo. _podría_ diseñar algo donde las instancias se eliminen periódicamente. La pregunta es si eso vale la pena en la mayoría de los casos. Supongo que depende en gran medida de mí tomar una decisión. :)

Para mí, el beneficio más obvio de compartir el mismo HttpClient [ Handler ] es la simplicidad. No tendrá que mantener cosas como el mapeo entre hosts y clientes. No sabría sobre las cosas de TIME_WAIT, realmente tampoco mi experiencia.

Con respecto al DNS, mencionó un posible enfoque cuando no controla el servidor. También podría implementar un mecanismo que consulte periódicamente el DNS y elimine las instancias solo si se produce un cambio real...

Si bien las respuestas dadas que especifican lo que debería suceder en el lado del servidor son precisas para un mundo ideal, en el mundo real, los desarrolladores a menudo no tienen control de la pila completa del lado del servidor con la que se están comunicando. En ese sentido, me gusta mucho el concepto de ConnectionLeaseTimeout, ya que es una concesión a las realidades de la vida diaria de John y Jane Doeveloper.
Alguien sugirió escribir middleware para aplicar dicho tiempo de espera en .NET Core. ¿Haría el trabajo esta clase basada en NodaTime y System.Collections.Immutable?

    public class ConnectionLeaseTimeoutHandler : DelegatingHandler
    {
        private static string GetConnectionKey(Uri requestUri) => $"{requestUri.Scheme}://{requestUri.Host}:{requestUri.Port}";

        private readonly IClock clock;
        private readonly Duration leaseTimeout;
        private ImmutableDictionary<string, Instant> connectionLeases = ImmutableDictionary<string, Instant>.Empty;

        public ConnectionLeaseTimeoutHandler(HttpMessageHandler innerHandler, IClock clock, Duration leaseTimeout)
            : base(innerHandler)
        {
            this.clock = clock;
            this.leaseTimeout = leaseTimeout;
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string key = GetConnectionKey(request.RequestUri);
            Instant now = clock.GetCurrentInstant();

            Instant leaseStart = ImmutableInterlocked.GetOrAdd(ref connectionLeases, key, now);

            if (now - leaseStart > leaseTimeout)
            {
                request.Headers.ConnectionClose = true;
                ImmutableInterlocked.TryRemove(ref connectionLeases, key, out leaseStart);
            }

            return base.SendAsync(request, cancellationToken);
        }
    }

@snboisen Creo que se ve bien, pero no me

Para mí, la pregunta más importante es si enviar un encabezado Connection: close es una solución viable. Vale la pena repetir aquí lo que dijo @darrelmiller en los comentarios de la publicación del blog que generó este hilo:

ConnectionLeaseTimeout es una bestia extraña, de la que no estaba al tanto hasta que Oren lo mencionó hoy. Hará que las solicitudes salientes incluyan los encabezados Connection:Close después de que caduque el arrendamiento, lo que provocará que la conexión finalice después de que el servidor envíe su respuesta. Tiene sentido por qué esto está configurado en infinito de forma predeterminada porque es una forma extraña de forzar el cierre de las conexiones cuando un cliente está realizando solicitudes activamente.

Respeto la opinión de Darrel y hace que todo este enfoque parezca bastante truculento. Por otro lado, estoy de acuerdo en que ¿qué debe hacer un desarrollador que no tiene control de lo que sucede en el lado del servidor? Si funciona _a veces_, ¿quizás es la mejor opción que tenemos? Ciertamente es "más barato" que desechar periódicamente HttpClients o hacer búsquedas de DSN periódicamente.

¿Hay un ticket de Azure abierto para este problema?
Desde mi perspectiva, parece que todos están tratando de solucionar la forma en que esto se implementa en el administrador de tráfico de Azure.
No soy un usuario de Azure, así que lo miro desde la distancia, sintiéndome menos inclinado a pasar por sus aros.

@tmds No he visto uno, pero como otros han mencionado, esto se relaciona con algo más que Azure Traffic Manager: AWS tiene un concepto similar y está Azure App Service con su intercambio de espacios de ensayo/producción. Y apuesto a que otros proveedores de la nube también ofrecen algo similar a este mecanismo de intercambio.

@snboisen ¿Está diciendo que AWS y otros proveedores de la nube (¿cuáles?) también están utilizando tiempos de espera de DNS para implementar estas características?

@tmds Para AWS, sí, al menos para conmutación por error: https://aws.amazon.com/route53/faqs/#adjust_ttl_to_use_failover
No sé acerca de otros proveedores de nube, pero parece probable que algunos también lo hagan.

@tmds Para AWS, sí, al menos para conmutación por error: https://aws.amazon.com/route53/faqs/#adjust_ttl_to_use_failover
No sé acerca de otros proveedores de nube, pero parece probable que algunos también lo hagan.

Hay dos escenarios problemáticos aquí:

  • el cliente está conectado a un servidor que ya no está
  • el cliente está conectado a un servidor que ahora ejecuta una versión obsoleta del software

La conmutación por error de DNS está relacionada con la primera. Es apropiado eliminar servidores inalcanzables de DNS (en AWS, Azure, ...).
La forma correcta de detectarlo en el cliente es un mecanismo de tiempo de espera. No estoy seguro de si existe tal propiedad en HttpClient (tiempo de espera de respuesta máximo en una conexión establecida).
En el nivel de protocolo, los protocolos websocket y http2 más nuevos definen mensajes ping/pong para verificar el estado de la conexión.

Cuando sugiero abrir un problema con Azure, es para el segundo escenario.
No hay ninguna razón por la que un cliente deba cerrar una conexión que funcione. Si el servidor ya no es apropiado, los clientes deben desconectarse y el servidor debe volverse inalcanzable.
Encontré un buen documento sobre las implementaciones de AWS Blue/Green: https://d0.awsstatic.com/whitepapers/AWS_Blue_Green_Deployments.pdf.
Enumera varias técnicas. Para aquellos que usan DNS menciona _DNS TTL complexities_. Para aquellos que usan un balanceador de carga _sin complejidades de DNS_.

@tmds Gracias por el enlace, es muy interesante.

Me pregunto qué tan común es confiar en DNS TTL versus usar un balanceador de carga. Esta última parece una solución mucho más confiable en general.

Experimentó un comportamiento similar al trabajar con 2 aplicaciones de API diferentes en Azure App Services que se comunicaban a través de HttpClient . Notamos alrededor de 250 solicitudes en nuestras pruebas de carga, comenzamos a recibir tiempos de espera y "No se pudo establecer una conexión" HttpRequestException : no importaba si se trataba de usuarios simultáneos o solicitudes secuenciales.

Cuando cambiamos a un bloque using para garantizar que HttpClient se elimine en cada solicitud, no recibimos ninguna de estas excepciones. El impacto en el rendimiento, si lo hubo, fue insignificante desde un Singleton hasta la sintaxis using .

Cuando cambiamos a un bloque using para garantizar que HttpClient se elimine en cada solicitud, no recibimos ninguna de estas excepciones. El impacto en el rendimiento, si lo hubo, fue insignificante desde un Singleton hasta la sintaxis using .

No se trata necesariamente de rendimiento, se trata de no perder conexiones TCP y obtener SocketException s.

Desde MSDN :

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

Además, consulte https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

@khellang Gracias por el enlace (y la aclaración sobre los SocketException , definitivamente tuve un malentendido allí). Me encontré con esa publicación y otras 3 o 4 publicaciones que describen exactamente lo mismo.

Entiendo que la intención es una creación de una sola instancia, pero cuando la usamos en Azure App Services, constantemente alcanzamos HttpRequestExceptions con una carga bastante modesta (mencionada anteriormente, 250 solicitudes secuenciales). A menos que haya otra solución, desechar HttpClient es la solución alternativa que tuvimos que implementar para evitar encontrarnos con estas excepciones constantes.

Supongo que una solución "más correcta" sería detectar estas excepciones, cancelar todas las solicitudes pendientes, desechar explícitamente HttpClient , volver a crear instancias de HttpClient y volver a intentar todas las solicitudes.

Parece un poco complicado para algo que

está destinado a ser instanciado una vez y reutilizado a lo largo de la vida de la aplicación

Nos encantaría una solución más limpia, pero parece que no es algo malo con nuestro código, sino una limitación con el marco. Estoy más que feliz de estar equivocado, solo necesito alguna orientación sobre cómo podemos usar esto según lo previsto y no encontrar estos problemas.

@snboisen Probé su ConnectionLeaseTimeoutHandler y descubrí que no funciona con múltiples conexiones. Se emite un "cierre" para una sola conexión cada vez que se agota el tiempo de espera de la concesión, pero puede haber varias conexiones abiertas con una única instancia de HttpClient al enviar solicitudes en paralelo. Por lo que veo en Fiddler (uso una dirección IP incorrecta para generar errores para verificar el cambio de DNS) después de que se envía "cerrar", algunas solicitudes usan la nueva IP y algunas solicitudes usan la IP anterior. En mi caso, tengo un límite de conexión de 2 y se necesitan varias iteraciones del tiempo de espera de la concesión de la conexión para actualizar ambas conexiones. Obviamente, sería mucho peor con más conexiones simultáneas desde un servidor de producción, pero probablemente cambiaría después de un tiempo.

No conozco una forma de emitir "cerrar" a cada conexión individual en uso por una instancia de HttpClient. Eso sin duda sería ideal, pero lo que voy a hacer es usar un método de fábrica GetOrCreate() para HttpClient y crear una nueva instancia de HttpClient cada 120 segundos para garantizar que se creen todas las conexiones nuevas. Simplemente no estoy seguro de cómo deshacerse de la instancia antigua de HttpClient una vez que finalicen todas las solicitudes para cerrar las conexiones, pero creo que .NET se encarga de eso porque nunca ha sido un problema crear una nueva instancia de HttpClient por solicitud.

Como nota al margen, creo que la actualización debe ocurrir en el cliente. El servidor no puede ser responsable de comunicar los cambios de DNS ni ningún middleware. El propósito de DNS es compensar tal responsabilidad para que pueda cambiar las IP en cualquier momento sin coordinación entre un cliente y un servidor y no solo para proporcionar un nombre descriptivo para una IP. El caso de uso perfecto es una dirección IP elástica de AWS. El cambio solo se refleja en el DNS. AWS no sabrá enviar encabezados "cerrados" desde su servidor web que se ejecuta en una instancia ec2. AWS no estará al tanto de todos los servidores web que se encuentran encima de una instancia ec2 que administran. El escenario para el servidor anterior que no envía un encabezado "cerrado" es la implementación de un servidor nuevo donde se implementa exactamente el mismo software en un servidor nuevo con todo el tráfico cambiado por DNS. Nada debería cambiar en el servidor anterior, especialmente si tiene que volver a cambiar. Todo debe cambiarse sin problemas en un solo lugar con DNS.

Independientemente de los problemas más importantes discutidos aquí, ¿se ha implementado ahora ConnectionLeaseTimeout en el núcleo? Y si es así, ¿por qué no se cierra este tema? Pude codificar un ejemplo exactamente como la publicación original sin problemas en este momento. Ya sea que eso resuelva mis problemas de DNS de conexión o no, _parece_ que ConnectionLeaseTimeout _está_ implementado en el núcleo.

ServicePointMananager.ConnectionLeaseTimeout no está implementado en .NET Core. El uso de esta propiedad en .NET Core no es operativo. Es funcional solo en .NET Framework. Y de forma predeterminada, esta propiedad está configurada como "desactivada" en .NET Framework.

Entiendo que la intención es una creación de una sola instancia, pero cuando la usamos en Azure App Services, constantemente estamos accediendo a HttpRequestExceptions con una carga bastante modesta (mencionada anteriormente, 250 solicitudes secuenciales). A menos que haya otra solución, desechar HttpClient es la solución alternativa que tuvimos que implementar para evitar encontrarnos con estas excepciones constantes.

Hemos tenido éxito en remediar el problema de agotamiento del socket al envolver HttpClient en una API personalizada que permite que cada solicitud http en una aplicación reutilice la misma instancia de HttpClient (hace más que solo esto , como RestSharp pero este también es uno de los beneficios). Recuperar esto está detrás de un lock{} porque cada 60 minutos creamos un nuevo HttpClient y lo devolvemos en su lugar. Esto nos ha permitido realizar solicitudes http de gran volumen desde nuestra aplicación web (debido a las fuertes integraciones que tenemos) sin volver a utilizar las conexiones antiguas que no se han cerrado. Hemos estado haciendo esto durante aproximadamente un año con éxito (después de enfrentar los problemas de agotamiento del socket).

Estoy seguro de que todavía hay implicaciones de rendimiento en esto (especialmente debido a todo el bloqueo), pero es mejor que todo muera debido al agotamiento del zócalo.

@KallDrexx, ¿por qué necesita bloquear la instancia 'global'? Ingenuamente, lo almacenaría en estático y lo reemplazaría cada 60 minutos por uno nuevo. ¿No sería eso suficiente?

Nuestro código para obtener un HttpClient es:

```c#
var timeSinceCreated = DateTime.UtcNow - _lastCreatedAt;
if (timeSinceCreated.TotalSeconds > SecondsToRecreateClient)
{
bloquear (candado)
{
timeSinceCreated = DateTime.UtcNow - _lastCreatedAt;
if (timeSinceCreated.TotalSeconds > SecondsToRecreateClient)
{
_currentClient = new HttpClient();
_lastCreatedAt = DateTime.UtcNow;
}
}
}

        return _currentClient;

```

Entonces, supongo que el bloqueo solo ocurre una vez por minuto para que varios subprocesos no intenten crear un nuevo HttpApiClient simultáneamente. Pensé que estábamos bloqueando más agresivamente.

Ya veo, así que es una razón de conveniencia.
Alternativamente, puede usar la actualización basada en el temporizador (asincrónica), o permitir que se creen varios de ellos en paralelo (estilo de inicialización diferida que no es seguro para subprocesos), o usar un mecanismo de sincronización diferente y no bloquear los otros subprocesos durante el tiempo de creación (ellos podría reutilizar el anterior).

Ya veo, así que es una razón de conveniencia.

No sé si lo clasificaría como una comodidad.

Si demasiados subprocesos intentan crear un nuevo HttpClient al mismo tiempo, corremos el riesgo de que se agoten los sockets debido a que se crean demasiados cada minuto, especialmente si los sockets antiguos no se han limpiado correctamente en ese minuto. (provocando una cascada).

Además, todavía necesitaría alguna medida de bloqueo porque no sé si actualizar un DateTime es una operación atómica (sospecho que no) y, por lo tanto, algunos subprocesos pueden leer el _lastCreatedAt en en medio de una operación de actualización.

Y para que todos los subprocesos reutilicen el cliente anterior y, al mismo tiempo, un subproceso cree un nuevo cliente requiere mucha lógica compleja, por lo que tenemos la garantía de que un subproceso crea con éxito el nuevo cliente. Sin mencionar que todavía necesitamos bloqueos porque no podemos garantizar que un subproceso no devolverá _currentClient mientras otro subproceso lo está instanciando.

Hay demasiadas incógnitas y complejidad añadida como para arriesgarse a hacer eso en producción.

@KallDrexx

  • Parece que ReaderWriterLockSlim sería más adecuado para su uso.
  • DateTime asignación de x64 verdadero (porque contiene un solo campo ulong ). Tenga en cuenta que su código actual no es seguro para subprocesos si ese no es el caso, porque está observando el DateTime fuera del candado.
  • El enfoque basado en el temporizador sugerido por @karelz no requerirá un DateTime en absoluto... el temporizador simplemente cambiaría el HttpClient . Sin embargo, aún necesitaría rodear el campo HttpClient con alguna barrera de memoria ( volatile , Interlocked.Exchange etc.).

Parece que ReaderWriterLockSlim sería más adecuado para su uso.

Genial, no sabía de esa clase! Sin embargo, mencionar esto también me hizo pensar en usar un SemaphoreSlim , ya que es compatible con async/await (donde parece que ReaderWriterLockSlim no lo es), lo que puede ayudar con una posible contención de subprocesos cuando se requiere un nuevo HttpClient . Por otra parte, ReaderWriterLockSlim manejará mejor el posible problema de no atómica de DateTime que usted señaló correctamente, así que tendré que pensar en esto, ¡gracias!

El enfoque basado en el temporizador sugerido por @karelz no requerirá un DateTime en absoluto... el temporizador simplemente cambiaría el HttpClient. Sin embargo, aún necesitaría rodear el campo HttpClient con alguna barrera de memoria (volátil, Interlocked.Exchange, etc.).

Ah, había leído mal lo que quería decir. Eso tiene sentido, aunque necesito familiarizarme más con los mecanismos de barrera de la memoria para hacerlo correctamente.

Gracias.

Editar: solo para agregar, esta es una buena razón por la que creo que se debe construir algo para manejar esto, de modo que cada consumidor de HttpClient no tenga que volver a implementar/pasar por esto.

Me pregunto si sigue siendo un problema en .net core 2.0. ¿Todavía tenemos que preocuparnos por el cambio de DNS?

Me pregunto si sigue siendo un problema en .net core 2.0. ¿Todavía tenemos que preocuparnos por el cambio de DNS?

Tengo entendido que no es un problema con Dns en sí, solo que HttpClient intentará reutilizar las conexiones existentes (para evitar el agotamiento del socket) y, por lo tanto, incluso cuando cambia el DNS, todavía se está conectando a la antigua servidor porque está repasando la conexión anterior.

@KallDrexx , ¿significa que todavía tendrá problemas para la implementación azul-verde?

@withinoneyear según tengo entendido, sí. Si crea un HttpClient y lo utiliza contra Production Green, luego cambia la producción a Blue, el HttpClient seguirá teniendo una conexión abierta con Green hasta que se cierre la conexión.

Sé que esto es antiguo, pero creo que no debe usar DateTime.UtcNow para la medición de intervalos de tiempo. Use una fuente de tiempo monitoreada como Stopwatch.GetTimestamp. De esta forma, es inmune al ajuste de la hora local por parte del operador oa la sincronización horaria. Hubo algunos conjuntos de chips defectuosos que hicieron retroceder los valores de QPC, pero no creo que muchos de ellos estén en uso ahora.

var sp = ServicePointManager.FindServicePoint(nuevo Uri("http://foo.bar/baz/123?a=ab"));
sp.ConnectionLeaseTimeout = 60*1000; // 1 minuto

Este código se coloca en el constructor?

@whynotme8998 @lundcm Creo que no. Si su HttpClient es singleton y accede a múltiples puntos, el constructor no sería el lugar correcto para colocarlo.
Compruebe esta implementación . También en el README del proyecto hay un enlace a una publicación de blog que explica el problema. Ese artículo también apunta a este hilo.

¿Puede alguien ayudarme a entender exactamente qué significa ConnectionLeaseTimeout ?

Si en mi aplicación lo configuro para 1 minuto, ¿eso significa que cada minuto se agotará el tiempo de arrendamiento de la conexión? ¿Qué significa exactamente eso para hablar con mi back-end?

Veo que la gente está hablando de la actualización de DNS en el back-end. En una aplicación web estándar de Azure, ¿la publicación de mi back-end ASP.NET provocaría una actualización de DNS?

¿Un tiempo de espera de 1 minuto no dejaría potencialmente una ventana en la que el DNS se actualizó pero el tiempo de espera aún no se ha producido?

Editar. Darse cuenta de que esto no funciona en .NET Standard 2.0. ¿Cuál es el trabajo?

Esperando ese https://www.stevejgordon.co.uk/introduction-to-httpclientfactory-aspnetcore o implementando partes de él mismo.

¡Cambiar ConnectionLeaseTimeout no funcionó en Xamarin con .netstandard 2.0!

@paradisehuman Supongo que te refieres a ServicePoint.ConnectionLeaseTimeout . AFAIK tampoco funciona en .NET Core. En .NET Core 2.1 presentamos SocketsHttpHandler.PooledConnectionLifetime . Además, el nuevo HttpClientFactory en ASP.NET puede aprovechar ambos y hace aún más por usted.
Mono/Xamarin tiene su propia implementación de pila de redes (no consumen el código fuente de CoreFX para redes), por lo que sería mejor registrar el error en su repositorio con más detalles.

Desafortunadamente, no hay forma de hacer esto con .NET Core hoy. ServicePointManager debe transferirse a .NET Core o una funcionalidad equivalente similar debe habilitarse de alguna otra manera.

@onovotny , SocketsHttpHandler en .NET Core 2.1 expone PooledConnectionLifetime, que tiene un propósito similar a ConnectionLeaseTimeout, solo en el nivel del controlador. ¿Podemos considerar este tema abordado en este punto?

@stephentoub acaba de ver esto ahora, sí, creo que PooledConnectionLifetime debería abordarlo. La comunidad puede abrir otro problema si todavía se necesita algo.

@onovotny @karelz @stephentoub ,

¿Estarían todos de acuerdo en que este es un resumen preciso de la mejor manera de resolver el problema original de HttpClient/DNS singleton?

  • En .NET Framework 2.0+, use ServicePoint.ConnectionLeaseTimeout .

  • En .NET Core 2.1, use SocketsHttpHandler.PooledConnectionLifetime .

  • Para todas las demás plataformas/objetivos/versiones, no existe una solución/truco/solución alternativa fiable, así que ni se moleste en intentarlo.

A medida que sigo aprendiendo cosas de este subproceso y otros (como ConnectionLeaseTimeout no funciona de manera confiable en todas las plataformas en .NET Standard 2.0), estoy de vuelta en el tablero de dibujo tratando de resolver esto desde una biblioteca de una manera que funciona en tantas plataformas/versiones como sea posible. Olfatear plataformas está bien. Eliminar/recrear periódicamente HttpClient está bien, si es efectivo. Estoy bastante seguro de que me equivoqué en mi primer intento, que fue simplemente enviar un encabezado connection:close al host a intervalos regulares. ¡Gracias de antemano por cualquier consejo!

@tmenier Para todas las demás plataformas/objetivos/versiones, no existe una solución/truco/solución alternativa confiable, así que ni se moleste en intentarlo.

Siempre puede reciclar la instancia de forma regular (por ejemplo, simplemente cree una nueva instancia y configúrela en una variable estática que otros usan).
Eso es lo que hace HttpClientFactory . Puede elegir usar HttpClientFactory lugar.

@karelz Gracias, creo que esto me proporciona un camino a seguir. ¿Quizás debería omitir la detección de la plataforma y reciclar instancias en todas partes, o es recomendable usar los otros enfoques donde estén disponibles (para evitar gastos generales, etc.)?

La parte complicada podría ser saber _cuándo_ es seguro desechar instancias recicladas (debo preocuparme por la concurrencia/solicitudes pendientes), pero puedo echar un vistazo a cómo HttpClientFactory ocupa de eso. Gracias de nuevo.

@tmenier Usted no quiere a instancias de reciclaje cada HttpClient llamada, como que dará lugar a tcp agotamiento zócalo. El reciclaje debe administrarse (tiempo de espera o mantenimiento de un grupo con HttpClient s que pueden caducar).

@KallDrexx Correcto, solo estoy hablando de reciclar _periódicamente_ un singleton (o "pseudo"-singleton), como cada pocos minutos. Creo que implicará administrar un grupo y algún tipo de eliminación retrasada de instancias inactivas, una vez que se determine que no tienen solicitudes pendientes.

Ah, lo siento, no entendí bien lo que querías decir con reciclar.

No debería necesitar deshacerse de los viejos. Deje que GC los recopile.
El código debe ser tan simple como configurar un nuevo "pseudo"-singleton periódicamente según el temporizador. Nada mas.

Descubrí que no desecharlos puede provocar ralentizaciones a medida que se quedan sin conexiones http en escenarios de alto rendimiento.

@tmenier Acerca del uso de SocketsHttpHandler.PooledConnectionLifetime en En .NET Core 2.1, la creación de HttpClientHandler está completamente encapsulada en https://github.com/dotnet/wcf/blob/master/src/System.Private.ServiceModel/src/System/ ServiceModel/Channels/HttpChannelFactory.cs. ¿Hay alguna manera de hacer esto cuando se usa el cliente WCF?

@edavedian Sugeriría preguntar en dotnet/wcf repo. cc @mconnew @Lxiamail

la pila subyacente se resuelve en WinHTTP o Curl que no exponen las perillas de control avanzadas

¿Hay en .Net Core un cliente HTTP que funcione sobre sockets?

¿Hay en .Net Core un cliente HTTP que funcione sobre sockets?

Sí, SocketsHttpHandler y, a partir de la versión 2.1, es el valor predeterminado.

¿ HttpClient almacena en caché los URI que golpea, de manera similar a lo que creo que hace RestClient ? Si es así, hágame saber en qué versión (.NET Core 3.0/.NET Framework 3.0, etc.) es compatible.

Veo que el almacenamiento en caché solo se realiza en RestClient si el proyecto es una aplicación .Net Core 2.1+.

¿HttpClient almacena en caché los URI que golpea, de manera similar a lo que creo que hace RestClient?

HttpClient no realiza ningún almacenamiento en caché.

@SpiritBob no está claro a qué forma de almacenamiento en caché te refieres. Este problema está cerrado; Si por favor abre un nuevo número con preguntas, podemos ayudarlo a encontrar respuestas.

@scalablecory Creo que davidsh respondió con lo que tenía en mente. ¡Gracias!

Acabo de terminar de leer este hilo y parece que hubo un caso de uso que no se abordó.

Al usar HttpClientFactory , los HttpClient resultantes cerrarán automáticamente las conexiones y realizarán una nueva búsqueda de DNS cuando reciban un HttpRequestException con un SocketException interno que indica que el host ya no es válido y ese DNS ha cambiado potencialmente (código de error 11001: no se conoce tal host)?

Si no, ¿cuál es la solución para que HttpClient realice búsquedas de DNS antes de que caduque el TTL en los registros de DNS?

Actualmente estoy usando HttpClientFactory con un HttpMessageHandler (configurado mediante inyección de dependencia usando AddHttpClient<T, U>().ConfigurePrimaryHttpMessageHandler(...) ) y cuando un registro DNS cambia antes de que caduque el TTL, se produce un error. Específicamente, veo que sucede cuando hago un montón de cargas en Azure Blob Storage.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

bencz picture bencz  ·  3Comentarios

v0l picture v0l  ·  3Comentarios

jchannon picture jchannon  ·  3Comentarios

EgorBo picture EgorBo  ·  3Comentarios

yahorsi picture yahorsi  ·  3Comentarios