1 points par GN⁺ 5 시간 전 | 1 commentaires | Partager sur WhatsApp
  • zeroserve, un serveur HTTPS petit et rapide, prend une archive tar d’un site web et la sert via HTTP/2 et TLS 1.3, tout en exécutant à chaque requête les programmes eBPF contenus dans l’archive comme middleware sandboxé en espace utilisateur
  • Sans fichier de configuration, un programme eBPF décide du routage par requête, des en-têtes, de l’authentification, de la limitation de débit et du proxy, fusionnant en une seule couche la configuration déclarative et la couche de script séparée de nginx et Caddy
  • Le site est indexé comme un unique fichier tar et n’est jamais extrait sur disque ; remplacer l’archive puis envoyer SIGHUP permet de remplacer de façon atomique le site, les scripts et les données TLS sans perte de connexion
  • Dans des benchmarks HTTPS sur un seul cœur, zeroserve a atteint 36 681 req/s sur de petits fichiers statiques, 46 945 req/s sur du JSON dynamique eBPF à 10 ms et 26 486 req/s en petit proxy, mais pour un proxy 100KB, nginx reste devant avec 5 882 req/s
  • zeroserve vise à être une alternative à nginx et Caddy en combinant déploiement en archive tar unique, configuration programmable, eBPF en espace utilisateur et TLS moderne, mais nginx reste mieux adapté aux grosses réponses proxy

Vue d’ensemble

  • zeroserve est un serveur HTTPS petit, rapide et sans configuration, qui sert une archive tar de site web via HTTP/2 et TLS 1.3
  • Les programmes eBPF placés dans l’archive sont exécutés sur chaque requête comme middleware sandboxé en espace utilisateur, et peuvent gérer la réécriture des requêtes, l’authentification, la limitation de débit et le reverse proxy vers un backend
  • Le projet vise un serveur capable de dépasser nginx, sur un seul cœur, dans la plupart des charges de travail incluant petits et gros fichiers statiques, middleware scripté et proxy à petites réponses
  • Les scripts eBPF sont compilés en code natif via JIT, isolés en espace utilisateur et conçus pour avoir un coût suffisamment faible pour s’exécuter à chaque requête
  • Les opérations réseau et disque sont soumises via io_uring à travers le runtime monoio
  • Prise en charge de TLS 1.3, HTTP/2, Encrypted Client Hello, sélection de certificat SNI et empreinte JA4
  • L’ensemble du site et les données TLS sont servis depuis une seule archive tar et peuvent être rechargés à chaud via SIGHUP

Modèle de configuration : le programme fait office de configuration

  • zeroserve se positionne comme une alternative à nginx et Caddy, et son choix de conception central concerne la manière de configurer le serveur
  • nginx et Caddy proposent des langages de configuration déclaratifs avec des blocs location, des règles rewrite, des directives map, try_files, puis ajoutent à côté un runtime de script optionnel comme Lua ou des plugins Caddy lorsqu’on atteint leurs limites
  • Dans cette architecture, le comportement est séparé entre une couche de directives avec son propre flux de contrôle et une couche de scripts exécutée à des moments précis du cycle de vie de la requête
  • zeroserve n’a aucun fichier de configuration : un unique programme eBPF voit toutes les requêtes et décide du routage, des en-têtes, de l’authentification, de la limitation de débit et du proxy

Servir directement une archive tar unique

  • Le site entier est un unique fichier tar, et zeroserve construit au chargement une table path -> byte-range, puis sert les fichiers en lisant directement les plages d’octets dans l’archive elle-même
  • Aucun fichier n’est extrait sur disque ; le site n’existe donc qu’à l’intérieur d’un seul fichier, sans racine documentaire qu’une mauvaise règle location pourrait exposer
  • Le déploiement consiste en un remplacement atomique d’un seul fichier : on remplace l’archive tar, puis on envoie SIGHUP
  • Le packaging du répertoire et la commande d’exécution prennent la forme suivante
zeroserve --pack ./public > site.tar  
zeroserve --addr 0.0.0.0:8080 site.tar  
Publicité
  • La commande de hot reload prend la forme suivante
