ร่วมเบตาของ Hanzo แล้วรับส่วนลด 50% สำหรับทุกแผนแบบชำระเงิน

ครึ่งล้าน RPS บน GB10: คอขวดคือเครือข่าย ไม่ใช่ตัวเครื่อง

เราอยากรู้ว่าเพลนข้อมูลคลาวด์ของเราวิ่งได้เร็วแค่ไหนบน NVIDIA GB10 คำตอบสอนเรื่องคิวรับของ NIC มากกว่าเรื่องโค้ดของเราเอง — และเพิ่มปริมาณงานบนสายเส้นเดิมมากกว่าสองเท่า

ในหน้านี้

เราสร้างคลาวด์บนไบนารี Go ไฟล์เดียว — อาร์ทิแฟกต์เดียวที่เมานต์เกตเวย์ AI, การค้า, IAM และ KMS แล้วให้บริการ api.hanzo.ai พื้นผิว HTTP ของมันคือ zip เฟรมเวิร์กเว็บที่เป็นของ ZAP โดยกำเนิด หุ้มบน fasthttp เราจึงถามคำถามง่ายๆ ว่า ของสิ่งนี้ไปได้เร็วแค่ไหนจริงๆ แล้วอะไรคือตัวหยุด

คำตอบที่ซื่อสัตย์ทำให้เราประหลาดใจสามครั้ง นี่คือเรื่องสืบทั้งเรื่อง พร้อมตัวเลข ทุกอย่างเปิดสาธารณะใน hanzoai/benchmarks

ข้อแรก: เฟรมเวิร์กคือภาษีหรือไม่

ไม่ zip แบกประมาณ 24 นาโนวินาทีคงที่ และการจองหน่วยความจำครั้งเดียวขนาด 48 ไบต์ เหนือเอนจิน fiber/fasthttp ดิบที่มันหุ้ม — ต่ำกว่า 1% บนแฮนด์เลอร์จริงใดๆ จากต้นจนจบ บนลูปแบ็ก zip ทำได้ 0.95× ของ fasthttp ดิบ เฟรมเวิร์กเป็นแค่สัญญาณรบกวน ดี ไปต่อ

เฟรมเวิร์กคำขอ/วินาที
fasthttp ดิบ365–400k
zip (HTTP)338–348k

ข้อสอง: ผลแบบกระจายกลับ ช้าลง

ตรงนี้เริ่มน่าสนใจ เรามีสองเครื่องบนลิงก์สายตรง 2.5 GbE: spark (NVIDIA GB10, 20 คอร์) เป็นเซิร์ฟเวอร์ evo (x86 32 คอร์) เป็นตัวสร้างโหลด การย้ายโหลดออกจากเครื่องควรปลดคอร์ของ spark ให้ไปให้บริการ — เราคาดว่าตัวเลขจะ สูงขึ้น

มัน ต่ำลง 194k คำขอ/วินาที และเมื่อเราเพิ่มการเชื่อมต่อเพื่อดันให้หนักขึ้น มัน แย่ลง: 167k ที่ 4,000 การเชื่อมต่อ, 154k ที่ 8,000 โดยเวลาแฝง p99 ไต่จาก 5 ms ถึง 52 ms นั่นคือ การพังเพราะความแออัด ตามตำรา

แต่ลิงก์อยู่ที่ประมาณ ~70 MB/s เท่านั้น — 22% ของ 2.5 GbE ไม่ใช่แบนด์วิดท์ และ 20 คอร์ของ spark ส่วนใหญ่ยังว่าง ไม่ใช่ CPU แล้วอะไรเล่า

ตัวการ: คิวรับเพียงหนึ่ง

ethtool -l เปิดโปง: NIC ทำงานด้วย คิว RX ของฮาร์ดแวร์เพียงคิวเดียว ทุกแพ็กเก็ตขาเข้าถูกประมวลผลโดย softirq ของคอร์ เดียว — และคอร์นั้นคือเพดาน ที่แย่กว่านั้น ไดรเวอร์ปฏิเสธการเพิ่มคิวอย่างตรงไปตรงมา (ethtool -L → "Operation not supported")

ดังนั้นเครื่องพัฒนาที่เร็วที่สุดที่เรามีจึงถูกบีบโดยเธรดเคอร์เนลเดียวที่ตักแพ็กเก็ต ขณะที่สิบเก้าคอร์ยืนดู

