EN
en direct

AWS Lambda réécrit sa journalisation réseau en eBPF et Rust pour tenir la densité de ses microVMs

Les ingénieurs de Lambda détaillent comment ils ont remplacé un comptage de paquets à base d’iptables, incapable de passer à l’IPv6, par un pipeline eBPF et Rust qui journalise chaque flux de milliers de microVMs Firecracker par hôte. La leçon dépasse AWS : à densité multi-tenant, la comptabilité réseau doit être quasi gratuite et prouvablement exacte.

Un boulier massif aux centaines de perles sombres, une seule perle isolée captant un reflet ambre au milieu des rangées.

11 septembre 2026. Prashant Kumar Singh et Kshitij Gupta, deux ingénieurs de AWS Lambda, publient un retour d’expérience technique sur la façon dont la plateforme journalise chaque flux réseau qui traverse ses hôtes. Le chiffre qui cadre tout : un seul worker Lambda est une instance EC2 bare-metal remplie de milliers de microVMs, chacune isolée dans un invité Firecracker, qui discutent toutes avec S3, les autres services AWS, Internet et le VPC du client. Quelque chose doit tenir un registre honnête de ce trafic. L’article raconte pourquoi l’ancien système a plafonné et comment une réécriture en eBPF et Rust a tenu l’échelle. La leçon vaut pour quiconque opère de l’observabilité sur une plateforme dense : à ce niveau, chaque octet de RAM et chaque microseconde de CPU comptent deux fois.

Un registre qu’on n’a pas le droit de fausser

Un flow log réseau est le système d’enregistrement de ce qui arrive à un paquet : investigation, réponse à incident, audit, reconstitution d’une charge de travail. Les mêmes enregistrements alimentent la facturation et le metering, des services qui exigent une exactitude absolue. Tout doit être conservé pour l’audit et la conformité.

Deux propriétés dominent les autres. La première est la justesse d’attribution : chaque paquet, chaque flux, doit être rattaché à la microVM où il a atterri et au tenant qui l’a produit. La seconde est la complétude : aucun paquet manquant. Un enregistrement perdu ou mal attribué provoque des erreurs de facturation et des trous d’observabilité — et cela, pour chaque microVM, quand Lambda sert des millions de requêtes par seconde. Le surcoût compte pour une raison invisible au client mais centrale pour l’exploitant : à la densité de Lambda, chaque mégaoctet de RAM et chaque microseconde de CPU en plus rognent l’utilisation, la marge opérationnelle et la capacité à absorber la charge.

Pourquoi l’ancien système a plafonné

En construisant le nouveau Lambda multi-tenant à base de microVMs Firecracker, l’équipe a d’abord repris un système hérité de l’ère EC2 mono-tenant, plus lâche. Deux parties : une extension côté noyau qui comptait les paquets et les associait aux tenants via des règles, et un démon en espace utilisateur qui lisait les compteurs, les agrégeait en enregistrements, les sérialisait et les téléversait. Le système fonctionnait pour un petit nombre de machines virtuelles. Il a cassé à la densité de Lambda, pour deux raisons mutuellement exclusives.

La première est l’explosion des règles. iptables parcourt ses règles de façon quasi linéaire pour chaque paquet, et chaque nouvelle microVM empile des règles sur la chaîne. Un worker hébergeant deux mille microVMs exigeait plus de cent mille règles iptables pour tenir le registre. Chaque paquet payait une taxe proportionnelle à la densité de l’hôte — exactement la mauvaise direction, puisque le but était justement d’empiler plus de microVMs par hôte.

La seconde raison est plus dure : le module noyau emprunté ne parlait pas IPv6. Un registre qui ne voit pas la moitié de l’espace d’adressage n’est pas un registre digne de confiance, et le jour où le support IPv6 dual-stack a été proposé pour Lambda, l’ancienne approche était finie, quelles que soient les optimisations de performance. Comme le formule l’article : un enregistrement aveugle sur la moitié du réseau n’en est pas un.

Trois pièces, une seule chaîne

La réécriture est un assemblage de trois composants coopérants, chacun avec un rôle net.

En bas, la capture noyau : un ensemble de petits programmes eBPF attachés au hook traffic-control (tc) des périphériques virtuels de chaque réseau. Ils interceptent les paquets et émettent un événement compact par paquet dans un ring buffer. Ils ne font qu’observer — aucun chemin de code ne permet de copier, bloquer, abandonner ou réécrire un paquet.

Au milieu, le tagger : un processus en espace utilisateur, non privilégié, écrit en Rust, un par réseau. Il vide son propre ring buffer, agrège les événements bruts en enregistrements par flux, et les écrit sur disque au format Amazon Ion hérité. En haut, l’orchestrateur : un processus privilégié par hôte, propriétaire de tout ce qui exige des droits élevés — charger les programmes eBPF, câbler le traffic control, superviser la flotte de taggers. Il expose une petite API de cycle de vie sur une socket Unix pour que le plan de contrôle crée, réaffecte et détruise le tagging au gré des microVMs.

