تصل نسبة استهلاك وحدة المعالجة المركزية (CPU) لقاعدة البيانات لديك إلى 100% في الثالثة صباحاً. تبدو حركة المرور طبيعية، وعدد الطلبات ليس مرتفعاً بشكل غير معتاد. ومع ذلك، تنهار مخازن البيانات الأساسية لديك لأن كل طلب من تلك الطلبات يطلب الشيء نفسه تماماً في الوقت نفسه تماماً.

هذه هي مشكلة "القطيع الهائج" (thundering herd problem).

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

أين يتشكل القطيع

يظهر هذا النمط كلما تزامنت عمليات عديدة على حدث واحد. لست بحاجة إلى حركة مرور خبيثة؛ فالأنظمة العادية تفعل ذلك بنفسها.

انتهاء صلاحية التخزين المؤقت (Cache expiration). تنتهي صلاحية مفتاح تخزين مؤقت نشط (hot cache key). قد يكون كتالوج منتجات، أو علامة ميزة (feature flag)، أو قائمة أذونات مستخدم. تلاحظ آلاف خوادم التطبيقات الفراغ في آن واحد. وكل خادم، وبحسن نية، يفتح اتصال قاعدة بيانات خاص به ويقوم بتشغيل نفس الاستعلام الثقيل لإعادة بناء القيمة. لم يتم تهيئة قاعدة البيانات أبداً للإجابة على نفس السؤال المكلف ألفي مرة بالتوازي. مفتاح واحد منتهي الصلاحية قد يؤدي إلى انهيار العنقود (cluster) بأكمله.

إيقاظ مجمع الاتصالات (Connection pool wakeups). في بعض البنيات البرمجية، تتوقف العديد من عمليات العمل (worker processes) عند شرط واحد، بانتظار وصول مهام جديدة. وعندما يتحقق هذا الشرط، يقوم نظام التشغيل بإيقاظ كل عامل نائم. يقوم عامل واحد فقط فعلياً بالحصول على الاتصال أو المهمة، بينما يستيقظ الباقون ليكتشفوا أنهم خسروا السباق، ثم يعودون للنوم. هذه الدورة تستهلك وحدة المعالجة المركزية في عمليات تبديل السياق (context switches) ويمكن أن تحرم العمل الإنتاجي الفعلي من الموارد.

عواصف إعادة المحاولة (Retry storms). تتعثر خدمة تابعة (downstream service). يلتقط كل عميل مهلة الانتظار (timeout) وينتظر فترة زمنية ثابتة، لنقل ثانية واحدة بالضبط، قبل المحاولة مرة أخرى. وعندما تتعافى الخدمة وتقبل حركة المرور مجدداً، يهاجمها كل عميل في اللحظة نفسها. فتسقط الخدمة مرة أخرى وهي لا تزال في مرحلة التعافي والبدء. وتتكرر الدورة.

تصادم مهام Cron. إذا قمت بجدولة أعمال الصيانة، أو إنشاء التقارير، أو تسخين التخزين المؤقت (cache warming) لتعمل في تمام الساعة 00:00 بالتوقيت العالمي المنسق (UTC) عبر مجموعة من المثيلات (instances)، فإنك تصنع طفرة في حركة المرور بدقة متناهية. الحمل هنا يمكن التنبؤ به، لكن تركيزه قاتل.

دمج الطلبات (Request Coalescing)

الحل الأكثر فعالية لـ "تدافع التخزين المؤقت" (cache stampedes) هو التوقف عن الإجابة على نفس السؤال أكثر من مرة واحدة. عندما يحدث إخفاق في التخزين المؤقت (cache miss)، فأنت لا تريد 2500 خيط معالجة (thread) يقوم كل منها بتشغيل استعلام قاعدة بيانات. بل تريد خيطاً واحداً يقوم بتشغيل الاستعلام بينما ينتظر الـ 2499 الآخرون تلك النتيجة.

يُطلق على هذا النمط غالباً اسم "دمج الطلبات" (request coalescing). في لغة Go، توفر حزمة singleflight في golang.org/x/sync تطبيقاً نموذجياً لهذا النمط. أول goroutine يستدعي Do لمفتاح معين يبدأ العمل، بينما يتوقف المستدعون اللاحقون لنفس المفتاح عند استدعاء Do نفسه. وعندما ينتهي العمل، يتلقى جميع المنتظرين النتيجة معاً. وبذلك تكون قد نفذت استعلاماً واحداً وخدمت 2500 طلب.

يمكنك بناء سلوك مماثل باستخدام خريطة متزامنة في الذاكرة (in-memory concurrent map) من الوعود (promises)، أو المستقبلات (futures)، أو القنوات (channels). الحيلة تكمن في التحقق من الطلب الجاري وتسجيله بشكل ذري (atomically). إذا فشل الطلب، يحصل الجميع على الخطأ، وهو السلوك الصحيح عادةً. وإذا نجح، يحصل الجميع على القيمة المخزنة مؤقتاً، وتصيب الطلبات اللاحقة التخزين المؤقت مباشرة. وبذلك ينخفض حمل قاعدة البيانات من طفرة عمودية حادة إلى خط مستوٍ.

انتهاء الصلاحية المبكر الاحتمالي (Probabilistic Early Expiration)

أحياناً، قد ترغب في تجنب التدافع تماماً بدلاً من إدارته. وهنا يأتي دور "انتهاء الصلاحية المبكر الاحتمالي"، والذي يتم تنفيذه عبر خوارزميات مثل XFetch.

بدلاً من وقت انتهاء صلاحية صارم، تحمل كل قيمة مخزنة مؤقتاً نافذة انتهاء صلاحية مرنة (soft expiration window). عندما يصل طلب ما، يقوم التطبيق بتوليد رقم عشوائي وتطبيق فحص احتمالي بسيط. إذا كانت نتيجة "رمية النرد" إيجابية، يقوم هذا الطلب الوحيد بتحديث التخزين المؤقت مبكراً. وإذا لم تكن كذلك، يقوم الطلب بتقديم القيمة القديمة قليلاً (stale value).

ولأن كل طلب يرمي نردَه الخاص، تتوزع عمليات التحديث عبر النافذة المرنة. قد يقوم أحد العملاء بالتحديث قبل أربعين ثانية من انتهاء الصلاحية الصارم، وآخر قبل اثنتي عشرة ثانية، بينما لا يقوم معظم الآخرين بالتحديث على الإطلاق. ينتقل العمل من طفرة متزامنة واحدة إلى لطيف