Cloudflare Just Saved 100TB of Memory

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00Combien peut réellement vous coûter un seul octet ?
00:00:02Eh bien, à l'échelle de Cloudflare, gaspiller un seul octet par entrée
00:00:04leur coûte plus de 250 gigaoctets de mémoire
00:00:06dans l'ensemble de leur parc,
00:00:08et ils ont récemment réduit de moitié cette mémoire par entrée,
00:00:11libérant ainsi 100 téraoctets de mémoire.
00:00:13Par les temps qui courent, cela fait beaucoup d'argent.
00:00:15Ils y sont parvenus grâce à cinq modifications assez simples
00:00:17apportées à leur système de cache,
00:00:18tout en rendant le DNS plus rapide en même temps.
00:00:21Voyons donc en détail comment ils ont fait.
00:00:27Vous avez peut-être déjà entendu parler de 1.1.1.1.
00:00:30C'est le résolveur DNS public de Cloudflare,
00:00:32et il fonctionne en prenant le domaine que vous souhaitez visiter,
00:00:34comme betterstack.com,
00:00:35pour découvrir quelle est la véritable adresse IP de ce domaine.
00:00:39L'élément qui effectue ce travail
00:00:40s'appelle Big Pineapple, il est écrit en Rust,
00:00:42et comme vous pouvez l'imaginer,
00:00:43c'est un logiciel extrêmement sollicité,
00:00:46qui stocke plus de 250 milliards d'entrées de cache DNS à tout moment.
00:00:50Cela garantit que si quelqu'un souhaite se rendre sur betterstack.com
00:00:52deux secondes après quelqu'un d'autre,
00:00:53il n'a pas besoin de parcourir toute la hiérarchie DNS à nouveau,
00:00:56il garde simplement la réponse en mémoire et la renvoie directement.
00:00:59Mais comme je l'ai mentionné en introduction,
00:01:01250 milliards d'entrées de cache signifient qu'un octet gaspillé par entrée
00:01:04coûte environ 250 gigaoctets de RAM,
00:01:07et c'est pourquoi ces cinq changements ont eu un si grand impact.
00:01:10Premièrement, penchons-nous sur le coût de la capacité.
00:01:12Voici à quoi ressemblait une entrée de cache auparavant.
00:01:14Un horodatage, une durée de vie (TTL), des compteurs de succès,
00:01:16puis un ensemble de vecteurs.
00:01:17Un vecteur d'enregistrements de réponse,
00:01:18un vecteur d'enregistrements d'autorité,
00:01:20un vecteur d'enregistrements additionnels,
00:01:21un vecteur d'erreurs,
00:01:22et bien d'autres encore.
00:01:23Pour ceux d'entre vous qui ne connaissent pas Rust,
00:01:25un vecteur est simplement le type de données par défaut pour une liste extensible,
00:01:27et il est représenté par trois éléments en mémoire.
00:01:29Un pointeur, une longueur et une capacité.
00:01:31C'est l'espace réservé pour la croissance,
00:01:32afin de ne pas avoir à réallouer à chaque fois qu'on y ajoute un élément.
00:01:35Ce qui est très utile si l'élément grandit effectivement.
00:01:38Mais ils ont examiné de vraies entrées de cache DNS,
00:01:40et ils ont réalisé que tout était simplement écrit dans le cache
00:01:42et n'était plus jamais modifié.
00:01:44Elles étaient seulement lues,
00:01:45de sorte que ces vecteurs ne grandissaient jamais.
00:01:47Cela signifie qu'il y a de l'espace tas suralloué,
00:01:49qui reste là à être gaspillé.
00:01:50Il y a un vecteur avec une capacité pour huit éléments,
00:01:52mais seulement cinq stockés,
00:01:53ce qui laisse trois emplacements inutilisés,
00:01:55et le champ de capacité lui-même est un usize,
00:01:57ce qui représente huit octets de mémoire,
00:01:58dont ils n'ont tout simplement pas besoin.
00:02:00La solution à ce problème était incroyablement simple.
00:02:02Il suffit de remplacer un vecteur par un Box,
00:02:04car la taille d'un Box est fixée à la taille qu'il avait lors de sa création,
00:02:07il n'a donc pas besoin de champ de capacité,
00:02:08et il stocke exactement ce qui s'y trouve.
00:02:10Il n'a pas besoin d'allouer de capacité supplémentaire pour l'avenir.
00:02:13Ils ont également réalisé qu'ils pouvaient appliquer cette même économie
00:02:15aux champs de chaînes de caractères,
00:02:16car ils sont essentiellement constitués d'un vecteur de U8,
00:02:18afin que ce texte puisse croître ou diminuer,
00:02:20mais si vous n'en avez pas besoin,
00:02:21vous pouvez simplement utiliser une tranche de chaîne Box (Box),
00:02:23c'est-à-dire que cette chaîne ne va pas changer.
00:02:26Au total, sur une entrée de cache,
00:02:27il y avait huit champs de vecteurs et de chaînes,
00:02:29donc les remplacer par un Box a permis d'économiser huit octets par champ,
00:02:31soit 64 octets par entrée,
00:02:33tout en éliminant l'espace tas excédentaire
00:02:35qu'un vecteur réserve pour une croissance future,
00:02:36ce qui signifie que les économies combinées ont atteint plus de 15 téraoctets
00:02:39lorsqu'elles sont portées à 250 milliards d'entrées de cache.
00:02:42Tout cela grâce à un simple changement de type de données.
00:02:45Pour notre prochain changement cependant,
00:02:46ils ont examiné certaines de ces listes,
00:02:48et se sont simplement demandés : en avons-nous vraiment besoin ?
00:02:50Une réponse DNS contient trois types d'entrées,
00:02:52réponse, autorité et additionnel,
00:02:54et comme nous l'avons vu plus tôt,
00:02:55celles-ci étaient mises en cache telles quelles,
00:02:57sous forme de trois listes séparées,
00:02:58et même si nous avons supprimé le champ de capacité lors du premier changement,
00:03:01chaque liste est toujours composée d'un pointeur et d'une longueur,
00:03:03soit huit octets plus huit octets multipliés par trois,
00:03:05ce qui fait 48 octets au total,
00:03:07et trois allocations de tas séparées.
00:03:09Mais en examinant comment ces listes
00:03:10étaient réellement utilisées,
00:03:12ils ont réalisé qu'elles étaient toujours lues ensemble,
00:03:13toujours écrites ensemble,
00:03:14et qu'elles sont toujours dans le même ordre exact.
00:03:16Donc, au lieu de trois listes,
00:03:17ils pouvaient les traiter comme une seule liste avec deux séparateurs.
00:03:20Les trois listes deviennent donc une seule liste d'enregistrements,
00:03:22plus deux petits décalages (offsets) indiquant que les réponses se terminent ici,
00:03:25et que l'autorité se termine là,
00:03:26et comme une réponse DNS n'aura jamais quatre milliards d'enregistrements,
00:03:29un entier non signé de 16 bits suffira pour les décalages,
00:03:33ce qui ne fait que deux octets chacun.
00:03:34Cela signifie qu'au total,
00:03:35ils ont remplacé deux en-têtes complets de 16 octets par deux nombres de 2 octets,
00:03:39soit 28 octets économisés par entrée,
00:03:41ce qui permet également à Rust de supprimer un peu d'espacement superflu entre les champs,
00:03:44rendant la structure (struct) plus petite que la simple somme des champs supprimés.
00:03:47Ils ont même poussé ce concept plus loin,
00:03:49en fusionnant plusieurs champs booléens en un seul drapeau de bits (bit flag).
00:03:52Passons au changement numéro trois :
00:03:53et si l'on arrêtait de répéter le même nom deux fois ?
00:03:55Chaque enregistrement DNS possède un propriétaire,
00:03:57qui est simplement le nom de domaine auquel cet enregistrement appartient.
00:04:00Ainsi, lorsque vous interrogez betterstack.com et que vous récupérez vos enregistrements,
00:04:03chacun d'eux porte la mention betterstack.com.
00:04:05Mais cette information est en quelque sorte redondante.
00:04:08C'est vous qui avez posé la question,
00:04:09le système connaît donc déjà le nom de domaine.
00:04:11Et Cloudflare stockait déjà ce domaine de requête comme clé de cache,
00:04:14alors pourquoi ont-ils besoin de le stocker à nouveau comme propriétaire ?
00:04:17Eh bien, ce n'est pas le cas.
00:04:17Cloudflare a donc simplement transformé ce champ en un nom de type Box optionnel,
00:04:20où si le propriétaire correspond à la requête,
00:04:22on stocke None,
00:04:23mais s'il est réellement différent,
00:04:24ce qui peut arriver dans quelques cas comme les chaînes CNAME,
00:04:27on stocke le propriétaire comme avant.
00:04:29En procédant ainsi, dans les cas normaux où ils correspondent,
00:04:32on économise toute une allocation de tas par enregistrement,
00:04:34cette économie consistait donc simplement à examiner les données présentes dans leur cache
00:04:37et à réaliser qu'il y avait des valeurs en double.
00:04:39Pour l'économie numéro quatre, intéressons-nous à l'adresse IP de 144 octets.
00:04:43Leurs données d'enregistrement formaient une énumération (enum) Rust pour les enregistrements A, AAAA, TXT,
00:04:47SVCB et NAPTR, un seul type couvrant l'ensemble.
00:04:51Mais voici le problème avec les enums.
00:04:52Elles sont toujours aussi grandes que leur variante la plus volumineuse.
00:04:54Chaque valeur de ce type occupe la même quantité d'espace,
00:04:57qu'elle en ait besoin ou non.
00:04:58Dans ce cas, NAPTR est la plus grande valeur, occupant 136 octets,
00:05:03et si l'on ajoute le tag et le remplissage à l'énumération, on arrive à 144 octets.
00:05:07Si l'on compare cela aux besoins d'un enregistrement A, qui est juste une simple adresse IPv4,
00:05:12cela ne nécessiterait que quatre octets.
00:05:13Cela signifie que chaque enregistrement A de ce cache résidait dans une boîte de 144 octets,
00:05:17n'en utilisant que quatre,
00:05:19et les enregistrements A et AAAA constituent la majeure partie du trafic réel.
00:05:22Dans le benchmark de Cloudflare, il y a 56 % d'enregistrements A et 25 % d'AAAA, donc la majorité du cache n'était que du remplissage.
00:05:29La solution a été notre chère boîte.
00:05:32Ils ont mis les grandes variantes dans des boîtes et ont laissé A et AAAA en ligne, car c'est petit et courant,
00:05:36ainsi text, SVCB et NAPTR se trouvent désormais derrière un pointeur,
00:05:40ce qui fait que l'énumération n'a plus besoin d'être plus grande que la plus grande restante, à savoir cette adresse IPv6 de 16 octets.
00:05:45Au total, pour un enregistrement A ou AAAA, cela représente une économie de 120 octets chacun.
00:05:50Mais ce changement comporte un compromis.
00:05:52Quand on met une variante en boîte, ses données passent d'une entrée de cache
00:05:55pour se retrouver dans sa propre région du tas, ailleurs,
00:05:58et cela engendre deux nouveaux coûts.
00:06:00Le premier est l'allocateur.
00:06:02Cloudflare utilise Jemalloc, et Jemalloc ne fournit pas le nombre exact d'octets demandés,
00:06:06il groupe les allocations dans des blocs de taille fixe et arrondit à la valeur supérieure.
00:06:10Ainsi, un enregistrement text demandant 32 octets atterrit dans un bloc de 32 octets sans rien gaspiller,
00:06:15mais un enregistrement MX demandant 40 octets est arrondi à 48 octets, perdant silencieusement 8 octets.
00:06:21Le deuxième coût est la localité.
00:06:22Avant la mise en boîte, toutes les données d'enregistrement d'une entrée résidaient dans un bloc de mémoire contigu,
00:06:27et après, chacune se trouve ailleurs, ce qui implique de suivre un pointeur pour la lire,
00:06:31et si ce pointeur se trouve loin du reste de l'entrée,
00:06:34votre processeur doit aller chercher une toute nouvelle ligne de cache rien que pour la lire.
00:06:37Bien que la mise en boîte ait résolu le problème du remplissage, elle en a créé un nouveau,
00:06:41ce qui a poussé Cloudflare à se demander : et si on ne les stockait pas du tout sous forme de types Rust ?
00:06:45C'est le changement numéro cinq.
00:06:46Cloudflare l'a décrit comme un juste milieu : garder le reste de l'entrée de cache comme des champs structurés normaux,
00:06:50mais prendre les données d'enregistrement elles-mêmes et les stocker sous forme de bruts.
00:06:54Ainsi, au lieu d'une énumération ou d'une boîte par enregistrement, toute l'entrée reçoit un unique tableau d'octets,
00:07:00chaque enregistrement étant écrit avec un préfixe de longueur de 2 octets, suivi de ses données.
00:07:03Cela annule les deux coûts dont nous venons de parler.
00:07:06Toutes ces allocations en boîte distinctes fusionnent en une seule pour l'ensemble des données d'enregistrement,
00:07:10il n'y a donc plus d'arrondi par enregistrement vers un bloc Jemalloc,
00:07:13et tout est de nouveau regroupé de manière contiguë, ce qui permet de récupérer la localité de cache perdue.
00:07:18En prime, cela accélère également les recherches.
00:07:21Auparavant, lors de chaque consultation, un enregistrement analysé résidait en mémoire,
00:07:25et il fallait le sérialiser champ par champ au format réseau DNS avant de pouvoir l'envoyer.
00:07:30Maintenant, il est déjà au format réseau DNS, donc la plupart des types sont copiés directement du tampon
00:07:35vers le message sortant.
00:07:37Les seuls qui ont encore besoin d'analyse sont ceux contenant des noms de domaine,
00:07:40à savoir CNAME, NS, MX et SOA,
00:07:43et cela uniquement parce que la compression des noms DNS oblige à réécrire ces noms de toute façon.
00:07:47Ce changement final réduit la mémoire et supprime beaucoup de travail du chemin critique.
00:07:51Voilà donc 5 modifications de bas niveau apportées au cache,
00:07:54qui, combinées, réduisent l'empreinte par entrée de 953 octets à 420,
00:08:00soit 56 % de moins, et la mémoire allouée par entrée est passée de 1,1 kilo-octet
00:08:05à 461 octets, soit une réduction de 58 %.
00:08:09De plus, leur débit d'insertion a augmenté de 43 %,
00:08:12passant de 625 000 entrées par seconde à 893 000,
00:08:17et les recherches sont devenues 19 % plus rapides, passant de 828 nanosecondes à 670.
00:08:23Ils ont déployé ces changements en production cette année,
00:08:25et leur mémoire résidente P99 est passée de 9,3 gigaoctets à 5,3,
00:08:29soit une baisse de 43 % sur le trafic réel,
00:08:32ce qui représente à l'échelle du parc environ 100 téraoctets de mémoire libérée,
00:08:35soit l'équivalent de la RAM de 130 serveurs de génération 13.
00:08:38Ils prévoient maintenant d'utiliser cet espace supplémentaire pour avoir un cache plus grand,
00:08:41accélérant encore les choses.
00:08:43J'aime beaucoup cet article de blog car il met en lumière un choix auquel nous serons confrontés.
00:08:46Devrions-nous nous poser avant de construire tout cela pour concevoir toutes les optimisations,
00:08:50ou est-ce que cela aurait été une optimisation prématurée ?
00:08:52J'imagine qu'à l'époque, en 2018, lors de sa création,
00:08:55le coût de la RAM pour Cloudflare n'était pas aussi important qu'aujourd'hui.
00:08:59L'article complet est une excellente lecture, je vous mets le lien en description.
00:09:02Dites-moi ce que vous en pensez dans les commentaires,
00:09:03abonnez-vous pendant que vous y êtes,
00:09:04et comme toujours, à la prochaine.
00:09:05À la prochaine.

