Elixir comme objectif de développement pour Python async

Dans le livre «Python. Aux sommets de l'excellence »Luciano Ramallo décrit une histoire. En 2000, Luciano a suivi des cours, et une fois Guido van Rossum a regardé le public. Une fois qu'un tel événement s'est produit, tout le monde a commencé à lui poser des questions. Lorsqu'on lui a demandé quelles fonctions Python avait empruntées à d'autres langues, Guido a répondu: "Tout ce qui est bon en Python est volé à d'autres langues."

Ça l'est vraiment. Python a longtemps vécu dans le contexte d'autres langages de programmation et absorbe les concepts de son environnement: asyncio est emprunté, grâce à l'apparition d'expressions lambda Lisp, et Tornado a été copié depuis libevent. Mais si quelqu'un doit emprunter des idées, c'est celui d'Erlang. Il a été créé il y a 30 ans, et tous les concepts en Python qui sont actuellement en cours d'implémentation ou qui viennent d'être décrits fonctionnent depuis longtemps à Erlang: multicœur, messages comme base de communication, appels de méthode et introspection à l'intérieur d'un système de production en direct. Ces idées, sous une forme ou une autre, trouvent leur expression dans des systèmes comme Seastar.io .


Si vous ne tenez pas compte de la Data Science, dans laquelle Python est désormais hors compétition, alors tout le reste est déjà implémenté dans Erlang: travailler avec un réseau, gérer les sockets HTTP et Web, travailler avec les bases de données. Par conséquent, il est important pour les développeurs Python de comprendre où le langage se déplacera: le long d'une route qui a déjà passé il y a 30 ans.

Pour comprendre l'histoire du développement d'autres langues et comprendre où les progrès progressent, nous avons invité Maxim Lapshin ( erlyvideo ), l'auteur du projet Erlyvideo.ru, à Moscou Python Conf ++ .

Sous la coupe se trouve la version texte de ce rapport, à savoir: dans quelle direction le système est obligé de se développer, qui continue de migrer du simple code linéaire vers libevent et au-delà, ce qui est courant et quelles sont les différences entre Elixir et Python. Nous porterons une attention particulière à la façon de gérer les sockets, les threads et les données dans différents langages de programmation et plates-formes.


Erlyvideo.ru dispose d'un système de vidéosurveillance dans lequel le contrôle d'accès pour les caméras est écrit en Python. Il s'agit d'une tâche classique pour cette langue. Il y a des utilisateurs et des caméras, des vidéos dont ils peuvent regarder: quelqu'un voit des caméras, tandis que d'autres voient un site normal.

Python a été choisi car il est pratique d'y écrire un tel service: il y a des frameworks, des ORM, des programmeurs, après tout. Le logiciel développé est emballé et vendu aux utilisateurs. Erlyvideo.ru est une entreprise qui vend des logiciels et non seulement fournit des services.

Quels problèmes avec Python je veux résoudre.

Pourquoi y a-t-il de tels problèmes avec le multicœur? Nous avons lancé Flussonic sur les ordinateurs des stades avant même qu'Intel ne le fasse. Mais Python a des difficultés avec cela: pourquoi n'utilise-t-il toujours pas les 80 cœurs de nos serveurs pour fonctionner?

Comment ne pas souffrir de prises ouvertes? La surveillance du nombre de sockets ouvertes est un gros problème. Quand il atteint la limite, fermez et évitez également les fuites.

Les variables globales oubliées ont-elles une solution? Les fuites de variables globales sont un enfer pour tout langage de récupération de place comme Java ou C #.

Comment utiliser le fer sans gaspiller les ressources? Comment s'en sortir sans exécuter 40 employés Jung et 64 Go de RAM si nous voulons utiliser les serveurs efficacement et ne pas jeter des centaines de milliers de dollars par mois sur du matériel inutile?

Pourquoi le multicœur est nécessaire


Pour que tous les cœurs soient pleinement utilisés, il faut beaucoup plus de travailleurs que de cœurs. Par exemple, pour 40 cœurs de processeur, 100 travailleurs sont nécessaires: un travailleur est allé à la base de données, l'autre est occupé par autre chose.

