تعتمد هذه المقالة فقط على مصادر مؤرخة تابعة لجهات خارجية - ولا تعتمد أبدًا على تشفير Aurabase المخترع. لم يتم تضمين زمن الوصول p99 الخاص بالواجهة الخلفية لدينا. نكتب نواة تطبيقنا بلغة Rust، بدون أداة تجميع البيانات المهملة: وهي حقيقة يمكن التحقق منها مباشرة في الكود ومساحة العمل Cargo وخدمات axum. ومع ذلك، لم ننشر بعد منهجية مرجعية p99 قابلة للتكرار لإثبات ذلك بالأرقام. يشرح هذا النص آلية، وليس نتيجة مقاسة.
الأساسيات
- يقيس p99 أبطأ استعلام من بين مائة - المكان الذي يكون فيه توقف أداة تجميع البيانات المهملة (GC) هو الأكثر ضررًا، وليس في المتوسط (Dean & Barroso، "The Tail at Scale،" Google، 2013).
- يقوم GC بمقاطعة البرنامج بأكمله ("إيقاف العالم") لتحرير الذاكرة غير المستخدمة. لا يحتوي الصدأ على GC: يتم تحرير الذاكرة في اللحظة المحددة عندما تخرج القيمة عن النطاق، ويتم التحقق منها بواسطة المترجم.
- قام Discord بتوثيق خدمة ذاكرة التخزين المؤقت في عام 2020 حيث قام Go بتشغيل دورة جمع البيانات المهملة كل دقيقتين على الأقل، حيث تسببت كل دورة في ارتفاع كبير في زمن الوصول (مدونة Discord Engineering).
- يستغرق تقليل الإيقاف المؤقت لـ GC سنوات من الهندسة، حتى في Google: فقد انتقل مُجمِّع GB من 300-400 مللي ثانية إلى 500 ميكروثانية بين عامي 2015 و2018، دون أن يصل إلى الصفر على الإطلاق (go.dev).
- تمت كتابة جوهر الواجهة الخلفية لـ Aurabase بلغة Rust، بدون أداة تجميع البيانات المهملة - وتم التحقق منها في الكود. لم يتم نشر أي أرقام زمن وصول Aurabase p99 حتى الآن: تظل هذه آلية وليست قياسًا.
لماذا p99 ليس متوسطًا؟
المتوسط يخفي الأساسيات. إذا تم الرد على 99 طلبًا من أصل 100 خلال 5 مللي ثانية واستغرق طلب واحد فقط 500 مللي ثانية، فسيظل المتوسط منخفضًا. لكن مستخدمًا واحدًا من كل مائة يواجه الانتظار لفترة أطول مائة مرة. يقيس p99 هذا الطلب بالضبط: أبطأ جزء من المائة، وهو الطلب الذي ينتهك اتفاقية مستوى الخدمة الخاصة بك بينما تظل لوحة التحكم الخاصة بمتوسط زمن الاستجابة باللون الأخضر.
في جوجل، قام جيفري دين ولويز أندريه باروسو بإضفاء الطابع الرسمي على هذه المشكلة في "The Tail at Scale" (Communications of the ACM، المجلد 56، 2013). ملاحظتهم، والتي غالبًا ما يتم الاستشهاد بها منذ ذلك الحين: "قد تهيمن حلقات الكمون العالي المؤقتة التي لا تعد مهمة في الأنظمة متوسطة الحجم على الأداء العام للخدمة على نطاق واسع". باختصار: حلقات عرضية من الكمون، لا تكاد تذكر على نطاق صغير، تنتهي في نهاية المطاف بالسيطرة على الأداء المتصور للنظام الموزع.
الواجهة الخلفية التي تعالج آلاف الطلبات في الثانية لا بد أن ترسل، عند نقطة أو أخرى، طلبًا يقع أثناء توقف GC مؤقتًا. على نطاق واسع، هذه ليست حالة غير شائعة. وهذا يقين إحصائي.
ما الذي يفعله جامع القمامة، ولماذا يوقف كل شيء مؤقتًا؟
يقوم جامع البيانات المهملة (GC) بتتبع الكائنات الحية للبرنامج بشكل مستمر - تلك التي لا يزال يتم الرجوع إليها في مكان ما - ويحرر ذاكرة الكائنات التي أصبح يتعذر الوصول إليها. يسمى هذا التتبع tracing: يمر GC عبر الرسم البياني المرجعي، ويحدد ما لا يزال مستخدمًا، ثم يمسح الباقي.
المشكلة: يؤدي اجتياز هذا الرسم البياني أثناء استمرار البرنامج في إنشاء مراجع جديدة إلى نتائج غير متناسقة. الإجابة التاريخية، التي لا تزال تستخدم كملاذ أخير من قبل العديد من GCs الحديثة، هي stop-the-world - يتوقف البرنامج بأكمله مؤقتًا أثناء وضع العلامات والمسح الضوئي. كلما كانت الكومة أكبر، كلما كانت فترة التوقف المؤقت أطول: تعتمد مدتها على حجم البيانات المباشرة، وليس على عبء العمل الحالي.
تستخدم معظم GCs الحديثة استراتيجية الأجيال: فهي تفترض أن غالبية الأشياء تموت في سن مبكرة. ولذلك يتم فحص التخصيصات الأخيرة كثيرًا، ولكن بسرعة، في منطقة ذاكرة صغيرة. تنتقل الكائنات التي تنجو من عدة دورات إلى منطقة أكبر، ويتم فحصها بشكل غير متكرر - ولكن عندما تحتاج هذه المنطقة إلى التنظيف، فإن فترة الإيقاف المؤقت المرتبطة بها تنمو مع حجمها. إن هذا الفاصل "الرئيسي"، وليس الفواصل "الثانوية" الصغيرة، هو الذي يهيمن على p99 لخدمة ذات حركة مرور عالية وتخصيص مرتفع.
تعمل وحدات التحكم العامة المتزامنة والأجيال الحديثة على تقليل تكرار ومدة فترات الراحة هذه من خلال العمل بالتوازي مع البرنامج. لكن جميعها تقريبًا تحتفظ بآلية احتياطية لإيقاف العالم في الحالات الحدودية - ويستغرق تقليلها سنوات من الهندسة. يقدم القسم 04 مثالاً كميًا ومصادرًا.
Discord، 2020: يصبح انقطاع GC حادثًا إنتاجيًا
في فبراير 2020، نشر المهندس جيسي هوارث منشورًا أصبح مرجعًا في الصناعة: "لماذا يتحول Discord من Go إلى Rust" (مدونة Discord Engineering). تدير الخدمة ذات الصلة، حالات القراءة، حالة قراءة الرسائل لملايين المستخدمين - عشرات الملايين من الإدخالات لكل ذاكرة تخزين مؤقت - مع مئات الآلاف من التحديثات في الثانية.
التشخيص مباشر، كما هو مذكور في المقالة: "سيفرض Go عملية جمع البيانات المهملة كل دقيقتين على الأقل". بمعنى آخر، يقوم Go بتشغيل دورة جمع البيانات المهملة كل دقيقتين على الأقل على هذه الخدمة - وتنتج كل دورة ارتفاعًا كبيرًا في زمن الوصول مرئيًا في الرسوم البيانية للفريق.
قام الفريق أولاً بتقليل حجم ذاكرة التخزين المؤقت لتنعيم المسامير. ظلت التسوية غير مواتية: توقفات أقل لـ GC مؤقتًا، ولكن المزيد من طلبات ذاكرة التخزين المؤقت المفقودة التي تقع على قاعدة البيانات - وبالتالي انخفاض إجمالي p99 في مكان آخر. كان الإصلاح الأساسي هو إعادة كتابة الخدمة في Rust، دون الحاجة إلى مراقبة أداة تجميع البيانات المهملة.
أثار المنشور نقاشًا تقنيًا حيويًا: أكثر من 1580 نقطة و642 تعليقًا على Hacker News في نفس يوم نشره (4 فبراير 2020) - في إشارة إلى أن المشكلة تتجاوز بكثير قضية Discord.
ثلاث سنوات من الهندسة في Google لإسقاط التوقف المؤقت من 400 مللي ثانية إلى 500 ميكروثانية
يوضح جامع البيانات المهملة Go حجم الجهد المطلوب لترويض توقف GC مؤقتًا - حتى مع موارد فريق متخصص في Google. قام ريك هدسون، المدير الفني لـ Go GC، بتوثيق هذه القصة في منشورين رسميين على مدونة Go.
| قبل أغسطس 2015 | 300-400 مللي ثانية | جامع Go التاريخي، قبل إعادة التصميم |
|---|---|---|
| أغسطس 2015 · اذهب 1.5 | 30-40 مللي ثانية | أول جامع منافس، الهدف <10 مللي ثانية |
| 2016 · اذهب 1.6 | < 10 مللي ثانية (SLO مع الاستمرار) | تم تحقيق الهدف الأولي في الإنتاج |
| مارس 2017 · اذهب 1.8 | تحت ميلي ثانية | إزالة فحص المكدس الخاص بإيقاف العالم |
| أغسطس 2017 · اذهب 1.9 | 100-200 ميكروثانية (علامة) | معيار غير رسمي جديد ذكره الفريق |
| 2018 · الإعلان عن SLO | 500 ميكرو ثانية لكل دورة | هدف الخدمة تم صياغته رسميًا بواسطة ريك هدسون |
المصدر: "الذهاب: رحلة جامع القمامة في Go"، go.dev، 12 يوليو 2018؛ و"Go GC: إعطاء الأولوية لزمن الاستجابة المنخفض والبساطة"، go.dev، 31 أغسطس 2015.
لقد أدت ثلاث سنوات من العمل المتفاني إلى خفض الاستراحة النموذجية بعامل الألف. لكن التوقف المؤقت لم يختف أبدًا: إنه هدف خدمة (SLO)، وليس ضمانًا مطلقًا للصفر. يجب أن يجتاز تتبع GC، من خلال البناء، رسمًا بيانيًا للكائنات الحية من وقت لآخر. المتغير الوحيد القابل للتعديل هو تكرار هذه الرحلة ومدتها، وليس وجودها.
وهذا الاختيار للأولوية ليس محايدا. يستهدف Go في المقام الأول خدمات الشبكة والواجهات الخلفية للويب، حيث يؤدي التوقف المؤقت لعدة مئات من المللي ثانية إلى انقطاع تجربة المستخدم مباشرةً - ومن هنا يأتي الجهد الهائل المستثمر في زمن الوصول بدلاً من إنتاجية GC الأولية. وقد ورثت أوقات التشغيل المُدارة الأخرى مقايضات مختلفة، والتي تشكلت من خلال حالات الاستخدام التاريخية الخاصة بها، قبل أن تعوض عن ذلك باستخدام أدوات التجميع ذات الإيقاف المؤقت المنخفض الخاصة بها. تظل النقطة المشتركة كما هي: جميعها تبدأ من تتبع GC، وبالتالي من آلية الإيقاف المؤقت التي سيتم تقليلها إلى الحد الأدنى - ولا يتم إلغاؤها أبدًا عن طريق البناء.
لماذا لا يعاني الصدأ من هذه المشكلة عن طريق البناء
الصدأ لا يقلل من توقف GC مؤقتًا: فهو يلغي الآلية التي تسببها. يتتبع المترجم، عند التجميع، من يملك كل قيمة ذاكرة - وهذا هوownership. عندما يخرج مالك القيمة عن النطاق، يقوم Rust تلقائيًا بإدراج الاستدعاء الذي يحرر تلك الذاكرة، في نفس المكان في الكود الثنائي. تسمى هذه الآلية RAII (تهيئة الحصول على الموارد): الإصدار حتمي، ولم تتم جدولته بواسطة أداة تجميع البيانات المهملة التي تعمل في الخلفية.
يلخص Alexandru Nedelcu، مؤلف مدونة تقنية معترف بها في نظام Scala/Rust البيئي، المقايضة في مقال حديث: "المقايضة التي تقوم بها Rust هي سهولة الاستخدام، وتفضيل الأداء مع زمن استجابة وأمان يمكن التنبؤ بهما" (alexn.org، 21 يوليو 2026). يستبدل الصدأ بعضًا من بساطة الكتابة بزمن انتقال يمكن التنبؤ به.
تلخص المقالة نفسها سبب عدم كفاية GCs الحديثة دائمًا: "تحاول GCs الحديثة القيام بعملها بشكل تدريجي ومتزامن، دون التأثير على البرنامج. لكن قدرتها محدودة، وتعود إلى دورة GC التي توقف العالم والتي تجمد البرنامج بأكمله، وبالتالي تؤثر على زمن الوصول".
إليك الآلية في حوالي عشرة أسطر - مثال عام، وليس مقتطفًا من كود Aurabase:
فارق بسيط مهم: ليس كل شيء مجاني. تضيف أنواع العد المرجعي (Rc, Arc) تكلفة صغيرة لكل نسخة وإصدار. وتظل هذه التكلفة محلية وحتمية. لا يوجد أبدًا توقف مؤقت يؤدي إلى تجميد البرنامج بأكمله أثناء مروره عبر كومة الذاكرة.
توضيح مفيد للواجهة الخلفية غير المتزامنة: وقت تشغيل Rust async (tokio، الذي تستخدمه جميع خدمات Aurabase) ليس له علاقة بأداة تجميع البيانات المهملة. يقوم بجدولة المهام التعاونية على مجموعة من مؤشرات الترابط، ولكن لا يتكرر أبدًا من خلال رسم بياني للكائن المباشر لتحرير الذاكرة. يعد الارتباك أمرًا شائعًا بسبب الأنظمة البيئية حيث تتم إدارة وقت التشغيل غير المتزامن وGC بواسطة نفس الجهاز الظاهري.
ما الذي يتغير هذا بالنسبة للواجهة الخلفية لحركة المرور العالية
في الواجهة الخلفية التي تخدم آلاف الطلبات المتزامنة، يؤدي غياب GC إلى إزالة متغير من المعادلة p99. لم تعد هناك حاجة إلى تحديد حجم كومة الذاكرة، أو ضبط أجيال المجمع، أو مراقبة دورة يمكن أن تقع في أسوأ وقت. يعتمد زمن استجابة الطلب الفردي على عمله، وليس على بعض الأحداث العالمية التي لا يمكن التنبؤ بها في مكان آخر من البرنامج.
يطبق جوهر الواجهة الخلفية لـ Aurabase هذا المبدأ: جميع الخدمات (aura-gateway, aura-auth, aura-db, aura-realtime, aura-storage, aura-functions, aura-ai...) مكتوبة بلغة Rust، منظمة في مساحة عمل Cargo واحدة . يمكن التحقق من ذلك مباشرة في المستودع:
مقتطف فعلي من Cargo.toml، إصدار مساحة العمل 2021، محلل الإصدار v2 - تم التحقق منه في مستودع Aurabase.
ما لا تثبته هذه الحقيقة، في هذه المرحلة: رقم زمن الوصول p99 الذي تم قياسه لـ Aurabase. لم ننشر بعد منهجية مرجعية قابلة للتكرار للواجهة الخلفية الخاصة بنا - وهذا عمل قيد التقدم، وليس نتيجة متاحة اليوم. إن عدم وجود أداة تجميع البيانات المهملة هي آلية تم التحقق منها في الكود. هذا وحده ليس دليلاً على الكمون p99 المقاس. ضع هذا التمييز في الاعتبار في مواجهة أي حجة تسويقية حول هذا الموضوع، بما في ذلك حجةنا - راجع المقارنة الفنية الخاصة بنا Aurabase vs Supabase للحصول على تفاصيل البنية.
يتطلب قياس p99 بشكل صحيح نظامًا خاصًا به: ظروف حمل تمثيلية، ونسب مئوية محسوبة على نافذة منزلقة واسعة بما فيه الكفاية، وبيئة اختبار قريبة من الإنتاج. إن نشر رقم بدون هذه المنهجية يشبه نشر رقم تسويقي. وهذا بالضبط ما نرفض القيام به في هذا المقال.
ما لا يحل غياب GC
تؤدي إزالة أداة تجميع البيانات المهملة إلى التخلص من مصدر واحد فقط من مصادر الكمون الخلفي - وليس جميعها. لا يزال بإمكان الواجهة الخلفية لـ Rust إظهار p99 متدهورًا بسبب انتظار الشبكة، أو تجمع اتصال Postgres المشبع، أو قفل قاعدة البيانات المتنازع عليها، أو استعلام SQL مفهرس بشكل سيئ، أو استدعاء API بطيء لجهة خارجية. تعمل الآلية الموضحة في هذه المقالة على إزالة السبب الهيكلي. ولا يوفر الحصانة ضد الآخرين.
في Aurabase على سبيل المثال، تتواصل كل خدمة مع Postgres عبر مجمع اتصال (sqlx) ومع الخدمات الأخرى عبر NATS JetStream. إن مجموعة صغيرة الحجم، أو اشتراك NATS بطيء الاستخدام، أو استعلام SQL بدون فهرس مناسب، يؤدي كل منها إلى ارتفاع زمن الاستجابة الخاص به - بغض النظر عن عدم وجود أداة تجميع البيانات المهملة.
الاستنتاج العملي: يعد غياب GC سببًا معماريًا جيدًا لاختيار واجهة خلفية Rust لنظام مدرك لـ p99. ولا يعد هذا في حد ذاته ضمانًا لوقت الاستجابة - لا في Aurabase ولا في أي مكان آخر. تظل الطريقة المهمة كما هي: القياس، ونشر المنهجية، ثم تصحيح ما تكشفه القياسات. إذا كنت تقوم بالترحيل من الواجهة الخلفية باستخدام GC، فإن دليل الترحيل Supabase إلى Aurabase يوضح بالتفصيل ما الذي يتغير وما الذي يظل على حاله.