Key Takeaway

Cinq modifications de bas niveau appliquées au cache DNS en Rust ont réduit la mémoire par entrée de 58 % et libéré 100 téraoctets sur l'ensemble du parc de Cloudflare.

Highlights

  • Cloudflare a libéré 100 téraoctets de mémoire grâce à cinq optimisations de son système de cache DNS.

  • La suppression du champ de capacité inutile dans les vecteurs Rust a permis d'économiser plus de 15 téraoctets.

  • Le débit d'insertion du cache a augmenté de 43 %, passant de 625 000 à 893 000 entrées par seconde.

  • La mémoire résidente P99 a diminué de 43 %, réduisant l'empreinte de 9,3 gigaoctets à 5,3 gigaoctets.

  • Le temps de recherche des requêtes DNS a baissé de 19 %, passant de 828 nanosecondes à 670 nanosecondes.

Timeline

Contexte et impact de la mémoire du cache

  • Un octet gaspillé par entrée de cache coûte plus de 250 gigaoctets de RAM à l'échelle de Cloudflare.
  • Le résolveur DNS public Big Pineapple gère plus de 250 milliards d'entrées en Rust.

Le système stocke les adresses IP en mémoire pour éviter de parcourir la hiérarchie DNS à chaque requête. Le gaspillage de mémoire à cette échelle représente un coût financier élevé, ce qui motive la mise en place d'optimisations structurelles.

