1 puntos por GN⁺ 5 시간 전 | 1 comentarios | Compartir por WhatsApp
  • zeroserve, un servidor HTTPS pequeño y rápido, recibe un tarball de sitio web y lo sirve con HTTP/2 y TLS 1.3, ejecutando además en cada solicitud los programas eBPF incluidos en el tarball como middleware en sandbox en espacio de usuario
  • Sin archivo de configuración, un programa eBPF decide por solicitud el enrutamiento, los encabezados, la autenticación, el rate limiting y el proxy, unificando la configuración declarativa de nginx y Caddy con una capa separada de scripting
  • El sitio se indexa como un único archivo tar y no se extrae al disco; al reemplazar el tarball y enviar SIGHUP, se sustituyen de forma atómica el sitio, los scripts y los materiales TLS sin perder conexiones
  • En benchmarks HTTPS de un solo núcleo, zeroserve registró 36,681 req/s con archivos estáticos pequeños, 46,945 req/s con JSON dinámico eBPF de 10 ms y 26,486 req/s como proxy pequeño, pero en proxy de 100 KB nginx llevó ventaja con 5,882 req/s
  • zeroserve busca ser una alternativa a nginx y Caddy combinando despliegue en un solo tarball, configuración programática, eBPF en espacio de usuario y TLS moderno, aunque para respuestas proxy grandes nginx sigue siendo más adecuado

Resumen

  • zeroserve es un servidor HTTPS pequeño, rápido y sin configuración que sirve un único tarball de sitio web mediante HTTP/2 y TLS 1.3
  • Los programas eBPF incluidos dentro del tarball se ejecutan en cada solicitud como middleware en sandbox en espacio de usuario, y pueden manejar reescritura de solicitudes, autenticación, rate limiting y reverse proxy hacia backends
  • Es un servidor diseñado para superar a nginx en un solo núcleo en la mayoría de las cargas de trabajo con archivos estáticos pequeños y grandes, middleware con scripts y proxy de respuestas pequeñas
  • Los scripts eBPF se compilan JIT a código nativo y se aíslan en espacio de usuario, con el objetivo de que su costo sea lo bastante bajo como para ejecutarse en cada solicitud
  • Las operaciones de red y disco se envían mediante io_uring a través del runtime monoio
  • Soporta TLS 1.3, HTTP/2, Encrypted Client Hello, selección de certificado por SNI y fingerprinting JA4
  • Todo el sitio y el material TLS se sirven desde un solo tarball y pueden recargarse en caliente con SIGHUP

Modelo de configuración: el programa es la configuración

  • zeroserve apunta a ser una alternativa a nginx y Caddy, y su decisión central de diseño está en el modo de configuración
  • nginx y Caddy ofrecen lenguajes de configuración declarativos como bloques location, reglas rewrite, directivas map y try_files, y cuando llegan a sus límites les añaden al lado un runtime opcional de scripting como Lua o plugins de Caddy
  • En esa estructura, el comportamiento se divide entre una capa de directivas con su propio flujo de control y una capa de scripts que se ejecuta en puntos específicos del ciclo de vida de la solicitud
  • zeroserve no tiene archivo de configuración: un solo programa eBPF ve todas las solicitudes y decide el enrutamiento, los encabezados, la autenticación, el rate limiting y el proxy

Servir directamente un solo tarball

  • Todo el sitio es un único archivo tar, y al cargarlo zeroserve construye un mapa path -> byte-range y sirve archivos leyendo rangos de bytes directamente del propio tarball
  • Como no se extrae ningún archivo al disco, el sitio existe solo dentro de un único archivo y no hay document root que una regla location mal hecha pueda exponer
  • El despliegue consiste en el reemplazo atómico de un solo archivo, y publicar una nueva versión implica sustituir el tarball y luego enviar SIGHUP
  • El empaquetado del directorio y el comando de ejecución tienen esta forma
zeroserve --pack ./public > site.tar  
zeroserve --addr 0.0.0.0:8080 site.tar  
Publicidad
  • El comando de recarga en caliente tiene esta forma
