- O zeroserve, um servidor HTTPS pequeno e rápido, recebe um tarball de site e o serve com HTTP/2 e TLS 1.3, executando programas eBPF dentro do tarball como middleware em sandbox no espaço do usuário a cada requisição
- Sem arquivo de configuração, um programa eBPF decide por requisição o roteamento, os cabeçalhos, a autenticação, o rate limiting e o proxy, unificando a configuração declarativa de nginx e Caddy com uma camada separada de scripting
- O site é indexado como um único arquivo tar e não é extraído em disco; ao trocar o tarball e enviar
SIGHUP, site, scripts e materiais TLS são substituídos atomicamente sem perda de conexão
- Em benchmarks HTTPS de núcleo único, o zeroserve registrou 36.681 req/s com arquivos estáticos pequenos, 46.945 req/s com JSON dinâmico em eBPF de 10 ms e 26.486 req/s com proxy pequeno, mas no proxy de 100 KB o nginx ficou à frente com 5.882 req/s
- O zeroserve busca ser uma alternativa a nginx e Caddy combinando deploy em um único tarball, configuração programável, eBPF em espaço do usuário e TLS moderno, mas para respostas grandes em proxy o nginx é mais adequado
Visão geral
- zeroserve é um servidor HTTPS pequeno, rápido e sem configuração que serve um único tarball de site via HTTP/2 e TLS 1.3
- Programas eBPF colocados dentro do tarball são executados em todas as requisições como middleware em sandbox no espaço do usuário, permitindo reescrita de requisições, autenticação, rate limiting e reverse proxy para backends
- É um servidor projetado para, em base de núcleo único, superar o nginx na maioria das cargas de trabalho com arquivos estáticos pequenos e grandes, middleware com script e proxy com respostas pequenas
- Os scripts eBPF são compilados JIT para código nativo e isolados em sandbox no espaço do usuário, com custo baixo o suficiente para rodar por requisição
- Operações de rede e disco são enviadas via
io_uring por meio do runtime monoio
- Suporte a TLS 1.3, HTTP/2, Encrypted Client Hello, seleção de certificado por SNI e fingerprinting JA4
- O site inteiro e os materiais TLS são servidos a partir de um único tarball, com hot reload via
SIGHUP
Modelo de configuração: o programa é a configuração
- O zeroserve tem como objetivo ser uma alternativa ao nginx e ao Caddy, e sua principal escolha de design está na forma de configuração
- Nginx e Caddy oferecem linguagens de configuração declarativas com blocos
location, regras rewrite, diretivas map e recursos como try_files; quando chegam ao limite, acoplam runtimes opcionais de scripting, como Lua ou plugins do Caddy
- Nessa estrutura, o comportamento fica dividido entre uma camada de diretivas com seu próprio fluxo de controle e uma camada de scripts executada em pontos específicos do ciclo de vida da requisição
- No zeroserve não há arquivo de configuração: um único programa eBPF vê todas as requisições e decide roteamento, cabeçalhos, autenticação, rate limiting e proxy
Servindo um único tarball diretamente
- O site inteiro é um único arquivo
tar, e o zeroserve cria no carregamento um mapa path -> byte-range e serve os arquivos lendo intervalos de bytes diretamente do próprio tarball
- Como nenhum arquivo é extraído em disco, o site existe apenas dentro de um único arquivo, e não há document root exposto por uma regra
location mal configurada
- O deploy é feito por substituição atômica de um único arquivo: para publicar uma nova versão, troca-se o tarball e depois envia-se
SIGHUP
- O empacotamento do diretório e o comando de execução seguem o formato abaixo
zeroserve --pack ./public > site.tar
zeroserve --addr 0.0.0.0:8080 site.tar
- O comando de hot reload segue o formato abaixo
killall -SIGHUP zeroserve
- O reload substitui atomicamente, dentro do mesmo processo, o site, os scripts e os materiais TLS, funcionando sem perda de conexão
- Cada instância é um event loop de thread única; isso é um limite por processo, mas é apresentado como uma forma adequada quando a unidade de escala é “mais processos”
Scripting com eBPF no espaço do usuário
- Todos os arquivos
.c colocados em .zeroserve/scripts/ são compilados em objetos eBPF com clang e llc no momento do empacotamento e executados em todas as requisições
- O eBPF roda no espaço do usuário por meio do runtime async-ebpf, dentro de um processo comum sem privilégios, sem depender do subsistema BPF do kernel nem de
CAP_BPF
- O async-ebpf incorpora o uBPF, compilando JIT o bytecode em código de máquina nativo x86-64
- Um pointer cage mascara todos os acessos à memória do código compilado JIT para uma arena exclusiva do programa, confinando acessos inválidos à memória do script
- Os scripts são executados diretamente no event loop único do zeroserve; para que um script lento não pare outras conexões, um timer pode interromper o código nativo compilado JIT durante a execução e devolver o controle ao event loop
- O modelo de programação é uma cadeia de scripts executados em ordem de nome de arquivo, e eles compartilham um mapa de metadados por requisição
- Quando um script chama
zs_respond ou zs_reverse_proxy, a cadeia é encerrada por curto-circuito
- Chaves sob
zs.response.header.* viram cabeçalhos de todas as respostas, e outras chaves são usadas em uma pequena etapa de template que substitui placeholders como <zs-meta>visitor</zs-meta> em arquivos HTML no momento da saída
- A superfície de helpers oferece leitura de método, caminho, query, cabeçalhos e endereço do peer da requisição, além de reescrita de URI e definição/remoção de cabeçalhos
- Helpers de criptografia e encoding fornecem SHA-256, HMAC-SHA256, base64, hex e
getrandom
- Helpers de JSON oferecem parsing do corpo da requisição, criação/modificação da árvore do documento e resposta com
zs_json_respond
- O rate limiting suporta token bucket com base em chaves arbitrárias, como IP do peer ou chave de API, e o estado é mantido mesmo após hot reload
- Helpers de AWS SigV4 oferecem suporte a cabeçalhos
Authorization assinados e URLs presignadas para comunicação com S3 e outros serviços da AWS
- O login via OIDC fornece um fluxo relying-party baseado em Authorization Code + PKCE e armazena toda a sessão de login em um cookie selado com XChaCha20-Poly1305, mantendo o servidor stateless e permitindo colocar um site estático atrás de “Entrar com o Google”
- Endpoints dinâmicos funcionam com scripts respondendo diretamente em caminhos específicos; no exemplo, uma requisição para
/health retorna o cabeçalho application/json e o corpo {"status":"ok"}
- Cada script roda com um limite padrão de 256 KB de memória, e o runtime faz time slicing de scripts longos no executor e aplica throttling a scripts descontrolados
- Os scripts podem chamar uns aos outros com
zs_call, e a profundidade de chamada é limitada
- Um script preso em loop infinito atrasa apenas a própria requisição; o timer preemptivo o interrompe para que o servidor continue processando as demais
- A camada TLS é exclusiva para TLS 1.3 e terminada com BoringSSL
- O Encrypted Client Hello evita que o SNI real apareça em texto puro e oferece seleção de certificado por SNI baseada em diretório, além de fingerprinting JA4 do cliente exposto aos scripts
- O modo de relay ECH transparente repassa byte a byte para o upstream real handshakes que não consegue descriptografar, permitindo que nomes protegidos fiquem ocultos atrás de nomes públicos
Desempenho
-
Condições do benchmark
- Comparação entre zeroserve, nginx 1.26 e Caddy 2.11 servindo o mesmo conteúdo e o mesmo certificado autoassinado via HTTPS em um Ryzen 7 3700X de 8 núcleos
- Como a instância do zeroserve é, por design, single-threaded, o critério de comparação é desempenho por núcleo
- Todos os servidores foram fixados a uma única CPU com
taskset; o nginx usou worker_processes 1, o Caddy GOMAXPROCS=1 e o zeroserve sua estrutura single-thread existente
- A carga foi gerada em outro núcleo com
wrk -t4 -c100, usando a mediana de 3 execuções de 10 segundos
- O
wrk usa HTTP/1.1, então os números representam HTTP/1.1 sobre TLS 1.3 e o custo em estado estável de conexões HTTPS já abertas, com o custo de handshake amortizado por conexões keep-alive longas
-
Arquivo estático pequeno de 174 B
| Servidor |
req/s |
p99 |
| zeroserve |
36.681 |
5,4 ms |
| nginx |
31.226 |
7,8 ms |
| Caddy |
12.830 |
22 ms |
- O zeroserve serviu arquivos pequenos cerca de 17% mais rápido que o nginx em um único núcleo, com menor latência de cauda
- Casos básicos de sites estáticos como páginas HTML, JSON pequeno e CSS são o principal alvo de tuning do zeroserve
-
Arquivo estático grande de 100 KB
| Servidor |
req/s |
Vazão |
p99 |
| zeroserve |
8.000 |
782 MB/s |
22 ms |
| nginx |
7.600 |
773 MB/s |
28 ms |
| Caddy |
6.084 |
590 MB/s |
44 ms |
- Os resultados dos três servidores ficaram próximos, com o zeroserve ligeiramente à frente, a cerca de 780 MB/s em um único núcleo
- O
sendfile() do nginx, uma vantagem em arquivos grandes, não é usado sob TLS, pois os bytes precisam ser criptografados no espaço do usuário, então os três servidores ficam limitados pelo loop de criptografia e escrita
- Com o kernel TLS desativado nos três servidores, o caminho de leitura e escrita via
io_uring do zeroserve acabou sendo um pouco mais rápido
eBPF vs Lua
- A comparação de scripting usa como referência o nginx + LuaJIT
ngx_http_lua_module, uma forma comum de executar código rápido dentro de servidores web
- O zeroserve define por padrão o timer de preempção de scripts para 2 ms; um intervalo mais fino estrangula scripts problemáticos mais rapidamente, mas também impõe custo aos scripts normais
- Com o padrão de 2 ms, em respostas totalmente dinâmicas o eBPF fica em cerca de 32k req/s, abaixo dos 41k req/s do Lua no nginx
- Ao aumentar
--preempt-timer-interval-ms para 10, a vazão de scripting recupera cerca de 40% e o resultado se inverte
-
Middleware de injeção de cabeçalho por requisição
| Engine |
req/s |
p99 |
| zeroserve eBPF 10 ms |
43.709 |
5,1 ms |
| zeroserve eBPF padrão 2 ms |
31.334 |
6,7 ms |
nginx Lua header_filter |
28.653 |
8,4 ms |
- No caso de middleware em que o script roda mas arquivos estáticos continuam sendo servidos, o eBPF em 10 ms ficou cerca de 50% acima do nginx Lua, com menor latência de cauda
-
Resposta JSON totalmente dinâmica
| Engine |
req/s |
p99 |
| zeroserve eBPF 10 ms |
46.945 |
4,5 ms |
nginx Lua content_by_lua |
41.231 |
6,4 ms |
| zeroserve eBPF padrão 2 ms |
32.393 |
6,7 ms |
- O eBPF ajustado para intervalo de 10 ms registrou maior throughput que o
content_by_lua do nginx até mesmo em respostas totalmente sintéticas
- Ambos os motores são compilados para código nativo: o LuaJIT é um tracing JIT, e o async-ebpf compila JIT o eBPF via uBPF
- Em um cenário em que a criptografia TLS é um custo comum por requisição, o caminho ajustado com eBPF ficou à frente em throughput
- Com o padrão de 2 ms, o eBPF mantém vantagem em middleware, mas perde a liderança em respostas sintéticas; por isso, recomenda-se 10 ms para scripts de produção
Uso como reverse proxy
- O zeroserve faz proxy para um backend quando o script chama
zs_reverse_proxy("http://127.0.0.1:9000")
- O pool de conexões upstream suporta até 128 conexões por backend e reutilização em idle por 30 segundos
- Para comparação justa, no nginx foram usados explicitamente
keepalive 128, proxy_http_version 1.1 e um cabeçalho Connection vazio, já que por padrão ele fecha a conexão upstream a cada requisição
- O Caddy reutilizou conexões com seu comportamento padrão
- Cada proxy terminava TLS em um único núcleo e encaminhava para um backend compartilhado em texto puro; o backend rodava em um servidor separado de 2 núcleos sustentando seus próprios 100k req/s, para medir apenas a sobrecarga de proxy
-
Proxy de resposta pequena de 174 B
| Proxy |
req/s |
p50 |
p99 |
| zeroserve |
26.486 |
3,3 ms |
8 ms |
| nginx |
21.761 |
4,2 ms |
10,5 ms |
| Caddy |
7.683 |
10,3 ms |
33 ms |
- O proxy com
io_uring e pooling do zeroserve ficou cerca de 22% à frente do nginx e entregou cerca de 3,4x o throughput do Caddy
- Em cargas típicas de proxy, como chamadas de API, JSON pequeno e HTML de servidor de aplicação, o zeroserve faz terminação TLS e encaminhamento ao backend mais rapidamente
-
Proxy de resposta de 100 KB
| Proxy |
req/s |
Vazão |
| nginx |
5.882 |
585 MB/s |
| Caddy |
4.285 |
406 MB/s |
| zeroserve |
3.631 |
359 MB/s |
- Quando o corpo da resposta em proxy cresce, o buffering do nginx move os bytes com mais eficiência e ele assume a liderança, com o Caddy no meio e o zeroserve atrás
- Se as respostas de proxy forem grandes, o nginx é a melhor ferramenta; se forem muitas e pequenas, o zeroserve é mais rápido
Memória
- Uma única instância ociosa do zeroserve usa cerca de 15 MB de PSS, mais que os cerca de 6 MB do nginx e menos que os cerca de 60 MB do Caddy
- O importante é que a unidade de execução é o processo inteiro; ao rodar uma cópia por núcleo, o mesmo binário é mapeado e as páginas de código são compartilhadas
- Processos adicionais acrescentam pouca memória além do próprio working set
Disponibilidade
- O zeroserve é um projeto open source publicado no GitHub
1 comentários
Comentários do Hacker News
Com o desaparecimento dos benchmarks de servidores web da TechEmpower, parece que esses projetos novos têm menos chances de provar seu valor
Edit: acho que eu estava desatualizado, e pelo visto o que está em alta hoje é https://www.http-arena.com/leaderboard/. Boa sorte
Só que nunca foi algo executado com frequência, e olhando o histórico das rodadas, acontece menos de uma vez por ano
É bom ver esse tipo de tentativa surgindo porque ficou relativamente barato e rápido explorar isso graças aos LLMs
Mas o que isso me passa é que o próprio nginx é bastante impressionante. Outro ponto que chamou atenção foi a explicação de que este projeto é uma alternativa a nginx e Caddy e aposta no jeito de configurar
nginx e Caddy oferecem linguagens de configuração declarativas e, quando se chega ao limite delas, acoplam ao lado um runtime de script como Lua ou plugins do Caddy, então o funcionamento fica dividido em duas camadas
Mas acho que essa aposta está errada. Há muito tempo as pessoas preferem configuração em vez de código, e em muitos casos os recursos embutidos já bastam, sem necessidade de escrever código em C
Todo formato de arquivo de configuração parece começar simples. Até YAML era bem razoável no básico, mas as pessoas começaram a querer coisas mais complexas com âncoras e aliases
Até o GitLab tem um formato próprio com algo parecido com condicionais e variáveis, e é quase um hack que só funciona em lugares específicos. O Apache também seguiu um caminho parecido com seu formato de configuração baseado em XML
No fim, surgiram inúmeras linguagens de programação sob medida para gerenciar configuração. Em ambientes corporativos, ninguém edita isso direto; em vez disso, escrevem workflows de Ansible como scripts para fazer cirurgia remota
Se tivessem simplesmente embutido um interpretador como Lua ou Python no servidor para cuidar da configuração, daria para pular esse processo, e seria mais simples do que alterar programaticamente arquivos de configuração personalizados
Claro, dá para dizer que tentativas sob medida são mais otimizadas para usos específicos do que linguagens gerais, mas esse argumento só se encaixa no escopo estreito de exemplos de brinquedo em que esse mecanismo talvez nem fosse necessário
Lembra dos arquivos INI do Windows? Eram bons tempos, quando código era código e dados eram dados
Ou, de forma ainda mais simples, ler todos os manifestos de Ingress de um cluster Kubernetes e recriar o pack
O ponto é que a interface entre ferramenta e configuração também é só mais uma API, e os operadores de sistemas já descrevem o estado do sistema com construções de nível mais alto, enquanto os bytes concretos que formam a configuração são apenas o resultado disso
Do ponto de vista da IA, esse jeito pode ser mais fácil de lidar. Como a IA consegue trabalhar com os dois lados, pode demorar bastante até que essa mudança se firme como uma ideia claramente boa
Gostei da ideia
Mas eu me sentiria mais tranquilo se fosse possível colocar arquivos
.rsno diretório eBPF em vez de arquivos.c. Ainda mais porque já é um projeto em RustE, de alguma forma, eu esperava um servidor web com aceleração pelo kernel. Se desse para fazer isso com segurança via eBPF, seria realmente impressionante
E monothread? No Linux, fazer fork e compartilhar a fila de conexões recebidas é praticamente trivial, e em Rust isso deve dar para fazer em poucas linhas. Com SO_REUSEPORT, o resto fica por conta do kernel
Aliás, se a ideia é apostar em io_uring, acho que também tem que apostar em kTLS. Se der para evitar o processamento de SSL em espaço de usuário depois do handshake, o design fica muito mais simples
Até agora eu vinha usando nftables para esse tipo de coisa, então não precisei disso diretamente
Muito legal. Fico curioso se isso pode ser combinado com outros tipos de programas BPF, como um programa XDP ou um programa anexado a socket maps, para integrar recursos HTTP de L7 em camadas mais baixas
A ideia é boa, mas não sei se faz sentido focar em arquivos estáticos. Hoje em dia é raro subir um servidor novo para isso
Então parece algo feito para mim, embora eu reconheça que provavelmente não sou um usuário típico
Parece bom e os recursos também são razoáveis. Mas há algo que parece artificial demais, então não me convence de imediato
Não dá para saber se as métricas são falsas, se as funções utilitárias realmente funcionam, ou se houve algum trabalho sério de hardening
Consigo aceitar que tenha sido feito com vibe coding e que até o README tenha sido gerado automaticamente. Mas até o post de anúncio no blog foi feito por IA, e não tenho nenhuma base para julgar se o entendimento de qualidade de software é parecido com o meu
Que mundo estranho. Se isso tivesse sido anunciado alguns anos atrás sem avisar sobre IA, eu provavelmente teria aceitado sem desconfiar; agora, quando vejo um README bonito e parâmetros de linha de comando plausíveis, já suspeito na hora que o README alucinou e que talvez nem existam opções de verdade
Eu uso bastante ajuda de IA ao desenvolver o próprio zeroserve, mas verifico pessoalmente a saída da IA e assumo a responsabilidade por ela
Em um único core, o zeroserve serve arquivos pequenos cerca de 17% mais rápido que o nginx e com menor latência de cauda. Esse é o caso em que o zeroserve acerta: páginas HTML, JSON pequeno e CSS
Em arquivos estáticos grandes de 100KB, o zeroserve faz 8.000 req/s, 782 MB/s e p99 de 22ms; o nginx, 7.600 req/s, 773 MB/s e p99 de 28ms; e o Caddy, 6.084 req/s, 590 MB/s e p99 de 44ms
Mesmo assim, eu escolheria um projeto antigo que já foi auditado, testado em produção e endurecido, em vez de um projeto novo como este. A melhora não é grande o bastante para compensar o risco
Decidi permanecer o máximo possível na era antiga. Pessoas inteligentes publicam software, e pessoas inteligentes fazem a manutenção. Elas não precisam de IA. Esse é o meu nicho
Podemos desaparecer, mas ainda assim prefiro esse lado. Só que isso vem com a condição de que essas pessoas inteligentes escrevam documentação. Há muitas pessoas inteligentes que odeiam escrever documentação
Há muito tempo decidi que software sem documentação, por melhor que seja, não vale meu tempo. Estou falando principalmente de aplicações; quase não li documentação de Linux, embora outros digam que ela não é tão ruim assim, então não sei
É um conceito novo interessante e eu gosto dele
A verdadeira questão é o comprometimento dos desenvolvedores e a comunidade. O pessoal do Caddy e do Nginx tem dado suporte consistente aos seus produtos, e este projeto também vai exigir muito foco e atenção
Por que tarball?
Ele não extrai nada para o disco. Como o site inteiro está contido naquele único arquivo, não existe um document root que uma regra de location incorreta possa expor, e a implantação vira uma única troca atômica de arquivo
Ainda assim, essa explicação também pode ser só uma justificativa no estilo de LLM. O texto está cheio de expressões como “the right shape” e “the surface is broad”