Traiter 200 millions de requêtes par jour avec CGI-bin
(simonwillison.net)- Jake Gold a fait tourner un programme CGI en Go + SQLite sur un AMD 3700X à 16 threads afin de vérifier jusqu’où le CGI des années 1990 pouvait tenir sur du matériel moderne
- Le résultat montre qu’il est possible de traiter plus de 2 400 requêtes par seconde et plus de 200 millions de requêtes par jour, même sur un matériel ordinaire
- Comme le CGI démarre, exécute puis termine un processus pour chaque requête, sa surcharge était historiquement importante, ce qui a conduit à l’émergence de solutions comme PHP et FastCGI
- En 2020, Simon Willison a utilisé datasette-ripgrep pour traiter des recherches en appelant le CLI ripgrep écrit en Rust, ce qui l’a amené à remettre en question l’idée bien ancrée selon laquelle il faut éviter d’exécuter des processus pendant une requête web
- Avec des langages à démarrage rapide comme Go et Rust, ainsi que des environnements multi-CPU, une approche de type CGI peut redevenir une option réaliste, même si ce n’est pas la recommandation générale
Les performances du CGI revisitées à l’ère du matériel moderne
- Serving 200 million requests per day with a cgi-bin est un article dans lequel Jake Gold teste les performances du CGI façon années 1990 avec un programme CGI en Go + SQLite
- L’environnement de test repose sur un système équipé d’un AMD 3700X à 16 threads
- Le résultat essentiel est qu’avec du CGI, il est possible de traiter plus de 2 400 requêtes par seconde et plus de 200 millions de requêtes par jour sur un matériel ordinaire
- Le CGI fonctionne en lançant un processus distinct pour chaque requête entrante, puis en l’exécutant avant de le terminer
- Pour éviter cette surcharge, la communauté web des débuts a créé PHP et FastCGI, qui gardent le code en mémoire afin de réduire les coûts supplémentaires
Pourquoi le CGI n’est peut-être plus aussi mauvais qu’avant
- L’un des goulots d’étranglement historiques du CGI venait du fait que les scripts web étaient écrits dans des langages comme Perl, Python, Java, qui n’étaient pas conçus pour démarrer extrêmement vite
- Aujourd’hui, avec Go et Rust, un traitement des requêtes de style CGI peut fonctionner bien plus efficacement
- En 2020, en créant datasette-ripgrep, Simon Willison a lancé depuis le shell l’outil CLI ripgrep, écrit en Rust, pour exécuter des recherches, avec de bons résultats
- Comme les programmes CGI s’exécutent dans des processus distincts, ils peuvent aussi bien s’adapter à des architectures exploitant plusieurs CPU
- Les serveurs modernes peuvent disposer de 384 threads CPU
- Même une petite VM peut avoir 16 CPU
- Les performances du CPU et de la mémoire ont elles aussi fortement progressé
- Une manière de développer des applications web héritée de 1998 devient un terrain d’expérimentation intéressant lorsqu’elle rencontre Go et Rust, mais cela ne constitue pas pour autant l’option par défaut que la plupart des services devraient suivre
1 commentaires
Avis sur Hacker News
Même avec quelque chose comme Python, CGI est assez rapide de nos jours
Si le démarrage d’un script CGI prend 400 ms de CPU et que le serveur a 64 cœurs, cela fait 160 requêtes par seconde, soit 14 millions de hits par jour et par serveur
Pour un service web qui peine déjà à gérer quelques millions de requêtes par jour hors assets statiques, le goulet d’étranglement n’est pas le temps de démarrage des processus CGI
Autrefois, j’aurais dit que c’était « une technologie ennuyeuse prise en charge depuis longtemps dans la bibliothèque standard de Python », mais les mainteneurs Python restants semblent considérer la stabilité et la compatibilité ascendante comme nuisibles, et ont retiré de la bibliothèque standard des modules trop ennuyeux et trop stables
Le module cgi a été supprimé dans Python 3.13
Je l’ai utilisé au quotidien pendant la majeure partie des 25 dernières années, si bien que j’ai encore l’habitude d’utiliser Python pour le prototypage, mais je le regrette désormais et j’hésite entre JS et Lua
Fait intéressant, ce document renvoie à https://peps.python.org/pep-0206/, daté du 14 juillet 2000, donc il y a 25 ans, où le paquet cgi était déjà décrit comme « mal conçu et désormais presque impossible à corriger »
Le paquet https://github.com/jackrosenthal/legacy-cgi semble fournir un remplaçant drop-in du module de la bibliothèque standard
Cette prise en charge reste présente dans CGIHTTPRequestHandler du module http.server
Ce qu’il y avait dans le module cgi, ce n’étaient que quelques fonctions pour analyser les données de formulaires HTML
Mais je ne comprends pas trop qu’on envisage ensuite comme alternative JS, qui n’a pas de bibliothèque standard à proprement parler
Lua n’a pas non plus de module CGI dans sa bibliothèque standard
Aujourd’hui, il s’agit plutôt de trouver les parties absurdement gaspilleuses ajoutées pour sortir vite, puis de les arrêter
Si l’on s’y prend correctement, une application peut presque ne pas ajouter de latence en dehors des accès au stockage
J’ai appris Python à partir de la 1.6, mais je l’ai surtout utilisé pour du scripting système
Entre 1999 et 2003, j’ai utilisé Tcl dans des modules Apache et IIS, puis réécrit sans cesse les modules en C, et j’ai appris trop de leçons à la dure
Récemment, j’ai testé sur un mini-serveur à 350 dollars un binaire Golang, RabbitMQ, Redis et MySQL, tous sur la même machine, et il tenait 5 000 requêtes par seconde en continu
Rapporté à 24 heures, cela fait 400 millions de requêtes par jour
C’est étonnant de voir à quel point les outils gratuits d’aujourd’hui sont excellents, et tout aussi étonnant de voir combien nous payons malgré tout aux fournisseurs cloud
La comparaison n’est pas exactement parfaite, mais j’ai vraiment apprécié de tout développer et optimiser sur une machine dans mon sous-sol
C’est étrange de continuer à accepter une telle surcharge sans autre raison que « Google fait comme ça »
Il faut vraiment que j’écrive un article sur l’architecture monolithe modulaire que nous utilisons avec succès
On peut louer un serveur dédié chez un hébergeur et l’utiliser à sa guise. En revanche, on est fortement contraint par les limites de bande passante ou de transfert
Si le cloud est utilisé, c’est parce qu’il y a beaucoup d’intérêts en jeu. Par exemple, les VC et les investisseurs détiennent aussi des parts dans des entreprises cloud, et ils craignent que leur investissement échoue à cause d’un pic de trafic imaginaire qui n’arrivera en réalité jamais
Les équipes commerciales du cloud sont douées pour jouer sur les angoisses des investisseurs
Si un disque tombe en panne pendant que je voyage, je n’ai pas de solution, et si je me verrouille dehors par erreur, je n’ai pas non plus de terminal série de secours
Je pourrais mettre en place une configuration comme les gens de r/homelab, mais il devient alors difficile de savoir si j’économise réellement de l’argent, surtout en incluant mon temps
Au final, je considère que les fournisseurs cloud restent une assez bonne affaire grâce aux économies d’échelle
Curieusement, on ne peut pas obtenir seulement l’un des deux séparément
Imaginez quelle grosse entreprise ou quel service on pourrait faire tourner avec simplement quatre disques de 20 To et un CPU correct
Il n’est pas facile d’obtenir une telle configuration chez un fournisseur cloud
La partie disant que « la première communauté web a vite compris que c’était une mauvaise idée et a inventé des technologies comme PHP » est, à strictement parler, que la technologie clé était mod_php
PHP lui-même n’était pas différent de Perl dans sa manière d’être exécuté, mais grâce aux choix de conception de mod_php par rapport à mod_perl, on pouvait simplement déposer des scripts PHP sur le serveur et les exécuter rapidement, alors que mod_perl demandait un peu de réflexion et de magie pour fonctionner
Il y avait de la gestion de contenu, l’envoi de devoirs, un calendrier d’événements, la gestion des notes, du chat en temps réel et des forums, le tout en C pur au-dessus de CGI, et c’était un enfer à travailler
Le jour où j’ai découvert PHP, j’ai presque eu les larmes aux yeux : tout ce que je peinais à écrire depuis zéro en lisant des RFC ou en faisant de la rétro-ingénierie de HTTP n’était, en PHP, qu’un simple appel de fonction
Plus besoin de déboguer une implémentation bancale de urlencode ni de perdre une journée à cause d’un retour chariot bizarre dans un en-tête HTTP
Il semble que la première version du module Apache ait été destinée à PHP/FI Version 2.0 en 1996 : https://www.php.net/manual/phpfi2.php#module
Si cgi-bin doit accéder à la base de données, il faut ouvrir une connexion à chaque démarrage de processus
Garder le code en mémoire, comme avec FastCGI, ne sert pas seulement à éviter le coût du temps de démarrage, mais aussi à pouvoir disposer d’un pool de connexions à la base de données, ou au moins de connexions persistantes par thread
C’est ce qui arrivait quand on combinait « Python est monothread, donc lançons-en beaucoup » et « Python est lent, donc lançons-en beaucoup »
À grande échelle, il faut finalement un pool de connexions partagé en dehors de Python, comme pgbouncer, et beaucoup de tuning pour absorber la charge sans tuer la base de données
Bien sûr, ensuite, après avoir réimplémenté le tout dans un langage multithread un peu performant, les choses sont redevenues très simples
En pratique, cela consistait à lancer un démon séparé jouant le rôle de proxy
En utilisant des sockets Unix au lieu de TCP/IP, le coût de connexion devient relativement faible
Sur ce matériel, une appli hello world à 2 400 requêtes par seconde, ce n’est pas un peu mauvais ?
Je ne vois pas non plus ce qu’on gagne exactement en sacrifiant les performances. Le code n’est même pas devenu plus simple
Ces sites ne représentent qu’une partie du total
Ça pourrait probablement même encaisser un pic de trafic venu de HN
Cela a aussi été discuté hier : https://news.ycombinator.com/item?id=44464272
Dans mon entreprise, on sert encore parfois un répertoire cgi-bin pour de petites applis web internes développées rapidement
Si l’on garde les choses simples, c’est agréable à utiliser
CGI ne veut pas dire qu’il faut imprimer directement du HTTP/1.0 sur stdout
Par exemple, avec le wsgiref.handlers.CGIHandler intégré à Python, n’importe quelle appli WSGI peut être exécutée comme script CGI
import wsgiref.handlers, flask
app = flask.Flask(name)
wsgiref.handlers.CGIHandler().run(app)
Nous exécutons les scripts avec uwsgi et son plugin CGI[1]
Je trouve cela plus simple et plus flexible que de lancer Apache ou lighttpd à cause de mod_cgi
Comme uwsgi s’exécute comme unité systemd, on peut aussi exploiter toutes les fonctionnalités de durcissement et de sandboxing de systemd
Un aspect pratique du traitement CGI d’uwsgi, absent de mod_cgi, est qu’on peut préciser l’interpréteur à utiliser pour certains types de fichiers
cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3 # all dependencies go here
Le délai jusqu’au premier octet est de 250 à 350 ms, ce qui est acceptable pour notre usage
[1] : https://uwsgi-docs.readthedocs.io/en/latest/CGI.html
J’ai récemment utilisé Apache pour un projet perso, et l’une des raisons était les fonctionnalités .htaccess ; j’ai donc eu une discussion similaire
On peut placer un fichier .htaccess n’importe où, et Apache peut le lire à chaque requête comme configuration serveur supplémentaire : https://httpd.apache.org/docs/2.4/howto/htaccess.html
La grande raison de l’éviter était les performances. Il fallait un accès disque supplémentaire à chaque requête, et il valait toujours mieux, si possible, mettre cela dans le fichier de configuration principal
Mais aujourd’hui, la plupart des serveurs ont des SSD, et il y a de fortes chances que Linux ait aussi assez de RAM disponible pour le cache du système de fichiers
Bien sûr, Apache doit toujours parser la configuration à chaque requête, et pas une seule fois, donc les performances restent un peu moins bonnes
Mais vu que la plupart des CPU de serveurs sont beaucoup plus puissants aujourd’hui, c’est acceptable dans de nombreux cas d’usage
Le projet perso en est à une version très précoce, mais il est déjà utilisé : https://github.com/StaticPatch/StaticPatch/tree/main
« Je ne suis pas un vrai programmeur. Je colle des trucs ensemble jusqu’à ce que ça marche, puis je passe à autre chose. Les vrais programmeurs diraient : “D’accord, ça marche, mais il y a des fuites mémoire un peu partout. Il faudrait peut-être corriger ça ?” Moi, je n’ai qu’à redémarrer Apache toutes les 10 requêtes. »
PHP a énormément progressé depuis, mais une grande partie de ce progrès a consisté à corriger les erreurs initiales
« Si PHP 8 est bien meilleur, c’est parce qu’il contient beaucoup moins de mon code. »
À cause de ce choix, 99,99 % des requêtes HTTP sont ralenties par des lectures disque inutiles
J’y ai davantage réfléchi dans le cadre d’un workflow de prototypage rapide
Pour les langages JIT modernes, j’ai l’impression que, sauf à adopter le modèle FastCGI, le temps de démarrage est surtout dominé par les imports
Cette idée m’est venue en commençant à utiliser le serveur web h2o pour des scripts locaux : avec mruby et des handlers FastCGI, on peut écrire des fichiers de configuration propres et rapides, et les performances sont excellentes : https://h2o.examp1e.net/configure/fastcgi_directives.html
Un autre cas où c’est utile, c’est quand on veut permettre à des clients d’étendre un logiciel local avec leur propre code
Par exemple, au lieu de leur faire utiliser MCP pour étendre un outil d’IA, on pourrait leur faire implémenter une structure de requête donnée en CGI
Je me demande si le service MCP lui-même ne pourrait pas être implémenté en CGI
Un framework MCP pourrait aussi exposer ses fonctionnalités sous forme de programme prenant en charge les deux modes d’exécution
Il va falloir que je creuse un peu la spécification
Pour moi,
inetd, c’était tout simplement CGIGrâce à ça, Internet est devenu beaucoup plus amusant
J’ai moi-même hébergé plusieurs scripts shell avec inetd, et j’avais même un serveur HTTP entièrement écrit en Bash
Ce VPS a disparu depuis longtemps, et à l’époque je n’utilisais pas non plus de gestion de versions
Le laptop sur lequel je l’avais écrit a disparu lui aussi
Mais c’était vraiment amusant
Le déploiement était aussi simple que Makefile + scp, et les tests étaient un autre script Bash combinant plusieurs
netcatet grepC’était le bon vieux temps