Tham gia bản thử Hanzo và được giảm 50% mọi gói trả phí

Nửa triệu RPS trên GB10: Nút thắt là mạng, không phải máy

Chúng tôi muốn xem mặt phẳng dữ liệu đám mây của mình chạy nhanh đến đâu trên NVIDIA GB10. Câu trả lời dạy chúng tôi về hàng đợi nhận của NIC nhiều hơn về chính mã của mình — và tăng hơn gấp đôi thông lượng trên đúng đường dây ấy.

Trong trang này

Chúng tôi xây đám mây của mình trên một tệp nhị phân Go duy nhất — một tạo tác gắn cổng AI, thương mại, IAM và KMS rồi phục vụ api.hanzo.ai. Bề mặt HTTP của nó là zip, khung web gốc ZAP của chúng tôi, bọc trên fasthttp. Nên chúng tôi hỏi một câu đơn giản: thứ này thực sự chạy nhanh đến đâu, và cái gì chặn nó?

Câu trả lời thành thật làm chúng tôi ngạc nhiên ba lần. Đây là toàn bộ câu chuyện điều tra, kèm số liệu. Mọi thứ đều công khai trong hanzoai/benchmarks.

Thứ nhất: khung có phải là khoản thuế không?

Không. zip mang khoảng 24 nano giây không đổi và một lần cấp phát 48 byte trên động cơ fiber/fasthttp thô mà nó bọc — dưới 1% với bất kỳ trình xử lý thật nào. Từ đầu đến cuối, trên loopback, zip đạt 0.95× fasthttp thô. Khung là nhiễu. Tốt. Đi tiếp.

khungyêu cầu/giây
fasthttp thô365–400k
zip (HTTP)338–348k

Thứ hai: kết quả phân tán lại chậm hơn

Đây là chỗ thú vị. Chúng tôi có hai máy trên một liên kết dây trực tiếp 2.5 GbE: spark (một NVIDIA GB10, 20 lõi) làm máy chủ, evo (x86 32 lõi) làm bộ sinh tải. Đưa tải ra khỏi máy lẽ ra phải giải phóng lõi của spark để phục vụ — chúng tôi tưởng con số sẽ tăng.

Nó giảm. 194k yêu cầu/giây. Và khi chúng tôi thêm kết nối để đẩy mạnh hơn, tình hình tệ hơn: 167k ở 4,000 kết nối, 154k ở 8,000, với độ trễ p99 leo từ 5 ms lên 52 ms. Đó là sụp đổ tắc nghẽn đúng như sách giáo khoa.

Nhưng liên kết chỉ ở ~70 MB/s — 22% của 2.5 GbE. Không phải băng thông. Và 20 lõi của spark phần lớn đang nghỉ. Không phải CPU. Vậy là gì?

Thủ phạm: một hàng đợi nhận

ethtool -l tố cáo: NIC đang chạy với một hàng đợi RX phần cứng duy nhất. Mọi gói đến đều được softirq của một lõi xử lý — và lõi đó chính là trần. Tệ hơn, trình điều khiển từ chối thẳng việc thêm hàng đợi (ethtool -L → "Operation not supported").

Thế là chiếc máy phát triển nhanh nhất chúng tôi có bị một luồng nhân bóp nghẹt khi xúc gói, trong khi mười chín lõi đứng nhìn.

Cách sửa: lái gói trong phần mềm

Khi phần cứng không chịu đa hàng đợi, nhân sẽ chịu — Lái gói nhận băm các luồng lên các lõi trong phần mềm. Chúng tôi bật RPS (và RFS/XPS) trên cả 20 lõi, nâng backlog nhận từ 1,000 lên 300,000, đặt bộ đệm socket 256 MiB, và ghim bộ điều tốc hiệu năng. Một tune.sh bất biến, chạy trên cả hai máy.

Cùng đường dây. Cùng trình xử lý. 518,000 yêu cầu/giây — bước nhảy 2.67×, đỉnh 696k.

đồng thờichưa chỉnhđã chỉnh
1,000194k518k
4,000154k474k (đỉnh 696k)

Điểm đáng nói: con số mạng đã chỉnh (518k) giờ vượt loopback (365–400k) — vì trên dây, lõi của spark phục vụ toàn thời gian thay vì chia sẻ với bộ sinh tải. Ở 518k máy chỉ nghỉ khoảng ~12%. Đó sát trần thật từ một bộ nạp duy nhất.

Thứ ba: ZAP lẽ ra phải nhanh hơn — nên chúng tôi đã làm cho nó nhanh hơn

zip gốc ZAP — một địa chỉ lắng nghe trần nói giao vận nhị phân của chúng tôi; http:// nói HTTP. Chúng tôi tưởng giao vận nhị phân sẽ thắng, dựng một máy khách tải ZAP (công cụ HTTP không nói được nó), rồi đo.

