Pourquoi Rust est Ă  la tĂȘte de la rĂ©fĂ©rence du framework TechEmpower

En fait, je n'avais pas l'intention de voir de quelle couleur Ă©taient les tripes de Rust. J'ai choisi un projet de passe-temps sur Go, je suis allĂ© sur GitHub pour voir l'Ă©tat de fasthttp: est-ce qu'il se dĂ©veloppe? Eh bien, au moins pris en charge? A grandi. Je suis allĂ©, j'ai regardĂ© oĂč se situe fasthttp dans les benchmarks TechEmpower . Je regarde: et lĂ , fasthttp montre Ă  peine la moitiĂ© de ce que le leader rĂ©ussit - Ă  certains actix sur certains Rust. Quelle douleur.


Ici, je croisais les bras, me cognais la tĂȘte au sol (trois fois) et criais: "AllĂ©luia, en vĂ©ritĂ©, Rust est un vrai dieu, comme j'Ă©tais aveugle avant!". Mais soit les poignĂ©es n'ont pas fonctionnĂ©, soit le front a regrettĂ© ... Au lieu de cela, je suis entrĂ© dans le code des tests Ă©crits en Go et des tests actix-web en Rust. Pour le trier.


AprÚs quelques heures, j'ai découvert:


  1. pourquoi le framework rouille actix-web occupe la premiĂšre place dans tous les tests TechEmpower,
  2. comment Java démarre Script.

Maintenant, je vais tout vous dire dans l'ordre.


Qu'est-ce que TechEmpower Framework Benchmark?


Si un framework Web démontre s'il va ou, par exemple, pense parfois à chuchoter à des amis "je suis rapide", alors il tombera sûrement dans le référentiel du framework TechEmpower. Un endroit populaire pour mesurer les performances.


Le site a un design particulier: les onglets des filtres, des tours, des conditions et des résultats pour différents types de tests sont dispersés sur la page avec une main généreuse. Si généreux et si généreux que vous ne les remarquez tout simplement pas. Mais cela vaut la peine de cliquer sur les onglets, les informations derriÚre eux sont utiles.


Le moyen le plus simple consiste à obtenir les résultats du test en clair, "Bonjour tout le monde!" pour les serveurs Web. Les auteurs du cadre lui donnent généralement un lien: nous sommes censés rester dans les cent premiers. Le cas est correct et utile. En général, donner du texte en clair est bon pour beaucoup, et les dirigeants vont en groupe serré.


À proximitĂ©, dans ces mĂȘmes onglets, se trouvent les rĂ©sultats de tests d'autres types (scĂ©narios). Il y en a sept, plus de dĂ©tails peuvent ĂȘtre trouvĂ©s ici . Ces scripts testent non seulement la façon dont le framework / la plateforme gĂšre le traitement d'une simple requĂȘte http, mais Ă©galement une combinaison avec un client de base de donnĂ©es, un moteur de modĂšle ou un sĂ©rialiseur JSON.


Il existe des données de test dans un environnement virtuel, sur un matériel physique. En plus des graphiques, il existe des données tabulaires. En général, beaucoup de choses intéressantes, il vaut la peine de creuser, pas seulement de regarder la position de "votre" plate-forme.


La premiĂšre chose qui m'est venue Ă  l'esprit aprĂšs avoir parcouru les rĂ©sultats du test: "Pourquoi tout est-il SI SI diffĂ©rent du texte en clair?!". En texte clair, les dirigeants forment un groupe restreint, mais lorsqu'il s'agit de travailler avec la base de donnĂ©es, actix-web mĂšne avec une marge importante. Dans le mĂȘme temps, il affiche un temps de traitement des demandes stable. Shaitan.


Autre anomalie: une solution JavaScript incroyablement puissante. Il s'appelle ex4x. Il s'est avéré que son code était légÚrement moins que complÚtement écrit en Java. Utilisé par le runtime Java, JDBC. Le code JavaScript est traduit en bytecode et colle les bibliothÚques Java. Ils l'ont littéralement pris - et ont attaché Script à Java. Les astuces des visages pùles n'ont pas de limites.


Comment regarder le code et ce qu'il contient


