Nous portons ce projet et en sommes le principal contributeur (github.com/sozu-proxy/sozu). Avec Sōzu 2.0, nous expliquions comment le proxy passait du reverse proxy vers l’edge programmable : nouveau multiplexer HTTP/2, sécurité renforcée par défaut, surface d’observabilité plus riche, politiques de trafic, durcissement opérationnel et agilité cryptographique.
Sōzu 2.0.2 est arrivé quelques jours plus tard avec une release plus courte, mais très révélatrice. Elle ajoutait une défense en profondeur face à la classe HTTP/2 bomb et corrigeait la fondation temporelle nécessaire à la reconstruction de spans OpenTelemetry fiables depuis les access logs. Cette version montrait la façon dont nous voulons exploiter l’edge : lorsqu’une classe d’attaque protocolaire apparaît, le proxy doit en absorber autant que possible ; lorsque l’observabilité est subtilement fausse, il faut corriger la fondation avant de construire le produit par-dessus.
Sōzu 2.1.0 est l’étape suivante de cette histoire. L’un des chantiers que nous annoncions comme étant sur l’établi pour la 2.0 a atterri : le load balancer UDP.
Ce n’est pas simplement une case protocolaire de plus. Sōzu prenait déjà en charge HTTP, HTTPS et TCP. Avec la 2.1.0, UDP entre dans le même modèle : le même plan de contrôle reconfigurable à chaud, la même posture d’exploitation, la même discipline de métriques et le même processus de release open source. Sōzu s’élargit ainsi depuis l’edge web vers un load balancer d’infrastructure plus général, capable de placer des services datagrammes comme DNS, syslog, NTP et des workloads UDP génériques derrière le proxy, sans imposer un deuxième load balancer en frontal.
Pour Clever Cloud, cela compte parce que le réseau d’une plateforme ne se limite pas à HTTP. Kubernetes managé et PaaS ont tous deux besoin d’une histoire propre pour le trafic de transport direct. Pour les opérateurs qui exécutent Sōzu eux-mêmes, cela signifie qu’un même composant peut couvrir davantage de surface L4/L7 tout en conservant les propriétés de conception qui rendent Sōzu utile.
Pourquoi UDP compte encore
Une grande partie du trafic visible sur le web passe par HTTP, et l’on pourrait être tenté de traiter tout le reste comme un cas particulier. Une vraie plateforme ne peut pas se le permettre.
DNS est UDP par défaut. Syslog s’exécute souvent sur UDP. NTP utilise UDP. Beaucoup de protocoles internes ou d’infrastructure utilisent des datagrammes parce qu’ils sont simples, sensibles à la latence, ou suffisamment proches d’un modèle requête/réponse pour qu’une connexion TCP complète soit inutile. Les plateformes Kubernetes et PaaS doivent également exposer proprement des services de transport direct, pas seulement des applications web terminées en HTTP.
Avant Sōzu 2.1.0, un opérateur voulant un edge unique pour HTTP, TCP et UDP devait généralement composer plusieurs systèmes. Sōzu couvrait HTTP et TCP, tandis qu’un autre composant traitait UDP. Cette séparation a un coût opérationnel : deux modèles de health check, deux façons d’observer le trafic, deux cycles de release, deux surfaces de panne, deux configurations, et deux endroits où l’état de routage peut diverger.
L’enjeu du support UDP de première classe dans Sōzu, c’est la convergence. Un listener UDP existe désormais au même niveau que les listeners tcp, http et https. Les frontends et backends UDP font partie de la même mécanique de commandes, de configuration et d’état. Les opérateurs disposent de métriques spécifiques à UDP au niveau du proxy, au lieu de traiter le trafic datagramme comme un phénomène extérieur.
Le bénéfice utilisateur n’est pas que chaque application aurait soudainement besoin d’UDP. Il est que la plateforme peut faire converger plus de classes de trafic sous un même modèle opérationnel : reconfiguration à chaud, health checks, métriques, discipline de release et composant d’edge unique qui peut évoluer vers des fonctionnalités produit, plutôt que rester un assemblage d’exceptions par protocole.
Ce qui est disponible dans Sōzu 2.1.0
Sōzu 2.1.0 ajoute un nouveau type de listener protocol = “udp” aux côtés des listeners HTTP, HTTPS et TCP. Il est opt-in. Les configurations existantes ne deviennent pas UDP par accident, et les consommateurs de sozu-command-lib peuvent passer de 2.0.2 à 2.1.0 sans réécrire leurs intégrations.
L’idée centrale consiste à représenter le trafic UDP sous forme de flows virtuels. UDP n’a pas de connexion, pas de stream, pas de boucle accept(). Un datagramme arrive sur une socket. Il n’existe pas de socket par client créée par le noyau, comme en TCP. Sōzu reconstruit donc en espace utilisateur la partie utile d’une connexion : un flow identifié par l’adresse source du client, avec la possibilité d’inclure le port source lorsque c’est le bon modèle d’affinité pour le workload.
Une fois ce flow établi, Sōzu peut prendre les décisions qu’un load balancer doit prendre pour n’importe quel transport :
- quel backend porte ce client ;
- combien de temps le flow doit rester vivant ;
- que faire lorsqu’un backend devient unhealthy ;
- comment délester en surcharge ;
- quelles métriques et quels logs émettre.
La release ajoute deux algorithmes adaptés à l’affinité de flow UDP. Le choix recommandé est HRW, aussi appelé rendezvous hashing. HRW possède une propriété très utile dans un système reconfigurable à chaud : lorsque l’ensemble des backends change, il minimise les déplacements sans nécessiter de table pré-calculée. Sōzu propose également Maglev en option, pour les ensembles de backends plus grands ou les débits élevés où un lookup O(1) devient plus intéressant.
Sōzu peut aussi envoyer des informations PROXY protocol v2 aux backends UDP. Cela transporte la véritable adresse client vers le backend, qui n’a pas à considérer le proxy comme seul pair visible. Par défaut, Sōzu l’envoie sur le premier datagramme d’un flow, avec une option pour l’envoyer sur chaque datagramme lorsque le backend a besoin de ce contexte à chaque fois.
Les health checks font partie du design UDP, et non d’une rustine. Un backend UDP peut être vérifié via une sonde TCP compagnon, qui est le signal pratique courant pour les services où UDP nu ne fournit pas d’état “connecté” fiable. Sōzu peut aussi utiliser une sonde UDP applicative lorsque cela a du sens. Les checks prennent en charge l’hystérésis rise/fall et une sémantique fail-open, afin qu’une panne transitoire ou globale de health check ne transforme pas automatiquement le trafic en trou noir.
Enfin, UDP obtient sa propre surface de métriques : datagrammes entrants et sortants, octets entrants et sortants, flows actifs, flows créés, évincés ou délestés, datagrammes droppés par raison, santé des backends et durée des flows. C’est le même principe que pour l’observabilité de la 2.0 : une fonctionnalité n’est exploitable en production que lorsque les opérateurs peuvent voir ce qu’elle fait.
UDP est facile à mal ajouter
La difficulté du load balancing UDP n’est pas de recevoir un datagramme et de l’envoyer ailleurs. La difficulté est de décider quel état existe autour de ce datagramme.
TCP donne au proxy un cycle de vie de connexion. HTTP lui donne des requêtes, des réponses, des headers et des status codes. UDP lui donne des paquets. Ces paquets peuvent arriver dans le désordre. Ils peuvent disparaître. Un client peut cesser d’envoyer sans rien fermer, puisqu’il n’y a rien à fermer. Un backend peut devenir unhealthy alors qu’un flow virtuel pointe encore vers lui. Une reconfiguration peut arriver pendant que des flows existent. Un listener peut atteindre son plafond de flows. Un timeout peut se déclencher juste après le rafraîchissement d’un flow. Ce ne sont pas des cas exotiques. C’est la forme normale du trafic datagramme sous charge.
C’est pourquoi le travail UDP de Sōzu 2.1.0 porte autant sur la testabilité que sur la fonctionnalité.
Le coeur UDP est structuré autour d’une séparation sans-io. Le coeur pur porte la table des flows, les décisions d’admission, les timers, la politique de teardown et les demandes de load balancing. La coquille impure porte les sockets, le buffer pool, les health checks, les appels système et l’émission des métriques. Le coeur n’a pas de socket, pas de lecture d’horloge murale dans le datapath, pas de source d’aléa cachée. Le temps et les seeds sont injectés.
Cette forme compte parce qu’elle rend le coeur simulable de manière déterministe. La même seed produit la même séquence. La même horloge virtuelle saute aux mêmes instants. La même tempête de reconfiguration se produit à la même étape. Une panne peut imprimer une seed et être rejouée exactement.
Sōzu 2.1.0 embarque un harnais de simulation déterministe, dans l’esprit FoundationDB/VOPR, pour le coeur UDP. Le sweep par défaut exécute 256 seeds, chacune pilotant une charge adversariale randomisée : datagrammes client, réponses backend, résolutions backend périmées, rafales de reconfiguration, changements de plafond de flows, sauts d’horloge, drains et teardown de masse. Après chaque action, le harnais draine les sorties du coeur et vérifie les invariants du modèle.
Par-dessus, la release contient un travail de densité d’assertions inspiré de TigerBeetle TigerStyle. À travers le proxy, le plan de contrôle et le supervisor, Sōzu porte désormais environ 1100 debug_assert! et plusieurs sweeps d’invariants. Ces assertions sont compilées hors des builds de release, mais elles sont actives dans les tests, le fuzzing, les tests end-to-end, les simulations et les builds de développement. Elles transforment une dérive d’état silencieuse en échec local et bruyant.
Ces chiffres sont utiles parce qu’ils ne mesurent pas du volume. Le nombre de lignes de code ne prouve pas la fiabilité. Un sweep de 256 seeds et des invariants denses disent autre chose : le code est poussé dans des transitions d’état adversariales, et les structures de données doivent prouver en continu qu’elles restent cohérentes.
Pour un proxy, cette distinction est essentielle. Un compteur faux n’est pas seulement un compteur faux. Il peut signifier qu’un flow fuit. Un timer périmé n’est pas seulement un timer. Il peut signifier qu’un backend continue de recevoir du trafic alors que l’état prétend le contraire. Une divergence silencieuse entre une table de routage et un slab n’est pas qu’une incohérence interne. C’est le genre de défaut qui devient un symptôme de production plusieurs couches plus loin que le bug réel.
La simulation déterministe comme direction
Le simulateur UDP indique aussi la direction que nous voulons donner à l’ingénierie de Sōzu.
FoundationDB a rendu célèbre la simulation déterministe dans le logiciel d’infrastructure : exécuter le système dans un monde simulé, injecter des fautes, explorer des milliers d’ordonnancements et rendre chaque panne reproductible depuis une seed. TigerBeetle pousse une discipline complémentaire : énoncer les invariants directement dans le code, affirmer l’espace positif et l’espace négatif, et rendre l’état illégal impossible à ignorer pendant les tests.
Sōzu 2.1.0 applique ces idées au coeur UDP. Le prochain chantier architectural consiste à faire entrer davantage de Sōzu dans cette forme : plus de composants sans-io, plus d’horloges injectées et de frontières déterministes, plus de couches simulables sur les parties pertinentes du modèle OSI avant de les exposer comme comportement de production.
C’est également pour cela que Moonpool compte pour Clever Cloud. Moonpool, développé par PierreZ, est le moteur de simulation déterministe que nous utilisons chez Clever Cloud pour cette direction en Rust. La version 0.7.0 est désormais disponible publiquement sur crates.io, avec moonpool et moonpool-sim. Le harnais UDP de Sōzu est aujourd’hui un simulateur synchrone in-tree, adapté au coeur UDP pur, mais la direction générale est la même : workloads déterministes, temps virtuel, seeds reproductibles, et systèmes réseau que l’on peut faire échouer avant que la production ne s’en charge.
Le premier bénéfice est la fiabilité. Nous voulons moins de bugs de coin dans le code protocolaire et la reconfiguration. Le deuxième bénéfice est la vitesse de développement. Lorsque davantage de proxy est structuré en machines d’état pures avec des frontières d’I/O explicites, les nouveaux comportements protocole et load balancing deviennent plus sûrs à faire évoluer. Le troisième bénéfice est l’exploitabilité. Hot reload, surcharge et panne deviennent plus faciles à raisonner parce que le code a été exercé sous ces formes exactes.
C’est la couche située sous l’edge programmable. Les boutons exposés aux utilisateurs ne valent la peine d’exister que si la mécanique qui les porte est suffisamment déterministe pour inspirer confiance.
Ce que cela rend possible
Pour Kubernetes managé, le support UDP donne à Sōzu un meilleur socle pour les services de transport direct. Une plateforme peut proposer du trafic web, des services TCP et des services UDP dans un modèle de load balancing plus cohérent, plutôt que de séparer les responsabilités entre composants sans lien.
Pour le PaaS, cela ouvre la voie à des fonctionnalités produit autour des workloads datagrammes sans repartir d’une pile opérationnelle séparée. Des services proches de DNS, des flux proches de syslog, des sondes proches de NTP et des applications UDP génériques peuvent être pensés avec le vocabulaire du même proxy : listeners, clusters, backends, health, métriques et reconfiguration à chaud.
Pour les opérateurs Sōzu hors Clever Cloud, cela signifie que le projet couvre davantage de surface d’edge infrastructure. Si vous utilisez déjà Sōzu parce que vous voulez un proxy open source, reconfigurable à chaud, avec des releases signées et un code Rust auditable, la 2.1.0 élargit la classe de trafic que vous pouvez placer sous ce modèle.
La conséquence côté observabilité est également importante. Le trafic UDP dispose désormais de métriques natives dans le proxy. Vous pouvez demander combien de flows sont actifs, combien ont été délestés, pourquoi des datagrammes ont été droppés, si les backends sont healthy et combien de temps vivent les flows. C’est ce qui transforme une fonctionnalité protocolaire en capacité exploitable.
La fiabilité vient aussi de ce qui entoure UDP
Le titre de cette release est UDP, mais la 2.1.0 continue aussi le travail opérationnel de la 2.0.
La reconfiguration à chaud est l’une des promesses centrales de Sōzu, donc la correction du replay d’état compte. Cette release corrige un cas où un listener dont la configuration changeait tout en restant actif pouvait être rejoué comme une suppression suivie d’un ajout sans réémettre l’activation. Ce type de bug explique précisément pourquoi le modèle d’état doit être testé comme un cycle de vie, et pas seulement comme une somme de commandes isolées.
La release corrige aussi un panic par underflow arithmétique dans le pattern trie pour des hostnames dont le segment le plus à gauche est une regex. Elle corrige enfin la chaîne de release en pinning le binaire cosign sur la ligne 2.x, après un changement d’outillage ayant cassé le flux des artefacts de signature pendant la release 2.0.2.
Ce ne sont pas les parties de la release que les utilisateurs verront en premier. Elles font pourtant partie du produit. À l’échelle de Clever Cloud, la tranquillité opérationnelle est un bénéfice client. Un proxy qui se reconfigure correctement, signe ses artefacts de manière prévisible et transforme ses cas limites en tests laisse les applications continuer à servir du trafic pendant que la plateforme bouge sous elles.
La suite
Le support UDP va continuer à mûrir avec l’usage réel. Il y aura de la place pour du travail de performance, du batching et du retour d’expérience opérationnel. Mais la direction plus large est architecturale : rendre davantage de Sōzu déterministe, davantage sans-io lorsque cela a du sens, et davantage de modes de panne reproductibles avant qu’ils ne deviennent des incidents.
C’est la leçon de Sōzu 2.1.0. L’edge programmable n’est pas seulement un ensemble de toggles exposés aux utilisateurs. C’est aussi une manière de construire du logiciel d’infrastructure : protocole par protocole, machine d’état par machine d’état, avec assez d’observabilité pour l’exploiter et assez de simulation pour lui faire confiance.
Sōzu est un projet open source publié sous AGPL-3.0, avec sa command library sous LGPL-3.0. Le code, les notes de release, les issues et les discussions de conception vivent sur github.com/sozu-proxy/sozu. Merci aux contributeurs et aux opérateurs qui poussent le projet vers un edge plus large, plus sûr et plus programmable.
Références
Sōzu
- Notes de release Sōzu 2.1.0. Load balancing UDP de première classe, simulation déterministe, densité d’assertions, CI et documentation. https://github.com/sozu-proxy/sozu/releases/tag/2.1.0
- RFC et master plan UDP de Sōzu. Issue publique de conception pour le load balancing UDP de première classe. https://github.com/sozu-proxy/sozu/issues/1273
- PR d’implémentation UDP de Sōzu. Implémentation des listeners UDP de première classe et du load balancing associé. https://github.com/sozu-proxy/sozu/pull/1274
- Issue historique sur le load balancing UDP. Issue publique ouverte en 2020. https://github.com/sozu-proxy/sozu/issues/654
- Sōzu 2.0 : du reverse proxy à l’edge programmable. Article de release jalon. https://www.clever.cloud/fr/blog/engineering-fr/2026/05/29/sozu-2-0-reverse-proxy-edge-programmable/
- Documentation de configuration Sōzu. Listeners UDP, clusters UDP, health checks, PROXY protocol et métriques. https://github.com/sozu-proxy/sozu/blob/2.1.0/doc/configure.md
- Documentation de test Sōzu. Doctrine de test : unit, e2e, fuzz, simulation déterministe et gardes de régression. https://github.com/sozu-proxy/sozu/blob/2.1.0/doc/testing.md
- Documentation de simulation UDP Sōzu. Sweep par défaut de 256 seeds et knobs de replay. https://github.com/sozu-proxy/sozu/blob/2.1.0/doc/udp_simulation.md
Protocoles et algorithmes
- Spécification PROXY protocol v2. Propagation de l’adresse client à travers les proxies. https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
- Rendezvous hashing / HRW. Sélection avec affinité et perturbation minimale lorsque l’ensemble des backends change. https://en.wikipedia.org/wiki/Rendezvous_hashing
- Maglev: A Fast and Reliable Software Network Load Balancer. Papier Google décrivant le consistent hashing Maglev. https://research.google/pubs/maglev-a-fast-and-reliable-software-network-load-balancer/
Tests
- FoundationDB testing. Lignée de la simulation déterministe et de l’injection de fautes. https://apple.github.io/foundationdb/testing.html
- TigerBeetle TigerStyle. Style d’ingénierie assertion-first. https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md
- TigerBeetle VOPR. Approche de simulation testing. https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/internals/vopr.md
- Moonpool. Simulation déterministe pour systèmes distribués en Rust. https://crates.io/crates/moonpool
- moonpool-sim. Moteur de simulation du framework Moonpool. https://crates.io/crates/moonpool-sim
- Dépôt Moonpool. Développé par PierreZ. https://github.com/PierreZ/moonpool