Gabung beta Hanzo dan dapatkan diskon 50% untuk setiap paket berbayar

Setengah Juta RPS di GB10: Penghambatnya Jaringan, Bukan Mesinnya

Kami ingin melihat seberapa cepat bidang data cloud kami bisa berjalan di NVIDIA GB10. Jawabannya lebih banyak mengajar kami tentang antrean terima NIC daripada tentang kode kami sendiri — dan lebih dari menggandakan throughput pada kabel yang sama.

Di halaman ini

Kami membangun cloud kami di atas satu biner Go — satu artefak yang memasang gerbang AI, perdagangan, IAM, dan KMS lalu melayani api.hanzo.ai. Permukaan HTTP-nya adalah zip, kerangka web asli ZAP kami, dibungkus di atas fasthttp. Jadi kami mengajukan pertanyaan sederhana: seberapa cepat benda ini sebenarnya bisa berjalan, dan apa yang menghentikannya?

Jawaban yang jujur mengejutkan kami tiga kali. Inilah seluruh cerita detektifnya, lengkap dengan angkanya. Semuanya publik di hanzoai/benchmarks.

Pertama: apakah kerangkanya yang jadi pajak?

Tidak. zip membawa sekitar 24 nanodetik yang tetap dan satu alokasi 48 bita di atas mesin fiber/fasthttp mentah yang dibungkusnya — di bawah 1% pada penangan yang nyata. Ujung ke ujung, di loopback, zip mencapai 0.95× fasthttp mentah. Kerangkanya derau. Baik. Lanjut.

kerangkapermintaan/detik
fasthttp mentah365–400k
zip (HTTP)338–348k

Kedua: hasil terdistribusi justru lebih lambat

Di sinilah menariknya. Kami punya dua mesin pada tautan kabel langsung 2.5 GbE: spark (sebuah NVIDIA GB10, 20 inti) sebagai peladen, evo (x86 32 inti) sebagai pembangkit beban. Memindahkan beban keluar dari mesin seharusnya membebaskan inti spark untuk melayani — kami mengira angkanya akan naik.

Angkanya turun. 194k permintaan/detik. Dan ketika kami menambah koneksi untuk menekan lebih keras, hasilnya lebih buruk: 167k pada 4,000 koneksi, 154k pada 8,000, dengan latensi p99 naik dari 5 ms ke 52 ms. Itu keruntuhan kemacetan seperti di buku teks.

Padahal tautannya hanya di ~70 MB/s — 22% dari 2.5 GbE. Bukan lebar pita. Dan 20 inti spark sebagian besar menganggur. Bukan CPU. Lalu apa?

Biang keladinya: satu antrean terima

ethtool -l membongkarnya: NIC berjalan dengan satu antrean RX perangkat keras. Setiap paket masuk diproses oleh softirq satu inti — dan inti itulah langit-langitnya. Lebih parah, driver menolak mentah-mentah menambah antrean (ethtool -L → "Operation not supported").

Jadi mesin pengembang tercepat yang kami miliki dicekik oleh satu utas kernel yang menyekop paket, sementara sembilan belas inti menonton.

Perbaikannya: arahkan paket di perangkat lunak

Ketika perangkat keras tidak mau banyak antrean, kernel mau — Pengarahan Paket Terima menghash aliran ke seluruh inti di perangkat lunak. Kami menyalakan RPS (dan RFS/XPS) di seluruh 20 inti, menaikkan backlog terima dari 1,000 ke 300,000, mengatur buffer soket 256 MiB, dan menyematkan governor kinerja. Satu tune.sh yang idempoten, dijalankan di kedua mesin.

Kabel yang sama. Penangan yang sama. 518,000 permintaan/detik — lompatan 2.67×, memuncak di 696k.

konkurensibelum diseteldisetel
1,000194k518k
4,000154k474k (puncak 696k)

Yang mengejutkan: angka jaringan yang sudah disetel (518k) kini mengalahkan loopback (365–400k) — karena di atas kabel, inti spark melayani penuh waktu alih-alih berbagi dengan pembangkit beban. Pada 518k mesin hanya menganggur ~12%. Itu dekat dengan langit-langit nyata dari satu pemuat.

Ketiga: ZAP seharusnya lebih cepat — maka kami membuatnya demikian

zip asli ZAP — alamat dengar yang telanjang berbicara transport biner kami; http:// berbicara HTTP. Kami mengira transport biner akan menang, membangun klien beban ZAP (alat HTTP tidak bisa berbicara dengannya), dan mengukur.

ZAP mencapai 91k. HTTP mencapai 481k. Transport asli kalah — sebesar 5×.

