1 pontos por GN⁺ 5 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • 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  
Publicidade
  • 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
    Publicidade
  • 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
Publicidade

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

 
GN⁺ 5 시간 전
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

    • Não sei o que significa dizer que morreu. Ainda está em https://www.techempower.com/benchmarks/#section=data-r23, e o benchmark mais recente é de fevereiro de 2025
      Só que nunca foi algo executado com frequência, e olhando o histórico das rodadas, acontece menos de uma vez por ano
    • A UI/UX de LLM é muito ruim. Não entendo por que não melhoram isso, quando daria para melhorar bastante a experiência do usuário com só um ou dois dias de fim de semana
  • É 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

    • Não é tão fácil ter essa certeza
      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
    • Aposto que, nas próximas 96 horas, alguém consegue fazer com LLM uma ferramenta empacotada de conversão que transforme arquivos de configuração do nginx ou do Caddy em código que o zeroserve consiga usar
      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
    • Talvez valha abstrair a complexidade e alcançar uma configuração em estilo de "arquivo de configuração" com macros
    • Vale observar se essa preferência muda à medida que a IA torna cada vez mais viável o caminho de fala humana → efeito na máquina
      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
    • Não sei por que querem tanto dar crédito aos LLMs. Só porque usaram ajuda de LLM para escrever o post não quer dizer que os experimentos também tenham sido feitos pelo LLM
  • Gostei da ideia
    Mas eu me sentiria mais tranquilo se fosse possível colocar arquivos .rs no diretório eBPF em vez de arquivos .c. Ainda mais porque já é um projeto em Rust
    E, 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

    • Obrigado. Vou implementar fork + SO_REUSEPORT
      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

    • Na semana passada fiz exatamente isso ao converter o Ghost para estático, e fiquei meio pensando se um único binário autocontido não seria mais rápido
      Então parece algo feito para mim, embora eu reconheça que provavelmente não sou um usuário típico
    • Depende do domínio. Em várias áreas científicas, grandes conjuntos de dados são distribuídos de forma eficiente em formatos de arquivo estáticos. Por exemplo, coisas como https://zarr.dev/ e https://parquet.apache.org/
  • 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

    • Sou o autor. Algumas partes centrais deste projeto, como async-ebpf, foram escritas muito antes de esses agentes de programação aparecerem
      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
    • Pelos benchmarks, com um arquivo estático pequeno de 174B, o zeroserve faz 36.681 req/s com p99 de 5,4ms, o nginx 31.226 req/s com p99 de 7,8ms, e o Caddy 12.830 req/s com p99 de 22ms
      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
    • É uma situação realmente lamentável. Recentemente houve o projeto ffmpeg-wasm, e quando testei ele funcionava. Mas era IA de vibe coding, e eu não suporto IA. Mesmo funcionando, para mim dá no mesmo
      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?

    • É um formato simples que facilita acessar recursos por intervalo de bytes, todo mundo tem ferramentas para lidar com ele e, acima de tudo, não faz compressão
    • Segundo o primeiro parágrafo da seção “One tarball, served in place”, o site inteiro é um único arquivo tar, e o zeroserve o indexa no carregamento para criar um mapa de intervalos de bytes a partir dos caminhos; depois, serve os arquivos fazendo leituras por intervalo de bytes diretamente no próprio 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”