Valkey: Speicherbedarf pro Schlüssel sinkt von 7.2.14 auf 9.1.1 um 37,5 %
Perconas Lucas Yang hat einen Benchmark veröffentlicht, der misst, wie viel Speicher jede Valkey-Version bei kleinen String-Schlüsseln tatsächlich einspart. Das Hauptergebnis: Der Speicherbedarf pro Schlüssel sinkt zwischen Valkey 7.2.14 und 9.1.1 um 37,5 %. Valkey ist ein quelloffener In-Memory-Datenspeicher für Caching, Sessions, Echtzeitanwendungen, E-Commerce, Adtech und Gaming. Dieselbe Arbeitslast, die unter 7.2.14 durchschnittlich 102,48 Byte pro Schlüssel benötigte, verbraucht unter 9.1.1 nur noch 64,02 Byte pro Schlüssel. Das bedeutet, dass dieselbe Hardware nach dem Upgrade rund 60 % mehr dieser kleinen Schlüssel speichern kann. Die Reduzierung ist besonders nützlich für stringlastige Umgebungen, wie sie bei Caching, Sessions und Authentifizierung, Echtzeitanwendungen, E-Commerce, Adtech und Martech sowie Gaming üblich sind.
Um den Einfluss der Version zu isolieren, testete Yang fünf aus dem Quellcode erstellte Versionen: 7.2.14, 8.0.10, 8.1.9, 9.0.5 und 9.1.1. Bei jedem Lauf wurde jemalloc 5.3.0 als Speicherallokator verwendet, da unterschiedliche Allokatoren den Speicherverbrauch unabhängig von Valkey selbst verändern können. Die Instanzen liefen im Standalone-Modus statt im Cluster-Modus, mit maxmemory 0 sowie deaktivierter Persistenz und Eviction durch save "" und appendonly no. Die Arbeitslast wurde mit valkey-benchmark -t set -n 1000000 -r 1000000 -d 16 erzeugt, also eine Million SET-Befehle mit 16-Byte-Werten. Da der Schlüsselraum ebenfalls eine Million beträgt, führen Kollisionen bei zufälligen Schlüsseln dazu, dass etwa 63,2 % der Operationen eindeutige Schlüssel sind – rund 632.000 Schlüssel –, was der offiziellen Valkey-Benchmark-Methodik entspricht. Der Basisspeicher einer leeren Instanz wurde von jeder Messung abgezogen, sodass die Zahlen den tatsächlichen marginalen Speicherbedarf pro Schlüssel zeigen.
Die Ergebnisse zeigen, dass der größte einzelne Sprung in Version 8.1 erfolgte. Unter 7.2.14 benötigte die Arbeitslast 61,78 MB für 632.181 eindeutige Schlüssel, also 102,48 Byte pro Schlüssel. Version 8.0.10 reduzierte das auf 57,00 MB und 94,49 Byte pro Schlüssel – ein Rückgang von 7,8 %. Version 8.1.9 senkte den Speicherbedarf dann auf 43,43 MB und 71,98 Byte pro Schlüssel, eine weitere Reduzierung um 23,8 % gegenüber 8.0 und eine kumulative Einsparung von 29,8 % gegenüber 7.2.14. Version 9.0.5 blieb mit 43,36 MB und 71,99 Byte pro Schlüssel praktisch unverändert (0,0 % Änderung gegenüber 8.1), während 9.1.1 auf 38,58 MB und 64,02 Byte pro Schlüssel sank – nochmals 11,1 % weniger und 37,5 % weniger als die 7.2.14-Basis. Für eine 1-TB-7.2-Bereitstellung würden dieselben Daten auf 8.0 etwa 800 GB, auf 8.1 etwa 640 GB und auf 9.1 etwa 630 GB belegen.
Die erste strukturelle Änderung erschien in Valkey 8.0. In 7.2 war ein Wörterbucheintrag (dictEntry) eine 24-Byte-Struktur mit drei Zeigern: einem auf den Schlüssel, einem auf den Wert und einem auf den nächsten Eintrag in einer Hash-Kollisionskette. Der Schlüsselzeiger verwies auf einen separat allozierten SDS-Blob (Simple Dynamic String), sodass jeder Lookup eine zusätzliche Speicherallokation und einen zusätzlichen Speichersprung nur zum Lesen des Schlüssels erforderte. Version 8.0 führte embeddedDictEntry ein, das die Schlüsselbytes direkt nach der Struktur inline platziert. Das neue Layout umfasst 16 Byte plus Schlüssellänge und fasst Schlüssel und Eintrag in einer einzigen Allokation zusammen. Der gemessene Effekt sind 8 Byte weniger pro Schlüssel, ein Speichersprung weniger bei jedem Lookup, und die Änderung erfolgt automatisch beim Upgrade ohne Konfigurationsänderungen.
Valkey 8.1 lieferte den größten einzelnen Gewinn. In 8.0 war das Wörterbuch eine klassische verkettete Hashtabelle, bei der der Zugriff auf einen Wert einem Pfad von Bucket zu Eintrag zu Schlüssel zu Wert folgte – vier Speichersprünge plus weitere bei Kollisionen. Version 8.1 hat dies als neue Hashtabellenstruktur neu geschrieben, bei der ein Bucket genau 64 Byte groß ist, die Größe einer CPU-Cache-Zeile (der kleine Speicherblock, den ein Prozessor auf einmal lädt). Der Bucket verfügt über einen 8-Byte-Metadatenbereich, der ein Has-Child-Bucket-Bit, sieben Filled-Bits und sieben Byte Sekundärhash packt. Dadurch können die meisten nicht passenden Kandidaten ohne zusätzlichen Speicherzugriff übersprungen werden. Schlüssel und Wert sind im selben serverObject eingebettet, wodurch sich der Lookup auf etwa zwei Sprünge verkürzt – Bucket zu serverObject – und eine Hash-Kollision meist innerhalb derselben Cache-Zeile bleibt. Die dictEntry-Struktur entfällt vollständig. Das Ergebnis sind etwa 20 Byte weniger pro Schlüssel-Wert-Paar und 30 Byte weniger für Schlüssel mit TTL. Dies erklärt den großen Rückgang von 8.0 auf 8.1.
Der letzte gemessene Gewinn erfolgte in 9.1. In früheren Valkey-Versionen verschwendeten eingebettete Strings (embstr) noch ein 8-Byte-Zeigerfeld, das lediglich auf die zusammenhängende Nutzlast verwies. Valkey 9.1 änderte dies für kurze Strings: Die String-Bytes werden direkt inline in denselben 8 Bytes gespeichert, die normalerweise für den Zeiger verwendet werden. Es wird kein Zeiger benötigt, und es erfolgt keine separate Allokation für den Wert. Das Ergebnis ist eine Speicherreduzierung von bis zu 20 % für Strings unter 128 Byte, und die Schlüssel-plus-16-Byte-Wert-Paare des Benchmarks liegen in diesem Bereich. Zusammengenommen zeigen die drei strukturellen Änderungen, dass Valkeys Speichereffizienz aus gezielten Low-Level-Umstrukturierungen resultierte und nicht aus einer einzigen umfassenden Neufassung.
Über die Einsparungen zwischen den Versionen hinaus verglich Yang auch zwei Möglichkeiten, dieselben Daten zu modellieren. Ansatz A speichert jedes Benutzerattribut als separaten String-Schlüssel, z. B. SET user:1:name Alice, SET user:1:age 30, SET user:1:city Toronto und SET user:1:score 100. Ansatz B speichert einen Hash-Schlüssel pro Benutzer mit vier Feldern, z. B. HSET user:1 name Alice age 30 city Toronto score 100. Für eine Million Datensätze benötigte Ansatz A 220 MB, Ansatz B 100 MB – eine Reduzierung um 54 %. Der Unterschied besteht, weil jeder Schlüssel einen festen Overhead pro Schlüssel verursacht. Vier separate Schlüssel zahlen diesen Overhead viermal, während ein Hash-Schlüssel ihn einmal zahlt. Die Wahl ist jedoch nicht kostenlos. String-Schlüssel unterstützen nativ EXPIRE für TTL pro Schlüssel, während Hash vor Valkey 7.4 kein TTL auf Feldebene nativ unterstützte. Hash gewinnt bei atomaren Operationen über mehrere Felder: Ein einzelnes HSET oder HMGET kann mehrere Felder gleichzeitig lesen oder schreiben, während String mehrere Roundtrips benötigt. Hash hält außerdem alle Felder automatisch in einem Slot für Cluster-Lokalität, während String einen manuellen Hashtag benötigt, um zusammengehörige Schlüssel zu colokalisieren. Es gibt keine allgemeingültige Antwort; es hängt davon ab, ob Speichereffizienz oder Flexibilität pro Feld für den Anwendungsfall wichtiger ist.
Insgesamt quantifizieren die Percona-Tests, wie Valkeys Speicherbedarf für kleine String-Schlüssel über fünf Versionen von 102,48 Byte auf 64,02 Byte pro Schlüssel gesunken ist, mit dem größten Schritt in 8.1 und einer abschließenden Inline-String-Optimierung in 9.1. Für Teams, die Valkey in großem Umfang betreiben, kann ein verwalteter europäischer Cloud- und Dateninfrastrukturanbieter wie AEU Data das Monitoring, automatisierte Backups und die Hochverfügbarkeit bereitstellen, die es sicherer machen, diese Speicherreduzierungen in der Produktion auszurollen und zu verifizieren.