killall -SIGHUP zeroserve  
  • Le rechargement remplace de manière atomique, dans le même processus, le site, les scripts et les données TLS, sans perte de connexion
  • Chaque instance fonctionne comme une boucle d’événements monothread ; c’est une limite à l’échelle d’un processus, mais cohérente si l’unité de montée en charge est « plus de processus »

Scripting eBPF en espace utilisateur

  • Tous les fichiers .c placés sous .zeroserve/scripts/ sont compilés au moment du packaging en objets eBPF via clang et llc, puis exécutés sur chaque requête
  • eBPF s’exécute en espace utilisateur dans le runtime async-ebpf, au sein d’un processus non privilégié ordinaire, sans sous-système BPF du noyau ni CAP_BPF
  • async-ebpf embarque uBPF et compile le bytecode en code machine x86-64 natif via JIT
  • La pointer cage masque tous les accès mémoire du code compilé par JIT vers une arène dédiée au programme, afin de confiner tout accès invalide à la mémoire du script lui-même
  • Les scripts s’exécutent directement dans l’unique boucle d’événements de zeroserve, et pour éviter qu’un script lent ne bloque les autres connexions, un timer peut interrompre l’exécution du code natif compilé par JIT et rendre le contrôle à la boucle d’événements
  • Le modèle de programmation repose sur une chaîne de scripts exécutés dans l’ordre de tri des noms de fichiers, les scripts partageant une map de métadonnées par requête
  • Si un script appelle zs_respond ou zs_reverse_proxy, la chaîne s’arrête immédiatement
  • Les clés sous zs.response.header.* deviennent des en-têtes de toutes les réponses, tandis que les autres clés servent à une petite passe de templating qui remplace à l’émission des placeholders comme <zs-meta>visitor</zs-meta> dans les fichiers HTML
  • La surface des helpers permet de lire la méthode, le chemin, la query, les en-têtes et l’adresse du pair, ainsi que de réécrire l’URI, définir ou supprimer des en-têtes
  • Les helpers de chiffrement et d’encodage fournissent SHA-256, HMAC-SHA256, base64, hex et getrandom
  • Les helpers JSON prennent en charge le parsing du corps de requête, la création et la modification d’arbres de documents, ainsi que des réponses via zs_json_respond
  • La limitation de débit prend en charge des token buckets basés sur des clés arbitraires comme l’IP du pair ou une clé API, et l’état est conservé même après un hot reload
  • Les helpers AWS SigV4 prennent en charge les en-têtes Authorization signés et les URL présignées pour communiquer avec S3 et d’autres services AWS
  • La connexion OIDC fournit un flux relying party basé sur Authorization Code + PKCE, et place l’intégralité de la session de connexion dans un cookie sealed XChaCha20-Poly1305 afin de garder le serveur sans état tout en protégeant un site statique derrière un « Se connecter avec Google »
  • Les endpoints dynamiques fonctionnent en laissant le script répondre directement sur certains chemins ; dans l’exemple, une requête sur /health renvoie un en-tête application/json et un corps {"status":"ok"}
  • Chaque script s’exécute avec une limite mémoire par défaut de 256KB, et le runtime répartit dans le temps les scripts trop longs et bride ceux qui s’emballent
  • Les scripts peuvent s’appeler entre eux via zs_call, avec une profondeur d’appel limitée
  • Un script bloqué dans une boucle infinie ne retarde que sa propre requête ; le timer préemptif l’interrompt afin que le serveur continue à traiter les autres requêtes
  • La couche TLS est limitée à TLS 1.3 et terminée par BoringSSL
  • Encrypted Client Hello évite que le vrai SNI apparaisse en clair, et le serveur fournit une sélection de certificats SNI basée sur des répertoires ainsi qu’une empreinte client JA4 exposée aux scripts
  • Le mode de relais ECH transparent transmet octet par octet les handshakes qu’il ne peut pas déchiffrer vers le véritable upstream, afin que les noms protégés puissent être mélangés derrière un nom public

