Hanzo betasına katıl, ücretli her planda %50 indirim kazan

Bir GB10 üzerinde yarım milyon RPS: darboğaz makine değil, ağdı

Bulutumuzun veri düzleminin bir NVIDIA GB10 üzerinde ne kadar hızlı gidebildiğini görmek istedik. Yanıt bize kendi kodumuzdan çok NIC alma kuyruklarını öğretti ve aynı tel üzerinde aktarımı iki kattan fazla artırdı.

Bu sayfada

Bulutumuzu tek bir Go ikilisi üzerine kuruyoruz: yapay zeka ağ geçidini, ticareti, IAM ve KMS hizmetlerini takıp api.hanzo.ai sunan bir yapı. HTTP yüzeyi zip, ZAP'a özgü web çerçevemiz, fasthttp üzerine sarılmış. Basit bir soru sorduk: bu şey gerçekten ne kadar hızlı gidebilir, ve onu ne durdurur.

Dürüst yanıt bizi üç kez şaşırttı. İşte bütün soruşturma, sayılarla. Her şey hanzoai/benchmarks içinde herkese açık.

Birinci: çerçeve vergi mi?

Hayır. zip, sardığı ham fiber/fasthttp motorunun üzerine sabit ~24 nanosaniye ve tek bir 48 baytlık ayırma koyar — gerçek bir işleyicide 1% altında. Uçtan uca, loopback üzerinde, zip ham fasthttp değerinin 0.95× kadarını yapar. Çerçeve gürültüdür. İyi. Devam.

çerçeveist./sn
ham fasthttp365–400k
zip (HTTP)338–348k

İkinci: dağıtık sonuç yavaşladı

İlginçleştiği yer burası. Doğrudan 2.5 GbE kablolu bağlantıda iki makinemiz var: sunucu olarak spark (bir NVIDIA GB10, 20 çekirdek), yük üreteci olarak evo (32 çekirdekli x86). Yükü makineden almak spark'ın çekirdeklerini hizmete boşaltmalıydı — yükselmesini bekliyorduk.

Aşağı gitti. 194k req/sec. Daha sert itmek için bağlantı ekleyince kötüleşti: 4,000 bağlantıda 167k, 8,000 bağlantıda 154k, p99 gecikmesi 5 ms'den 52 ms'ye çıktı. Bu ders kitabı gibi bir tıkanıklık çökmesi.

Ama bağlantı yalnızca ~70 MB/s idi — 2.5 GbE'nin yüzde 22'si. Bant değil. Ve spark'ın 20 çekirdeği çoğunlukla boştu. CPU değil. Peki ne?

Suçlu: tek bir alma kuyruğu

ethtool -l ele verdi: NIC tek bir donanım RX kuyruğu ile çalışıyordu. Gelen her paketi bir çekirdeğin softirq'u işliyordu — ve o çekirdek tavandı. Daha kötüsü, sürücü kuyruk eklemeyi düpedüz reddetti (ethtool -L → "Operation not supported").

Böylece elimizdeki en hızlı geliştirici makinesi, on dokuz çekirdek izlerken paket küreyen tek bir çekirdek iş parçacığı tarafından kısılıyordu.

Düzeltme: paketleri yazılımda yönlendirmek

Donanım çoklu kuyruk istemeyince çekirdek ister: Receive Packet Steering akışları yazılımda çekirdeklere yayar. RPS'yi (ve RFS/XPS'i) 20 çekirdeğin tümünde açtık, alma birikimini 1,000'den 300,000'e çıkardık, 256 MiB soket tamponu koyduk ve performance governor'ını sabitledik. İki makinede de çalışan idempotent bir tune.sh.

Aynı tel. Aynı işleyici. 518,000 req/sec — 2.67× sıçrama, 696k tepede.

eşzamanlılıkayarsızayarlı
1,000194k518k
4,000154k474k (tepe 696k)

Asıl nokta: ayarlı ağ sayısı (518k) artık loopback değerini (365–400k) geçiyor, çünkü tel üzerinde spark'ın çekirdekleri yük üreteciyle paylaşmak yerine tam zamanlı hizmet veriyor. 518k düzeyinde makine yalnızca ~%12 boşta. Bu, tek bir yükleyicinin gerçek tavanına yakın.

Üçüncü: ZAP daha hızlı olmalıydı — biz de öyle yaptık

zip ZAP'a özgüdür — çıplak bir dinleme adresi ikili taşımayı konuşur; http:// HTTP konuşur. İkili taşımanın kazanmasını bekledik, bir ZAP yük istemcisi kurduk (HTTP araçları onu konuşamaz) ve ölçtük.

ZAP 91k yaptı. HTTP 481k yaptı. Yerel taşıma kaybetti — 5× ile.

