Cloudflare Just Saved 100TB of Memory

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00Was kann ein einziges Byte eigentlich kosten?
00:00:02Nun, bei der Skalierung von Cloudflare kostet das Verschwenden eines einzigen Bytes pro Eintrag
00:00:04sie über 250 Gigabyte Arbeitsspeicher
00:00:06in ihrem gesamten System,
00:00:08und vor Kurzem haben sie diesen Speicherbedarf pro Eintrag halbiert,
00:00:11wodurch 100 Terabyte Arbeitsspeicher frei wurden.
00:00:13In der heutigen Zeit ist das eine Menge Geld.
00:00:15Sie haben dies tatsächlich durch fünf recht einfache Änderungen
00:00:17an ihrem Caching-System erreicht
00:00:18und gleichzeitig DNS sogar noch schneller gemacht.
00:00:21Schauen wir uns also an, wie sie das angestellt haben.
00:00:27Vielleicht habt ihr schon mal von 1.1.1.1 gehört.
00:00:30Es ist der öffentliche DNS-Resolver von Cloudflare
00:00:32und funktioniert, indem er die gewünschte Domain nimmt,
00:00:34wie etwa betterstack.com,
00:00:35und herausfindet, wie die echte IP für diese Domain lautet.
00:00:39Das Tool, das diese Arbeit erledigt,
00:00:40heißt Big Pineapple, ist in Rust geschrieben
00:00:42und, wie ihr euch denken könnt,
00:00:43ein unglaublich stark beanspruchtes Stück Software,
00:00:46das zu jedem Zeitpunkt über 250 Milliarden DNS-Cache-Einträge speichert.
00:00:50Das stellt sicher, dass wenn jemand betterstack.com aufrufen möchte,
00:00:52zwei Sekunden nach jemand anderem,
00:00:53nicht noch einmal die gesamte DNS-Hierarchie durchlaufen werden muss,
00:00:56sondern die Antwort im Speicher behalten und direkt zurückgeliefert wird.
00:00:59Aber wie ich eingangs erwähnte,
00:01:01bedeuten 250 Milliarden Cache-Einträge, dass ein verschwendetes Byte pro Eintrag
00:01:04etwa 250 Gigabyte RAM kostet,
00:01:07und genau deshalb hatten diese fünf Änderungen einen so großen Einfluss.
00:01:10Als Erstes haben wir die Kosten für die Kapazität.
00:01:12So sah ein Cache-Eintrag früher aus.
00:01:14Zeitstempel, Gültigkeitsdauer (TTL), Zugriffs zählen,
00:01:16und dann eine Reihe von Vektoren.
00:01:17Ein Vektor mit Antwortdatensätzen,
00:01:18ein Vektor mit Autoritätsdatensätzen,
00:01:20ein Vektor mit zusätzlichen Datensätzen,
00:01:21ein Vektor für Fehler
00:01:22und noch vieles mehr.
00:01:23Für diejenigen unter euch, die Rust nicht kennen:
00:01:25Ein Vektor ist einfach der Standard-Datentyp für eine wachsende Liste
00:01:27und besteht im Speicher aus drei Dingen:
00:01:29einem Zeiger, einer Länge und einer Kapazität.
00:01:31Das ist der Platz, der für weiteres Wachstum reserviert ist,
00:01:32damit nicht jedes Mal neu zugewiesen werden muss, wenn man Elemente hinzufügt.
00:01:35Das ist sehr nützlich, wenn die Daten tatsächlich wachsen.
00:01:38Aber sie haben sich reale DNS-Cache-Einträge angesehen
00:01:40und festgestellt, dass alles nur in den Cache geschrieben
00:01:42und danach nie wieder angefasst wurde.
00:01:44Es wurde ausschließlich gelesen,
00:01:45sodass diese Vektoren niemals wuchsen.
00:01:47Das bedeutet, dass übermäßig zugewiesener Heap-Speicher
00:01:49einfach ungenutzt dalag.
00:01:50Wenn ein Vektor Platz für acht Elemente hat,
00:01:52aber nur fünf gespeichert sind,
00:01:53bleiben drei Plätze ungenutzt,
00:01:55und das Kapazitätsfeld selbst ist ein usize,
00:01:57was acht Byte Arbeitsspeicher entspricht,
00:01:58die für sie schlicht nicht benötigt werden.
00:02:00Die Lösung hierfür war unglaublich einfach:
00:02:02Man ersetzt den Vektor einfach durch eine Box,
00:02:04da die Größe einer Box fest ist und der Größe bei der Erstellung entspricht.
00:02:07Sie benötigt daher kein Kapazitätsfeld
00:02:08und speichert exakt das, was vorhanden ist.
00:02:10Es muss kein überschüssiger Platz für die Zukunft reserviert werden.
00:02:13Sie erkannten auch, dass sie genau dieselbe Einsparung
00:02:15auf die String-Felder anwenden konnten,
00:02:16da diese im Wesentlichen auch nur Vektoren aus u8 sind.
00:02:18Normalerweise kann Text wachsen oder schrumpfen,
00:02:20aber wenn das nicht nötig ist,
00:02:21kann man einfach einen Box-String-Slice verwenden,
00:02:23nach dem Motto: Dieser String wird sich nicht mehr ändern.
00:02:26Insgesamt gab es in einem Cache-Eintrag
00:02:27acht Vektor- und String-Felder,
00:02:29sodass das Ersetzen durch eine Box acht Byte pro Feld einsparte,
00:02:31also 64 Byte pro Eintrag.
00:02:33Zudem wurde der überschüssige Heap-Speicher eliminiert,
00:02:35den ein Vektor für zukünftiges Wachstum reserviert.
00:02:36Skaliert auf 250 Milliarden Cache-Einträge summierten sich die Einsparungen
00:02:39somit auf über 15 Terabyte.
00:02:42Und das alles durch eine einfache Änderung des Datentyps.
00:02:45Für unsere nächste Änderung
00:02:46haben sie sich einige dieser Listen angesehen
00:02:48und schlicht gefragt: Brauchen wir die überhaupt?
00:02:50Eine DNS-Antwort enthält drei Einträge:
00:02:52Answer, Authority und Additional.
00:02:54Wie wir vorhin gesehen haben,
00:02:55wurden diese beim Eintreffen zwischengespeichert,
00:02:57in drei getrennten Listen,
00:02:58Obwohl wir das Kapazitätsfeld in Änderung eins entfernt hatten,
00:03:01besteht jede Liste immer noch aus einem Zeiger und einer Länge,
00:03:03also 8 Byte plus 8 Byte, mal drei,
00:03:05was insgesamt 48 Byte ausmacht
00:03:07sowie drei separate Heap-Zuweisungen.
00:03:09Als sie sich jedoch ansahen, wie diese Listen
00:03:10tatsächlich verwendet wurden,
00:03:12merkten sie, dass sie immer zusammen gelesen,
00:03:13immer zusammen geschrieben
00:03:14und stets in exakt derselben Reihenfolge verarbeitet werden.
00:03:16Anstelle von drei Listen
00:03:17könnten sie es also auch als eine Liste mit zwei Trennzeichen behandeln.
00:03:20Aus den drei Listen wird also eine einzige Liste von Datensätzen
00:03:22plus zwei kleinen Offsets, die besagen: Antworten enden hier
00:03:25und Authority endet hier.
00:03:26und da eine DNS-Antwort niemals vier Milliarden Einträge haben wird,
00:03:29reicht für die Offsets eine 16-Bit-Ganzzahl ohne Vorzeichen,
00:03:33was gerade einmal zwei Byte pro Stück sind.
00:03:34Das bedeutet insgesamt,
00:03:35dass sie zwei vollständige 16-Byte-Header durch zwei 2-Byte-Zahlen ersetzt haben,
00:03:39wodurch 28 Byte pro Eintrag eingespart wurden.
00:03:41Dies erlaubt es Rust zudem, zusätzlichen Leerraum zwischen Feldern zu entfernen,
00:03:44wodurch die Struktur noch kleiner wird als nur durch die bloß entfernten Felder.
00:03:47Sie haben dieses Konzept sogar noch weitergeführt
00:03:49und mehrere Boolesche Felder zu einem einzigen Bit-Flag zusammengefasst.
00:03:52Kommen wir zu Änderung Nummer drei:
00:03:53Was wäre, wenn wir denselben Namen nicht mehr doppelt nennen?
00:03:55Jeder DNS-Datensatz besitzt einen Inhaber,
00:03:57also schlicht den Domainnamen, zu dem dieser Datensatz gehört.
00:04:00Wenn ihr also betterstack.com abfragt und eure Datensätze zurückerhaltet,
00:04:03steht auf jedem einzelnen betterstack.com drauf.
00:04:05Aber diese Information ist im Grunde redundant.
00:04:08Ihr seid schließlich die Person, die die Frage gestellt hat,
00:04:09der Domainname ist also bereits bekannt.
00:04:11Cloudflare speicherte diese Abfrage-Domain ohnehin bereits als Cache-Schlüssel,
00:04:14warum also mussten sie ihn noch einmal als Inhaber speichern?
00:04:17Nun, das mussten sie nicht.
00:04:17Cloudflare änderte dieses Feld daher in einen optionalen Box-Namen,
00:04:20sodass, wenn der Inhaber mit der Abfrage übereinstimmt,
00:04:22einfach none gespeichert wird,
00:04:23während er in den wenigen Fällen, in denen er wirklich abweicht,
00:04:24wie etwa bei CNAME-Chains,
00:04:27wie zuvor als Inhaber hinterlegt wird.
00:04:29Dadurch sparen sie in den Normalfällen, in denen sie übereinstimmen,
00:04:32eine ganze Heap-Zuweisung pro Datensatz ein.
00:04:34Diese Einsparung basierte schlicht darauf, sich anzusehen, welche Daten im Cache liegen,
00:04:37und zu erkennen, dass dort doppelte Werte vorhanden waren.
00:04:39Bei Einsparung Nummer vier haben wir die 144 Byte für die IP-Adresse.
00:04:43Ihre Datensatzdaten bestanden aus einem Rust-Enum aus A-Records, AAAA-Records, Text,
00:04:47SVCB und NAPTR – ein einziger Typ, der all das abdeckte.
00:04:51Aber hier liegt das Problem mit Enums:
00:04:52Sie sind immer so groß wie ihre größte Variante.
00:04:54Jeder Wert dieses Typs belegt exakt denselben Platz,
00:04:57egal ob er ihn benötigt oder nicht.
00:04:58In diesem Fall ist NAPTR der größte Wert mit 136 Bytes,
00:05:03und wenn man Tag und Padding zum Enum hinzurechnet, kommt man auf 144 Bytes.
00:05:07Vergleicht man das mit dem, was ein A-Record benötigt, nämlich eine einfache IPv4-Adresse,
00:05:12bräuchte diese nur vier Bytes.
00:05:13Das bedeutet, jeder A-Record in diesem Cache saß in einer 144-Byte-Box,
00:05:17obwohl er nur vier davon nutzte,
00:05:19und A- sowie 4A-Records machen den Großteil des echten Traffics aus.
00:05:22In Cloudflares Benchmark-Mix sind es 56 % A-Records und 25 % 4A, weshalb der Großteil des Caches nur aus Padding bestand.
00:05:29Die Lösung hierfür war unsere bewährte Box.
00:05:32Sie packten die großen Varianten in Boxen und ließen A und 4A inline, da diese klein und häufig sind,
00:05:36sodass text, SVCB und NAPTR nun hinter einem Zeiger liegen,
00:05:40sodass das Enum nur noch so groß sein muss wie die größte verbleibende Variante, nämlich die 16-Byte-IPv6-Adresse.
00:05:45Insgesamt werden dadurch bei einem A- oder 4A-Record jeweils 120 Bytes eingespart.
00:05:50Diese Änderung bringt jedoch tatsächlich einen Nachteil mit sich.
00:05:52Wenn man eine Variante in eine Box packt, wandert ihr Datensatz aus dem Cache-Eintrag heraus
00:05:55sondern landet irgendwo ganz woanders in einem eigenen Heap-Bereich,
00:05:58was zwei neue Kostenpunkte nach sich zieht.
00:06:00Der erste ist der Allokator.
00:06:02Cloudflare verwendet Jemalloc, und Jemalloc gibt dir nicht exakt die Anzahl Bytes, nach der du fragst,
00:06:06sondern fasst Allokationen in feste Bins zusammen und rundet zur nächsten Größe auf.
00:06:10Ein Text-Record, der 32 Bytes anfordert, landet also in einem 32-Byte-Bin und verschwendet nichts,
00:06:15aber ein MX-Record fragt nach 40, wird auf 48 Bytes aufgerundet und verliert klammheimlich 8 Bytes.
00:06:21Die zweiten Kosten betreffen die Lokalität.
00:06:22Vor dem Boxen befanden sich alle Datensätze eines Eintrags in einem zusammenhängenden Speicherblock,
00:06:27und nach dem Boxen lebt jeder an einem anderen Ort, sodass das Lesen das Folgen eines Zeigers erfordert,
00:06:31und wenn dieser Zeiger weit entfernt vom Rest des Eintrags landet,
00:06:34muss die CPU eine völlig neue Cache-Zeile abrufen, nur um ihn zu lesen.
00:06:37Das Boxen hat zwar das Padding-Problem gelöst, aber ein eigenes Problem geschaffen,
00:06:41und das war der Moment, in dem sich Cloudflare dachte: Was, wenn wir sie gar nicht als Rust-Typen speichern?
00:06:45Nun, das ist Änderung Nummer fünf.
00:06:46Cloudflare beschrieb dies als Mittelweg: Man behält den Rest des Cache-Eintrags als normal strukturierte
00:06:50Felder bei, nimmt aber die eigentlichen Record-Daten und speichert sie als Rohe Bytes.
00:06:54Anstelle eines Enums oder einer Box pro Record erhält der gesamte Eintrag ein einziges, boxed u8-Array,
00:07:00wobei jeder Record als ein 2-Byte-Längenpräfix gefolgt von seinen Daten geschrieben wird.
00:07:03Dadurch werden beide Kostenfaktoren rückgängig gemacht, über die wir gerade gesprochen haben.
00:07:06All diese separaten Box-Allokationen verschmelzen zu einer einzigen Allokation für alle Record-Daten,
00:07:10sodass kein Aufrunden auf ein Jemalloc-Bin pro Record mehr stattfindet
00:07:13und alles wieder zusammenhängend gepackt ist, wodurch man die Cache-Lokalität zurückerhält, die das Boxen weggenommen hatte.
00:07:18Und als zusätzlicher Bonus werden dadurch auch Lookups schneller.
00:07:21Zuvor befand sich bei jedem Cache-Treffer ein geparster Record im Speicher,
00:07:25und man musste ihn Feld für Feld wieder in das DNS-Wire-Format serialisieren, bevor man ihn irgendwohin senden konnte.
00:07:30Jetzt liegt es jedoch bereits im DNS-Wire-Format vor, sodass die meisten Record-Typen direkt aus dem Puffer
00:07:35in die ausgehende Nachricht kopiert werden.
00:07:37Die einzigen, die noch geparst werden müssen, sind Records, die Domainnamen enthalten,
00:07:40also CNAME, NS, MX und SOA,
00:07:43und das auch nur, weil die DNS-Namenkomprimierung bedeutet, dass diese Namen ohnehin neu geschrieben werden müssen.
00:07:47Diese letzte Änderung reduziert also den Speicher und entfernt jede Menge Arbeit aus dem kritischen Pfad.
00:07:51Das sind also 5 Low-Level-Änderungen am Cache,
00:07:54die in Kombination den Fußabdruck pro Eintrag von 953 Bytes auf 420 senken,
00:08:00was einer Verringerung um 56 % entspricht, während der pro Eintrag allokierte Speicher von 1,1 Kilobytes
00:08:05auf 461 Bytes sank, also eine Kürzung um 58 %.
00:08:09Darüber hinaus stieg auch ihr Einfüge-Durchsatz um 43 %,
00:08:12von 625.000 Einträgen pro Sekunde auf 893.000,
00:08:17und auch Lookups wurden 19 % schneller, runter von 828 Nanosekunden auf 670.
00:08:23Sie haben diese Änderungen dieses Jahr in der Produktion ausgerollt,
00:08:25und ihr P99-Resident-Speicher sank von 9,3 Gigabyte auf 5,3,
00:08:29was einer Reduzierung von 43 % im echten Traffic entspricht,
00:08:32was fleetweit etwa 100 Terabyte freigemachten Speicher bedeutet,
00:08:35oder der RAM-Menge von 130 Servern der 13. Generation.
00:08:38Nun planen sie, diesen zusätzlichen Platz für einen größeren Cache zu nutzen,
00:08:41um die Dinge noch weiter zu beschleunigen.
00:08:43Mir gefällt dieser Blogbeitrag wirklich sehr, weil er eine Entscheidung hervorhebt, die wir treffen müssen.
00:08:46Hätten wir uns vor dem Bau hinsetzen und all diese Optimierungen ausarbeiten sollen,
00:08:50oder wäre das eine verfrühte Optimierung gewesen?
00:08:52Und ich schätze, damals im Jahr 2018, als das gebaut wurde,
00:08:55waren die RAM-Kosten für Cloudflare nicht so signifikant wie heute.
00:08:59Der vollständige Beitrag ist ein großartiger Artikel, den ich unten verlinken werde.
00:09:02Lasst mich in den Kommentaren wissen, was ihr darüber denkt,
00:09:03abonniert den Kanal,
00:09:04und wie immer bis zum nächsten Mal.
00:09:05Bis zum nächsten Mal.