Performances

  • Conditions de benchmark

    • Comparaison de zeroserve, nginx 1.26 et Caddy 2.11 pour servir en HTTPS le même contenu avec le même certificat auto-signé sur un Ryzen 7 3700X à 8 cœurs
    • Comme une instance zeroserve est monothread par conception, la comparaison se fait sur les performances par cœur
    • Tous les serveurs étaient épinglés à un CPU via taskset ; nginx utilisait worker_processes 1, Caddy GOMAXPROCS=1, et zeroserve sa structure monothread existante
    • La charge était générée depuis d’autres cœurs avec wrk -t4 -c100, et la médiane de trois exécutions de 10 secondes a été retenue
    • wrk utilise HTTP/1.1 : les chiffres correspondent donc à HTTP/1.1 sur TLS 1.3, soit le coût en régime établi de connexions HTTPS déjà ouvertes, le coût du handshake étant amorti par de longues connexions keep-alive
  • Petit fichier statique 174B

    Serveur req/s p99
    zeroserve 36 681 5,4 ms
    nginx 31 226 7,8 ms
    Caddy 12 830 22 ms
    • zeroserve a servi les petits fichiers environ 17 % plus vite que nginx sur un seul cœur, avec une latence de queue plus faible
    • Les cas de base des sites statiques, comme les pages HTML, petits JSON ou feuilles CSS, sont la cible d’optimisation de zeroserve
    Publicité
  • Gros fichier statique 100KB

    Serveur req/s Débit 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
    • Les résultats des trois serveurs étaient proches, zeroserve passant légèrement devant avec environ 780 MB/s sur un seul cœur
    • L’atout de nginx sur les gros fichiers, sendfile(), n’est pas utilisé sous TLS, car les octets doivent être chiffrés en espace utilisateur ; les trois serveurs sont donc tous limités par la boucle de chiffrement et d’écriture
    • Avec kernel TLS désactivé sur les trois serveurs, le chemin de lecture/écriture io_uring de zeroserve s’est montré légèrement plus rapide

eBPF vs Lua

  • La comparaison côté scripting se fait avec nginx + LuaJIT ngx_http_lua_module, une manière courante d’exécuter du code rapide dans un serveur web
  • Par défaut, zeroserve configure le timer de préemption des scripts à 2 ms ; un intervalle fin permet de brider rapidement les scripts problématiques, mais ajoute aussi un coût aux scripts normaux
  • Avec la valeur par défaut de 2 ms, eBPF atteint environ 32k req/s sur des réponses entièrement dynamiques, contre 41k req/s pour Lua sur nginx
  • En augmentant --preempt-timer-interval-ms à 10, le débit du scripting récupère environ 40 % et la hiérarchie s’inverse
  • Middleware d’injection d’en-têtes par requête

    Moteur req/s p99
    zeroserve eBPF 10ms 43 709 5,1 ms
    zeroserve eBPF 2ms défaut 31 334 6,7 ms
    nginx Lua header_filter 28 653 8,4 ms
    • Dans ce cas de middleware où le script s’exécute mais où les fichiers statiques continuent d’être servis, eBPF à 10 ms dépasse nginx Lua d’environ 50 % avec une latence de queue plus faible
  • Réponse JSON entièrement dynamique

    Moteur req/s p99
    zeroserve eBPF 10ms 46 945 4,5 ms
    nginx Lua content_by_lua 41 231 6,4 ms
    zeroserve eBPF 2ms défaut 32 393 6,7 ms
    • Avec un intervalle réglé à 10 ms, eBPF dépasse nginx content_by_lua en débit même sur des réponses entièrement synthétiques
    • Les deux moteurs sont compilés en code natif : LuaJIT est un tracing JIT, tandis qu’async-ebpf compile eBPF via uBPF
    • Dans un contexte où le chiffrement TLS représente un coût commun par requête, le chemin eBPF réglé gagne sur le débit
    • Avec la valeur par défaut de 2 ms, eBPF conserve son avantage en middleware mais cède la tête sur les réponses synthétiques ; l’usage de 10 ms est donc recommandé pour les scripts en production
Publicité