Un travailleur peut consommer de 300 à 400 Mo. Nous écrivons toujours cela en Python, et non en Ruby on Rails, qui peut consommer plusieurs fois plus et 40 Go de RAM seront facilement et facilement gaspillés. Ce n'est pas très cher, mais pourquoi acheter de la mémoire là où on ne peut pas l'acheter.

Le multicœur aide à fouiller les données partagées et à réduire la consommation de mémoire , et à exécuter de manière pratique et sûre de nombreux processus indépendants. Il est beaucoup plus facile à programmer, mais plus cher à partir de la mémoire.

Gestion des sockets


Sur le socket Web, nous interrogeons les données d'exécution des caméras depuis le backend. Le logiciel Python se connecte à Flussonic et interroge les données d'état des caméras: qu'elles fonctionnent ou non, y a-t-il de nouveaux événements?

D'un autre côté, le client se connecte et, via le socket Web, nous envoyons ces données au navigateur. Nous voulons transférer les données des clients en temps réel: la caméra s'allume et s'éteint, le chat mange, dort, déchire un canapé, appuie sur le bouton et éloigne le chat.

Mais, par exemple, une sorte de problème s'est produit: la base de données n'a pas répondu à la demande, tout le code est tombé, il y avait deux sockets ouvertes. Nous avons commencé à recharger, fait quelque chose, encore une fois ce problème - il y avait deux sockets. L'erreur de base de données a été traitée de manière incorrecte et deux connexions ouvertes ont été bloquées. Au fil du temps, cela conduit à des fuites de socket.

Variables globales oubliées


Faire un dict global pour la liste des navigateurs connectés via la prise Web. Une personne se connecte au site, nous lui ouvrons une prise web. Ensuite, nous mettons le socket Web avec son identifiant dans une sorte de dict global, et il s'avère qu'une sorte d'erreur se produit.

Par exemple, ils ont enregistré un lien de connexion dans dict pour envoyer des données. Une exception a fonctionné, oublié de supprimer le lien et les données ont été bloquées . Donc après un certain temps, 64 Go commencent à manquer et je veux doubler la mémoire du serveur. Ce n'est pas une solution, car les données fuiront de toute façon.
Nous faisons toujours des erreurs - nous sommes des personnes et nous ne pouvons pas tout suivre.
La question est que certaines erreurs se produisent, mĂŞme celles que nous ne nous attendions pas Ă  voir.

Excursion historique


Pour aborder le sujet principal, approfondissons l'histoire. Tout ce dont nous parlons maintenant de Python, Go et Erlang, d'autres personnes ont fait tout ce chemin il y a environ 30 ans. En Python, nous allons loin et comblons les bosses qui se sont déjà produites il y a des décennies. Le chemin se répète de manière étonnante.

Dos


D'abord, passons au DOS, c'est le plus proche. Avant lui, il y avait des choses complètement différentes et tout le monde n'est pas vivant qui se souvient des ordinateurs avant DOS.

Le programme DOS occupait l'ordinateur (presque) exclusivement . Pendant qu'un jeu, par exemple, est en cours d'exécution, rien d'autre n'est exécuté. Vous n'allez pas sur Internet - il n'y est pas encore, et vous n'irez nulle part. C'était triste, mais les souvenirs en sont chaleureux, car il est associé à la jeunesse.

Multitâche coopératif


Comme c'était vraiment douloureux avec DOS, de nouveaux défis sont apparus, les ordinateurs sont devenus plus puissants. Il y a des décennies, ils ont développé le concept de multitâche coopératif , même avant Windows 3.11.

Les données sont séparées par des processus, et chaque processus est effectué séparément: ils sont en quelque sorte protégés les uns des autres. Un mauvais code dans un processus ne pourra pas gâcher le code dans le navigateur (alors les premiers navigateurs sont déjà apparus).

La question suivante est: comment le temps de calcul sera-t-il réparti entre les différents processus? Ensuite, ce n'était pas qu'il n'y avait pas plus d'un cœur, un système à double processeur était une rareté. Le schéma était le suivant: alors qu'un processus est allé, par exemple, sur un disque pour les données, le deuxième processus reçoit le contrôle du système d'exploitation. Le premier pourra prendre le contrôle lorsque le second lui-même donnera volontairement. Je simplifie grandement la situation, mais le processus a en quelque sorte volontairement permis de le retirer du processeur .

