Eine halbe Million RPS auf einem GB10: Der Engpass war das Netz, nicht der Rechner
Wir wollten sehen, wie schnell die Datenebene unserer Cloud auf einer NVIDIA GB10 werden kann. Die Antwort hat uns mehr über die Empfangsqueues der NIC beigebracht als über unseren eigenen Code, und sie hat unseren Durchsatz auf demselben Kabel mehr als verdoppelt.

Auf dieser Seite
Wir bauen unsere Cloud auf einem einzigen Go-Binary: ein Artefakt, das das KI-Gateway, den Commerce, IAM und KMS einhängt und api.hanzo.ai bedient. Seine HTTP-Oberfläche ist zip, unser ZAP-natives Web-Framework, gelegt über fasthttp. Wir haben eine einfache Frage gestellt: wie schnell kann das Ding wirklich werden, und was hält es auf.
Die ehrliche Antwort hat uns dreimal überrascht. Hier ist die ganze Spurensuche, mit allen Zahlen. Alles ist öffentlich in hanzoai/benchmarks.
Erstens: ist das Framework die Steuer?
Nein. zip trägt konstante ~24 Nanosekunden und eine einzelne 48-Byte-Allokation über der rohen fiber/fasthttp-Engine, die es umhüllt — unter 1% bei jedem echten Handler. Ende zu Ende, auf Loopback, schafft zip 0.95× des rohen fasthttp. Das Framework ist Rauschen. Gut. Weiter.
| Framework | Req/s |
|---|---|
| rohes fasthttp | 365–400k |
| zip (HTTP) | 338–348k |
Zweitens: das verteilte Ergebnis wurde langsamer
Hier wurde es interessant. Wir haben zwei Rechner an einer direkten 2.5-GbE-Leitung: spark (eine NVIDIA GB10, 20 Kerne) als Server, evo (32-Kern-x86) als Lastgenerator. Die Last vom Rechner zu nehmen sollte sparks Kerne zum Bedienen freigeben — wir haben erwartet, dass es steigt.
Es ging nach unten. 194k req/sec. Und als wir Verbindungen dazugaben, um härter zu drücken, wurde es schlimmer: 167k bei 4,000 Verbindungen, 154k bei 8,000, wobei die p99-Latenz von 5 ms auf 52 ms kletterte. Das ist ein Überlastungskollaps aus dem Lehrbuch.
Aber die Leitung lag nur bei ~70 MB/s — 22% von 2.5 GbE. Nicht die Bandbreite. Und sparks 20 Kerne waren meist untätig. Nicht die CPU. Also was?
Der Schuldige: eine Empfangsqueue
ethtool -l hat es verraten: die NIC lief mit einer einzigen Hardware-RX-Queue. Jedes eingehende Paket wurde von der Softirq eines Kerns verarbeitet — und dieser eine Kern war die Decke. Schlimmer: der Treiber hat sich schlicht geweigert, Queues hinzuzufügen (ethtool -L → "Operation not supported").
Der schnellste Entwicklerrechner, den wir haben, wurde also von einem einzelnen Kernel-Thread ausgebremst, der Pakete schaufelte, während neunzehn Kerne zusahen.
Der Fix: Pakete in Software lenken
Wenn die Hardware kein Multi-Queue will, will es der Kernel: Receive Packet Steering verteilt Flows in Software über die Kerne. Wir haben RPS (und RFS/XPS) auf allen 20 Kernen eingeschaltet, den Empfangs-Backlog von 1,000 auf 300,000 gehoben, 256 MiB Socket-Puffer gesetzt und den Performance-Governor festgenagelt. Ein idempotentes tune.sh, auf beiden Rechnern gelaufen.
Dasselbe Kabel. Derselbe Handler. 518,000 req/sec — ein Sprung von 2.67×, mit Spitze bei 696k.
| Nebenläufigkeit | ohne Tuning | mit Tuning |
|---|---|---|
| 1,000 | 194k | 518k |
| 4,000 | 154k | 474k (Spitze 696k) |
Der Haken: die getunte Netz-Zahl (518k) schlägt jetzt Loopback (365–400k) — weil über das Kabel sparks Kerne Vollzeit bedienen, statt sich den Lastgenerator zu teilen. Bei 518k liegt der Rechner nur zu ~12% brach. Das ist nah an der echten Decke eines einzelnen Laders.
Drittens: ZAP sollte schneller sein — also haben wir es dazu gemacht
zip ist ZAP-nativ — eine nackte Listen-Adresse spricht unseren binären Transport; http:// spricht HTTP. Wir haben erwartet, dass der binäre Transport gewinnt, einen ZAP-Lastclient gebaut (HTTP-Werkzeuge können ihn nicht sprechen) und gemessen.
ZAP schaffte 91k. HTTP schaffte 481k. Der native Transport hat verloren — um 5×.
Wir sind ihm nachgegangen, und es ist nicht das Kabel — es ist der Codec. Der Latenzschweif erzählte die Geschichte (p50 gesunde 303 µs, aber p99 bei 13 ms — eine Garbage-Collection-Signatur), und GOGC von 100 auf 800 zu lockern hat ZAP auf 203k mehr als verdoppelt. Über einen Lauf von sechs Sekunden hat der ZAP-Server 2,203 Garbage Collections ausgelöst; der HTTP-Server 57. Drei Allokationsstellen trugen es: der Marshaler prägt pro Aufruf einen frischen Frame-Puffer, das Header-Dekodieren kopiert jeden String in eine Map, und — die große — der Server von zaphttp hat einen frischen Request-Kontext pro Request allokiert, wo der eigene Server von fasthttp ihn pro Verbindung poolt.
Also haben wir es gefixt. Die Änderung (zap-proto/http, Branch perf/zero-alloc-codec) ist mechanisch, nicht architektonisch: den Request-Kontext einmal pro Verbindung poolen, einen Frame-Encoder von Hand schreiben, der in einen gepoolten Puffer anhängt, und das 4-Byte-Längenpräfix über bufio.Peek/Discard abziehen, damit kein Header-Array auf den Heap entkommt. Die Serve-Schleife serialisiert mit AppendResponse in diesen wiederverwendeten Puffer und dekodiert Header an Ort und Stelle — über eine warme Verbindung allokiert sie also nichts pro Request — und Golden Tests bestätigen, dass das Kabel vorher und nachher bytegenau identisch ist. Reine Codec-Umschreibung, keine Protokolländerung.
Das Ergebnis: ZAP geht von 91k → 570k req/sec, ein Schub von 6.3×, der die 481k von HTTP überholt — ZAP gewinnt jetzt mit 1.18× auf Loopback, wobei p99 von 13 ms auf 1.19 ms zusammenfällt und die GC von 2,203 auf 62 fällt.
Zwei Dinge, bei denen ich mich korrigieren musste — und beide überleben den Fix, also sage ich sie klar. Erstens, der Gewinn ist CPU, nicht das Kabel. Der ZAP-Frame ist tatsächlich größer als HTTP (124 gegen 75 Bytes für einen /health-Request); ZAP gewinnt, indem es weniger Zyklen pro Byte ausgibt, nicht weniger Bytes. Zweitens, zaphttp überspringt JSON nicht. Es trägt den HTTP-Body auf beiden Transporten als opake Bytes, also zahlt ein Handler denselben Marshal-Preis so oder so — bei einer chatförmigen JSON-Nutzlast ist ZAP tatsächlich 0.95× HTTP, ein kleiner Verlust durch das Kopieren des Bodys in den Frame-Schwanz. JSON-Elision ist eine Eigenschaft von nativem typisiertem ZAP-RPC, eine eigene Achse, die zaphttp nicht übt. Der Loopback-Gewinn hier ist ehrlich und rein ein Gewinn von CPU und Allokation. Und über das echte 2.5-GbE-Kabel? Beide Transporte liegen bei ~530k gleichauf — der Link ist gesättigt und der Codec ist nicht mehr der Engpass.
Viertens: überspringt ZAP JSON wirklich?
Dieser letzte Satz — „JSON-Elision ist eine native ZAP-Eigenschaft, die zaphttp nicht übt“ — ist ein Schuldschein, und ein Leser sollte ihn mir einlösen lassen. zaphttp trägt den Body opak, also testet es nie das, wobei binäre Protokolle gewinnen sollen: strukturierte Daten lesen, ohne Text zu parsen. Also habe ich den Test gebaut, der das tut.
Ein gemeinsamer Datensatz — ein Aufruf in der Form von eth_getBalance, vier Felder in jede Richtung (account, block, id → balance, nonce, blockHash) — ging auf vier Wegen hin und zurück: Stdlib encoding/json, goccy/go-json und sonic (die zwei schnellsten JSON-Bibliotheken in Go) und das typisierte Binary von zap-proto/go. Dieselben Felder hinein, dieselbe Rechnung, dieselben Felder heraus; ein Roundtrip-Äquivalenztest erzwingt bytegenaues Neu-Encodieren, damit keine Seite Arbeit auslässt, die die andere tut. Und ich habe JSON an seiner schnellsten Implementierung gehalten, nicht an der Stdlib, damit niemand das Ergebnis wegwischen kann.
Das Dekodieren ist ein Gemetzel. Beim Lesen des Vier-Feld-Requests:
| Codec | Dekodieren | Allokationen |
|---|---|---|
| JSON-Stdlib | 2334 ns | 13 |
| JSON sonic | 845 ns | 8 |
| JSON goccy | 473 ns | 4 |
| ZAP typisiert | 13 ns | 0 |
Dreizehn Nanosekunden, null Allokationen — 35× schneller als das schnellste JSON, 174× schneller als die Stdlib. Es gibt kein Parsen: das Feld sitzt an einem bekannten Offset, du liest es. Das ist das ganze Versprechen eines typisierten Binärkabels, und es hält.
Aber ich habe versprochen, die Teile zu berichten, die ZAP nicht schmeicheln, also:
- Das Encodieren ist unentschieden, und es allokiert mehr. ZAP serialisiert in 629 ns mit 12 Allokationen gegen 426 ns / 4 von goccy. Der veröffentlichte Builder von
zap-proto/gov1.3.0 schiebt variabel lange Schwänze über eine Kopie auf und sein Objekt-Builder entweicht auf den Heap; die gepoolte API fester Breite, die das fixt, ist nicht im veröffentlichten Tag, also ist das die ehrliche Zahl. Unterm Strich bevorzugt der volle Server-Handler (dekodieren + rechnen + encodieren) ZAP — 274 ns gegen 431 von goccy — weil der Dekodiergewinn das Encode-Unentschieden ersäuft. Aber das Dekodieren trägt es. - Ende zu Ende schrumpft das 174× auf ~1.2×. Über einen echten
/rpc-Endpunkt: ZAP 535k req/s, sonic 483k (1.11×), goccy 452k (1.18×), Stdlib 407k (1.31×). Real, aber weit weg von der Microbenchmark-Lücke — weil bei einer halben Million Requests pro Sekunde Socket und Scheduler der Preis sind und die Serialisierung nur eine dünne Scheibe davon. Der Codec hört auf, der Engpass zu sein, lange bevor du merken würdest, dass er schneller wurde.
Eine Überraschung, die für ZAP spricht: bei einem echten strukturierten Datensatz ist sein Kabel kleiner, nicht größer — 162 gegen 233 Bytes — weil JSON Bytes für 0x-Hex-Strings, Dezimalzahlen, Anführungszeichen und Feldnamen ausgibt. Der Vorbehalt „ZAP ist größer“ aus dem Framing-Test gilt nur für triviale Nutzlasten in der Größe von /health; gib ihm echte typisierte Felder, und binär gewinnt auch bei der Größe.
Also, ehrlich: überspringt ZAP JSON? Ja — und auf dem Lesepfad ist es nicht knapp. Ob du es spürst, hängt davon ab, ob Serialisierung dein Engpass ist. Bei 500k rps winziger Datensätze ist sie es nicht. Tausch fette verschachtelte Nutzlasten oder ein dekodierlastiges Fan-out ein, und dieses 174× zeigt sich dort, wo du arbeitest.
Was wir gelernt haben
- Miss den ganzen Pfad, und hinterfrage das Werkzeug.
heyhat uns bei ~100k gedeckelt — das war der Client, nicht der Server.bombardierhat 4× mehr gezeigt. - Der Rechner ist selten der erste Engpass. Eine einzelne NIC-RX-Queue hat uns 2.67× gekostet, bevor wir eine Zeile Anwendungscode angefasst haben.
- Miss Allokationen, nicht nur den Durchsatz. Dass ZAP „langsam war“, lag ein
GOGC-Flag davon entfernt, offensichtlich ein Codec-Problem zu sein — und sobald wir den Kontext gepoolt und die Allokationen auf null gebracht haben, hat der native Transport HTTP überholt. - Ein Microbenchmark und ein Lasttest beantworten verschiedene Fragen. Typisiertes Dekodieren ist isoliert 174× schneller als JSON und Ende zu Ende 1.2× schneller — beide Zahlen sind wahr, und nur eine davon zu zitieren wäre eine Lüge durch Weglassen.
- Berichte es fair und real. Derselbe Handler, warme Verbindungen, kein rosinengepicktes GOGC, JSON an seiner schnellsten Bibliothek gehalten und nicht an seiner langsamsten, das Skript veröffentlicht. Als die ehrliche Zahl ein Verlust war — das Encode-Unentschieden, die zusammengedrückte Ende-zu-Ende-Marge — haben wir den Verlust mitgeliefert.
Der allocationsfreie ZAP-Codec ist fertig, und er gewinnt. Was bleibt, ist physikalisch: ein Dual-NIC-Lader (parallele Empfangspfade Richtung siebenstellig), schnellere Verbindung (10 GbE / RDMA), ein writev-Split, damit der Frame-Schwanz von ZAP auf großen Nutzlasten eine Body-Kopie überspringt, und die Post-Quanten-Transporte, die uns Go 1.26 nativ gibt — PQ-TLS 1.3, ZAP-over-PQ-TLS und PQ-QUIC — wo wir die ML-KEM-Handshake-Kosten vom Durchsatz im stationären Zustand trennen.
Wir können das so weit treiben, wie wir müssen.
Weiterlesen

Hanzo DNS and Networking: Programmable Infrastructure
Launching Hanzo DNS and Networking — programmable DNS with service discovery and zero-trust virtual networks.

BitDelta: 14 Zen-Modelle aus einem GPU-Cluster ausliefern
Wie die 1-Bit-Delta-Kompression von BitDelta 14 Zen-Modellvarianten aus gemeinsamer GPU-Infrastruktur ausliefert: die Mathematik, die Architektur und die Kompromisse.

Von der Commerce-KI zu Frontier-Modellen: Hanzos Jahrzehnt der Infrastruktur
Zwölf Jahre Aufbau von KI-Infrastruktur — von einer Echtzeit-Empfehlungsengine 2014 zu 94+ Frontier-Modellen und einer Post-Quanten-Blockchain 2026. Die technische Geschichte, wie wir hierher gekommen sind.