AdTechSep 10, 2026قراءة 10 دقائق

وقفات GC في Go داخل مزايد RTB: mark assist والمهل النهائية وقرار Rust

المزايدة الآنيةRustجمع القمامةزمن استجابة الذيل
تعذّر تحميل الصورة

مزايد Go يخسر المزادات بانتهاء المهلة بينما يبدو متوسطه سليمًا لديه مشكلتان تحت عرَض واحد: مهلة نهائية تخص منصة التبادل وتغطي الرحلة ذهابًا وإيابًا، وجامع قمامة يحمّل mark assist على الـ goroutine التي تقيّم المزايدة. هكذا تُقسَّم المهلة النهائية، وأي مقابض Go تؤتي ثمارها، وما الذي لا يصلحه مسار Rust الساخن.

المزايد الذي يخسر المزادات بانتهاء المهلة بينما يبدو متوسطه سليمًا يوصَف بالرقم الخطأ. المهلة النهائية تخص منصة التبادل وتغطي الشبكة في الاتجاهين. وجامع القمامة خاصية للعملية، فتُحمَّل وقفته على كل الاتصالات دفعة واحدة، أما قفزة على اتصال واحد وحده فتحتاج تفسيرًا آخر.

إعادة الكتابة بـ Rust أو ضبط الـ runtime خيار بين إجابتين عن سؤال لم يطرحه أحد: أي جزء من المهلة النهائية يُنفَق، وعلى ماذا. وما يلي يفصل الميزانية عن الجامع، والجامع عن المجدول، وإعادة الكتابة عن المكوّن الذي يستحقها.

الجواب القصير بنيوي. منصة التبادل تضبط المهلة النهائية وهي تشمل الشبكة، فالإصلاح الأول يقسّم p99 لاتصال واحد إلى زمن المعالج، وزمن انتظار معالِج، وزمن على الشبكة. وما تستطيع amBrain إثباته علنًا: بنينا RTBBidder، وهي منصة جانب طلب سُلّمت من الصفر، يقيّم فيها كل قرار مزايدة عشرات شروط الاستهداف لكل ظهور. وأرقام زمن الاستجابة التي ننشرها كقياسات تأتي من مسارات تداول لا من مزايد إعلاني، ولا رقم أدناه مقيس على مزايد لدينا.

المهلة النهائية تخص منصة التبادل، وهي تغطي الرحلة ذهابًا وإيابًا

تعرّف مواصفة OpenRTB الـ tmax بأنه أقصى زمن بالميلي ثانية تسمح به منصة التبادل لاستقبال المزايدات، شاملًا زمن الاستجابة عبر الإنترنت، وتقول إن القيمة تَجُبّ أي إرشاد سابق. والميزانية رحلة ذهاب وإياب تصل داخل كل طلب.

العتبة التي تُحاسب عليها ليست تلك التي على لوحتك. توثيق Google Authorized Buyers يشترط أن تصل 85 percent من الردود داخل المهلة النهائية مقيسةً عند موقع التداول، ويخنق المزايدين الذين لا يبلغونها. وبين ساعتها وساعتك يقع كل ما ليس حسابًا:

  • الرحلة إلى منصة التبادل والعودة، وهي جغرافيا وربط شبكي قبل أن تكون هندسة
  • إنشاء الاتصال حين يسقط keepalive، لأن مصافحة TLS جديدة داخل ميزانية مزاد تعني مزادًا خاسرًا
  • الزمن في طابور القبول قبل أن يرى معالجك الطلب، وهو ينمو تحديدًا حين تكون في أشد انشغالك
  • فك التسلسل، بتكلفة يحددها مقدار ما تحوّله من الطلب إلى كائنات، لا حجمه