killall -SIGHUP zeroserve  
  • La recarga reemplaza de forma atómica el sitio, los scripts y el material TLS dentro del mismo proceso, y funciona sin perder conexiones
  • Cada instancia es un event loop de un solo hilo; esto es una limitación por proceso, pero se presenta como una forma adecuada cuando la unidad de escalado es “más procesos”

Scripting eBPF en espacio de usuario

  • Todos los archivos .c colocados bajo .zeroserve/scripts/ se compilan a objetos eBPF con clang y llc al momento del empaquetado, y se ejecutan en cada solicitud
  • eBPF no corre sobre el subsistema BPF del kernel ni requiere CAP_BPF, sino que se ejecuta en espacio de usuario dentro del runtime async-ebpf en un proceso normal sin privilegios
  • async-ebpf integra uBPF y compila JIT el bytecode a código máquina nativo x86-64
  • El pointer cage enmascara todos los accesos a memoria del código compilado JIT dentro de una arena dedicada al programa, confinando los accesos inválidos dentro de la memoria del script
  • Los scripts se ejecutan directamente en el event loop único de zeroserve, y para evitar que un script lento detenga otras conexiones, un temporizador puede interrumpir el código nativo compilado JIT en plena ejecución y devolver el control al event loop
  • El modelo de programación es una cadena de scripts ejecutados según el orden alfabético de los nombres de archivo, y los scripts comparten un mapa de metadatos por solicitud
  • Si un script llama a zs_respond o zs_reverse_proxy, la cadena se corta de inmediato
  • Las claves bajo zs.response.header.* se convierten en encabezados de todas las respuestas, y otras claves se usan en una pequeña pasada de plantillas que reemplaza placeholders como <zs-meta>visitor</zs-meta> en archivos HTML al momento de salida
  • La superficie de helpers permite leer método, ruta, query, encabezados y dirección del peer de la solicitud, así como reescribir URI y establecer o eliminar encabezados
  • Los helpers criptográficos y de codificación ofrecen SHA-256, HMAC-SHA256, base64, hex y getrandom
  • Los helpers JSON permiten parsear el cuerpo de la solicitud, crear y modificar árboles de documentos y responder con zs_json_respond
  • El rate limiting soporta token buckets basados en claves arbitrarias como IP del peer o API key, y el estado se conserva incluso después de una recarga en caliente
  • Los helpers de AWS SigV4 soportan encabezados Authorization firmados y URLs presignadas para comunicarse con S3 y otros servicios de AWS
  • El inicio de sesión OIDC ofrece un flujo de relying party basado en Authorization Code + PKCE, y guarda toda la sesión en cookies sealed con XChaCha20-Poly1305 para mantener el servidor stateless mientras se pone un sitio estático detrás de “Iniciar sesión con Google”
  • Los endpoints dinámicos responden directamente desde scripts en rutas específicas; en el ejemplo, una solicitud a /health devuelve el encabezado application/json y el cuerpo {"status":"ok"}
  • Cada script se ejecuta con un límite predeterminado de 256 KB de memoria, y el runtime reparte el tiempo de ejecución de scripts largos y limita los que se descontrolan
  • Los scripts pueden llamarse entre sí con zs_call, y la profundidad de llamada está limitada
  • Un script atrapado en un bucle infinito solo retrasa su propia solicitud; el temporizador preventivo lo interrumpe para que el servidor siga atendiendo otras solicitudes
  • La capa TLS es exclusivamente TLS 1.3 y termina con BoringSSL
  • Encrypted Client Hello evita que el SNI real aparezca en texto plano, y además ofrece selección de certificados SNI basada en directorios y fingerprinting de cliente JA4 expuesto a los scripts
  • El modo de relay ECH transparente pasa byte por byte al upstream real cualquier handshake que no pueda descifrarse, permitiendo que nombres protegidos se mezclen detrás de un nombre público

