Performances d'E/S côté serveur : Node vs PHP vs Java vs Go
Publié: 2022-03-11Comprendre le modèle d'entrée/sortie (E/S) de votre application peut faire la différence entre une application qui traite la charge à laquelle elle est soumise et une autre qui s'effondre face à des cas d'utilisation réels. Peut-être que même si votre application est petite et ne supporte pas des charges élevées, cela peut avoir beaucoup moins d'importance. Mais à mesure que la charge de trafic de votre application augmente, travailler avec le mauvais modèle d'E/S peut vous faire mal.
Et comme dans la plupart des situations où plusieurs approches sont possibles, il ne s'agit pas seulement de savoir laquelle est la meilleure, il s'agit de comprendre les compromis. Promenons-nous dans le paysage des E/S et voyons ce que nous pouvons espionner.
Dans cet article, nous allons comparer Node, Java, Go et PHP avec Apache, discuter de la façon dont les différents langages modélisent leurs E/S, les avantages et les inconvénients de chaque modèle, et conclure avec quelques repères rudimentaires. Si vous êtes préoccupé par les performances d'E/S de votre prochaine application Web, cet article est pour vous.
Notions de base sur les E/S : rappel rapide
Pour comprendre les facteurs impliqués dans les E/S, nous devons d'abord passer en revue les concepts au niveau du système d'exploitation. Bien qu'il soit peu probable que vous deviez traiter directement bon nombre de ces concepts, vous les traitez indirectement via l'environnement d'exécution de votre application tout le temps. Et les détails comptent.
Appels système
Tout d'abord, nous avons des appels système, qui peuvent être décrits comme suit :
- Votre programme (en « espace utilisateur », comme on dit) doit demander au noyau du système d'exploitation d'effectuer une opération d'E/S en son nom.
- Un "appel système" est le moyen par lequel votre programme demande au noyau de faire quelque chose. Les spécificités de la mise en œuvre varient d'un système d'exploitation à l'autre, mais le concept de base est le même. Il va y avoir une instruction spécifique qui transfère le contrôle de votre programme au noyau (comme un appel de fonction mais avec une sauce spéciale spécifiquement pour faire face à cette situation). De manière générale, les appels système sont bloquants, ce qui signifie que votre programme attend que le noyau revienne à votre code.
- Le noyau effectue l'opération d'E/S sous-jacente sur le périphérique physique en question (disque, carte réseau, etc.) et répond à l'appel système. Dans le monde réel, le noyau peut avoir à faire un certain nombre de choses pour répondre à votre demande, notamment attendre que l'appareil soit prêt, mettre à jour son état interne, etc., mais en tant que développeur d'applications, vous ne vous en souciez pas. C'est le travail du noyau.
Appels bloquants ou non bloquants
Maintenant, je viens de dire ci-dessus que les appels système bloquent, et c'est vrai dans un sens général. Cependant, certains appels sont classés comme "non bloquants", ce qui signifie que le noyau prend votre requête, la place dans la file d'attente ou dans la mémoire tampon quelque part, puis revient immédiatement sans attendre que les E/S réelles se produisent. Ainsi, il "bloque" pendant une très courte période de temps, juste assez longtemps pour mettre votre demande en file d'attente.
Quelques exemples (d'appels système Linux) peuvent aider à clarifier : - read()
est un appel bloquant - vous lui transmettez un descripteur indiquant quel fichier et un tampon indiquant où fournir les données qu'il lit, et l'appel revient lorsque les données sont là. Notez que cela a l'avantage d'être agréable et simple. - epoll_create()
, epoll_ctl()
et epoll_wait()
sont des appels qui, respectivement, vous permettent de créer un groupe de handles à écouter, d'ajouter/supprimer des handlers de ce groupe puis de bloquer jusqu'à ce qu'il y ait une activité. Cela vous permet de contrôler efficacement un grand nombre d'opérations d'E/S avec un seul thread, mais je m'avance. C'est très bien si vous avez besoin de la fonctionnalité, mais comme vous pouvez le voir, c'est certainement plus complexe à utiliser.
Il est important de comprendre l'ordre de grandeur de la différence de temps ici. Si un cœur de processeur fonctionne à 3 GHz, sans entrer dans les optimisations que le processeur peut faire, il effectue 3 milliards de cycles par seconde (ou 3 cycles par nanoseconde). Un appel système non bloquant peut prendre de l'ordre de 10s de cycles pour se terminer - ou "relativement quelques nanosecondes". Un appel qui bloque la réception d'informations sur le réseau peut prendre beaucoup plus de temps - disons par exemple 200 millisecondes (1/5 de seconde). Et disons, par exemple, que l'appel non bloquant a pris 20 nanosecondes et que l'appel bloquant a pris 200 000 000 nanosecondes. Votre processus a juste attendu 10 millions de fois plus longtemps pour l'appel bloquant.
Le noyau fournit les moyens d'effectuer à la fois des E/S bloquantes ("lire à partir de cette connexion réseau et donnez-moi les données") et des E/S non bloquantes ("dites-moi quand l'une de ces connexions réseau contient de nouvelles données"). Et quel mécanisme est utilisé bloquera le processus d'appel pendant des durées très différentes.
Planification
La troisième chose qu'il est essentiel de suivre est ce qui se passe lorsque vous avez beaucoup de threads ou de processus qui commencent à se bloquer.
Pour nos besoins, il n'y a pas une grande différence entre un thread et un processus. Dans la vraie vie, la différence la plus notable en termes de performances est que, puisque les threads partagent la même mémoire et que les processus ont chacun leur propre espace mémoire, la création de processus séparés a tendance à occuper beaucoup plus de mémoire. Mais lorsque nous parlons de planification, cela se résume en fait à une liste de choses (threads et processus) dont chacun a besoin pour obtenir une tranche de temps d'exécution sur les cœurs de processeur disponibles. Si vous avez 300 threads en cours d'exécution et 8 cœurs pour les exécuter, vous devez diviser le temps pour que chacun reçoive sa part, chaque cœur s'exécutant pendant une courte période de temps, puis passant au thread suivant. Cela se fait via un « commutateur de contexte », faisant passer le CPU de l'exécution d'un thread/processus à l'autre.
Ces changements de contexte ont un coût qui leur est associé - ils prennent un certain temps. Dans certains cas rapides, cela peut être inférieur à 100 nanosecondes, mais il n'est pas rare que cela prenne 1000 nanosecondes ou plus selon les détails de mise en œuvre, la vitesse/l'architecture du processeur, le cache du processeur, etc.
Et plus il y a de threads (ou de processus), plus il y a de changement de contexte. Lorsque nous parlons de milliers de threads et de centaines de nanosecondes pour chacun, les choses peuvent devenir très lentes.
Cependant, les appels non bloquants indiquent essentiellement au noyau "appelez-moi uniquement lorsque vous avez de nouvelles données ou un nouvel événement sur l'une de ces connexions". Ces appels non bloquants sont conçus pour gérer efficacement des charges d'E/S importantes et réduire le changement de contexte.
Avec moi jusqu'ici ? Parce que vient maintenant la partie amusante : regardons ce que font certains langages populaires avec ces outils et tirons quelques conclusions sur les compromis entre la facilité d'utilisation et les performances… et d'autres informations intéressantes.
À noter, alors que les exemples présentés dans cet article sont triviaux (et partiels, avec seulement les bits pertinents affichés); l'accès à la base de données, les systèmes de mise en cache externes (memcache, etc.) et tout ce qui nécessite des E/S finira par effectuer une sorte d'appel d'E/S sous le capot qui aura le même effet que les exemples simples présentés. De plus, pour les scénarios où les E/S sont décrites comme « bloquantes » (PHP, Java), les lectures et écritures de requête et de réponse HTTP bloquent elles-mêmes les appels : encore une fois, plus d'E/S sont cachées dans le système avec les problèmes de performances qui en découlent prendre en compte.
Il y a beaucoup de facteurs qui entrent dans le choix d'un langage de programmation pour un projet. Il y a même beaucoup de facteurs lorsque vous ne considérez que les performances. Mais, si vous craignez que votre programme soit limité principalement par les E/S, si les performances des E/S sont déterminantes pour votre projet, ce sont des choses que vous devez savoir.
L'approche « Keep It Simple » : PHP
Dans les années 90, beaucoup de gens portaient des chaussures Converse et écrivaient des scripts CGI en Perl. Ensuite, PHP est arrivé et, même si certaines personnes aiment s'en moquer, cela a rendu beaucoup plus facile la création de pages Web dynamiques.
Le modèle utilisé par PHP est assez simple. Il existe quelques variantes, mais votre serveur PHP moyen ressemble à :
Une requête HTTP provient du navigateur d'un utilisateur et atteint votre serveur Web Apache. Apache crée un processus séparé pour chaque requête, avec quelques optimisations pour les réutiliser afin de minimiser le nombre de processus à effectuer (la création de processus est, relativement parlant, lente). Apache appelle PHP et lui dit d'exécuter le fichier .php
approprié sur le disque. Le code PHP exécute et bloque les appels d'E/S. Vous appelez file_get_contents()
en PHP et sous le capot, il effectue des appels système read()
et attend les résultats.
Et bien sûr, le code réel est simplement intégré directement dans votre page, et les opérations bloquent :
<?php // blocking file I/O $file_data = file_get_contents('/path/to/file.dat'); // blocking network I/O $curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl); // some more blocking network I/O $result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>
En termes d'intégration avec le système, c'est comme ça :
Assez simple : un processus par demande. Les appels d'E/S bloquent simplement. Avantage? C'est simple et ça marche. Désavantage? Frappez-le avec 20 000 clients simultanément et votre serveur s'enflammera. Cette approche ne s'adapte pas bien car les outils fournis par le noyau pour traiter les E/S à volume élevé (epoll, etc.) ne sont pas utilisés. Et pour ajouter l'insulte à l'injure, l'exécution d'un processus distinct pour chaque demande a tendance à utiliser beaucoup de ressources système, en particulier la mémoire, qui est souvent la première chose dont vous manquez dans un scénario comme celui-ci.
Remarque : L'approche utilisée pour Ruby est très similaire à celle de PHP, et d'une manière large, générale et ondulante à la main, elles peuvent être considérées comme identiques pour nos besoins.
L'approche multithread : Java
Alors Java arrive, juste au moment où vous avez acheté votre premier nom de domaine et c'était cool de dire au hasard "point com" après une phrase. Et Java a intégré le multithreading dans le langage, ce qui (surtout lorsqu'il a été créé) est assez génial.
La plupart des serveurs Web Java fonctionnent en démarrant un nouveau fil d'exécution pour chaque demande qui arrive, puis dans ce fil en appelant éventuellement la fonction que vous, en tant que développeur d'application, avez écrite.
Faire des E/S dans un servlet Java a tendance à ressembler à :
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream("/path/to/file"); // blocking network I/O URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println("..."); }
Étant donné que notre méthode doGet
ci-dessus correspond à une requête et est exécutée dans son propre thread, au lieu d'un processus séparé pour chaque requête qui nécessite sa propre mémoire, nous avons un thread séparé. Cela a quelques avantages intéressants, comme la possibilité de partager l'état, les données mises en cache, etc. entre les threads car ils peuvent accéder à la mémoire de l'autre, mais l'impact sur la façon dont il interagit avec le calendrier reste presque identique à ce qui se fait dans le PHP exemple précédemment. Chaque requête obtient un nouveau thread et les différentes opérations d'E/S se bloquent à l'intérieur de ce thread jusqu'à ce que la requête soit entièrement traitée. Les threads sont regroupés pour minimiser le coût de leur création et de leur destruction, mais des milliers de connexions signifient des milliers de threads, ce qui est mauvais pour le planificateur.
Une étape importante est que dans la version 1.4, Java (et une mise à niveau importante à nouveau dans la 1.7) a obtenu la possibilité d'effectuer des appels d'E/S non bloquants. La plupart des applications, Web et autres, ne l'utilisent pas, mais au moins elle est disponible. Certains serveurs Web Java tentent d'en tirer parti de différentes manières ; cependant, la grande majorité des applications Java déployées fonctionnent toujours comme décrit ci-dessus.
Java nous rapproche et a certainement de bonnes fonctionnalités prêtes à l'emploi pour les E/S, mais cela ne résout toujours pas vraiment le problème de ce qui se passe lorsque vous avez une application fortement liée aux E/S qui se fait pilonner le sol avec plusieurs milliers de fils bloquants.
E/S non bloquantes en tant que citoyen de première classe : nœud
Node.js est le gamin le plus populaire en matière d'amélioration des E/S. Quiconque a eu la moindre introduction à Node s'est entendu dire qu'il est "non bloquant" et qu'il gère efficacement les E/S. Et cela est vrai dans un sens général. Mais le diable est dans les détails et les moyens par lesquels cette sorcellerie a été réalisée comptent quand il s'agit de performances.