Multitâche préemptif


Le multitâche coopératif a conduit au problème suivant: le processus pourrait simplement se bloquer car il est mal écrit. Si le processeur prend beaucoup de temps à traiter, il bloque le reste . Dans ce cas, l'ordinateur est tombé en panne et rien ne pouvait être fait avec, par exemple, la commutation de la fenêtre.

En réponse à ce problème, le multitâche préemptif a été inventé. Le système d'exploitation lui-même gère désormais strictement: supprime les processus de l'exécution, sépare complètement leurs données, protège la mémoire des processus les uns des autres et donne à chacun un certain temps de calcul. Le système d'exploitation alloue les mêmes intervalles de temps à chaque processus .

La question du décalage horaire est toujours ouverte. Aujourd'hui, les développeurs de systèmes d'exploitation trouvent toujours ce qui est juste, dans quel ordre, à qui et combien de temps à consacrer à la gestion. Aujourd'hui, nous voyons le développement de ces idées.

Streams


Mais cela ne suffisait pas. Les processus doivent échanger des données: via le réseau, cela coûte cher, mais reste en quelque sorte compliqué. Par conséquent, le concept de flux a été inventé.
Les threads sont des processus légers qui partagent une mémoire commune.
Les flux ont été créés dans l'espoir que tout sera facile, simple et amusant. Désormais, la programmation multi-thread est considérée comme anti-modèle . Si la logique métier est écrite dans des threads, ce code devrait très probablement être jeté, car il contient probablement des erreurs. S'il vous semble qu'il n'y a pas d'erreurs, vous ne les avez tout simplement pas encore trouvées.

La programmation multithread est une chose extrêmement complexe. Il y a peu de gens qui se sont vraiment consacrés à la capacité d'écrire sur les threads et ils obtiennent quelque chose de vraiment fonctionnel.

Pendant ce temps, des ordinateurs multicœurs sont apparus. Ils ont apporté des choses terribles avec eux. Il a fallu une approche complètement différente des données, des questions se sont posées avec la localité des données, maintenant vous devez comprendre à partir de quel noyau vous allez vers quelles données.

Un noyau doit mettre les données ici, l'autre là, et en aucun cas confondre ces choses, car les clusters sont apparus à l'intérieur de l'ordinateur. À l'intérieur d'un ordinateur moderne, il y a un cluster lorsqu'une partie de la mémoire est soudée à un cœur et l'autre à un autre. Le temps de transit entre ces données peut varier selon des ordres de grandeur.

Exemples Python


Prenons un exemple simple de «Service pour aider le client». Il sélectionne le meilleur prix pour les marchandises sur plusieurs plateformes: nous conduisons au nom des marchandises et recherchons des parquets avec un prix minimum.

C'est le code de l'ancien Django, Python 2. Aujourd'hui, il n'est pas très populaire, peu de gens démarrent des projets dessus.

@api_view(['GET']) def best_price(request): name = request.GET['name'] price1 = http_fetch_price('market.yandex.ru', name) price2 = http_fetch_price('ebay.com', name) price3 = http_fetch_price('taobao.com', name) return Response(min([price1,price2,price3])) 

Une demande arrive, nous allons dans un backend, puis dans un autre. Aux endroits où http_fetch_price est http_fetch_price , les threads sont bloqués. En ce moment, l'ensemble du travailleur se lance dans un voyage à Yandex.Market, puis à eBay, puis jusqu'à un timeout sur Taobao, et à la fin donne une réponse. Pendant tout ce temps, tout le travailleur est debout .

Il est très difficile d'interroger plusieurs backends en même temps. C'est une mauvaise situation: la mémoire est consommée, le lancement d'un grand nombre de travailleurs et la surveillance de l'ensemble du service sont nécessaires. Il est nécessaire de vérifier la fréquence de ces demandes, avez-vous encore besoin d'exécuter des travailleurs ou y en a-t-il encore d'autres. Ce sont les problèmes mêmes dont j'ai parlé. Il est nécessaire d'interroger successivement plusieurs backends .

Que voit-on en Python? Un processus par tâche, en Python il n'y a toujours pas de multicœur. La situation est claire: dans les langages de cette classe, il est difficile de créer un multicœur simple et sûr, car cela réduira les performances .

