Software Architecture

خوارزميات الإجماع في الممارسة العملية: تطبيق Raft للخدمات المصغرة ذات الحالة

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

لماذا Raft للخدمات ذات الحالة؟

يجب على الخدمات المصغرة ذات الحالة الحفاظ على رؤية متسقة لبياناتها عبر جميع النسخ. إذا وفرت عقدتان قراءات متضاربة، فإن سلامة النظام تتعرض للخطر. تقسم Raft مشكلة الإجماع إلى ثلاث مشاكل فرعية: انتخاب القائد، تكرار السجل، والسلامة. يجعل هذا النهج المعياري الأمر أسهل بكثير في التصحيح والاختبار مقارنةً بالخوارزميات الأخرى. بالنسبة لمعمارية الخدمات المصغرة، هذا التنبؤ بالسلوك لا يقدر بثمن، مما يسمح للمطورين بالاستدلال حول أوضاع الفشل بثقة أكبر.

المكونات الأساسية: العقد، الأدوار، والفترات (Terms)

في عنقود Raft، يمكن لكل خادم أن يوجد في إحدى الحالات الثلاث: قائد (Leader)، متابع (Follower)، أو مرشح (Candidate). تبدأ جميع الخوادم كمتابعين. إذا لم يسمع متابع من القائد خلال مهلة انتخاب محددة، ينتقل إلى حالة المرشح ويبدأ عملية انتخاب. ينتخب العنقود قائداً بناءً على من لديه السجل الأكثر حداثة. بشكل حاسم، تستخدم Raft الفترات (Terms) — وهي ساعات منطقية تزداد بشكل أحادي الاتجاه — لمساعدة النظام على رفض الطلبات القديمة من القادة أو المرشحين السابقين.

تطبيق انتخاب القائد

يُطلق انتخاب القائد بواسطة مؤقت. لضمان البقاء حياً (Liveness)، يتم عشوائية مهلات الانتخاب لتقليل احتمالية انقسام الأصوات. أدناه مثال مبسط على كود زائف (Pseudocode) لانتقال متابع إلى حالة مرشح:


class RaftNode {
    state = "follower"
    currentTerm = 0
    votedFor = null
    electionTimer = new Timer()

    function startElection() {
        this.state = "candidate"
        this.currentTerm += 1
        this.votedFor = this.id
        var votesReceived = 1

        for (var peer in peers) {
            if (peer.id != this.id) {
                send(peer, {
                    type: "RequestVote",
                    term: this.currentTerm,
                    candidateId: this.id,
                    lastLogIndex: this.log.length - 1,
                    lastLogTerm: this.log[this.log.length - 1].term
                })
            }
        }
    }

    function onMessage(msg) {
        if (msg.term > this.currentTerm) {
            this.currentTerm = msg.term
            this.state = "follower"
            this.votedFor = null
        }

        if (msg.type == "RequestVote") {
            if (this.votedFor == null || this.votedFor == msg.candidateId) {
                if (isCandidateUpToDate(msg)) {
                    this.votedFor = msg.candidateId
                    votesReceived += 1
                    respond(msg.candidateId, {
                        type: "VoteResponse",
                        term: this.currentTerm,
                        granted: true
                    })
                }
            }
        }

        if (votesReceived > (clusterSize / 2) + 1) {
            this.state = "leader"
            startHeartbeat()
        }
    }
}

تكرار السجل والسلامة

بمجرد انتخاب القائد، يبدأ في تكرار إدخالات السجل. يعين القائد لكل إدخال فترة (Term) وفهرساً (Index). يضيف الإدخالات إلى سجله الخاص ويرسل استدعاءات RPC من نوع AppendEntries إلى جميع المتابعين. يُعتبر إدخال السجل مُلتزماً (committed) بمجرد تكراره إلى أغلبية العنقود. ثم يتضمن القائد أعلى فهرس مُلتزم في رسائل AppendEntries اللاحقة لإبلاغ المتابعين بالإدخالات الآمنة للتطبيق على آلة الحالة الخاصة بهم.

اعتبارات عملية للإنتاج

  • التصويت المسبق (Pre-Votes): لتجنب الاضطراب أثناء انقسامات الشبكة، قم بتنفيذ رسائل التصويت المسبق قبل بدء انتخاب حقيقي. يضمن هذا أن المرشح سيكسب على الأرجح قبل زيادة الفترة وإزعاج القائد الحالي.
  • التقاطات الحالة (Snapshotting): مع نمو السجل، يصبح إرسال التاريخ الكامل إلى العقد الجديدة غير فعال. قم بتنفيذ لقطات دورية لآلة الحالة لضغط السجل وتسريع المزامنة الأولية.
  • معالجة مهلات انتهاء العميل: إذا فشل طلب عميل لأنه أُرسل إلى غير القائد، يجب على العميل إعادة المحاولة. قم بتنفيذ تراجع أسي (Exponential Backoff) لمنع إغراق العنقود أثناء عمليات الفشل والتعافي.

الاختبار والتصحيح

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

خاتمة

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

Share: