در دنیای سیستمهای توزیعشده، دستیابی به یکسانسازی بین گرهها بدون وجود یک نقطه شکست مرکزی، یکی از چالشبرانگیزترین مسائل معماری است. در حالی که بسیاری از برنامههای مدرن برای سادهسازی مقیاسپذیری، بهطور پیشفرض از طراحیهای بدون وضعیت استفاده میکنند، میکروسرویسهای دارای وضعیت—مانند تختههای امتیاز، انبارهای نشست یا قفلهای توزیعشده—به تضمینهای یکسانسازی سختگیرانه نیاز دارند. در اینجا Raft، یک الگوریتم اجماع طراحیشده برای درک آسان، به عنوان یک جایگزین عملی برای پروتکل پیچیدهتر Paxos ارائه میشود. در این مقاله، ما نحوه پیادهسازی Raft را بهطور خاص برای میکروسرویسهای دارای وضعیت بررسی خواهیم کرد، با تمرکز بر انتخاب رهبر، تکثیر لاگ و تضمینهای ایمنی.
چرا Raft برای سرویسهای دارای وضعیت؟
میکروسرویسهای دارای وضعیت باید دیدگاهی یکسان از دادههای خود را در تمام کپیها (replicas) حفظ کنند. اگر دو گره خوانشهای متناقض ارائه دهند، یکپارچگی سیستم به خطر میافتد. Raft مسئله اجماع را به سه زیرمسئله تقسیم میکند: انتخاب رهبر، تکثیر لاگ و ایمنی. این رویکرد ماژولار آن را در مقایسه با الگوریتمهای دیگر بهطور قابلتوجهی آسانتر برای عیبیابی و تست میکند. برای یک معماری میکروسرویس، این پیشبینیپذیری بیارزش است و به توسعهدهندگان اجازه میدهد با اطمینان بیشتر درباره حالتهای شکست استدلال کنند.
اجزای اصلی: گرهها، نقشها و دورهها (Terms)
در یک خوشه Raft، هر سرور میتواند در یکی از سه حالت وجود داشته باشد: رهبر (Leader)، پیرو (Follower) یا نامزد (Candidate). تمام سرورها به عنوان پیرو شروع میکنند. اگر یک پیرو در یک بازه زمانی مشخص (timeout انتخاب) از رهبر خبری نشود، به حالت نامزد تغییر وضعیت میدهد و یک انتخابات را آغاز میکند. خوشه رهبر را بر اساس کسی که دارای جدیدترین لاگ است انتخاب میکند. نکته کلیدی این است که Raft از دورهها (Terms)—ساعتهای منطقی که بهطور یکنواخت افزایش مییابند—استفاده میکند تا به رد درخواستهای قدیمی از رهبران یا نامزدهای قبلی کمک کند.
پیادهسازی انتخاب رهبر
انتخاب رهبر توسط یک تایمر فعال میشود. برای اطمینان از زنده بودن (liveness)، timeoutهای انتخاب بهصورت تصادفی تنظیم میشوند تا شانس رأیهای تقسیمشده کاهش یابد. در زیر یک مثال سادهشده از شبهکد برای تغییر وضعیت یک پیرو به نامزد آورده شده است:
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): با رشد لاگ، ارسال کل تاریخچه به گرههای جدید کارآمد نیست. اسنپشاتهای دورهای از ماشین وضعیت را پیادهسازی کنید تا لاگ فشرده شود و همگامسازی اولیه سریعتر انجام شود.
- مدیریت Timeout کلاینت: اگر درخواست یک کلاینت به دلیل ارسال به یک گره غیررهبر شکست بخورد، کلاینت باید دوباره تلاش کند. برای جلوگیری از فشار بیش از حد به خوشه در طول جابهجاییهای شکست (failovers)، از backoff نمایی استفاده کنید.
تست و عیبیابی
الگوریتمهای اجماع به دلیل ماهیت غیرقطعی خود، بهطور معروفی سخت برای تست هستند. ما استفاده از چارچوب Jepsen را برای شبیهسازی تقسیمات شبکه، شکست گرهها و تأخیر پیامها توصیه میکنیم. علاوه بر این، از چککنندههای خطیپذیری (Linearizability) استفاده کنید تا تأیید کنید که ماشین وضعیت شما تحت تمام شرایط آزمایششده مانند یک گره منطقی واحد عمل میکند. ابزارسازی کد شما با لاگهای جزئی از دورهها (Terms)، شاخصها (Indices) و تغییرات وضعیت (State Transitions) برای عیبیابی باگهای ظریف حیاتی است.
نتیجهگیری
پیادهسازی Raft در میکروسرویسهای دارای وضعیت، یک بنیاد محکم برای ساخت سیستمهای توزیعشده با دسترسپذیری بالا و یکسانسازی دقیق فراهم میکند. اگرچه پیچیدگی آن ناچیز نیست، اما شفافیت ارائهشده توسط طراحی Raft آن را به یک انتخاب عملی برای تیمهای مهندسی تبدیل میکند. با پیادهسازی دقیق انتخاب رهبر، تکثیر لاگ و بررسیهای ایمنی، میتوانید اطمینان حاصل کنید که میکروسرویسهای شما حتی در مواجهه با شکستهای جزئی، یکسان باقی میمانند. با مقیاسپذیری سیستم شما، در نظر بگیرید که از پیشرأی و اسنپشات استفاده کنید تا عملکرد و قابلیت اطمینان حفظ شود.