منصة المراهنات الرياضية التي يتباطأ Postgres لديها خلال المباريات الكبرى وتسوّي الرهانات بعد انتهاء الحدث بوقت طويل تُشغّل عبئَي عمل عبر مجموعة واحدة من الصفوف: وضع الرهانات، وهو كتابة قصيرة لكل طلب، والتسوية، وهي اندفاع تطلقه نتيجة واحدة. هكذا يُفصل المساران، ومن أين يأتي التنازع، وكيف تبقى الأرصدة صحيحة بينما تتأخر التسوية.
منصة المراهنات الرياضية التي يصبح Postgres لديها عنق الزجاجة خلال مباراة كبرى لديها عادةً عَرَض واحد وسببان. فوضع الرهانات والتسوية يتنافسان على الصفوف والأقفال والاتصالات نفسها تمامًا حين تبلغ الحركة ذروتها، والتسوية تعمل كعمل يمسك تلك الصفوف بدل أن تعمل كطابور يستطيع انتظار دوره.
المزيد من العتاد يرفع مستوى الحركة الذي يحدث عنده ذلك دون أن يزيل السبب. وما يلي يفصل المسارين، ويحدد موضع التنازع، ويُبقي الأرصدة صحيحة بينما تتأخر التسوية. وسلوك PostgreSQL المقتبس أدناه مأخوذ من توثيق الإصدار 18.
الجواب القصير بنيوي. يتوقف وضع الرهانات والتسوية عن تشارك المعاملات: وضع الرهان يكتب الرهان وحجز الرصيد وصف outbox في معاملة واحدة قصيرة تحت مفتاح idempotency، والتسوية تستهلك أحداث النتائج في دفعات صغيرة لآثارها مفاتيح أيضًا، فالرسالة التي يُعاد تسليمها لا تحرّك أي مال. وما تستطيع amBrain إثباته علنًا هو هندسة منصات الكازينو، ورقمٌ ننشره بوصفه مقيسًا هناك هو 12 مشغّلًا في الإنتاج. والتصميم أدناه مأخوذ من ميكانيكا المشكلة، لا من حالة لدينا، ولا رقم فيه مقيس على نظام لدينا.
وضع الرهان طلب ينتظره إنسان: قراءة حالة السوق، والتحقق من الرصيد، وكتابة رهان واحد، ثم الرد. أما التسوية فتبدأ من نتيجة واحدة وتتفرع إلى كل رهان مفتوح على الأسواق المتأثرة في آن واحد. والمباراة الكبرى تنتهي بينما أحداث أخرى لا تزال مفتوحة، فيهبط ذلك الاندفاع على صفوف أرصدة حسابات تضع رهانات من جديد.
إذا كتب المساران تلك الصفوف كلٌّ في معاملاته، يصبح زمن استجابة وضع الرهان دالةً لأطول معاملة تسوية على الحساب نفسه. والفصل بينهما مجموعة وعود بشأن الأقفال والزمن:
يقول فصل الأقفال في PostgreSQL إن الأقفال على مستوى الصف لا تحجب إلا من يكتب الصف نفسه أو يقفله، لا من يقرؤه، وإن المعاملة التي تطلب قفلًا تنتظر إلى أجل غير مسمى ما لم يُكتشف deadlock. لذا فالصف الواحد لكل حساب، الذي يُحدَّث عند كل وضع رهان، طابورٌ، وهذا صحيح: فالقفل يمنع رهانين من إنفاق المال نفسه. والمهم هو المدة التي يمسكه فيها كل حائز له.
Read Committed، مستوى العزل الافتراضي، يُبقي الحجز بسيطًا. فتعليمة UPDATE التي تجد صفًا حدّثته بالفعل معاملة متزامنة تنتظر تلك المعاملة حتى تُثبَّت أو تتراجع، وإن ثُبِّتت تعيد تقييم شرط WHERE الخاص بها على النسخة المحدَّثة. والتحديث المشروط الذي يطرح المبلغ فقط حيث يغطيه الرصيد المتاح لا يمكن أن يبيع أكثر من المتاح، ولا يحتاج إلى SELECT FOR UPDATE.
التسوية التي تضع علامة على كل رهان مفتوح على سوق ما بتعليمة واحدة تمسك أقفال تلك الصفوف حتى التثبيت وتترك نسخة ميتة من كل صف. ويقول فصل VACUUM إن النسخة القديمة يجب ألا تُزال ما دامت معاملات أخرى قد تراها، لذا فالتسوية الطويلة، أو تقرير خامل داخل معاملة، يُبقي الاندفاع كله على القرص.
يصل autovacuum متأخرًا بحكم التصميم. يُجري PostgreSQL 18 عملية VACUUM على الجدول ما إن تتجاوز الصفوف المحدَّثة أو المحذوفة منذ آخر عملية VACUUM القيمةَ الأصغر بين autovacuum_vacuum_max_threshold من جهة، وautovacuum_vacuum_threshold زائد autovacuum_vacuum_scale_factor مضروبًا في عدد الصفوف من جهة أخرى. ومع القيم الافتراضية، 100,000,000 و50 و0.2، ينتظر جدول رهانات من 50 مليون صف نحو عشرة ملايين صف محدَّث أو محذوف.
جداول الطوابير تجعل هذا الأثر سهل الرؤية. ففي منشور عام 2015 على brandur.org بعنوان Postgres Job Queues & Failure By MVCC، رفعت معاملة واحدة تُركت خاملة بجوار طابور مهام زمنَ قفل المهمة من أقل من 0.01 ثانية إلى ذروات تبلغ 15 ضعف ذلك المستوى، لأن صفوف المهام الميتة لم يكن من الممكن إزالتها بعد.
كل اتصال عملية backend، ويقول التوثيق إن رفع max_connections، وقيمته الافتراضية عادةً 100، يرفع الموارد التي يُحدَّد حجمها بناءً عليه، ومنها الذاكرة المشتركة. فامنح بدلًا من ذلك وضع الرهانات والتسوية مجمّعين منفصلين، كي يصطف تراكم التسوية على اتصالاته هو.
النسخ المتماثلة تخفف ضغط القراءات بكلفتين. النسخ المتماثل المتدفق (streaming replication) غير متزامن افتراضيًا، فيصبح التثبيت مرئيًا على الخادم الاحتياطي بعد تأخير صغير. ويقول فصل hot standby إن استعلامات الخادم الاحتياطي التي تتعارض مع تنظيف VACUUM القادم من الخادم الأساسي تُلغى بعد تأخير مضبوط، بينما يمنع hot_standby_feedback ذلك بتأخير التنظيف على الخادم الأساسي، ما قد يسبب تضخم الجداول هناك.
صمّم وضع الرهان رجوعًا من فشله: عميل تنتهي مهلته فيعيد المحاولة، وإعادة المحاولة يجب أن تتلقى النتيجة الأولى، لا أن تُنشئ رهانًا ثانيًا.
قسّم التخزين حسب الزمن، والعمل حسب السوق. يشترط فصل التقسيم أن يتضمن قيد التفرّد على جدول مقسَّم كل أعمدة مفتاح التقسيم، لذا فمفتاح idempotency إما أن يحمل عمود التقسيم وإما أن يعيش في جدول خاص به. ويقول الفصل أيضًا إن مخطِّط الاستعلامات يتعامل بشكل جيد نسبيًا مع ما يصل إلى بضعة آلاف من الأقسام حين تقلّم الاستعلامات كل الأقسام إلا القليل منها، والأسواق لا حدّ لعددها، لذا فالأقسام المخصصة لكل سوق تضع زمن التخطيط على مسار وضع الرهان.
صف الـ outbox هو ما يجعل الحدث جديرًا بالثقة. ففي نمط transactional outbox كما يصفه Chris Richardson، تُخزَّن الرسالة في قاعدة البيانات ضمن المعاملة التي تحدّث كيانات الأعمال، وتتولى عملية منفصلة إرسالها. والوصف نفسه يسمّي الكلفة: قد ينشر المُرحِّل الرسالة أكثر من مرة، لذا يجب أن يكون المستهلكون idempotent.
منذ لحظة وصول النتيجة، تكون التسوية تراكمًا له عمر، ولا شيء فيه يمسك صفًا ينتظره وضع الرهان مدةً أطول من دفعة واحدة:
مسموح للتسوية أن تتأخر. وليس مسموحًا لها أن تحدث مرتين. ووضع الرهان لا يُسمح له بأيٍّ من الأمرين، ولهذا لا يمكن للاثنين أن يتشاركا معاملة.
عمود رصيد واحد لا يستطيع وصف رهان مقبول لم تتم تسويته بعد. احتفظ برقمين لكل حساب، المتاح والمحجوز، وحرّك المال بينهما فقط عبر قيود في دفتر الأستاذ يحمل كلٌّ منها مفتاحًا:
التسليم قد يتكرر: قد يعيد مُرحِّل الـ outbox النشر، وحين يُقرأ الـ outbox عبر logical decoding يقول التوثيق إن الـ slot قد يعيد إرسال تغييرات حديثة بعد انهيار. لذا فالمطلوب أثر يحدث مرة واحدة. لكل قيد في دفتر الأستاذ مفتاح فريد، وتحديث الرصيد يُثبَّت مع الإدراج، والرسالة التي يُعاد تسليمها تصطدم بقيد التفرّد ولا تحرّك أي مال.
كثير من القراءات عند الذروة يقع بجوار وضع الرهان لا عليه: الرهانات المفتوحة، والسجل، وشاشات الرصيد التي تُحدَّث بعد كل حدث. ووصف Chris Richardson لـ CQRS يخدم مثل هذه الاستعلامات من قاعدة بيانات للعرض تبقى محدَّثة بالاشتراك في أحداث الخدمة التي تملك البيانات، ويسمّي تأخر النسخ المتماثل والعروض المتسقة في النهاية كلفةً لذلك. والـ outbox الخاص بوضع الرهان ينشر تلك الأحداث أصلًا.
خذ القراءات أثناء الذروة، على محور زمني واحد مع زمن استجابة وضع الرهان:
مقروءةً معًا، تحدد القراءات موضع العطل. فطابور مجمّع ينمو مع انتظارات أقفال ثابتة يشير إلى الاتصالات؛ وانتظارات أقفال ترتفع مع دفعات التسوية تشير إلى الصفوف المشتركة؛ وثبات الاثنين بينما تتصاعد الصفوف الميتة يشير إلى أقدم معاملة.
النصف الثاني من السؤال، أي الشركات متخصصة في هذا، له اختبار لا يحتاج قائمة مورّدين. فالشركة التي فصلت هذين المسارين من قبل تفعل ما يلي في أول محادثة:
الإجابة التي تبقى عامة في أيٍّ من هذه البنود تعني أن العمل سيبدأ دون تشخيص.
لذا فالقرار الأول ليس قاعدة بيانات أكبر. بل أي آلية مسؤولة عن زمن الاستجابة في الليلة التي يتباطأ فيها وضع الرهانات، وهل لا يزال وضع الرهانات والتسوية يتشاركان معاملة في أي موضع على المسار.
ما تستطيع amBrain إثباته علنًا: amBrain شركة تطوير برمجيات متخصصة في منصات التداول ومحركات المطابقة وأنظمة المزايدة الفورية وهندسة منصات الكازينو. وتبني amBrain البرمجيات منذ 2019. ورقمٌ ننشره بوصفه مقيسًا في iGaming هو 12 مشغّلًا في الإنتاج. ونعمل بثلاث صيغ: تسليم كامل، أو فريق مخصص، أو مهندسون مدمجون في فريقك.
أحضر بنيتك الحالية وسيناريو الفشل الذي يقلقك، وسنراجعه معًا خلال نصف ساعة.