Utilisation comme reverse proxy

  • zeroserve effectue du proxy vers un backend lorsque le script appelle zs_reverse_proxy("http://127.0.0.1:9000";)
  • Le pool de connexions upstream prend en charge jusqu’à 128 connexions par backend avec réutilisation après 30 secondes d’inactivité
  • Pour une comparaison équitable, nginx a explicitement utilisé keepalive 128, proxy_http_version 1.1 et un en-tête Connection vide, car par défaut il ferme la connexion upstream à chaque requête
  • Caddy réutilise les connexions selon son comportement par défaut
  • Chaque proxy terminait TLS sur un seul cœur puis relayait vers un backend partagé en clair, le backend étant un serveur séparé à 2 cœurs capable de soutenir 100k req/s, afin de ne mesurer que le surcoût du proxy
  • Proxy de petite réponse 174B

    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
    • Le proxy io_uring avec pooling de zeroserve a dépassé nginx d’environ 22 % et affiché un débit environ 3,4 fois supérieur à celui de Caddy
    • Sur les charges proxy courantes comme les appels API, petits JSON ou HTML d’un serveur d’application, zeroserve termine TLS et relaie vers le backend plus rapidement
  • Proxy de réponse 100KB

    Proxy req/s Débit
    nginx 5 882 585 MB/s
    Caddy 4 285 406 MB/s
    zeroserve 3 631 359 MB/s
    • Quand les corps de réponse proxy grossissent, le buffering de nginx déplace les octets plus efficacement et prend l’avantage, Caddy restant au milieu et zeroserve derrière
    • Pour de grosses réponses proxy, nginx est le meilleur choix ; pour des réponses petites et nombreuses, zeroserve est plus rapide

Mémoire

  • Une instance zeroserve inactive utilise environ 15MB de PSS, soit plus que les ~6MB de nginx mais moins que les ~60MB de Caddy
  • Il est important de noter que l’unité d’exécution est le processus entier ; lorsqu’on lance une copie par cœur, le même binaire est mappé et les pages de code sont partagées
  • Les processus supplémentaires n’ajoutent donc que peu de mémoire en dehors de leur propre working set

Publication

  • zeroserve est un projet open source publié sur GitHub