Le découplage fait le travail : la capture dans le noyau, l’agrégation en espace utilisateur, et un processus par hôte pour orchestrer le tout. Les enregistrements produits repartent vers les consommateurs en aval sans changement.

Capturer dans le noyau sans gêner le trafic

Les programmes de capture s’attachent au qdisc clsact de traffic control, côté entrée et côté sortie de chaque périphérique. Un réseau couvre deux périphériques, donc quatre points d’attache par réseau. Chaque programme lit le paquet et retourne l’action « continue » : aucun paquet client n’est jamais modifié ni abandonné, et rien n’ajoute de latence significative.

Pour chaque paquet, le programme parcourt les en-têtes — Ethernet, puis IPv4 ou IPv6, puis TCP, UDP ou ICMP — et écrit un événement de taille fixe, environ 24 octets pour IPv4 :

c
/* un événement par paquet, ~24 octets en IPv4 */
struct flow_event {
    u8  ip_version;      /* 4 ou 6 */
    u8  protocol;        /* TCP / UDP / ICMP */
    u8  direction;       /* entrée ou sortie */
    u8  device_id;       /* quel périphérique du réseau */
    u16 local_port;      /* « local » = toujours le côté sandbox */
    u16 remote_port;
    u32 flags_and_bytes; /* flags TCP en bits [31:24], octets en [23:0] */
    u32 local_addr;      /* 16 octets en IPv6 */
    u32 remote_addr;
    u32 received_time_ms;
};

Un paquet, un événement, un compteur d’octets. Les flags TCP et le compteur partagent un seul mot de 32 bits : huit bits de flags en tête, 24 bits d’octets dessous. En faire moins dans le noyau est tout l’enjeu — l’agrégation est le travail de quelqu’un d’autre.

Faire passer cette logique au vérificateur eBPF a demandé du travail. Le parser d’en-têtes est devenu un sous-programme partagé, prouvé une seule fois plutôt que ré-inliné à chaque point d’attache. La marche des en-têtes d’extension IPv6 est bornée à un nombre fixe de sauts pour garantir la terminaison. Les fragments au-delà du premier rapportent des ports et flags nuls plutôt que des ordures. Et les super-paquets coalescés par GSO/GRO voient leurs compteurs d’octets traités correctement — un port faux ou un octet surcompté créerait une fausse entrée de journal.

Le vérificateur n’est pas la seule barrière. Ce code produit un registre critique pour la facturation, la conformité et l’audit, alors chaque programme eBPF est écrit en C et passe par un model checker formel (CBMC) à chaque build. Ses harnais vérifient, entre autres, que la disposition en octets de la structure d’événement reste compatible avec ce que chaque programme attend — un décalage silencieux d’un octet est le genre de bug qui corrompt chaque enregistrement sans que personne ne s’en aperçoive avant d’avoir besoin du journal.

Dimensionner le ring buffer par le calcul

Le ring buffer est la seule chose partagée entre le producteur noyau et le consommateur utilisateur, et sa taille est un vrai compromis. Trop petit, on perd des événements en rafale — un trou dans le journal au pire moment. Trop grand, on gaspille de la mémoire, payée sur chaque ring buffer de l’hôte.

L’équipe n’a pas deviné : elle a dérivé un plancher du débit maximal de chaque microVM. Pour un plafond de 100 000 paquets par seconde par direction et une vidange toutes les 100 millisecondes :

text
ring ≈ 62 500 pps × 0,1 s × ~24 octets × 2 directions ≈ 300 Ko

L’API du ring buffer exigeant une puissance de deux, le défaut est fixé à 512 Kio. C’est le plus petit tampon qui ne peut pas déborder entre deux vidanges au débit maximal du client — autrement dit, le plancher garantit qu’un client ne peut pas distancer l’enregistreur, même en essayant. Le déploiement actuel provisionne plus généreusement, de l’ordre de quelques mégaoctets, pendant le calibrage par charge. Le nombre à défendre est le plancher, et il vient d’une limite système, pas d’une supposition.

La cadence de vidange a une propriété supplémentaire : réveiller un processus n’est pas gratuit, et une flotte de milliers de processus qui se réveillent en permanence ferait trembler le CPU. Alors le noyau décide quand déranger. Il ne force un réveil que lorsque le ring dépasse environ 1 % de remplissage ; en dessous, il reste silencieux. Côté utilisateur, le tagger ne relit pas plus d’une fois par 100 millisecondes. Un flux calme attend simplement la prochaine vidange ; un flux chargé franchit le seuil et est lu presque immédiatement. Rien n’est sur minuterie fixe, donc aucun des deux cas ne se trompe de cadence.