لذا تقع المهلة النهائية الداخلية تحت tmax بمقدار ما يقوله المدرَّج التكراري لديك عن تكلفة الرد على ذلك الاتصال، ويُشتق هذا المقدار لكل منصة تبادل على حدة بدل ضبطه مرة واحدة للأسطول. والمهلة النهائية ليست خطة سعة: العمل الملغى عند المهلة أنفق CPU بالفعل.

وتحت الحمل الزائد يعني ذلك دفع الثمن كاملًا لردود لا يعدّها أحد، فالإصلاح الغائب هو التحكم في القبول: اقرأ tmax، وقارنه بتأخير الطابور الذي تقيسه، وأجب بـ no-bid حين لا يستقيم الحساب. الـ no-bid السريع يُحتسب ضمن الـ 85 percent؛ والمزايدة المتأخرة لا تُحتسب.

جامع واحد يخدم كل الاتصالات، فاتصال واحد بمفرده عطل مختلف

جامع القمامة خاصية للعملية، فالدورة التي تنطلق على أي اتصال تُحمَّل على كل اتصال. وأول من يفوّت المهلة هو صاحب أضيق tmax وأثقل طلب. استبعد أولًا ما ينتج الصورة نفسها بلا جامع قمامة:

  • اتصالات قليلة جدًا من منصة تبادل واحدة، لأن HTTP/1.1 يحمل طلبًا واحدًا في المرة: QPS لكل اتصال مضروبًا في زمن المعالج قرب الواحد يضع الطابور على الاتصال
  • اتصال HTTP/2 واحد، حيث تُوقف حزمة مفقودة واحدة كل تدفق يشاركه - وهو head-of-line blocking الذي يذكره RFC 9114 عام 2022 سببًا لوجود HTTP/3
  • سقوط keepalive، وهو إعداد افتراضي أكثر منه عطلًا: http.Server.IdleTimeout يعود إلى ReadTimeout حين يكون صفرًا، بينما تطلب Google مهلة خمول 2.5-minute ويغلق nginx عند 75 seconds
  • بحث متزامن عن السمات لا تطلقه إلا بعض منصات التبادل، حيث يعود الذيل إلى مخزن بعيد لا إليك

لا يصلح أيًّا من الأربعة إعدادُ جامع، وللتقسيم أدوات. ملف 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: بايتات أقل لكل طلب مزايدة، أو دورة أطول يغطي فيها عمال الخلفية قدرًا أكبر من الوسم. والأول وحده يصمد أمام تغيّر مزيج الحركة.

mark assist هو الفاتورة التي يدفعها طلب المزايدة لديك

جامع Go متزامن، والدليل الرسمي صريح في أن طول الوقفة لا يتناسب مع حجم الكومة، فانتقالات stop-the-world قصيرة. والـ assists هي المصدر الذي يهم: الـ goroutines تساعد الجامع حين يكون التخصيص سريعًا، لأن الوسم في الخلفية يأخذ ربعًا ثابتًا من المعالجات ويُحمَّل النقص على المخصِّص.

المعدل يجعل هذا عتبة لا منحدرًا. معدل التخصيص هو QPS مضروبًا في البايتات لكل طلب مزايدة مقابل حصة خلفية ثابتة، فالكود الذي لا يقوم بأي assist عند خُمس حركتك قد يقوم بـ assist في كل طلب تقريبًا عند 100K QPS.

تكلفة الوسم تتناسب مع رسم المؤشرات الحية لا مع القمامة، والمزايد يحمل الشكل الخطأ لها: فهارس الحملات، وشرائح الجمهور، وذاكرات حدود تكرار العرض. ونشرت Discord النتيجة نفسها عام 2020، مع جامع يمسح ذاكرة LRU كاملة ليقرر إن كانت الذاكرة حرة. خمس آليات تختبئ تحت عبارة واحدة:

  • انتقالات stop-the-world: قفزة مستوية على كل اتصال في اللحظة نفسها، ونادرًا ما تطول بما يكفي لخسارة مزاد بمفردها
  • mark assist: لا وقفة في التتبع إطلاقًا، بل زمن معالج نما فقط - اقرأه من حصة الـ assist في GC CPU
  • تكلفة الوسم: GC CPU يرتفع حين تكبر الكومة الحية رغم أن التخصيص لم يكبر، وهو ما تحركه المصفوفات المسطحة ولا تحركه قمامة أقل
  • تنازع المجدول: طلب يبقى جاهزًا للتنفيذ دون أن يُنفَّذ، وهو ما يظهره مقياس زمن المجدول ولا يظهره مقياس الوقفات
  • الدورة القسرية: قفزة بدورية تقارب دقيقتين على نسخة هادئة، تشير إلى أرضية الجمع لا إلى حركتك