Si vous accédez à la dictée à partir de différents threads, l'accès aux données peut être écrit comme ceci: collez deux instances Python en mémoire afin qu'elles fouillent les données - elles les cassent simplement. Par exemple, pour aller dicter et ne rien casser, vous devez mettre des mutex devant. S'il y a un mutex avant chaque dict, le système ralentira environ 1000 fois - ce sera simplement gênant. Il est difficile de le faire glisser dans un multicœur.

Nous n'avons qu'un seul thread d'exécution et seuls les processus peuvent évoluer . En fait, nous avons réinventé DOS à l'intérieur du processus - le langage de script de 2010. À l'intérieur du processus, il y a une chose qui ressemble à DOS: pendant que nous faisons quelque chose, tous les autres processus ne fonctionnent pas. Personne n'a aimé les énormes dépassements de coûts et la lenteur des réponses.

Les réacteurs Socket sont apparus en Python il y a quelque temps, bien que le concept lui-même soit né il y a longtemps. Vous pouvez désormais vous attendre à la disponibilité de plusieurs sockets à la fois.

Au début, le réacteur est devenu en demande sur des serveurs comme nginx. Y compris en raison de l'utilisation correcte de cette technologie, elle est devenue populaire. Ensuite, le concept a rampé dans des langages de script comme Python et Ruby.
L'idée du réacteur est que nous sommes passés à une programmation orientée événements.

Programmation orientée événement


Un contexte d'exécution produit une demande. En attendant une réponse, un contexte différent est en cours d'exécution. Il est à noter que nous avons presque traversé le même stade d'évolution que la transition de DOS à Windows 3.11. Seuls les gens l'avaient fait 20 ans plus tôt, et en Python et Ruby, il est apparu il y a 10 ans.

Tordu


Il s'agit d'un cadre de réseau piloté par les événements. Il est apparu en 2002 et est écrit en Python. J'ai pris l'exemple ci-dessus et l'ai réécrit sur Twisted.

 def render_GET(self, request): price1 = deferred_fetch_price('market.yandex.ru', name) price2 = deferred_fetch_price('ebay.com', name) price3 = deferred_fetch_price('taobao.com', name) dl = defer.DeferredList([price1,price2,price3]) def reply(prices): request.write('%d'.format(min(prices))) request.finish() dl.addCallback(reply) return server.NOT_DONE_YET 

Il peut y avoir des erreurs, des inexactitudes et la gestion des erreurs notoire n'est pas suffisante. Mais le schéma approximatif est le suivant: nous ne faisons pas de demande, mais demandons d'aller chercher cette demande un peu plus tard, quand il y aura du temps. Dans la lignée de defer.DeferredList nous voulons rassembler les réponses de plusieurs requêtes.

En fait, le code se compose de deux parties. Dans la première partie, ce qui s'est passé avant la demande, et dans la seconde, ce qui s'est passé.
Toute l'histoire de la programmation événementielle est saturée de la douleur de casser le code linéaire sur «avant la demande» et «après la demande».
Cela fait mal car les morceaux de code sont mélangés: les dernières lignes sont toujours exécutées dans la requête d'origine, et la fonction de reply sera appelée après.

Il n'est pas facile de garder à l'esprit précisément parce que nous avons cassé le code linéaire, mais il fallait le faire. Sans entrer dans les détails, le code réécrit de Django à Twisted produira une pseudo-accélération complètement incroyable .

Idée tordue

Un objet peut être activé lorsque la prise est prête.
Nous prenons des objets dans lesquels nous collectons les données nécessaires du contexte et lions leur activation au socket. La disponibilité des sockets est désormais l'un des contrôles les plus importants pour l'ensemble du système. Les objets seront nos contextes.

Mais en même temps, le langage sépare toujours le concept même du contexte d'exécution dans lequel vivent les exceptions. Le contexte d'exécution vit séparément des objets et est vaguement lié à eux . Ici, le problème se pose avec le fait que nous essayons de collecter des données à l'intérieur des objets: il n'y a aucun moyen sans eux, mais le langage ne le prend pas en charge.