Kami mengejarnya, dan bukan kabelnya — kodeknya. Ekor latensi yang bercerita (p50 sehat di 303 µs, tetapi p99 di 13 ms — tanda pengumpulan sampah), dan melonggarkan GOGC dari 100 ke 800 lebih dari menggandakan ZAP menjadi 203k. Dalam lari enam detik peladen ZAP memicu 2,203 pengumpulan sampah; peladen HTTP, 57. Tiga situs alokasi yang menanggungnya: marshaler mencetak buffer bingkai baru setiap panggilan, dekode header menyalin setiap string ke dalam peta, dan — yang besar — peladen zaphttp mengalokasikan konteks permintaan baru per permintaan sementara peladen fasthttp sendiri mengumpulkannya per koneksi.

Maka kami memperbaikinya. Perubahannya (zap-proto/http, cabang perf/zero-alloc-codec) mekanis, bukan arsitektural: kumpulkan konteks permintaan sekali per koneksi, tulis sendiri enkoder bingkai yang menambahkan ke buffer terumpun, dan kuras awalan panjang 4 bita lewat bufio.Peek/Discard supaya tidak ada larik header yang lolos ke heap. Gelung layan menserialkan dengan AppendResponse ke buffer yang dipakai ulang itu dan mendekode header di tempat — jadi di atas koneksi yang sudah hangat ia mengalokasikan nihil per permintaan — dan uji emas menegaskan bahwa kabelnya identik bita demi bita sebelum dan sesudah. Penulisan ulang kodek semata, tanpa perubahan protokol.

Hasilnya: ZAP bergerak 91k → 570k permintaan/detik, angkat 6.3× yang melampaui 481k milik HTTP — ZAP kini menang 1.18× di loopback, dengan p99 runtuh dari 13 ms ke 1.19 ms dan GC turun dari 2,203 ke 62.

Dua hal yang harus saya koreksi sendiri — dan keduanya bertahan setelah perbaikan, jadi layak dinyatakan terus terang. Pertama, kemenangannya di CPU, bukan di kabel. Bingkai ZAP sebenarnya lebih besar daripada HTTP (124 lawan 75 bita untuk permintaan /health); ZAP menang karena menghabiskan lebih sedikit siklus per bita, bukan lebih sedikit bita. Kedua, zaphttp tidak melewatkan JSON. Ia membawa tubuh HTTP sebagai bita buram di kedua transport, jadi penangan membayar biaya marshal yang sama di kedua sisi — pada muatan JSON berbentuk obrolan, ZAP sebenarnya 0.95× HTTP, kerugian kecil karena menyalin tubuh ke ekor bingkai. Pengelakan JSON adalah sifat RPC bertipe asli ZAP, sumbu terpisah yang tidak dijalankan zaphttp. Kemenangan loopback di sini jujur dan murni kemenangan CPU/alokasi. Dan di kabel 2.5 GbE yang nyata? Kedua transport seri di ~530k — tautannya jenuh dan kodek bukan lagi penghambat.

Keempat: jadi apakah ZAP benar-benar melewatkan JSON?

Kalimat terakhir itu — "pengelakan JSON adalah sifat ZAP asli yang tidak dijalankan zaphttp" — adalah surat janji, dan pembaca berhak menagihnya. zaphttp membawa tubuh secara buram, jadi ia tidak pernah menguji hal yang seharusnya dimenangi protokol biner: membaca data terstruktur tanpa mengurai teks. Maka saya membangun uji yang melakukan itu.

Satu rekaman bersama — panggilan berbentuk eth_getBalance, empat bidang tiap arah (account, block, id → balance, nonce, blockHash) — bolak-balik dengan empat cara: encoding/json pustaka standar, goccy/go-json dan sonic (dua pustaka JSON tercepat di Go), serta biner bertipe dari zap-proto/go. Bidang masuk yang sama, komputasi yang sama, bidang keluar yang sama; uji kesetaraan bolak-balik memaksakan pengodean ulang yang tepat per bita supaya tidak ada pihak yang melewatkan kerja pihak lain. Dan saya menahan JSON pada implementasi tercepatnya, bukan pustaka standar, supaya tidak ada yang mengibaskan hasilnya.

Dekode adalah pembantaian. Membaca permintaan empat bidang:

kodekdekodealokasi
JSON pustaka standar2334 ns13
JSON sonic845 ns8
JSON goccy473 ns4
ZAP bertipe13 ns0

Tiga belas nanodetik, nol alokasi — 35× lebih cepat daripada JSON tercepat, 174× lebih cepat daripada pustaka standar. Tidak ada urai: bidang duduk di ofset yang diketahui, Anda membacanya. Itulah seluruh tawaran kabel biner bertipe, dan ia bertahan.