اقرأ ذلك كخمس فواتير منفصلة. واحدة فقط يسددها إعداد جامع، ولا واحدة يسددها تغيير اللغة قبل قياس التقسيم.

الحلقة المغلقة تمحو الدليل الذي كنت تحتاجه

سمّى Gil Tene هذا العطل coordinated omission: نظام القياس ينسّق مع النظام قيد الاختبار بطريقة تتجنب قياس القيم الشاذة، لأن الحلقة المغلقة تنتظر ردًا وتتوقف عن الإرسال أثناء التوقف. ونشرت ScyllaDB مقارنة عام 2021 أبلغ فيها أحد أحمال العمل عن p99 يبلغ 249 microseconds في الحلقة المغلقة و665 ms تحت حمل مفتوح مع تصحيح، بفارق نحو 2,700 مرة.

  • حمل بحلقة مفتوحة بمعدل ثابت، مع احتساب زمن الاستجابة من وقت الإرسال المقصود لا من لحظة مغادرة الطلب
  • تصحيح على طريقة HdrHistogram كلما كان المولّد لا يصفّ الطلبات التي عجز عن إرسالها
  • p99 وp99.9 بدل المتوسط، مع احتساب الـ fan-out لديك: أظهر Dean وBarroso عام 2013 أن ملامسة 100 خادم بـ p99 يبلغ ثانية واحدة تترك 63 percent من الطلبات بطيئة
  • ملف حركة منسوخ عن منصة التبادل التي تؤلمك، يُشغَّل مدة أطول من فترة الجمع القسري وتحت حصة cgroup نفسها

مولّد الحمل الذي ينتظر ردًا يتوقف عن الإرسال في أثناء التوقف نفسه الذي بُني ليجده، ثم يُدخل الصمت في متوسط النتيجة. والمئين الذي يطبعه بعد ذلك يصف المولّد لا مزايدك.

خصّص ذاكرة أقل أولًا، ثم أدر ثلاثة مقابض

سمِّ رقم التوقف قبل أن تضبط، لأن إعادة كتابة يقررها الإنهاك ليست قرارًا. رقمان لا رقم واحد: تخصيصات الكومة لكل طلب، وهي ما يقود الـ assist، والكومة الحية، وهي ما يقود الوسم.

الترتيب هو المصدر قبل السقف، لا الأكبر مكسبًا أولًا. ابدأ حيث يتولد الـ assist: escape analysis على مسار المزايدة، وbuffers يُعاد استخدامها بدل تخصيصها، وcodec يقرأ الحقول التي تحتاجها بدل تجسيد رسم كائنات جديد. وأبقِ الـ slices قصيرة العمر: slice داخل buffer الطلب يثبّت ذلك الـ buffer كله طوال عمر المزايدة.

  • sync.Pool يخفف الضغط ولا يعد بشيء: قد يُزال أي عنصر في أي وقت دون إشعار، والكائن في التجمّع يبقى موسومًا بين إعادته وإخراجه
  • المجموعة الحية هي الفاتورة: أعد بناء فهارس الحملات والشرائح في مصفوفات مسطحة خارج المسار الساخن، فيتوقف الرسم الموسوم عن النمو مع عدد الحملات
  • GOGC يبادل الذاكرة بـ CPU الجامع بمعدل يذكره الدليل صراحة: مضاعفته تخفض تكلفة GC CPU إلى النصف تقريبًا، وتنخفض معها حصة الـ assist
  • GOMEMLIMIT هو تلك المبادلة مقابل سقف، مرن بالتصميم، لأن الحد الصارم يحوّل قفزة الكومة إلى توقف غير محدود
  • GOMAXPROCS داخل حاوية: Go 1.25 يقرأ حد cgroup لوحدة المعالجة وليس طلبات المعالجة صراحةً، فالـ pod الذي له requests بلا limit يبقى على السلوك القديم