ทางแก้: บังคับทิศแพ็กเก็ตในซอฟต์แวร์

เมื่อฮาร์ดแวร์ไม่ยอมหลายคิว เคอร์เนลยอม — การบังคับทิศแพ็กเก็ตรับ แฮชโฟลว์กระจายไปตามคอร์ในซอฟต์แวร์ เราเปิด RPS (และ RFS/XPS) ครบทั้ง 20 คอร์ ยกแบ็กล็อกการรับจาก 1,000 เป็น 300,000 ตั้งบัฟเฟอร์ซ็อกเก็ต 256 MiB และปักตัวกำกับประสิทธิภาพไว้ สคริปต์เดียวที่รันซ้ำได้ผลเดิม tune.sh รันบนทั้งสองเครื่อง

สายเดิม แฮนด์เลอร์เดิม 518,000 คำขอ/วินาที — กระโดด 2.67× สูงสุดที่ 696k

ความพร้อมกันยังไม่จูนจูนแล้ว
1,000194k518k
4,000154k474k (สูงสุด 696k)

จุดที่สะดุดตา: ตัวเลข เครือข่าย ที่จูนแล้ว (518k) ตอนนี้ชนะ ลูปแบ็ก (365–400k) — เพราะบนสาย คอร์ของ spark ให้บริการเต็มเวลา แทนที่จะแบ่งกับตัวสร้างโหลด ที่ 518k เครื่องว่างเพียง ~12% นั่นใกล้เพดานจริงจากตัวโหลดเครื่องเดียว

ข้อสาม: ZAP ควรจะเร็วกว่า — เราจึงทำให้มันเร็ว

zip เป็น ของ ZAP โดยกำเนิด — ที่อยู่สำหรับรอรับเปล่าๆ พูดทรานสปอร์ตไบนารีของเรา ส่วน http:// พูด HTTP เราคาดว่าทรานสปอร์ตไบนารีจะชนะ จึงสร้างไคลเอนต์โหลด ZAP (เครื่องมือ HTTP พูดกับมันไม่ได้) แล้ววัด

ZAP ได้ 91k HTTP ได้ 481k ทรานสปอร์ตดั้งเดิม แพ้ — ถึง 5×

เราไล่ตาม และไม่ใช่สาย — เป็นตัวแปลงรหัส หางของเวลาแฝงเล่าเรื่อง (p50 สุขภาพดีที่ 303 µs แต่ p99 ที่ 13 ms — ลายเซ็นของการเก็บกวาดขยะ) และการผ่อน GOGC จาก 100 เป็น 800 เพิ่ม ZAP มากกว่าสองเท่า เป็น 203k ในการรันหกวินาที เซิร์ฟเวอร์ ZAP ก่อการเก็บกวาดขยะ 2,203 ครั้ง เซิร์ฟเวอร์ HTTP ทำ 57 สามจุดจองหน่วยความจำแบกเรื่องนี้: ตัว marshal สร้างบัฟเฟอร์เฟรมใหม่ทุกครั้งที่เรียก การถอดรหัสส่วนหัวก็อปปี้ทุกสตริงเข้าแมป และ — จุดใหญ่ — เซิร์ฟเวอร์ของ zaphttp จองบริบทคำขอใหม่ ต่อคำขอ ในขณะที่เซิร์ฟเวอร์ของ fasthttp เองรวมมันไว้ ต่อการเชื่อมต่อ

เราจึงแก้ การเปลี่ยนแปลง (zap-proto/http สาขา perf/zero-alloc-codec) เป็นงานกลไก ไม่ใช่งานสถาปัตย์: รวมบริบทคำขอครั้งเดียวต่อการเชื่อมต่อ เขียนตัวเข้ารหัสเฟรมด้วยมือที่ต่อท้ายลงบัฟเฟอร์ที่รวมไว้ และระบายคำนำหน้าความยาว 4 ไบต์ผ่าน bufio.Peek/Discard เพื่อไม่ให้อาร์เรย์ส่วนหัวหนีขึ้นฮีป วงให้บริการทำให้เป็นอนุกรมด้วย AppendResponse ลงบัฟเฟอร์ที่ใช้ซ้ำนั้น และถอดรหัสส่วนหัวในที่ — ดังนั้นบนการเชื่อมต่อที่อุ่นแล้ว มันจอง ไม่มีอะไรต่อคำขอ — และการทดสอบทองคำยืนยันว่าสาย เหมือนกันทีละไบต์ ก่อนและหลัง เขียนตัวแปลงรหัสใหม่ล้วนๆ ไม่เปลี่ยนโปรโตคอล

