
Von ClickHouse zu RocksDB: Wie wir Solanas Archivschicht neu aufgebaut haben
Inhaltsverzeichnis
- Was Solana-Archivierung ist und warum sie wichtig ist
- ClickHouse: Die pragmatische erste Wahl
- Wo ClickHouse an seine Grenzen stieß
- RocksDB: Keine Datenbank
- Wie RocksDB die Schwächen von ClickHouse behebt
- Was wir darauf aufgebaut haben
- RocksDB für hohe Skalierung optimieren
- Optimiere pro Index, nicht pro Datenbank
- Entscheide, ob du WAL wirklich brauchst
- Stimme Bloom-Filter auf dein Treffer-/Fehlschlagverhältnis ab
- Komprimiere, was komprimierbar ist, nicht was häufig abgefragt wird
- Erwäge direktes I/O
- Wähle einen Cache, der hoher Konkurrenz standhält
- Parallelisiere Multi-Key-Lookups, statt sie zu serialisieren
- Worauf wir hinarbeiten
Wenn wir erzählen, dass wir über 300 Terabyte an Solana-Archivdaten von ClickHouse zu RocksDB verschoben haben, ist die Reaktion fast immer dieselbe: Warum solltet ihr das jemals tun?
ClickHouse ist die offensichtliche Wahl für analytische Workloads im Petabyte-Bereich, RocksDB dagegen nicht.
Einige der weltweit größten Datenverarbeiter vertrauen auf ClickHouse.
Cloudflare betreibt es beispielsweise mit mehr als tausend Replikaten, um Hunderte Millionen Inserts pro Sekunde zu verarbeiten.
Anthropic betreibt ein eigenes, vom Netz isoliertes ClickHouse-Deployment für Claudes Observability. Auch Uber, eBay und Bloomberg setzen es seit Jahren produktiv ein.
RocksDB ist dagegen ein eingebetteter Key-Value-Store mit spärlicher Dokumentation. Er dient hauptsächlich als Engine für andere Datenbanken wie CockroachDB, TiKV und MyRocks, nicht als direkte Grundlage für einen kundenorientierten Dienst mit historischen Daten.
Der Wechsel reduzierte jedoch unseren komprimierten Speicherbedarf von ~330 TB auf ~190 TB, beseitigte den Long Tail unserer langsamsten Abfragen (z. B. sank die p95-Latenz für getTransactionsForAddress-Aufrufe von 350 ms auf 30 ms) und versetzte uns in die Lage, Solanas Leseschicht weiterzuentwickeln. Mit ClickHouse wäre das bei der von Helius benötigten Performance und Skalierung nicht möglich gewesen.
Dieser Artikel erklärt, warum wir von ClickHouse migriert sind, was wir dabei über den Workload gelernt haben und wie wir RocksDB in großem Maßstab produktiv einsetzen.
Was Solana-Archivierung ist und warum sie wichtig ist
Im Kern ist Solana ein Netzwerk aus Nodes, die miteinander kommunizieren, um sich auf neue Informationen zu einigen, ohne einander vertrauen zu müssen.
Ein Node ist ein Computer im Solana-Netzwerk, auf dem ein Client wie Agave oder Firedancer läuft. Dieser folgt bestimmten Regeln und unterstützt so die Einigung über neue Informationen.
Ein Validator ist ein Solana-Node, der das Netzwerk absichert, indem er Blöcke produziert, also Transaktionsgruppen an Solanas Ledger anhängt, und über die Gültigkeit anderer Blöcke abstimmt.
RPC-Nodes sind Validatoren, die weder Blöcke produzieren noch abstimmen. Stattdessen beobachten sie das Netzwerk und verfolgen alle neu erzeugten Informationen. Über RPCs können Nutzer mit der JSON-RPC-Spezifikation bestimmte Daten aus dem Netzwerk abfragen.
Diese Nodes speichern die Daten jedoch nicht für immer.
Damit Solanas Hardwareanforderungen eingehalten werden, entfernt das System ältere Blöcke, Transaktionen und Account-Zustände. Nodes behalten dadurch nur die neueste Ansicht von Solanas Historie.
Das wird zum Problem, wenn du eine Transaktionssignatur von vor einem Jahr suchen, sämtliche Signaturen eines Wallets über dessen gesamte Lebensdauer abrufen oder die vollständige Ausführungshistorie eines Programms durchsuchen willst.
Archivierung bezeichnet allgemein die gesamte Schicht, die Solanas Daten seit dem Genesis-Block speichert. Sie protokolliert und indexiert alles, was die Chain erzeugt – jeden Block, jede Transaktion, jede Account-Interaktion, jeden Cross Program Invocation (CPI) und jedes Transaktionsprotokoll – und hält diese Daten über das übliche kurzfristige Speicherfenster von ~2 Tagen, also 1 Epoche, hinaus abfragbar.
Erst die Archivierung ermöglicht historische Abfragen.
Der Standardansatz für die Speicherung von Archivdaten auf Solana war bisher Google BigTable. Anza betreibt eine BigTable-Instanz, auf die RPC-Anbieter bei Bedarf für historische Abfragen zugreifen können.
Das funktioniert, aber BigTable ist teuer. Egress-Kosten machen den Betrieb einer eigenen Kopie schmerzhaft, und du kannst technisch kaum etwas tun, um sie zu beschleunigen. Du bist Googles Speicher ausgeliefert – samt Kostenstruktur und ohne Kontrolle.
Old Faithful ist eine von Triton One gepflegte Tool-Sammlung. Sie kann Content Addressable Archives (CARs) aus RocksDB-Ledgerarchiven erstellen und über Solanas standardmäßige RPC- und gRPC-Schnittstellen bereitstellen. Damit ist sie ein wichtiger Schritt zur Dezentralisierung von Solanas Archivschicht und eine wertvolle Quelle für redundante, überprüfbare historische Daten.
Die Kompromisse bei Developer Experience und Performance machen sie jedoch zu keiner echten Alternative für latenzkritische Workloads. Der Einstieg erfordert beispielsweise eigene Tools statt Schnittstellen, gegen die Teams entwickeln können. Zudem ist sie eher für dauerhafte Archivierung als für Performance und Skalierung optimiert.
Das Kernproblem der Archivierung sind N Petabyte Rohdaten. Wie erreichst du bei über 500 Milliarden Transaktionen, ~1,3 Billionen Zeilen in Account-zu-Transaktion-Indizes und zufälligen Zugriffsmustern Lookups unter 10 ms? Wie sehen End-to-End-Abfragen unter 50 ms aus? Weder Google BigTable noch Old Faithful bieten dafür eine Lösung.
Wenn du außerdem eigene Filter- und Sortieroptionen entwickeln oder die Developer Experience mit mehr als den standardmäßigen JSON-RPC-Methoden verbessern willst, brauchst du einen eigenen Archivindex.
Also haben wir einen gebaut.
ClickHouse: Die pragmatische erste Wahl
ClickHouse war der pragmatischste Ausgangspunkt für unser neues Archivsystem. Damit konnten wir am schnellsten ein Produkt veröffentlichen, das bereits besser war als eine auf BigTable basierende Lösung.
ClickHouse ist eine ausgereifte spaltenorientierte Datenbank mit hervorragenden Tools und guter Dokumentation. Sie komprimiert Zeitreihendaten effizient. Das ist für Solana besonders nützlich, weil die Blockdaten grundsätzlich zeitlich geordnet sind.
Eine Datenbank lässt sich unkompliziert erstellen, mit SQL abfragen, durch Schemaänderungen weiterentwickeln und für eine kurze Markteinführungszeit optimieren. So erhalten Kunden relativ schnell ein nutzbares Produkt.
ClickHouse verarbeitete den Traffic nicht allein. Es bildete vielmehr die unterste Ebene unseres Speicher-Stacks, den wir nach dem Alter der Daten organisiert hatten.
Die neuesten Daten, also ungefähr die letzten ein bis zwei Minuten oder einige Hundert Slots, lagen in einem In-Memory-Store. Postgres enthielt ungefähr die Daten der letzten zwei Wochen. ClickHouse speicherte den Rest von Solanas Historie.
Vor den drei Quellen saß ein intelligenter Router. Er leitete punktuelle Lookups an die aktuellste Ebene mit den jeweiligen Daten weiter. Bereichsabfragen teilte er auf die Ebenen auf und setzte die Antworten wieder zu einem einzigen Ergebnis zusammen.
Für die vorgesehenen Workloads funktionierte ClickHouse relativ gut. Dazu gehörten beispielsweise „Gib mir jeden Block in diesem Slot-Bereich“ oder „Durchsuche die Aktivitäten dieses Accounts im Zeitverlauf“.
Zeitreihenabfragen waren kein Problem. Wir konnten Backfills ausführen, Ad-hoc-Analysen durchführen und die meisten Fragen beantworten, die RPC-Anbieter traditionell hatten. Auch die Ingestion kam nie ins Schwitzen: Über LaserStream schrieben wir lediglich ~10 MB/s in unseren Index.
Der Fehler war nicht, dass wir ClickHouse ausgewählt hatten. Der Fehler war die Annahme, unser Workload würde in einer Form bleiben, die ClickHouse auch in großem Maßstab bewältigen kann.
Wo ClickHouse an seine Grenzen stieß
Nicht alle für uns wichtigen historischen Solana-Methoden basieren auf Zeitreihen. Eine Signatur auf Solana, also der universelle Transaktionsbezeichner, besteht im Wesentlichen aus 64 Byte Zufallsdaten.
Wenn jemand getTransaction für eine bestimmte Signatur aufruft, ist das ein gleichmäßig zufälliger punktueller Lookup. Es gibt weder einen Slot noch einen Zeitraum oder irgendeine andere Form von Lokalität, die sich nutzen ließe.
Dasselbe gilt für andere Abfragen wie „Rufe alle Signaturen ab, die diesen Account berührt haben“, weil auch der Account-Schlüssel praktisch zufällig ist. Dieses Problem tritt immer dann auf, wenn ein Primärschlüssel eine UUID, ein Hash oder ein anderer Bezeichner mit hoher Entropie ist.
Spaltenorientierte Engines sind nicht für dieses Problem ausgelegt.
ClickHouse speichert Daten in sortierten Parts und verwendet dünn besetzte Primärindizes. Bei einem Lookup mit zufälligem Schlüssel lässt sich daher kaum etwas ausschließen. Irgendwann liest du mehr Granules als gewünscht. Ein einzelner Signatur-Lookup konnte 20 Granules betreffen – also etwa 10.000 Zeilen und ~60 Disk-I/Os –, noch bevor Kosten für Binärsuche oder CPU anfielen.
Allein die I/Os benötigen für einen Lookup ungefähr 5 ms. Da der Store spaltenorientiert ist, müssen beim Lesen einer Transaktion ~15 Spalten als separate I/Os über ein Granule mit 512 Zeilen gelesen werden.
Ein getTransactionsForAddress-Aufruf, der beispielsweise vollständige Transaktionsdetails anfordert, verzweigt sich dadurch in bis zu 100 solcher Lookups. Das hebt die minimale Latenz auf fast 100 ms, bevor auch nur ein Byte serialisiert wurde.
Ein großer Batch von getTransaction mit bis zu 1.000 Einträgen erhöhte diese Untergrenze auf fast eine volle Sekunde.
Die Abstraktionen, die Scans beschleunigen, etwa vektorisierte Ausführung und späte Materialisierung, lösen dieses Problem nicht vollständig. Die Lookups waren bereits so stark optimiert, wie es uns möglich war. Es fehlte kein Index. Es gab keine Projektion, die wir hätten hinzufügen können.
Kurz gesagt: Das grundlegende Datenlayout war für unsere Anforderungen falsch.
Die aufwendigsten von uns hinzugefügten Methoden, etwa getTransactionsForAddress und getTransfersByAddress, verwendeten genau jene Muster mit zufälligen Schlüsseln, die ClickHouse am schlechtesten verarbeitete.
Einige dieser Abfragen dauerten 2–3 Sekunden, obwohl sie nur wenige Millisekunden hätten benötigen dürfen. Wir erhielten Alarme wegen Performance-Einbrüchen bei Workloads, die wir grundsätzlich nicht beheben konnten.
Da Institutionen und Enterprise-Kunden Helius in großem Maßstab einsetzen, ist das langfristig völlig inakzeptabel.
ClickHouse für diesen Workload zu skalieren bedeutete, die Anzahl der Server zu erhöhen. Bei unserer Größenordnung vervielfachte das den Workload. Mehr Geld allein in Hardware zu stecken, hätte das Problem nicht gelöst.
Solanas RPC-Stack wird immer ausgereifter. Das Ökosystem hat einen Wendepunkt erreicht, an dem die standardmäßigen JSON-RPC-Methoden nicht mehr ausreichen. Kunden verlangen umfangreiche historische Abfragen, und niemand sonst würde sie auf BigTable entwickeln.
Wenn wir diese Grenze verschieben wollten, musste sich die Speicherschicht ändern.
RocksDB: Keine Datenbank
Trotz ihres Namens ist RocksDB keine Datenbank. Sie ist eine Bibliothek.
Es gibt weder Abfragesprache noch Client-Server-Protokoll, SQL-Engine, Joins oder Indizes im traditionellen Datenbanksinn. Die Schnittstelle sagt lediglich: „Hier ist ein Schlüssel aus einigen Bytes und hier ein Wert, ebenfalls aus einigen Bytes. Speichere ihn dauerhaft auf dem Datenträger und gib mir später den Wert für diesen Schlüssel zurück.“ Das ist alles.
Sie ist ein Grundbaustein. Man könnte sagen: der Grundbaustein für Speicher-Engines.
Alles, was du von einer traditionellen Datenbank erwartest, etwa Abfrageplaner, Wire-Protokoll, Replikation und Observability, musst du selbst entwickeln.
Das klingt zunächst nach einem Nachteil, bis du verstehst, was du dadurch bekommst. Ein reiner Key-Value-Store auf Basis eines Log-Structured-Merge-Baums (LSM) auf dem Datenträger hat genau die richtige Struktur für gleichmäßig zufällige Schlüssel-Lookups mit hohem Durchsatz. Es gibt keinen Abfrageplaner, der zusätzlichen Overhead verursacht, keine Annahmen eines spaltenorientierten Layouts, die berücksichtigt werden müssen, und kein SQL-Frontend, für das Ressourcen eingeplant werden müssen.
Du speicherst Bytes, liest sie wieder aus und platzierst Bloom-Filter und Caches an den richtigen Stellen, damit zufällige Lesezugriffe günstig bleiben.
Genau hier fällt die Einordnung als Anti-Pattern auseinander.
Wir haben ClickHouse nicht durch RocksDB ersetzt. Wir haben ClickHouse durch eine eigene Datenbank ersetzt, deren Speicher-Engine RocksDB ist. Das ist ein wesentlicher Unterschied.
Es ist außerdem dasselbe Muster, das die meisten produktiven Datenbanken intern verwenden. CockroachDB, TiKV, MyRocks und die State Stores von Kafka Streams basieren beispielsweise alle auf RocksDB.
Der Unterschied besteht darin, dass sie zunächst eine weitere Datenbank darumlegen. Wir haben die Datenbank dagegen selbst um RocksDB herum entwickelt und exakt auf die Zugriffsmuster der Archivierung abgestimmt.
Wie RocksDB die Schwächen von ClickHouse behebt
Wie löst ein Key-Value-Store also genau das Problem zufälliger Schlüssel, an dem eine spaltenorientierte Engine scheiterte? Entscheidend ist, wie die Bytes auf dem Datenträger gespeichert werden.
RocksDB verwendet Level-Compaction. Neue Schreibvorgänge landen in Level 0, einer unsortierten Ablage. Das entspricht praktisch derselben Situation wie bei ClickHouse, wo Parts innerhalb einer Partition nicht global sortiert sind.
Die Level 1 bis N sind dagegen vollständig sortierte Runs. Sobald die Daten kompaktiert sind, können Min-/Max-Metadaten die Suche selbst bei gleichmäßig zufälligen Schlüsseln tatsächlich eingrenzen, weil das Level global geordnet ist. In gewisser Weise verhält sich alles in ClickHouse wie Level 0 von RocksDB.
Das nutzen wir für unsere Backfills.
Beim Aufbau des Signaturindex sortieren wir die gesamte Historie vorab und laden sie direkt in das unterste Level. Danach landen nur noch neue Daten in Level 0 und werden schnell nach unten zusammengeführt.
Dadurch liest fast jeder Lookup aus einem einzigen sortierten Run. Mit RocksDB haben wir zufällige Daten effektiv sortiert.
Was wir darauf aufgebaut haben
ClickHouse liefert vieles direkt mit: eine Abfrage-Engine, ein Wire-Protokoll, einen Client und eine Möglichkeit, Daten bereitzustellen. RocksDB bietet nichts davon, sondern speichert lediglich Bytes dauerhaft. Alles andere mussten wir selbst entwickeln.
Also haben wir auf RocksDB unsere eigene Datenbank aufgebaut, die auf die Zugriffsmuster der Solana-Archivierung spezialisiert ist.
Heute liegen zwei Indizes in RocksDB:
- Signatur -> Speicherort, also der Signaturindex, der die 64-Byte-Signatur einer Transaktion dem Slot und der Position innerhalb des Blocks zuordnet.
- Slot -> Block, also der Slot-zu-Block-Index, der den obigen Speicherort den Transaktionsdaten zuordnet.
Wichtig ist, dass es keinen direkten Pfad von einer Signatur zu ihren Transaktionsdaten gibt. Genau deshalb existieren diese beiden Indizes.
Eine Signatur ist im Wesentlichen ein zufälliger 64-Byte-Bezeichner. Wo eine Transaktion tatsächlich liegt, bestimmen ihr Slot und ihr Index innerhalb dieses Blocks. Das Abrufen einer Transaktion erfordert daher zwei Schritte: einen, um die Signatur einem bestimmten Speicherort zuzuordnen, und einen weiteren, um die Transaktionsdetails dort abzurufen. Das Abrufen eines Blocks benötigt dagegen nur einen Schritt, weil der Aufrufer den Slot bereits übermittelt.
Gemeinsam unterstützen diese Indizes einige der aufwendigsten Methoden von Helius, etwa getBlock, getTransaction und getTransactionsForAddress, insbesondere wenn details auf full gesetzt sind.
Der Lesepfad ähnelt stark dem in ClickHouse. Eine Anfrage trifft ein, unser interner Client führt einen Datenbankaufruf aus und das Ergebnis kommt zurück. Geändert hat sich die darunterliegende Engine. Jede Methode folgt einem hartcodierten, manuell optimierten Pfad. Fragt ein Nutzer eine Transaktion ab, führt ein speziell entwickelter getTransaction-Pfad direkt zu den Bytes.
Dieser Pfad ist schnell, weil wir jede Ebene kontrollieren. Wir verwenden io_uring für Datei- und Netzwerk-I/O, um Daten aus RocksDB zu streamen. Wenn Daten unkomprimiert auf dem Datenträger liegen, kopieren wir sie direkt vom Datenträger auf die Netzwerkkarte, ohne Umwege durch den Userspace.
Die Methoden sind hartcodiert, das I/O ist durchgängig optimiert und dazwischen gibt es keine unnötigen Komponenten.
Der Nutzen zeigt sich deutlich in der Produktion.
Vor Kurzem haben wir fünf bis sechs Stunden am Stück 150 Gbit/s Traffic für getBlock bewältigt und dabei die meisten unserer Netzwerkkarten ausgelastet. Keine Probleme, keine Alarme, dieselbe rohe Performance.
Über alle Bereiche hinweg gilt:
- Der komprimierte Speicherbedarf sank ungefähr um die Hälfte, von ~330 TB auf ~190 TB
- Die p95-Latenz für
getTransaction-Aufrufe sank von 7 ms auf 1 ms - Die p95-Latenz für
getTransactionsForAddress-Aufrufe sank von 350 ms auf 30 ms - Die p95-Latenz für
getBlock-Aufrufe sank von 50 ms auf 35 ms
Wie erwartet verbesserten sich getBlock-Aufrufe am wenigsten. ClickHouse speichert Transaktionen bereits in Blöcken mit 512 Zeilen. Dadurch amortisiert sich der Nachteil der Spaltenorientierung bei Lesevorgängen in Blockgröße. Ein großer Teil der End-to-End-Zeit von getBlock entfällt auf Base58- und JSON-Codierung sowie das erneute Zusammensetzen des Blocks. Ein Austausch der Speicher-Engine beschleunigt diesen Prozess daher nicht.
Die tiefergehende Geschichte darüber, wie wir io_uring, asynchrones Rust und eine synchrone Bibliothek wie RocksDB unter einem Netzwerk-Workload mit hohem Durchsatz zusammenarbeiten ließen, verdient einen eigenen zukünftigen Beitrag.
Der folgende Abschnitt behandelt jedoch einige Optimierungen, die sich bei unserer Arbeit mit RocksDB als hilfreich erwiesen haben.
RocksDB für hohe Skalierung optimieren
Es gibt keine einzelne „schnelle“ RocksDB-Konfiguration. Die richtigen Einstellungen hängen vollständig vom Zugriffsmuster des jeweiligen Index ab.
Unser Index Signatur -> Speicherort und unser Index Slot -> Block laufen im selben Prozess auf derselben Hardware. Bei ihrer Optimierung verfolgen sie jedoch fast vollständig entgegengesetzte Ziele.
Statt dir eine Konfigurationsdatei mit Einstellungen zu geben, die sich wahrscheinlich ohnehin nicht auf deinen Workload übertragen lassen, erklärt dieser Abschnitt daher, wie wir über die übertragbaren Kompromisse nachdenken.
Optimiere pro Index, nicht pro Datenbank
Jeder unserer Indizes ist eine eigene Column Family mit eigenen Optionen. Einer verarbeitet gleichmäßig zufällige punktuelle Lookups, der andere große, komprimierbare Werte, die der Reihe nach abgerufen werden. Beide gleich zu behandeln, hätte viel Optimierungspotenzial verschenkt. Fast jede der folgenden Entscheidungen solltest du als „Führe für dieses Zugriffsmuster X aus“ lesen.
Entscheide, ob du WAL wirklich brauchst
Archivdaten lassen sich neu aufbauen. Sie werden über LaserStream eingestreamt und aus der Chain selbst abgeleitet.
Wir schreiben mit deaktiviertem Write Ahead Log (WAL), damit wir nicht für eine Write-Ahead-Log-Persistenz bezahlen, die wir nicht benötigen.
Die Feinheit besteht darin, dass das Deaktivieren des WAL auch RocksDBs standardmäßige Crash-Konsistenz über Column Families hinweg ausschaltet. Die Konsistenz muss daher auf andere Weise wiederhergestellt werden. Die Lektion lautet: Die Einstellungen für Persistenz sollten zur Wiederherstellbarkeit deiner Daten passen. Daten, die sich aus einer vorgelagerten Quelle neu aufbauen lassen, stellen völlig andere Anforderungen als ein führendes Datensystem.
Stimme Bloom-Filter auf dein Treffer-/Fehlschlagverhältnis ab
Bloom-Filter rechtfertigen ihren Speicherbedarf, indem sie kostengünstig erkennen, dass ein Schlüssel bei einem Lookup nicht vorhanden ist. Sie helfen also nur bei Fehlschlägen.
Ein Signatur-Lookup ist praktisch immer ein Treffer, weil der Aufrufer eine Signatur hat und die zugehörigen Transaktionsdaten abrufen will.
Das unterste LSM-Level enthält den überwiegenden Großteil unserer Daten. Wenn sich der Großteil deiner Daten in einem einzigen Level befindet und dein Workload von Treffern dominiert wird, verbrauchen die Filter auf diesem Level den meisten Speicher, leisten aber am wenigsten. In diesem Fall solltest du hinterfragen, ob du sie dort überhaupt brauchst.
Komprimiere, was komprimierbar ist, nicht was häufig abgefragt wird
Die Komprimierung wird pro Index entschieden. Daten mit hoher Entropie, etwa eine 64-Byte-Signatur, lassen sich nicht auf ein Verhältnis unter 1,0 komprimieren. Komprimierung würde dem häufig genutzten Pfad jedes einzelnen Lookups daher nur Dekomprimierungskosten hinzufügen. Deshalb setzen wir die Komprimierung auf None.
Blockdaten lassen sich dagegen gut komprimieren, weil sie umfangreicher sind und mehr Wiederholungen enthalten. Deshalb verwendet unser Index Slot -> Block zstd. Beachte, dass beide Indizes dieselbe Datenbank verwenden, aber von unterschiedlichen Entscheidungen profitieren. Maßgeblich ist ausschließlich, ob die Bytes komprimierbar sind und ob Latenz oder Speicherbedarf den Index begrenzt.
Diese Trennung pro Index hat wesentlich dazu beigetragen, dass wir unseren Speicherbedarf von ~330 TB auf ~190 TB reduzieren konnten, ohne die Latenz punktueller Lookups zu verschlechtern.
Erwäge direktes I/O
Wir lesen mit direktem I/O und verwenden es auch für Flush und Compaction. In dieser Größenordnung konkurriert der Page Cache des Betriebssystems mit unserem eigenen Block-Cache um denselben RAM. Bei zufälligen punktuellen Lookups ist dieses doppelte Caching meist Verschwendung. Wir bevorzugen einen einzigen großen Block-Cache, den wir selbst verwalten und der eine vorhersehbare Latenz bietet.
Beachte, dass dies vom Workload abhängt:
Direktes I/O kann scanlastige oder unzureichend ausgestattete Systeme beeinträchtigen. Du solltest es daher mit A/B-Tests prüfen, statt es blind zu übernehmen.
Wähle einen Cache, der hoher Konkurrenz standhält
Bei hoher paralleler Last auf häufig abgefragten Schlüsseln wird der standardmäßige gemeinsame LRU-Cache durch Lock-Contention zum Engpass. Für häufig genutzte Indizes verwenden wir RocksDBs HyperClockCache. Er hält deutlich besser stand, wenn viele Threads gleichzeitig auf dieselben beliebten Einträge zugreifen.
Parallelisiere Multi-Key-Lookups, statt sie zu serialisieren
Statt N Lesevorgänge zu serialisieren, kann RocksDB über io_uring viele I/Os gleichzeitig auslösen und parallel abschließen lassen. Bei einer Methode wie getTransactionsForAddress, die sich in viele zugrunde liegende punktuelle Lookups verzweigt, entscheidet das darüber, ob die Latenz mit der Schlüsselanzahl steigt oder ungefähr gleich bleibt.
RocksDB bietet alle Optionen, trifft aber keine Annahmen über deine Daten. Unsere Erfolge beruhen darauf, unsere Zugriffsmuster zu verstehen und jeden Index an seine eigene Struktur anzupassen. Wir suchen nicht nach einer einzigen globalen Konfiguration, die alles gut kann.
Worauf wir hinarbeiten
Heute läuft die Archivierung in unseren größeren Regionen wie EWR, FRA und Tokio. Da die Speicher-Engine Lookups jetzt innerhalb von Mikrosekunden beantwortet und unsere Netzwerkkarten auslastet, hält uns die Datenbank nachts nicht mehr wach. Wer einen Engpass beseitigt, entdeckt meist den nächsten.
Sobald ein Lookup praktisch kostenlos ist, verlagern sich die dominanten Kosten von der Software auf die Entfernung zwischen Nutzer und Maschine. Eine Anfrage muss weiterhin vom Standort des Nutzers zu unserem Backend und wieder zurück gelangen. Ab diesem Punkt kämpfst du gegen die Physik.
Genau dieses Problem löst Gatekeeper.
Gatekeeper ist unser intern entwickeltes Edge-Gateway. Es wurde in Rust auf Basis von Hyper geschrieben, terminiert Verbindungen in der Nähe der Nutzer und leitet jede Anfrage über den kürzesten verfügbaren Pfad an unser Backend weiter. Hier findet heute der Kampf um die Latenz statt: Connection-Pooling, TLS- und Socket-Optimierung, Routing nach Nähe und Zustand sowie Deployments ohne Ausfallzeit über unsere globale Flotte hinweg. All das spart Millisekunden auf dem Weg zu Bytes, die unsere Archivierung bereits innerhalb von Mikrosekunden bereitstellt.
Eine einzelne Anfrage zu beschleunigen, ist ein Datenbankproblem. Jede Anfrage von jedem Ort der Welt aus schnell zu machen – ohne Wartungsfenster und ohne abgebrochene Verbindungen –, ist ein völlig anderes Problem. Darauf konzentrieren wir uns als Nächstes.
Das verstehen wir unter Innovation an Solanas Leseschicht: jede Ebene richtig umzusetzen, von der Speicher-Engine, die Lookups beantwortet, bis zum Edge-Gateway, das bestimmt, wie schnell die Antwort den Endnutzer erreicht.
Archivierung läuft letztlich auf punktuelle Lookups mit zufälligen Schlüsseln hinaus. Dieses Zugriffsmuster bringt eine spaltenorientierte Engine aus strukturellen Gründen an ihre Grenzen. Es geht nicht um Optimierung, Sharding oder die Sortierung von Views. Die Einschränkungen, auf die wir mit ClickHouse gestoßen sind, liegen in dieser Entscheidung selbst und nicht zufällig in der Konfiguration. Solanas historischer Workload wird durch sein schwierigstes Zugriffsmuster definiert, nicht durch sein einfachstes.
Bei einem Netzwerk, das zur Abwicklungsschicht des globalen Finanzsystems werden will, darf die Leseschicht nicht nur eine besser optimierte Version der bereits überwundenen Lösung sein. Institutionen und Anwendungen, die auf umfangreiche historische Abfragen angewiesen sind, benötigen Methoden, Abdeckung und Latenzen, die der Standard-Stack nicht bieten kann. Solanas Leseschicht muss von Anfang an auf einem Fundament aufbauen, das für den schwierigen Fall geeignet ist. Darauf setzen wir mit RocksDB, und an diesem Maßstab messen wir alles andere.
Wenn du die Zukunft des Finanzwesens in großem Maßstab mitgestalten willst, dann entwickle sie mit uns. Solana ist auf dem Weg, die Abwicklungsschicht des globalen Finanzsystems zu werden, und Archivierung ist nur ein Teil eines viel größeren Puzzles. Wenn dich LSM-Compaction und Optimierung pro Index interessieren oder du anspruchsvolle Systemprobleme auf der besten Hardware lösen willst, die man für Geld kaufen kann, wirst du dich hier zu Hause fühlen.
Wir stellen im gesamten Engineering-Team ein. Alle offenen Stellen findest du unter helius.dev/careers.
Ähnliche Artikel
Helius abonnieren
Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen


