Maison  >  Article  >  développement back-end  >  Concours de performances d'E/S des serveurs Node, PHP, Java et Go, qui, selon vous, gagnera ?

Concours de performances d'E/S des serveurs Node, PHP, Java et Go, qui, selon vous, gagnera ?

醉折花枝作酒筹
醉折花枝作酒筹avant
2021-07-22 09:26:333505parcourir

Cet article présente d'abord brièvement les concepts de base liés aux E/S, puis compare horizontalement les performances d'E/S de Node, PHP, Java et Go, et donne des suggestions de sélection. Présentons-le ci-dessous, les amis dans le besoin peuvent s'y référer.

Concours de performances d'E/S des serveurs Node, PHP, Java et Go, qui, selon vous, gagnera ?

Comprendre le modèle d'entrée/sortie (E/S) d'une application permet de mieux comprendre comment elle gère la charge de manière idéale et dans la pratique. Peut-être que votre application est petite et n'a pas besoin de supporter une charge élevée, il y a donc moins de choses à considérer. Cependant, à mesure que la charge de trafic des applications augmente, l’utilisation d’un mauvais modèle d’E/S peut avoir des conséquences très graves.

Dans cet article, nous comparerons Node, Java, Go et PHP avec Apache, discuterons de la manière dont différents langages modélisent les E/S, des avantages et des inconvénients de chaque modèle et de quelques mesures de performances de base. Si vous êtes plus préoccupé par les performances d’E/S de votre prochaine application Web, cet article vous aidera.

Bases des E/S : un aperçu rapide

Pour comprendre les facteurs liés aux E/S, nous devons d'abord comprendre ces concepts au niveau du système d'exploitation. Même s'il est peu probable que vous soyez exposé à trop de concepts directement au début, vous les rencontrerez toujours lors du fonctionnement de l'application, que ce soit directement ou indirectement. Les détails comptent.

Appel système

Tout d'abord, découvrons l'appel système, qui est décrit en détail comme suit :

  • L'application demande au noyau du système d'exploitation d'effectuer des opérations d'E/S pour elle.

  • Un "appel système" se produit lorsqu'un programme demande au noyau d'effectuer une opération. Les détails d'implémentation varient selon les systèmes d'exploitation, mais le concept de base est le même. Lorsqu'un "appel système" est exécuté, certaines instructions spécifiques permettant de contrôler le programme seront transférées au noyau. De manière générale, les appels système sont bloquants, ce qui signifie que le programme attend que le noyau renvoie le résultat.

  • Le noyau effectue des opérations d'E/S de bas niveau sur les périphériques physiques (disques, cartes réseau, etc.) et répond aux appels système. Dans le monde réel, le noyau devra peut-être faire beaucoup de choses pour répondre à votre demande, notamment attendre que le périphérique soit prêt, mettre à jour son état interne, etc., mais en tant que développeur d'applications, vous n'avez pas à vous en soucier. à ce sujet, c'est l'affaire du noyau.

Appels bloquants et appels non bloquants

J'ai dit plus haut que les appels système sont généralement bloquants. Cependant, certains appels sont « non bloquants », ce qui signifie que le noyau place la requête dans une file d'attente ou dans un tampon et la renvoie immédiatement sans attendre que l'E/S réelle se produise. Ainsi, cela ne « bloque » que pendant une courte période, mais la file d'attente prend un certain temps.

Pour illustrer ce point, voici quelques exemples (appels système Linux) :

  • read() est un appel bloquant. Nous devons lui transmettre un descripteur de fichier et un tampon pour enregistrer les données, et revenir lorsque les données sont enregistrées dans le tampon. Il a l’avantage d’être à la fois élégant et simple.

  • epoll_create(), epoll_ctl() et epoll_wait() peuvent être utilisés pour créer un groupe de handles à écouter, ajouter/supprimer des handles dans ce groupe et bloquer le programme jusqu'à ce qu'il y ait une activité sur le handle. Ces appels système vous permettent de contrôler efficacement un grand nombre d'opérations d'E/S en utilisant un seul thread. Ces fonctionnalités, bien que très utiles, sont assez complexes à utiliser.

Il est très important de comprendre l'ordre de grandeur du décalage horaire ici. Si un cœur de processeur non optimisé fonctionne à 3 GHz, il peut exécuter 3 milliards de cycles par seconde (soit 3 cycles par nanoseconde). Un appel système non bloquant peut prendre plus de 10 cycles, soit quelques nanosecondes. Le blocage des appels pour recevoir des informations du réseau peut prendre plus de temps, disons 200 millisecondes (1/5 seconde).

Disons que l'appel non bloquant a pris 20 nanosecondes et que l'appel bloquant a pris 200 000 000 nanosecondes. De cette façon, le processus devra peut-être attendre 10 millions de cycles pour bloquer l'appel.