Essentiellement, le changement de paradigme que Node implémente est qu'au lieu de dire essentiellement "écrivez votre code ici pour gérer la demande", ils disent plutôt "écrivez du code ici pour commencer à gérer la demande". Chaque fois que vous devez faire quelque chose qui implique des E/S, vous faites la demande et donnez une fonction de rappel que Node appellera quand c'est fait.
Le code de nœud typique pour effectuer une opération d'E/S dans une requête ressemble à ceci :
http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });
Comme vous pouvez le voir, il y a deux fonctions de rappel ici. Le premier est appelé lorsqu'une requête démarre et le second est appelé lorsque les données du fichier sont disponibles.
Cela donne essentiellement à Node la possibilité de gérer efficacement les E/S entre ces rappels. Un scénario où ce serait encore plus pertinent est celui où vous effectuez un appel de base de données dans Node, mais je ne m'embêterai pas avec l'exemple car c'est exactement le même principe : vous démarrez l'appel de base de données et donnez à Node une fonction de rappel, il effectue les opérations d'E/S séparément à l'aide d'appels non bloquants, puis invoque votre fonction de rappel lorsque les données que vous avez demandées sont disponibles. Ce mécanisme de mise en file d'attente des appels d'E / S et de laisser Node le gérer, puis d'obtenir un rappel s'appelle la «boucle d'événement». Et ça marche plutôt bien.
Il y a cependant un hic à ce modèle. Sous le capot, la raison en est beaucoup plus liée à la façon dont le moteur JavaScript V8 (le moteur JS de Chrome utilisé par Node) est implémenté 1 qu'autre chose. Le code JS que vous écrivez s'exécute dans un seul thread. Penses-y un moment. Cela signifie que même si les E/S sont effectuées à l'aide de techniques non bloquantes efficaces, votre JS peut exécuter des opérations liées au processeur dans un seul thread, chaque bloc de code bloquant le suivant. Un exemple courant de cas où cela peut se produire consiste à boucler sur les enregistrements de la base de données pour les traiter d'une manière ou d'une autre avant de les envoyer au client. Voici un exemple qui montre comment cela fonctionne :
var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i < rows.length; i++) { // do processing on each row } response.end(...); // write out the results }) };
Bien que Node gère efficacement les E/S, la boucle for
de l'exemple ci-dessus utilise des cycles CPU dans votre seul et unique thread principal. Cela signifie que si vous avez 10 000 connexions, cette boucle pourrait entraîner l'exploration de toute votre application, en fonction du temps que cela prend. Chaque demande doit partager une tranche de temps, une à la fois, dans votre fil principal.
La prémisse sur laquelle tout ce concept est basé est que les opérations d'E/S sont la partie la plus lente, il est donc très important de les gérer efficacement, même si cela signifie effectuer d'autres traitements en série. C'est vrai dans certains cas, mais pas dans tous.
L'autre point est que, et bien que ce ne soit qu'une opinion, il peut être assez fastidieux d'écrire un tas de rappels imbriqués et certains soutiennent que cela rend le code beaucoup plus difficile à suivre. Il n'est pas rare de voir des rappels imbriqués à quatre, cinq ou même plus de niveaux dans le code Node.
Nous revenons aux compromis. Le modèle Node fonctionne bien si votre principal problème de performances est I/O. Cependant, son talon d'Achille est que vous pouvez entrer dans une fonction qui gère une requête HTTP et mettre du code gourmand en CPU et amener chaque connexion à une analyse si vous ne faites pas attention.
Naturellement non bloquant : Go
Avant d'entrer dans la section pour Go, il est approprié pour moi de révéler que je suis un fanboy de Go. Je l'ai utilisé pour de nombreux projets et je suis ouvertement partisan de ses avantages en termes de productivité, et je les vois dans mon travail lorsque je l'utilise.
Cela dit, regardons comment il gère les E/S. L'une des principales caractéristiques du langage Go est qu'il contient son propre planificateur. Au lieu que chaque thread d'exécution corresponde à un seul thread du système d'exploitation, il fonctionne avec le concept de « goroutines ». Et le runtime Go peut attribuer une goroutine à un thread du système d'exploitation et l'exécuter, ou la suspendre et ne pas l'associer à un thread du système d'exploitation, en fonction de ce que fait cette goroutine. Chaque requête provenant du serveur HTTP de Go est traitée dans une Goroutine distincte.
Le schéma du fonctionnement du planificateur ressemble à ceci :
Sous le capot, cela est implémenté par divers points dans le runtime Go qui implémentent l'appel d'E/S en faisant la demande d'écrire/lire/connecter/etc., mettre la goroutine actuelle en veille, avec les informations pour réactiver la goroutine lorsque d'autres mesures peuvent être prises.
En effet, le runtime Go fait quelque chose de pas très différent de ce que fait Node, sauf que le mécanisme de rappel est intégré à l'implémentation de l'appel d'E/S et interagit automatiquement avec le planificateur. Il ne souffre pas non plus de la restriction d'avoir tout votre code de gestionnaire exécuté dans le même thread, Go mappera automatiquement vos Goroutines sur autant de threads du système d'exploitation qu'il jugera appropriés en fonction de la logique de son planificateur. Le résultat est un code comme celui-ci :
func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query("SELECT ...") for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }
Comme vous pouvez le voir ci-dessus, la structure de code de base de ce que nous faisons ressemble à celle des approches les plus simplistes, tout en réalisant des E/S non bloquantes sous le capot.
Dans la plupart des cas, cela finit par être "le meilleur des deux mondes". Les E/S non bloquantes sont utilisées pour toutes les choses importantes, mais votre code semble bloquer et a donc tendance à être plus simple à comprendre et à entretenir. L'interaction entre le planificateur Go et le planificateur du système d'exploitation gère le reste. Ce n'est pas de la magie complète, et si vous construisez un grand système, cela vaut la peine de prendre le temps de comprendre plus en détail son fonctionnement. mais en même temps, l'environnement que vous obtenez "prêt à l'emploi" fonctionne et s'adapte assez bien.
Go peut avoir ses défauts, mais d'une manière générale, la façon dont il gère les E/S n'en fait pas partie.
Mensonges, mensonges maudits et repères
Il est difficile de donner des timings exacts sur le changement de contexte impliqué par ces différents modèles. Je pourrais aussi dire que c'est moins utile pour vous. Donc, à la place, je vais vous donner quelques repères de base qui comparent les performances globales du serveur HTTP de ces environnements de serveur. Gardez à l'esprit que de nombreux facteurs sont impliqués dans les performances de l'ensemble du chemin de requête/réponse HTTP de bout en bout, et les chiffres présentés ici ne sont que quelques exemples que j'ai rassemblés pour donner une comparaison de base.
Pour chacun de ces environnements, j'ai écrit le code approprié à lire dans un fichier de 64k avec des octets aléatoires, j'ai exécuté un hachage SHA-256 dessus N nombre de fois (N étant spécifié dans la chaîne de requête de l'URL, par exemple, .../test.php?n=100
) et imprimez le hachage résultant en hexadécimal. J'ai choisi cela parce que c'est un moyen très simple d'exécuter les mêmes tests avec des E/S cohérentes et un moyen contrôlé d'augmenter l'utilisation du processeur.
Voir ces notes de référence pour un peu plus de détails sur les environnements utilisés.
Examinons d'abord quelques exemples de faible simultanéité. L'exécution de 2 000 itérations avec 300 requêtes simultanées et un seul hachage par requête (N=1) nous donne ceci :
Il est difficile de tirer une conclusion à partir de ce seul graphique, mais il me semble que, à ce volume de connexion et de calcul, nous voyons des moments qui ont plus à voir avec l'exécution générale des langages eux-mêmes, d'autant plus que le E/S. Notez que les langages considérés comme des "langages de script" (dactylographie lâche, interprétation dynamique) sont les plus lents.
Mais que se passe-t-il si nous augmentons N à 1000, toujours avec 300 requêtes simultanées - la même charge mais 100 fois plus d'itérations de hachage (beaucoup plus de charge CPU) :
Tout d'un coup, les performances du nœud chutent considérablement, car les opérations gourmandes en CPU de chaque requête se bloquent mutuellement. Et curieusement, les performances de PHP s'améliorent (par rapport aux autres) et battent Java dans ce test. (Il convient de noter qu'en PHP, l'implémentation SHA-256 est écrite en C et que le chemin d'exécution passe beaucoup plus de temps dans cette boucle, puisque nous effectuons maintenant 1000 itérations de hachage).
Essayons maintenant 5000 connexions simultanées (avec N = 1) - ou aussi près que possible. Malheureusement, pour la plupart de ces environnements, le taux d'échec n'était pas négligeable. Pour ce graphique, nous examinerons le nombre total de requêtes par seconde. Plus c'est haut, mieux c'est :
Et l'image semble tout à fait différente. C'est une supposition, mais il semble qu'à un volume de connexion élevé, la surcharge par connexion impliquée dans la génération de nouveaux processus et la mémoire supplémentaire qui lui est associée dans PHP + Apache semblent devenir un facteur dominant et abaisser les performances de PHP. Clairement, Go est le gagnant ici, suivi de Java, Node et enfin PHP.
Bien que les facteurs impliqués dans votre débit global soient nombreux et varient également considérablement d'une application à l'autre, plus vous comprendrez les entrailles de ce qui se passe sous le capot et les compromis impliqués, mieux vous vous en sortirez.
En résumé
Avec tout ce qui précède, il est assez clair qu'à mesure que les langages ont évolué, les solutions pour traiter les applications à grande échelle qui font beaucoup d'E/S ont évolué avec elles.
Pour être juste, PHP et Java, malgré les descriptions de cet article, ont des implémentations d'E/S non bloquantes disponibles pour une utilisation dans les applications Web. Mais celles-ci ne sont pas aussi courantes que les approches décrites ci-dessus, et la surcharge opérationnelle associée à la maintenance des serveurs utilisant de telles approches devrait être prise en compte. Sans oublier que votre code doit être structuré de manière à fonctionner avec de tels environnements ; votre application Web PHP ou Java "normale" ne fonctionnera généralement pas sans modifications importantes dans un tel environnement.
À titre de comparaison, si nous considérons quelques facteurs importants qui affectent les performances ainsi que la facilité d'utilisation, nous obtenons ceci :
Langue | Threads vs processus | E/S non bloquantes | Facilité d'utilisation |
---|---|---|---|
PHP | Processus | Non | |
Java | Fils | Disponible | Nécessite des rappels |
Node.js | Fils | Oui | Nécessite des rappels |
Va | Fils (Goroutines) | Oui | Aucun rappel nécessaire |
Les threads vont généralement être beaucoup plus économes en mémoire que les processus, car ils partagent le même espace mémoire alors que les processus ne le font pas. En combinant cela avec les facteurs liés aux E/S non bloquantes, nous pouvons voir qu'au moins avec les facteurs considérés ci-dessus, à mesure que nous avançons dans la liste, la configuration générale en ce qui concerne les E/S s'améliore. Donc, si je devais choisir un gagnant dans le concours ci-dessus, ce serait certainement Go.
Même ainsi, en pratique, le choix d'un environnement dans lequel construire votre application est étroitement lié à la familiarité de votre équipe avec cet environnement et à la productivité globale que vous pouvez atteindre avec celui-ci. Il n'est donc peut-être pas logique que chaque équipe se lance et commence à développer des applications et des services Web dans Node or Go. En effet, trouver des développeurs ou la familiarité de votre équipe interne est souvent cité comme la principale raison de ne pas utiliser une langue et/ou un environnement différent. Cela dit, les temps ont beaucoup changé depuis une quinzaine d'années.
Espérons que ce qui précède aide à brosser un tableau plus clair de ce qui se passe sous le capot et vous donne quelques idées sur la façon de gérer l'évolutivité réelle de votre application. Bonne entrée et sortie !