Tout cela conduit à un enfer de rappel classique. Pour ce qu'ils aiment, par exemple, Node.js - jusqu'à récemment, il n'y avait aucune autre méthode, mais elle apparaissait toujours en Python. Le problème est qu'il y a des ruptures de code aux points de l'IO externe qui conduisent à un rappel.

Il y a beaucoup de questions. Est-il possible de «coller» les bords de l'écart dans le code? Est-il possible de revenir au code humain normal? Que faire si un objet logique fonctionne avec deux sockets et que l'un d'eux est fermé? Comment ne pas oublier de fermer la seconde? Est-il possible d'utiliser en quelque sorte tous les cœurs?

Async io


Une bonne réponse à ces questions est Async IO. Il s'agit d'un pas en avant, mais pas facile. Async IO est une chose compliquée, sous le capot de laquelle il existe de nombreuses nuances douloureuses.

 async def best_price(request): name = request.GET['name'] price1 = async_http_fetch_price('market.yandex.ru', name) price2 = async_http_fetch_price('ebay.com', name) price3 = async_http_fetch_price('taobao.com', name) prices = await asyncio.wait([price1,price2,price3]) return min(prices) 

L'écart de code est caché sous la syntaxe async/await . Nous avons pris tout ce qui était avant, mais ne sommes pas allés au réseau dans ce code. Nous avons supprimé Callback(reply) , qui était dans l'exemple précédent, et l'avons caché derrière await - l'endroit où le code sera coupé avec des ciseaux. Il sera divisé en deux parties: la partie appelante et la partie rappel, qui traite les résultats.

C'est un excellent sucre syntaxique . Il existe des méthodes pour coller plusieurs attentes en une seule. C'est cool, mais il y a une nuance: tout peut être cassé par une prise "classique" . En Python, il existe encore un grand nombre de bibliothèques qui vont au socket de manière synchrone, créent une timer library et tout gâchent pour vous. Comment déboguer cela, je ne sais pas.

Mais asyncio n'aide pas avec les fuites et le multicœur . Par conséquent, il n'y a pas de changements fondamentaux, bien qu'il soit devenu meilleur.

Nous avons toujours tous les problèmes dont nous avons parlé au début:

  • facile Ă  fuir avec des prises;
  • facile de laisser des liens dans des variables globales;
  • gestion très minutieuse des erreurs;
  • il est toujours difficile de faire du multicĹ“ur.

Que faire


Si tout cela va évoluer, je ne sais pas, mais je vais montrer l'implémentation dans d'autres langages et plateformes.

Contextes d'exécution isolés. Dans les contextes d'exécution, les résultats sont accumulés, les sockets sont conservés: des objets logiques dans lesquels nous stockons généralement toutes les données sur les rappels et les sockets. Un concept: prendre des contextes d'exécution, les coller sur des fils d'exécution et les isoler complètement les uns des autres.

Changement de paradigme des objets. Connectons le contexte au thread d'exécution. Il y a des analogues, ce n'est pas quelque chose de frais. Si quelqu'un a essayé de modifier le code source d'Apache et d'y écrire des modules, il sait qu'il existe un pool Apache. Aucun lien autorisé entre le pool Apache. Les données d'un pool Apache - le pool associé aux requêtes, se trouve à l'intérieur, et vous ne pouvez rien en retirer.

Théoriquement, c'est possible, mais si vous le faites, soit quelqu'un grondera, soit il n'acceptera pas le correctif, soit il aura un débogage long et pénible sur la production. Après cela, personne ne fera cela et permettra aux autres de faire de telles choses. Il est tout simplement impossible de se référer aux données entre les contextes, une isolation complète est nécessaire.

Comment échanger une activité? Ce qu'il faut, ce ne sont pas de petites monades, qui sont fermées en elles-mêmes et ne communiquent pas entre elles. Nous avons besoin d'eux pour communiquer. Une approche est la messagerie. Il s'agit en gros du chemin emprunté par Windows lors de l'échange de messages entre processus. Dans un système d'exploitation normal, vous ne pouvez pas donner de lien vers la mémoire d'un autre processus, mais vous pouvez signaler via le réseau, comme sous UNIX, ou via des messages, comme sous Windows.

