spacefox a lancé dans ce journal un "concours" d'implémentation d'un programme dans divers langages. Le but est d'écrire un serveur HTTP qui retourne une redirection vers une page aléatoire : https://avatar.spacefox.fr/Renard-$random.png
.
Pas mal d'implémentations ont déjà été proposées, je vais essayer à mon tour en essayer 4 :
- Deno, pour découvrir
- Node.js, pour avoir une référence par rapport à Deno
- nginx (ngx_http_lua_module) et Varnish, car ce sont deux outils qu'on utiliserait dans la vraie vie pour implémenter le programme.
Outil de benchmark
Pour commencer, au lieu d'utiliser ab
, je vais utiliser wrk
. En effet, je n'arrive pas à obtenir plus de 19 000 req/s avec ab
sur ma machine, alors que, avec wrk (spoiler alert), on mesure jusqu'à 250 000 req/s.
Méthodologie
J'ai fait les tests sur ma machine, un desktop sous ArchLinux, avec un core i5-8400 (6 cœurs), avec wrk -d10s -t4 -c 100
.
J'ai essayé d'obtenir des résultats représentatifs, mais sans y passer trop de temps et sans chercher une rigueur excessive. Notamment, wrk
et le programme sont exécutés sur la même machine, ce qui peut fausser les résultats. Également, je n'ai pas fait spécialement attention aux versions et aux optimisations, si ce n'est d'avoir des versions récentes et de ne pas avoir des désoptimisations flagrantes (compilation en mode debug, logs sur stdout, parallélisme insuffisant…).
Résultats
Java
Pour avoir une baseline, l'original de spacefox :
Requests/sec: 20 267.84
Transfer/sec: 2.75MB
Pas de surprise, on est sur le même ordre de grandeur que spacefox (17000 avec ab sur sa machine perso).
rust + hyper
Maintenant la version rust/hyper de abriotde (qui est parallélisée) :
Requests/sec: 232 214.97
Transfer/sec: 31.34MB
Sans surprise, c'est beaucoup plus rapide. Rust et hyper sont très rapides, et devraient être la borne haute de ce qu'on peut atteindre sans optimisations complexes.
Deno
Requests/sec: 42 960.47
Transfer/sec: 6.23MB
Là c'est une surprise pour moi. En général Java est un peu plus rapide que Node/Deno, mais sur ce benchmark Deno est 2x plus rapide. Après je connais mal Java, et c'est possible que j'ai raté des flags ad hoc. A noter que le JavaScript est exécuté sur un thread, comme pour la version Java.
node.js
Requests/sec: 33 400.03
Transfer/sec: 6.18MB
Un peu plus lent que deno. En effet, deno est censé être le successeur de node.js, et il a des optimisations qui n'existent pas sur node.js.
Varnish
Varnish est un caching reverse proxy, qui peut être programmé dans un langage spécifique, le VCL, qui, à l'exécution, est transformé en C puis compilé. Il est donc rapide:
Requests/sec: 159 889.33
Transfer/sec: 28.16MB
nginx + ngx_http_lua_module
Requests/sec: 243841.01
Transfer/sec: 83.70MB
nginx est réputé pour être rapide, mais, avec le module lua, je m'attendais à ce qu'il soit plus lent que l'implémentation rust. En fait il est systématiquement plus rapide, et ce malgré le double de données transféré (il répond une petite page html, alors que les autres implémentations renvoient un body vide).
Conclusion
nginx est à la fois le plus rapide et le plus simple à configurer (surtout si on l'utilise déjà pour d'autres services).
Coté langages de programmation, il y a de grosses différences de performances, mais tous sont largement assez rapides sauf cas très particuliers, surtout si, pour ceux qui ne sont pas parallèles, on exécute plusieurs instances derrière un reverse proxy.
Code source
Deno
import { serve } from "https://deno.land/std@0.144.0/http/server.ts";
serve((request) => {
const url = new URL(request.url);
if (url.pathname !== "/") return new Response("nope", { status: 404 });
return new Response(null, {
status: 302,
headers: {
location: `https://avatar.spacefox.fr/Renard-${Math.round(
Math.random() * 10
)}.png`,
},
});
});
Node.js
const http = require("node:http");
const server = http.createServer((req, res) => {
if (req.path !== "/") res.writeHead(404);
res.writeHead(302, {
location: `https://avatar.spacefox.fr/Renard-${Math.round(
Math.random() * 10
)}.png`,
});
res.end();
});
server.listen(8000);
Varnish
vcl 4.0;
import std;
backend default none;
sub vcl_recv {
if (req.url != "/") {
return(synth(404, "Not Found"));
}
return(synth(302, ""));
}
sub vcl_synth {
if (resp.status == 302) {
set resp.http.location = "https://avatar.spacefox.fr/Renard-" +
std.real2integer(std.random(0,10),0) +
".png";
}
return(deliver);
}
nginx
events {
worker_connections 2048;
}
worker_processes 6;
http {
access_log /dev/null;
server {
listen 8080;
location / {
set_by_lua_block $random {
return math.random(1, 10)
}
return 302 https://avatar.spacefox.fr/Renard-$random.png;
}
}
}
# 1 worker process pour nginx ?
Posté par Adrien Dorsaz (site web personnel, Mastodon) . Évalué à 4.
Merci pour ces exemples :)
Si je comprends bien le code de nginx utilise 6 processus différents pour traiter les requêtes.
Je pense que pour pouvoir comparer avec Node.js, il faudrait évaluer avec un seul worker process.
En effet, Node.js ne démarre qu'un seul processus avec un seul thread par défaut.
[^] # Re: 1 worker process pour nginx ?
Posté par n_e . Évalué à 3.
Vu que dans les programmes déjà existants Java était single-threadé et Rust multi-threadé la question se posait, et j'ai préféré prendre ce qui était le plus performant dans une configuration relativement standard.
Avec node.js en mode cluster on arrive à :
soit 4.5x la perf. monothreadée, donc un scaling quasi-linéaire (sur les 6 CPUs, 1 CPU était pris par wrk, et 0.7 par d'autres process).
[^] # Re: 1 worker process pour nginx ?
Posté par YBoy360 (site web personnel) . Évalué à 1.
Et sur ta machine, quel est le temps CPU en mono-thread des processus Java, wrk avec Java, puis Rust, et enfin wrk avec Rust ?
Le test avec ab me donne quasi es mêmes résultats.
Lorsque je fais un vmstat (je sais c'est grossier), voici en gros le temps cpu avec la version Java multi-thread:
Le CPU n'est occupé qu'a 12 %, et wrk prend autant de temps CPU que le processus Java…
[^] # Re: 1 worker process pour nginx ?
Posté par n_e . Évalué à 1.
Selon vmstat, avec varnish je suis à 100%, en rust à 90%, java à 50% (avec 6 cœurs).
wrk utilise entre 0.7 et 1.5 cœurs selon les benchs, et j'ai des trucs divers qui utilise 0.7 cœurs.
Tu as 16 cœurs ?
[^] # Re: 1 worker process pour nginx ?
Posté par YBoy360 (site web personnel) . Évalué à 1.
Oui, soit 32 thread HW (2 thread par cœur). Je l'ai acheté pour le "работаю" y a pas longtemps …
# Varnish vs nginx et biais de configuration
Posté par Zenitram (site web personnel) . Évalué à 4.
Je suis étonné que nginx gagne contre Varnish car Varnish compile et donc rn théorie devrait bien plus optimiser.
Ne serait-ce pas un problème de configuration et un biais du fait que tu configures l'un et pas l'autre?
Un rapide coup d'oeil sur la doc Varnish dit qu'il y a 2 epoll/kqueue et que c'est configurable, je ne suis pas expert mais peut-être qu'à la vue de la "difficulté" de la tâche tu en es plutôt à tester une config d'outil contre une autre et que tu as configuré un des outils mais pas l'autre… Peut-être tester la configuration par défaut de chaque outil et donc tester nginx sans "worker_connections 2048;" ni "worker_processes 6;" pour éviter un biais, ou trouver un équivalent chez Varnish (thread_pool_max et thread_pools?).
[^] # Re: Varnish vs nginx et biais de configuration
Posté par n_e . Évalué à 2.
nginx aussi compile, il utilise LuaJIT :)
Concernant Varnish, ton lien indique :
Cela dit c'est complètement possible que j'aie raté des choses.
[^] # Re: Varnish vs nginx et biais de configuration
Posté par Zenitram (site web personnel) . Évalué à 2. Dernière modification le 19 juin 2022 à 20:03.
Je ne connais pas assez (pas loin de pas du tout :-p), juste que Varnish a quand même bonne réputation et du coup je m'attendais à ce qu'il soit dans le même style que rust+hyper et nginx+lua, mais il peut aussi faire plus de choses qui ralentissent, ou conf par défaut moins prévue pour de petits codes comme le test.
Comme rust+hyper et nginx+lua sont en pratique quasi à égalité, je trouve bizarre que Varnish soit (relativement) si loin alors que les mêmes principes et les mêmes recherches d'optimisations sont sur les 3.
PS : et sinon, il faut le dire, les autres c'est du bloat pas écologique :).
[^] # Re: Varnish vs nginx et biais de configuration
Posté par Christie Poutrelle (site web personnel) . Évalué à 4.
Sur plusieurs projets, on a remplacé Varnish en reverse proxy cache par le microcache de nginx, et au delà de la simplification de l'infra (parce qu'on a souvent nginx derrière Varnish pour la couche applicative, en CGI pour PHP ou autre) nginx semble gagner la partie en ce qui concerne les performances brutes. Après, avec nginx on l'utilise en "microcache" (donc on cache moins agressivement, moins longtemps qu'avec Varnish, qui lui est conçu pour cacher longtemps et beaucoup).
# Version Java multi-thread
Posté par YBoy360 (site web personnel) . Évalué à 1.
Chez moi, wrk prend 30 % de temps CPU de plus que le processus Java. Difficile donc de déduire quoi que ce soit, quelque soit le nombre de thread que je choisisse, la limite semble identique et le temps CPU pratiquement constant.
Ce que l'on peut dire, c'est que ce bench est mal parallélisé en Java, avec ce code.
En version mono-thread:
En multi-thread:
J'utilise les paramètres par défaut pour la JVM. Pour passer en version multi-thread, voici le code:
que j'ai repris du précédant journal.
[^] # Re: Version Java multi-thread
Posté par n_e . Évalué à 1.
Tel que je comprends la doc, il n'y a que les handlers de requête qui sont parallélisés. Du coup à partir du moment où tout ce qui n'est pas handler utilise 100% de CPU, ça ne sert à rien d'ajouter davantage de parallélisme.
# C
Posté par superna (site web personnel) . Évalué à 4.
Avec libmicrohttpd (installé depuis un package debian sur Ubuntu 20.04):
Avec
./wrk -d10s -t4:
Code:Requests/sec: 217045.72
Transfer/sec: 31.69MB
et
[^] # HAProxy, vive le C, vive la France
Posté par benja . Évalué à 2.
HAproxy fait beaucoup mieux!
avec ton code j'obtiens
[^] # Re: HAProxy, vive le C, vive la France
Posté par benja . Évalué à 1.
Et curieusement l'empreinte disque (approximation ~mémoire) est similaire. Je crois que l'on peut prudemment déclarer une victoire écrasante du C. :P
[^] # Re: HAProxy, vive le C, vive la France
Posté par benja . Évalué à 1.
Tests faits sur FreeBSD current GENERIC (donc pas du tout optimisé pour le benchmark), ce qui doit aussi expliquer les mauvais résultats que j'obtiens avec ton programme, je t'invite à tester haproxy pour pouvoir comparer correctement.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.