Le noyau propose deux méthodes : le blocage des E/S ("lire les données du réseau") et les E/S non bloquantes ("dites-moi quand il y a de nouvelles données sur la connexion réseau"), et les deux mécanismes bloquent l'appel. processus La durée est complètement différente.

Planification

La troisième chose très critique est ce qui se passe lorsqu'un grand nombre de threads ou de processus commencent à se bloquer.

Pour nous, il n'y a pas beaucoup de différence entre les threads et les processus. En réalité, la différence la plus significative liée aux performances est que, puisque les threads partagent la même mémoire et que chaque processus possède son propre espace mémoire, un seul processus a tendance à occuper plus de mémoire. Cependant, lorsque nous parlons de planification, nous parlons en réalité de réaliser une série de choses, et chaque chose nécessite un certain temps d'exécution sur les cœurs de processeur disponibles.

Si vous avez 8 cœurs pour exécuter 300 threads, vous devez alors diviser le temps pour que chaque thread obtienne sa tranche de temps et que chaque cœur s'exécute pendant une courte période, puis passe au thread suivant. Cela se fait via un « commutateur de contexte », qui permet au CPU de passer d'un thread/processus à l'autre.

Ce type de changement de contexte a un certain coût, c'est-à-dire qu'il prend un certain temps. Cela peut prendre moins de 100 nanosecondes lorsqu'il est rapide, mais si les détails d'implémentation, la vitesse/l'architecture du processeur, le cache du processeur et d'autres logiciels et matériels sont différents, il est normal que cela prenne 1 000 nanosecondes ou plus.

Plus le nombre de threads (ou processus) est élevé, plus le nombre de changements de contexte est important. S'il y a des milliers de threads et que chaque thread prend des centaines de nanosecondes pour basculer, le système deviendra très lent.

Cependant, un appel non bloquant indique essentiellement au noyau "ne m'appeler que lorsque de nouvelles données ou événements arrivent sur ces connexions". Ces appels non bloquants gèrent efficacement de grandes charges d’E/S et réduisent les changements de contexte.

Il convient de noter que même si les exemples de cet article sont petits, l'accès aux bases de données, les systèmes de mise en cache externes (memcache et autres) et tout ce qui nécessite des E/S finiront par effectuer un certain type d'appel d'E/S, ce qui est le cas. identique à Le principe de l'exemple est le même.

De nombreux facteurs affectent le choix du langage de programmation dans un projet Même si vous ne considérez que les performances, il existe de nombreux facteurs. Cependant, si vous craignez que votre programme soit principalement limité par les E/S et que les performances soient un facteur important pour déterminer le succès ou l'échec du projet, alors les suggestions suivantes sont ce que vous devez prendre en compte.

"Keep It Simple" : PHP

Dans les années 90, beaucoup de gens portaient des chaussures Converse et écrivaient des scripts CGI en utilisant Perl. Ensuite, PHP est arrivé et beaucoup de gens l’ont apprécié et ont facilité la création de pages Web dynamiques.

Le modèle utilisé par PHP est très simple. Bien qu'il soit impossible d'être exactement le même, le principe général du serveur PHP est le suivant :

Le navigateur de l'utilisateur émet une requête HTTP, et la requête entre dans le serveur web Apache. Apache crée un processus distinct pour chaque requête et réutilise ces processus via certaines méthodes d'optimisation afin de minimiser les opérations à effectuer (la création de processus est relativement lente).

Apache appelle PHP et lui dit d'exécuter un certain fichier .php sur le disque.

Le code PHP commence à s'exécuter et bloque les appels d'E/S. Le file_get_contents() que vous appelez en PHP appelle en fait l'appel système read() et attend le résultat renvoyé.

<?php// blocking file I/O$file_data = file_get_contents(‘/path/to/file.dat’);

// blocking network I/O$curl = curl_init(&#39;http://example.com/example-microservice&#39;);
$result = curl_exec($curl);

// some more blocking network I/O$result = $db->query(&#39;SELECT id, data FROM examples ORDER BY id DESC limit 100&#39;);

?>

C'est simple : une seule démarche par demande. Les appels E/S bloquent. Et les avantages ? Simple mais efficace. Et les inconvénients ? S'il y a 20 000 clients simultanés, le serveur sera paralysé. Cette approche est difficile à mettre à l'échelle car les outils fournis par le noyau pour gérer de grandes quantités d'E/S (epoll, etc.) ne sont pas pleinement utilisés. Pire encore, exécuter un processus distinct pour chaque requête a tendance à consommer beaucoup de ressources système, notamment la mémoire, qui est souvent la première à être épuisée.

*Remarque : à ce stade, la situation de Ruby est très similaire à celle de PHP.

Multi-threading : Java

C'est ainsi que Java est apparu. Et Java intègre le multithreading dans le langage, ce qui est génial, surtout lorsqu'il s'agit de créer des threads.

