3× اتصالات أكثر. نفس النواة. خلف أبحاث الأداء في محرك القبول لدى FlashProxy

كيف استخدمت FlashProxy محسّناً ذكياً مستقلاً لاكتشاف محرك قبول TCP يستخدم تعليمات CPU أقل بـ 3× من Go. مفتوح المصدر وبرخصة MIT.
هناك فئة من مشاكل الأداء لا تظهر إلا على نطاق واسع، وهي محبطة حقاً حين تطفو على السطح. أنت لست محدوداً بمنطق التوجيه. ولا بالتشفير. ولا بأي شيء من المفترض أن يقوم به تطبيقك فعلياً. أنت محدود بالبنية التحتية المحيطة به. بالأنابيب.
هذا بالضبط ما حدث لنا.
عند حوالي 40,000 اتصال في الثانية، بدأ بروكسي Go الخاص بنا في بيئة الإنتاج يصطدم بحدود CPU في مسار القبول. كان المسبب هو النموذج القياسي في Go للخدمات الشبكية: goroutine واحد لكل اتصال، وsyscall واحد لكل عملية. كل اتصال قصير العمر (فحص صحي، أو اختبار موازن تحميل، أو إعادة توجيه صغيرة) يلمس النواة أربع مرات: accept وread وwrite وclose. عند ارتفاع معدل تغيير الاتصالات، تتوقف تلك الرحلات الأربع عن كونها عبئاً إضافياً وتصبح التكلفة الكاملة بأكملها. كود التطبيق مجاني تقريباً. أما إدخال البيانات وإخراجها من النواة فليس كذلك.
الحل الذي يلجأ إليه الجميع هو io_uring، واجهة الإدخال/الإخراج غير المتزامن في Linux التي تجمع عمليات النواة في دُفعات وتقلل بشكل كبير عدد المرات التي تضطر فيها إلى عبور الحدود بين فضاء المستخدم والنواة. كنا نعرف ذلك. أما السؤال الأصعب فكان: كيف نضبطه بدقة؟
لماذا لم نضبطه بأنفسنا فحسب
يحتوي io_uring على قائمة طويلة من التحسينات الموثقة: multishot accept وregistered file descriptors وDEFER_TASKRUN وcompletion chains وMSG_MORE coalescing. المشكلة أن هذه التقنيات تتفاعل مع بعضها بطرق يصعب التنبؤ بها حقاً. بعض التركيبات تُعزز بعضها. وبعضها يلغي بعضها. وبعض حالات التراجع تبقى غير مرئية تماماً حتى تُجري القياسات تحت حمل واقعي على أجهزة حقيقية.
يستطيع مطور يجلس مع perf stat اختبار حفنة من التركيبات في يوم واحد. لكن مساحة البحث الحقيقية، التي تشمل التركيبات والترتيبات وقيم المعاملات وتفاعلات إصدارات النواة، أكبر بكثير من ذلك. والأهم من ذلك أن الحدس البشري يُشكّل عبئاً هنا. فنحن نميل إلى التمسك بالنظريات التي تبدو صحيحة على الورق حتى حين تقول البيانات عكس ذلك.
لذلك بنينا حلقة قياس مغلقة وأسندنا مشكلة البحث إلى وكيل ذكاء اصطناعي.
كيف عملت حلقة البحث
أعددنا تطبيقَي خادم يعملان جنباً إلى جنب. الضابط كان خادم Go يُحاكي مسار القبول في بيئة الإنتاج بدقة تامة: goroutine لكل اتصال وSO_REUSEPORT fan-out. جُمِّد طوال فترة التجربة. أما المعالجة فكانت خادم C يستخدم liburing، يبدأ من حلقة io_uring accept أحادية الطلقة الأساسية. كان هذا هو الكود الوحيد القابل للتغيير.
خدم كلا الخادمين العقد ذاته: قبول الاتصال، وقراءة بايتات الطلب، وكتابة استجابة HTTP 200 ثابتة، ثم الإغلاق. نفس الأجهزة، ونفس النواة، ونفس الحمل. المتغير الوحيد كان طريقة معالجتهما لذلك.
عمل Claude Code بلا واجهة مستخدم بوصفه المحسِّن. في كل تكرار كان يقرأ النسخة الفائزة الحالية، والسجل الكامل للطفرات السابقة المخزنة كـgit commits، وقاعدة معرفة تضم الدروس المتراكمة عبر جميع الجولات، وقاعدة بيانات بيانات التنميط. كان يصيغ فرضية واحدة ويُجري تعديله، ثم يُسلِّم العملية بالكامل.
جهاز bash منفصل، لا يستطيع الذكاء الاصطناعي لمسه، كان يبني الطفرة ويثبتها على نواة CPU معزولة ويشغّل مولد الحمل ويسجّل النتيجة. كانت صيغة التسجيل:
score = 1,000,000,000/mean(instructions per connection)
أعداد تعليمات CPU قياسٌ دقيق للأجهزة. خلافاً لأرقام الإنتاجية، فهي محصّنة ضد الخنق الحراري وتذبذب تردد الساعة. وتُخبرك بالضبط عن كمية العمل التي تؤديها CPU لكل اتصال، وهو ما يُحدد فعلياً الطاقة الاستيعابية على نطاق واسع.
إذا حسّنت طفرةٌ ما النتيجةَ بأكثر من 3% على النسخة الفائزة الحالية، رُقِّيت. وإن لم تفعل، حُذفت باستخدام git reset --hard واستمرت الحلقة. كتب الذكاء الاصطناعي الفرضيات. واتخذ الجهاز كل قرار بالاحتفاظ أو الرجوع. ولم يتدخل أيٌّ منهما في عمل الآخر.
لمنع المحسِّن من التلاعب بالمعيار، خضع كل جولة مسجَّلة للتحقق: يجب أن تكون بايتات الرد دقيقة تماماً، ويجب أن تكتمل الاتصالات من طرف إلى طرف، ويجب أن يبقى معدل الفشل أقل من 0.01%. أي انتهاك كان يُسجَّل بدرجة صفر.
ما الذي وجدناه
عمل المحسِّن لمدة يومين وتقارب على ستة تغييرات تُفسِّر معاً الفجوة الكاملة في الأداء.
أعلام الحلقة DEFER_TASKRUN وSINGLE_ISSUER. تنقل هذه الأعلام عمل مهام الإكمال إلى حلقة أحداث خيط العامل الخاصة به، مما يُلغي الإيقاظ عبر وحدات CPU. كانت هذه أكبر قفزة في الجولة بأكملها، وهي تخفيض مباشر في تكلفة CPU للنواة لكل عملية، لا مجرد حيلة لزيادة الإنتاجية.
واصفات الملفات المسجَّلة (Registered file descriptors). مع الواصفات المباشرة، تعيش الاتصالات المقبولة في جدول الحلقة الخاص بها بدلاً من جدول واصفات ملفات العملية. هذا يتخطى تثبيت fd عند القبول والبحث في كل عملية لاحقة. اختبر المحسِّن أحجام جداول متعددة ووجد أن 4,096 إدخالاً هو التهيئة المثلى.
الاستقبال متعدد الطلقات (Multishot accept). بدلاً من إعادة تهيئة عملية القبول بعد كل اتصال، تُهيِّئها مرة واحدة وتنشر النواة إكمالاً لكل اتصال جديد تلقائياً. قلّص هذا استدعاءات io_uring_enter لكل اتصال إلى 0.34.
قائمة اتصالات حرة لكل عامل (Per-worker connection freelist). أدى التخصيص المسبق لـ128 كائن اتصال لكل عامل إلى إزالة malloc من المسار الساخن كلياً. انخفضت حصة تعليمات CPU المخصصة لـlibc من 1.32% إلى 0.92%.
رد MSG_MORE ودمج FIN (FIN fusion). يؤدي إرسال الرد مع MSG_MORE إلى الاحتفاظ به في قائمة انتظار الكتابة TCP حتى يتمكن FIN الاتصال من الركوب عليه، فيُرسَلان معاً في جزء TCP واحد. جرس NIC واحد بدلاً من اثنين. اكتشف المحسِّن هذا بملاحظة أن دالة كتابة نواة منخفضة المستوى كانت تستهلك ضعف حصتها المتوقعة من التعليمات، وتتبّع ذلك إلى تقسيم جزء غير ضروري.
الإكمالات المجمّعة مع CQE_SKIP_SUCCESS. وسم عمليات الإرسال والإغلاق بحيث لا تُولِّد أحداث إكمال عند النجاح يعني أن عمليات القبول والاستقبال فقط هي التي تُنتج إكمالات، بمعدل اثنين تقريباً لكل اتصال بدلاً من أربعة. يقود استدعاء إرسال-وانتظار واحد اتصالاتٍ كثيرة في آنٍ معاً.
الفشل الأكثر تعليماً
في مرحلة ما، حاول المحسِّن ربط عمليات الاستقبال والإرسال والإغلاق في سلسلة، وهي تقنية تجعل كل عملية تنطلق تلقائياً حين تكتمل العملية السابقة. كان الهدف تقليل جولات الذهاب والإياب مع النواة، وقد نجح في ذلك: انخفضت استدعاءات enter لكل اتصال من 1.90 إلى 1.40.
انخفضت النتيجة 34%. تم الرجوع فوراً.
الدرس الذي أُدرج في قاعدة المعرفة: تقليل مداخل النواة ليس هو الرافعة. تسلسل السلسلة يُكلِّف أكثر مما يوفره من مداخل.
هذا بالضبط نوع النتيجة التي تكسر الحدس البشري. المقياس الذي بدا كالاختناق لم يكن الاختناق الفعلي. ربما كان المهندس يدافع عن هذا التحسين لفترة أطول. قاست الحلقة، رفضت، وتابعت.
أين توقف البحث
بعد الانتصارات الستة، استكشف المحسِّن نحو اثني عشر مرشحاً آخر. رُجع عن الجميع، لا لأنها أحدثت تراجعاً، بل لأن ضوضاء القياس تجاوزت عتبة الترقية البالغة 3%. لم يكن ثمة شيء متبقٍّ للاكتشاف.
عند تهيئة النسخة الفائزة، تنتمي نحو 94% من CPU المتبقية إلى مكدس TCP في Linux. وحوالي 1% هو كود التطبيق. وحوالي 4% يستغل المكوّنات الداخلية. لا يوجد كود user-space متبقٍّ يمكن تحسينه بصورة ذات معنى. حدّد البحث الحدَّ الأدنى بدقة وتوقف عنده.
النتائج
على نواة واحدة مثبّتة، محدودة بـCPU، مع 512 اتصال ثابت قيد التنفيذ على loopback:
Go goroutine-per-connection: 83,250 تعليمة لكل اتصال
خط الأساس الافتراضي لـio_uring: 59,931 تعليمة لكل اتصال
محرك القبول لدى FlashProxy: 27,363 تعليمة لكل اتصال
هذا يعني 3.04× تعليمات CPU أقل لكل اتصال مقارنةً بـGo، و2.19× أقل من خط أساس io_uring. على نواة مشبعة واحدة، يترجم هذا إلى ما يعادل ستة أضعاف إنتاجية الاتصال مقارنةً بنموذج goroutine.
هذه معايير loopback مصممة لعزل تكلفة CPU بصورة نظيفة. النسب هي ما يهم، لا الأرقام المطلقة. التقنيات الأساسية (multishot accept وDEFER_TASKRUN وregistered descriptors) موثقة في أنماط io_uring المعروفة. ما أنتجه البحث كان إثباتاً لأي التركيبات تعمل فعلاً معاً، وأيها يبدو جيداً على الورق لكن يُكلِّفك في التطبيق الفعلي.
المكتبة مفتوحة المصدر
التصميم الفائز متاح الآن في flashaccept، مكتبة C مفتوحة المصدر تُغلِّف جميع التحسينات الستة خلف واجهة برمجية بسيطة. تُعطيها منفذاً ومعالج طلب، فتُشغِّل حلقة قبول io_uring محسَّنة لكل نواة تلقائياً.
تتطلب Linux وliburing 2.3 أو إصدار أحدث. على النوى الأقدم تتراجع المكتبة بسلاسة. حالة الاستخدام المقصودة هي الاتصالات عالية التغيير قصيرة العمر: الفحوصات الصحية، وإعادة التوجيه، واختبارات موازن التحميل، واستجابات RPC الصغيرة. جهاز البحث الكامل، بما في ذلك خط الأساس والجهاز وتهيئة المحسِّن وقاعدة المعرفة المتراكمة الكاملة، مرفق وقابل للتكرار الكامل.
برخصة MIT. متوفر الآن على github.com/thealonlevi/flashaccept.
جاء هذا البحث مباشرةً من بناء البنية التحتية للبروكسي لدى FlashProxy. إذا كنت تُشغِّل خدمة Linux عالية التغيير ومسار القبول هو اختناقك، فقد بنينا هذا بالضبط لهذه المشكلة. وإذا أردت توسيعه أو تشغيل جهاز البحث بنفسك، فالمستودع يحتوي على كل ما تحتاجه.
الأسئلة الشائعة
ما هو Flashaccept؟
Flashaccept مكتبة C مفتوحة المصدر بنتها FlashProxy، توفر محرك قبول TCP عالي الأداء لـLinux. تستخدم io_uring تحت الغطاء وتقبل الاتصالات بـ3.04× تعليمات CPU أقل من خادم Go goroutine-per-connection القياسي. مُرخَّصة بموجب MIT ومتاحة على GitHub.
ما أعباء العمل المصمَّمة لها؟
صُمِّمت Flashaccept للاتصالات عالية التغيير قصيرة العمر حيث تحدث دورة الطلب-الرد-الإغلاق بحجم كبير: نقاط نهاية الفحص الصحي، واختبارات موازن التحميل، وإعادة توجيه HTTP، واستجابات RPC الصغيرة. لم تُصمَّم للاتصالات keep-alive أو الجلسات متعددة التبادلات في الإصدار v1.
ما إصدار Linux وliburing الذي أحتاجه؟
تحتاج إلى Linux مع liburing الإصدار 2.3 أو أحدث، الذي يأتي مع Ubuntu 24.04 والإصدارات الأحدث. يتطلب المسار السريع (multishot accept وdirect descriptors) نواة 5.19 أو أحدث. على النوى الأقدم تتراجع المكتبة بسلاسة إلى قبول أحادي الطلقة وواصفات ملفات عادية.
ما هو io_uring ولماذا يهم لأداء البروكسي؟
io_uring واجهة نواة Linux قُدِّمت في النواة 5.1، تُتيح للتطبيقات تقديم عمليات الإدخال/الإخراج واستقبالها بصورة غير متزامنة باستخدام مخازن حلقية في الذاكرة المشتركة، مما يُقلِّص بشكل كبير عدد استدعاءات النظام المطلوبة. بالنسبة لـ البنية التحتية للبروكسي التي تتعامل مع عشرات الآلاف من الاتصالات قصيرة العمر في الثانية، تصبح تكلفة عبور الحدود بين فضاء المستخدم والنواة بصورة متكررة هي النفقة السائدة لـCPU. ويُقلِّص io_uring هذه التكلفة بشكل ملحوظ.
هل flashaccept ما تُشغِّله FlashProxy في بيئة الإنتاج؟
جاء البحث مباشرةً من أعمال التوسع في بيئة الإنتاج لدينا. تعمل البنية التحتية للبروكسي في FlashProxy عبر 190+ دولة وتتعامل مع حجم اتصال ضخم. كان تحسين مسار القبول متطلباً هندسياً حقيقياً، لا مجرد تمرين بحثي. وflashaccept هو النتيجة المُقطَّرة لذلك العمل، مفتوحة المصدر ليتمكن الآخرون من الاستفادة منها.
هل يمكنني استخدام flashaccept اليوم؟
نعم. إنه الإصدار 1.0.1، مُرخَّص بموجب MIT، ومتاح عبر GitHub وvcpkg وConan وArch AUR. اختُبرت المكتبة تحت AddressSanitizer وUBSan عبر 400,000+ اتصال عبر جميع مسارات التهيئة الأربعة.
أين يمكنني معرفة المزيد عن البنية التحتية لـFlashProxy؟
يمكنك قراءة المزيد عن كيفية بناء بنية البروكسي وتوسيعها على مدونة FlashProxy أو استكشاف شبكة البروكسي الخاصة بنا مباشرةً.


