La biblioteca Fasthttp es una alternativa acelerada a net / http de los paquetes estándar de Golang.
¿Cómo se arregla? ¿Por qué es tan rápida?
Traigo a su atención una transcripción del informe de los clientes internos de Alexander Valyalkin Fasthttp.
Los patrones Fasthttp se pueden usar para acelerar sus aplicaciones, su código.
A quién le importa, bienvenido al gato.
Soy Alexander Valyalkin Yo trabajo en VertaMedia. Desarrollé fasthttp para nuestras necesidades. Incluye la implementación del cliente http y el servidor http. Fasthttp es mucho más rápido que net / http de los paquetes Go estándar.

Fasthttp es una implementación rápida del servidor http y el cliente. Ubicado fasthttp en github.com

Creo que muchos han escuchado sobre el servidor fasthttp, que es muy rápido. Pero pocos han oído hablar del cliente fasthttp. El servidor Fasthttp participa en el benchmark de techempower , el famoso benchmark en círculos estrechos para servidores http. El servidor Fasthttp participa en las rondas 12 y 13. La ronda 13 aún no ha salido (en 2016 - aprox. Ed.).

Los resultados de una de las pruebas de la ronda 12, donde fasthttp está casi en la cima. Los números muestran cuántas consultas hace por segundo en esta prueba. En esta prueba, se realiza una solicitud para una página que devuelve hello world. En hello world fasthttp es muy rápido.

Resultados preliminares de la próxima ronda, que aún no se ha publicado (en 2016 - aprox. Ed.). 4 implementaciones fasthttp ocupan el primer lugar en el punto de referencia, que no solo regala hello world, sino que también se arrastra a la base de datos y forma una página html basada en la plantilla.

Muy pocas personas saben sobre el cliente fasthttp. Pero en realidad también es genial. En este informe, le contaré sobre el dispositivo interno fasthttp client y por qué fue desarrollado.

En realidad, hay varios clientes en fasthttp: Client, HostClient y PipelineClient. Además, te contaré más sobre cada uno de ellos.

Fasthttp.Client es un cliente http de uso general regular. Con él, puede realizar solicitudes a cualquier sitio de Internet, recibir respuestas. Sus características: funciona rápidamente, puede limitar la cantidad de conexiones abiertas por host, a diferencia del paquete net / http. La documentación está en https://godoc.org/github.com/valyala/fasthttp#Client .

Fasthttp.HostClient es un cliente especializado para comunicarse con un solo servidor. Por lo general, se utiliza para acceder a la API HTTP: API REST, API JSON. También se puede usar para proxy del tráfico de Internet a un DataCenter interno en varios servidores. La documentación está aquí: https://godoc.org/github.com/valyala/fasthttp#HostClient .
Al igual que Fasthttp.Client, Fasthttp.HostClient puede limitar el número de conexiones abiertas a cada uno de los servidores Backend. Esta funcionalidad está ausente en net / http, y también esta función está ausente en nginx gratis. Esta funcionalidad es solo en nginx pagado, que yo sepa.

Fasthttp.PipelineClient es un cliente especializado que le permite administrar solicitudes de canalización a un servidor o a un número limitado de servidores. Se puede utilizar para acceder a la API, a través del protocolo HTTP, donde necesita realizar muchas solicitudes y lo más rápido posible. La limitación de Fasthttp.PipelineClient es que puede sufrir el bloqueo de Head of Line. Esto es cuando enviamos muchas solicitudes al servidor y no esperamos una respuesta a cada solicitud. El servidor está bloqueado en una de estas solicitudes. Debido a esto, todas las demás solicitudes que lo siguieron esperarán hasta que este servidor procese una solicitud lenta. Fasthttp.PipelineClient debe usarse solo si está seguro de que el servidor responderá instantáneamente a sus solicitudes. Documentación

Ahora comenzaré a hablar sobre la implementación interna de cada uno de estos clientes. Comenzaré con Fasthttp.HostClient, porque casi todos los demás clientes se crean sobre la base.

Esta es la implementación más simple del cliente HTTP en pseudocódigo en Go. Estamos conectados, recibimos una respuesta http en esta URL. Nos estamos conectando a este host. Tenemos conexión En este código, para que sea menor que el volumen, faltan todas las comprobaciones de errores. De hecho, esto no es así. Siempre debe verificar si hay errores. Crea una conexión. Conexión cercana con aplazar. Enviamos una solicitud de esta conexión por URL. Recibimos la respuesta, devolvemos esta respuesta. ¿Qué hay de malo con esta implementación de HTTP Client?

El primer problema es que en esta implementación, se establece una conexión para cada solicitud. Esta implementación no es compatible con HTTP KeepAlive. ¿Cómo resolver este problema? Puede usar el Pool de conexiones para cada servidor. No puede usar el Pool de conexiones para todos los servidores, porque la siguiente solicitud no está clara a qué servidor enviar. Cada servidor debe tener su propio grupo de conexiones. Y usamos HTTP KeepAlive. Esto significa que Connection Header no necesita especificar Connection Close. En HTTP / 1.1, de forma predeterminada, existe soporte para HTTP KeepAlive y Connection Close debe eliminarse del encabezado. Aquí está la implementación en pseudocódigo del cliente con soporte de Connection Pool. Hay un conjunto de varios grupos de conexiones para cada host. La primera función, connPoolForHost, devuelve el Pool de conexiones para un host determinado desde una URL determinada. Luego obtenemos la conexión de este Pool de conexiones, planeamos usar Defer para enviar esta conexión de regreso al Pool, enviar una solicitud KeepAlive para esta conexión y devolver una respuesta. Después de la respuesta, se ejecuta Defer y la conexión vuelve al Pool. Por lo tanto, habilitamos el soporte HTTP KeepAlive y todo comienza a funcionar más rápido. Porque no perdemos tiempo creando una conexión para cada solicitud.
Pero la solución también tiene problemas. Si observa la firma de la función, puede ver que devuelve un objeto de respuesta para cada solicitud. Esto significa que para este objeto necesita asignar memoria cada vez, inicializarlo y devolverlo. Esto es malo para el rendimiento. Puede ser malo si tiene muchas de esas llamadas para obtener funciones.

Por lo tanto, este problema puede resolverse como se resuelve en Fasthttp colocando el objeto puntero en el objeto de respuesta en los parámetros de esta función. De esa manera, ese código de llamada puede reutilizar este objeto de respuesta muchas veces. En la diapositiva está la implementación de esta idea. Pasamos una referencia al objeto de respuesta a la función Get, y la función llena esta respuesta. La última línea llena este objeto.

Así es como podría verse en su código. Una función que acepta un canal al que se le pasa una lista de URL para sondear. Organizaremos un ciclo en este canal. Creamos un objeto de respuesta una vez y lo reutilizamos en un bucle. Llame a Get, pase un puntero al objeto, procese esta respuesta. Después de haberlo procesado, lo restablecemos a su estado original. De esta manera evitamos la asignación de memoria y aceleramos nuestro código.

El tercer problema es la conexión cercana. Conexión cerrada: encabezado HTTP, que se puede encontrar tanto en la solicitud como en la respuesta. Si tenemos ese encabezado, entonces esta conexión debería cerrarse. Por lo tanto, en la implementación del cliente, es imprescindible proporcionar un cierre de conexión. Si envió una solicitud con el encabezado Cerrar conexión, luego de recibir la respuesta, debe cerrar esta conexión. Si envió una solicitud sin Cerrar conexión y devolvió una respuesta con Cerrar conexión, también debe cerrar esta conexión después de recibir una respuesta.

Aquí está el pseudocódigo para esta implementación. Después de recibir una respuesta, verificamos si los encabezados de cierre de conexión están instalados allí. Si está instalado, simplemente cierre la conexión. Si no está instalado, regrese la conexión al grupo. Si no se hace esto, entonces si el servidor cierra la conexión después de que devuelve las respuestas, su grupo de conexiones contendrá la conexión interrumpida que cerró el servidor, e intentará escribir algo en ellas y obtendrá errores.

