Hinter einem Handler, der Order-Book-Updates unter Burst-Last verliert und umsortiert, stecken meist zwei getrennte Fehler mit einem gemeinsamen Symptom: die Sequenzbehandlung auf der Ingest-Seite und eine Verteilungsschicht, in der eine langsame Session verändert, was alle anderen Sessions bekommen. So werden Sequenz, Recovery, Fan-out und Backpressure gebaut - und hier hört Conflation auf, ehrlich zu sein.
Hinter einem Handler, der L2-Updates unter Burst-Last verliert und umsortiert, stecken meist zwei getrennte Fehler mit einem gemeinsamen Symptom. Der eine sitzt auf der Ingest-Seite, wo die Feed-Sequenz verfolgt wird und ein Gap erkannt statt geschluckt werden muss. Der andere sitzt auf der Verteilungsseite, wo ein normalisiertes Buch an viele Sessions verteilt wird und ein einzelner langsamer Reader verändert, was die anderen bekommen.
Sie haben verschiedene Lösungen, und die falsche anzuwenden verschiebt das Symptom, statt es zu beseitigen. Es folgt die Form der Pipeline, wenn die Sequenz einen Burst überstehen muss: was ein Gap tatsächlich ist, wie die Recovery einen Snapshot mit einem Live-Stream verbindet, wie Fan-out die Reihenfolge einmal entscheidet und wo Conflation ehrlich ist.
Die kurze Antwort ist strukturell. Die Reihenfolge wird an genau einer Stelle entschieden, stromaufwärts jeder Session: Ein einzelner Writer pro Instrument faltet den nummerierten Feed zu einem Buch, und die Sessions bekommen Sichten, die aus diesem Fold abgeleitet sind - sie sortieren nie selbst um. Was amBrain öffentlich belegen kann: Eine von uns gebaute Mini-Börse läuft in Produktion in der MOEX-Kolokation, wir haben das Trading-Terminal Spectre Trade gebaut, und die Marktdatenlatenz, die wir veröffentlichen, ist gemessen - unter 5 ms auf den Pfaden, die wir bauen. Diese Zahl beschreibt unsere Pfade, nicht einen Benchmark des Designs weiter unten.
Eine Sequenznummer verspricht Reihenfolge, nicht Zustellung
Feeds nummerieren ihre Updates, und diese Nummer ist die einzige Ordnungsautorität, die Sie haben. Die Ankunftszeit ist keine: Multicast-Pfade sortieren um, mehrere Kanäle tragen ein Instrument, Empfangsqueues verteilen sich über Kerne, und ein Burst dehnt das alles. Ein Handler, der nach Ankunft ordnet, ist nur korrekt, solange das Netz ruhig ist - also in dem Zustand, um den sich nie jemand Sorgen gemacht hat.
Sechs Eigenschaften des Feeds müssen bekannt sein, bevor Recovery-Logik geschrieben wird. Jede verändert, was ein Gap bedeutet.
- Die Einheit, für die die Sequenz gilt - Kanal, Instrument oder Buch. Eine Nummer pro Kanal sagt Ihnen nicht, welches Instrument ein Update verloren hat, und eine Nummer pro Instrument sagt Ihnen nicht, dass ein Kanal stehengeblieben ist
- Die Inkrementregel: streng fortlaufend innerhalb der Einheit oder steigend mit erlaubten Lücken. Beides gibt es, und die zweite als die erste zu lesen, erzeugt Recoverys, die nie nötig waren
- Ob die Nummern an einer Session-Grenze neu beginnen und was diese markiert - ein als Gap gelesener Neustart schickt jedes Instrument im selben Moment in die Recovery
- Ob Heartbeats die aktuelle Sequenz tragen. Ohne sie sehen eine tote Verbindung und ein stilles Instrument gleich aus
- Ob es Retransmission gibt und über welches Fenster. Gibt es keine, ist Snapshot-Recovery der einzige Rückweg, und sie muss billig genug sein, um sie oft zu nutzen
- Auf welche Sequenznummer ein Snapshot ausgerichtet ist. Ohne sie lässt sich ein Snapshot überhaupt nicht mit einem Live-Stream verbinden
Wo eine Eigenschaft wirklich unbekannt ist, messen Sie sie, statt eine Vermutung festzuschreiben. Jede der sechs wird zu einem Zweig im Recovery-Pfad, und eine falsche Annahme dort zeigt sich später als Buch, das still vom Venue abweicht.
Gap und Umsortierung sehen einige Millisekunden lang gleich aus
Beide beginnen gleich: Das nächste Update trägt nicht die erwartete Nummer. Der Unterschied ist Zeit, deshalb wird nicht bei Ankunft klassifiziert, sondern wenn eine begrenzte Wartezeit abläuft.
- Außer der Reihe: Sie erwarteten N, bekamen N+2, und N+1 trifft ein, solange die Wartezeit noch offen ist. Nichts fehlt, und der einzige Preis ist das Warten
- Duplikat oder Retransmission: eine Nummer auf oder unter der zuletzt angewendeten. Wird verworfen, ohne das Buch zu berühren, und gezählt, denn eine steigende Duplikatrate sagt etwas über den Pfad aus
- Gap: Die Wartezeit ist abgelaufen und N+1 kam nie an. Das Buch kann nicht über das Loch hinaus fortschreiten, und dieses Instrument geht in die Recovery
- Stale: die richtige Nummer, zu spät, um nützlich zu sein. Die Bytes sind angekommen, und stromabwärts ist es ein Verlust
Eine Regel verhindert, dass die Korruption still bleibt: Ein Update wird nur angewendet, wenn seine Sequenz genau die erwartete ist. Alles andere geht in den Wartepuffer oder in die Recovery. Ein Buch, das ein Delta außer der Reihe annimmt, liefert weiter Preise und sieht gesund aus - die Abweichung vom Venue findet später ein Kunde, an einem Fill, der keinen Sinn ergab.
Der Wartepuffer ist eine begrenzte Struktur, keine wachsende Queue. Er hält Updates, die der erwarteten Nummer vorauslaufen, nach Sequenz indiziert, sodass ihre Freigabe ein Lookup ist und keine Sortierung.
- Die Freigabe ist eine Schleife: die erwartete Nummer anwenden, dann das bereits Gepufferte, solange die Nummern lückenlos aufeinander folgen
- Die Frist wird in Zeit ausgedrückt, nicht nur in einer Anzahl offener Updates - ein Burst füllt ein zählerbasiertes Fenster weit früher, als das Design es vorsah
- Was der Puffer wartet, warten alle Consumer mit. Bemessen Sie die Frist an der auf Ihrem eigenen Pfad gemessenen Umsortierung, nicht an einer Zahl, die sich sicher anfühlte
- Ein Pufferüberlauf ist selbst eine Gap-Erklärung: Die Wartezeit ist im Speicher ebenso begrenzt wie in der Zeit
- Der Wartepuffer ist pro Instrument oder pro Kanal, nie global. Ein stilles Instrument darf nicht alles um sich herum aufhalten
Recovery ist ein Snapshot, verbunden mit einem bereits gepufferten Stream
Der Join ist der Teil, der schiefgeht. Ein Snapshot ist ein Buch zum Stand einer Sequenznummer und im Moment seiner Erzeugung schon veraltet; brauchbar wird er durch den inkrementellen Stream, der während des Holens gepuffert wurde.
- Puffern Sie den inkrementellen Stream, bevor der Snapshot angefordert wird. Ein Snapshot ohne Live-Stream dahinter hinkt dem Markt schon hinterher, wenn er ankommt
- Lesen Sie die Sequenznummer, zu der der Snapshot konsistent ist. Veröffentlicht der Feed keine, ist der Feed in der Praxis Snapshot-only, und das Design muss das laut aussprechen
- Gepufferte Updates auf oder unter der Snapshot-Sequenz verwerfen, den Rest der Reihe nach anwenden. Ist das erste davon nicht das Update direkt nach dem Snapshot, ist der Join gescheitert und die Recovery beginnt von vorn
- Füllt sich der Puffer, bevor der Snapshot ankommt, starten Sie die Recovery neu, statt einen Teil davon anzuwenden - eine teilweise angewendete Recovery ist von einem gesunden Buch nicht zu unterscheiden
- Veröffentlichen Sie das Instrument während der Recovery als degradiert, als expliziten Zustand im Stream. Ein Buch mit einem Loch, als aktuell ausgeliefert, ist schlimmer als gar kein Buch
- Nach dem Join prüfen: die Prüfsumme, die der Feed veröffentlicht, sofern er eine veröffentlicht, oder die Übereinstimmung zwischen Ihrem gefalteten Buch und dem nächsten Snapshot
Recovery ist ein normales Ereignis und kein Vorfall, und ihre Kosten gehören in den Kapazitätsplan: wie lange das Holen eines Snapshots dauert, wie viel Stream währenddessen gepuffert wird und wie viele Instrumente gleichzeitig recovern können, bevor der Snapshot-Dienst zum Engpass wird.
Fan-out: einmal normalisieren, einmal encodieren, vielfach senden
Hunderte Terminal-Sessions wollen dasselbe Buch. Der Fehler, der sich unter einem Burst vervielfacht, ist Arbeit pro Session, die ihrer Natur nach nicht pro Session ist: für jeden Subscriber ein Buch neu aufzubauen oder dasselbe Update einmal pro Socket zu serialisieren.
- Ein Writer pro Instrument-Shard besitzt das Buch. Reader verändern es nie, was sowohl den Lock als auch die Frage erübrigt, wessen Version maßgeblich ist
- Der Writer veröffentlicht versionierte Updates in einen Ringpuffer, dem die Reader im eigenen Tempo folgen, sodass ein zurückfallender Reader niemanden bremst
- Jedes Update wird einmal pro Wire-Format encodiert und über Sessions hinweg per Referenz geteilt. Nur Framing und Flow Control sind pro Session
- Jede Session trägt ihre eigene ausgehende Sequenznummer, sodass ein Client seine eigenen Verluste erkennen kann, ohne etwas über den Upstream-Feed zu wissen
- Die Reihenfolge wird pro Instrument garantiert, denn das ist die Garantie, auf die sich Clients stützen. Reihenfolge über Instrumente hinweg wird entweder ausdrücklich zugesagt und umgesetzt oder gar nicht zugesagt
- Über einen Prozess hinaus wird Fan-out zu einer Relay-Schicht: Jedes Relay nimmt stromaufwärts eine Subscription und bedient einen Teil der Sessions, sodass die Arbeit des Writers konstant bleibt
Die Kosten von Fan-out entscheiden sich daran, wie oft ein Update transformiert wird, nicht daran, wie viele Sockets es empfangen. Einmal encodieren und eine Referenz weitergeben skaliert mit den Sessions; ein Buch pro Session neu aufzubauen nicht.
Ein langsamer Consumer ist eine gewählte Policy, kein Unfall
Irgendwo gibt es eine Session in einem schlechten Netz oder ein Terminal, dessen Render-Schleife hängt, und sein ausgehender Puffer füllt sich. Es gibt vier mögliche Verhaltensweisen, und zwei davon wählt man nur versehentlich.
- Den Writer blockieren, bis die langsame Session abfließt: nie. Das macht aus einer schlechten Verbindung ein Latenzereignis für alle auf dem Shard
- Die Queue ohne Grenze wachsen lassen: Aus einem langsamen Consumer wird erschöpfter Speicher und daraus ein Ausfall, der mit der ursprünglichen Session nichts zu tun hat
- Begrenzte Queue mit Conflation: richtig für Buchzustand, wo ein Client das aktuelle Bild will und nicht jeden Zwischenschritt
- Begrenzte Queue mit Disconnect am High Watermark: richtig für Streams ohne mögliche Conflation, wo mit einem verworfenen Eintrag auch Bedeutung verloren geht
- Welche Policy auch gilt: Die Queue ist pro Session, und der Rückstand wird laufend gemessen - Queue-Tiefe und der Abstand zwischen veröffentlichter Sequenz und der Sequenz, die in den Socket geschrieben wurde
- Ein Disconnect nennt seinen Grund. Ein unerklärtes Schließen wird in einer Schleife wiederholt; auf ein erklärtes folgt eine neue Subscription
Backpressure ist die Stelle, an der beide Seiten aufeinandertreffen. Wenn der ausgehende Pfad auf den Buch-Writer zurückdrücken kann, verzögert ein langsames Terminal irgendwann das Falten des Feeds, und die Gap-Erkennung schlägt aus Gründen an, die mit dem Venue nichts zu tun haben. Ein begrenzter Ring zwischen beiden unterbricht diese Kette.
Conflation ist ehrlich beim Zustand und falsch bei Events
Ein Buch ist Zustand: Der Client will die aktuellen Level, und ein bereits ersetzter Wert trägt keine eigene Bedeutung mehr. Ein Trade-Tape ist ein Log von Events, in dem jeder Eintrag eine Tatsache ist, die passiert ist und sich nicht durch Zusammenfassen wegkürzen lässt.
- Mit Conflation: Preislevel-Updates, Top of Book, aggregierte Tiefe und abgeleitete Kennzahlen wie letzter Preis oder Session-Volumen
- Ohne Conflation: Trades und Prints, Order- und Execution-Reports, Auktions- und Phasenwechsel und alles, was ein Client über die Zeit aggregiert - ein Tape aus einem Stream mit Conflation ist eine falsche Zahl, die selbstbewusst vertreten wird
- Conflation je Schlüssel, nicht je Stream. Das jeweils letzte Update pro Preislevel zu behalten, erhält das Buch; nur das insgesamt letzte Update zu behalten, wirft jedes Level weg, das sich nicht zuletzt geändert hat
- Ein Update mit Conflation trägt die Sequenznummer des Zustands, den es abbildet, damit der Client weiß, welchem Punkt es entspricht
- Das Conflation-Intervall ist Teil der Latenz, die Sie berichten. Ein Stream mit Intervall-Conflation wird nicht durch die Latenz beschrieben, die am Stream ohne Conflation gemessen wurde
- Ein Client, der jeden Zwischenzustand braucht - ein Backtest, ein Compliance-Nachweis -, nimmt den Stream ohne Conflation und zahlt mit Bandbreite
Conflation ist eine Formänderung, keine Kompressionseinstellung. Sobald ein Stream Conflation hat, kann ein Client nicht mehr rekonstruieren, was zwischen zwei Updates passiert ist, und man darf ihm nicht sagen, der Stream sei vollständig. Beides zu veröffentlichen - einen Buch-Stream mit Conflation und einen Event-Stream ohne -, hält beide Arten von Clients korrekt.
Reconnect ist eine Resynchronisation - und alle kommen gleichzeitig
Wenn eine Session zurückkommt, ist das Buch, das sie hält, wertlos, solange der Server die Kontinuität nicht belegen kann. Der Standard ist ein frischer Snapshot je Subscription, mit seiner Sequenznummer, angewendet auf einen Client, der zuvor seinen lokalen Zustand verworfen hat.
- Ein Resume ab einer Sequenznummer wird nur dort angeboten, wo ein begrenzter Replay-Puffer existiert. Ist die angefragte Nummer herausgefallen, sagt der Server das und fällt auf einen Snapshot zurück, statt einen Stream mit Loch zu senden
- Der Session-Zustand über einen Reconnect hinweg ist eine ausdrückliche Entscheidung: Entweder hält der Server Subscriptions für eine begrenzte Zeit unter einem Session-Token, oder der Client meldet sie beim Verbinden neu an. Beides funktioniert; eine implizite Mischung nicht
- Doppelte Zustellung nach einem Resume ist erwartbar, und der Client verwirft anhand der Sequenz. At-least-once plus Sequenznummerierung ist leichter korrekt umzusetzen als Exactly-once
- Reconnects kommen gemeinsam, denn was eine Session getrennt hat, hat meist viele getrennt. Jittered Backoff im Client und Admission Control im Server verhindern, dass die Recovery zum zweiten Ausfall wird
- Snapshots für diesen Andrang kommen aus einem Cache je Instrument, der in festem Takt aufgefrischt wird, sodass der Writer einen Snapshot nach Zeitplan serialisiert statt einmal pro sich neu verbindender Session
- Das Buch auf Client-Seite wird neu aufgebaut, nie geflickt. Ein Terminal, das seine alten Level behält und neue Deltas darauf anwendet, trägt den Fehler von vor dem Disconnect in ein Buch, das jetzt frisch aussieht
Der Fehlerfall, für den es sich zu entwerfen lohnt, ist nicht ein einzelner Reconnect. Es ist ein Netzereignis, das Hunderte Sessions in derselben Sekunde zurückbringt, von denen jede einen Snapshot für jedes beobachtete Instrument anfordert, während die Ingest-Seite sich von dem Gap erholt, das dasselbe Ereignis erzeugt hat.
Was amBrain öffentlich belegen kann: Wir bauen von Jerewan, Armenien, aus Trading-Plattformen mit niedriger Latenz, Matching Engines und Real-Time-Bidding-Systeme in Rust, und die Marktdatenlatenz, die wir veröffentlichen - unter 5 ms -, ist auf den Pfaden gemessen, die wir bauen. Wenn Ihr Handler unter Burst-Last die Sequenz verliert, lohnt sich das Gespräch, das die Ingest-Seite von der Verteilungsseite trennt, bevor eine von beiden neu geschrieben wird.