مزايد Go يخسر المزادات بانتهاء المهلة بينما يبدو متوسطه سليمًا لديه مشكلتان تحت عرَض واحد: مهلة نهائية تخص منصة التبادل وتغطي الرحلة ذهابًا وإيابًا، وجامع قمامة يحمّل mark assist على الـ goroutine التي تقيّم المزايدة. هكذا تُقسَّم المهلة النهائية، وأي مقابض Go تؤتي ثمارها، وما الذي لا يصلحه مسار Rust الساخن.
المزايد الذي يخسر المزادات بانتهاء المهلة بينما يبدو متوسطه سليمًا يوصَف بالرقم الخطأ. المهلة النهائية تخص منصة التبادل وتغطي الشبكة في الاتجاهين. وجامع القمامة خاصية للعملية، فتُحمَّل وقفته على كل الاتصالات دفعة واحدة، أما قفزة على اتصال واحد وحده فتحتاج تفسيرًا آخر.
إعادة الكتابة بـ Rust أو ضبط الـ runtime خيار بين إجابتين عن سؤال لم يطرحه أحد: أي جزء من المهلة النهائية يُنفَق، وعلى ماذا. وما يلي يفصل الميزانية عن الجامع، والجامع عن المجدول، وإعادة الكتابة عن المكوّن الذي يستحقها.
الجواب القصير بنيوي. منصة التبادل تضبط المهلة النهائية وهي تشمل الشبكة، فالإصلاح الأول يقسّم p99 لاتصال واحد إلى زمن المعالج، وزمن انتظار معالِج، وزمن على الشبكة. وما تستطيع amBrain إثباته علنًا: بنينا RTBBidder، وهي منصة جانب طلب سُلّمت من الصفر، يقيّم فيها كل قرار مزايدة عشرات شروط الاستهداف لكل ظهور. وأرقام زمن الاستجابة التي ننشرها كقياسات تأتي من مسارات تداول لا من مزايد إعلاني، ولا رقم أدناه مقيس على مزايد لدينا.
تعرّف مواصفة OpenRTB الـ tmax بأنه أقصى زمن بالميلي ثانية تسمح به منصة التبادل لاستقبال المزايدات، شاملًا زمن الاستجابة عبر الإنترنت، وتقول إن القيمة تَجُبّ أي إرشاد سابق. والميزانية رحلة ذهاب وإياب تصل داخل كل طلب.
العتبة التي تُحاسب عليها ليست تلك التي على لوحتك. توثيق Google Authorized Buyers يشترط أن تصل 85 percent من الردود داخل المهلة النهائية مقيسةً عند موقع التداول، ويخنق المزايدين الذين لا يبلغونها. وبين ساعتها وساعتك يقع كل ما ليس حسابًا:
لذا تقع المهلة النهائية الداخلية تحت tmax بمقدار ما يقوله المدرَّج التكراري لديك عن تكلفة الرد على ذلك الاتصال، ويُشتق هذا المقدار لكل منصة تبادل على حدة بدل ضبطه مرة واحدة للأسطول. والمهلة النهائية ليست خطة سعة: العمل الملغى عند المهلة أنفق CPU بالفعل.
وتحت الحمل الزائد يعني ذلك دفع الثمن كاملًا لردود لا يعدّها أحد، فالإصلاح الغائب هو التحكم في القبول: اقرأ tmax، وقارنه بتأخير الطابور الذي تقيسه، وأجب بـ no-bid حين لا يستقيم الحساب. الـ no-bid السريع يُحتسب ضمن الـ 85 percent؛ والمزايدة المتأخرة لا تُحتسب.
جامع القمامة خاصية للعملية، فالدورة التي تنطلق على أي اتصال تُحمَّل على كل اتصال. وأول من يفوّت المهلة هو صاحب أضيق tmax وأثقل طلب. استبعد أولًا ما ينتج الصورة نفسها بلا جامع قمامة:
لا يصلح أيًّا من الأربعة إعدادُ جامع، وللتقسيم أدوات. ملف CPU يفصل فواتير الجامع حسب الرمز، runtime.gcAssistAlloc لأعباء المعالج وruntime.gcBgMarkWorker للوسم في الخلفية. وزمن stop-the-world هو /sched/pauses/total/gc:seconds، وانتظار الجاهزية هو /sched/latencies:seconds، وطابور القبول يُقرأ خارج العملية عبر ListenOverflows.
تمييز واحد يقرر أي إصلاح في Go تحتاج. وقفة stop-the-world تُحمَّل على كل goroutine دفعة واحدة، فتظهر قفزة مستوية على كل الاتصالات. أما mark assist فيُحمَّل على الـ goroutine التي خصّصت، فيهبط على الطلبات الأكثر تخصيصًا. وشيئان يخفضان الـ assist: بايتات أقل لكل طلب مزايدة، أو دورة أطول يغطي فيها عمال الخلفية قدرًا أكبر من الوسم. والأول وحده يصمد أمام تغيّر مزيج الحركة.
جامع Go متزامن، والدليل الرسمي صريح في أن طول الوقفة لا يتناسب مع حجم الكومة، فانتقالات stop-the-world قصيرة. والـ assists هي المصدر الذي يهم: الـ goroutines تساعد الجامع حين يكون التخصيص سريعًا، لأن الوسم في الخلفية يأخذ ربعًا ثابتًا من المعالجات ويُحمَّل النقص على المخصِّص.
المعدل يجعل هذا عتبة لا منحدرًا. معدل التخصيص هو QPS مضروبًا في البايتات لكل طلب مزايدة مقابل حصة خلفية ثابتة، فالكود الذي لا يقوم بأي assist عند خُمس حركتك قد يقوم بـ assist في كل طلب تقريبًا عند 100K QPS.
تكلفة الوسم تتناسب مع رسم المؤشرات الحية لا مع القمامة، والمزايد يحمل الشكل الخطأ لها: فهارس الحملات، وشرائح الجمهور، وذاكرات حدود تكرار العرض. ونشرت Discord النتيجة نفسها عام 2020، مع جامع يمسح ذاكرة LRU كاملة ليقرر إن كانت الذاكرة حرة. خمس آليات تختبئ تحت عبارة واحدة:
اقرأ ذلك كخمس فواتير منفصلة. واحدة فقط يسددها إعداد جامع، ولا واحدة يسددها تغيير اللغة قبل قياس التقسيم.
سمّى Gil Tene هذا العطل coordinated omission: نظام القياس ينسّق مع النظام قيد الاختبار بطريقة تتجنب قياس القيم الشاذة، لأن الحلقة المغلقة تنتظر ردًا وتتوقف عن الإرسال أثناء التوقف. ونشرت ScyllaDB مقارنة عام 2021 أبلغ فيها أحد أحمال العمل عن p99 يبلغ 249 microseconds في الحلقة المغلقة و665 ms تحت حمل مفتوح مع تصحيح، بفارق نحو 2,700 مرة.
مولّد الحمل الذي ينتظر ردًا يتوقف عن الإرسال في أثناء التوقف نفسه الذي بُني ليجده، ثم يُدخل الصمت في متوسط النتيجة. والمئين الذي يطبعه بعد ذلك يصف المولّد لا مزايدك.
سمِّ رقم التوقف قبل أن تضبط، لأن إعادة كتابة يقررها الإنهاك ليست قرارًا. رقمان لا رقم واحد: تخصيصات الكومة لكل طلب، وهي ما يقود الـ assist، والكومة الحية، وهي ما يقود الوسم.
الترتيب هو المصدر قبل السقف، لا الأكبر مكسبًا أولًا. ابدأ حيث يتولد الـ assist: escape analysis على مسار المزايدة، وbuffers يُعاد استخدامها بدل تخصيصها، وcodec يقرأ الحقول التي تحتاجها بدل تجسيد رسم كائنات جديد. وأبقِ الـ slices قصيرة العمر: slice داخل buffer الطلب يثبّت ذلك الـ buffer كله طوال عمر المزايدة.
للنهج سقف: أبلغت Uber عام 2021 أن ضبط GOGC مقابل حد ذاكرة الحاوية استعاد نحو 70,000 نواة عبر خدماتها الحرجة. وتلك نتيجة تكلفة لا مئين.
وصفت RTB House خدمة مزايدة على JVM في يونيو 2025 أنتج فيها التقسيم إلى microservices حجمًا كبيرًا من الطلبات الصغيرة: كان على زمن الاستجابة المضاف أن يبقى داخل 7 ms مقابل طلب متوسطه نحو 2.5 ms، وانكسر المئينان 98 و99 تحت وقفات G1 المتكررة. فانتقلوا إلى generational ZGC ودفعوا الثمن ذاكرةً.
لاحظ ما تطلّبه ذلك الإصلاح: جامع ثانٍ للانتقال إليه. وGo يشحن جامعًا واحدًا غير قابل للاستبدال، فالروافع في Go هي معدل التخصيص، وشكل المجموعة الحية، وGOGC مقابل GOMEMLIMIT. أما ما يزيله مسار Rust الساخن فمحدد: لا assist، ولا وسم في الخلفية، ولا دورة قسرية. وما لا يزول أطول مما تتوقعه أغلب الفرق:
إذا كان الذيل يسكن في تحليل الطلب أو في الاتصال بمنصة التبادل أو في اصطفاف المجدول، فإن Rust لا يعيد أيًا من تلك الميلي ثوانٍ، وإعادة كتابة المكوّن الخطأ تنفق ربع سنة لتبقى على معدل انتهاء المهلة نفسه.
لذا انقل أصغر قطعة تملك التخصيصات، لا الخدمة كلها: حلقة تقييم الظهور وفهارسها، واختيار المرشحين، والاستهداف، وعمليات البحث عن تكرار العرض والميزانية، والتقييم. وسعّر الحدّ لكل عبور: مرة واحدة لكل طلب مزايدة مع buffer مسطح، ولا مرة لكل قاعدة استهداف أبدًا. وتحقّق على نسخ منفصلة تُغذّى بتدفق مرآة، لا داخل العملية قيد الاختبار أبدًا، حيث يضاعف المسار الظلّي الكميتين اللتين تقيسهما.
اختبار قبول يصلح للمسارين: شغّل نافذة قصيرة وجامع القمامة مطفأ، تحت سقف ذاكرة تتحكم به، وسجّل p99.9 داخل المعالج. شغّله على نسخة واحدة خلف جزء من الحركة مع تراجع تلقائي، لأن إطفاء GOGC يجعل قفزة الكومة نحو السقف تدفع الـ runtime إلى دورات متلاحقة، والدليل يقول إن ذلك التوقف قد يكون غير محدود. ولا تقرأ النتيجة إلا إذا لم يتدخل محدِّد GC CPU أبدًا: فمتى تدخّل صار المئين يصف المحدِّد.
النصف الثاني من السؤال، من يقوم بهذا النوع من العمل، له اختبار لا يحتاج قائمة مورّدين. فالشركة التي تصلح زمن استجابة يقوده GC تتصرف بغير ما تتصرف شركة تبيع إعادة كتابة:
لا شيء منها يتطلب ثقة: كل واحد منها مستند يمكنك طلبه في المحادثة الأولى، وأي مستند يعود غامضًا يقول إن التشخيص يجري تخطّيه.
فالسؤال الأول ليس أي لغة. بل أي من المجاميع الثلاثة - المعالج أو المجدول أو الشبكة - يملك الميلي ثواني المفقودة على الاتصال الذي تنتهي مهلته، وهل تتحرك البايتات المخصصة لكل طلب مزايدة حين تهاجمها.
ما تستطيع amBrain إثباته علنًا: amBrain شركة تطوير برمجيات متخصصة في منصات التداول ومحركات المطابقة وأنظمة المزايدة الفورية وهندسة منصات الكازينو، ونبني البرمجيات منذ 2019، وما نبنيه في AdTech هو تطوير DSP ومنصات المزايدة الفورية وهندسة منصات تبادل الإعلانات. ونعمل بثلاث صيغ: تسليم كامل، أو فريق مخصص، أو مهندسون مدمجون في فريقك. وإذا كنت توازن بين إعادة كتابة وجولة ضبط، فالمحادثة الجديرة بالإجراء هي التي تجري التقسيم قبل أن تختار لغة.
أحضر بنيتك الحالية وسيناريو الفشل الذي يقلقك، وسنراجعه معًا خلال نصف ساعة.