حادثة Knight Capital: كيف خسرت شركة 460 مليون دولار بسبب سيرفر واحد

في 1 أغسطس 2012 خسرت Knight Capital أكثر من 460 مليون دولار خلال 45 دقيقة بسبب نشر (deployment) ناقص على سيرفر واحد. هذه دروس الحادثة لكل مطوّر.

قد تبدو القصة بعيدة عنك إن كنت تبني تطبيق Laravel لا نظام تداول. لكن الخطأ الذي أسقط الشركة مألوف جدًا: نشر لم يكتمل على كل السيرفرات، وكود قديم لم يُحذف، وعلم (flag) أُعيد استخدامه لغرض جديد، وغياب أي تنبيه حقيقي. ما نعرفه عن الحادثة موثّق في أمر رسمي أصدرته هيئة الأوراق المالية الأمريكية (SEC)، ولذلك سأعتمد عليه في كل رقم وارد هنا.

ماذا حدث بالضبط في حادثة Knight Capital؟

كانت Knight شركة وساطة أمريكية تدير نظامًا آليًا لتوجيه أوامر الأسهم اسمه SMARS. وكانت بورصة نيويورك (NYSE) ستطلق في 1 أغسطس 2012 برنامجًا جديدًا للسيولة للمستثمرين الأفراد اسمه Retail Liquidity Program أو RLP، فكتبت Knight كودًا جديدًا في SMARS لدعمه.

بحسب أمر الـ SEC (الفقرات 12 إلى 17)، جرت الأحداث هكذا:

  • الكود الجديد كان سيحل محل كود قديم غير مستخدم اسمه Power Peg، توقفت الشركة عن استخدامه قبل ذلك بسنوات (عام 2003)، لكنه بقي موجودًا وقابلًا للاستدعاء داخل النظام.
  • الكود الجديد أعاد استخدام علم قديم كان يفعّل Power Peg. أي أن قيمة هذا العلم "yes" كانت ستشغّل RLP الجديد على السيرفرات المحدّثة.
  • بدءًا من 27 يوليو نُشر الكود الجديد تدريجيًا على السيرفرات الثمانية لنظام SMARS. لكن فنيًا واحدًا لم ينسخ الكود إلى السيرفر الثامن، ولم يراجع أحد عمله، ولم تكن هناك إجراءات مكتوبة تفرض مراجعة ثانية.
  • صباح 1 أغسطس، عالجت السيرفرات السبعة الأوامر بشكل صحيح. أما السيرفر الثامن فاستقبل الأوامر بالعلم نفسه، فشغّل كود Power Peg القديم.

والمشكلة أن Power Peg نفسه كان معطوبًا: في 2005 نُقلت دالة تتبّع الكمية المنفّذة إلى موضع أبكر في الكود، دون إعادة اختبار Power Peg بعد ذلك. النتيجة أن السيرفر الثامن لم يعد يتوقف عند اكتمال الأمر الأصلي، فظل يرسل أوامر فرعية متتالية.

الحصيلة بالأرقام

حسب الـ SEC، حوّل SMARS 212 أمرًا صغيرًا من عملاء التجزئة إلى ملايين الأوامر في السوق خلال نحو 45 دقيقة: أكثر من 4 ملايين عملية تنفيذ في 154 سهمًا، بما يزيد على 397 مليون سهم. وبلغت خسارة Knight من هذه المراكز أكثر من 460 مليون دولار. ودفعت الشركة لاحقًا غرامة مدنية قدرها 12 مليون دولار ضمن تسوية مع الهيئة، دون أن تُقرّ بالمخالفة أو تنفيها.

أخطاء متراكمة لا خطأ واحد

من السهل أن نلخّص الحادثة بعبارة "نسي موظف نسخ ملف". لكن أمر الـ SEC يبيّن أن المشكلة كانت سلسلة ثغرات، وأن سدّ أي حلقة منها كان يمكن أن يخفف الضرر أو يمنعه.

1. نشر يدوي بلا مراجعة ولا تحقق

لم تكن لدى Knight إجراءات مكتوبة لنشر الكود في SMARS، ولا شرط لمراجعة فني ثانٍ. ولم يكن هناك أي فحص يؤكد أن السيرفرات الثمانية تشغّل الإصدار نفسه.

2. كود ميّت بقي في الإنتاج