Key Takeaway

Fünf Low-Level-Optimierungen an Cloudflares DNS-Caching-System reduzieren den Speicherfußabdruck pro Eintrag um 56 Prozent und setzen fleetweit 100 Terabyte RAM frei.

Highlights

  • Cloudflare hat durch fünf Änderungen an seinem Caching-System 100 Terabyte Arbeitsspeicher freigesetzt.

  • Der öffentliche DNS-Resolver 1.1.1.1 speichert zu jedem Zeitpunkt 250 Milliarden DNS-Cache-Einträge.

  • Das Ersetzen von Vektoren durch Box-Datentypen spart 64 Byte pro Cache-Eintrag ein.

  • Die Zusammenfassung von drei DNS-Antwortlisten zu einer einzigen Liste reduziert Header-Größen drastisch.

  • Die Umstellung auf rohe Bytes für Datensatzdaten senkt den Speicherbedarf pro Eintrag von 953 auf 420 Bytes.

  • Durch die Optimierungen sank der P99-Resident-Speicher im echten Traffic um 43 Prozent.

Timeline

Problemstellung und Skalierung des DNS-Resolvers

  • Das Verschwenden eines einzigen Bytes pro Eintrag kostet Cloudflare über 250 Gigabyte Arbeitsspeicher.
  • Der in Rust geschriebene DNS-Resolver Big Pineapple verwaltet 250 Milliarden Cache-Einträge.
  • Die Zwischenspeicherung verhindert das wiederholte Durchlaufen der DNS-Hierarchie.

