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

تعمل خوارزمية Paxos عبر جولات متكررة من تبادل الرسائل حتى تصل مجموعة من العقد إلى اتفاق نهائي حول قيمة مقترحة. وما يجعلها مثيرة للاهتمام هو أن كل عقدة يمكن أن تؤدي أكثر من دور في الوقت نفسه، مثل Proposer وAcceptor وLearner.
في هذا المقال، سنعيد صياغة المفهوم تقنياً بشكل عملي، مع التركيز على كيفية تنفيذ الخوارزمية باستخدام الدوال النقية Pure Functions، وكيف يمكن فصل منطق الحالة عن التأثيرات الجانبية Side Effects للحصول على كود أوضح وأسهل في الاختبار.
قبل البدء: ما الذي سنغطيه وما الذي لن نغطيه؟
هذا المقال يركّز على جانب التنفيذ البرمجي أكثر من الإثبات الرياضي. لذلك، لن ندخل في تفاصيل البرهان الرسمي الذي يفسر لماذا تعمل خوارزمية Paxos نظرياً، بل سنفترض صحة النموذج وننتقل مباشرة إلى كيفية بنائه برمجياً.
كذلك، التنفيذ المطروح هنا يعبّر عن نسخة أساسية ومبسطة من Basic Paxos لجولة واحدة، من دون التوسع في جوانب متقدمة مثل:
- التعامل مع فشل الآلات
Machine Failure. - انتخاب القائد
Leader Election. - القيود الصارمة
Invariantsالخاصة بالأنظمة الإنتاجية. - التكرار متعدد الجولات ضمن آلة حالات مكررة
Replicated State Machine.
الهدف هنا هو فهم بنية التنفيذ الدالي للخوارزمية، وليس بناء نظام إنتاجي كامل.
نظرة سريعة على خوارزمية Paxos
تتكون خوارزمية Paxos من ثلاثة أدوار رئيسية:
Proposer: الجهة التي تقترح قيمة.Acceptor: الجهة التي تستقبل المقترحات وتقرر قبولها أو تجاهلها وفق قواعد محددة.Learner: الجهة التي تستنتج ما إذا تم الوصول إلى توافق نهائي.
وتنقسم الخوارزمية إلى مرحلتين أساسيتين:
- مرحلة التحضير
Prepare Phase. - مرحلة القبول
Accept Phase.

مرحلة التحضير Prepare Phase
في هذه المرحلة، يختار كل Proposer رقماً فريداً للمقترح Proposal Number، ثم يرسل طلب تحضير إلى مجموعة من Acceptors. لا يُشترط أن تستلم كل العقد الرسالة، بل يكفي الحصول على أغلبية Quorum.
عندما يستقبل Acceptor رسالة التحضير، فإنه يقارن رقم المقترح الوارد مع أعلى رقم شاهده سابقاً:
- إذا كان الرقم الجديد أعلى، فإنه يرسل وعداً
Promiseبأنه لن يقبل مقترحاً أقدم. - إذا كان قد قبل سابقاً قيمة أخرى، فإنه يرفق بهذه الاستجابة رقم المقترح المقبول وقيمته.
- إذا كان الرقم الوارد أقل من رقم سبق التعهد له، فإنه يتجاهل الرسالة.

مرحلة القبول Accept Phase
إذا تلقى Proposer وعوداً من أغلبية كافية، فإنه يفحص الردود:
- إذا تضمنت إحدى رسائل
Promiseقيمة مقبولة سابقاً، فعليه اعتماد تلك القيمة وإرسالها في طلب القبول. - إذا لم تصله استجابات كافية، فعادةً ما يرفع رقم المقترح ويحاول من جديد.
- إذا حصل على أغلبية ومرّت عملية القبول بنجاح، تُعد القيمة مرشحة للوصول إلى التوافق.
أما Acceptor، فإذا استلم طلب قبول Accept Request يطابق الرقم الذي وعد به، فإنه يرد بتأكيد قبول. وإذا كان الطلب يحمل رقماً أقدم من التعهد الحالي، فإنه يتجاهله.
وفي جهة Learner، يبدأ التوافق بالظهور عندما تصله تأكيدات كافية من الأغلبية حول قيمة بعينها.

