شاردینگ دیتابیس: وقتی یک سرور دیگه جایی برای داده نداره
ایندکس و کش و replica خواندنی، همه تا یک جایی جواب میدن. اگه حجم دادهات آنقدر بزرگ بشه که حتی قویترین سرور دیتابیس هم دیگه جا نداشته باشه نگهش داره، یا نرخ نوشتن آنقدر بالا بره که یک سرور تنها نتونه جوابگو باشه، اونوقت با یک مسئلهی متفاوت روبهرو میشی. ایندکس کمکت میکنه سریعتر پیدا کنی، replica کمکت میکنه بار خواندن رو پخش کنی، ولی هیچکدوم مشکل «داده آنقدر زیاده که روی یک ماشین جا نمیشه» رو حل نمیکنن. شاردینگ دقیقاً برای همین مسئله ساخته شده.
شاردینگ یعنی چی
شاردینگ یعنی دادهی یک جدول بزرگ رو به چند بخش (shard) تقسیم کنی و هر بخش رو روی یک سرور جدا نگه داری. برخلاف replication که هر سرور یک کپی کامل از کل داده رو داره، توی شاردینگ هر سرور فقط بخشی از داده رو داره. مثلاً اگه یک جدول کاربر با ۱۰۰ میلیون ردیف داری، میتونی کاربرهای با ID زوج رو روی یک سرور بذاری و کاربرهای با ID فرد رو روی سرور دیگه. حالا هر سرور فقط نصف داده رو مدیریت میکنه، و هم فضا و هم بار پردازشی بین دو ماشین تقسیم شده.
نکتهی مهم اینه که شاردینگ و replication دو مسئلهی متفاوت رو حل میکنن و معمولاً با هم ترکیب میشن: هر شارد خودش میتونه چند replica داشته باشه برای availability، و همزمان داده هم بین شاردهای مختلف تقسیم شده برای مقیاسپذیری.
چطور تصمیم بگیریم کدوم رکورد بره کدوم شارد
قلب هر سیستم شاردینگ، یک چیز به اسم shard key یا partition key است — ستونی که تعیین میکنه هر ردیف روی کدوم شارد قرار بگیره. چند استراتژی رایج برای این تصمیم وجود داره.
اولین و سادهترین، range-based sharding است: بازههای مقدار رو بین شاردها تقسیم میکنی، مثلاً کاربرهایی با ID بین ۱ تا ۱ میلیون روی شارد اول، بین ۱ تا ۲ میلیون روی شارد دوم. مزیتش اینه که query هایی که روی یک بازه کار میکنن (مثل «همهی سفارشهای هفتهی گذشته») ممکنه فقط به یک شارد نیاز داشته باشن. مشکلش اینه که اگه داده بهطور نامتوازن رشد کنه (مثلاً کاربرهای جدید همیشه ID بزرگتر میگیرن)، شارد آخر همیشه شلوغتر از بقیه میشه — به این پدیده hot shard میگن.
دومی hash-based sharding است: روی مقدار shard key یک تابع هش اجرا میکنی و نتیجه تعیین میکنه کدوم شارد. این روش توزیع خیلی یکنواختتری میده و مشکل hot shard رو تا حد زیادی حل میکنه، ولی query هایی که روی بازه کار میکنن رو سختتر میکنه، چون دادهی مرتبط ممکنه پخش بشه روی همهی شاردها.
سومی directory-based sharding است، جایی که یک جدول جدا (یا سرویس جدا) نگه میداری که میگه هر کلید دقیقاً کدوم شارده. انعطاف بیشتری میده — میتونی هر موقع خواستی داده رو جابهجا کنی بدون اینکه فرمول هش عوض بشه — ولی خودش یک نقطهی اضافهست که باید مدیریتش کنی و مراقب باشی bottleneck نشه.
چیزی که واقعاً سخته: query هایی که چند شارد رو لمس میکنن
توی یک دیتابیس تکسروری، یک JOIN بین دو جدول یا یک عملیات تراکنشی روی چند ردیف، کار همیشگی و بیدردسریه. توی یک سیستم شارد شده، این همون کاریه که واقعاً سخت میشه. اگه دو ردیفی که میخوای join کنی روی دو شارد مختلف باشن، دیگه نمیتونی از یک JOIN سادهی SQL استفاده کنی؛ باید یا داده رو توی اپلیکیشن خودت از دو شارد بخونی و دستی join کنی، یا از یک لایهی query routing استفاده کنی که این کار رو برات مخفی کنه.
همین مسئله برای تراکنشها هم صدق میکنه. یک تراکنش که باید همزمان روی دو ردیف از دو شارد مختلف atomic عمل کنه (یا هر دو موفق بشن یا هیچکدوم)، به یک پروتکل به اسم two-phase commit یا الگوهای پیچیدهتری مثل Saga نیاز داره. این دقیقاً همون چیزیه که شاردینگ رو از یک تصمیم سادهی زیرساختی به یک تصمیم معماری تبدیل میکنه؛ وقتی شارد میکنی، عملاً داری از دنیای تراکنشهای سادهی ACID فاصله میگیری، مگر اینکه خیلی هوشمندانه shard key رو طوری انتخاب کنی که دادههای مرتبط همیشه کنار هم بمونن.
re-sharding: کابوس واقعی
فرض کن با hash-based sharding شروع کردی و چهار شارد داری. حالا نیاز داری شاردهای بیشتری اضافه کنی. مشکل اینجاست که اگه فرمول هشت چیزی ساده مثل hash(key) % 4 باشه، وقتی این عدد رو به ۵ تغییر بدی، تقریباً تمام mapping ها بههم میریزه — چون باقیماندهی تقسیم برای اکثر کلیدها عوض میشه. یعنی باید تقریباً کل داده رو جابهجا کنی، درست همون چیزی که موقع طراحی سیستم میخواستی ازش فرار کنی.
راهحل رایج برای این مشکل consistent hashing است — همون تکنیکی که توی مقالهی لود بالانسینگ دربارهی Maglev گوگل ازش گفتیم. با consistent hashing، اضافهکردن یک شارد جدید فقط باعث میشه بخش کوچیکی از داده جابهجا بشه، نه همهچیز. برای همین سیستمهای بزرگمقیاسی مثل Cassandra و DynamoDB این تکنیک رو بهعنوان پایهی مکانیزم شاردینگشون انتخاب کردن.
مثالهای واقعی
Vitess، که اصلش رو یوتیوب برای مقیاسدادن MySQL ساخت (و بعداً Slack و GitHub هم ازش استفاده کردن)، دقیقاً یک لایهی میانی روی MySQL میذاره که شاردینگ رو تا حد زیادی از دید اپلیکیشن مخفی میکنه. Citus هم همین کار رو برای PostgreSQL انجام میده — جدولها رو بهصورت شفاف بین چند نود پخش میکنه و کوئریها رو بهصورت خودکار روت میکنه. MongoDB هم شاردینگ رو بهعنوان یک قابلیت داخلی و نه یک ابزار جانبی ارائه میده، که همین یکی از دلایلیه که خیلی از تیمها برای دادهی خیلی بزرگ سراغش میرن.
جمعبندی
شاردینگ آخرین ابزاریه که باید بهش فکر کنی، نه اولین. قبل از رسیدن به این مرحله معمولاً ایندکسگذاری بهتر، کش، و read replica ها میتونن مدت زیادی جوابگو باشن. شاردینگ رو وقتی به سراغش میری که واقعاً داده یا نرخ نوشتن از ظرفیت یک ماشین رد شده، چون همراهش پیچیدگیهای واقعی میاد: query های چندشارده سخت میشن، تراکنشها پیچیده میشن، و re-sharding میتونه یک پروژهی چندهفتهای بشه اگه از اول درست طراحی نشده باشه.