Bei der enormen Skalierung von Cloudflare hat jede ineffiziente Speichernutzung massive Auswirkungen auf das Gesamtsystem. Der öffentliche DNS-Resolver 1.1.1.1 sorgt dafür, dass wiederholte Abfragen direkt aus dem Speicher bedient werden. Wegen der riesigen Anzahl an gespeicherten Einträgen führt bereits ein verschwendetes Byte zu einem enormen Mehrbedarf an RAM.

Erste Optimierung: Beseitigung ungenutzter Vektor-Kapazitäten

  • Cache-Einträge enthielten Vektoren für Datensätze, die nach dem Schreiben nie wieder wuchsen.
  • Das Ersetzen von Vektoren durch feste Box-Typen entfernt ungenutzten Heap-Speicher und das Kapazitätsfeld.
  • Diese Änderung spart allein 64 Byte pro Eintrag und insgesamt über 15 Terabyte ein.

Da DNS-Cache-Einträge nach dem Schreiben ausschließlich gelesen werden, ist die für Vektoren reservierte Wachstumskapazität überflüssig. Das Kapazitätsfeld und der überschüssige Heap-Speicher belegen unnötig Arbeitsspeicher. Durch den Einsatz von Box-Datentypen wird exakt die benötigte Größe ohne Vorratsspeicherung zugewiesen.

