La bibliothèque Fasthttp est une alternative accélérée à net / http à partir des packages Golang standard.
Comment est-il organisé? Pourquoi est-elle si rapide?
Je porte à votre attention une transcription du rapport des clients internes d'Alexander Valyalkin Fasthttp.
Les modèles Fasthttp peuvent être utilisés pour accélérer vos applications, votre code.
Peu importe, bienvenue au chat.
Je suis Alexander Valyalkin. Je travaille chez VertaMedia. J'ai développé fasthttp pour nos besoins. Il inclut l'implémentation du client http et du serveur http. Fasthttp est beaucoup plus rapide que net / http des packages Go standard.

Fasthttp est une implémentation rapide du serveur et du client http. Situé fasthttp sur github.com

Je pense que beaucoup ont entendu parler du serveur fasthttp, qu'il est très rapide. Mais peu ont entendu parler du client fasthttp. Le serveur Fasthttp participe à la référence de techempower - la célèbre référence dans les cercles étroits pour les serveurs http. Le serveur Fasthttp participe aux tours 12 et 13. Le tour 13 n'est pas encore sorti (en 2016 - environ).

Les résultats de l'un des tests de la ronde 12, où fasthttp est presque au sommet. Les chiffres indiquent le nombre de requêtes qu'il effectue par seconde sur ce test. Dans ce test, une demande est faite pour une page qui renvoie bonjour le monde. Sur hello world, fasthttp est très rapide.

Résultats préliminaires du prochain tour, qui n'a pas encore été publié (en 2016 - env. Ed.). 4 implémentations fasthttp occupent la première place dans le benchmark, que non seulement Hello World révèle, mais qui rampe également dans la base de données et forme une page HTML basée sur le modèle.

Très peu de gens connaissent le client fasthttp. Mais en fait, il est aussi cool. Dans ce rapport, je vais vous parler du client fasthttp du périphérique interne et pourquoi il a été développé.

Il existe en fait plusieurs clients dans fasthttp: Client, HostClient et PipelineClient. De plus, je vais vous en dire plus sur chacun d'eux.

Fasthttp.Client est un client http à usage général régulier. Avec lui, vous pouvez faire des demandes à n'importe quel site Internet, recevoir des réponses. Ses caractéristiques: il fonctionne rapidement, il peut limiter le nombre de connexions ouvertes par hôte, contrairement au package net / http. La documentation se trouve sur https://godoc.org/github.com/valyala/fasthttp#Client .

Fasthttp.HostClient est un client spécialisé pour communiquer avec un seul serveur. Habituellement, il est utilisé pour accéder à l'API HTTP: API REST, API JSON. Il peut également être utilisé pour proxyner le trafic depuis Internet vers un DataCenter interne sur plusieurs serveurs. La documentation est ici: https://godoc.org/github.com/valyala/fasthttp#HostClient .
Comme Fasthttp.Client, Fasthttp.HostClient peut limiter le nombre de connexions ouvertes à chacun des serveurs principaux. Cette fonctionnalité est absente dans net / http, et cette fonctionnalité est également absente dans nginx gratuit. Pour autant que je sache, cette fonctionnalité n'est disponible que dans Nginx payant.

Fasthttp.PipelineClient est un client spécialisé qui vous permet de gérer les requêtes de pipeline vers un serveur ou vers un nombre limité de serveurs. Il peut être utilisé pour accéder à l'API, via le protocole HTTP, où vous devez effectuer un grand nombre de requêtes et le plus rapidement possible. La limitation de Fasthttp.PipelineClient est qu'il peut souffrir d'un blocage de Head of Line. C'est lorsque nous envoyons un grand nombre de demandes au serveur et n'attendons pas de réponse à chaque demande. Le serveur est bloqué sur l'une de ces demandes. Pour cette raison, toutes les autres requêtes qui l'ont suivi attendent que ce serveur traite une requête lente. Fasthttp.PipelineClient ne doit être utilisé que si vous êtes sûr que le serveur répondra instantanément à vos demandes. La documentation

Je vais maintenant parler de l'implémentation interne de chacun de ces clients. Je vais commencer par Fasthttp.HostClient, car presque tous les autres clients sont construits sur sa base.