ZAP đạt 91k. HTTP đạt 481k. Giao vận gốc thua — 5×.

Chúng tôi đuổi theo, và không phải đường dây — mà là codec. Đuôi độ trễ kể câu chuyện (p50 khỏe ở 303 µs, nhưng p99 ở 13 ms — chữ ký thu gom rác), và nới GOGC từ 100 lên 800 tăng hơn gấp đôi ZAP lên 203k. Trong một lần chạy sáu giây, máy chủ ZAP kích 2,203 lần thu gom rác; máy chủ HTTP, 57. Ba chỗ cấp phát gánh nó: bộ marshal đúc một bộ đệm khung mới mỗi lần gọi, giải mã tiêu đề chép mọi chuỗi vào một map, và — chỗ lớn — máy chủ của zaphttp cấp phát một ngữ cảnh yêu cầu mới mỗi yêu cầu trong khi máy chủ của chính fasthttp gom nó mỗi kết nối.

Nên chúng tôi sửa. Thay đổi (zap-proto/http, nhánh perf/zero-alloc-codec) là cơ học, không phải kiến trúc: gom ngữ cảnh yêu cầu một lần mỗi kết nối, viết tay bộ mã hóa khung nối vào bộ đệm đã gom, và rút tiền tố độ dài 4 byte qua bufio.Peek/Discard để không mảng tiêu đề nào thoát ra heap. Vòng phục vụ tuần tự hóa bằng AppendResponse vào bộ đệm dùng lại đó và giải mã tiêu đề tại chỗ — nên trên một kết nối đã ấm nó cấp phát không gì mỗi yêu cầu — và các bài kiểm vàng xác nhận dây giống từng byte trước và sau. Viết lại codec thuần, không đổi giao thức.

Kết quả: ZAP đi 91k → 570k yêu cầu/giây, mức nâng 6.3× vượt 481k của HTTP — ZAP giờ thắng 1.18× trên loopback, với p99 sụp từ 13 ms xuống 1.19 ms và GC giảm từ 2,203 xuống 62.

Hai điều tôi phải tự sửa — và cả hai vẫn đúng sau bản sửa, nên đáng nói thẳng. Thứ nhất, chiến thắng là CPU, không phải đường dây. Khung ZAP thực ra lớn hơn HTTP (124 so với 75 byte cho một yêu cầu /health); ZAP thắng vì tốn ít chu kỳ hơn trên mỗi byte, không phải ít byte hơn. Thứ hai, zaphttp không bỏ qua JSON. Nó mang thân HTTP như các byte mờ trên cả hai giao vận, nên trình xử lý trả cùng chi phí marshal theo cả hai cách — trên một tải JSON dạng trò chuyện, ZAP thực tế là 0.95× HTTP, một khoản thua nhỏ vì chép thân vào đuôi khung. Việc lược JSON là tính chất của RPC có kiểu gốc ZAP, một trục riêng mà zaphttp không đụng tới. Chiến thắng loopback ở đây là thật và thuần túy là thắng về CPU/cấp phát. Còn trên dây 2.5 GbE thật? Cả hai giao vận hòa ở ~530k — liên kết đã bão hòa và codec không còn là nút thắt.

Thứ tư: vậy ZAP có thực sự bỏ qua JSON không?

Dòng cuối ấy — "lược JSON là tính chất ZAP gốc mà zaphttp không thực hiện" — là một giấy nợ, và người đọc nên bắt tôi trả. zaphttp mang thân một cách mờ, nên nó không bao giờ kiểm cái mà giao thức nhị phân đáng ra phải thắng: đọc dữ liệu có cấu trúc mà không phân tích văn bản. Nên tôi dựng bài kiểm làm đúng việc đó.

Một bản ghi dùng chung — một lời gọi dạng eth_getBalance, bốn trường mỗi chiều (account, block, id → balance, nonce, blockHash) — đi vòng bốn cách: encoding/json của thư viện chuẩn, goccy/go-json và sonic (hai thư viện JSON nhanh nhất trong Go), và nhị phân có kiểu của zap-proto/go. Cùng trường vào, cùng tính toán, cùng trường ra; một bài kiểm tương đương vòng buộc mã hóa lại đúng từng byte để không bên nào bỏ được việc bên kia làm. Và tôi giữ JSON ở bản thực hiện nhanh nhất của nó, không phải thư viện chuẩn, để không ai phẩy tay gạt kết quả.

Giải mã là một cuộc tàn sát. Đọc yêu cầu bốn trường:

codecgiải mãcấp phát
JSON thư viện chuẩn2334 ns13
JSON sonic845 ns8
JSON goccy473 ns4
ZAP có kiểu13 ns0