Rendimiento

  • Condiciones del benchmark

    • Se comparó HTTPS sirviendo el mismo contenido y el mismo certificado autofirmado entre zeroserve, nginx 1.26 y Caddy 2.11 en un Ryzen 7 3700X de 8 núcleos
    • Como una instancia de zeroserve es monohilo por diseño, el criterio de comparación es el rendimiento por núcleo
    • Todos los servidores se fijaron a una sola CPU con taskset; nginx usó worker_processes 1, Caddy usó GOMAXPROCS=1 y zeroserve mantuvo su estructura monohilo existente
    • La carga se generó desde otros núcleos con wrk -t4 -c100, usando la mediana de 3 ejecuciones de 10 segundos
    • wrk usa HTTP/1.1, por lo que las cifras reflejan HTTP/1.1 sobre TLS 1.3, es decir, el costo en estado estable de conexiones HTTPS ya abiertas con conexiones keep-alive largas que amortizan el costo del handshake
  • Archivo estático pequeño de 174 B

    Servidor req/s p99
    zeroserve 36,681 5.4 ms
    nginx 31,226 7.8 ms
    Caddy 12,830 22 ms
    • zeroserve sirvió archivos pequeños cerca de un 17% más rápido que nginx en un solo núcleo, y también con menor latencia de cola
    • Casos base de sitios estáticos como páginas HTML, JSON pequeños y CSS forman parte del objetivo de tuning de zeroserve
    Publicidad
  • Archivo estático grande de 100 KB

    Servidor req/s Throughput 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
    • Los resultados de los tres servidores fueron cercanos, con zeroserve apenas al frente en un solo núcleo con unos 780 MB/s
    • La ventaja de nginx con archivos grandes mediante sendfile() no se usa bajo TLS, ya que los bytes deben cifrarse en espacio de usuario, así que los tres servidores quedan atados al cifrado y al loop de escritura
    • Con kernel TLS desactivado en los tres, la ruta de lectura y escritura con io_uring de zeroserve fue ligeramente más rápida

eBPF vs Lua

  • La comparación de scripting se hizo contra nginx + LuaJIT ngx_http_lua_module, una forma común de ejecutar código rápido dentro de un servidor web
  • zeroserve configura por defecto el temporizador preventivo de scripts cada 2 ms; un intervalo fino acelera la limitación de scripts problemáticos, pero también añade costo a los scripts normales
  • Con el valor predeterminado de 2 ms, eBPF queda en unos 32k req/s en respuestas totalmente dinámicas, por debajo de los 41k req/s de nginx Lua
  • Al subir --preempt-timer-interval-ms a 10, el throughput de scripting se recupera cerca de un 40% y el resultado se invierte
  • Middleware de inyección de encabezados por solicitud

    Motor req/s p99
    zeroserve eBPF 10 ms 43,709 5.1 ms
    zeroserve eBPF 2 ms por defecto 31,334 6.7 ms
    nginx Lua header_filter 28,653 8.4 ms
    • En este caso de middleware, donde el script se ejecuta pero el archivo estático se sigue sirviendo, eBPF a 10 ms supera a nginx Lua por cerca de un 50% y con menor latencia de cola
  • Respuesta JSON totalmente dinámica

    Motor req/s p99
    zeroserve eBPF 10 ms 46,945 4.5 ms
    nginx Lua content_by_lua 41,231 6.4 ms
    zeroserve eBPF 2 ms por defecto 32,393 6.7 ms
    • Con el ajuste de 10 ms, eBPF supera en throughput al content_by_lua de nginx incluso en respuestas completamente sintéticas
    • Ambos motores se compilan a código nativo; LuaJIT usa tracing JIT y async-ebpf compila JIT eBPF mediante uBPF
    • En un escenario donde el cifrado TLS es el costo común por solicitud, la ruta eBPF ajustada queda por delante en throughput
    • Con el valor predeterminado de 2 ms, eBPF mantiene ventaja en middleware pero pierde el liderazgo en respuestas sintéticas, por lo que se recomienda usar 10 ms en scripts de producción