Élimination de la capacité surallouée des vecteurs

  • Le remplacement des vecteurs par des Box supprime le champ de capacité de huit octets inutile.
  • L'utilisation de tranches de chaînes Box économise 64 octets par entrée de cache.

Les entrées de cache DNS ne subissent aucune modification après leur écriture, rendant les vecteurs extensibles superflus. Supprimer la capacité non utilisée évite le gaspillage d'espace tas sur l'ensemble des 250 milliards d'entrées.

Fusion des listes et suppression des données redondantes

  • Trois listes de réponses séparées fusionnent en une seule liste avec deux décalages de 16 bits.
  • Le nom de domaine propriétaire est supprimé lorsqu'il correspond à la clé de requête du cache.

L'analyse des listes montre qu'elles sont lues et écrites simultanément dans un ordre constant. Remplacer les en-têtes multiples par des décalages et éliminer les doublons de noms de domaine réduit considérablement la taille de la structure.

Optimisation des énumérations et stockage des données brutes

  • Les variantes d'énumération volumineuses sont placées dans des boîtes pour alléger les enregistrements A et AAAA.
  • Le stockage des enregistrements sous forme de tableau d'octets brut restaure la localité du cache.

Le stockage d'enregistrements légers dans une énumération dimensionnée pour le type NAPTR générait du remplissage inutile. Le regroupement des données brutes avec un préfixe de longueur élimine les arrondis de l'allocateur Jemalloc et accélère la sérialisation.

Bilan des performances et gains globaux

  • L'empreinte mémoire par entrée diminue de 56 %, passant de 953 octets à 420 octets.
  • Le déploiement en production libère 100 téraoctets de mémoire à l'échelle du parc de serveurs.

Les cinq modifications combinées améliorent à la fois l'utilisation de la mémoire et la vitesse de traitement. La réduction de l'empreinte équivaut à la RAM de 130 serveurs, que Cloudflare prévoit d'utiliser pour agrandir le cache.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video