ผลลัพธ์: ZAP ไปที่ 91k → 570k คำขอ/วินาที ยกขึ้น 6.3× ซึ่ง แซง 481k ของ HTTP — ตอนนี้ ZAP ชนะ 1.18× บนลูปแบ็ก โดย p99 ทรุดจาก 13 ms เหลือ 1.19 ms และ GC ลดจาก 2,203 เหลือ 62

มีสองเรื่องที่ฉันต้องแก้ความเข้าใจเอง — และทั้งคู่ยังจริงหลังการแก้ จึงควรพูดให้ตรง ข้อแรก ชัยชนะอยู่ที่ CPU ไม่ใช่สาย เฟรมของ ZAP จริงๆ ใหญ่กว่า HTTP (124 เทียบกับ 75 ไบต์สำหรับคำขอ /health) ZAP ชนะเพราะใช้รอบน้อยลงต่อไบต์ ไม่ใช่เพราะใช้ไบต์น้อยลง ข้อสอง zaphttp ไม่ได้ข้าม JSON มันถือตัว HTTP เป็นไบต์ทึบแสงบนทั้งสองทรานสปอร์ต ดังนั้นแฮนด์เลอร์จ่ายต้นทุน marshal เท่ากันทั้งสองทาง — บนเพย์โหลด JSON รูปแชต ZAP จริงๆ คือ 0.95× ของ HTTP แพ้เล็กน้อยเพราะก็อปปี้ตัวลงหางเฟรม การละ JSON เป็นคุณสมบัติของ RPC แบบมีชนิดที่เป็น ของดั้งเดิม ของ ZAP คนละแกนที่ zaphttp ไม่ได้ใช้ ชัยชนะบนลูปแบ็กตรงนี้ซื่อสัตย์ และเป็นชัยชนะของ CPU กับการจองหน่วยความจำล้วนๆ แล้วบนสาย 2.5 GbE จริงล่ะ ทั้งสองทรานสปอร์ต เสมอที่ ~530k — ลิงก์อิ่มตัว และตัวแปลงรหัสไม่ใช่คอขวดอีกต่อไป

ข้อสี่: แล้ว ZAP ข้าม JSON จริงหรือ

ประโยคสุดท้ายนั้น — «การละ JSON เป็นคุณสมบัติของ ZAP ดั้งเดิมที่ zaphttp ไม่ได้ใช้» — เป็นตั๋วสัญญา และผู้อ่านควรทวงให้ฉันใช้หนี้ zaphttp ถือตัวแบบทึบแสง ดังนั้นมันไม่เคยทดสอบสิ่งที่โปรโตคอลไบนารี ควร จะชนะ นั่นคือการอ่านข้อมูลที่มีโครงสร้างโดยไม่แยกวิเคราะห์ข้อความ ฉันจึงสร้างการทดสอบที่ทำสิ่งนั้น

เรกคอร์ดที่ใช้ร่วมกันหนึ่งรายการ — การเรียกรูป eth_getBalance สี่ฟิลด์แต่ละทาง (account, block, id → balance, nonce, blockHash) — ไปกลับสี่วิธี: encoding/json ของไลบรารีมาตรฐาน, goccy/go-json และ sonic (ไลบรารี JSON ที่เร็วที่สุดสองตัวใน Go) และไบนารีแบบมีชนิดของ zap-proto/go ฟิลด์เข้าเหมือนกัน การคำนวณเหมือนกัน ฟิลด์ออกเหมือนกัน การทดสอบความสมมูลไปกลับบังคับการเข้ารหัสซ้ำให้ตรงทีละไบต์ เพื่อไม่ให้ฝ่ายใดข้ามงานที่อีกฝ่ายทำ และฉันยึด JSON ไว้ที่การทำให้เป็นจริงที่ เร็วที่สุด ของมัน ไม่ใช่ไลบรารีมาตรฐาน เพื่อไม่ให้ใครปัดผลทิ้ง

การถอดรหัสคือการสังหาร การอ่านคำขอสี่ฟิลด์:

ตัวแปลงรหัสถอดรหัสการจอง
JSON มาตรฐาน2334 ns13
JSON sonic845 ns8
JSON goccy473 ns4
ZAP แบบมีชนิด13 ns0

