Sommaire
Les systèmes unix/linux sont souvent vus comme vieux et le fait de barbus. Actuellement il y a une grande hype autour de « système réactifs ». Je me propose de faire le lien entre les 2 et d'expliquer comment c'est grâce à des bases unix très vieilles que l'on peut aujourd'hui construire des systèmes réactifs.
Tout est fichier
Dans unix tout est fichier. Cela se représente facilement quand on voit son système de fichier et par exemple /dev
qui montre sous forme de fichier tous les périphériques de votre machine. On peut le voir autrement dans le développement système. En effet toutes les io de vos programmes sont représentés par des descripteurs de fichier. Ils sont donc manipulables comme des fichiers et vous n'avez pas besoin de savoir qu'il s'agit d'une socket, d'un pipe ou d'un fichier pour lire ou écrire dedans.
Au sein des opérations sur les descripteurs de fichier, il y en a une sur laquelle je voudrais attirer votre attention. select(2)
est un appel système qui va permettre d'attendre sur plusieurs descripteurs de fichier en même temps. Par attendre, il est entendu pouvoir lire ou écrire sur ces descripteurs. Il s'agit donc d'un appel bloquant. Quand il revient il vous indique le nombre de descripteurs qui ont l'opération attendue de possible. Cela permet a un processus de manipuler plusieurs descripteurs. Pour information ça ne vient pas de sortir, d'après mon man cet appel est apparu avec 4.2BSD.
Le problème de select(2)
c'est qu'il est limité dans le nombre de descripteurs qu'il scrute (actuellement la limite sur linux est fixée à 1024). Plus récemment poll(2)
est apparu (normalisé pour la première fois avec POSIX.1-2001). Ce dernier prend un tableau et non plus un set de taille fixée, il n'a donc plus de limite dans le nombre de descripteurs de fichier. Il y a un super article (comme d'habitude) de Christophe Blaess sur le fonctionnement et l'implémentation dans le noyau linux.
Il existe aussi une solution plus moderne appelée epoll(4)
, mais elle est spécifique à linux. Une autre solution préconisée par FreeBSD, NetBSD et OpenBSD est d'utiliser les kqueue(2)
Certains (comme le développeur de curl) conseillent donc d'utiliser des bibliothèques de plus haut niveau qui vont fournir une API de plus haut niveau et qui vont pouvoir choisir en fonction du système l'appel le plus intéressant.
Le C10K problem
Le C10K problem est un problème relativement général qui constate que les serveurs n'arrivent pas à dépasser une limite de 10 000 connexions simultanées quel que soit le matériel qui est dessous (une page bien détaillée bien qu'un peu vieille). Suite à l'identification de ce problème, différentes approches ont été proposées. Dont l'une qui m'intéresse plus que les autres ici : le pattern réacteur.
Il s'agit d'un pattern d'architecture. Son objectif est de démultiplexer des requêtes pour ensuite les traiter de manière synchrones et sérialisées. L'idée est simple : on a une boucle d'événements qui va attendre des requêtes et lorsqu'elle en reçoit elle va l'envoyer à l'handler approprié. Le tout de manière totalement synchrone. Ce fonctionnement est utilisé dans nginx.
Nginx lance un processus master. Master lance un processus par cœur : ce sont les workers. Les workers écoutent la même socket (par exemple le port tcp 443 sur votre interface loopback). Lorsque l'on reçoit une requête, un processus va être réveillé et le faire travailler. Cette technique est devenue très efficace depuis l'introduction de l'option SO_REUSEPORT
dans le noyau 3.9. On peut trouver une description plus complète sur quora.
Mais pourquoi ?
Les méthodes classiques consistent à associer une requête à un thread (voir un processus). Cette solution génère plus de fils d'exécution que de CPU. Cela entraîne une forte pression sur l'ordonnancement du noyau. Le contexte de chacun de ses fils est aussi assez coûteux. Enfin, même s'il existe des techniques pour améliorer cela la création/libération des threads est assez coûteuse. Tout cela contribue à faire exploser les serveurs avant la limite des 10k connexions.
Dans la vie quotidienne, ça augmente les ressources utilisées et la latence. Or aujourd'hui pour beaucoup de cas d'usage, c'est la latence qui est critique (l'exécution pure est fortement optimisée et on n'a pas un volume de données important). Une partie importante des évolutions de HTTP2 par exemple vont dans ce sens. On peut aussi voir une grosse part du travail fait par google sur les couches réseau du noyau linux.
Un article intéressant sur le sujet écrit par Philippe Prados : Multitâche ou réactif ?.
Pour HTTP2 l'interview en 2 parties de Dridi Boukelmoune est intéressante et dans la seconde partie il explique clairement que l'objectif, c'est la latence (partie 1 et partie 2)
La hype !
On peut encore aller plus loin… Tout est fichier ! On peut généraliser ce comportement ! On peut aller jusqu'à envoyer toutes les requêtes sur la même boucle d'événement. Ainsi à chaque IO, on donne éventuellement à une autre requête (ou une autre IO) la possibilité de s'exécuter. On minimise encore le temps d'attente des fils d'exécution. Cette démarche permet de tirer parti des infrastructures du noyau et de tenter de maximiser le temps d'exécution utile du CPU. Et s'adapte particulièrement bien à des logiciels qui font beaucoup d'IO (accès à des fichiers, à une ou plusieurs bases de données, à un serveur d'authentification,…) par rapport à leur temps d'exécution réel (pas d'algorithme particulièrement compliqué).
Il y a différentes implémentation de ça :
- Node.js est la plus connue. Je pense que le fait de voir du js se hisser à ce niveau de performance a poussé à avoir des explications.
- celle que je connais le mieux : Vert.x : une suite de bibliothèques qui fournit une boucle d'évènements, des API pour un peu tout (faire du réseau, de la base de données, etc) qui sont asynchrones et s'interfacent avec la boucle d'évènements
- POE pour perl. Ici il n'est pas question de faire des appels asynchrones classiques à base d'handler, de future ou de promesses, mais plutôt avec des yields. POE permet de changer la boucle d'évènement ce qui peut être assez pratique.
- twisted pour python est très orienté réseau.
# Typo dans le titre
Posté par esdeem . Évalué à 4. Dernière modification le 06 mars 2018 à 16:28.
Je pense que ce journal n'a rien à voir avec la musique ancienne et que donc les vielles sont vieilles elles aussi.
0. Assume good faith 1. Be kind to other people 2. Express yourself 4. Apply rule 0
[^] # Re: Typo dans le titre
Posté par Marotte ⛧ . Évalué à 5. Dernière modification le 06 mars 2018 à 16:39.
Y’en a ptètre plein d’autre mais j’ai relevé celle là malgré une tolérance plutôt élevée aux fautes… Elle fait comme qui dirait « buter du cerveau » :)
[^] # Re: Typo dans le titre
Posté par barmic . Évalué à 2.
Arf désolé -_-'
[^] # Re: Typo dans le titre
Posté par gUI (Mastodon) . Évalué à 1.
Oui, il y a bcp de fautes, c'est dommage pour un article aussi intéressant.
En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.
[^] # Re: Typo dans le titre
Posté par Benoît Sibaud (site web personnel) . Évalué à 9.
J'ai fait une passe de relecture sur le journal.
[^] # Re: Typo dans le titre
Posté par barmic . Évalué à 3.
Merci beaucoup !
[^] # Re: Typo dans le titre
Posté par liberforce (site web personnel) . Évalué à 3.
Tu pourrais corriger la faute signalée dans ce fil stp ? Le "ont étaient" est toujours présent. Merci.
[^] # Re: Typo dans le titre
Posté par Benoît Sibaud (site web personnel) . Évalué à 4.
Corrigé, merci. Et merci une seconde passe et Grammalecte pour d'autres points.
[^] # Re: Typo dans le titre
Posté par Snark . Évalué à 1.
"un processus va être réveillé_", pas "réveillée"
[^] # Re: Typo dans le titre
Posté par Snark . Évalué à 1.
"interview en 2 parties est intéressant" -> "interview en deux parties est intéressante"
[^] # Re: Typo dans le titre
Posté par Snark . Évalué à 1.
"Node.js est le plus connu. Je pense que le fait de voir du js se hisser à ce niveau de performance à pousser à avoir des explications."
->
Node.js est la plus connue. Je pense que le fait de voir du js se hisser à ce niveau de performance a pouss& à avoir des explications."
(ceci dit je ne vois pas trop ce que "pousser à avoir des explications" peut vouloir dire…)
[^] # Re: Typo dans le titre
Posté par Benoît Sibaud (site web personnel) . Évalué à 3.
Corrigé, merci.
[^] # Re: Typo dans le titre
Posté par romi . Évalué à 1.
c'est la latence est critique -> c'est la latence qui est critique ?
[^] # Re: Typo dans le titre
Posté par romi . Évalué à 1.
On peut encore aller le loin… Tout est fichier ! -> On peut encore aller plus loin… Tout est fichier !
[^] # Re: Typo dans le titre
Posté par Benoît Sibaud (site web personnel) . Évalué à 3.
Corrigé, merci.
# Vieille solution
Posté par Nicolas Boulay (site web personnel) . Évalué à 3.
La méthode de remplacer un thread par travail à faire par une fonction cyclique, existe depuis longtemps dans la gestion des interruptions : cf https://en.wikipedia.org/wiki/Interrupt_coalescing
D'ailleurs, dans le domaine de l'embarqué, pour avoir des boucles ultra courtes, il s'agit simplement de fonction lancé par un timer à intervalle régulier sans gestion de la mémoire (tout est pré-alloué). Je ne sais pas si une personne a déjà essayer de faire un serveur qui pré-alloue les ressources pour gérer 11 000 connections à la fois.
"La première sécurité est la liberté"
[^] # Re: Vieille solution
Posté par barmic . Évalué à 2.
Je n'ai pas était assez clair, mais l'idée général n'est pas de dire que c'est particulièrement nouveau. Le développement de POE par exemple a commencé il y a 20 ans.
Par contre si je comprends bien l'interrupt coalescing. Il s'agit de remplacer les interruptions hardware pour les envoyer des une file. Pour réduire les interruptions et avoir un meilleur débit en augmentant la latence, là où la programmation réactive a pour objectif de réduire la latence quitte à limiter le débit.
[^] # Re: Vieille solution
Posté par Nicolas Boulay (site web personnel) . Évalué à 3.
De mémoire, c'est revenu à la mode, quand les cartes ethernet 1000mbits sont arrivé : les PC n'arrivaient pas à utiliser la moitié de la bande passante disponible. Les OS passaient leur temps dans la gestion d'interruption très bas niveau. Le but est de remplacer des cycles de gestion par des cycles vraiment utiles. Donc, oui, cela augmente le débit. Il n'y a pas de gestion logiciel de file, c'est le hardware qui gère.
Concernant la latence, il est faux de dire qu'une latence est basse quand il y a n threads pour n taches. En cas de forte charge, on a aucune idée de la latence maximale possible. Une fonction cyclique va fournir la même latence quelle que soit la charge.
Pour résumer, un système uniquement réactif aura une latence faible si il y a peu d’événements, dés que cela monte, une fonction cyclique offrira bien plus de garantie.
"La première sécurité est la liberté"
[^] # Re: Vieille solution
Posté par barmic . Évalué à 1.
Qu'entends-tu par fonction cyclique ?
Si tu regarde le lien que j'ai donné d'octo, il explique probablement mieux que moi. Si ton nombre de threads est lié au nombre de requêtes que tu reçois, tu augmente la latence avec l'augmentation du nombre de requêtes.
[^] # Re: Vieille solution
Posté par Nicolas Boulay (site web personnel) . Évalué à 3.
Une fonction cyclique est une fonction qui est exécutée toutes les millisecondes par exemple.
A moins d'avoir un sacré bon scheduler, l'augmentation ne sera pas linéaire du tout. Une fois que n est bien plus grand que le nombre de cpu, "l'overhead" de gestion grandit avec le nombre de changements de contexte, etc…
"La première sécurité est la liberté"
# Retour vers le passé
Posté par Bruno Michel (site web personnel) . Évalué à 10.
J'ai l'impression de revenir dans le passé, d'une bonne dizaine d'années, avec ce journal.
poll
,epoll
etkqueue
doivent tous exister depuis au moins 15 ans, ça me fait bizarre d'en entendre parler avec "plus récemment".Pareil, ce problème est vieux. Actuellement, même une Raspberry Pi doit arriver à surmonter ça sans problème. De nos jours, la limite pour un serveur doit plutôt être dans les centaines de milliers de connexions dans la majorité des cas, et quelques millions pour les implémentations un peu plus poussées. J'ai par exemple en tête le travail sur Phoenix, un framework web écrit en Elixir et qui tourne donc sur la machine virtuelle d'Erlang, pour atteindre les 2 millions de websockets sur un seul serveur (2015). En cherchant rapidement, j'ai trouvé un truc en Java qui tient les 10 millions de connexions. Aucun de ces deux exemples n'utilisent de la programmation événementielle.
Je me rappelle qu'il y a quelques années, vers 2010-2012, je m'intéressais à la programmation événementielle, d'abord avec EventMachine en Ruby, puis avec Node.js. Mais, ça donnait du code compliqué et les autres langages ont fini par faire mieux (Go notamment). D'ailleurs, je trouve intéressant l'entretien avec Ryan Dahl, créateur de Node.js, où il explique que Go a un meilleur modèle d'IO que nodejs pour les serveurs. Voici une citation rapide, mais il y a plus de détails dans l'entretien complet :
[^] # Re: Retour vers le passé
Posté par barmic . Évalué à 7.
L'objectif était justement de faire le lien entre tout ce qu'on peut lire aujourd'hui sur le réactif et le fait que ça prend ses racines dans des choses qui ne sont pas particulièrement récent (j'ai bien décris à quel moment chacun de ses appels est apparu justement).
Elixir par construction se base sur le système à acteurs d'erlang qui est une manière évènementielle de coder.
Pour MigratoryData, c'est basé sur du publish-subscribe qui est classé dans l'event driven programming sur wikipedia.
Après je n'ai pas caché qu'il y a d'autres solutions, j'ai dis explicitement que je m'intéressais à l'une d'entre elles.
Ça peut devenir plus compliqué, je suis d'accord, mais les API comme Rx sont très populaires aujourd'hui et je trouve que les Functional Reactive Programming sont plutôt lisible (voir elm par exemple, même si ça n'est pas pour du serveur).
Tout ce que je vois dans l'article c'est de dire que les coroutines c'est mieux que les callback-hell, mais il y a bien d'autres API sur de l'asynchrones (les futures, les promesses, les observables,…). Il dit lui même que l'introduction du mot clef "async" doit avoir changer ce problème.
[^] # Re: Retour vers le passé
Posté par Bruno Michel (site web personnel) . Évalué à 5. Dernière modification le 06 mars 2018 à 18:51.
Je suis un peu perdu, mais pour des serveurs, si les systèmes à base d'acteurs/goroutines/green threads sont des manières événementielles de coder, qu'est-ce qui ne l'est pas ?
De mon point de vue, il y a d'un côté la programmation événementielle, avec un seul thread, une boucle événementielle et des callbacks (potentiellement abstraits via des promises, futures, observables). Et de l'autre côté, on a des modèles qui s'exécutent sur plusieurs threads et du parallélisme. Nodejs est dans la première catégorie, Elixir et Go dans la seconde.
Pour les serveurs où les performances (et notamment la latence) sont importantes, j'ai l'impression que le premier modèle est nettement en déclin (la hype, c'était plutôt vers 2010-2013), avec notamment beaucoup de Go. Nodejs a plutôt l'air de survivre auprès des développeurs Front (que ce soit pour des outils à la webpack, browserify, babel, etc. ou pour avoir le même code Angular/React/Vue qui tourne côté client et côté serveur).
Par contre, pour ce qui passe dans une interface graphique ou dans le navigateur, oui, là, je trouve que le modèle de programmation événementielle colle bien. Et ça a l'air plus vivant. Mais les raisons ne sont pas les mêmes et l'historique non plus (JavaScript en 1995, mais je crois que les débuts datent des bibliothèques graphiques de Smalltalk dans les années 80).
Non, il dit que JavaScript a fait des progrès, notamment avec le mot clef
async
, mais que le modèle du JavaScript reste inférieur pour les serveurs à celui de Go.[^] # Re: Retour vers le passé
Posté par totof2000 . Évalué à 4.
Je pense justement qu'Erlang est un mix des 2 : Erlang est basé sur des processus, de la même façon que d'autres langages sont basés sur des objets. Donc pour Erlang tu as un tas de processus (cas 2) et pour chaque processus tu as une boucle evenementielle et des callbacks. Donc dire qu'Erlang est 1 ou qu'Erlang est 2 est vrai, tout dépend à quel niveau tu regardes.
[^] # Re: Retour vers le passé
Posté par barmic . Évalué à 4.
Là tu décris le paterne reactor. C'est une façon de faire de l'évènementiel. On doit pouvoir dire qu'il s'agit de tout ce qui est réactif.
Il y a un tas de façons de gérer ça donc à part dire que c'est de la programmation parallèle, je ne vois pas bien quoi en dire de plus.
Je n'ai pas réussi à trouver l'architecture interne de haproxy ou de sozu […] je viens de trouver une conférence d'un développeur de sozu. Je te l'ai mis au timecode qui parle de ça, ils font de l'eventloop monothread. Pour ceux qui ne connaissent pas sozu est un reverse proxy sorti l'an dernier (et open source) dont l'objectif est la performance et la capacité à n'avoir aucune downtime y compris lors de ses mises à jour et des changements de configuration. Ils l'ont écris en rust et sont d'abord aller voir ce qu'il y avait sur le marché. Je pense qu'on peut dire qu'ils ont une bonne vision de l'état de l'art du domaine (et au passage il explique que haproxy (comme nginx) est aussi une eventloop monothread).
Je connais mal les green threads comment ils se comportent par rapport aux contexte switch ? Je veux dire :
Tu aurais un exemple de service qui traite un paquet de requêtes en go par exemple ? J'aimerais bien regarder le modèle de threading utilisé.
J'ai volontairement pas parlé du front parce que le sujet est très différent.
Je n'ai probablement pas lu avec assez d'attention je n'ai pas trouvé ce passage.
[^] # Re: Retour vers le passé
Posté par Bruno Michel (site web personnel) . Évalué à 5.
Désolé, je ne comprends toujours pas. Ça veut dire quoi « être réactif » ? Qu'est-ce qui est du domaine de la programmation événementielle si ce n'est l'utilisation du patern reactor ? Si la programmation événementielle, c'est réagir à des événements, j'ai l'impression que la définition englobe toutes les implémentations côté serveur, je ne vois pas comment un serveur pourrait ne pas « réagir » à des requêtes réseau.
Merci, je vais regarder ça, ça m'intéresse beaucoup !
C'est de moins en moins vrai pour le côté mono-thread. Le multithread arrive pour haproxy et il me semble que nginx a des pools de threads de nos jours.
https://dave.cheney.net/2015/08/08/performance-without-the-event-loop est une introduction aux goroutines de Go de ce point de vue. En gros, chaque goroutine est très légère, on peut facilement en avoir des dizaines de milliers ou plus. Le runtime de Go lance plusieurs threads, un par coeur (en fait, c'est configurable via une variable d'environnement mais c'est la valeur par défaut) qui vont servir à exécuter ces goroutines. Et il lance également des threads pour les opérations bloquantes d'I/O. Il fait ensuite en sorte que chaque thread d'exécution exécute une goroutine pendant un laps de temps assez court, puis passe à une autre. Si une goroutine est en attente, que ce soit parce qu'elle va faire une opération d'I/O ou qu'elle attende d'envoyer ou recevoir une valeur sur un channel, elle n'est plus éligible à être exécuté sur ces threads.
Je ne pense pas que le runtime du langage fasse de meilleurs choix que CFS, par contre, il peut faire ce choix plus rapidement. Et un autre avantage est qu'une goroutine (ou plus généralement un green thread) coûte beaucoup moins cher en RAM. Le problème C10K était notamment hostile pour les programmes qui utilisaient un thread par connexion car la RAM était rapidement saturée.
Oui, on reste dans l'espace utilisateur. En général, les green threads marche en mode coopératif : ce n'est pas un scheduler qui vient interrompre l'exécution, mais le green thread qui vérifie régulièrement s'il doit laisser la main à un autre green thread (par exemple, lors du lancement du garbage collector). Enfin, les opérations à faire pour passer d'un green thread à un autre sont, il me semble, moins couteuses que pour passer d'un thread à un autre.
Je ne sais pas.
[^] # Re: Retour vers le passé
Posté par barmic . Évalué à 2.
C'est le mot à la mode pour dire qu'on utilise un pattern reactor (AM*H*A).
Pour moi une distinction importante consiste dans le fait de recevoir un évènement et que cet évènement contient toute l'information utile à mettre en rapport avec le fait de tout envoyer dans le contexte d'un fil d'exécution. Quand tu spaw un thread par requête et que ton framework te positionne dans le contexte de ton thread toute l'information utile (la requête, l'authentification, etc), tu peux difficilement dire que tu es évènementiel. C'est typiquement ce que fais Spring par exemple. Il utilise intensivement le thread local storage.
Merci pour le lien. De ce que j'ai l'impression il remplace les appels asynchrones par des appels synchrones, mais qui indique qu'il faut les préempter et c'est la différence avec CFS. Par contre je ne vois pas comment ça pourrait bien se passer en terme de cache. Le context switch est drastiquement plus léger, mais il est toujours là, on invalide forcément tous les caches, etc.
Merci pour les exemples. J'ai l'impression que les green threads sont là pour être plus simple qu'un monde juste asynchrone. Mais je n'arrive pas à voir cela comme véritablement plus efficace que d'autres solutions pour pallier aux callback hell.
[^] # Re: Retour vers le passé
Posté par barmic . Évalué à 1.
C'est intéressant. En fait il n'ont fait presque qu'un code glue autour de groupcache et les API étant bien faites, il n'a pas besoin de faire de copies intermédiaires entre groupcache et la réponse HTTP. Ce qui joue nettement sur le débit ce qui est leur priorité. J'ai pas vu d'endroit où il parlait de la manière dont il charge les données depuis VCS.
C'est marrant, il parle d'utiliser un système de fichiers distribué, mais n'en utilise pas.
Pour ce qui est des event loop :
splice(2)
Sympa, au final ils ont une perf inférieure à nginx. J'aime bien les conclusions du bench qu'ils mettent en avant :
Mais c'est un reverse proxy qui a l'air super sympa (plus que sozu pour mon usage perso), je vais l'essayer.
# Sympa ton journal
Posté par Michaël (site web personnel) . Évalué à 8. Dernière modification le 07 mars 2018 à 00:32.
Le premier point que je voudrais ajouter, c'est que NodeJS utilise libuv qui a été développé pour et est utilisée pour plein d'autres langages. En plus de langages relativement classiques on la trouve pour OCaml et Lisp ou la version de haut niveau par exemple.
Le gros problème de l'approche de NodeJS est qu'elle propose une approche de la programmation événementielle basée sur des callbacks et qu'on finit invariablement par avoir du code spaghetti assez indigeste, pas super facile à maintenir – et pas non plus super facile à mettre au point. Un des gros problèmes de la programmation par callbacks est qu'elle inverse l'ordre de composition des fonctions. Au lieu d'écrire
Applique f au résultat de g
on écritFait g puis donne son résultat à f
ce qui lorsqu'on empile les appels de fonctions anonymes pour produire la pyramide du destin où le code se déplace plus rapidement vers la droite que vers la complétion de sa tâche. ;)En pratique le
step1
est toujours un truc un peu banal du type “print” ou “exit” et le biscuit (les variables intéressantes et les appels de fonction intéressants) sont enterrés dans les niveaux inférieurs – même avec de l'habitude c'est assez malheureux et on a du mal à suivre le code ni à retrouver les parties intéressantes.Pour retomber sur ses pattes il y a la programmation par monades qui permet de recomposer les traitements “dans le bon sens” – que l'on retrouve sous plusieurs formes de façon parfois un peu cachée comme les promises.
[^] # Re: Sympa ton journal
Posté par Moonz . Évalué à 6. Dernière modification le 07 mars 2018 à 08:44.
En JS moderne tu as
await
pour éviter ça[^] # Re: Sympa ton journal
Posté par barmic . Évalué à 2.
C'est une autre façon de présenter les promesses, les futures et les observables oui :)
Je suis d'accord que c'est totalement infernal de travailler à base de callback. Ça rend très complexe le moindre refactoring aussi et ça empêche de voir certain partern dans le code.
[^] # Re: Sympa ton journal
Posté par Axioplase ıɥs∀ (site web personnel) . Évalué à 3.
Mouais. Tu peux aussi utiliser un langage avec des vrais macros… Et ne pense pas que "la programmation par monades" veuille dire grand-chose. Tu penses (probablement) à une monade spécifique qui fait de la composition de fonctions. En fait, le code que tu montres, on dirait du code en CPS…
[^] # Re: Sympa ton journal
Posté par kantien . Évalué à 4.
Bah, c'est la monade identité, qui n'est pas spécialement la plus utile.
Par contre, je comprends pas trop où veut en venir Michaël : avec les monades et leur
bind
, on les utilise plutôt dans le sens inverse de la composition (à la manière du pipe du shell).Là j'ai trois types A, B et C et deux fonctions
f : A -> B
etg : B -> C
. Si je les compose, j'obtiens une fonction de A vers C. On peut le dire en mode direct (à la mode mathématique) : appliqueg
au résultat def
; ou en mode inverse : passe le résultat def
àg
. La conception à la manière d'une pipeline, c'est de le voire comme sur le dessin en mode inverse : on écrit les traitements dans l'ordre où ils sont effectués (f
puisg
).Après une monade, c'est un type paramétrique (ici
F
sur le dessin) avec certaines bonnes propriétés. Tout d'abord on doit pouvoir faire unmap
dessus : à partir def : A -> B
, on peut construiremap f : F(A) -> F(B)
qui respecte la composition. Si je compose f et g, puis que j'applique map, ou que j'applique map puis que je compose, on doit obtenir le même résultat.Ensuite, pour que ce type paramétrique soit une monade, il faut pouvoir combiner une fonction
A -> F(B)
avec un autreB -> F(C)
: ce sont les diagonales que l'on cherche à assembler. Mais pour ce faire, on utilise plutôt l'opérateurbind
(ou>>=
en notation infixe) qui prend une valeur de typeF(A)
, une fonction de typeA -> F(B)
et renvoie une valeur de typeF(B)
. C'est l'équivalent du pipe pour les monades : le pipe est à la composition de fonctions, ce que le bind est à la composition d'opérateurs monadiques.Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Sympa ton journal
Posté par Michaël (site web personnel) . Évalué à 2.
Effectivement je me suis un peu emmêlé les pinceaux, l‘habitude du OCaml et de la composition »contravariante«. Mais effectivement l‘important est que l‘ordre du texte corresponde à celui du traitement.
Les langages avec des vraies macros , comme Collin Lisp que je cite permettent de tout programmer de façon assez agréable. Par exemple avec cl-async, on peut presque réutiliser son code synchrone avec des promises, il suffit presque de changer les let en alet;)
[^] # Re: Sympa ton journal
Posté par kantien . Évalué à 2.
Avec l'extension de syntaxe de Lwt aussi ;-).
Dans
utop
, il suffit de faire :Pour une version avec
bind
, proche de la syntaxe NodeJS, où on passe la promesse à une fonction anonyme :La même, mais avec l'extension de syntaxe : on retrouve une forme proche de celle d'un code synchrone (les
let x = e in
sont remplacés par deslet%lwt x = e in
) :Il y a une forme (à base de
%lwt
à rajouter) pour presque toutes les constructions syntaxiques du langage.Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Sympa ton journal
Posté par Michaël (site web personnel) . Évalué à 2.
Je m'en suis très librement inspiré inspiré pour coder le PPX de Lemonade (the sparkling monad library). ;)
[^] # Re: Sympa ton journal
Posté par Michaël (site web personnel) . Évalué à 3.
Pour être très précis je pense en l‘espèce à la monade utilisée dans Lwt (OCaml) Comme le dit kantien, c‘est la monade qui enferme chaque valeur dans thread (pas nécessairement un Thread système) calculant cette valeur. La monade définit de plus une finction spéciale qui me permet de passer la valeur à l‘ordonnanceur.
C‘en est non? Peut-être pas de façon parfaite »selon la définition du livre« mais on pourrait le déformer légèrement pour qu‘il s‘y plie.
# Intéressant
Posté par stopspam . Évalué à -5.
ça fait un moment que je ne m'étais pas arrêter sur une page linuxfr, me contentant de marquer comme lu sur mon flux rss.
Beau journal.
Arf sérieux, l'éditeur est toujours aussi pourrit et peu pratique. On est en 2018, sortez-vous enfin les doigts.
[^] # Re: Intéressant
Posté par Dr BG . Évalué à 10.
Ils sont où les tiens ?
[^] # Re: Intéressant
Posté par stopspam . Évalué à 1.
sur mon clavier
# Petit joueur
Posté par benoar . Évalué à 7.
Et cette « boucle d'évènement » unique, elle s'appelle… ton noyau. Démultiplexer des « requêtes » selon un sélecteur donné, c'est ce que fait le noyau quand il répartit les paquets reçus du réseau selon l'adresse et le port de destination du paquet (oui, je me suis placé à un niveau « en dessous »). Forcément, quand tu as oublié qu'il existait autre chose que le Web et le port 80, ça peut faire bizarre comme sensation.
Moui… tu viens de décrire l’ordonnanceur du noyau, quoi, qui répartit les tâches entre différents processus représentant différentes applications qui écoutent sur des ports différents, non ?
En fait, la manie du tout-Web a amené de gros anciens SPOF sous forme dispatchers de requêtes HTTP.
Alors imagine quand ton sélecteur est à un offset donné dans ton paquet (= port de destination), facile à parser, qu'en plus en cas de problème de charge tu veuilles pouvoir répartir les requêtes avec l'assistance du client (= adresse IP de destination, obtenue par la base géante clé-valeur qui s'appelle DNS parmi un ensemble façon round-robin avec priorités, autrement appelés enregistrements SRV), ou alors si tu gardes une seule IP mais que tu veux pouvoir faire de la répartition de charge y compris sur ton architecture réseau amont (= flow ID ; seulement en IPv6), tu auras alors l'architecture ultime, distribuée, et qui en plus n'aura pas d'état incrusté dedans (= connexion HTTP ; la connexion n'a de sens que pour nœuds aux extrémités) et qui scalera à mort. Et tu pourras l'appeler « Internet ».
Sans parler de comment tu gères ta file quand elle est pleine, parce que le tail-drop ça fait plus de 20 ans qu'on sait que c'est mauvais.
[^] # Re: Petit joueur
Posté par barmic . Évalué à 2. Dernière modification le 07 mars 2018 à 16:02.
Je décris justement que les systèmes d'exploitation de type unix ont déjà implémenté pour toi tout ça (c'est la première partie de mon billet). L'article de Christophe Blaess est vraiment bien sur le sujet.
Il n'y a pas que le monde tcp/udp (comme les fichiers par exemple) et ça tombe bien ce que les SE implémente permet aussi de les gérer (cf: titre de la première partie ;) ).
Non pas vraiment. Il s'agit ici d'avoir une granularité assez fine pour faire un switch non pas sur les threads (ce qui est plutôt lourd), mais au sein d'un même thread.
Et c'est toi qui parle que de TCP ? ;) L'accès aux fichiers ou à certains périphériques entre très bien dans ce modèle (je parle d'IO tout le long pas de HTTP).
J'ai pas retrouvé rapidement de sources, mais je sais que vertx fait très gaffe à ça. Il fait attention à s'appuyer sur le SE pour gérer la congestion TCP par exemple. Tu peux aussi, pour d'autres besoins, utiliser Rx et sa gestion de la backpressure.
[^] # Re: Petit joueur
Posté par Michaël (site web personnel) . Évalué à 4. Dernière modification le 08 mars 2018 à 00:20.
Oui, le modèle de gestion du multitâche popularisé par NodeJS a plusieurs intérêts. Le premier c'est qu'en gros il fournit un cadre assez simple de programmation multi-thread en proposant de prendre en charge complètement un thread dédié aux appels systèmes et un autre qui s'occupe de traiter les évènements correspondant à ces appels. Comme programmeur, je sais qu'a tout moment je n'ai qu'un fil d'éxécution et donc je peux laisser tomber les mutexes, j'ai des programmes moins difficiles à déboguer que le “multi-thread” total. Le second effet, qui n'est pas négligeable, est que si j'ai besoin d'ajuster la capacité de mon application, je n'ai qu'un seule variable à considérer: le nombre de processus que je veux faire tourner sur une machine. Dans le monde “complètement multi thread” j'ai un espace beaucoup compliqué à explorer: pour mon serveur apache, est-ce qu'il faut mieux 16 processus avec chacun 2 thread, 8 processus avec chacun 4 threads, etc.? Quelle stratégie utiliser pour allouer les threads dans mon application? Avoir un modèle simple pour décrire la capacité de son application est appréciable – quitte à potentiellement sacrifier un peu en efficacité en acceptant de ne pas explorer certaines formes d'organisation.
[^] # Re: Petit joueur
Posté par groumly . Évalué à 4.
Je suis loin d’etre convaincu que les promises soient un modèle simple,surtout en JavaScript.
!!!
Juste parce que t’as un seul thread ne veut pas dire que tes parties critiques ne vont pas s’executer “entrelacées”. Si t’avais besoin de mutex avant, t’as toujours besoin de mutexes après.
Linuxfr, le portail francais du logiciel libre et du neo nazisme.
[^] # Re: Petit joueur
Posté par Michaël (site web personnel) . Évalué à 2.
C'est vrai, mais ce que j'ai oublié d'ajouter est qu'on a affaire à un modèle de programmation collaboratif et non préemptif – ce qui signifie que je suis sûr que personne ne touche à ma variable en même temps que je la manipule. Et comme les mutexes sont là pour rendre atomiques du point de vue de l'ordonnanceur des opérations qui en réalité ne le sont pas on n'en a plus besoin.
[^] # Re: Petit joueur
Posté par groumly . Évalué à 2.
Jusqu’au jour où tu fais un appel de fonction un temps soit peu lourd en cpu qui va rendre la main à ton event loop et paf le bug.
Linuxfr, le portail francais du logiciel libre et du neo nazisme.
[^] # Re: Petit joueur
Posté par Michaël (site web personnel) . Évalué à 2.
Non c'est justement ce qui est garanti de ne jamais arriver: en NodeJs par exemple il n'y a à tout moment qu'au plus un fil d'exécution qui execute du JavaScript.
[^] # Re: Petit joueur
Posté par groumly . Évalué à 2. Dernière modification le 09 mars 2018 à 08:21.
Ca n’a aucun sens, c’est comme si je te disait qu’il est impossible d’avoir des problèmes de concurrence sur un pentium 4 parce qu’il est monocoeur.
Ta section critique que t’es garantie qu’elle ne sera jamais interrompue, ça ne marche que si elle fait un traitement synchrone, i.e. qui bloque ton event loop. Bloquer une event loop, c’est mal, ça tue la latence, du coup on limite ça à des opérations très légères. Les opérations plus lourdes font un bout de processing, et rendent la main à l’event Loop, pour continuer leur processing à la boucle suivante. J’imagine que je ne t’apprends rien en disant ça.
Il suffit d’introduire un seul appel de fonction un tant soit peu costaud en cpu, genre un hash crypto, ou que sais je encore, et t’es force de repasser en asynchrone. Ta garantie que ta section critique ne sera pas interrompue vient de partir en fumée, et tu te retrouves avec du code méchamment bugge.
Dit autrement, ton approche marchotte tant que ton appli est triviale et/ou ne prend aucune charge (ce qui en pratique est la réalité de beaucoup d’appli node), et se ramasse/se bloque dès que tu monte en charge (ce qui est aussi en pratique la réalité de pas mal d’appli node), mais je pourrais dire exactement la même chose de n’importe quel modèle de programmation.
En pratique, le modèle “collaboratif”, il te force à rendre la main en permanence, et t’es pas mieux loti qu’avec du préemptif. Ou alors, c’est que ton service n’est pas utilisé. C’est sur que n’avoir aucune charge aide a ne pas avoir de bugs, m’enfin, ça me paraît pas être flatteur, ni pertinent.
Linuxfr, le portail francais du logiciel libre et du neo nazisme.
[^] # Re: Petit joueur
Posté par Bruno Michel (site web personnel) . Évalué à 3.
Un des gros inconvénients du modèle de Nodejs est d'avoir un seul fil d'exécution en collaboratif. Cela veut dire que si ce fil d'exécution est bloqué pendant 100ms pour calculer le hash d'un mot de passe avec une fonction moderne de hashage comme scrypt, il n'y a aucune autre requête HTTP qui va être traitée pendant ce temps et la latence va augmenter de 100ms pour toutes les requêtes de ce processus.
Je préfère le modèle de multi-tâche de Go. En tant que développeur, je peux créer plein de goroutines (des fils d'exécution très légers) et le runtime de Go les place dans des threads (en préemptif). On a également la distinction entre les threads dédiées aux appels système et les autre threads dédiés aux traitements. Le nombre de threads dédiés aux traitements est la variable d'ajustement pour la montée en puissance, il est limité par la variable d'environnement
GOMAXPROCS
. Mais, par défaut, c'est le nombre de cœurs de la machine et c'est une très bonne valeur par défaut. J'apprécie beaucoup d'avoir par défaut la version la plus efficace, alors que pour Nodejs, ça demande souvent encore un peu de boulot de passer d'un seul à plusieurs processus : écrire le petit bout de code pour cluster, faire attention aux variables globales qui ne le sont plus (cache, pools de connexions à la base de données), etc.[^] # Re: Petit joueur
Posté par barmic . Évalué à 1.
Effectivement c'est un traitement lourd qui doit être fait hors de l'event loop. Je ne connais pas Node, mais vertx permet ça sans problème (soit pour un appel simple et ponctuel un enrobage, soit pour un truc plus sophistiquer utiliser une forme de worker (avec le quel tu communique via un bus d'évènement qui passe par l'event loop)).
Le lien que tu m'a donné plus haut parle de stack de 2Kio (pour Go 1.5 ça a peut être évolué) soit si tu en lance un par connexion pour le C10K, si je ne me suis pas trompé dans mes comptes on est dans les 20Tio de mémoire consommée.
Il y a un gros mélange entre une implémentation (Node) et un modèle théorique (reactor), avec vertx c'est une ligne.
[^] # Re: Petit joueur
Posté par ckyl . Évalué à 4.
Heu 2Kio * 10 000 çà doit être plus proche des 20 Mio que des 20 Tio non ? :)
[^] # Re: Petit joueur
Posté par barmic . Évalué à 2.
Oui je me suis lourdement trompé… Désolé.
[^] # Re: Petit joueur
Posté par Michaël (site web personnel) . Évalué à 4.
Ce serait effectivement une manière assez plus maladroite de faire! ;) Dans ce paradigme on n'attend que les “threads” collaborent entre eux: de temps en temps il faut donc rendre la main à la boucle. Plutôt qu'un long discours, un petit exemple:
(À tout hasard je précise que je suis loin d'être un spécialiste ou un amoureux de JavaScript, moi mon truc c'est plutôt OCaml, Lisp, SH et TeX. Donc mon code est très certainement assez médiocre. ;) )
Ma fonction
combine
déduit un hash des valeurs de a1 et a2, et j'utilise un “schéma de Fibonacci” pour faire une boucle, qui toutes les 20 itérations repasse la main à l'ordonnanceur pour collaborer un peu avec ses petits copains: il suffit de se retenir de faire le calcul et de demander à la boucle de continuer quand bon lui semble.Voici un code complet qui met ça dans un serveur HTTP
On peut tester avec quelques commandes
(while true; do curl 'localhost:8080?n=10'; done)
(while true; do curl 'localhost:8080?n=100'; done)
et(while true; do curl 'localhost:8080?n=10000'; done)
qui tournent dans des terminaux.C'est sympa cette distinction. En pratique ça se passe comment? Il faut annoter ses routines ou bien le compilateur s'en sort tout seul? Aussi c'est un peu bizarre de penser qu'une fonction change de catégorie si on la “pepper-printf” pour déboguer.
Si on cherche à faire facilement du redimensionnement de capacité, la bonne stratégie dépend du contexte. Si par exemple on travaille avec des ressources de calcul élastiques avec des instances de 2-4 cœurs qu'on ajuste à la capacité demandée on peut se passer complètement de la couche cluster et compter sur un reverse-proxy type haproxy ou bien par exemple le répartiteur de charge de docker ou bien le consul de hashicorp par exemple – ou tout autre composant qui va répartir le flux sur plusieurs processus qui tournent sur des serveurs différents. Et même si on a une machine grassouillette avec 192 cœurs dont on veut tirer parti, ce n'est pas forcément aberrant de procéder de la sorte aussi.
[^] # Re: Petit joueur
Posté par Bruno Michel (site web personnel) . Évalué à 4.
Oui, mais pourtant, je l'ai déjà vu plusieurs fois sur des projets nodejs en production. Ce n'est pas toujours facile de voir à l'avance où ça va se produire. Et même quand on le sait, ce n'est pas facile à transformer. Ça demande d'une part de modifier du code synchrone en code asynchrone (normalement, ce n'est pas très compliqué, c'est juste du travail de bucheron) et, d'autre part, il faut réussir à introduire le découpage qui va bien (là, c'est plus compliqué).
Si on prend ton exemple, et que l'on ajoute la version bloquante pour comparer :
On se rend compte que la version coopérative est beaucoup plus lente :
Les utilisateurs ne seront pas très contents s'ils doivent attendre 7 secondes à chaque fois qu'ils s'authentifient. Et il y a d'autres solutions, comme déporter ces calculs dans d'autres processus. Mon expérience de projets nodejs en production montre que ce n'est pas évident de trouver les parties de code qui vont poser problème (surtout qu'ils se situent en général pas dans le code que l'on écrit soi-même, mais souvent dans une des centaines de bibliothèques que l'on importe via npm) et, qu'en pratique, la latence n'est pas géniale.
Ça se passe comme en nodejs : le développeur n'a pas besoin d'annoter ça, le runtime du langage va faire en sorte que les calculs se passent dans le thread principal (pour nodejs) ou les threads principaux (pour go), et quand il y a une opération bloquant d'I/O, on passe ce contexte d'exécution sur un thread dédié aux I/O. Et quand l'opération d'I/O est finie, le contexte d'exécution est de nouveau dans un état où le scheduler peut l'exécuter sur un des threads principaux. La grosse différence, c'est qu'un processus nodejs n'utilise qu'un seul cœur (en gros) alors qu'un processus go peut utiliser tous les cœurs.
Oui, bien sûr, quand on fait des projets un peu conséquents, avec des besoins de performances ou de haute disponibilité, le langage est moins importante que l'architecture globale. Mais le fait qu'un processus nodejs ne sache utiliser qu'un seul cœur reste une bonne épine dans le pied quand on conçoit ces architectures.
[^] # Re: Petit joueur
Posté par barmic . Évalué à 1.
Il y a une stratégie de localisation pour ça ? Genre mettre toutes les goroutines "système" dans un même thread système ? Je crois qu'il y a des ordonnanceurs du noyau qui ont des euristiques pour détecter des motifs d'utilisation d'appels système. Je crois que quand j'avais lu ça c'était pour pondérer la priorité, du coup ça permettrait à un green thread qui ferait énormément d'appels systèmes de ne pas trop influencer négativement le thread système sur le quel il est).
[^] # Re: Petit joueur
Posté par barmic . Évalué à 2.
C'est marrant je vois plutôt l'inverse dans ce qui décris les green threads. C'est compliqué de gérer de l'asynchrone donc on veut rester sur un modèle à thread et que l'on me cache le fait que derrière c'est asynchrone (le runtime est obligé de faire un switch synchrone/asynchrone parce que bloquer un thread système qui celui-ci embarque un paquet de green thread).
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.