2 Punkte von GN⁺ 2025-07-07 | 1 Kommentare | Auf WhatsApp teilen
  • 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

 
GN⁺ 2025-07-07
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

    • Die Begründung für die Entfernung von cgi steht unter https://peps.python.org/pep-0594/#cgi
      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
    • Was die Python-Maintainer entfernen, ist das Modul namens cgi, nicht die Unterstützung zum Implementieren von CGI-Skripten an sich
      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
    • Ich kann verstehen, dass man unzufrieden damit ist, dass Python CGI aus der Standardbibliothek entfernt
      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
    • Früher war performantes Web eine Art technische Kunstform
      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
    • In solchen Fällen bevorzuge ich eher PHP oder JS, wo ein JIT standardmäßig vorhanden ist
      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

    • Ich habe zu oft gesehen, dass Leute ein Kubernetes-Microservices-Monster deployen, das die Entwicklung um den Faktor 10 verlangsamt, ohne zu wissen, dass ein Server weit mehr als 1 Request pro Sekunde verarbeiten kann
      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 muss nicht zwingend Geld an einen Cloud-Anbieter zahlen
      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
    • Ich habe darüber nachgedacht, ein Side Project im Keller zu hosten, aber dann wäre es anfällig für Stromausfälle und ISP-Störungen
      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
    • Wenn man sich VM-Kosten ansieht, sind sie deshalb hoch, weil man nicht viel Rechenleistung braucht, sondern Server mit viel lokalem Speicher
      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

    • Damals entwickelte ich mit einem Freund etwas, das man später wohl Lernmanagementsystem genannt hätte
      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
    • Stimmt, aber mod_php wurde dem PHP-Ökosystem früh hinzugefügt und wurde schnell zur Standardmethode der Auslieferung
      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

    • Bei größerer Skalierung leidet die Datenbank unter der Zahl der Verbindungen
      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
    • Deshalb hat sich CGI letztlich zu einem Modell weiterentwickelt, das zwischen Requests etwas Zustand beibehält
    • Dafür gab es auch eine Standardmethode
      Im Grunde startet man einen separaten Daemon, der als Proxy fungiert
      Wenn man statt TCP/IP Unix-Sockets verwendet, sind die Verbindungskosten vergleichsweise niedrig
    • Man kann UDP verwenden
  • 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

    • Das ist nur dann ein schlechter Wert, wenn man mehr als 2.000 Requests pro Sekunde braucht
      Solche Websites sind nur ein Teil des Ganzen
    • Nicht großartig, aber für viele Use Cases ausreichend
      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 uws­gis 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

    • Gute Info, und wsgiref.handlers.CGIHandler scheint noch nicht zur Abschaffung vorgesehen zu sein
  • 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

    • Um Rasmus Lerdorf zu zitieren:
      „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.“
    • Ich verstehe nicht, warum Apache nicht einfach das Dateisystem überwacht
      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

    • Ein MCP-Frontend für CGI-Programme in Endnutzerumgebungen ist keine schlechte Idee
      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
    • FastCGI verliert tendenziell fast alle Vorteile von CGI
  • Für mich war inetd praktisch CGI
    Dadurch 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 kombinierte
    Das waren gute Zeiten