Agréger en Rust

Le choix de Rust tient à des raisons banales et pratiques. À cette densité, des milliers de taggers tournent par hôte, chacun portant un petit état qui doit être juste. Un runtime à garbage collector imposerait des pauses et une mémoire qui gonfle sous charge, et une pause au mauvais endroit créerait un trou dans le registre. Rust offre une mémoire prévisible, pas de collecteur, et un compilateur qui refuse de compiler des classes entières de bugs de mauvaise attribution. Chaque tagger tourne dans quelques centaines de kilo-octets de RAM, pour un budget d’environ un mégaoctet. C’est ce qui rend des milliers d’instances abordables par hôte.

À l’intérieur, c’est un petit ensemble de tâches coopérantes sur un runtime async mono-threadé : une tâche lit le ring, une autre possède l’état des flux, une troisième écrit les paquets. Les seules opérations reléguées sur un pool bloquant sont celles qui bloquent vraiment — recevoir le descripteur du ring et sérialiser en Ion. Le tagger lit le ring via epoll : il dort quand il n’y a rien, se réveille quand il y a quelque chose.

À mesure que les événements arrivent, le tagger les verse dans une table de flux indexée par périphérique, quintuplet et un identifiant d’attribution fourni par le plan de contrôle. Les événements correspondants accumulent octets, compteurs de paquets et flags TCP par OU logique. L’agrégation se fait à la lecture, donc le chemin chaud reste une recherche et une addition.

Ce que ça change pour vous

Le retour d’expérience de Lambda est transposable à qui opère de la télémétrie à grande échelle.

  • Remplacez le comptage par règles par de la capture passive. iptables n’a pas tenu la densité ; un hook eBPF en « observe seulement » supprime la taxe par paquet et le risque de toucher au trafic client.
  • Traitez la justesse comme une exigence de build. Le CBMC sur du code eBPF en C montre qu’un registre de facturation mérite une preuve, pas une confiance.
  • Dimensionnez vos tampons par le calcul, pas au doigt mouillé. Le plancher de 512 Kio découle d’un débit maximal et d’une cadence, pas d’une intuition — la seule façon de garantir qu’aucun client ne peut distancer l’enregistreur.
  • Choisissez Rust pour l’état à haute densité. L’absence de collecteur et la mémoire prévisible sont ce qui rend des milliers de processus par hôte économiquement viables.

La conclusion de l’article n’est pas un slogan : c’est une discipline d’ingénierie. L’observabilité au niveau d’un fournisseur de cloud se joue sur des registres dont personne ne voit le coût tant qu’ils sont justes, et que tout le monde découvre le jour où ils ne le sont plus.

Verdict

La réécriture de la journalisation réseau de Lambda est un cas d’école de remplacement d’un système hérité par un pipeline eBPF + Rust — pas pour la nouveauté, mais parce que le comptage à base d’iptables était structurellement incapable de passer à l’IPv6 et de survivre à la densité des microVMs.

Si vous opérez de l’observabilité réseau sur un parc dense, retenez trois choses : la capture passive côté noyau plutôt que des règles qui explosent, une taille de tampon dérivée d’une limite système, et un langage à mémoire prévisible pour l’état par processus. Si vous gérez une plateforme multi-tenant, ajoutez la preuve formelle sur le code qui produit vos registres de facturation. Le coût d’un registre faux n’apparaît jamais au moment où on l’écrit — il apparaît le jour où on en a besoin.

Références

Le brief cyber, chaque mardi

Les failles qui comptent, les correctifs à appliquer, en dix minutes de lecture.

Pas de spam. Désinscription en un clic.
à lire ensuite

Sur le même sujet

Amazon EBS étend les Volume Clones à la copie entre comptes AWS

AWS étend les Volume Clones d’Amazon EBS à la copie entre comptes, avec un réchiffrement optionnel dans le compte cible. Les équipes multi-comptes peuvent désormais rafraîchir leurs environnements de test avec des données de production à jour, à condition de maîtriser les contraintes de chiffrement et de zone.

Google investit 13 milliards d’euros en Finlande pour bâtir l’infrastructure IA européenne

Le 9 septembre 2026, Google annonce 13 milliards d’euros sur 2027-2028 dans quatre sites finlandais — Hamina, Kajaani, Muhos et Vaala — son plus gros investissement européen, adossé à des contrats d’énergie nucléaire et éolienne. C’est un signal sur la façon dont la capacité cloud souveraine européenne se concentre autour du Nord.

← Retour au fil

Tapez au moins deux caractères.

naviguer ouvrir esc fermer