لماذا يُعد تنفيذ Paxos عملياً أكثر تعقيداً من شرحه نظرياً؟
على الورق، تبدو الخوارزمية مباشرة نسبياً، لكن عند تحويلها إلى كود، تظهر تحديات كثيرة، من أهمها إدارة الحالة الداخلية لكل عقدة، والتعامل مع الرسائل الواردة بالترتيب أو بدونه، وفصل المنطق الصافي عن عمليات الإدخال والإخراج.
كثير من التطبيقات المعروفة بُنيت وفق نمط البرمجة كائنية التوجه OOP أو باستخدام أنظمة الممثلين Actor Systems. لكن تنفيذ الخوارزمية بأسلوب دالي Functional Programming يقدم مزايا مهمة، أبرزها:
- سهولة الاختبار.
- تحسين قابلية التنبؤ بسلوك النظام.
- تقليل الاعتماد على الحالة المتغيرة
Mutable State. - فصل منطق البروتوكول عن التأثيرات الخارجية.
تحدي التصميم: كيف نمثل الأدوار في نموذج دالي؟
بناء نموذج المجال Domain Model
أول خطوة في التنفيذ هي تصميم النماذج التي تمثل الأدوار الثلاثة. وبدلاً من جعل الكائن يحتوي على البيانات والمنطق معاً كما في OOP، يمكننا في النمط الدالي فصل البيانات عن العمليات.
نموذج Proposer
يحتاج Proposer إلى العناصر التالية:
- القيمة المراد اقتراحها.
- رقم المقترح
Proposal Number. - حجم الأغلبية المطلوبة
Quorum Size.
ويجب أن يكون رقم المقترح فريداً ومتزايداً باستمرار. من الأساليب الشائعة لتوليده دمج رقم تسلسلي مع معرف آلة Machine ID لضمان التفرد.
نموذج Acceptor
يحتفظ Acceptor عادةً بحالتين مهمتين:
- أعلى رقم تعهد به
Promise Proposal Number. - القيمة المقبولة سابقاً مع رقمها إن وُجدت.
وبما أن هاتين القيمتين قد لا تكونان موجودتين في البداية، فمن المناسب تمثيلهما باستخدام Option أو نوع اختياري مكافئ.
نموذج Learner
يحتاج Learner إلى تتبع جميع الردود المقبولة التي تصله، حتى يعرف ما إذا كانت قيمة معينة تجاوزت حد الأغلبية. لذلك، يمكنه الاحتفاظ بـ:
- حجم
Quorum. - خريطة عدّ
Key-Value Mapتربط كل قيمة بعدد مرات قبولها. - القيمة النهائية التي تم اختيارها عند تحقق التوافق.
فصل العمليات عن النماذج: لماذا يُعد هذا القرار مهماً؟
في التصميم الكائني المعتاد، توضع الدوال داخل الكائن نفسه، وتقوم بتعديل حالته الداخلية مباشرة. أما في التصميم الدالي، فمن الأفضل فصل العمليات في وحدات مستقلة، مثل:
ProposerOpsAcceptorOpsLearnerOps
هذا الأسلوب يحقق فصلاً واضحاً بين:
- البيانات
Data. - سلوك التحديث
Operations. - التأثيرات الخارجية
Effects.
والنتيجة هي تصميم أكثر نظافة وأسهل في التطوير والصيانة.
الرسائل في خوارزمية Paxos
بما أن الخوارزمية تعتمد على التراسل بين العقد، فمن الضروري تعريف أنواع الرسائل بدقة. في النسخة الأساسية، يمكننا تمثيل أربع رسائل رئيسية:
PrepareMessagePromiseMessageAcceptMessageAcceptedMessage
وعند استخدام لغة مثل Scala، يمكن جمع هذه الرسائل ضمن sealed trait لتوفير تمثيل آمن ومغلق للأنواع. كذلك، إذا كان Acceptor قد لا يملك قيمة مقبولة سلفاً عند إرساله وعداً، فمن الملائم استخدام Option لاحتواء القيمة المقبولة داخل رسالة Promise.
تمثيل التأثيرات الجانبية عبر نوع Action
من أكبر مزايا استخدام الدوال النقية في خوارزميات التوافق أننا نستطيع حصر عمليات IO في طبقة منفصلة. وبدلاً من تنفيذ الإرسال الفعلي للرسائل داخل منطق الخوارزمية مباشرة، يمكننا تمثيل هذا السلوك على شكل قيمة تصف ما يجب فعله لاحقاً.
هنا يأتي دور نوع Action. هذا النوع لا ينفذ الأثر الجانبي بنفسه، بل يصفه فقط. على سبيل المثال، يمكن أن يتضمن أوامر مثل:
Broadcast: لبث رسالة إلى مجموعة من العقد.Send: لإرسال رسالة إلى عقدة محددة.
بهذا الأسلوب، يصبح منطق Paxos نفسه نقياً، بينما تُنفّذ التأثيرات الفعلية في طبقة خارجية مخصصة لذلك.
كيف ندير الحالة دون استخدام Mutation؟
السؤال الجوهري هنا هو: إذا كانت الخوارزمية تعتمد على تحديث الحالة باستمرار، فكيف ننفذها دون تعديل مباشر على الكائنات؟
الإجابة العملية هي استخدام State Monad. هذا النمط يسمح بتمرير الحالة السابقة إلى دالة تُرجع حالة جديدة مع نتيجة مرافقة. أي أن الدالة لا تعدّل الحالة القديمة، بل تنتج نسخة محدثة منها.
على سبيل المثال، يمكن تمثيل عملية تخص Proposer بالنمط التالي:
State[Proposer, Action]
وهذا يعادل منطقياً دالة بالشكل:
Proposer => (Proposer, Action)
بهذا الشكل، نحصل على إدارة حالة نقية وقابلة للتتبع، وهو ما يجعل الاختبار أسهل بكثير، خصوصاً في البيئات المتزامنة أو الموزعة.
تصميم وحدات العمليات Ops
لتبسيط التنفيذ، يمكن إنشاء ثلاث وحدات منفصلة:
ProposerOpsAcceptorOpsLearnerOps
كل وحدة تحتوي على الدوال الخاصة بمعالجة الرسائل وتحديث الحالة المرتبطة بالدور المعني. هذا التصميم مستوحى جزئياً من طريقة بناء الأنظمة المعتمدة على Actors، لكنه يحافظ على النقاء الدالي بدلاً من إخفاء الحالة داخل كائنات قابلة للتغيير.
فعلى سبيل المثال، عندما يستقبل Acceptor رسالة PrepareMessage، تقوم الدالة بمقارنة proposalId الوارد مع أعلى proposalId سبق التعامل معه:
- إذا كان الرقم أعلى، تُعاد حالة جديدة مع
PromiseMessage. - إذا كان الرقم أقل، تُعاد الحالة نفسها مع تجاهل منطقي للرسالة أو بدون إجراء.
هذا النمط يجعل كل دالة واضحة: مدخلات، حالة قديمة، حالة جديدة، ثم وصف للفعل المطلوب.
مثال مفاهيمي على منطق المعالجة النقي
الفكرة الأساسية في التنفيذ الدالي هي أن كل رسالة تمر عبر دالة خالصة Pure Function، فتنتج نتيجتين:
- حالة جديدة محدثة.
- إجراء مطلوب تنفيذه لاحقاً.
وهذا يمنحنا مزايا مهمة:
- المنطق لا يعتمد على متغيرات مخفية.
- اختبار كل حالة يصبح مباشراً.
- إعادة تمثيل السيناريوهات المعقدة أسهل.
- فصل الخوارزمية عن وسيلة النقل الفعلية للرسائل.
بمعنى آخر، الخوارزمية لا تهتم إن كان الإرسال سيتم عبر الشبكة، أو عبر محاكاة محلية، أو ضمن اختبار وحدات Unit Test. كل ما تعرفه هو أنها تنتج Action يصف المطلوب.
لماذا هذا الأسلوب مناسب لاختبار خوارزميات التوافق؟
اختبار خوارزمية مثل Paxos قد يكون معقداً إذا كان الكود مليئاً بالتأثيرات الجانبية والتغييرات الخفية في الحالة. لكن عندما يصبح كل شيء ممثلاً عبر دوال نقية، يمكن بسهولة:
- إدخال حالة مبدئية معروفة.
- تمرير رسالة محددة.
- مقارنة الحالة الناتجة بالقيمة المتوقعة.
- التحقق من
Actionالناتج دون تنفيذ أي شبكة حقيقية.
وهذا يرفع جودة الكود ويقلل صعوبة تتبع الأخطاء، خصوصاً في البرمجيات الموزعة حيث تكون المشاكل متقطعة وصعبة الإعادة.
ملاحظات تصميمية مهمة عند تنفيذ Paxos دالياً
- ابدأ بتعريف جميع أنواع الرسائل قبل كتابة منطق التنفيذ.
- احرص على أن تكون النماذج
Modelsبسيطة وصريحة. - افصل بين تحديث الحالة وبين تنفيذ
IO. - اجعل كل دالة تستقبل كل ما تحتاجه بوضوح، من دون الاعتماد على سياق خفي.
- استخدم أنواعاً مثل
OptionوEitherعند الحاجة للتعبير الدقيق عن الاحتمالات.
هل هذا التنفيذ يكفي للإنتاج؟
ليس بالضرورة. فالنسخة الأساسية من Paxos ممتازة للفهم وبناء أساس نظري وعملي متين، لكنها لا تمثل كل ما يحتاجه نظام إنتاجي حقيقي. عند الانتقال إلى بيئة تشغيل فعلية، ستحتاج غالباً إلى إضافة عناصر أخرى مثل:
- إدارة الأعطال.
- إعادة المحاولة
Retry. - القيادة أو التنسيق المركزي الجزئي.
- التخزين المستمر
Persistent Storage. - المراقبة والتتبع
Observability.
ومع ذلك، فإن بناء الخوارزمية أولاً بشكل نقي يمنحك أساساً قوياً يمكن تطويره تدريجياً بثقة أكبر.
الخلاصة التقنية
تنفيذ خوارزمية Paxos باستخدام الدوال النقية ليس مجرد تمرين أكاديمي، بل أسلوب عملي لإنتاج منطق أوضح، قابل للاختبار، وأسهل في الصيانة. جوهر الفكرة هو فصل منطق التوافق عن التأثيرات الجانبية، وتمثيل كل انتقال في الحالة بشكل صريح يمكن تتبعه وفهمه. ورغم أن الخوارزمية بطبيعتها تعتمد على الحالة والتراسل، فإن المقاربة الدالية تثبت أن التعقيد يمكن احتواؤه بذكاء عند تصميم النماذج والرسائل وطبقة Action بعناية.