Mười ba nano giây, không cấp phát — nhanh hơn JSON nhanh nhất 35×, nhanh hơn thư viện chuẩn 174×. Không có phân tích: trường nằm ở một độ lệch đã biết, bạn đọc nó. Đó là toàn bộ luận điểm của một dây nhị phân có kiểu, và nó đứng vững.

Nhưng tôi đã hứa báo cả những phần không tô hồng ZAP, nên:

  • Mã hóa là hòa, và nó cấp phát nhiều hơn. ZAP tuần tự hóa trong 629 ns với 12 lần cấp phát so với 426 ns / 4 của goccy. Bộ dựng zap-proto/go v1.3.0 đã phát hành hoãn các đuôi độ dài biến đổi qua một lần chép và để bộ dựng đối tượng thoát lên heap; API độ rộng cố định đã gom, thứ sửa việc đó, không có trong thẻ đã phát hành, nên đó là con số thành thật. Tính ròng, trình xử lý máy chủ đầy đủ (giải mã + tính toán + mã hóa) có nghiêng về ZAP — 274 ns so với 431 của goccy — vì chiến thắng giải mã át hòa của mã hóa. Nhưng chính giải mã đang gánh.
  • Từ đầu đến cuối, 174× co lại còn ~1.2×. Trên một điểm cuối /rpc thật: ZAP 535k yêu cầu/giây, sonic 483k (1.11×), goccy 452k (1.18×), thư viện chuẩn 407k (1.31×). Có thật, nhưng không gần khoảng cách vi chuẩn — vì ở nửa triệu yêu cầu một giây, socket và bộ lập lịch mới là chi phí, còn tuần tự hóa chỉ là một lát mỏng. Codec ngừng là nút thắt từ lâu trước khi bạn nhận ra nó đã nhanh hơn.

Một bất ngờ nghiêng về ZAP: trên một bản ghi có cấu trúc thật, dây của nó nhỏ hơn, không lớn hơn — 162 so với 233 byte — vì JSON tiêu byte cho chuỗi hex 0x, số nguyên thập phân, dấu nháy và tên trường. Lưu ý "ZAP lớn hơn" từ bài kiểm khung chỉ đúng với tải tầm thường cỡ /health; đưa cho nó các trường có kiểu thật thì nhị phân thắng cả về kích thước.

Vậy, thành thật: ZAP có bỏ qua JSON không? Có — và trên đường đọc, khoảng cách không hề sát. Bạn có cảm được hay không tùy việc tuần tự hóa có phải nút thắt của bạn không. Ở 500k rps bản ghi nhỏ thì không. Đổi sang tải lồng nhau dày hoặc quạt ra nặng giải mã, và 174× ấy bắt đầu hiện ra đúng chỗ bạn sống.

Chúng tôi đã học được gì

  • Đo cả đường đi, và nghi ngờ công cụ. hey kẹp chúng tôi ở ~100k — đó là máy khách, không phải máy chủ. bombardier lộ ra nhiều hơn 4×.
  • Máy hiếm khi là nút thắt đầu tiên. Một hàng đợi RX của NIC khiến chúng tôi mất 2.67× trước khi đụng một dòng mã ứng dụng.
  • Đo cấp phát, không chỉ thông lượng. ZAP "chậm" chỉ cách một cờ GOGC để lộ rõ là vấn đề codec — và khi chúng tôi gom ngữ cảnh và đưa cấp phát về không, giao vận gốc vượt HTTP.
  • Vi chuẩn và thử tải trả lời hai câu khác nhau. Giải mã có kiểu nhanh hơn JSON 174× khi tách riêng và nhanh hơn 1.2× từ đầu đến cuối — cả hai số đều đúng, và chỉ trích một số là nói dối bằng cách bỏ sót.
  • Báo cho công bằng và thật. Cùng trình xử lý, kết nối đã ấm, không chọn GOGC, giữ JSON ở thư viện nhanh nhất chứ không phải chậm nhất, công bố tập lệnh. Khi con số thành thật là một thất bại — hòa của mã hóa, biên từ-đầu-đến-cuối bị nén — chúng tôi cũng giao thất bại ấy.

Codec ZAP không cấp phát đã xong, và nó thắng. Những điểm dừng còn lại là vật lý: một bộ nạp hai NIC (các đường nhận song song hướng tới bảy chữ số), liên kết nhanh hơn (10 GbE / RDMA), một lần tách writev để đuôi khung của ZAP bỏ một lần chép thân trên tải lớn, và các giao vận hậu lượng tử mà Go 1.26 đưa cho chúng tôi sẵn — PQ-TLS 1.3, ZAP-over-PQ-TLS, và PQ-QUIC — nơi chúng tôi sẽ tách chi phí bắt tay ML-KEM khỏi thông lượng trạng thái dừng.

Chúng tôi có thể đưa việc này đi xa đến mức cần.

Bình luận

Đăng nhập bằng Hanzo ID để bình luận.

Đang tải bình luận…

Đọc tiếp