Il s'agit de l'implémentation la plus simple du client HTTP en pseudo-code sur Go. Nous sommes connectés, nous obtenons une réponse http à cette URL. Nous nous connectons à cet hôte. Nous obtenons une connexion. Dans ce code, afin qu'il soit inférieur au volume, toutes les vérifications d'erreur sont manquantes. En fait, ce n'est pas le cas. Vous devez toujours vérifier les erreurs. Créez une connexion. Connexion étroite avec différer. Nous envoyons une demande de connexion par URL. Nous recevons la réponse, nous renvoyons cette réponse. Quel est le problème avec cette implémentation du client HTTP?

Le premier problème est que dans cette implémentation, la connexion est établie pour chaque requête. Cette implémentation ne prend pas en charge HTTP KeepAlive. Comment résoudre ce problème? Vous pouvez utiliser le pool de connexions pour chaque serveur. Vous ne pouvez pas utiliser le pool de connexions pour tous les serveurs, car la demande suivante ne précise pas vers quel serveur envoyer. Chaque serveur doit avoir son propre pool de connexions. Et nous utilisons HTTP KeepAlive. Cela signifie que l'en-tête de connexion n'a pas besoin de spécifier la fermeture de connexion. Dans HTTP / 1.1, par défaut, HTTP KeepAlive est pris en charge et Connection Close doit être supprimé de l'en-tête. Voici l'implémentation dans le pseudo-code client avec prise en charge du pool de connexions. Il existe un ensemble de plusieurs pools de connexions pour chaque hôte. La première fonction, connPoolForHost, renvoie le pool de connexions pour un hôte donné à partir d'une URL donnée. Ensuite, nous obtenons la connexion de ce pool de connexions, nous prévoyons d'utiliser Defer pour renvoyer cette connexion au pool, envoyer une demande KeepAlive pour cette connexion et renvoyer une réponse. Après la réponse, Defer est exécuté et la connexion revient à Pool. Ainsi, nous activons le support HTTP KeepAlive et tout commence à fonctionner plus rapidement. Parce que nous ne perdons pas de temps à créer une connexion pour chaque demande.
Mais la solution a aussi des problèmes. Si vous regardez la signature de la fonction, vous pouvez voir qu'elle renvoie un objet de réponse pour chaque demande. Cela signifie que pour cet objet, vous devez allouer de la mémoire à chaque fois, l'initialiser et la renvoyer. C'est mauvais pour les performances. Cela peut être mauvais si vous avez beaucoup d'appels de ce type pour obtenir des fonctions.

Par conséquent, ce problème peut être résolu tel qu'il est résolu dans Fasthttp en plaçant l'objet pointeur sur l'objet réponse dans les paramètres de cette fonction. De cette façon, ce code appelant peut réutiliser cet objet de réponse plusieurs fois. Sur la diapositive se trouve la mise en œuvre de cette idée. Nous transmettons une référence à l'objet de réponse à la fonction Get - et la fonction remplit cette réponse. La dernière ligne remplit cet objet.

Voici à quoi cela pourrait ressembler dans votre code. Une fonction qui accepte un canal qui reçoit une liste d'URL à interroger. Nous organiserons un cycle sur cette chaîne. Nous créons un objet de réponse une fois et le réutilisons en boucle. Appelez Get, passez un pointeur sur l'objet, traitez cette réponse. Après l'avoir traité, nous le réinitialisons à son état d'origine. De cette façon, nous évitons l'allocation de mémoire et accélérons notre code.

Le troisième problème est la fermeture de la connexion. Fermeture de connexion - en-tête HTTP, qui peut être trouvé à la fois dans la demande et la réponse. Si nous obtenons un tel en-tête, cette connexion doit être fermée. Par conséquent, dans la mise en œuvre du client, il est nécessaire de prévoir la fermeture de la connexion. Si vous avez envoyé une demande avec l'en-tête Connexion fermée, après avoir reçu la réponse, vous devez fermer cette connexion. Si vous avez envoyé une demande sans fermeture de connexion et que vous avez renvoyé une réponse avec fermeture de connexion, vous devez également fermer cette connexion après avoir reçu une réponse.

Voici le pseudo-code de cette implémentation. Après avoir reçu une réponse, nous vérifions si les en-têtes de fermeture de connexion y sont installés. S'il est installé, fermez simplement la connexion. S'il n'est pas installé, renvoyez la connexion au pool. Si cela n'est pas fait, alors si le serveur ferme la connexion après avoir renvoyé les réponses, votre pool de connexions contiendra la connexion interrompue que le serveur a fermée, et vous essaierez d'y écrire quelque chose et vous obtiendrez des erreurs.

Le quatrième problème auquel les clients HTTP sont exposés est les serveurs lents ou un réseau lent et inactif. Les serveurs peuvent cesser de répondre à vos demandes pour diverses raisons. Par exemple, le serveur est cassé ou le réseau entre votre client et le serveur a cessé de fonctionner. Pour cette raison, tous vos goroutines qui appellent la fonction Get décrite précédemment seront bloqués, en attendant une réponse du serveur indéfiniment. Par exemple, vous implémentez un proxy http qui accepte une connexion entrante et appelle la fonction Get sur chaque connexion, puis un grand nombre de goroutines seront créés et ils se bloqueront tous sur votre serveur jusqu'à ce que le serveur plante, jusqu'à ce que la mémoire soit épuisée.

Comment résoudre ce problème? Il y a une telle décision naïve qui vient d'abord à l'esprit - il suffit d'envelopper ce Get dans un goroutine séparé. Ensuite, dans goroutine, passez un canal vide, qui sera fermé après l'exécution de Get. Après avoir démarré ce goroutine, attendez un moment sur cette chaîne (timeout). Dans ce cas, si un certain temps s'écoule et que cette Get n'est pas exécutée, la sortie de cette fonction se produira par timeout. Si cette Get est exécutée, le canal se fermera et la sortie se produira. Mais cette décision est erronée, car elle transfère le problème d'une tête malade à une tête saine. Tout de même, les goroutines seront créées et suspendues quel que soit le délai d'attente que vous utilisez. Le nombre de goroutines à l'origine du délai d'expiration de Get sera limité, mais un nombre illimité de goroutines sera créé dans Get avec un délai d'expiration.

Comment résoudre ce problème? La première solution consiste à limiter le nombre de goroutines bloquées dans la fonction Get. Cela peut être fait en utilisant un modèle bien connu comme l'utilisation d'un canal tamponné de longueur limitée, qui comptera le nombre de goroutines qui exécutent la fonction Get. Si cette quantité de goroutine dépasse une certaine limite - la capacité de ce canal, alors nous quitterons la branche par défaut. Cela signifie que nous avons tous les goroutines qui sont exécutés sont occupés, et dans la branche par défaut, nous avons juste besoin de retourner Erreur, qu'il n'y a pas de ressources libres. Avant de créer goroutine, nous essayons d'écrire une structure vide sur ce canal. Si cela ne fonctionne pas, nous avons dépassé la quantité de goroutines. Si cela s'est avéré, nous créons ce gorutin et après l'exécution de Get, nous lisons une valeur de ce canal. Ainsi, nous limitons la quantité de goroutines qui peuvent être bloquées dans Get.