Zweite und dritte Optimierung: Listen zusammenfassen und redundante Felder entfernen

  • Drei separate Antwortlisten wurden zu einer einzigen Liste mit kleinen Offsets zusammengelegt.
  • Das redundante Speichern des Domainnamens als Inhaber entfällt bei Übereinstimmung mit dem Abfrageschlüssel.
  • Die Reduzierung von Headern und optionalen Box-Namen spart weitere Heap-Zuweisungen pro Datensatz ein.

DNS-Antworten bestehen aus drei Teilen, die stets gemeinsam verarbeitet werden. Anstelle von drei separaten Listen mit eigenen Headern speichert eine einzige Liste die Daten kompakt ab. Zudem wird der Domainname nicht mehr doppelt hinterlegt, wenn er bereits als Cache-Schlüssel vorliegt.

Vierte und fünfte Optimierung: Enum-Padding beheben und rohe Bytes verwenden

  • Große Enum-Varianten erzeugten bei kleinen A- und AAAA-Records massives Padding im Speicher.
  • Die Speicherung der Record-Daten als rohes Byte-Array stellt die Cache-Lokalität wieder her.
  • Die meisten Record-Typen können nun ohne vorheriges Parsen direkt aus dem Puffer kopiert werden.

Enums richten sich nach ihrer größten Variante, wodurch kleine IP-Adressen viel ungenutzten Platz beanspruchten. Das Auslagern oder Umstellen auf ein boxed Byte-Array löst dieses Padding-Problem und verhindert unnötiges Aufrunden durch den Allokator. Gleichzeitig beschleunigt dies Lookups, da die Daten im DNS-Wire-Format vorliegen.

Gesamtergebnisse und Auswirkungen in der Produktion

  • Der Speicherfußabdruck pro Eintrag sank von 953 auf 420 Bytes.
  • Der Einfüge-Durchsatz stieg um 43 Prozent und Lookups wurden 19 Prozent schneller.
  • Der P99-Resident-Speicher sank im echten Traffic um 43 Prozent, wodurch 100 Terabyte RAM frei wurden.

Die Kombination aller fünf Low-Level-Änderungen führt zu massiven Effizienzsteigerungen im gesamten Netzwerk. Der freigesetzte Arbeitsspeicher entspricht der RAM-Menge von 130 Servern und wird für einen größeren Cache genutzt. Diese Optimierungen zeigen, wie wichtig genaue Speicheranalysen bei großskalierten Systemen sind.

Community Posts

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

Write about this video