La plupart des serveurs Web Java démarreront un nouveau thread d'exécution pour chaque requête, puis appelleront la fonction écrite par le développeur dans ce fil.

L'exécution d'E/S dans un servlet Java ressemble souvent à ceci :

publicvoiddoGet(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 la méthode doGet ci-dessus correspond à une requête et s'exécute dans son propre thread, plutôt que de s'exécuter dans un processus distinct qui nécessite une mémoire indépendante, nous allons donc créer un fil séparé. Chaque requête obtient un nouveau thread et diverses opérations d'E/S sont bloquées à l'intérieur de ce thread jusqu'à ce que la requête soit traitée. L'application créera un pool de threads pour minimiser le coût de création et de destruction des threads, mais des milliers de connexions signifient des milliers de threads, ce qui n'est pas une bonne chose pour le planificateur.

Il convient de noter que la version 1.4 de Java (à nouveau mise à niveau dans la version 1.7) ajoute la possibilité d'effectuer des appels d'E/S non bloquants. Bien que la plupart des applications n'utilisent pas cette fonctionnalité, elle est au moins disponible. Certains serveurs Web Java expérimentent cette fonctionnalité, mais la grande majorité des applications Java déployées fonctionnent toujours selon les principes décrits ci-dessus.

Java fournit de nombreuses fonctionnalités prêtes à l'emploi pour les E/S, mais si vous rencontrez la situation de créer un grand nombre de threads bloquants pour effectuer un grand nombre d'opérations d'E/S, Java n'a pas de bonne solution .

Faites des E/S non bloquantes une priorité absolue : Node

Celui qui fonctionne le mieux en E/S et qui est le plus populaire parmi les utilisateurs est Node.js. Toute personne ayant une compréhension de base de Node sait qu'il est « non bloquant » et gère efficacement les E/S. Cela est vrai dans un sens général. Mais les détails et la manière dont cela est mis en œuvre comptent.

Lorsque vous devez effectuer certaines opérations impliquant des E/S, vous devez faire une demande et donner une fonction de rappel qui appellera cette fonction après avoir traité la demande.

Le code typique pour effectuer des opérations d'E/S dans une requête ressemble à ceci :

http.createServer(function(request, response) {
    fs.readFile(&#39;/path/to/file&#39;, &#39;utf8&#39;, function(err, data) {
        response.end(data);
    });
});

Comme indiqué ci-dessus, il existe ici deux fonctions de rappel. La première fonction est appelée lorsque la requête démarre et la deuxième fonction est appelée lorsque les données du fichier sont disponibles.

这样,Node就能更有效地处理这些回调函数的I/O。有一个更能说明问题的例子:在Node中调用数据库操作。首先,你的程序开始调用数据库操作,并给Node一个回调函数,Node会使用非阻塞调用来单独执行I/O操作,然后在请求的数据可用时调用你的回调函数。这种对I/O调用进行排队并让Node处理I/O调用然后得到一个回调的机制称为“事件循环”。这个机制非常不错。

然而,这个模型有一个问题。在底层,这个问题出现的原因跟V8 JavaScript引擎(Node使用的是Chrome的JS引擎)的实现有关,即:你写的JS代码都运行在一个线程中。请思考一下。这意味着,尽管使用高效的非阻塞技术来执行I/O,但是JS代码在单个线程操作中运行基于CPU的操作,每个代码块都会阻塞下一个代码块的运行。有一个常见的例子:在数据库记录上循环,以某种方式处理记录,然后将它们输出到客户端。下面这段代码展示了这个例子的原理:

var handler = function(request, response) {

    connection.query(&#39;SELECT ...&#39;, 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

    })

};

虽然Node处理I/O的效率很高,但是上面例子中的for循环在一个主线程中使用了CPU周期。这意味着如果你有10000个连接,那么这个循环就可能会占用整个应用程序的时间。每个请求都必须要在主线程中占用一小段时间。

这整个概念的前提是I/O操作是最慢的部分,因此,即使串行处理是不得已的,但对它们进行有效处理也是非常重要的。这在某些情况下是成立的,但并非一成不变。

另一点观点是,写一堆嵌套的回调很麻烦,有些人认为这样的代码很丑陋。在Node代码中嵌入四个、五个甚至更多层的回调并不罕见。

又到了权衡利弊的时候了。如果你的主要性能问题是I/O的话,那么这个Node模型能帮到你。但是,它的缺点在于,如果你在一个处理HTTP请求的函数中放入了CPU处理密集型代码的话,一不小心就会让每个连接都出现拥堵。

原生无阻塞:Go

在介绍Go之前,我透露一下,我是一个Go的粉丝。我已经在许多项目中使用了Go。