La deuxième solution, qui complète la première, consiste à définir des délais d'expiration sur la connexion au serveur. Cela débloquera la fonction get si le serveur ne répond pas pendant longtemps ou si le réseau est en panne.
Si le réseau ne fonctionne pas dans la solution n ° 1, tout se bloque. Après avoir tapé cuncurrency un nombre limité de goroutines suspendues ici, la fonction getimeout retournera toujours une erreur. Pour qu'il fonctionne normalement, vous avez besoin d'une deuxième solution (Solution # 2), qui définit un délai pour la lecture et l'écriture à partir de la connexion. Cela permet de déverrouiller les goroutines bloquées si le réseau ou le serveur cesse de fonctionner.

La solution n ° 1 a une course aux données. L'objet de réponse à partir duquel le pointeur a été transmis sera occupé si Get est bloqué. Mais cette fonction Get timeout peut expirer. Dans ce cas, nous quittons cette fonction, une réponse qui se bloque et après un certain temps sera réécrite. Ainsi, une course aux données est obtenue. Puisque nous avons une réponse après avoir quitté la fonction, elle est toujours utilisée quelque part dans goroutine.
Le problème est résolu en créant une copie de réponse et en passant la copie de réponse à goroutine. Une fois Get terminé, copiez la réponse de cette réponse dans notre réponse d'origine, qui est transmise ici. Ainsi, la course aux données est résolue. Cette copie de la réponse vit un court instant et retourne dans le pool. Nous réutilisons la réponse. Une copie de réponse peut ne pas entrer dans le pool uniquement après expiration du délai. Par timeout, il y a une perte de réponse du pool.

Dois-je fermer la connexion après que le serveur n'a pas renvoyé de réponse dans un délai? La réponse est non. Au contraire, oui, si vous souhaitez sauvegarder le serveur. Parce que lorsque vous envoyez une demande au serveur, attendez un certain temps, le serveur ne répond pas pendant ce temps - il ne répond pas aux demandes. Par exemple, vous fermez cette connexion, mais cela ne signifie pas que le serveur cessera immédiatement d'exécuter cette demande. Le serveur continuera de l'exécuter. Le serveur détectera que cette demande n'a pas besoin d'être exécutée après avoir tenté de vous renvoyer une réponse. Vous avez fermé la connexion, réessayé de créer une nouvelle demande, encore une fois le délai écoulé, fermé à nouveau, créé une nouvelle demande. Vous aurez une charge sur l'augmentation du serveur. Par conséquent, votre service dépend de vos demandes. Ce sont des DoS au niveau des requêtes http. Si vous avez des serveurs qui s'exécutent lentement et que vous ne souhaitez pas les sauvegarder, vous n'avez pas besoin de fermer la connexion après un délai d'expiration. Vous devez attendre un moment, laisser la connexion expier pour ce serveur. Laissez-le essayer de vous donner une réponse. En attendant, utilisez d'autres connexions gratuites. Tout ce qui a été dit avant, ce sont toutes les étapes de l'implémentation de Fasthttp.Client et les problèmes survenus lors de l'implémentation de Fasthttp.Client. Ces problèmes sont résolus dans Fasthttp.HostClient.
Nous avons maintenant un client rapide? Pas vraiment. Vous devez voir comment le pool de connexions est implémenté.

L'implémentation naïve de Connection Pool ressemble à ceci. Il existe une sorte d'adresse de serveur où vous devez installer la connexion. Il y a une liste de connexions gratuites et un verrou pour synchroniser l'accès à cette liste.

Voici la fonction pour obtenir la connexion à partir du pool de connexions. Nous regardons une liste de notre collection. S'il y a quelque chose là-bas, alors nous obtenons une connexion gratuite et le renvoyons. S'il n'y a rien, créez une nouvelle connexion à ce serveur et renvoyez-la. Qu'est-ce qui ne va pas ici?
La fonction connPool.Put renvoie une connexion gratuite.
Au compte timeout. Dans Fasthttp.Client, vous pouvez spécifier la durée de vie maximale d'une connexion inutilisée ouverte. Une fois ce délai écoulé, les connexions inutilisées sont fermées automatiquement et rejetées de ce pool.
Les connexions plus anciennes deviennent inutilisées au fil du temps et sont automatiquement fermées et supprimées du pool.
Lorsque la connexion est retirée du pool et qu'il s'avère que son serveur a été fermé et que vous avez essayé d'y écrire quelque chose, une deuxième tentative est effectuée - une nouvelle connexion est obtenue et tente d'envoyer à nouveau des demandes pour cette connexion. Mais ce n'est que si cette demande est idempotente - c'est-à-dire une demande qui peut être exécutée plusieurs fois sans effets secondaires sur le serveur - est-ce une demande GET ou HEAD. Par exemple, dans le net / http standard, nous venons d'ajouter une vérification des connexions fermées. Là, ils ont fait un contrôle plus délicat. Ils vérifient, lorsqu'ils essaient d'envoyer une nouvelle demande à la connexion à partir du pool, si au moins un octet est envoyé à cette connexion. S'il est déclenché, retournez ensuite Erreur. Si vous n'êtes pas parti, nous prenons une nouvelle connexion à partir du pool.

Quel est le problème avec la piscine? Sa taille n'est pas limitée. Même implémentation que dans net / http. Si vous écrivez un client qui passe de millions de goroutines à un serveur lent, le client tentera de créer une connexion d'un million à ce serveur. Il n'y a pas de limite sur le nombre maximum de connexions dans le package net / http standard. Pour le client utilisé pour accéder à l'API via HTTP, il est conseillé de limiter la taille de ce pool de connexions. Sinon, vos clients risquent de baisser, car vous utiliserez toutes les ressources: threads, objets, connexion, goroutines et mémoire. En outre, cela peut conduire à un DoS de vos serveurs, car beaucoup de connexions seront établies avec eux, qui ne sont pas utilisées ou sont utilisées de manière inefficace, car le serveur ne peut pas maintenir autant de connexion.

Limiter le pool de connexions. Le code n'est pas ici, car il est trop volumineux pour tenir sur une seule diapositive. Les personnes intéressées peuvent voir la mise en œuvre de cette fonction sur github.com.

Le deuxième problème. Beaucoup de demandes arrivent au client à un moment donné. Et après cela, il y a une baisse et un retour au nombre précédent de demandes. Par exemple, 10 000 demandes sont arrivées simultanément, puis le nombre de demandes est revenu à 1 000 par unité de temps. Après cela, le pool de connexions passera à 10000 connexions. Ces connexions y resteront à l'infini. Ce problème se trouvait dans le client net / http standard avant la version 1.7. Par conséquent, vous devez résoudre ce problème.

Ce problème est résolu en limitant la durée de vie d'une connexion inutilisée. Si pendant un certain temps aucune requête n'a été envoyée via la connexion, elle se ferme simplement et est rejetée hors du pool. Il n'y a pas d'implémentation car elle est trop grande.

Nous avons un client qui travaille vite et bien? Pas vraiment comme ça. Nous avons toujours la fonction de créer une connexion - dialHost.

Regardons sa mise en œuvre. Une implémentation naïve ressemble à ceci. L'adresse à laquelle vous souhaitez vous connecter est simplement transmise. Nous appelons la fonction standard net.Dial. Elle retourne la connexion. Quel est le problème avec cette mise en œuvre?

Par défaut, net.Dial effectue une demande DNS pour chaque appel. Cela peut entraîner une utilisation accrue des ressources de votre sous-système DNS. Si les clients API se connectent à des serveurs qui ne prennent pas en charge les connexions KeepAlive, ils ferment les connexions. Vous êtes pris en charge par KeepAlive, et les serveurs ne le sont pas. Après une telle réponse, le serveur ferme la connexion. Il s'avère que net.Dial est appelé à chaque demande. Il y a environ 10 000 demandes de ce type par seconde. Vous avez 10 mille fois par seconde va résoudre en DNS. Cela charge le sous-système DNS.

Comment résoudre ce problème? Créez un cache qui mappe l'hôte dans IP pendant une courte période directement dans votre code Go, et n'appelez pas la résolution DNS sur chaque net.Dial. Connectez-vous à des adresses IP prêtes à l'emploi.

Le deuxième problème est la charge inégale sur le serveur si vous avez plusieurs serveurs cachés derrière le nom de domaine. Par exemple, comme Round Robin DNS. Si vous mettez en cache une adresse IP dans DNS pendant un certain temps, alors pendant ce temps, toutes vos demandes iront à un serveur. Bien que vous puissiez en avoir plusieurs. Il est nécessaire de résoudre ce problème. Il est résolu en énumérant toutes les adresses IP disponibles qui sont cachées derrière un nom de domaine donné. Cela se fait également dans Fasthttp.Client.

Le troisième problème est que net.Dial peut également se bloquer indéfiniment en raison de problèmes avec le réseau ou le serveur auquel vous essayez de vous connecter. Dans ce cas, vos goroutines se bloqueront sur la fonction Get. Cela peut également conduire à une utilisation accrue des ressources.
La solution consiste à ajouter un délai d'attente. 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 .
.
— .