Le code pour tous les tests est sur GitHub. Tout est dans un seul rĂ©fĂ©rentiel, ce qui est trĂšs pratique. Vous pouvez cloner et regarder, vous pouvez regarder directement sur GitHub. Les tests impliquent plus de 300 combinaisons diffĂ©rentes de l'infrastructure avec des sĂ©rialiseurs, des moteurs de modĂšle et le client de base de donnĂ©es. Dans diffĂ©rents langages de programmation, avec une approche diffĂ©rente du dĂ©veloppement. Les implĂ©mentations dans une langue sont proches, cela peut ĂȘtre comparĂ© Ă  l'implĂ©mentation dans d'autres langues. Le code est maintenu par la communautĂ©, ce n'est pas le travail d'une seule personne ou Ă©quipe.


Le code de rĂ©fĂ©rence est un endroit idĂ©al pour Ă©largir vos horizons. Il est intĂ©ressant d'analyser comment diffĂ©rentes personnes rĂ©solvent les mĂȘmes problĂšmes. Il n'y a pas beaucoup de code, les bibliothĂšques et les solutions utilisĂ©es sont faciles Ă  distinguer. Je ne regrette pas du tout d’ĂȘtre arrivĂ© lĂ -bas. J'ai beaucoup appris. Tout d'abord Ă  propos de Rust.


Avant Rust, j'avais une idée trÚs vague. Tout article sur C, C ++, D, et surtout Go est sûr d'avoir quelques commentateurs qui expliquent en détail et avec angoisse que la vanité, le non-sens et la stupidité sont écrits dans autre chose, tant qu'il y a Gascogne Rouille. Parfois, ils s'emballent tellement qu'ils donnent des exemples de code qu'une personne non préparée ou peu d'accepter conduit dans une stupeur: "Pourquoi, pourquoi, pourquoi tous ces symboles?!"


Par conséquent, l'ouverture du code était effrayante.


J'ai regardĂ©. Il s'est avĂ©rĂ© que les programmes de Rust peuvent ĂȘtre lus. De plus, le code est si bien lu que j'ai mĂȘme installĂ© Rust, j'ai essayĂ© de compiler le test et de le bricoler un peu.


Ici j'ai failli abandonner cette affaire, car la compilation dure longtemps. Un temps trĂšs long. Si j'Ă©tais d'Artagnan, ou mĂȘme simplement colĂ©rique, je me serais prĂ©cipitĂ© en Gascogne, et mille dĂ©mons traĂźneraient avec dĂ©couragement. Mais je l'ai fait. J'ai encore bu du thĂ©. Il semble que mĂȘme pas une tasse: sur mon ordinateur portable, la premiĂšre compilation a pris environ 20 minutes, puis, tout va plus amusant. Peut-ĂȘtre jusqu'Ă  la prochaine grande mise Ă  jour des caisses.


Mais n'est-ce pas Rust lui-mĂȘme?


Non. Pas un langage de programmation.


Bien sĂ»r, Rust est une langue merveilleuse. Puissant, flexible, mais par habitude et verbeux. Mais le langage lui-mĂȘme n'Ă©crira pas de code rapide. La langue est l'un des outils, l'une des dĂ©cisions prises par le programmeur.


Comme je l'ai dit - donner du texte en clair est rapidement obtenu par beaucoup. Les performances des frameworks actix-web, fasthttp et une douzaine d'autres lors du traitement d'une simple demande sont assez comparables, c'est-à-dire que d'autres langages ont la capacité technique de rivaliser avec Rust.


Actix-web lui-mĂȘme, bien sĂ»r, est «à blĂąmer»: un produit rapide, pragmatique et excellent. La sĂ©rialisation est pratique, le moteur de modĂšle est bon - cela aide aussi beaucoup.


Plus particuliÚrement, les résultats des tests effectués avec la base de données diffÚrent.