Publicidad

Uso como reverse proxy

  • zeroserve hace proxy hacia un backend cuando el script llama a zs_reverse_proxy("http://127.0.0.1:9000";)
  • El pool de conexiones upstream soporta hasta 128 conexiones por backend y 30 segundos de reutilización en idle
  • Para una comparación justa, nginx usó explícitamente keepalive 128, proxy_http_version 1.1 y un encabezado Connection vacío, considerando que por defecto tiende a cerrar la conexión upstream en cada solicitud
  • Caddy reutilizó conexiones con su comportamiento por defecto
  • Cada proxy terminaba TLS en un solo núcleo y reenviaba a un backend compartido en texto plano; el backend corría en un servidor aparte de 2 núcleos sosteniendo sus propios 100k req/s, de modo que se midiera solo el overhead del proxy
  • Proxy de respuesta pequeña 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
    • El proxy con io_uring y pooling de zeroserve superó a nginx por cerca de un 22% y registró unas 3.4 veces más throughput que Caddy
    • En cargas típicas de proxy como llamadas API, JSON pequeños o HTML desde un app server, zeroserve termina TLS y reenvía al backend más rápido
  • Proxy de respuesta de 100 KB

    Proxy req/s Throughput
    nginx 5,882 585 MB/s
    Caddy 4,285 406 MB/s
    zeroserve 3,631 359 MB/s
    • Cuando el cuerpo de la respuesta proxy crece, el buffering de nginx mueve los bytes con mayor eficiencia y toma la delantera, seguido por Caddy y luego zeroserve
    • Si las respuestas proxy son grandes, nginx es una mejor herramienta; si son muchas y pequeñas, zeroserve es más rápido

Memoria

  • Una sola instancia de zeroserve en idle usa cerca de 15 MB de PSS, más que los ~6 MB de nginx pero menos que los ~60 MB de Caddy
  • Es importante que la unidad de ejecución sea el proceso completo; al correr una copia por núcleo, se mapea el mismo binario y se comparten las páginas de código
  • Los procesos adicionales agregan poca memoria más allá de su propio working set

Publicación

  • zeroserve es un proyecto open source publicado en GitHub