Toutes les ressources du processus et du contexte deviennent un fil d'exécution . Nous avons collé ensemble:

  • donnĂ©es d'exĂ©cution dans une machine virtuelle dans laquelle des exceptions se produisent;
  • le fil d'exĂ©cution, comme ce qui est exĂ©cutĂ© sur le processeur;
  • Un objet dans lequel toutes les donnĂ©es sont collectĂ©es logiquement.

Félicitations - nous avons inventé UNIX dans un langage de programmation! Cette idée a été inventée vers 1969. Jusqu'à présent, il n'est pas encore en Python, mais Python est susceptible d'y arriver. Et peut-être qu'elle ne viendra pas - je ne sais pas.

Qu'est-ce que ça donne


Tout d'abord, le contrôle automatique des ressources . À Moscou Python Conf ++ 2019, ils ont dit que vous pouvez écrire un programme sur Go et traiter toutes les erreurs. Le programme restera comme un gant et fonctionnera pendant des mois. C'est vrai, mais nous ne gérons pas toutes les erreurs.

Nous sommes des gens vivants, nous avons toujours des délais, le désir de faire quelque chose d'utile, et de ne pas gérer la 535ème erreur pour aujourd'hui. Un code parsemé de gestion des erreurs ne provoque jamais de sentiments chaleureux chez qui que ce soit.

Par conséquent, nous écrivons tous «chemin heureux», puis nous le découvrirons sur la production. Soyons honnêtes: seulement lorsque vous avez besoin de traiter quelque chose, nous commençons alors le traitement. La programmation défensive est un peu différente, et ce n'est pas un développement commercial.

Par conséquent, lorsque nous avons un contrôle automatique des erreurs - c'est très bien . Mais les systèmes d'exploitation l'ont créé il y a 50 ans: si un processus meurt, tout ce qu'il ouvre se ferme automatiquement. Aujourd'hui, personne n'a besoin d'écrire du code qui nettoiera les fichiers derrière le processus tué. Cela n'a pas existé depuis 50 ans dans n'importe quel système d'exploitation, mais en Python, vous devez toujours le suivre attentivement et soigneusement avec vos mains. C'est bizarre.

Vous pouvez amener l'informatique lourde dans un contexte différent , mais elle peut déjà aller dans un autre cœur. Nous avons partagé les données, nous n'avons plus besoin de mutex. Vous pouvez envoyer les données dans un contexte différent, par exemple: "Vous le ferez quelque part, puis dites-moi que vous avez terminé et fait quelque chose."

Une implémentation asyncio sans les mots "async / wait" . Un peu d'aide de la machine virtuelle, du runtime. C'est ce dont nous avons parlé avec async/await : vous pouvez également convertir en messages, supprimer async/await et l'obtenir au niveau de la machine virtuelle.

Processus d'Erlang


Erlang a été inventé il y a 30 ans. Les gars barbus, qui n'étaient pas très barbus à l'époque, ont regardé UNIX et ont transféré tous les concepts au langage de programmation. Ils ont décidé qu'ils auraient maintenant leur propre truc pour dormir la nuit et aller tranquillement pêcher sans ordinateur. Il n'y avait pas encore d'ordinateurs portables, mais les barbus savaient déjà que cela devait être pensé à l'avance.

Nous avons eu Erlang (Elixir) - des contextes actifs qui s'exécutent d'eux-mêmes . Poursuivez mon exemple sur Erlang. Sur Elixir, il ressemble à peu près, avec quelques variations.

 best_price(Name) -> Price1 = spawn_price_fetcher('market.yandex.ru', Name), Price2 = spawn_price_fetcher('ebay.com', Name), Price3 = spawn_price_fetcher('taobao.com', Name), lists:min(wait4([Price1,Price2,Price3])). 

Nous lançons plusieurs récupérateurs - ce sont plusieurs nouveaux contextes distincts que nous attendons. Ils ont attendu, collecté les données et renvoyé le résultat comme prix minimum. Tout cela est similaire à async/await , mais sans les mots «async / wait».

Caractéristiques d'Elixir


Elixir est situé à la base d'Erlang, et tous les concepts de langage sont tranquillement portés sur Elixir. Quelles sont ses fonctionnalités?

