On dit souvent que les méthodes asynchrones apportent à Node.JS un avantage dans sa gestion des accès concurrents. On dit que pour une opération aussi coûteuse que de la lecture sur le disque par exemple, il saura gérer plus de connexions, et dans l’ensemble répondre plus vite si cette opération est faite en utilisant l’API asynchrone plutôt que l’API synchrone.
OK. Soit. En ce qui me concerne je n’ai pas le niveau technique pour comprendre les tenants et les aboutissants de tout ça, donc comment m’en convaincre? Et bien, benchmarkons
Partons d’un serveur HTTP très simple: il n’a qu’une page, qui affiche « coucou gamin » dès que le serveur a fini de lire un fichier local de quelques méga-octets.
Dans la suite de l’article je vais vous proposer 3 implémentations de ce serveur, et les résultats d’un test par Apache Bench.
Version asynchrone
Dans cette version on lit le fichier en utilisant « fs.readFile()« , et on ne répond que lorsque le fichier a été lu.
const fs = require('fs'); require('http').createServer(function (req, res) { fs.readFile('/path/to/file', function(err, content) { var status = err ? 500 : 200; res.writeHead(status, {"Content-Type": "text/plain"}); res.end("Coucou gamin"); }); }).listen(8081); |
On exécute ApacheBench sur le port 8081 avec 1000 requêtes et 100 accès concurrents: ab -n 1000 -c 100 http://localhost:8081/
Et voici le résultat:
Concurrency Level: 100 Time taken for tests: 14.054 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 76000 bytes HTML transferred: 12000 bytes Requests per second: 71.15 [#/sec] (mean) Time per request: 1405.418 [ms] (mean) Time per request: 14.054 [ms] (mean, across all concurrent requests) Transfer rate: 5.28 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.7 0 3 Processing: 744 1366 113.4 1382 1745 Waiting: 744 1366 113.4 1382 1745 Total: 746 1367 112.9 1382 1745 Percentage of the requests served within a certain time (ms) 50% 1382 66% 1394 75% 1402 80% 1409 90% 1431 95% 1462 98% 1536 99% 1633 100% 1745 (longest request) |
Version synchrone
Dans cette version on lit le fichier en utilisant « fs.readFileSync()« .
const fs = require('fs'); require('http').createServer(function (req, res) { var status, content; try { content = fs.readFileSync('/path/to/file'); status = 200; } catch (err) { status = 500; } res.writeHead(status, {"Content-Type": "text/plain"}); res.end("Coucou gamin"); }).listen(8082); |
On effectue le même test: ab -n 1000 -c 100 http://localhost:8082/
Et voici le résultat:
Concurrency Level: 100 Time taken for tests: 37.742 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 76000 bytes HTML transferred: 12000 bytes Requests per second: 26.50 [#/sec] (mean) Time per request: 3774.162 [ms] (mean) Time per request: 37.747 [ms] (mean, across all concurrent requests) Transfer rate: 1.97 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1 1.6 0 9 Processing: 43 3583 2063.0 3525 7598 Waiting: 42 3583 2063.0 3525 7598 Total: 43 3584 2062.5 3525 7598 Percentage of the requests served within a certain time (ms) 50% 3525 66% 4817 75% 5073 80% 5486 90% 6502 95% 7032 98% 7357 99% 7450 100% 7598 (longest request) |
Fausse version asynchrone
Pour tricher, on va prendre le code de la version synchrone, et on le colle dans un « setTimeout(…, 0) ». De cette manière on a bien un code qui rend la main tout de suite, mais qui néanmoins se base sur l’API synchrone. J’ai déjà vu ce type de « tricherie » qui donne l’impression qu’on a du code asynchrone, donc pour être tout-à-fait exhaustif je tenais à le tester.
const fs = require('fs'); require('http').createServer(function (req, res) { setTimeout(function() { var status, content; try { content = fs.readFileSync('/path/to/file'); status = 200; } catch (err) { status = 500; } res.writeHead(status, {"Content-Type": "text/plain"}); res.end("Coucou gamin"); }, 0); }).listen(8083); |
On effectue le même test: ab -n 1000 -c 100 http://localhost:8083/
Et voici le résultat:
Concurrency Level: 100 Time taken for tests: 36.903 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 76000 bytes HTML transferred: 12000 bytes Requests per second: 27.10 [#/sec] (mean) Time per request: 3690.269 [ms] (mean) Time per request: 36.903 [ms] (mean, across all concurrent requests) Transfer rate: 2.01 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.8 0 4 Processing: 48 3507 2034.4 3469 7435 Waiting: 48 3507 2034.4 3468 7435 Total: 49 3508 2034.2 3469 7435 Percentage of the requests served within a certain time (ms) 50% 3469 66% 4441 75% 5170 80% 5594 90% 6465 95% 6931 98% 7209 99% 7303 100% 7435 (longest request) |
À noter car c’est intéressant: j’ai refait plusieurs fois ce test en changeant la valeur dans setTimeout. Il n’y a quasiment aucune différence qu’on aille de 1 à 5ms d’attente. On commence à voir une différence avec 10ms, ou on perd 2 requêtes par seconde pour tomber à 25.
Conclusion
| Mode de programmation | Req/s | Time/req |
|---|---|---|
| Asynchrone | 71.15 (100%) | 14.054 (100%) |
| Synchrone | 26.50 (37%) | 37,742 (268%) |
| Faux asynchrone | 27.10 (38%) | 36,903 (262%) |
En terme de performances, on parle donc bien dans notre cas d’un rapport 1 à 3. Ce n’est pas juste 10% de différence non, le serveur sera 3 fois plus performant dans le cas de l’asynchrone. Et pas la peine de tricher avec setTimeout()!
Pour information, j’ai fait ce test avec de nombreuses combinaisons de nombre de requêtes et d’accès concurrents (je ne sais pas trop comment sortir des graphes, et là j’ai la flemme
). Comme on pourrait s’y attendre, en réduisant le nombre d’accès concurrents, on réduit l’écart. Mais ne vous y trompez pas: avec 1 seul accès simultané, j’observe toujours un rapport de 1 à 2 (en moyenne 17ms/req contre 37ms/req).
Conclusion: oubliez les méthodes de l’API Node.JS qui finissent par « Sync ». Non, vraiment, oubliez-les! Merci d’avance
En plus, si c’est de la soupe de callbacks dont vous avez peur, il y a de quoi se rassurer

Comments:6
Laisser un commentaire