نصف مليون طلب في الثانية على GB10: الشبكة كانت عنق الزجاجة، لا الصندوق
أردنا أن نرى إلى أي سرعة يبلغ مستوى بياناتنا السحابي على NVIDIA GB10. الجواب علّمنا عن طوابير استقبال بطاقة الشبكة أكثر مما علّمنا عن كودنا — وضاعف إنتاجيتنا على السلك نفسه بأكثر من الضعف.

في هذه الصفحة
نبني سحابتنا على ملف Go ثنائي واحد — أثر واحد يركّب بوابة الذكاء الاصطناعي والتجارة و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. ليست عرض النطاق. وأنوية spark العشرين كانت عاطلة في معظمها. ليس المعالج. إذًا ماذا؟
السبب: طابور استقبال واحد
ethtool -l فضحه: كانت بطاقة الشبكة تعمل بـ طابور RX عتادي واحد. كل رزمة واردة كانت تُعالَج بمقاطعة softirq على نواة واحدة — وتلك النواة كانت السقف. والأسوأ أن برنامج التشغيل رفض رفضًا قاطعًا إضافة طوابير (ethtool -L → "Operation not supported").
فأسرع جهاز تطوير نملكه كان يُخنق بخيط نواة واحد يجرف الرزم، بينما تسع عشرة نواة تتفرج.
الإصلاح: وجّه الرزم في البرمجيات
حين يرفض العتاد تعدد الطوابير، تقبله النواة — توجيه رزم الاستقبال يجزّئ التدفقات على الأنوية في البرمجيات. شغّلنا RPS (وRFS/XPS) على الأنوية العشرين كلها، ورفعنا تراكم الاستقبال من 1,000 إلى 300,000، وضبطنا مخازن المقبس على 256 MiB، وثبّتنا حاكم الأداء. سكربت واحد متماثل المفعول، tune.sh، يُشغَّل على الجهازين.
السلك نفسه. دالة المعالجة نفسها. 518,000 طلب/ثانية — قفزة بمقدار 2.67×، بذروة 696k.
| التزامن | بلا ضبط | بعد الضبط |
|---|---|---|
| 1,000 | 194k | 518k |
| 4,000 | 154k | 474k (ذروة 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. ثلاثة مواضع تخصيص حملت ذلك: المُجمّع يسكّ مخزن إطار جديدًا في كل استدعاء، وفك الترويسة ينسخ كل سلسلة إلى خريطة، و— الكبيرة — خادم 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 وهبوط جمع القمامة من 2,203 إلى 62.
شيئان اضطررت أن أصححهما بنفسي — وكلاهما يبقى بعد الإصلاح، فيستحقان أن يُقالا بوضوح. أولًا، المكسب في المعالج، لا في السلك. إطار ZAP في الحقيقة أكبر من HTTP (124 مقابل 75 بايت لطلب /health)؛ يفوز ZAP لأنه ينفق دورات أقل لكل بايت، لا بايتات أقل. ثانيًا، zaphttp لا يتخطى JSON. يحمل جسم HTTP بايتات معتمة على النقلين كليهما، فتدفع دالة المعالجة كلفة التجميع نفسها في الحالتين — وعلى حمولة JSON بشكل محادثة يكون ZAP في الواقع 0.95× من HTTP، خسارة طفيفة من نسخ الجسم إلى ذيل الإطار. إسقاط JSON خاصية لـ RPC المطبّع الأصيل في ZAP، وهو محور منفصل لا يمارسه zaphttp. مكسب الاسترجاع المحلي هنا صادق، وهو مكسب خالص للمعالج وللتخصيص. وعلى سلك 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 ns | 13 |
| JSON sonic | 845 ns | 8 |
| JSON goccy | 473 ns | 4 |
| ZAP المطبّع | 13 ns | 0 |
ثلاث عشرة نانوثانية، وصفر تخصيصات — أسرع بـ 35× من أسرع JSON، وأسرع بـ 174× من المكتبة القياسية. لا يوجد تحليل: الحقل يقع عند إزاحة معروفة، فتقرؤه. هذه أطروحة السلك الثنائي المطبّع كلها، وهي تصمد.
لكنني وعدت أن أذكر الأجزاء التي لا تجامل ZAP، فها هي:
- الترميز تعادل، وهو يخصص أكثر. يسلسل ZAP في 629 ns مع 12 تخصيصًا مقابل 426 ns / 4 لدى goccy. بانٍ
zap-proto/goالمنشور في v1.3.0 يؤجل الذيول متغيرة الطول عبر نسخ ويهرب بانٍ الكائن إلى الكومة؛ وواجهة العرض الثابت المجمّعة التي تصلحه ليست في الوسم الصادر، فهذا هو الرقم الصادق. بالصافي، دالة معالجة الخادم الكاملة (فك الترميز + الحساب + الترميز) تفضّل فعلًا 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 طلب في الثانية من سجلات صغيرة، ليس كذلك. أدخل حمولات متداخلة سمينة أو توزيعًا ثقيل فك الترميز، فيبدأ 174× بالظهور حيث تعيش.
ما تعلمناه
- قِس المسار كله، وشكّك في الأداة.
heyحصرنا عند ~100k — ذلك كان العميل لا الخادم.bombardierكشف 4× أكثر. - نادرًا ما يكون الجهاز أول عنق زجاجة. طابور RX واحد في بطاقة الشبكة كلفنا 2.67× قبل أن نمس سطرًا من كود التطبيق.
- قِس التخصيصات، لا الإنتاجية وحدها. «بطء» ZAP كان على بعد علم
GOGCواحد من أن يتضح أنه مشكلة مرمّز — وحين جمّعنا السياق وصفّرنا التخصيصات، تجاوز النقل الأصيل HTTP. - القياس المصغّر واختبار الحمل يجيبان عن سؤالين مختلفين. فك الترميز المطبّع أسرع بـ 174× من JSON في العزل وأسرع بـ 1.2× من الطرف إلى الطرف — الرقمان صادقان، والاقتصار على أحدهما كذب بالإغفال.
- اذكره بعدل وبواقعية. نفس دالة المعالجة، واتصالات دافئة، وبلا GOGC منتقاة، وJSON مُلزَم بأسرع مكتبة لا بأبطئها، وانشر السكربت. حين كان الرقم الصادق خسارة — تعادل الترميز، والهامش المضغوط من الطرف إلى الطرف — شحنّا الخسارة أيضًا.
مرمّز ZAP ذا التخصيص الصفري تم، وهو يفوز. ما تبقى من العوائق فيزيائي: مولّد حمل ببطاقتي شبكة (مسارات استقبال متوازية نحو سبعة أرقام)، وربط أسرع (10 GbE / RDMA)، وتقسيم writev حتى يتخطى ذيل إطار ZAP نسخة واحدة للجسم في الحمولات الكبيرة، والنقول ما بعد الكمومية التي يمنحها Go 1.26 لنا أصيلًا — PQ-TLS 1.3 وZAP-over-PQ-TLS وPQ-QUIC — حيث سنفصل كلفة مصافحة ML-KEM عن إنتاجية الحالة المستقرة.
نستطيع أن نأخذ هذا إلى المدى الذي نحتاجه.
اقرأ المزيد

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

BitDelta: تقديم 14 نموذج Zen من عنقود معالج رسومي واحد
كيف يتيح ضغط دلتا BitDelta بدقة 1 بت تقديم 14 متغيرًا من نماذج Zen عبر بنية تحتية مشتركة لمعالجات الرسوم — الرياضيات، والمعمارية، والمقايضات.

من ذكاء التجارة إلى النماذج الرائدة: عقد البنية التحتية لدى Hanzo
اثنا عشر عامًا من بناء بنية تحتية للذكاء الاصطناعي — من محرك توصيات في الزمن الحقيقي عام 2014 إلى 94+ نموذجًا رائدًا وسلسلة كتل ما بعد الكم عام 2026. القصة التقنية لكيفية وصولنا إلى هنا.