بقي Power Peg في الإنتاج سنوات بعد التوقف عن استخدامه، ولم يكن عند الشركة بروتوكول يفرض اختبار الكود غير المستخدم بعد الوصول إليه. الكود الذي لا تحتاجه يبقى التزامًا عليك، لأنه قد يعود للحياة.

3. إعادة استخدام علم قديم

حين تعيد توظيف علم موجود بدلًا من إنشاء علم جديد باسم جديد، فإنك تربط الميزة الجديدة بكل سلوك سابق لذلك العلم. ولو استخدم RLP علمًا جديدًا لما ارتبط سلوك السيرفر الثامن بكود Power Peg القديم، على الأرجح.

4. إنذارات وصلت ولم يقرأها أحد

قبل افتتاح السوق أرسل نظام داخلي 97 رسالة بريد تحمل الخطأ "Power Peg disabled". لكنها لم تُصمَّم كتنبيهات، ولم يراجعها الموظفون عادة. وصلت في الوقت الفعلي، وكانت فرصة لاكتشاف المشكلة قبل افتتاح السوق، لكن لم يتصرف عليها أحد.

5. غياب الفرامل الآلية

لم تكن هناك ضوابط في SMARS تقارن الأوامر الخارجة بالداخلة، ولا آلية توقف النظام تلقائيًا عند سلوك شاذ، ولا حدود رأسمالية مرتبطة بإيقاف آلي لإرسال الأوامر. أداة المراقبة الرئيسية PMON كانت تعتمد على مراقبة بشرية ولا تُصدر تنبيهات آلية.

6. استجابة للحادثة زادت الطين بلّة

لم تكن هناك إجراءات للتعامل مع الحوادث. وفي إحدى المحاولات، أزال الفريق الكود الجديد من السيرفرات السبعة السليمة، فصارت هي الأخرى تشغّل Power Peg، وتفاقمت المشكلة. هذا درس مهم عن الـ rollback: التراجع إلى حالة سابقة ليس آمنًا تلقائيًا، خصوصًا حين تكون الحالة السابقة تحتوي كودًا معطوبًا.

ماذا نتعلّم منها في مشاريع Laravel؟

أنت لا تتداول بملايين الدولارات، لكن المبادئ نفسها تنطبق على أي تطبيق إنتاجي.

انشر آليًا وليس يدويًا. استخدم سكربت أو أداة نشر (Forge أو Envoyer أو GitHub Actions أو Laravel Cloud) تنفّذ الخطوات نفسها على كل سيرفر. النشر اليدوي على عدة سيرفرات هو بالضبط ما حدث للسيرفر الثامن.

تحقق من أن كل السيرفرات تشغّل الإصدار نفسه. أبسط طريقة أن تكشف التطبيق رقم الإصدار في مسار داخلي:

// routes/web.php
Route::get('/_release', fn () => response()->json([
    'release' => config('app.release'), // يُضبط أثناء النشر، مثل commit SHA
]));

ثم يتحقق سكربت النشر من أن كل سيرفر يعيد القيمة نفسها قبل تحويل الزبائن إليه:

for host in app1 app2 app3; do
  curl -s "https://$host.internal/_release"
done | sort -u | wc -l   # يجب أن تكون النتيجة 1

احذف الكود الميّت. الفروع القديمة والـ jobs المتروكة والـ routes غير المستخدمة تُحذف، ولا تُترك "احتياطًا". نظام التحكم بالإصدارات يحفظ تاريخها إن احتجتها.

لا تعد استخدام علم أو اسم إعداد بمعنى جديد. أنشئ علمًا جديدًا، وأزل القديم بعد انتهاء الحاجة إليه.

اجعل التنبيهات تنبيهات فعلًا. البريد الذي يقرؤه البشر "عندما يتذكرون" ليس مراقبة. اربط الأخطاء بنظام تنبيه (Sentry أو Nightwatch أو غيرهما) بقواعد واضحة تصل إلى من يستطيع التصرف.

ضع فرامل آلية. حدود المعدل (rate limits) وقواطع الدائرة (circuit breakers) وحدود الإنفاق في الخدمات الخارجية، كلها تجعل الخطأ ضررًا محدودًا لا كارثة. ومن الأمثلة: حدّ على عدد رسائل البريد أو الطلبات الخارجية في الدقيقة.

جهّز خطة للحوادث قبل وقوعها. اكتب من يقرّر، وما الخطوة الأولى (إيقاف المصدر قبل التشخيص)، وكيف يُختبر أي rollback قبل الحاجة إليه.

خلاصة

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

المصادر