让我们看看它是如何处理I/O的吧。 Go语言的一个关键特性是它包含了自己的调度器。它并不会为每个执行线程对应一个操作系统线程,而是使用了“goroutines”这个概念。Go运行时会为一个goroutine分配一个操作系统线程,并控制它执行或暂停。Go HTTP服务器的每个请求都在一个单独的Goroutine中进行处理。

实际上,除了回调机制被内置到I/O调用的实现中并自动与调度器交互之外,Go运行时正在做的事情与Node不同。它也不会受到必须让所有的处理代码在同一个线程中运行的限制,Go会根据其调度程序中的逻辑自动将你的Goroutine映射到它认为合适的操作系统线程中。因此,它的代码是这样的:

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

}

如上所示,这样的基本代码结构更为简单,而且还实现了非阻塞I/O。

在大多数情况下,这真正做到了“两全其美”。非阻塞I/O可用于所有重要的事情,但是代码却看起来像是阻塞的,因此这样往往更容易理解和维护。 剩下的就是Go调度程序和OS调度程序之间的交互处理了。这并不是魔法,如果你正在建立一个大型系统,那么还是值得花时间去了解它的工作原理的。同时,“开箱即用”的特点使它能够更好地工作和扩展。

Go可能也有不少缺点,但总的来说,它处理I/O的方式并没有明显的缺点。

性能评测

对于这些不同模型的上下文切换,很难进行准确的计时。当然,我也可以说这对你并没有多大的用处。这里,我将对这些服务器环境下的HTTP服务进行基本的性能评测比较。请记住,端到端的HTTP请求/响应性能涉及到的因素有很多。

我针对每一个环境都写了一段代码来读取64k文件中的随机字节,然后对其运行N次SHA-256散列(在URL的查询字符串中指定N,例如.../test.php?n=100)并以十六进制打印结果。我之所以选择这个,是因为它可以很容易运行一些持续的I/O操作,并且可以通过受控的方式来增加CPU使用率。

在这种存在大量连接和计算的情况下,我们看到的结果更多的是与语言本身的执行有关。请注意,“脚本语言”的执行速度最慢。

Soudain, les performances de Node chutent considérablement à mesure que les opérations gourmandes en CPU dans chaque requête se bloquent. Fait intéressant, dans ce test, les performances de PHP se sont améliorées (par rapport aux autres), voire meilleures que celles de Java. (Il est à noter qu'en PHP, l'implémentation de SHA-256 est écrite en C, mais le chemin d'exécution prend plus de temps dans cette boucle car nous effectuons cette fois 1000 itérations de hachage).

Je suppose qu'avec un nombre de connexions plus élevé, l'application de nouveaux processus et de mémoire dans PHP + Apache semble être le principal facteur affectant les performances de PHP. Évidemment, Go est cette fois le vainqueur, suivi de Java, Node et enfin PHP.

Bien que de nombreux facteurs soient impliqués dans le débit global et qu'ils varient considérablement d'une application à l'autre, plus vous comprenez les principes sous-jacents et les compromis impliqués, meilleures seront les performances de votre application.

Résumé

Pour résumer, à mesure que les langages évoluent, les solutions permettant de gérer les grandes applications gourmandes en E/S évoluent également.

Pour être honnête, PHP et Java disposent d'implémentations d'E/S non bloquantes pour les applications Web. Cependant, ces implémentations ne sont pas aussi largement utilisées que les méthodes décrites ci-dessus et il y a des frais de maintenance à prendre en compte. Sans oublier que le code de l’application doit être structuré de manière adaptée à cet environnement.

Comparons plusieurs facteurs importants qui affectent les performances et la facilité d'utilisation :

语言 线程与进程 非阻塞I/O 易于使用
PHP 进程 -
Java 线程 有效 需要回调
Node.js 线程 需要回调
Go 线程 (Goroutines) 无需回调

Étant donné que les threads partagent le même espace mémoire, mais pas les processus, les threads sont généralement beaucoup plus efficaces en termes de mémoire que les processus. Dans la liste ci-dessus, en regardant de haut en bas, les facteurs liés aux E/S sont meilleurs que les précédents. Donc, si je devais choisir un gagnant dans la comparaison ci-dessus, ce serait certainement le Go.

Cela dit, en pratique, l'environnement dans lequel vous choisissez de construire votre application est étroitement lié à la familiarité de votre équipe avec l'environnement et à la productivité globale que votre équipe peut atteindre. Ainsi, utiliser Node ou Go pour développer des applications et des services Web n’est peut-être pas le meilleur choix pour les équipes.

J'espère que ce qui précède vous aidera à comprendre plus clairement ce qui se passe sous le capot et vous donnera quelques suggestions sur la façon de gérer l'évolutivité des applications.

Apprentissage recommandé : Tutoriel vidéo php

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer
Article précédent:Trois modes peu connus de FPMArticle suivant:Trois modes peu connus de FPM