El cuarto problema al que están expuestos los clientes HTTP es servidores lentos o una red lenta e inactiva. Los servidores pueden dejar de responder a sus solicitudes por varias razones. Por ejemplo, el servidor está roto o la red entre su cliente y el servidor ha dejado de funcionar. Debido a esto, todas sus rutinas que llaman a la función Obtener que se describió previamente serán bloqueadas, esperando una respuesta del servidor indefinidamente. Por ejemplo, si implementa un proxy http que acepta una conexión entrante y llama a la función Get en cada conexión, se creará una gran cantidad de goroutines y todos se colgarán en su servidor hasta que el servidor falle, hasta que se agote la memoria.

¿Cómo resolver este problema? Hay una decisión tan ingenua que se me viene a la mente: simplemente envuélvela en una rutina diferente. Luego, en goroutine, pase un canal vacío, que se cerrará después de ejecutar Get. Después de comenzar esta rutina, espere un momento en este canal (tiempo de espera). En este caso, si pasa algún tiempo y este Get no se ejecuta, la salida de esta función se producirá por tiempo de espera. Si se ejecuta este Get, el canal se cerrará y se producirá la salida. Pero esta decisión es incorrecta, porque transfiere el problema de una cabeza enferma a una sana. De todos modos, se crearán gorutinas y se suspenderán independientemente del tiempo de espera que use. El número de goroutines que causó Get timeout será limitado, pero habrá un número ilimitado de goroutines que se crearán dentro de Get con un tiempo de espera.

¿Cómo resolver este problema? La primera solución es limitar el número de gorutinas bloqueadas en la función Obtener. Esto se puede hacer usando un patrón tan conocido como el uso de un canal protegido de longitud limitada, que contará el número de goroutines que ejecutan la función Get. Si esta cantidad de gorutina excede un cierto límite: la capacidad de este canal, saldremos a la rama predeterminada. Esto significa que tenemos todas las rutinas que se realizan están ocupadas, y en la rama predeterminada solo necesitamos devolver Error, que no hay recursos libres. Antes de crear goroutine, tratamos de escribir una estructura vacía en este canal. Si esto no funciona, entonces hemos excedido la cantidad de gorutinas. Si resultó, creamos este gorutin y después de ejecutar Get, leemos un valor de este canal. Por lo tanto, limitamos la cantidad de goroutines que se pueden bloquear en Get.

