فخ اختبارات الأداء

ناشرو نتائج اختبارات الأداء لا يظهرون لك الصورة الكاملة أبداً. إنهم يقيسون معالجاً (handler) لا يفعل شيئاً تقريباً؛ مجرد "Hello World" بتنسيق JSON، أو سلسلة نصية ثابتة. في هذا الفراغ، يتفوق Fiber بوضوح، فهو يخدم طلبات أكثر في الثانية ويستهلك ذاكرة أقل من Gin. هذه هي الهندسة الحقيقية. لكن تطبيقك ليس في فراغ.

أنا أقوم ببناء تطبيق أمني في الوقت الفعلي في كينيا. يقوم التطبيق باستقبال إحداثيات GPS من المستخدمين في نيروبي ومومباسا، ويرسل تنبيهات حول بؤر الجريمة أو حوادث الطرق. وهذا يعني عمليات كتابة في PostgreSQL، واستدعاءات لـ Firebase Cloud Messaging، وعمليات بحث في الهندسة الجغرافية العكسية (reverse geocoding). عندما تتضمن بنيتك التحتية قفزات عبر الشبكة وجولات ذهاب وإياب إلى قاعدة البيانات، فإن إطار العمل الذي يوفر نانو-ثوانٍ في التوجيه (routing) يصبح غير مرئي. عنق الزجاجة ليس الموجه (router) أبداً، بل هو تأخر بوابة الرسائل القصيرة (SMS gateway)، أو استعلام PostGIS الذي يفحص جدول تقارير الحوادث. تبدو معدلات الإنتاجية الخام مغرية حتى تصطدم ببيئة الإنتاج.

الولاء للمكتبة القياسية

يعتمد Gin مباشرة على net/http. وهذا الأمر أهم مما يبدو عليه.

كل مطور Go يعرف net/http. وتتبع عمليات تصحيح الأخطاء (debugging) مسارات مكدسة (stack traces) مألوفة. والبرمجيات الوسيطة (Middleware) التي كُتبت قبل خمس سنوات لا تزال تعمل دون أي تعقيدات. وتتصرف كائنات الطلب والاستجابة تماماً كما تقول وثائق Go. يظل Gin قابلاً للتنبؤ لأنه يظل قريباً من اللغة نفسها.

أما Fiber فيستبدل هذا الأساس بـ fasthttp، وهو محرك HTTP مخصص ومحسن لتحقيق أداء خالٍ من تخصيص الذاكرة (zero-allocation). ولتحقيق ذلك، يستخدم تقنية تجميع الطلبات والاستجابات (Request/Response Pooling). فبدلاً من ترك جامع المهملات (garbage collector) يستعيد الذاكرة بعد كل طلب، يقوم Fiber بإعادة تدوير كتل الذاكرة؛ حيث يمسحها ثم يسلمها للاتصال التالي الوارد. هذه الحيلة هي مصدر السرعة، وهي أيضاً مصدر الخطر.

الخطر الخفي لإعادة استخدام الذاكرة

تبدو عملية التجميع (Pooling) آمنة من الناحية النظرية، ولكنها في الممارسة العملية تفرض عقداً ضمنياً دقيقاً: يجب ألا تحتفظ أبداً بمرجع (reference) لبيانات الطلب بعد انتهاء المعالج الخاص بك. إذا استمرت الـ goroutine لفترة أطول من الطلب، أو إذا التقطت جزءاً (slice) من جسم الطلب لمعالجته لاحقاً، فسيقوم Fiber باستعادة تلك الذاكرة وتسليمها للمستخدم التالي.

النتيجة هي تلوث البيانات. هذا الخطأ لا يظهر في اختبار الوحدة (unit test)، بل يظهر في صورة إحداثيات GPS لسائق في نيروبي تظهر فجأة على خريطة في كيسومو. أو يظهر في صورة تسرب رمز JWT من مستخدم إلى سياق مستخدم آخر. هذه إخفاقات مرتبطة بالتوقيت؛ فهي تختفي عندما تضيف سجلات الأحداث (logging) لأن عملية التسجيل تخصص ذاكرة جديدة وتغير التوقيت. ستنتهي بك الحال بمطاردة الأشباح.

مع Gin، تخصص المكتبة القياسية كائن طلب جديداً تماماً، فلا يوجد مجمع (pool) يمكن تسميمه. هذه السلامة تكتسب أهمية قصوى عندما يتعامل تطبيقك مع مواقع حقيقية مرتبطة بسلامة حقيقية.

جاذبية النظام البيئي

هناك أيضاً "ضريبة التوافق" الصامتة.

تفترض معظم البرمجيات الوسيطة في Go استخدام net/http. فأدوات التحقق من JWT، ومصدّرات OpenTelemetry، ومعالجات CORS، ومحددات معدل الطلبات (rate limiters) كلها تتحدث الواجهة القياسية. ويفهم Gin هذه الواجهة بشكل أصلي؛ أضف أي برمجية وسيطة وستعمل مباشرة.

أما Fiber فلديه نوع السياق (context type) الخاص به وتوقيع المعالج الخاص به، مما يتطلب استخدام محولات (adapters). أحياناً يكون المحول رسمياً، وأحياناً يتأخر عن المكتبة الأصلية لمدة عام، وأحياناً يتصرف بشكل مختلف قليلاً تحت ضغط العمل. عندما تدير فريقاً صغيراً، لن تملك ساعات إضافية لتصحيح سبب عمل برمجية وسيطة للمصادقة على جهازك المحمول بينما ترفض الرموز المشروعة على الخادم. أنت تريد أن تكون عملية go get رتيبة ومملة، وGin يحافظ على هذا الملل، وهو بالضبط ما تريده في الساعة الثالثة فجراً.

ما يحتاجه التطبيق فعلياً

تقوم خدمتي الأمنية بمعالجة إشارات الموقع كل بضع ثوانٍ من مستخدمين متزامنين. تقوم الخدمة بتحديد السياج الجغرافي (geofencing) للطرق عالية المخاطر، وتطلق التنبيهات. التنبيه البطيء أمر محبط، أما التنبيه الخاطئ فهو أمر خطير. إن إرسال شخص ما نحو موقع حادث نشط بسبب ذاكرة مجمعة تالفة هو أمر غير مقبول.

الموثوقية تتفوق على معدل الإنتاجية هنا. أحتاج إلى مسارات مكدسة (stack traces) منطقية عندما أستخدم pprof. أحتاج إلى ملفات تعريف الذاكرة (memory profiles) التي لا تتطلب مني فهم دورة الحياة الداخلية لمجمع ذاكرة مخصص. أحتاج إلى المطور التالي الذي سيرث هذا المشروع أن يبدأ العمل فيه خلال فترة ما بعد الظهيرة دون الحاجة لتعلم نموذج كائنات fasthttp. Gin يمنحني ذلك. الاستقرار التشغيلي لا ينعكس في اختبارات الأداء، ولكنه هو ما يحافظ على الخدمة