1 commentaires

 
GN⁺ 5 시간 전
Avis sur Hacker News
  • Avec la disparition des benchmarks de serveurs web TechEmpower, on a l’impression que ces nouveaux projets ont moins d’occasions de faire leurs preuves
    Édit : j’ai l’air d’être en retard, et il semble que la référence du moment soit https://www.http-arena.com/leaderboard/. Bonne chance

    • Je ne vois pas ce que veut dire « disparu ». C’est toujours là : https://www.techempower.com/benchmarks/#section=data-r23, et le dernier benchmark date de février 2025
      Cela dit, ils n’ont jamais tourné ça très souvent, et si on regarde l’historique des rounds, c’est exécuté moins d’une fois par an
    • L’UI/UX des LLM est vraiment mauvaise. Il suffirait de passer un ou deux jours de week-end là-dessus pour nettement améliorer l’expérience utilisateur, donc je ne comprends pas pourquoi personne ne le fait
  • J’aime voir ce genre d’essais apparaître maintenant qu’ils sont relativement peu coûteux et rapides à explorer grâce aux LLM
    Cela dit, ce qui m’a frappé ici, c’est à quel point nginx est lui-même impressionnant. L’autre point notable, c’est la présentation de ce projet comme une alternative à nginx et Caddy qui parie sur une autre façon de configurer
    nginx et Caddy proposent des langages de configuration déclaratifs, et quand on atteint leurs limites, on leur ajoute à côté un runtime de script comme Lua ou des plugins Caddy, ce qui crée un fonctionnement en deux couches
    Mais je pense que ce pari est mauvais. Les gens ont depuis longtemps préféré la configuration au code, et dans bien des cas les fonctions intégrées suffisent, sans avoir besoin d’écrire du C

    • Je ne suis pas sûr qu’on puisse en être aussi certain
      Tous les formats de fichier de configuration semblent commencer simplement. Même YAML était assez raisonnable au départ, puis les gens ont commencé à vouloir quelque chose de plus complexe avec des ancres et des alias
      Même GitLab a son propre format avec quelque chose qui ressemble à des conditions et des variables, et ce sont presque des hacks qui ne marchent qu’à certains endroits. Apache a suivi une trajectoire similaire avec son format de configuration basé sur XML
      Au final, on se retrouve avec une multitude de langages de programmation sur mesure pour gérer la configuration. En environnement d’entreprise, on n’édite même plus directement : on écrit des workflows Ansible en script pour faire de la chirurgie à distance
      Il aurait été plus simple d’intégrer directement au serveur un interpréteur comme Lua ou Python pour gérer la configuration, et d’éviter toute cette étape, plutôt que de corriger par programme des fichiers de configuration sur mesure
      On peut bien sûr dire que ces tentatives sur mesure sont mieux optimisées pour un usage précis que des langages généraux, mais cet argument ne tient que dans le cadre étroit d’exemples-jouets où, au départ, on n’aurait pas eu besoin de tout cet attirail
      Vous vous souvenez des fichiers INI de Windows ? C’était le bon temps, quand le code était du code et les données étaient des données
    • Je parie que d’ici 96 heures, quelqu’un fabriquera avec un LLM un outil d’emballage qui convertit des fichiers de configuration nginx ou Caddy en code utilisable par zeroserve
      Plus simplement encore, on pourrait relire tous les manifests Ingress d’un cluster Kubernetes et reconstruire le pack
      L’idée de fond, c’est que l’interface entre les outils et la configuration n’est elle aussi qu’une API de plus, et que les opérateurs système décrivent déjà l’état du système à un niveau d’abstraction supérieur ; les octets concrets qui composent la configuration ne sont que le résultat final
    • Je me demande s’il ne vaudrait pas mieux abstraire la complexité et recourir à des macros pour obtenir une composition « fichier de configuration »
    • À mesure que l’IA rend de plus en plus possible le passage de la parole humaine à l’effet machine, il vaut peut-être la peine de voir si cette préférence va évoluer
      Du point de vue de l’IA, cette approche peut être plus facile à manipuler. Comme l’IA sait traiter les deux, il faudra peut-être longtemps avant qu’un tel changement s’impose clairement comme une bonne idée
    • Je ne comprends pas pourquoi vous tenez tant à attribuer ça aux LLM. Ce n’est pas parce qu’un LLM a aidé à rédiger le texte que c’est lui qui a mené l’expérimentation
  • J’aime bien l’idée
    Mais je me sentirais plus rassuré si on pouvait mettre des fichiers .rs dans le répertoire eBPF au lieu de fichiers .c. C’est déjà un projet Rust, après tout
    Et d’une certaine façon, je m’attendais à un serveur web accéléré par le noyau. Si on pouvait faire ça proprement avec eBPF, ce serait vraiment remarquable
    Et puis, mono-thread ? Sous Linux, forker et partager la file des connexions entrantes, c’est presque trivial, et en Rust ça ne demanderait que quelques lignes. Avec SO_REUSEPORT, le noyau s’occupe du reste
    D’ailleurs, si vous comptez pousser io_uring, il faut pousser kTLS aussi. Si on peut éviter de gérer SSL en espace utilisateur après le handshake, cela simplifie énormément la conception

    • Merci. Je prévois d’implémenter fork + SO_REUSEPORT
      Jusqu’ici, j’utilisais nftables pour ce genre de besoin, donc je n’en avais pas directement l’usage
  • Très cool. Je me demande s’il serait possible de combiner cela avec d’autres types de programmes BPF, comme des programmes XDP ou des programmes attachés à des socket maps, afin d’intégrer plus bas dans la pile des fonctions HTTP de niveau L7

  • L’idée est bonne, mais je ne sais pas s’il faut vraiment se concentrer sur les fichiers statiques. De nos jours, on lance rarement un nouveau serveur pour ça

    • J’ai fait exactement ça la semaine dernière en rendant Ghost statique, et je me disais à moitié qu’un unique binaire autonome serait peut-être plus rapide
      Donc j’ai l’impression que c’est fait pour moi, même si je reconnais ne pas être un utilisateur typique
    • Cela dépend du domaine. Dans plusieurs disciplines scientifiques, on sert efficacement de gros jeux de données sous forme de formats de fichiers statiques. Par exemple https://zarr.dev/ ou https://parquet.apache.org/
  • Ça a l’air bien et les fonctionnalités sont correctes. Mais ça donne une impression trop artificielle pour vraiment m’enthousiasmer.
    Impossible de savoir si les métriques sont bidon, si les fonctions utilitaires marchent réellement, ou si un vrai travail de durcissement a été fait.
    Je peux accepter que ce soit fait en vibe coding et que le README ait été généré automatiquement. Mais si même le billet d’annonce du blog est écrit par l’IA, je n’ai absolument aucun moyen de juger si sa compréhension de la qualité logicielle est la même que la mienne.
    Monde étrange. Il y a quelques années, si ça avait été publié sans signaler l’IA, je l’aurais probablement accepté sans méfiance. Aujourd’hui, dès que je vois un README soigné et des paramètres de ligne de commande plausibles, je soupçonne tout de suite que le README a halluciné et qu’en réalité il n’y a peut-être même pas ces options.

    • L’auteur ici. Certaines parties essentielles de ce projet, comme async-ebpf, ont été écrites bien avant l’arrivée de ce genre d’agents de code.
      J’utilise beaucoup l’aide de l’IA pour construire zeroserve lui-même, mais je vérifie moi-même les sorties de l’IA et j’en assume la responsabilité.
    • D’après les benchmarks, sur un petit fichier statique de 174 B, zeroserve atteint 36 681 req/s avec un p99 de 5,4 ms, nginx 31 226 req/s avec un p99 de 7,8 ms, et Caddy 12 830 req/s avec un p99 de 22 ms.
      Sur un seul cœur, zeroserve sert donc les petits fichiers environ 17 % plus vite que nginx, avec une latence en queue plus resserrée. C’est le cas pour les pages HTML, les petits JSON et les CSS visés par zeroserve.
      Sur un gros fichier statique de 100 KB, zeroserve est à 8 000 req/s, 782 MB/s, p99 22 ms, tandis que nginx est à 7 600 req/s, 773 MB/s, p99 28 ms, et Caddy à 6 084 req/s, 590 MB/s, p99 44 ms.
      Malgré tout, je choisirais quand même un ancien projet audité, éprouvé en production et durci plutôt qu’un projet aussi récent. Le gain n’est pas assez important pour justifier le risque.
    • C’est vraiment une situation regrettable. Il y a eu récemment le projet ffmpeg-wasm et, en le testant, ça fonctionnait. Mais c’était de l’IA en vibe coding, et je ne supporte pas l’IA. Même si ça marche, ça ne change rien.
      J’ai décidé de rester autant que possible dans l’ancien monde. Des gens intelligents publient du logiciel, et d’autres gens intelligents en assurent la maintenance. Ils n’ont pas besoin d’IA. C’est mon créneau.
      On disparaîtra peut-être, mais je préfère quand même ça. À condition toutefois que ces gens intelligents écrivent de la documentation. Il y a aussi beaucoup de gens intelligents qui détestent écrire de la doc.
      Il y a longtemps, j’ai décidé qu’un logiciel sans documentation, aussi excellent soit-il, ne valait pas mon temps. Je parle surtout des applications ; je n’ai presque jamais lu la documentation Linux, même si certains disent qu’elle n’est pas si mauvaise, donc je ne sais pas.
  • Concept nouveau intéressant, et j’aime bien.
    La vraie question, c’est l’engagement du développeur et la communauté. Les gens derrière Caddy et Nginx assurent le support de leur produit depuis longtemps, et ce projet demandera lui aussi beaucoup de concentration et d’attention.

  • Pourquoi un tarball ?

    • C’est un format simple qui permet d’accéder facilement aux ressources par plages d’octets, tout le monde a les outils pour le manipuler et, surtout, il n’est pas compressé.
    • D’après le premier paragraphe de la section « One tarball, served in place », tout le site tient dans un seul fichier tar, et zeroserve l’indexe au chargement pour construire une carte de plages d’octets à partir des chemins, puis sert les fichiers en lisant directement des plages d’octets dans le tarball lui-même.
      Rien n’est extrait sur le disque. Comme le site entier tient dans ce seul fichier, il n’y a pas de racine documentaire à exposer par erreur via une mauvaise règle location, et le déploiement se résume à un remplacement atomique d’un seul fichier.
      Cela dit, cette explication aussi peut être une justification façon LLM. On voit partout dans le texte des expressions comme « the right shape » ou « the surface is broad ».