للنهج سقف: أبلغت Uber عام 2021 أن ضبط GOGC مقابل حد ذاكرة الحاوية استعاد نحو 70,000 نواة عبر خدماتها الحرجة. وتلك نتيجة تكلفة لا مئين.

ما يمنحك إياه مسار Rust الساخن، وما تدفعه مقابله

وصفت RTB House خدمة مزايدة على JVM في يونيو 2025 أنتج فيها التقسيم إلى microservices حجمًا كبيرًا من الطلبات الصغيرة: كان على زمن الاستجابة المضاف أن يبقى داخل 7 ms مقابل طلب متوسطه نحو 2.5 ms، وانكسر المئينان 98 و99 تحت وقفات G1 المتكررة. فانتقلوا إلى generational ZGC ودفعوا الثمن ذاكرةً.

لاحظ ما تطلّبه ذلك الإصلاح: جامع ثانٍ للانتقال إليه. وGo يشحن جامعًا واحدًا غير قابل للاستبدال، فالروافع في Go هي معدل التخصيص، وشكل المجموعة الحية، وGOGC مقابل GOMEMLIMIT. أما ما يزيله مسار Rust الساخن فمحدد: لا assist، ولا وسم في الخلفية، ولا دورة قسرية. وما لا يزول أطول مما تتوقعه أغلب الفرق:

  • المخصِّص، إضافة إلى page faults ووضع NUMA: تنص مكتبة Rust القياسية على أن المخصِّص العام الافتراضي غير محدد، وmalloc تحت خيوط كثيرة مصدر للذيل
  • الجدولة، لأن runtime غير متزامن مع تجمّع عمال يعيد إنتاج آثار مجدول Go لحظة هبوط مهمة حاجبة على عامل
  • محاسبة الذاكرة، لأن GOMEMLIMIT يغطي ذاكرة Go runtime فقط: مخصِّص Rust داخل العملية نفسها يقع خارج سقفك، وينتقل الإنفاذ إلى OOM killer
  • الناس، أي من يناوب على المسار الساخن ليلًا وكم تكلّف سلسلتا أدوات في مستودع واحد
  • الحد، إذا بقي Go في الخارج: قاست Cockroach Labs استدعاء cgo عند 171 ns مقابل 1.83 ns لاستدعاء Go عام 2015، والنسبة هي ما يبقى

إذا كان الذيل يسكن في تحليل الطلب أو في الاتصال بمنصة التبادل أو في اصطفاف المجدول، فإن Rust لا يعيد أيًا من تلك الميلي ثوانٍ، وإعادة كتابة المكوّن الخطأ تنفق ربع سنة لتبقى على معدل انتهاء المهلة نفسه.

لذا انقل أصغر قطعة تملك التخصيصات، لا الخدمة كلها: حلقة تقييم الظهور وفهارسها، واختيار المرشحين، والاستهداف، وعمليات البحث عن تكرار العرض والميزانية، والتقييم. وسعّر الحدّ لكل عبور: مرة واحدة لكل طلب مزايدة مع buffer مسطح، ولا مرة لكل قاعدة استهداف أبدًا. وتحقّق على نسخ منفصلة تُغذّى بتدفق مرآة، لا داخل العملية قيد الاختبار أبدًا، حيث يضاعف المسار الظلّي الكميتين اللتين تقيسهما.

