Cómo procesar 200 millones de solicitudes al día con CGI-bin
(simonwillison.net)- Jake Gold ejecutó un programa CGI de Go + SQLite en un AMD 3700X de 16 hilos para comprobar cuánto puede aguantar el CGI al estilo de los años 90 en hardware moderno
- El resultado muestra que incluso con hardware común se pueden procesar más de 2400 solicitudes por segundo y más de 200 millones al día
- CGI inicia, ejecuta y termina un proceso por cada solicitud, por lo que en el pasado su sobrecarga era alta; para reducirla surgieron enfoques como PHP y FastCGI
- Simon Willison contó que en 2020, en datasette-ripgrep, procesó búsquedas invocando el CLI de ripgrep basado en Rust, lo que le hizo replantearse la vieja creencia de que había que evitar ejecutar procesos durante una solicitud web
- Con lenguajes de arranque rápido como Go y Rust, y en entornos con múltiples CPU, el procesamiento estilo CGI puede ser una opción más realista que antes, aunque no es la recomendación general
Rendimiento de CGI visto de nuevo en hardware moderno
- Serving 200 million requests per day with a cgi-bin es un artículo en el que Jake Gold prueba el rendimiento del CGI al estilo de los años 90 con un programa CGI de Go + SQLite
- El entorno de prueba fue un sistema basado en AMD 3700X de 16 hilos
- El resultado clave es que, incluso con CGI, se pueden procesar más de 2400 solicitudes por segundo y más de 200 millones de solicitudes al día en hardware común
- CGI funciona iniciando un proceso separado para cada solicitud entrante, ejecutándolo y luego finalizándolo
- La comunidad web de los primeros años creó PHP y FastCGI para evitar esa sobrecarga, manteniendo el código residente en memoria y reduciendo costos adicionales
Por qué CGI podría no ser tan malo como antes
- Uno de los cuellos de botella históricos de CGI era que los scripts web se escribían en lenguajes como Perl, Python y Java, que no fueron diseñados con el objetivo de arrancar extremadamente rápido
- Hoy, usando Go y Rust, el procesamiento de solicitudes al estilo CGI puede funcionar de manera mucho más eficaz
- En 2020, Simon Willison creó datasette-ripgrep e invocó desde el shell la herramienta CLI ripgrep, escrita en Rust, para ejecutar búsquedas, con buenos resultados
- Como los programas CGI se ejecutan como procesos separados, también pueden encajar bien en una arquitectura que aproveche varios CPU
- Los servidores modernos pueden tener 384 hilos de CPU
- Incluso una VM pequeña puede tener 16 CPU
- El rendimiento de CPU y memoria también ha mejorado mucho frente al pasado
- La forma de escribir aplicaciones web de 1998 también puede convertirse en un experimento interesante al combinarse con Go y Rust, aunque no es la opción predeterminada que la mayoría de los servicios debería seguir
1 comentarios
Opiniones de Hacker News
Incluso usando algo como Python, hoy en día CGI es bastante rápido
Si iniciar un script CGI consume 400 ms de CPU y el servidor tiene 64 núcleos, puede atender 160 solicitudes por segundo, o 14 millones de hits al día por servidor
Si un servicio web, excluyendo los recursos estáticos, sufre incluso con varios millones de solicitudes al día, el cuello de botella no es el tiempo de arranque del proceso CGI
Antes habría dicho que era “una tecnología aburrida con soporte desde hace mucho en la biblioteca estándar de Python”, pero parece que los mantenedores restantes de Python consideran perjudiciales la estabilidad y la compatibilidad hacia atrás, y han ido eliminando de la biblioteca estándar módulos demasiado aburridos y estables
El módulo cgi fue eliminado en Python 3.13
Lo usé a diario durante la mayor parte de los últimos 25 años, así que todavía tengo la costumbre de usar Python para prototipar, pero ahora me da pena y estoy dudando entre JS y Lua
Curiosamente, ahí enlazan a https://peps.python.org/pep-0206/, del 14 de julio de 2000, es decir, de hace 25 años, donde ya describían el paquete cgi como “mal diseñado y ahora casi imposible de arreglar”
El paquete https://github.com/jackrosenthal/legacy-cgi parece ofrecer un reemplazo drop-in del módulo de la biblioteca estándar
Ese soporte sigue estando en CGIHTTPRequestHandler del módulo http.server
Lo que había en el módulo cgi eran solo algunas funciones para parsear datos de formularios HTML
Pero no entiendo muy bien que, como siguiente alternativa, se considere JS, que no tiene biblioteca estándar como tal
Lua tampoco tiene un módulo CGI en su biblioteca estándar
Ahora se parece más a encontrar las partes absurdamente derrochadoras que se metieron para lanzar rápido y hacer que se detengan
Si se intenta bien, una app casi podría no añadir latencia más allá del acceso al almacenamiento
Aprendí Python desde la versión 1.6, pero lo usé sobre todo para scripting de sistema operativo
Entre 1999 y 2003 usé Tcl en módulos de Apache e IIS, y aprendí demasiadas lecciones por las malas mientras reescribía módulos una y otra vez en C
Hace poco probé en un mini servidor de 350 dólares un binario en Golang, RabbitMQ, Redis y MySQL, todo en la misma máquina, y sostuvo 5,000 solicitudes por segundo
Extrapolado a 24 horas, son 400 millones de solicitudes al día
Sorprende lo buenas que son las herramientas gratuitas de hoy, y también sorprende que aun así paguemos tanto dinero a los proveedores cloud
No es una comparación exactamente igual, pero fue muy bueno desarrollar y tunear todo en una máquina del sótano
Es raro seguir aceptando ese overhead sin más razón que “porque Google lo hace así”
Definitivamente tengo que escribir sobre la arquitectura de monolito modular que nos está funcionando bien
Puedes alquilar un servidor dedicado con una empresa de hosting y usarlo a tu gusto. Eso sí, quedas bastante atado a límites de ancho de banda o transferencia
La nube se usa porque hay muchos intereses en juego. Por ejemplo, los VC y los inversionistas también tienen participación en empresas cloud, y temen que la inversión fracase por un pico de tráfico imaginario que en realidad no llegará
Las ventas de cloud son hábiles para tocar las ansiedades de los inversionistas
Si un disco falla mientras estoy de viaje, no hay mucho que hacer, y si me bloqueo por error, tampoco tengo una terminal serial de fallback
Podría montarlo como la gente de r/homelab, pero entonces no queda claro si realmente ahorro dinero, sobre todo contando mi tiempo
En conclusión, creo que los proveedores cloud son una oferta bastante buena gracias a las economías de escala
Curiosamente, no se puede conseguir solo una de esas cosas por separado
Imagina qué negocio o servicio tan grande podrías operar con 4 discos de 20 TB y una CPU razonable
No es fácil conseguir una configuración así de un proveedor cloud
La parte de “la comunidad web temprana aprendió rápido que esto era una mala idea e inventó tecnologías como PHP” estrictamente hablando tenía como tecnología clave a mod_php
PHP en sí no era distinto de Perl en su forma de ejecutarse, pero gracias a las decisiones de diseño de mod_php frente a mod_perl, los scripts PHP podían simplemente tirarse al servidor y ejecutarse rápido, mientras que hacer funcionar mod_perl requería algo de reflexión y magia
Tenía gestión de contenidos, subida de tareas, calendario de eventos, gestión de calificaciones, chat en tiempo real y foros, y todo era C puro sobre CGI, lo cual era un infierno para trabajar
El día que conocí PHP casi me dieron ganas de llorar, porque todo lo que hasta entonces escribíamos con esfuerzo desde cero leyendo RFC o haciendo ingeniería inversa de HTTP, en PHP era una simple llamada a función
Ya no hacía falta depurar implementaciones torpes de urlencode ni perder un día por un retorno de carro raro en un encabezado HTTP
Creo que la primera versión del módulo de Apache fue para PHP/FI Version 2.0, en 1996: https://www.php.net/manual/phpfi2.php#module
Si cgi-bin necesita acceder a la base de datos, debe abrir una conexión cada vez que se inicia el proceso
Mantener el código en memoria, como FastCGI, no es solo para evitar el costo del tiempo de arranque, sino también porque permite tener un pool de conexiones a la BD o al menos conexiones persistentes por hilo
Eso pasó al combinar “Python es de un solo hilo, así que levantemos muchos procesos” con “Python es lento, así que levantemos muchos procesos”
Cuando la escala crece, al final hay que usar un pool de conexiones compartido fuera de Python, como pgbouncer, y mucho tuning para manejar la carga sin matar la BD
Claro que después, al reimplementarlo en un lenguaje multihilo con cierto rendimiento, volvió a ser muy simple
En la práctica, consistía en levantar un demonio separado que actuaba como proxy
Si se usan sockets Unix en lugar de TCP/IP, el costo de conexión es relativamente bajo
¿No es un poco malo que en este hardware una app hello world haga 2,400 solicitudes por segundo?
No sé exactamente qué se gana a cambio de sacrificar rendimiento. El código tampoco se volvió más simple
Esos sitios son apenas una parte del total
Probablemente incluso aguante una avalancha de tráfico de HN
También se discutió ayer: https://news.ycombinator.com/item?id=44464272
En la empresa todavía servimos un directorio cgi-bin para apps web internas que a veces se hacen rápido
Si lo mantienes simple, resulta cómodo de usar
Que sea CGI no significa que tengas que imprimir HTTP/1.0 directamente a stdout
Por ejemplo, si usas el wsgiref.handlers.CGIHandler integrado de Python, puedes ejecutar cualquier app WSGI como script CGI
import wsgiref.handlers, flask
app = flask.Flask(name)
wsgiref.handlers.CGIHandler().run(app)
Nosotros ejecutamos los scripts con uwsgi y su plugin CGI[1]
Se siente más simple y flexible que levantar Apache o lighttpd por mod_cgi
Como uwsgi se ejecuta como una unidad de systemd, también podemos aprovechar todas las funciones de endurecimiento y sandboxing de systemd
Algo cómodo del manejo de CGI en uwsgi que no existe en mod_cgi es que puedes especificar el intérprete que se usará para ciertos tipos de archivo
cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3 # all dependencies go here
El tiempo hasta el primer byte es de 250~350 ms, y para nuestro uso es aceptable
[1]: https://uwsgi-docs.readthedocs.io/en/latest/CGI.html
Hace poco usé Apache para un proyecto paralelo, y una de las razones fue la funcionalidad .htaccess, así que tuve una conversación parecida
Puedes poner archivos .htaccess en cualquier lugar y Apache puede leerlos como configuración adicional del servidor en cada solicitud: https://httpd.apache.org/docs/2.4/howto/htaccess.html
Una gran razón para evitar esto era el rendimiento. Requería accesos adicionales a disco en cada solicitud, y siempre era mejor ponerlo en el archivo de configuración principal si era posible
Pero hoy la mayoría de los servidores tienen SSD, y es muy probable que Linux también tenga RAM libre para usar en la caché del sistema de archivos
Claro que el rendimiento sigue bajando un poco porque Apache tiene que parsear la configuración en cada solicitud, no una sola vez
Aun así, considerando que hoy la mayoría de los CPU de servidor son más potentes, en muchos casos de uso es algo asumible
El proyecto paralelo está en una versión muy inicial, pero ya se está usando: https://github.com/StaticPatch/StaticPatch/tree/main
“No soy un programador de verdad. Voy pegando cosas hasta que funcionen y luego sigo adelante. Los programadores de verdad dirían: ‘Bueno, funciona, pero hay fugas de memoria por todos lados. ¿No habría que arreglarlo?’. Yo simplemente reinicio Apache cada 10 solicitudes.”
PHP ha avanzado muchísimo desde entonces, pero gran parte de eso fue corregir los errores iniciales
“La razón por la que PHP 8 es mucho mejor es que contiene mucho menos de mi código.”
Por esta decisión, el 99.99% de las solicitudes HTTP terminan ralentizadas por lecturas de disco innecesarias
Estuve pensando más en esto como parte de un flujo de trabajo de prototipado rápido
Me parece que, en los lenguajes JIT modernos, el tiempo de arranque estará dominado en gran parte por los imports, a menos que vayan por el modelo FastCGI
Se me ocurrió esto al empezar a usar el servidor web h2o para scripts locales; con mruby y handlers FastCGI se pueden escribir archivos de configuración limpios y rápidos, y además es muy veloz: https://h2o.examp1e.net/configure/fastcgi_directives.html
Otro caso donde resulta útil es cuando se permite que los clientes extiendan software local con su propio código
Por ejemplo, en vez de hacer que usen MCP para extender herramientas de IA, se les podría pedir que implementen cierta estructura de requests como CGI
Me pregunto si el propio servicio MCP no podría implementarse como CGI
Un framework MCP también podría exponer funcionalidades en forma de programa que admita ambos modos de ejecución
Tendría que indagar un poco más en la especificación
Para mí,
inetdera básicamente CGIGracias a eso, internet se volvió mucho más divertida
Yo mismo alojé varios scripts de shell con inetd, e incluso tuve un servidor HTTP escrito completamente en Bash
Ese VPS desapareció hace mucho, y en ese entonces ni siquiera usaba control de versiones
La laptop en la que lo escribí también desapareció
Pero fue realmente divertido
El despliegue era tan fácil como Makefile + scp, y las pruebas eran otro script en Bash que combinaba varios
netcatcon grepQué buenos tiempos