AprÚs avoir creusé un peu dans le code, j'ai mis en évidence trois différences principales qui (il me semble) ont aidé les tests actix à se démarquer des concurrents dans les tests synthétiques:


  1. Mode de fonctionnement pipelined pipelined tokio-postgres;
  2. Utiliser une seule connexion avec un test Rust au lieu d'un pool de connexions avec un test Ă©crit en Go;
  3. Mise Ă  jour des benchmarks actix avec une seule commande envoyĂ©e via une simple requĂȘte au lieu d'envoyer plusieurs commandes UPDATE.

Quel type de mode convoyeur?


Voici un extrait de la documentation tokio-postgres (utilisé dans le cas-test de la bibliothÚque cliente PostgreSQL) expliquant ce que ses développeurs veulent dire:


Sequential Pipelined | Client | PostgreSQL | | Client | PostgreSQL | |----------------|-----------------| |----------------|-----------------| | send query 1 | | | send query 1 | | | | process query 1 | | send query 2 | process query 1 | | receive rows 1 | | | send query 3 | process query 2 | | send query 2 | | | receive rows 1 | process query 3 | | | process query 2 | | receive rows 2 | | | receive rows 2 | | | receive rows 3 | | | send query 3 | | | | process query 3 | | receive rows 3 | | 

Le client en mode pipelined (pipelined) n'attend pas de rĂ©ponse PostgreSQL, mais envoie la requĂȘte suivante pendant que PostgreSQL traite la prĂ©cĂ©dente. On peut voir que de cette façon, vous pouvez traiter la mĂȘme sĂ©quence de requĂȘtes de base de donnĂ©es beaucoup plus rapidement.


Si la connexion en mode pipeline est duplex (offrant la possibilitĂ© d'obtenir des rĂ©sultats en parallĂšle avec l'envoi), ce temps peut ĂȘtre lĂ©gĂšrement rĂ©duit. Il semble qu'il existe dĂ©jĂ  une version expĂ©rimentale de tokio-postgres oĂč une connexion duplex est ouverte.


Étant donnĂ© que le client PostgreSQL envoie plusieurs messages (Parse, Bind, Execute et Sync) Ă  chaque requĂȘte SQL envoyĂ©e pour exĂ©cution et reçoit une rĂ©ponse Ă  ceux-ci, le mode pipeline sera plus efficace mĂȘme lors du traitement de requĂȘtes uniques.


Et pourquoi n'est-ce pas dans Go?


Parce que Go utilise gĂ©nĂ©ralement des pools de connexions de base de donnĂ©es. Les connexions ne sont pas destinĂ©es Ă  ĂȘtre utilisĂ©es en parallĂšle.


Si vous exĂ©cutez les mĂȘmes requĂȘtes SQL via un pool, plutĂŽt qu'une seule connexion, vous pouvez thĂ©oriquement obtenir un temps d'exĂ©cution encore plus court avec un client sĂ©rie ordinaire que lorsque vous travaillez via une seule connexion, que ce soit trois fois en pipeline:


 | Connection | Connection 2 | Connection 3 | PostgreSQL | |----------------|----------------|----------------|-----------------| | send query 1 | | | | | | send query 2 | | process query 1 | | receive rows 1 | | send query 3 | process query 2 | | | receive rows 2 | | process query 3 | | | receive rows 3 | | 

On dirait que la peau de mouton (mode convoyeur) ne vaut pas la chandelle.


Ce n'est que sous une charge élevée que le nombre de connexions au serveur PostgreSQL peut poser problÚme.


Et qu'est-ce que le nombre de connexions a à voir avec ça?


Le point ici est de savoir comment le serveur PostgreSQL répond à une augmentation du nombre de connexions.


Le groupe de colonnes de gauche montre l'augmentation et la baisse des performances de PostgreSQL en fonction du nombre de connexions ouvertes:



( Adapté du post Percona )


On peut voir qu'avec une augmentation du nombre de connexions ouvertes, les performances du serveur PostgreSQL chutent rapidement.


De plus, l'ouverture d'une connexion directe n'est pas «gratuite». Immédiatement aprÚs l'ouverture, le client envoie des informations de service, "est d'accord" avec le serveur PostgreSQL sur la façon dont les demandes seront traitées.


Par conséquent, dans la pratique, vous devez limiter le nombre de connexions actives à PostgreSQL, en les passant souvent en plus via pgbouncer ou une autre odyssée.