สิบสามนาโนวินาที การจองเป็นศูนย์ — เร็วกว่า JSON ที่เร็วที่สุด 35× เร็วกว่าไลบรารีมาตรฐาน 174× ไม่มีการแยกวิเคราะห์: ฟิลด์อยู่ออฟเซ็ตที่รู้แล้ว คุณอ่านมัน นั่นคือข้อเสนอทั้งหมดของสายไบนารีแบบมีชนิด และมันยืนอยู่ได้

แต่ฉันสัญญาว่าจะรายงานส่วนที่ไม่ประจบ ZAP ดังนั้น:

  • การเข้ารหัสเสมอกัน และมันจอง มากกว่า ZAP ทำให้เป็นอนุกรมใน 629 ns ด้วยการจอง 12 ครั้ง เทียบกับ 426 ns / 4 ของ goccy ตัวสร้าง zap-proto/go v1.3.0 ที่เผยแพร่เลื่อนหางความยาวแปรผันผ่านการก็อปปี้ และปล่อยตัวสร้างออบเจกต์หนีขึ้นฮีป API ความกว้างคงที่ที่รวมไว้ซึ่งแก้เรื่องนี้ยังไม่อยู่ในแท็กที่ปล่อย ดังนั้นนี่คือตัวเลขที่ซื่อสัตย์ สุทธิแล้ว แฮนด์เลอร์เซิร์ฟเวอร์เต็ม (ถอดรหัส + คำนวณ + เข้ารหัส) เข้าข้าง ZAP จริง — 274 ns เทียบกับ 431 ของ goccy — เพราะชัยชนะของการถอดรหัสท่วมความเสมอกันของการเข้ารหัส แต่การถอดรหัสต่างหากที่แบกไว้
  • จากต้นจนจบ 174× หดเหลือ ~1.2× บนเอนด์พอยต์ /rpc จริง: ZAP 535k คำขอ/วินาที, sonic 483k (1.11×), goccy 452k (1.18×), ไลบรารีมาตรฐาน 407k (1.31×) จริง แต่ไม่ใกล้ช่องว่างของไมโครเบนช์มาร์ก — เพราะที่ครึ่งล้านคำขอต่อวินาที ซ็อกเก็ตกับตัวจัดตาราง คือ ต้นทุน และการทำให้เป็นอนุกรมเป็นเพียงชิ้นบาง ตัวแปลงรหัสเลิกเป็นคอขวดนานก่อนที่คุณจะสังเกตว่ามันเร็วขึ้น

ความประหลาดใจหนึ่งที่เข้าข้าง ฝั่ง ZAP: บนเรกคอร์ดที่มีโครงสร้างจริง สายของมัน เล็กกว่า ไม่ใหญ่กว่า — 162 เทียบกับ 233 ไบต์ — เพราะ JSON จ่ายไบต์ให้สตริงฐานสิบหก 0x จำนวนเต็มฐานสิบ เครื่องหมายคำพูด และชื่อฟิลด์ ข้อแม้ «ZAP ใหญ่กว่า» จากการทดสอบเฟรมใช้ได้เฉพาะเพย์โหลดจิ๋วขนาด /health ให้ฟิลด์แบบมีชนิดจริงแก่มัน แล้วไบนารีชนะเรื่องขนาดด้วย

ดังนั้น พูดตรงๆ: ZAP ข้าม JSON หรือไม่ ใช่ — และบนเส้นทางอ่าน ช่องว่างไม่ใกล้เลย คุณจะ รู้สึก ถึงมันหรือไม่ขึ้นกับว่าการทำให้เป็นอนุกรมเป็นคอขวดของคุณหรือเปล่า ที่ 500k rps ของเรกคอร์ดเล็กๆ ไม่ใช่ สลับเป็นเพย์โหลดซ้อนหนา หรือการกระจายที่หนักด้านการถอดรหัส แล้ว 174× นั้นจะเริ่มปรากฏตรงที่คุณอยู่