Tetapi saya berjanji melaporkan bagian yang tidak menyanjung ZAP, jadi:

  • Enkode seri, dan ia mengalokasikan lebih banyak. ZAP menserialkan dalam 629 ns dengan 12 alokasi lawan 426 ns / 4 milik goccy. Pembangun zap-proto/go v1.3.0 yang terbit menunda ekor panjang-variabel lewat satu salinan dan membuat pembangun objeknya lolos ke heap; API lebar-tetap terumpun yang memperbaikinya belum ada di tag yang dirilis, jadi itulah angka yang jujur. Setelah dihitung bersih, penangan peladen penuh (dekode + komputasi + enkode) memang berpihak pada ZAP — 274 ns lawan 431 milik goccy — karena kemenangan dekode menenggelamkan seri enkode. Tetapi dekode yang menanggungnya.
  • Ujung ke ujung, 174× menyusut menjadi ~1.2×. Di titik akhir /rpc yang nyata: ZAP 535k permintaan/detik, sonic 483k (1.11×), goccy 452k (1.18×), pustaka standar 407k (1.31×). Nyata, tetapi jauh dari celah tolok ukur mikro — karena pada setengah juta permintaan per detik, soket dan penjadwal adalah biayanya, dan serialisasi hanya irisan tipis. Kodek berhenti menjadi penghambat jauh sebelum Anda menyadari ia menjadi lebih cepat.

Satu kejutan yang berpihak kepada ZAP: pada rekaman terstruktur yang nyata, kabelnya lebih kecil, bukan lebih besar — 162 lawan 233 bita — karena JSON menghabiskan bita pada string heks 0x, bilangan bulat desimal, tanda kutip, dan nama bidang. Catatan "ZAP lebih besar" dari uji pembingkaian hanya berlaku untuk muatan sepele seukuran /health; beri ia bidang bertipe yang nyata dan biner menang pada ukuran juga.

Jadi, jujur: apakah ZAP melewatkan JSON? Ya — dan di jalur baca, selisihnya tidak tipis. Apakah Anda merasakannya bergantung pada apakah serialisasi adalah penghambat Anda. Pada 500k rps rekaman kecil, bukan. Masukkan muatan bersarang yang gemuk atau sebaran yang berat dekode, dan 174× itu mulai muncul di tempat Anda bekerja.

Yang kami pelajari

  • Ukur seluruh jalur, dan pertanyakan alatnya. hey membatasi kami di ~100k — itu klien, bukan peladen. bombardier mengungkap 4× lebih banyak.
  • Mesin jarang menjadi penghambat pertama. Satu antrean RX pada NIC merugikan kami 2.67× sebelum kami menyentuh satu baris kode aplikasi.
  • Ukur alokasi, bukan hanya throughput. ZAP yang "lambat" tinggal satu bendera GOGC lagi dari jelas menjadi masalah kodek — dan setelah kami mengumpul konteks dan menolkan alokasi, transport asli melampaui HTTP.
  • Tolok ukur mikro dan uji beban menjawab pertanyaan yang berbeda. Dekode bertipe 174× lebih cepat daripada JSON secara terisolasi dan 1.2× lebih cepat ujung ke ujung — kedua angka itu benar, dan mengutip hanya satu adalah dusta karena penghilangan.
  • Laporkan dengan adil dan nyata. Penangan yang sama, koneksi hangat, tanpa GOGC yang dipetik, JSON ditahan pada pustaka tercepatnya bukan yang terlambat, terbitkan skripnya. Ketika angka yang jujur adalah kekalahan — seri enkode, margin ujung-ke-ujung yang terkompresi — kami mengirim kekalahan itu juga.

Kodek ZAP tanpa alokasi sudah selesai, dan ia menang. Penghambat yang tersisa bersifat fisik: pemuat dua NIC (jalur terima paralel menuju tujuh digit), interkoneksi yang lebih cepat (10 GbE / RDMA), pemisahan writev supaya ekor bingkai ZAP melewatkan satu salinan tubuh pada muatan besar, dan transport pascakuantum yang Go 1.26 berikan secara asli — PQ-TLS 1.3, ZAP-over-PQ-TLS, dan PQ-QUIC — di mana kami akan memisahkan biaya jabat tangan ML-KEM dari throughput keadaan tunak.

Kami bisa membawa ini sejauh yang kami perlukan.

Komentar

Masuk dengan Hanzo ID untuk berkomentar.

Memuat komentar…

Baca selengkapnya