Alors pourquoi Actix-Web a-t-il été plus rapide?


Tout d'abord, actix-web lui-mĂȘme est sacrĂ©ment rapide. C'est lui qui fixe le «plafond», et il est lĂ©gĂšrement supĂ©rieur Ă  celui des autres. Les autres bibliothĂšques utilisĂ©es (serde, yarde) sont Ă©galement trĂšs, trĂšs productives. Mais il me semble que dans les tests fonctionnant avec PostgreSQL, il a Ă©tĂ© possible de se dĂ©tacher car le serveur actix-web dĂ©marre un thread sur le cƓur du processeur. Chaque thread ouvre une seule connexion Ă  PostgreSQL.


Moins il y a de connexions actives, plus PostgreSQL fonctionne rapidement (voir les graphiques ci-dessus).


Le client fonctionnant en mode pipeline (tokio-postgres) vous permet d'utiliser efficacement une connexion avec PostgreSQL pour le traitement parallĂšle des requĂȘtes des utilisateurs. Les gestionnaires de requĂȘtes HTTP dĂ©chargent leurs commandes SQL dans une file d'attente et s'alignent dans une autre pour recevoir les rĂ©sultats. Les rĂ©sultats sont amusants, les retards sont minimes, tout le monde est content. Les performances globales sont supĂ©rieures Ă  celles d'un systĂšme avec un pool de connexions.


Vous devez donc abandonner le pool, Ă©crire un client de pipeline PostgreSQL, et le bonheur et la vitesse incroyable viendront tout de suite?


C'est possible. Mais pas tout d'un coup.


Lorsque le mode convoyeur est peu susceptible de sauver et ne sauvera certainement pas


Le schéma utilisé dans le code de référence ne fonctionnera pas avec les transactions PostgreSQL.


Dans le benchmark, les transactions ne sont pas nécessaires et le code est écrit en tenant compte du fait qu'il n'y aura pas de transactions. En pratique, cela arrive.


Si le code backend ouvre une transaction PostgreSQL (par exemple, pour effectuer une modification dans deux tables atomiques différentes), toutes les commandes envoyées via cette connexion seront exécutées à l'intérieur de cette transaction.


Puisque la connexion avec PostgreSQL est utilisĂ©e en parallĂšle, tout y est mĂ©langĂ©. Les commandes qui doivent ĂȘtre exĂ©cutĂ©es dans une transaction telle que conçue par le dĂ©veloppeur sont mĂ©langĂ©es avec des commandes sql lancĂ©es par des gestionnaires de requĂȘtes http parallĂšles. Nous recevrons des pertes de donnĂ©es alĂ©atoires et des problĂšmes d'intĂ©gritĂ©.


Alors bonjour transaction - au revoir utilisation parallĂšle d'une connexion. Vous devrez vous assurer que la connexion n'est pas utilisĂ©e par d'autres gestionnaires de requĂȘtes http. Vous devrez soit arrĂȘter le traitement des requĂȘtes http entrantes avant de fermer la transaction, soit utiliser un pool pour les transactions, en ouvrant plusieurs connexions au serveur de base de donnĂ©es. Il existe plusieurs implĂ©mentations de pool pour Rust, et aucune. De plus, ils existent dans Rust sĂ©parĂ©ment de l'implĂ©mentation du client de base de donnĂ©es. Vous pouvez choisir selon le goĂ»t, la couleur, l'odeur ou au hasard. Go ne fonctionne pas de cette façon. Le pouvoir des gĂ©nĂ©riques, oui.


Un point important: dans le test, dont j'ai regardé le code, les transactions ne s'ouvrent pas. Cette question n'en vaut tout simplement pas la peine. Le code de référence est optimisé pour une tùche spécifique et des conditions de fonctionnement d'application trÚs spécifiques. La décision d'utiliser une connexion par flux de serveur a probablement été prise consciemment et s'est avérée trÚs efficace.


Y a-t-il autre chose d'intéressant dans le code de référence?


Oui