Interdiction des liaisons entre processeurs. Par processus, j'entends un processus léger à l'intérieur d'une machine virtuelle - le contexte. Simplifié, s'il est porté sur Python, les liaisons de données à l'intérieur d'un autre objet sont interdites dans Erlang. Vous pouvez avoir un lien vers l'objet entier sous forme de boîte fermée, mais vous ne pouvez pas référencer les données qu'il contient. Vous ne pouvez même pas obtenir syntaxiquement un pointeur vers des données qui se trouvent dans un autre objet. Vous ne pouvez connaître que l'objet lui-même.

Il n'y a pas de mutex à l'intérieur des processus (objets). C'est important - personnellement, je ne veux jamais jamais croiser dans ma vie l'histoire du débogage des vols multi-threads en production. Je ne souhaite cela à personne.

Les processus peuvent se déplacer autour des noyaux, c'est sûr. Nous n'avons plus besoin de contourner, comme en Java, un tas d'autres pointer et de les réécrire lors du déplacement de données d'un endroit à un autre: nous n'avons pas de données communes et de liens internes. Par exemple, d'où vient le problème de rareté de la hanche? En raison du fait que quelqu'un se réfère à ces données.

Si nous transférons des données à l'intérieur du tas vers un autre emplacement pour le compactage, nous devons parcourir tout le système. Il peut occuper des dizaines de gigaoctets et mettre à jour tous les pointeurs - c'est fou.

Sécurité complète des threads , car toute communication passe par des messages. À la reddition de tout cela, nous avons perdu le processus d'éviction . Il l'a obtenu facile et bon marché.

Les messages comme base de communication. À l'intérieur des objets, des appels de fonction ordinaires et entre les objets de message. L'arrivée des données du réseau est un message, la réponse d'un autre objet est un message, quelque chose d'autre à l'extérieur est également un message dans une file d'attente entrante. Ce n'est pas sous UNIX car il n'a pas pris racine.

Appels de méthode. Nous avons des objets que nous appelons processus. Les méthodes sur les processus sont appelées via des messages.

Les méthodes d'appel envoient également un message. C'est formidable que maintenant cela puisse être fait avec un timeout. Si quelque chose nous répond lentement, nous appelons la méthode sur un autre objet. Mais en même temps, nous disons que nous sommes prêts à attendre pas plus de 60 s, car j'ai un client avec un délai d'attente de 70 s. Je vais devoir aller lui dire "503" - viens demain, maintenant ils ne t'attendent pas.

De plus, la réponse à l'appel peut être différée . À l'intérieur de l'objet, vous pouvez accepter la demande d'appeler la méthode et dire: "Oui, oui, je vais vous poser maintenant, revenez dans une demi-heure, je vous répondrai." Vous ne pouvez pas parler, mais mettez-vous silencieusement de côté. Nous l'utilisons parfois.

Comment travailler avec un réseau?


Vous pouvez écrire du code linéaire, des rappels ou dans le style asyncio.gather . Un exemple de ce à quoi cela ressemblera.

 wait4([ ]) -> [ ]; wait4(List) -> receive {reply, Pid, Price} -> [Price] ++ wait4(List -- [Pid]) after 60000 -> [] end. 

Dans la fonction wait4 de l'exemple précédent, nous wait4 la liste de ceux dont nous attendons toujours les réponses. Si vous utilisez la méthode de receive , nous obtenons un message de ce processus, nous l'écrivons dans la liste. Si la liste est terminée, nous retournons tout ce qui était et accumulons la liste. Nous avons demandé en même temps trois objets pour nous conduire les données. S'ils ne se sont pas débrouillés ensemble en 60 secondes et qu'au moins l'un d'eux n'a pas répondu OK, nous aurons une liste vide. Mais il est important que nous ayons fait un délai général pour une demande immédiatement à tout un tas d'objets.

Quelqu'un pourrait dire: «Pensez, libcurl a la même chose.» Mais ici, il est important que d'autre part, il puisse y avoir non seulement un voyage HTTP, mais aussi un voyage DB, ainsi que certains calculs, par exemple, le calcul d'un nombre optimal pour le client.

Gestion des erreurs


Des erreurs sont passées du flux à l'objet, qui sont désormais identiques . Maintenant, l'erreur elle-même n'est pas attachée au thread, mais à l'objet où elle a été exécutée.

