Mit CGI-bin 200 Millionen Anfragen pro Tag verarbeiten
(simonwillison.net)- Jake Gold ließ ein Go-+SQLite-CGI-Programm auf einem AMD 3700X mit 16 Threads laufen, um zu prüfen, wie gut CGI im Stil der 1990er-Jahre auf moderner Hardware mithalten kann
- Das Ergebnis zeigt, dass selbst gewöhnliche Hardware mehr als 2.400 Anfragen pro Sekunde und damit über 200 Millionen Anfragen pro Tag verarbeiten kann
- CGI startet, führt aus und beendet für jede Anfrage einen Prozess; deshalb war der Overhead früher groß, und Ansätze wie PHP und FastCGI entstanden, um ihn zu reduzieren
- Simon Willison änderte 2020 mit datasette-ripgrep seine lange gehegte Überzeugung, dass man Prozessausführungen während Webanfragen vermeiden sollte, indem er für die Suche das Rust-basierte ripgrep-CLI aufrief
- Mit schnell startenden Sprachen wie Go und Rust sowie Multi-CPU-Umgebungen kann CGI-artige Verarbeitung realistischer sein als früher, ist aber keine allgemein empfohlene Standardmethode
CGI-Performance auf moderner Hardware neu betrachtet
- Serving 200 million requests per day with a cgi-bin ist ein Beitrag von Jake Gold, in dem er die Performance von CGI im Stil der 1990er-Jahre mit einem Go-+SQLite-CGI-Programm testet
- Die Testumgebung basiert auf einem System mit AMD 3700X und 16 Threads
- Das zentrale Ergebnis: Auch mit CGI kann gewöhnliche Hardware mehr als 2.400 Anfragen pro Sekunde und über 200 Millionen Anfragen pro Tag verarbeiten
- CGI funktioniert so, dass für jede eingehende Anfrage ein separater Prozess gestartet, ausgeführt und anschließend beendet wird
- Die frühe Web-Community entwickelte PHP und FastCGI, um diesen Overhead zu vermeiden, indem Code im Arbeitsspeicher resident gehalten und zusätzliche Kosten reduziert wurden
Warum CGI heute weniger schlecht sein könnte als früher
- Einer der früheren Engpässe von CGI war, dass Webskripte in Sprachen wie Perl, Python, Java geschrieben wurden, die nicht mit dem Ziel extrem schneller Startzeiten entworfen wurden
- Heute kann CGI-artige Anfrageverarbeitung mit Go und Rust deutlich effektiver funktionieren
- Simon Willison erstellte 2020 datasette-ripgrep, rief dabei das in Rust geschriebene CLI-Tool ripgrep per Shell auf, um Suchen auszuführen, und erzielte gute Ergebnisse
- Da CGI-Programme als separate Prozesse laufen, können sie gut zu Architekturen passen, die mehrere CPUs nutzen
- Moderne Server können 384 CPU-Threads haben
- Auch kleine VMs können 16 CPUs haben
- CPU- und Speicherleistung sind ebenfalls deutlich schneller geworden als früher
- Auch die Art, Webanwendungen wie 1998 zu schreiben, wird mit Go und Rust zu einem interessanten Experimentierfeld, ist aber nicht die Standardwahl, der die meisten Dienste folgen sollten
1 Kommentare
Meinungen auf Hacker News
Selbst mit etwas wie Python ist CGI heutzutage ziemlich schnell
Wenn der Start eines CGI-Skripts 400 ms CPU-Zeit kostet und der Server 64 Kerne hat, sind das 160 Requests pro Sekunde bzw. 14 Millionen Hits pro Tag und Server
Wenn ein Webdienst selbst ohne statische Assets schon mit ein paar Millionen Requests pro Tag überfordert ist, liegt der Flaschenhals nicht in der Startzeit des CGI-Prozesses
Früher hätte man gesagt: „eine langweilige Technik, die in der Python-Standardbibliothek lange unterstützt wurde“, aber die verbliebenen Python-Maintainer scheinen Stabilität und Abwärtskompatibilität für schädlich zu halten und haben Module, die zu langweilig und stabil waren, aus der Standardbibliothek entfernt
Das cgi-Modul wurde in Python 3.13 entfernt
Ich habe Python den größten Teil der letzten 25 Jahre täglich benutzt und greife aus Gewohnheit beim Prototyping immer noch dazu, aber inzwischen bedauere ich das und schwanke zwischen JS und Lua
Interessanterweise wird dort auf https://peps.python.org/pep-0206/ vom 14. Juli 2000 verlinkt, also von vor 25 Jahren; schon damals wurde das cgi-Paket als „schlecht designt und inzwischen fast unmöglich zu reparieren“ beschrieben
Das Paket https://github.com/jackrosenthal/legacy-cgi scheint einen Drop-in-Ersatz für das Standardbibliotheksmodul bereitzustellen
Diese Unterstützung bleibt im CGIHTTPRequestHandler des Moduls http.server erhalten
Im cgi-Modul waren nur ein paar Funktionen zum Parsen von HTML-Formulardaten enthalten
Aber dass man als nächste Alternative JS in Betracht zieht, das selbst gar keine Standardbibliothek hat, verstehe ich nicht ganz
Lua hat in seiner Standardbibliothek ebenfalls kein CGI-Modul
Heute geht es eher darum, die absurd verschwenderischen Teile zu finden, die man eingebaut hat, um schnell zu shippen, und sie abzustellen
Wenn man es richtig angeht, fügt die App abgesehen vom Zugriff auf den Speicher kaum zusätzliche Latenz hinzu
Python habe ich seit 1.6 gelernt, aber hauptsächlich für Betriebssystem-Skripting genutzt
Zwischen 1999 und 2003 habe ich in Apache- und IIS-Modulen Tcl verwendet und Module immer wieder in C neu geschrieben; dabei habe ich viele Lektionen auf die harte Tour gelernt
Kürzlich habe ich auf einem 350-Dollar-Mini-Server ein Golang-Binary, RabbitMQ, Redis und MySQL alle auf derselben Maschine getestet, und er hat dauerhaft 5.000 Requests pro Sekunde verarbeitet
Auf 24 Stunden hochgerechnet sind das 400 Millionen Requests
Es ist erstaunlich, wie gut kostenlose Tools heute sind, und ebenso erstaunlich, dass wir Cloud-Anbietern trotzdem so viel Geld zahlen
Es ist kein völlig identischer Vergleich, aber der Prozess, alles auf der Maschine im Keller zu entwickeln und zu tunen, hat wirklich Spaß gemacht
Es ist merkwürdig, diesen Overhead immer weiter in Kauf zu nehmen, ohne einen anderen Grund als „Google macht das so“
Ich sollte unbedingt einen Artikel über die modulare Monolith-Architektur schreiben, die bei uns gut funktioniert
Man kann bei einem Hoster einen dedizierten Server mieten und ihn nach Belieben nutzen. Allerdings ist man stark durch Bandbreiten- oder Traffic-Limits eingeschränkt
Dass Cloud genutzt wird, liegt daran, dass viele Interessen im Spiel sind. Zum Beispiel halten VCs und Investoren auch Anteile an Cloud-Unternehmen und fürchten, dass ihre Investition wegen eines imaginären Traffic-Spikes scheitert, der in Wirklichkeit nie kommen wird
Cloud-Vertrieb ist gut darin, die Ängste von Investoren anzusprechen
Wenn unterwegs ein Laufwerk ausfällt, gibt es keine Lösung, und wenn ich mich versehentlich aussperre, habe ich auch kein serielles Fallback-Terminal
Man könnte es so aufbauen wie die Leute bei r/homelab, aber dann ist unklar, ob man wirklich Geld spart, besonders wenn man meine Zeit einrechnet
Unterm Strich halte ich Cloud-Anbieter dank Skaleneffekten für ein ziemlich gutes Geschäft
Seltsamerweise bekommt man das eine nicht separat vom anderen
Stell dir vor, wie groß ein Geschäft oder Dienst sein könnte, den man mit vier 20-TB-Laufwerken und einer passablen CPU betreiben kann
Eine solche Konfiguration von einem Cloud-Anbieter zu bekommen, ist nicht leicht
Bei der Aussage „Die frühe Web-Community lernte schnell, dass das eine schlechte Idee war, und erfand Technologien wie PHP“ war die Kerntechnik streng genommen mod_php
PHP selbst unterschied sich in der Ausführungsweise nicht von Perl, aber dank der Designentscheidungen von mod_php im Vergleich zu mod_perl konnte man PHP-Skripte einfach auf den Server werfen und sie liefen schnell; mod_perl brauchte dagegen etwas Nachdenken und Magie, damit es funktionierte
Es hatte Content-Management, Aufgaben-Uploads, einen Veranstaltungskalender, Notenverwaltung, Echtzeit-Chat und Foren, und alles war reines C auf CGI; daran zu arbeiten war die Hölle
Als ich PHP kennenlernte, war ich fast zu Tränen gerührt, weil alles, was wir mühsam von Grund auf geschrieben hatten, indem wir RFCs lasen oder HTTP reverse-engineerten, in PHP einfache Funktionsaufrufe waren
Wir mussten nicht mehr fehlerhafte urlencode-Implementierungen debuggen oder einen ganzen Tag wegen eines seltsamen Carriage Returns in einem HTTP-Header verlieren
Die erste Version des Apache-Moduls war meines Wissens für PHP/FI Version 2.0 im Jahr 1996: https://www.php.net/manual/phpfi2.php#module
Wenn cgi-bin auf eine DB zugreifen muss, muss bei jedem Prozessstart eine Verbindung geöffnet werden
Code wie bei FastCGI im Speicher zu halten dient nicht nur dazu, die Kosten der Startzeit zu vermeiden, sondern auch dazu, einen DB-Verbindungspool oder zumindest persistente Verbindungen pro Thread haben zu können
Das passierte, wenn man „Python ist Single-Threaded, also starten wir viele Instanzen“ und „Python ist langsam, also starten wir viele Instanzen“ kombiniert hat
Bei größerer Skalierung braucht man am Ende einen gemeinsam genutzten Verbindungspool außerhalb von Python wie pgbouncer und viel Tuning, um die Last zu bewältigen, ohne die DB zu töten
Als es danach natürlich in einer halbwegs performanten Multithread-Sprache neu implementiert wurde, wurde alles wieder sehr einfach
Im Grunde startet man einen separaten Daemon, der als Proxy fungiert
Wenn man statt TCP/IP Unix-Sockets verwendet, sind die Verbindungskosten vergleichsweise niedrig
Sind 2.400 Requests pro Sekunde für eine Hello-World-App auf dieser Hardware nicht eher schlecht?
Ich weiß auch nicht, was man genau gewinnt, wenn man Performance aufgibt. Einfacher ist der Code dadurch auch nicht geworden
Solche Websites sind nur ein Teil des Ganzen
Einen HN-Traffic-Bomb dürfte es wohl aushalten
Wurde gestern schon diskutiert: https://news.ycombinator.com/item?id=44464272
In der Firma betreiben wir gelegentlich immer noch ein cgi-bin-Verzeichnis für schnell gebaute interne Web-Apps
Wenn man es einfach hält, ist die Usability gut
CGI bedeutet nicht, dass man HTTP/1.0 direkt auf stdout printen muss
Mit Pythons eingebautem wsgiref.handlers.CGIHandler kann man zum Beispiel jede WSGI-App als CGI-Skript ausführen
import wsgiref.handlers, flask
app = flask.Flask(name)
wsgiref.handlers.CGIHandler().run(app)
Wir führen die Skripte mit uwsgi und dessen CGI-Plugin[1] aus
Das wirkt einfacher und flexibler, als wegen mod_cgi Apache oder lighttpd zu starten
Da uwsgi als systemd-Unit läuft, können wir auch alle Hardening- und Sandboxing-Funktionen von systemd nutzen
Eine praktische Sache an uwsgis CGI-Handling, die mod_cgi nicht hat, ist, dass man den Interpreter für bestimmte Dateitypen festlegen kann
cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3 # all dependencies go here
Die Time to First Byte liegt bei 250–350 ms, was für unseren Zweck akzeptabel ist
[1]: https://uwsgi-docs.readthedocs.io/en/latest/CGI.html
Ich habe kürzlich Apache für ein Side Project verwendet, unter anderem wegen der .htaccess-Funktion, und hatte eine ähnliche Diskussion
Man kann eine .htaccess-Datei irgendwo ablegen, und Apache kann sie bei jedem Request als zusätzliche Serverkonfiguration einlesen: https://httpd.apache.org/docs/2.4/howto/htaccess.html
Ein wichtiger Grund, das zu vermeiden, war Performance. Bei jedem Request war ein zusätzlicher Plattenzugriff nötig, und wenn möglich war es immer besser, es in die Hauptkonfigurationsdatei zu schreiben
Heute haben die meisten Server aber SSDs, und Linux hat wahrscheinlich auch freien RAM, den es für den Dateisystem-Cache nutzen kann
Natürlich muss Apache die Konfiguration weiterhin bei jedem Request parsen, statt nur einmal, also wird die Performance immer noch etwas schlechter
Angesichts der heute meist stärkeren Server-CPUs ist das für viele Use Cases aber vertretbar
Das Side Project ist noch in einer sehr frühen Version, wird aber bereits genutzt: https://github.com/StaticPatch/StaticPatch/tree/main
„Ich bin kein echter Programmierer. Ich stecke Dinge zusammen, bis sie funktionieren, und dann gehe ich weiter. Echte Programmierer würden sagen: ‚Okay, es funktioniert, aber überall läuft Speicher aus. Sollten wir das nicht beheben?‘ Ich starte Apache einfach alle 10 Requests neu.“
PHP ist seitdem sehr weit gekommen, aber ein großer Teil davon bestand darin, frühe Fehler zu korrigieren
„Der Grund, warum PHP 8 viel besser ist, ist, dass viel weniger von meinem Code darin steckt.“
Wegen dieser Entscheidung werden 99,99 % der HTTP-Requests durch unnötige Disk-Reads verlangsamt
Habe im Rahmen eines schnellen Prototyping-Workflows mehr darüber nachgedacht
Bei modernen JIT-Sprachen dürfte die Startzeit größtenteils von Imports dominiert werden, solange man nicht auf das FastCGI-Modell setzt
Der Gedanke kam mir, als ich anfing, den h2o-Webserver für lokale Skripte zu nutzen; mit mruby und einem FastCGI-Handler lassen sich Konfigurationsdateien sauber und schnell schreiben, und es ist auch sehr schnell: https://h2o.examp1e.net/configure/fastcgi_directives.html
Ein weiterer nützlicher Fall ist, wenn Kunden lokale Software mit eigenem Code erweitern sollen
Statt sie zum Beispiel MCP verwenden zu lassen, um ein AI-Tool zu erweitern, könnte man sie eine bestimmte Request-Struktur als CGI implementieren lassen
Ich frage mich, ob sich auch ein MCP-Dienst selbst als CGI implementieren ließe
Ein MCP-Framework könnte Funktionen auch in Form eines Programms bereitstellen, das beide Ausführungsmodi unterstützt
Da muss ich mich wohl etwas tiefer in die Spezifikation einlesen
Für mich war
inetdpraktisch CGIDadurch wurde das Internet viel spannender
Ich habe selbst mehrere Shell-Skripte über inetd gehostet, und sogar einen HTTP-Server, der komplett in Bash geschrieben war
Dieser VPS ist längst verschwunden, und damals habe ich auch keine Versionsverwaltung verwendet
Auch der Laptop, auf dem ich das geschrieben hatte, ist weg
Trotzdem hat es unglaublich viel Spaß gemacht
Deployment war so einfach wie Makefile + scp, und die Tests bestanden aus einem weiteren Bash-Skript, das mehrere
netcat-Instanzen mit grep kombinierteDas waren gute Zeiten