La segunda solución, que complementa la primera, es establecer tiempos de espera en la conexión al servidor. Esto desbloqueará la función get si el servidor no responde durante mucho tiempo o si la red está inactiva.
Si la red no funciona en la Solución n. ° 1, entonces todo se bloqueará. Después de escribir cuncurrency en un número limitado de goroutines que colgaban aquí, la función getimeout siempre devolverá un error. Para que comience a funcionar normalmente, necesita una segunda solución (Solución # 2), que establece un tiempo de espera para leer y escribir desde la conexión. Esto ayuda a desbloquear goroutines bloqueados si la red o el servidor dejan de funcionar.

La solución # 1 tiene una carrera de datos. El objeto de respuesta desde el que se pasó el puntero se ocupará si se bloquea. Pero esta función Obtener tiempo de espera puede expirar. En este caso, salimos de esta función, una respuesta que se bloqueará y después de un tiempo se reescribirá. Por lo tanto, se obtiene una carrera de datos. Como tenemos respuesta después de salir de la función, todavía se usa en algún lugar de la rutina.
El problema se resuelve creando una copia de respuesta y pasando la copia de respuesta a goroutine. Después de completar Get, copie la respuesta de esta respuesta en nuestra respuesta original, que se pasa aquí. Por lo tanto, la carrera de datos está resuelta. Esta copia de la respuesta dura poco tiempo y regresa al grupo. Reutilizamos la respuesta. Es posible que una copia de respuesta no entre en el grupo solo por tiempo de espera. Por tiempo de espera, hay una pérdida de respuesta del grupo.

¿Necesito cerrar la conexión después de que el servidor no haya devuelto una respuesta dentro de un tiempo de espera? La respuesta es no. Más bien, sí, si desea hacer una copia de seguridad del servidor. Porque cuando envía una solicitud al servidor, espere un momento, el servidor no responde durante este tiempo, no responde a las solicitudes. Por ejemplo, cierra esta conexión, pero esto no significa que el servidor dejará de ejecutar esta solicitud de inmediato. El servidor continuará ejecutándolo. El servidor detectará que no es necesario ejecutar esta solicitud después de que intente devolverte una respuesta. Cerró la conexión, intentó nuevamente crear una nueva solicitud, nuevamente pasó el tiempo de espera, volvió a cerrar, creó una nueva solicitud. Tendrá una carga en el aumento del servidor. Como resultado, su servicio depende de sus solicitudes. Estos son DoS a nivel de solicitudes http. Si tiene servidores que se ejecutan lentamente y no desea hacer una copia de seguridad de ellos, entonces no necesita cerrar la conexión después de un tiempo de espera. Debe esperar un momento, dejar la conexión para expiar este servidor. Deja que intente darte una respuesta. Mientras tanto, use otras conexiones gratuitas. Todo lo que se dijo antes de esto son todas las etapas de la implementación de Fasthttp.Client y los problemas que ocurrieron durante la implementación de Fasthttp.Client. Estos problemas se resuelven en Fasthttp.HostClient.
Ahora tenemos un cliente rápido? En realidad no Necesita ver cómo se implementa Connection Pool.

La implementación ingenua de Connection Pool se ve así. Hay algún tipo de dirección de servidor donde necesita instalar la conexión. Hay una lista de conexiones libres y un bloqueo para sincronizar el acceso a esta lista.

Aquí está la función para obtener la conexión del grupo de conexiones. Estamos viendo una lista de nuestra colección. Si hay algo allí, entonces obtenemos una conexión gratuita y la devolvemos. Si no hay nada, cree una nueva conexión a este servidor y devuélvala. ¿Qué está mal aquí?
La función connPool.Put devuelve una conexión libre.
En la cuenta de tiempo de espera. En Fasthttp.Client, puede especificar la vida útil máxima de una conexión abierta no utilizada. Una vez transcurrido este tiempo, las conexiones no utilizadas se cierran automáticamente y se eliminan de este grupo.
Las conexiones más antiguas no se utilizan con el tiempo y se cierran y eliminan automáticamente del grupo.
Cuando la conexión se toma del grupo, y resulta que su servidor estaba cerrado, e intentó escribir algo allí, se realiza un segundo intento: se obtiene una nueva conexión e intenta enviar nuevamente las solicitudes para esta conexión. Pero esto es solo si esta solicitud es idempotente, es decir, una solicitud que se puede ejecutar muchas veces sin efectos secundarios en el servidor, es una solicitud GET o HEAD. Por ejemplo, en el estándar net / http, justo ahora, agregamos una verificación para conexiones cerradas. Allí hicieron un chequeo más complicado. Comprueban, cuando intentan enviar una nueva solicitud a la conexión desde el grupo, si se envía al menos un byte a esta conexión. Si se desactiva, entonces devuelve Error. Si no se fue, tomamos una nueva conexión del grupo.

¿Qué hay de malo en la piscina? Su tamaño no está limitado. Misma implementación que en net / http. Si escribe un cliente que está pasando de millones de gorutinas a un servidor lento, entonces el cliente intentará crear una conexión de millones a este servidor. No hay límite en el número máximo de conexiones en el paquete estándar net / http. Para el cliente que se utiliza para acceder a la API a través de HTTP, es aconsejable limitar el tamaño de este grupo de conexiones. De lo contrario, sus clientes pueden fallar, ya que utilizará todos los recursos: hilos, objetos, conexión, rutinas y memoria. Además, esto puede conducir a DoS de sus servidores, ya que se establecerá una gran cantidad de conexiones con ellos, que no se usan o se usan de manera ineficiente, porque el servidor no puede mantener tanta conexión.

Limite el grupo de conexiones. El código no está aquí, porque es demasiado grande para caber en una diapositiva. Los interesados pueden ver la implementación de esta función en github.com.

El segundo problema Muchas solicitudes llegan al cliente en algún momento. Y después de eso hay una disminución y un retorno al número anterior de solicitudes. Por ejemplo, 10,000 solicitudes llegaron simultáneamente, luego el número de solicitudes regresó a 1000 por unidad de tiempo. Después de eso, el grupo de conexiones crecerá a 10000 conexiones. Estas conexiones colgarán allí sin cesar. Este problema estaba en el cliente net / http estándar anterior a la versión 1.7. Por lo tanto, debe resolver este problema.

Este problema se resuelve limitando la vida de una conexión no utilizada. Si durante algún tiempo no se envió una sola solicitud a través de la conexión, simplemente se cierra y se descarta del grupo. No hay implementación porque es demasiado grande.

¿Tenemos un cliente que funciona rápido y genial? Realmente no es así. Todavía tenemos la función de crear conexión: dialHost.

Veamos su implementación. Una implementación ingenua se ve así. La dirección donde desea conectarse simplemente se transmite. Llamamos a la función estándar net.Dial. Ella devuelve la conexión. ¿Qué hay de malo en esta implementación?

Por defecto, net.Dial realiza una solicitud de DNS para cada llamada. Esto puede conducir a un mayor uso de los recursos de su subsistema DNS. Si los clientes API se conectan a servidores que no admiten conexiones KeepAlive, cierran las conexiones. KeepAlive lo respalda y los servidores no. Después de tal respuesta, el servidor cierra la conexión. Resulta que se llama a net.Dial en cada solicitud. Hay alrededor de 10 mil solicitudes de este tipo por segundo. Tienes 10 mil veces por segundo que se resuelve en dns. Esto carga el subsistema DNS.

¿Cómo resolver este problema? Cree un caché que mapee el host en IP por un corto tiempo directamente en su código Go, y no llame a dns resolviendo en cada red. Conéctese a direcciones IP listas para usar.

El segundo problema es la carga desigual en el servidor si tiene varios servidores ocultos detrás del nombre de dominio. Por ejemplo, como Round Robin DNS. Si almacena en caché una dirección IP en DNS por un tiempo, durante este tiempo todas sus solicitudes irán a un servidor. Aunque puede tener varios de ellos allí. Es necesario resolver este problema. Se resuelve enumerando todas las IP disponibles que están ocultas detrás de un nombre de dominio dado. Esto también se hace en Fasthttp.Client.

El tercer problema es que net.Dial también puede colgar indefinidamente debido a problemas con la red o el servidor donde está intentando conectarse. En este caso, sus gorutinas se colgarán de la función Obtener. Esto también puede conducir a un mayor uso de los recursos.
La solución es agregar un tiempo de espera. Dial package net. , , . , , , .

. Get Dial . - . Dial , , . , , . DialTimeout. , .

HostClient .
HostClient , . LoadBalance.
HostClient . , HostClient . connection . . .
Fauly host .
— . Dial. , Dial. Get, , - . , . , , .
— . Get , . , , , .
Error , Round Robin .
SSL , Golang . .

fasthttp.Client. HostClient, fasthttp.Client HostClient.

Get. HostClient . HostClient . HostClient Get. HostClient.

HostClient - , URL. web-crawling ( ), . HostClient . net/http, . , HostClient, . fasthttp.

Client HostClient, PipelineClient . PipelineClient connection pool. PipelineClient connection, . PipelineClient connection. connection pool. PipelineClient connection .

PipelineClient connection . PipelineConnClient.writer — connection, . PipelineConnClient.reader — connection , PipelineConnClient.writer. PipelineConnClient.reader , Get.

PipelineClient.Get PipelineClient. pipelineWork url, , response, channel done, response.
Get. C . channel, PipelineConnClient.writer connection. channel w.done, PipelineConnClient.reader, response request.

net/http fasthttp.Client 2 .

, , fasthttp. , , . fasthttp. , fasthttp, . allocation . .

net/http. , allocation net/nttp. .

: PipelineClient connection?
: — pending , . . request, pending , Error.
: API , fasthttp, net/http?
: . net/http . . string -, string . , net/http, . - , . fasthttp , . . net/http fasthttp , net/http POST-, response, () . fasthttp , request response . 10 request 10 response . , . fasthttp 10 request 10 response? . — . , net/http. . , net/http — .
PS .
.
— .