ریپلیکیشن دیتابیس: یک کپی زنده از داده ها، همیشه آماده
فرض کن سرور دیتابیس اصلی یک پروژه، به هر دلیلی — خرابی سختافزار، آپدیت نادرست، قطعی برق دیتاسنتر — از دسترس خارج بشه. اگه فقط یک نسخه از دادهات جایی وجود داشته باشه، این یعنی کل سرویس تا وقتی اون سرور برنگرده، از کار میافته. ریپلیکیشن دقیقاً برای جلوگیری از همین سناریو ساخته شده: بهجای نگهداشتن داده روی یک سرور تنها، یک یا چند کپی زنده و همیشه بهروز از همون داده رو روی سرورهای دیگه نگه میداری.
مهمه که این رو از شاردینگ جدا کنی. توی شاردینگ، هر سرور فقط بخشی از داده رو داره. توی ریپلیکیشن، هر سرور (یا حداقل هر replica) یک کپی کامل از کل داده رو نگه میداره. این دو تکنیک هدف متفاوتی دارن و معمولاً با هم ترکیب میشن، نه جایگزین همدیگه.
چرا ریپلیکیشن میخوایم
دو دلیل اصلی برای ریپلیکیشن وجود داره، و هر کدوم مسئلهی متفاوتی رو حل میکنن.
اول، دسترسپذیری بالا (high availability). اگه سرور اصلی (که بهش primary یا master میگن) از کار بیفته، یکی از replica ها میتونه جاش رو بگیره و نقش primary جدید رو بازی کنه، بدون اینکه سرویس کامل قطع بشه. به این فرآیند failover میگن.
دوم، توزیع بار خواندن. توی خیلی از اپلیکیشنها، تعداد خواندن (SELECT) خیلی بیشتر از نوشتن (INSERT/UPDATE) است. اگه همهی این خواندنها روی همون یک سرور primary انجام بشه، اون سرور زیر فشار میمونه. با ریپلیکیشن میتونی خواندنها رو بین چند replica پخش کنی و فقط نوشتنها رو روی primary نگه داری — الگویی که بهش read/write splitting میگن.
چطور داده از primary به replica میرسه
مکانیزم رایج این است که primary هر تغییری که روی دادهاش اعمال میشه رو توی یک لاگ ترتیبی ثبت میکنه — توی PostgreSQL بهش WAL (Write-Ahead Log) میگن، توی MySQL بهش binlog. هر replica این لاگ رو از primary میخونه و همون تغییرات رو دقیقاً به همون ترتیب روی نسخهی خودش اعمال میکنه. یعنی replica عملاً داره یک تاریخچهی کامل از هر اتفاقی که روی primary افتاده رو دنبال میکنه و بازش تولید میکنه.
sync در مقابل async: معاملهای که باید آگاهانه انتخابش کنی
اینجا یکی از مهمترین تصمیمهای طراحی توی ریپلیکیشن پیش میاد: وقتی یک نوشتن روی primary اتفاق میافته، آیا باید صبر کنه تا replica هم همون نوشتن رو تأیید کنه، یا میتونه بدون منتظر موندن به کلاینت جواب «موفق» بده؟
توی replication همزمان (synchronous)، primary صبر میکنه تا حداقل یک replica تغییر رو دریافت و تأیید کنه، بعد به کلاینت میگه عملیات موفق بوده. این تضمین میده که هیچوقت دادهای که کلاینت فکر میکنه ذخیره شده، گم نشه — حتی اگه دقیقاً یک لحظه بعد primary از کار بیفته. هزینهاش اینجاست که هر نوشتن باید منتظر شبکه و replica هم بمونه، پس تأخیر (latency) بالاتر میره.
توی replication ناهمزمان (asynchronous)، primary بلافاصله بعد از نوشتن محلی به کلاینت جواب موفق میده، و تغییر رو در پسزمینه به replica ها میفرسته. سریعتره، ولی یک ریسک واقعی داره: اگه primary دقیقاً بین «جوابدادن به کلاینت» و «رسوندن تغییر به replica» از کار بیفته، اون تغییر ممکنه هیچوقت به replica نرسه و گم بشه. این حالت رو replication lag میگن — فاصلهی زمانی بین اتفاقافتادن یک تغییر روی primary و رسیدنش به replica.
اکثر سیستمهای production، بهخاطر تعادل بین سرعت و امنیت داده، از async استفاده میکنن، و ریسکش رو با مانیتورینگ لگ ریپلیکیشن و طراحی درست مدیریت میکنن، نه اینکه کامل حذفش کنن.
مشکلی که replication lag واقعاً ایجاد میکنه
فرض کن یک کاربر همین الان یک پست جدید توی اپلیکیشنت ثبت کرده. درخواست نوشتن روی primary انجام شده و جواب موفق برگشته. حالا همون کاربر بلافاصله صفحه رو رفرش میکنه، و این دفعه درخواست خواندن به یک replica هدایت میشه که هنوز اون تغییر رو دریافت نکرده. نتیجه؟ کاربر پست خودش رو نمیبینه و فکر میکنه ثبت نشده. این پدیده رو read-after-write inconsistency میگن و یکی از رایجترین سردرگمیهایییه که موقع استفاده از read replica ها پیش میاد.
راهحل رایجش اینه که برای عملیاتهای حساس به این مشکل (مثل نمایش بلافاصلهی دادهای که کاربر خودش همین الان ثبت کرده)، خواندن رو موقتاً از primary انجام بدی، نه از replica؛ یا اینکه یک مکانیزم «sticky session» بذاری که هر کاربر برای مدتی بعد از نوشتن، به همون primary وصل بمونه.
failover: چه کسی primary جدید میشه
وقتی primary از کار میافته، سیستم باید تصمیم بگیره کدوم replica جای اون رو بگیره. این کار میتونه دستی باشه (یک مهندس وارد میشه و خودش replica رو ترفیع میده) یا خودکار، با ابزارهایی مثل Patroni برای PostgreSQL یا Orchestrator برای MySQL (رپو اصلی دیگه مینتین نمیشه من جدید رو هم لینک میکنم) که وضعیت سلامت primary رو مانیتور میکنن و در صورت خرابی، خودشون یکی از replica ها رو (معمولاً همونی که کمترین لگ رو داره) به primary جدید ترفیع میدن.
نکتهای که اینجا باید بهش دقت کرد: اگه دو replica همزمان فکر کنن primary هستن (که بهش پدیدهی split-brain میگن)، ممکنه هر دو بهطور مستقل نوشتن قبول کنن و داده از هم واگرا بشه. سیستمهای failover خوب معمولاً یک مکانیزم consensus (مثل الگوریتم Raft) دارن که تضمین میکنه در هر لحظه فقط یک primary واقعی وجود داره.
multi-master: وقتی هر جا میتونه بنویسه
توی مدل primary-replica معمولی، فقط primary اجازهی نوشتن داره. یک مدل جایگزین multi-master (یا multi-leader) است، جایی که چند سرور همزمان اجازهی نوشتن دارن و تغییراتشون رو با هم sync میکنن. این برای سیستمهای توزیعشدهی جغرافیایی جذابه — مثلاً یک سرور توی اروپا و یکی توی آسیا، هرکدوم بدون نیاز به رفتوبرگشت روی اقیانوس، مستقیم نوشتن رو قبول کنن. ولی هزینهی این مدل، مواجههست با تعارض (conflict): اگه دو کاربر همزمان روی دو سرور مختلف یک ردیف رو ویرایش کنن، سیستم باید تصمیم بگیره کدوم نسخه برندهست، که معمولاً با استراتژیهایی مثل «آخرین نوشتن برندهست» یا ساختارهای دادهی خاص به اسم CRDT حل میشه.
جمعبندی
ریپلیکیشن معاملهی همیشگی سیستمهای توزیعشده رو دوباره جلوی چشمت میذاره: بین سرعت، سازگاری، و دسترسپذیری باید انتخاب کنی، چون هر سهتاش رو همزمان و کامل نمیشه داشت. برای اکثر اپلیکیشنها، یک primary با چند async replica برای read scaling و disaster recovery کافیه. فقط وقتی واقعاً نیاز به نوشتن همزمان از چند نقطهی جغرافیایی داری، ارزششو داره که پیچیدگی multi-master رو به جون بخری.