Peşine düştük, ve tel değil — kodek. Gecikme kuyruğu hikâyeyi anlattı (p50 sağlıklı 303 µs, ama p99 13 ms'de — bir çöp toplama imzası), ve GOGC değerini 100'den 800'e gevşetmek ZAP'ı 203k'ye iki kattan fazla çıkardı. Altı saniyelik bir koşuda ZAP sunucusu 2,203 çöp toplama tetikledi; HTTP sunucusu 57. Bunu üç ayırma yeri taşıyordu: marshaler her çağrıda taze bir çerçeve tamponu basar, başlık çözümü her diziyi bir eşleme kopyalar ve — büyüğü — zaphttp sunucusu istek bağlamını istek başına yeniden ayırırken fasthttp sunucusu onu bağlantı başına havuza koyar.

Biz de düzelttik. Değişiklik (zap-proto/http, dal perf/zero-alloc-codec) mimari değil, mekanik: istek bağlamı bağlantı başına bir kez havuza alınır, elde yazılmış bir çerçeve kodlayıcı havuz tamponuna ekler ve 4 baytlık uzunluk öneki bufio.Peek/Discard üzerinden boşaltılır, böylece hiçbir başlık dizisi yığına kaçmaz. Hizmet döngüsü o yeniden kullanılan tampona AppendResponse ile serileştirir ve başlıkları yerinde çözer — böylece ısınmış bir bağlantıda istek başına hiçbir şey ayırmaz — ve golden testler telin önce ve sonra bayt bayt aynı olduğunu doğrular. Salt kodek yeniden yazımı, protokol değişikliği yok.

Sonuç: ZAP 91k → 570k req/sec gider; bu, 6.3× oranında bir kaldırmadır ve HTTP'nin 481k değerini geçer — ZAP artık loopback üzerinde 1.18× kazanır. p99 13 ms'den 1.19 ms'ye çöker ve GC 2,203'ten 62'ye düşer.

Kendimi düzeltmem gereken iki şey var — ikisi de düzeltmeden sağ çıktı, o yüzden açıkça söylemeliyim. Birincisi, kazanç CPU'dadır, telde değil. ZAP çerçevesi aslında HTTP'den daha büyüktür (bir /health isteği için 124'e karşı 75 bayt); ZAP daha az baytla değil, bayt başına daha az çevrim harcayarak kazanır. İkincisi, zaphttp JSON'u atlamaz. HTTP gövdesini her iki taşımada da opak bayt olarak taşır, bu yüzden bir işleyici her iki yolda da aynı marshal bedelini öder — sohbet biçimli bir JSON yükünde ZAP aslında HTTP'nin 0.95× kadarıdır, gövdeyi çerçeve kuyruğuna kopyalamaktan küçük bir kayıp. JSON'u elemek, yerel tipli ZAP RPC'nin bir özelliğidir, zaphttp uygulamasının çalıştırmadığı ayrı bir eksen. Buradaki loopback kazancı dürüsttür ve yalnızca CPU ile ayırma kazancıdır. Peki gerçek 2.5 GbE telinde? İki taşıma da ~530k düzeyinde berabere — bağlantı doymuştur ve kodek artık darboğaz değildir.

Dördüncü: ZAP JSON'u gerçekten atlar mı?

O son satır — «JSON'u elemek, zaphttp uygulamasının çalıştırmadığı yerel bir ZAP özelliğidir» — bir senettir ve okur bunu bana tahsil ettirmelidir. zaphttp gövdeyi opak taşır, bu yüzden ikili protokollerin kazanması beklenen şeyi hiç sınamaz: metin ayrıştırmadan yapılandırılmış veri okumak. Ben de bunu yapan testi kurdum.

Paylaşılan bir kayıt — eth_getBalance biçimli bir çağrı, her yöne dört alan (account, block, id → balance, nonce, blockHash) — dört yoldan gidip geldi: standart kitaplık encoding/json, goccy/go-json ve sonic (Go'daki en hızlı iki JSON kitaplığı) ve zap-proto/go tipli ikilisi. Aynı alanlar içeri, aynı hesap, aynı alanlar dışarı; bir gidiş-dönüş eşdeğerlik testi, iki tarafın da diğerinin yaptığı işi atlamaması için bayt bayt aynı yeniden kodlamayı zorlar. Ve JSON'u standart kitaplıkta değil, en hızlı uygulamasında tuttum, kimse sonucu savuşturamasın diye.

Çözüm bir kıyımdır. Dört alanlı isteği okurken:

kodekçözümayırmalar
JSON standart kitaplık2334 ns13
JSON sonic845 ns8
JSON goccy473 ns4
tipli ZAP13 ns0

On üç nanosaniye, sıfır ayırma — en hızlı JSON'dan 35×, standart kitaplıktan 174× daha hızlı. Ayrıştırma yok: alan bilinen bir ofsette durur, okursunuz. Tipli bir ikili telin bütün vaadi budur ve tutar.

Ama ZAP'ı övmeyen yanları da bildireceğime söz verdim, o yüzden:

  • Kodlama berabere, ve daha çok ayırır. ZAP 629 ns'de 12 ayırmayla serileştirir, goccy'nin 426 ns / 4 değerine karşı. Yayımlanan zap-proto/go v1.3.0 kurucusu değişken uzunluktaki kuyrukları bir kopyayla erteler ve nesne kurucusu yığına kaçar; bunu düzelten havuzlu sabit genişlik API'si yayımlanan etikette yok, o yüzden dürüst sayı budur. Net olarak tam sunucu işleyicisi (çöz + hesapla + kodla) ZAP'ı tutar — goccy'nin 431'ine karşı 274 ns — çünkü çözüm kazancı kodlama beraberliğini boğar. Ama bunu taşıyan çözümdür.
  • Uçtan uca 174× yaklaşık ~1.2× düzeyine iner. Gerçek bir /rpc uç noktasında: ZAP 535k req/s, sonic 483k (1.11×), goccy 452k (1.18×), standart kitaplık 407k (1.31×). Gerçek, ama mikro ölçüm aralığına hiç yakın değil — çünkü saniyede yarım milyon istekte soket ve zamanlayıcı maliyetin kendisidir, serileştirme ise ince bir dilimdir. Kodek, hızlandığını fark etmenizden çok önce darboğaz olmaktan çıkar.

ZAP'ın lehine kesen bir sürpriz: gerçek yapılandırılmış bir kayıtta teli daha küçük, daha büyük değil — 162'ye karşı 233 bayt — çünkü JSON baytlarını 0x onaltılık dizilere, ondalık tamsayılara, tırnaklara ve alan adlarına harcar. Çerçeveleme testinden çıkan «ZAP daha büyük» uyarısı yalnızca /health boyutunda önemsiz yükler için geçerlidir; gerçek tipli alanlar verin, ikili biçim boyutta da kazanır.

Yani, dürüstçe: ZAP JSON'u atlar mı? Evet — ve okuma yolunda yakın bile değildir. Bunu hissetmeniz, serileştirmenin darboğazınız olup olmadığına bağlıdır. Küçücük kayıtların 500k rps'inde değildir. Şişman iç içe yükler ya da çözüm ağırlıklı bir fan-out koyun, o 174× yaşadığınız yerde görünmeye başlar.

Ne öğrendik

  • Yolun tamamını ölçün ve aracı sorgulayın. hey bizi ~100k'de tavana vurdurdu — o istemciydi, sunucu değil. bombardier 4× daha fazlasını gösterdi.
  • Makine nadiren ilk darboğazdır. Tek bir NIC RX kuyruğu, uygulama kodundan bir satıra dokunmadan önce bize 2.67× mal oldu.
  • Yalnızca aktarımı değil, ayırmaları da ölçün. ZAP'ın «yavaş olması», açıkça bir kodek sorunu olmaktan bir GOGC bayrağı uzaktaydı — bağlamı havuza alıp ayırmaları sıfırlayınca yerel taşıma HTTP'yi geçti.
  • Bir mikro ölçüm ile bir yük testi farklı sorulara yanıt verir. Tipli çözüm yalıtımda JSON'dan 174×, uçtan uca 1.2× daha hızlıdır — iki sayı da doğrudur ve yalnız birini aktarmak eksiltme yalanı olur.
  • Adil ve gerçek bildirin. Aynı işleyici, ısınmış bağlantılar, elle seçilmiş GOGC yok, JSON en yavaş kitaplığında değil en hızlısında tutuldu, betik yayımlandı. Dürüst sayı bir kayıpken — kodlama beraberliği, sıkışmış uçtan uca pay — kaybı da yayımladık.

Sıfır ayırmalı ZAP kodeki bitti ve kazanıyor. Kalan duraklar fiziksel: çift NIC'li bir yükleyici (yedi haneye doğru paralel alma yolları), daha hızlı ara bağlantı (10 GbE / RDMA), büyük yüklerde ZAP çerçeve kuyruğunun bir gövde kopyasını atlaması için bir writev ayrımı, ve Go 1.26'nın bize yerli verdiği kuantum sonrası taşımalar — PQ-TLS 1.3, ZAP-over-PQ-TLS ve PQ-QUIC — burada ML-KEM el sıkışmasının bedelini kararlı durum aktarımından ayıracağız.

Bunu gerektiği kadar ileri götürebiliriz.

Yorumlar

Yorum yapmak için Hanzo ID ile giriş yap.

Yorumlar yükleniyor…

Devamını oku