اختبار قبول يصلح للمسارين: شغّل نافذة قصيرة وجامع القمامة مطفأ، تحت سقف ذاكرة تتحكم به، وسجّل p99.9 داخل المعالج. شغّله على نسخة واحدة خلف جزء من الحركة مع تراجع تلقائي، لأن إطفاء GOGC يجعل قفزة الكومة نحو السقف تدفع الـ runtime إلى دورات متلاحقة، والدليل يقول إن ذلك التوقف قد يكون غير محدود. ولا تقرأ النتيجة إلا إذا لم يتدخل محدِّد GC CPU أبدًا: فمتى تدخّل صار المئين يصف المحدِّد.

الشركة التي تصلح هذا تطلب تقرير انتهاء المهلة أولًا

النصف الثاني من السؤال، من يقوم بهذا النوع من العمل، له اختبار لا يحتاج قائمة مورّدين. فالشركة التي تصلح زمن استجابة يقوده GC تتصرف بغير ما تتصرف شركة تبيع إعادة كتابة:

  • يطلب توزيع tmax وتقرير انتهاء المهلة لكل منصة تبادل قبل أن يطلب المستودع
  • يذكر التقسيم - المعالج والمجدول والشبكة - وأيًّا من الثلاثة يتوقع أن يملك الميلي ثواني، قبل أن يقترح لغة
  • يأتي بمولّد حلقة مفتوحة وملف حركة خاصين به، ويرفض مئينًا من حلقة مغلقة دليلًا
  • يسمّي معيار الخروج مسبقًا: أي منصة تبادل، وأي مئين، وأي هامش مقابل tmax الخاص بها، وكم ذاكرة لكل ألف طلب
  • قادر على تزويد المسار الساخن بالكوادر بعد ذلك، لأن إعادة كتابة لا يناوب عليها أحد ليلًا هي الحادثة الثانية

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

فالسؤال الأول ليس أي لغة. بل أي من المجاميع الثلاثة - المعالج أو المجدول أو الشبكة - يملك الميلي ثواني المفقودة على الاتصال الذي تنتهي مهلته، وهل تتحرك البايتات المخصصة لكل طلب مزايدة حين تهاجمها.

ما تستطيع amBrain إثباته علنًا: amBrain شركة تطوير برمجيات متخصصة في منصات التداول ومحركات المطابقة وأنظمة المزايدة الفورية وهندسة منصات الكازينو، ونبني البرمجيات منذ 2019، وما نبنيه في AdTech هو تطوير DSP ومنصات المزايدة الفورية وهندسة منصات تبادل الإعلانات. ونعمل بثلاث صيغ: تسليم كامل، أو فريق مخصص، أو مهندسون مدمجون في فريقك. وإذا كنت توازن بين إعادة كتابة وجولة ضبط، فالمحادثة الجديرة بالإجراء هي التي تجري التقسيم قبل أن تختار لغة.

هل لديك تصميم مشابه على الطاولة؟

أحضر بنيتك الحالية وسيناريو الفشل الذي يقلقك، وسنراجعه معًا خلال نصف ساعة.

مقالات ذات صلة

تعذّر تحميل الصورة
AdTech
Sep 10, 2026قراءة 10 دقائق

قياس الإعلانات يفقد أحداثًا عند الذروة: الوصلات والمفاتيح المكررة والربط مع المزايد

اقرأ المقال
تعذّر تحميل الصورة
AdTech
Mar 5, 2026قراءة 7 دقائق

كيف يعيد الذكاء الاصطناعي تشكيل الإعلان البرمجي في 2026

اقرأ المقال
تعذّر تحميل الصورة
AdTech
Feb 14, 2026قراءة 6 دقائق

الاستهداف بالخصوصية أولًا: بناء ad tech بلا ملفات تعريف ارتباط من طرف ثالث

اقرأ المقال