1 comentarios

 
GN⁺ 5 시간 전
Comentarios de Hacker News
  • Con la desaparición de los benchmarks de servidores web de TechEmpower, parece que estos proyectos nuevos tienen menos oportunidades de demostrar su valor
    Edición: creo que estaba desactualizado, y ahora lo que está sonando es https://www.http-arena.com/leaderboard/. Suerte

    • No entiendo qué significa que haya muerto. Sigue ahí en https://www.techempower.com/benchmarks/#section=data-r23 y el último benchmark fue en febrero de 2025
      Aunque tampoco es que antes se ejecutara tan seguido; viendo el historial de rondas, se corre menos de una vez al año
    • La UI/UX de los LLM es malísima. No entiendo por qué no le dedican uno o dos días de fin de semana a mejorar bastante la experiencia de usuario en cosas así
  • Me gusta ver que intentos como este se puedan explorar de forma relativamente barata y rápida gracias a los LLM
    Pero lo que me deja esto es que nginx en sí mismo es bastante impresionante. Otra parte que me llamó la atención fue la explicación de que este proyecto es una alternativa a nginx y Caddy, y que apuesta por el modo de configuración
    nginx y Caddy ofrecen lenguajes de configuración declarativos, y cuando llegas a sus límites, la estructura es adjuntar al lado un runtime de scripting como Lua o plugins de Caddy, así que el comportamiento queda dividido en dos capas
    Pero creo que esa apuesta es equivocada. Desde hace mucho la gente prefiere configuración en vez de código, y muchas veces las funciones integradas bastan, así que no hace falta escribir código C

    • No es tan fácil estar tan seguro
      Todos los formatos de archivo de configuración parecen empezar simples. Incluso YAML era bastante razonable en lo básico, pero la gente empezó a querer cosas más complejas con anchors y aliases
      Hasta GitLab tiene su propio formato con algo parecido a condicionales y variables, y es casi un hack que solo funciona en ciertos lugares. Apache también siguió un camino parecido con su formato de configuración basado en XML
      Al final terminan apareciendo muchos lenguajes de programación a medida para gestionar la configuración. En entornos corporativos, ni siquiera se edita directamente, sino que se escriben workflows de Ansible como scripts para hacer cirugía remota
      Si simplemente se hubiera integrado en el servidor un intérprete como Lua o Python para manejar la configuración, nos habríamos saltado todo ese proceso, y habría sido más simple que andar modificando archivos de configuración a medida con programas
      Claro, se puede decir que esos intentos a medida están más optimizados para su uso específico que un lenguaje general, pero ese argumento solo encaja dentro del alcance estrecho de ejemplos de juguete donde ese mecanismo ni siquiera habría hecho falta desde el principio
      ¿Se acuerdan de los archivos INI de Windows? Eran buenos tiempos, cuando el código era código y los datos eran datos
    • Apuesto a que en las próximas 96 horas alguien va a crear con un LLM una herramienta que convierta y empaquete archivos de configuración de nginx o Caddy como código utilizable por zeroserve
      Más simple todavía: podría leer todos los manifiestos de Ingress de un clúster de Kubernetes y volver a generar el pack
      El punto es que la interfaz entre herramientas y configuración también es solo otra API, y los operadores del sistema ya describen el estado del sistema con construcciones de nivel más alto; los bytes concretos que forman la configuración son solo el resultado
    • Me pregunto si no convendría abstraer la complejidad y lograr una configuración tipo “archivo de configuración” mediante macros
    • Vale la pena observar si esta preferencia cambia a medida que la IA haga cada vez más posible lenguaje humano → efecto en máquina
      Desde la perspectiva de la IA, ese enfoque podría ser más fácil de manejar. Como la IA puede trabajar con ambos lados, puede que pase bastante tiempo antes de que ese cambio se consolide claramente como una buena idea
    • No entiendo por qué quieren darle tanto crédito a los LLM. Que haya ayudado un LLM a redactar el texto no significa que también haya hecho el experimento
  • La idea me gusta
    Pero me daría más confianza si en el directorio eBPF se pudieran poner archivos .rs en lugar de .c. Ya es un proyecto en Rust, además
    Y por alguna razón yo esperaba un servidor web acelerado por kernel. Si eso se pudiera hacer de forma segura con eBPF, sería realmente impresionante
    ¿Y además monohilo? En Linux, hacer fork y compartir la cola de conexiones entrantes es casi trivial, y en Rust deberían ser unas pocas líneas. Con SO_REUSEPORT, el kernel se encarga del resto
    Por cierto, si la idea es empujar io_uring, yo diría que también hay que empujar kTLS. Si se puede evitar el procesamiento SSL en espacio de usuario después del handshake, el diseño se simplifica muchísimo

    • Gracias. Planeo implementar fork + SO_REUSEPORT
      Hasta ahora había estado usando nftables para este tipo de cosas, así que no lo había necesitado directamente
  • Muy genial. Me pregunto si esto se podría combinar con otros tipos de programas BPF, como un programa XDP o programas conectados a mapas de sockets, para integrar funciones HTTP de L7 en capas más bajas

  • La idea está bien, pero no sé si convenga enfocarse en archivos estáticos. Hoy en día rara vez se levanta un servidor nuevo para ese propósito

    • La semana pasada hice justo eso al convertir Ghost a estático, y medio que estaba pensando si no sería más rápido un binario autocontenido
      Así que esto se siente como hecho para mí, aunque admito que probablemente no soy un usuario típico
    • Depende del dominio. En varias áreas científicas se distribuyen grandes datasets de forma eficiente en formatos de archivo estáticos. Por ejemplo, cosas como https://zarr.dev/ o https://parquet.apache.org/
  • Se ve bien y las funciones también parecen decentes. Pero se siente demasiado artificial en algún sentido, así que no termina de convencerme.
    No hay forma de saber si las métricas son falsas, si las funciones auxiliares realmente funcionan o si se hizo un trabajo serio de hardening.
    Puedo aceptar que lo hayan hecho con vibe coding y que hasta el README se haya generado automáticamente. Pero incluso la entrada del blog de lanzamiento fue hecha por IA, y no tengo ninguna base para juzgar si su idea de calidad de software se parece a la mía.
    Qué mundo tan raro. Hace unos años, si esto se hubiera anunciado sin avisar del uso de IA, lo habría aceptado sin sospechar. Ahora, en cuanto veo un README elegante y parámetros de línea de comandos que suenan plausibles, de inmediato sospecho que el README alucinó y que en realidad quizá ni existan esas opciones.

    • Soy el autor. Algunas partes clave de este proyecto, por ejemplo async-ebpf, se escribieron mucho antes de que existieran esos agentes de programación.
      Cuando construyo zeroserve sí uso bastante ayuda de IA, pero verifico personalmente la salida de la IA y yo asumo la responsabilidad.
    • Si miras los benchmarks, con un archivo estático pequeño de 174 B, zeroserve logra 36,681 req/s y p99 de 5.4 ms, nginx 31,226 req/s y p99 de 7.8 ms, y Caddy 12,830 req/s y p99 de 22 ms.
      En un solo núcleo, zeroserve sirve archivos pequeños alrededor de un 17% más rápido que nginx y con menor latencia de cola. Ese es el caso donde zeroserve acierta: páginas HTML, JSON pequeño y CSS.
      Con un archivo estático grande de 100 KB, zeroserve da 8,000 req/s, 782 MB/s y p99 de 22 ms; nginx 7,600 req/s, 773 MB/s y p99 de 28 ms; y Caddy 6,084 req/s, 590 MB/s y p99 de 44 ms.
      Aun así, yo elegiría un proyecto antiguo auditado, probado en producción y endurecido, antes que un proyecto nuevo como este. La mejora no es tan grande como para justificar el riesgo.
    • Es una situación realmente triste. Hace poco estuvo el proyecto ffmpeg-wasm; lo probé y sí funcionaba. Pero era IA de vibe coding, y yo no soporto la IA. Aunque funcione, me da igual.
      Decidí quedarme en la era antigua tanto como sea posible. Gente inteligente publica software y gente inteligente lo mantiene. Ellos no necesitan IA. Ese es mi nicho.
      Puede que desaparezcamos, pero aun así prefiero eso. Claro, con la condición de que esa gente inteligente escriba documentación. También hay mucha gente inteligente que odia escribirla.
      Hace mucho decidí que el software sin documentación, por excelente que sea, no vale mi tiempo. Hablo más que nada de aplicaciones; casi nunca miré la documentación de Linux, aunque otros dicen que no está tan mal, así que no sé.
  • Es un concepto nuevo interesante y me gusta.
    La verdadera pregunta es el nivel de compromiso del desarrollador y la comunidad. La gente de Caddy y Nginx ha dado soporte constante a sus productos, y este proyecto también va a requerir mucha concentración y atención.

  • ¿Por qué tarball?

    • Es un formato simple que facilita acceder a recursos por rangos de bytes, todo el mundo tiene herramientas para manejarlo y, sobre todo, porque no comprime.
    • Según el primer párrafo de la sección “One tarball, served in place”, todo el sitio es un solo archivo tar, y zeroserve lo indexa al cargarlo para crear un mapa de rangos de bytes a partir de las rutas, y luego sirve los archivos haciendo lecturas por rango de bytes directamente sobre el propio tarball.
      No extrae nada al disco. Como el sitio completo está contenido en ese único archivo, no hay un document root que una regla de location mal hecha pueda exponer, y el despliegue se vuelve un único reemplazo atómico de archivo.
      Aunque esa explicación también podría ser una justificación al estilo LLM. Por todo el texto aparecen expresiones como “the right shape” o “the surface is broad”.