Processando 200 milhões de requisições por dia com CGI-bin
(simonwillison.net)- Jake Gold executou um programa CGI em Go + SQLite em um AMD 3700X de 16 threads para verificar até que ponto o CGI no estilo dos anos 1990 aguenta em hardware moderno
- O resultado mostra que, mesmo em hardware comum, é possível processar mais de 2.400 requisições por segundo e mais de 200 milhões de requisições por dia
- Como o CGI inicia, executa e encerra um processo a cada requisição, no passado havia uma sobrecarga grande; para reduzi-la, surgiram abordagens como PHP e FastCGI
- Em 2020, no datasette-ripgrep, Simon Willison chamou a CLI ripgrep baseada em Rust para processar buscas, o que mudou a antiga crença de que era preciso evitar executar processos durante requisições web
- Com linguagens de inicialização rápida como Go e Rust e ambientes com múltiplas CPUs, o processamento no estilo CGI pode ser uma opção mais viável do que antes, embora não seja a recomendação geral
Reavaliando o desempenho do CGI em hardware moderno
- Serving 200 million requests per day with a cgi-bin é um texto em que Jake Gold testa o desempenho do CGI no estilo dos anos 1990 usando um programa CGI em Go + SQLite
- O ambiente de teste é um sistema baseado em AMD 3700X de 16 threads
- O principal resultado é que, mesmo com CGI, hardware comum consegue processar mais de 2.400 requisições por segundo e mais de 200 milhões de requisições por dia
- O CGI funciona iniciando um processo separado para cada requisição recebida, executando-o e depois encerrando-o
- Para evitar essa sobrecarga, a comunidade web inicial criou PHP e FastCGI, que mantinham o código residente na memória para reduzir custos adicionais
Por que o CGI talvez não seja tão ruim quanto antes
- Um dos gargalos do CGI no passado era o fato de scripts web serem escritos em linguagens como Perl, Python e Java, que não foram projetadas com foco em inicialização extremamente rápida
- Hoje, usando Go e Rust, o processamento de requisições no estilo CGI pode funcionar de forma muito mais eficaz
- Em 2020, ao criar o datasette-ripgrep, Simon Willison chamou via shell a ferramenta CLI ripgrep, escrita em Rust, para executar buscas, e obteve bons resultados
- Como programas CGI são executados como processos separados, eles também podem se ajustar bem a arquiteturas que aproveitam várias CPUs
- Servidores modernos podem ter 384 threads de CPU
- Mesmo VMs pequenas podem ter 16 CPUs
- O desempenho de CPU e memória também ficou muito mais rápido do que no passado
- A forma de escrever aplicações web de 1998 também se torna um experimento interessante quando encontra Go e Rust, mas não é a escolha padrão que a maioria dos serviços deve seguir
1 comentários
Opiniões no Hacker News
Mesmo usando algo como Python, hoje em dia CGI é bem rápido
Se iniciar um script CGI consome 400 ms de CPU e o servidor tem 64 núcleos, ele consegue lidar com 160 requisições por segundo, ou 14 milhões de hits por dia por servidor
Para um serviço web que sofre para atender alguns milhões de requisições por dia, excluindo assets estáticos, o gargalo não é o tempo de inicialização do processo CGI
Antigamente eu diria que era uma “tecnologia chata, com suporte de longa data na biblioteca padrão do Python”, mas os mantenedores restantes do Python parecem ver estabilidade e compatibilidade retroativa como prejudiciais, e têm removido da biblioteca padrão módulos que são chatos e estáveis demais
O módulo cgi foi removido no Python 3.13
Usei Python diariamente durante a maior parte dos últimos 25 anos, então ainda tenho o hábito de usá-lo para prototipagem, mas agora lamento isso e estou em dúvida entre JS e Lua
Curiosamente, ali há um link para https://peps.python.org/pep-0206/, de 14 de julho de 2000, ou seja, 25 anos atrás, e já naquela época o pacote cgi era descrito como “mal projetado e agora quase impossível de consertar”
O pacote https://github.com/jackrosenthal/legacy-cgi parece oferecer um substituto drop-in para o módulo da biblioteca padrão
Esse suporte continua no CGIHTTPRequestHandler do módulo http.server
O que havia no módulo cgi eram apenas algumas funções para fazer o parse de dados de formulários HTML
Mas não entendo muito bem considerar, como alternativa em seguida, JS, que não tem biblioteca padrão propriamente dita
Lua também não tem um módulo CGI na biblioteca padrão
Hoje está mais para encontrar as partes absurdamente desperdiçadoras que foram colocadas para lançar rápido e fazê-las parar
Se você tentar direito, a aplicação pode praticamente não acrescentar latência além do acesso ao armazenamento
Aprendi Python desde a versão 1.6, mas o usei principalmente para scripting de sistema operacional
Entre 1999 e 2003 usei Tcl em módulos do Apache e do IIS, e aprendi muitas lições da forma difícil enquanto reescrevia continuamente módulos em C
Recentemente testei em um mini servidor de US$ 350 um binário em Golang, RabbitMQ, Redis e MySQL, todos rodando na mesma máquina, e ele sustentou 5.000 requisições por segundo
Convertendo para 24 horas por dia, são 400 milhões de requisições
É impressionante como as ferramentas gratuitas de hoje são excelentes, e também é impressionante que ainda assim paguemos tanto dinheiro aos provedores de nuvem
Não é uma comparação totalmente equivalente, mas foi muito bom desenvolver e ajustar tudo em uma máquina no porão
É estranho continuar arcando com esse overhead sem motivo além de “o Google faz assim”
Preciso mesmo escrever um texto sobre a arquitetura de monólito modular que temos usado bem
Dá para alugar um servidor dedicado de uma empresa de hospedagem e usá-lo à vontade. Só que você fica bastante preso a limites de largura de banda ou de transferência
A nuvem é usada porque há muitos interesses envolvidos. Por exemplo, VCs e investidores também têm participação em empresas de nuvem e temem que o investimento fracasse por causa de um pico de tráfego imaginário que, na prática, nunca virá
Vendas de nuvem são hábeis em explorar as ansiedades dos investidores
Se um drive quebrar enquanto eu estiver viajando, não há solução; e, se eu me trancar para fora por engano, também não há terminal serial de fallback
Dá para montar algo como o pessoal do r/homelab, mas aí fica incerto se isso realmente economiza dinheiro, especialmente considerando meu tempo
No fim, vejo os provedores de nuvem como um negócio bastante bom graças à economia de escala
Estranhamente, não dá para obter só uma dessas coisas separadamente
Imagine o tamanho do negócio ou serviço que você poderia operar com 4 discos de 20 TB e uma CPU razoável
Não é fácil conseguir essa configuração de um provedor de nuvem
A parte que diz que “as primeiras comunidades da web aprenderam rapidamente que isso era uma má ideia e inventaram tecnologias como PHP” é, estritamente falando, que a tecnologia essencial foi mod_php
O PHP em si não era diferente de Perl na forma de execução, mas, graças às escolhas de projeto do mod_php em comparação com o mod_perl, scripts PHP podiam ser simplesmente jogados no servidor e rodavam rápido, enquanto o mod_perl exigia um pouco de reflexão e magia para funcionar
Tinha gerenciamento de conteúdo, upload de tarefas, calendário de eventos, gestão de notas, chat em tempo real e fóruns, tudo em C puro sobre CGI, e trabalhar nisso era um inferno
No dia em que conheci PHP, quase chorei, porque tudo que eu vinha implementando com dificuldade do zero, lendo RFCs ou fazendo engenharia reversa de HTTP, em PHP era uma simples chamada de função
Eu não precisava mais depurar uma implementação capenga de urlencode nem perder um dia por causa de um carriage return estranho em um cabeçalho HTTP
Pelo que vejo, a primeira versão do módulo Apache foi para PHP/FI Version 2.0, em 1996: https://www.php.net/manual/phpfi2.php#module
Se o cgi-bin precisar acessar o DB, será necessário abrir uma conexão toda vez que o processo iniciar
Deixar o código na memória, como no FastCGI, não é só para evitar o custo de tempo de inicialização, mas também porque permite ter um pool de conexões com o DB, ou pelo menos conexões persistentes por thread
Foi o que aconteceu quando se combinou “Python é single-thread, então vamos subir muitas instâncias” com “Python é lento, então vamos subir muitas instâncias”
Quando a escala aumenta, no fim é preciso usar um pool de conexões compartilhado fora do Python, como o pgbouncer, e muita otimização para lidar com a carga sem matar o DB
Claro que, depois disso, ao reimplementar em uma linguagem multithread com desempenho razoável, tudo voltou a ficar bem simples
Na prática, consistia em subir um daemon separado que atuava como proxy
Usando soquetes Unix em vez de TCP/IP, o custo de conexão fica relativamente baixo
Neste hardware, uma aplicação hello world com 2.400 requisições por segundo não é meio ruim?
Também não sei exatamente o que se ganha ao abrir mão de desempenho. O código nem ficou mais simples
Sites assim são apenas uma parte do total
Parece que aguentaria até uma bomba de tráfego do HN
Isso também foi discutido ontem: https://news.ycombinator.com/item?id=44464272
Na empresa, ainda servimos um diretório cgi-bin para apps web internos feitos rapidamente de vez em quando
Se mantiver simples, a usabilidade é boa
CGI não significa que você precise dar print diretamente de HTTP/1.0 para stdout
Por exemplo, usando o wsgiref.handlers.CGIHandler embutido do Python, qualquer app WSGI pode ser executado como um script CGI
import wsgiref.handlers, flask
app = flask.Flask(name)
wsgiref.handlers.CGIHandler().run(app)
Executamos os scripts com uwsgi e seu plugin CGI[1]
Acho mais simples e flexível do que subir Apache ou lighttpd por causa do mod_cgi
Como o uwsgi roda como uma unidade do systemd, também podemos aproveitar todos os recursos de hardening e sandboxing do systemd
Uma conveniência no tratamento de CGI do uwsgi que não existe no mod_cgi é poder especificar o interpretador a ser usado para determinados tipos de arquivo
cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3 # all dependencies go here
O tempo até o primeiro byte fica entre 250 e 350 ms, e para nosso uso é aceitável
[1]: https://uwsgi-docs.readthedocs.io/en/latest/CGI.html
Usei Apache em um projeto paralelo recente, e um dos motivos era o recurso .htaccess, então tive uma conversa parecida
Você pode colocar arquivos .htaccess em qualquer lugar, e o Apache pode lê-los a cada requisição como configuração adicional do servidor: https://httpd.apache.org/docs/2.4/howto/htaccess.html
Um grande motivo para evitar isso era desempenho. Exigia acesso adicional ao disco em cada requisição, e, quando possível, sempre era melhor colocar no arquivo de configuração principal
Mas hoje a maioria dos servidores tem SSD, e é bem provável que o Linux tenha RAM sobrando para usar no cache do sistema de arquivos
Claro que o Apache ainda precisa fazer o parsing da configuração a cada requisição, em vez de uma única vez, então o desempenho ainda piora um pouco
Ainda assim, considerando que a maioria das CPUs de servidor hoje é mais potente, isso é aceitável em muitos casos de uso
O projeto paralelo está em uma versão bem inicial, mas já está em uso: https://github.com/StaticPatch/StaticPatch/tree/main
“Eu não sou um programador de verdade. Vou juntando coisas até funcionar e então sigo em frente. Programadores de verdade diriam: ‘Certo, funciona, mas está vazando memória por todos os lados. Será que não deveríamos consertar?’ Eu simplesmente reinicio o Apache a cada 10 requisições.”
O PHP avançou muito desde então, mas boa parte disso foi o processo de corrigir os erros iniciais
“O motivo de o PHP 8 ser muito melhor é que há muito menos do meu código nele.”
Por causa dessa escolha, 99,99% das requisições HTTP acabam ficando mais lentas por leituras de disco desnecessárias
Pensei mais sobre isso como parte de um fluxo de trabalho de prototipagem rápida
Nas linguagens JIT modernas, acho que o tempo de inicialização será dominado principalmente pelos imports, a menos que se use o modelo FastCGI
Comecei a pensar nisso quando passei a usar o servidor web h2o para scripts locais; com mruby e um handler FastCGI, dá para escrever arquivos de configuração de forma limpa e rápida, e o desempenho também é muito alto: https://h2o.examp1e.net/configure/fastcgi_directives.html
Outro caso útil é quando se quer permitir que clientes estendam um software local com o próprio código
Por exemplo, em vez de fazer com que usem MCP para estender uma ferramenta de IA, você poderia pedir que implementassem uma estrutura específica de requisição como CGI
Fico me perguntando se o próprio serviço MCP também não poderia ser implementado em CGI
Um framework MCP poderia expor funcionalidades na forma de um programa que suporte ambos os modos de execução
Vou ter que investigar melhor a especificação
Para mim,
inetdera praticamente CGIGraças a isso, a internet ficou muito mais divertida
Eu mesmo hospedei vários scripts de shell com inetd, e também tinha um servidor HTTP escrito inteiramente em Bash
Aquele VPS desapareceu há muito tempo, e na época eu nem usava controle de versão
O notebook em que escrevi isso também sumiu
Mesmo assim, foi muito divertido
O deploy era tão simples quanto Makefile + scp, e os testes eram outro script Bash que misturava vários
netcatcom grepBons tempos