สิ่งที่เราเรียนรู้

  • วัดทั้งเส้นทาง และตั้งคำถามกับเครื่องมือ hey จำกัดเราไว้ที่ ~100k — นั่นคือ ไคลเอนต์ ไม่ใช่เซิร์ฟเวอร์ bombardier เผยให้เห็นมากกว่า 4×
  • ตัวเครื่องไม่ค่อยเป็นคอขวดแรก คิว RX เดียวของ NIC ทำให้เราเสีย 2.67× ก่อนที่เราจะแตะโค้ดแอปพลิเคชันแม้แต่บรรทัดเดียว
  • วัดการจองหน่วยความจำ ไม่ใช่แค่ปริมาณงาน ความ «ช้า» ของ ZAP ห่างจากความชัดว่าเป็นปัญหาตัวแปลงรหัสเพียงธง GOGC เดียว — และเมื่อเรารวมบริบทและทำให้การจองเป็นศูนย์ ทรานสปอร์ตดั้งเดิมก็แซง HTTP
  • ไมโครเบนช์มาร์กกับการทดสอบโหลดตอบคนละคำถาม การถอดรหัสแบบมีชนิดเร็วกว่า JSON 174× เมื่อแยกวัด และเร็วกว่า 1.2× จากต้นจนจบ — ทั้งสองตัวเลขจริง และการอ้างเพียงตัวเดียวคือการโกหกโดยการละไว้
  • รายงานให้ยุติธรรมและจริง แฮนด์เลอร์เดียวกัน การเชื่อมต่อที่อุ่นแล้ว ไม่เลือก GOGC ที่เข้าข้าง JSON ถูกยึดไว้ที่ไลบรารีที่เร็วที่สุดไม่ใช่ช้าที่สุด และเผยแพร่สคริปต์ เมื่อตัวเลขที่ซื่อสัตย์คือความพ่ายแพ้ — ความเสมอกันของการเข้ารหัส ระยะขอบจากต้นจนจบที่ถูกบีบ — เราก็ส่งความพ่ายแพ้นั้นไปด้วย

ตัวแปลงรหัส ZAP ที่ไม่จองหน่วยความจำเสร็จแล้ว และมันชนะ จุดหยุดที่เหลือเป็นเรื่องกายภาพ: ตัวโหลดสอง NIC (เส้นทางรับขนานสู่ตัวเลขเจ็ดหลัก) การเชื่อมต่อที่เร็วขึ้น (10 GbE / RDMA) การแยก writev เพื่อให้หางเฟรมของ ZAP ข้ามการก็อปปี้ตัวหนึ่งครั้งบนเพย์โหลดใหญ่ และทรานสปอร์ตหลังควอนตัมที่ Go 1.26 ให้เรามาโดยกำเนิด — PQ-TLS 1.3, ZAP-over-PQ-TLS และ PQ-QUIC — ที่ซึ่งเราจะแยกต้นทุนการจับมือ ML-KEM ออกจากปริมาณงานในสภาวะคงตัว

เราพาเรื่องนี้ไปได้ไกลเท่าที่จำเป็น

ความคิดเห็น

ลงชื่อเข้าใช้ด้วย Hanzo ID เพื่อแสดงความคิดเห็น

กำลังโหลดความคิดเห็น…

อ่านต่อ

Hanzo DNS and Networking: Programmable Infrastructure

Hanzo DNS and Networking: Programmable Infrastructure

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

BitDelta: ให้บริการโมเดล Zen 14 แบบจากคลัสเตอร์ประมวลผลเดียว

BitDelta: ให้บริการโมเดล Zen 14 แบบจากคลัสเตอร์ประมวลผลเดียว

การบีบอัดเดลตา 1 บิตของ BitDelta ทำให้เราให้บริการโมเดล Zen 14 แบบจากโครงสร้างพื้นฐานประมวลผลที่ใช้ร่วมกัน — คณิตศาสตร์ สถาปัตยกรรม และข้อแลกเปลี่ยน

จากเอไอพาณิชย์สู่โมเดลแนวหน้า: ทศวรรษโครงสร้างพื้นฐานของ Hanzo

จากเอไอพาณิชย์สู่โมเดลแนวหน้า: ทศวรรษโครงสร้างพื้นฐานของ Hanzo

สิบสองปีแห่งการสร้างโครงสร้างพื้นฐานด้านปัญญาประดิษฐ์ — จากเอนจินแนะนำแบบเรียลไทม์ในปี 2014 ถึงโมเดลแนวหน้า 94+ รายการและบล็อกเชนหลังควอนตัมในปี 2026 เรื่องราวเชิงเทคนิคของเส้นทางที่พาเรามาถึงจุดนี้