C'est beaucoup plus logique. Habituellement, lorsque nous dessinons toutes sortes de petits carrés et cercles sur le tableau dans l'espoir qu'ils prennent vie et commencent à nous apporter des résultats et de l'argent, nous dessinons généralement des objets, pas les flux dans lesquels ces objets seront exécutés. Par exemple, à la livraison, nous pouvons recevoir un message automatique sur la mort d'un autre objet .

Introspection ou débogage en production


Quoi de plus agréable que d'aller au prod et de débiter, surtout si l'erreur ne se produit que sous charge pendant les heures de pointe. Aux heures de pointe, nous disons:

- Allez, je vais recommencer maintenant!
- Sortez et il y a un redémarrage chez quelqu'un d'autre!

Ici, nous pouvons entrer dans un système vivant qui fonctionne actuellement et n'est pas spécialement préparé pour cela. Pour ce faire, vous n'avez pas besoin de le redémarrer avec le profileur, avec le débogueur, reconstruire.

Sans aucune perte de performances dans un système de production en direct, nous pouvons consulter une liste de processus: ce qui est à l'intérieur d'eux, comment tout cela fonctionne, les jeter, vérifier ce qui leur arrive. Tout cela est gratuit dès la sortie de la boîte.

Bonus


Le code est super fiable. Par exemple, Python a une fragilité avec l' old vs async , et il restera pendant cinq ans, rien de moins. Compte tenu de la vitesse à laquelle Python 3 a été implémenté, vous ne devriez pas espérer qu'il sera rapide.

La lecture et le suivi des messages sont plus faciles que le débogage des rappels . C'est important. Il semblerait que si nous avons encore des rappels pour le traitement des messages que nous pouvons voir, alors quoi de mieux? Par le fait que les messages sont une donnée en mémoire. Vous pouvez le regarder avec des yeux et comprendre ce qui est arrivé ici. Il peut être ajouté au traceur, obtenir une liste de messages dans un fichier texte. C'est plus pratique que les rappels.

Magnifique multicœur , gestion de la mémoire et introspection à l'intérieur d'un système de production en direct .

Les problèmes


Naturellement, Erlang a également des problèmes.

Perte de performance maximale due au fait que nous ne pouvons plus faire référence aux données dans un autre processus ou objet. Nous devons les déplacer, mais ce n'est pas gratuit.

La surcharge de la copie de données entre les processus. Nous pouvons écrire un programme en C qui s'exécutera sur les 80 cœurs et traitera un tableau de données, et nous supposerons qu'il le fait correctement et correctement. Dans Erlang, vous ne pouvez pas faire cela: vous devez couper soigneusement les données, les distribuer sur un tas de processus, suivre tout. Cette communication coûte des ressources - des cycles de processeur.

Est-ce rapide ou lent? Nous écrivons du code Erlang depuis 10 ans. Le seul concurrent qui a survécu à ces 10 années est écrit en Java. Avec lui, nous avons une parité de performance presque complète: quelqu'un dit que nous sommes pires, quelqu'un qu'il est. Mais ils ont Java avec tous ses problèmes, à commencer par JIT.

Nous écrivons un programme qui dessert des dizaines de milliers de sockets et pompe des dizaines de Go de données à travers lui-même. Soudain, il s'avère que dans ce cas, la justesse des algorithmes et la capacité de déboguer tout cela en production est plus importante que les chignons Java potentiels . Des milliards de dollars y ont été investis, mais cela ne confère au Java JIT aucun avantage magique.

Mais si nous voulons mesurer des repères stupides et dénués de sens, tels que "calculer les nombres de Fibonacci", alors Erlang sera probablement encore pire que Python ou comparable.

La surcharge de l'allocation des messages. Parfois ça fait mal. Par exemple, nous avons quelques morceaux en C dans le code, et à ces endroits, cela ne fonctionnait pas du tout avec Erlang. , , .

Erlang , , . , , receive send receive . — , . , , .

Python


. . , Python - .

, . - Python, , 20 , 40.

, . - , , Elixir, , .

Moscow Python Conf++ . , 6 4 . , , ) ) . Call for Papers 13 , 27 .

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


All Articles