Le scĂ©nario de mesure des performances est dĂ©crit en dĂ©tail. Ainsi que les critĂšres que le code participant aux tests doit satisfaire. L'un d'eux est que toutes les requĂȘtes adressĂ©es au serveur de base de donnĂ©es doivent ĂȘtre exĂ©cutĂ©es sĂ©quentiellement.


Le fragment de code suivant (légÚrement abrégé) semble ne pas répondre aux critÚres:


  let mut worlds = Vec::with_capacity(num); //  num    PostgreSQL for _ in 0..num { let w_id: i32 = self.rng.gen_range(1, 10_001); worlds.push( self.cl .query(&self.world, &[&w_id]) .into_future() .map(move |(row, _)| { // ... }), ); } //     stream::futures_unordered(worlds) .collect() .and_then(move |worlds| { // ... }) 

Tout ressemble Ă  un lancement typique de processus parallĂšles. Mais comme une connexion Ă  PostgreSQL est utilisĂ©e, les requĂȘtes vers le serveur de base de donnĂ©es sont envoyĂ©es sĂ©quentiellement. Un par un. Au besoin. Pas de crime.


Pourquoi Eh bien, tout d'abord, dans le code (il a Ă©tĂ© donnĂ© Ă  la rĂ©daction, qui a travaillĂ© au 18e tour), async / attente n'est pas encore utilisĂ©, il est apparu plus tard dans Rust. Et grĂące Ă  futures num il est plus facile d'envoyer des requĂȘtes SQL "en parallĂšle" - comme dans le code ci-dessus. Cela vous permet d'obtenir une amĂ©lioration supplĂ©mentaire des performances: alors que PostgreSQL accepte et traite la premiĂšre requĂȘte SQL, les autres y sont alimentĂ©es. Le serveur Web n'attend pas le rĂ©sultat de chacun, mais passe Ă  d'autres tĂąches et ne revient au traitement de la requĂȘte http que lorsque toutes les requĂȘtes SQL sont terminĂ©es.


Pour PostgreSQL, le bonus est que le mĂȘme type de requĂȘte dans le mĂȘme contexte (connexion) va de suite. La probabilitĂ© que le plan de requĂȘte ne soit pas reconstruit augmente.


Il s'avĂšre que les avantages du mode pipeline (voir le schĂ©ma de la documentation tokio-postgres) sont pleinement exploitĂ©s mĂȘme lors du traitement d'une seule requĂȘte http.


Quoi d'autre?


Utilisation du protocole de requĂȘte simple pour les mises Ă  jour par lots


Le protocole de communication entre le client et le serveur PostgreSQL permet des méthodes alternatives pour exécuter des commandes SQL. Le protocole habituel (Extended Query) consiste à envoyer plusieurs messages à un client: Parse, Bind, Execute et Sync. Une alternative est le protocole Simple Query, selon lequel un seul message suffit pour exécuter une commande et obtenir des résultats - Query.


La principale diffĂ©rence entre le protocole habituel est le transfert des paramĂštres de requĂȘte: ils sont transmis sĂ©parĂ©ment de la commande elle-mĂȘme. C’est plus sĂ»r. Le protocole simplifiĂ© suppose que tous les paramĂštres de la requĂȘte SQL seront convertis en chaĂźne et inclus dans le corps de la requĂȘte.


Une solution intéressante utilisée dans les benchmarks actix-web était de mettre à jour plusieurs entrées de table avec une seule commande envoyée via le protocole Simple Query.


Selon le benchmark, lors du traitement d'une demande utilisateur, le serveur web doit mettre Ă  jour plusieurs enregistrements de la table, Ă©crire des nombres alĂ©atoires. De toute Ă©vidence, la mise Ă  jour des enregistrements successivement avec des requĂȘtes sĂ©quentielles prend plus de temps qu'une seule requĂȘte mettant Ă  jour tous les enregistrements Ă  la fois.


La demande générée dans le code de test ressemble à ceci:


 UPDATE world SET randomnumber = temp.randomnumber FROM (VALUES (1, 2), (2, 3) ORDER BY 1) AS temp(id, randomnumber) WHERE temp.id = world.id 

OĂč (1, 2), (2, 3) sont les paires d'identificateurs de ligne / nouvelle valeur du champ de numĂ©ro alĂ©atoire.


Le nombre d'enregistrements mis Ă  jour est variable, prĂ©parer la demande (PREPARE) Ă  l'avance n'a pas de sens. Étant donnĂ© que les donnĂ©es Ă  mettre Ă  jour sont numĂ©riques et que la source peut ĂȘtre fiable (le code de test lui-mĂȘme), il n'y a aucun risque d'injection SQL, les donnĂ©es sont simplement incluses dans le corps SQL et tout est envoyĂ© Ă  l'aide du protocole Simple Query.


Une rumeur simple circule. J'ai rencontrĂ© une recommandation: "Ne travaillez que sur le protocole Simple Query, et tout sera rapide et correct." Je la perçois avec beaucoup de scepticisme. Simple Query vous permet de rĂ©duire le nombre de messages envoyĂ©s au serveur PostgreSQL en dĂ©plaçant le traitement des paramĂštres de requĂȘte cĂŽtĂ© client. Vous pouvez voir le gain pour les requĂȘtes gĂ©nĂ©rĂ©es dynamiquement avec un nombre variable de paramĂštres. Pour le mĂȘme type de requĂȘtes SQL (qui sont plus courantes), le gain n'est pas Ă©vident. Eh bien et Ă  quel point le traitement des paramĂštres de requĂȘte se rĂ©vĂ©lera sĂ»r, dans le cas de Simple Query, il dĂ©termine la mise en Ɠuvre de la bibliothĂšque cliente.


Comme je l'ai Ă©crit ci-dessus, dans ce cas, le corps de la requĂȘte SQL est gĂ©nĂ©rĂ© dynamiquement, les donnĂ©es sont numĂ©riques et gĂ©nĂ©rĂ©es par le serveur lui-mĂȘme. La combinaison parfaite pour Simple Query. Mais mĂȘme dans ce cas, il vaut la peine de tester d'autres options. Les alternatives dĂ©pendent de la plateforme et du client PostgreSQL: pgx (client for Go) permet d'envoyer un paquet de commandes, JDBC - pour exĂ©cuter une commande plusieurs fois de suite avec des paramĂštres diffĂ©rents. Les deux solutions peuvent fonctionner Ă  la mĂȘme vitesse ou mĂȘme ĂȘtre plus rapides.


Alors, pourquoi Rust est-il en tĂȘte?


Le chef, bien sûr, n'est pas Rust. Les tests basés sur actix-web mÚnent - c'est lui qui fixe le "plafond" de la performance. Il y a, par exemple, la fusée et le fer, qui occupent des positions modestes. Mais pour le moment, c'est actix-web qui détermine le potentiel d'utilisation de Rust dans le développement web. Quant à moi, le potentiel est trÚs élevé.


Un autre serveur "secret" non évident mais important basé sur actix-web, qui a permis de prendre la premiÚre place dans tous les benchmarks TechEmpower - dans la façon dont il fonctionne avec PostgreSQL:


  1. Une seule connexion avec PostgreSQL par flux de serveur Web s'ouvre. Cette connexion utilise le mode pipeline, ce qui lui permet d'ĂȘtre efficacement utilisĂ© pour le traitement parallĂšle des demandes des utilisateurs.
  2. Moins il y a de connexions actives, plus PostgreSQL rĂ©pond rapidement. La vitesse de traitement des demandes des utilisateurs augmente. Dans le mĂȘme temps, sous charge, l'ensemble du systĂšme fonctionne de maniĂšre plus stable (les dĂ©lais de traitement des demandes entrantes sont plus faibles, ils augmentent plus lentement).

Lorsque la vitesse est importante, cette option sera probablement plus rapide que l'utilisation de multiplexeurs (tels que pgbouncer et odyssey). Et il Ă©tait certainement plus rapide dans les repĂšres.


Il est trÚs intéressant de voir comment async / wait, qui est apparu dans Rust, et le récent drame avec actix-web affectera la popularité de Rust dans le développement Web. Il est également intéressant de voir comment les résultats des tests changeront aprÚs les avoir traités en asynchronisation / attente.

Source: https://habr.com/ru/post/fr485452/


All Articles