
چرا طراحی سیستم پیچیده است
نقش انتزاع در مهار پیچیدگی
تعریف ساده و دقیق انتزاع
انتزاع در زندگی روزمره
انتزاع در مهندسی نرمافزار
چرا بدون انتزاع طراحی سیستم ممکن نیست
نقش دیتابیس در سیستمهای بزرگ
مشکل همزمانی و ناسازگاری داده
تراکنش (Transaction) بهعنوان ابزار انتزاع
رفتار «همه یا هیچ»
مثال کامل: انتقال پول
چگونه دیتابیس پیچیدگی داخلی را پنهان میکند
چالشهای ارتباط سرویسها بر روی شبکه
RPC چیست
RPC چگونه فراخوانی شبکهای را شبیه تابع محلی میکند
ساختار کلی RPC
مثال کاربردی در یک فروشگاه آنلاین
چرا در سیستمهای توزیعشده نسخههای داده ممکن است متفاوت باشد
تعریف مدل سازگاری (Consistency Model)
سازگاری نهایی — Eventual Consistency
سازگاری بر پایه «وابستگی منطقی بین رویدادها» — Causal Consistency
سازگاری ترتیبی — Sequential Consistency
سازگاری سخت / خطی — Strict / Linearizable Consistency
مثالهایی برای هر مدل
اهمیت شناخت انواع خرابی در سیستم توزیعشده
تعریف مدل خرابی (Failure Model)
خرابی آشکار — Fail-stop Failure
خرابی خاموش — Crash Failure
خرابی در ارسال/دریافت پیام — Omission Failure
حالت «ارسال نشدن پیام» — Send Omission
حالت «دریافت نشدن پیام» — Receive Omission
خرابی زمانی — Temporal Failure
خرابی غیرقابلپیشبینی / نامطمئن — Byzantine Failure
مثال عملی برای هر نوع خرابی
نقش انتزاع در طراحی سیستمهای واقعی
مزایای طراحی با انتزاع
ارتباط انتزاع با مقیاسپذیری، قابلیت اطمینان و سادگی تفکر
این مفاهیم چگونه در طراحی Instagram، Uber، YouTube و سیستمهای مشابه بهکار میروند
چرا فهم انتزاع مقدمۀ تمام مباحث مهمتر مثل دیتابیس، کش، لابالنسر، صف پیام و… است
سیستمهای نرمافزاری مدرن از دهها و گاهی صدها جزء مستقل تشکیل شدهاند: سرویسها، پایگاهدادهها، کشها، صفهای پیام، سرویسهای احراز هویت، ذخیرهسازی، شبکه، مانیتورینگ و زیرساخت اجرا. هر جزء رفتار، خطاها و محدودیتهای خاص خود را دارد. این اجزا باید با سرعت، پایداری و هماهنگی کامل در کنار هم کار کنند.
اگر بخواهیم همهٔ جزئیات هر جزء را همزمان در ذهن نگه داریم، حجم اطلاعات از توان ذهنی ما فراتر میرود و امکان تصمیمگیری در سطح طراحی کلان از بین میرود.
انتزاع روشی است که به ما اجازه میدهد دربارهٔ یک موضوع پیچیده با یک تصویر سادهشده فکر کنیم. این روش بخشهایی از سیستم را بهصورت «جعبه»هایی تعریف میکند که فقط رفتار بیرونیشان برای ما مهم است، نه نحوهٔ پیادهسازی درونی آنها.
انتزاع تمرکز ما را از جزئیات غیرضروری جدا میکند و روی مسئلهٔ اصلی قرار میدهد. بدون انتزاع مجبور میشدیم همزمان به ریزترین مواردی مثل بستههای شبکه، قفلگذاری دیتابیس یا جزئیات سیستمعامل فکر کنیم، و هیچچیز قابل طراحی نمیشد.
انتزاع یعنی ایجاد یک مدل ساده از یک بخش پیچیدهٔ سیستم، بهگونهای که بتوان با آن مدل کار کرد بدون اینکه نیاز باشد همهٔ جزئیات داخلی را بدانیم.
در عمل یعنی: «چیزی را آنطور ببینیم و استفاده کنیم که لازم داریم، نه آنطور که در واقعیت پیادهسازی شده است».
نمونههای انتزاع را هر روز میبینیم.
وقتی از خودرو استفاده میکنیم، لازم نیست بدانیم موتور چگونه احتراق میکند، سوخت چطور تزریق میشود یا ECU چطور فرمان میدهد. برای ما خودرو یک «رابط ساده» دارد: فرمان، پدال گاز، پدال ترمز.
یا وقتی از برق شهری استفاده میکنیم، لازم نیست بدانیم برق چگونه تولید و منتقل میشود؛ فقط کلید را میزنیم و لامپ روشن میشود.
اینها نمونههای روشن انتزاع هستند: لایهٔ پیچیده پنهان شده و ما با یک مدل ساده کار میکنیم.
در نرمافزار نیز بسیاری از ابزارها و مفاهیم بر اساس انتزاع ساخته شدهاند.
کتابخانهها (Libraries)، چارچوبها (Frameworks)، APIها، حتی زبانهای برنامهنویسی—all یک لایهٔ سادهتر روی واقعیت پیچیده ایجاد میکنند.
وقتی از یک کتابخانهٔ آماده استفاده میکنیم، لازم نیست بدانیم الگوریتم داخلی آن چگونه نوشته شده؛ فقط رفتار و ورودی و خروجی آن را میشناسیم.
همین باعث میشود توسعه سریعتر، قابلپیشبینیتر و گفتوگوی بین برنامهنویسان سادهتر شود.
طراحی سیستم نیازمند نگاه کلان است: اینکه داده از کجا میآید، چگونه پردازش میشود، چگونه ذخیره میشود، چگونه بین سرویسها جابهجا میشود و در صورت افزایش حجم چه تغییری لازم است.
اگر هنگام طراحی مجبور باشیم به موارد ریزتری مانند جزئیات پروتکلهای شبکه، نحوهٔ مدیریت حافظه، اجرای تراکنشها یا پیادهسازی ذخیرهسازی فکر کنیم، ذهن ما از مسئلهٔ اصلی منحرف میشود و طراحی قابل انجام نیست.
انتزاع این اجازه را میدهد که سیستم را در لایههای جداگانه ببینیم—هر لایه با سطح مشخصی از جزئیات—و دقیقاً در همان سطحی فکر کنیم که برای تصمیمگیری لازم است.
سیستمهای توزیعشده سه نقطهٔ حساس دارند که بیشترین پیچیدگی در آنها دیده میشود:
کار با داده و مدیریت همزمانی → پایگاه داده
ارتباط سرویسها از طریق شبکه → RPC
رفتار داده در شرایط چند نسخهای → مدلهای سازگاری داده
انتزاع باعث میشود این سه بخش، که در باطن بسیار پیچیدهاند، برای طراح سیستم قابل مدیریت و قابل دلیلآوردن باشند.
در سیستمهای مدرن، پایگاه داده مرکز نگهداری اطلاعات است:
کاربر، سفارشها، پیامها، فایلها، پرداختها، لاگها و …
این بخش حساسترین نقطهٔ سیستم است، زیرا داده باید همیشه صحیح، منظم و قابل اعتماد باقی بماند.
اما دیتابیس در باطن با مشکلات جدی روبهروست:
چند کاربر ممکن است همزمان یک داده را تغییر دهند
ممکن است سیستم هنگام نوشتن داده خاموش شود
ممکن است بخشی از داده بازنویسی شود و بخشی نه
ممکن است دو عملیات مرتبط با هم نیمهکاره بمانند
اگر برنامهنویس مجبور بود کل این جزئیات را مدیریت کند، کار غیرممکن میشد.
اینجاست که انتزاع دیتابیس وارد میشود.
فرض کنید دو کاربر همزمان موجودی حساب خود را تغییر میدهند.
اگر عملیاتها به ترتیب درست اجرا نشوند، ممکن است موجودی اشتباه شود.
یا تصور کنید در یک فروشگاه آنلاین:
کاربر A در حال پرداخت است
کاربر B همزمان همان کالا را اضافه میکند
موجودی کالا محدود است
اگر دیتابیس خودش نتواند ترتیب درست عملیات را فراهم کند، دادهٔ اشتباه وارد سیستم میشود.
این پیچیدگیها برای برنامهنویس قابل مدیریت نیست، بنابراین دیتابیس باید یک تصویر سادهشده بدهد.
تراکنش ابزاری است که دیتابیس برای ساده کردن کار ارائه میدهد.
تراکنش میگوید:
یک مجموعهٔ چندعملیاتی یا کامل انجام میشود، یا اصلاً انجام نمیشود.
این رفتار «همه یا هیچ» یعنی:
عملیات ناقص هیچوقت پذیرفته نمیشود
داده در حالت نیمهکاره نمیماند
اگر سیستم وسط کار خراب شود، عملیات به حالت قبل برمیگردد
برنامهنویس فقط دستورهایی مثل این میبیند:
BEGIN
COMMIT
ROLLBACK
درحالیکه دیتابیس پشت صحنه:
قفلگذاری
نوشتن لاگ
هماهنگی نسخهٔ داده
جلوگیری از تداخل عملیاتها
بازیابی پس از خرابی
را انجام میدهد.
تمام این کارها «پنهان» میشوند.
این یعنی انتزاع واقعی.
فرض کنید میخواهید ۵۰٬۰۰۰ تومان از حساب A به حساب B منتقل کنید.
این عملیات شامل دو کار است:
کم کردن موجودی A
اضافه کردن موجودی B
این دو باید دقیقاً با هم انجام شوند.
اگر یکی انجام شود و دیگری نه، سیستم دچار خطای خطرناک میشود.
تراکنش تضمین میکند:
یا هر دو انجام میشود، یا هیچکدام.
و این سادهترین شکل انتزاعی است که برنامهنویس میبیند، درحالیکه در باطن پیچیدگی بسیار بیشتری وجود دارد.
در سیستمهای توزیعشده، سرویسها مستقلاند و روی سرورهای مختلف اجرا میشوند.
برای انجام یک کار ساده، ممکن است چند سرویس با هم تعامل کنند.
اما «ارتباط از طریق شبکه» مشکلات زیادی دارد:
بستهبندی پیام
رمزگذاری و رمزگشایی
ارسال از طریق شبکه
قطعی شبکه
تأخیر
تکرار مجدد درخواست (Retry)
Timeout
دریافت و بازکردن پیام
خطاهای احتمالی در هر مرحله
اگر برنامهنویس مجبور بود برای هر درخواست این مراحل را کنترل کند، ساخت سیستم امکانپذیر نبود.
RPC یک لایهٔ انتزاعی است که به برنامهنویس اجازه میدهد یک تابع را طوری صدا بزند که انگار آن تابع در همان برنامه اجرا میشود.
در ظاهر، یک تابع معمولی:
user = UserService.Get(10)
در باطن، یک عملیات شبکهای کامل:
بستهبندی درخواست
ارسال روی شبکه
اجرا روی سرور دیگر
دریافت نتیجه
باز کردن پاسخ
مدیریت خطاها
این انتزاع باعث میشود برنامهنویس «شبکه» را مثل «تابع» ببیند.
کاربرد RPC دقیقاً این است:
شبکه را از چشم برنامهنویس پنهان کند.
در عمل RPC:
ساختار پیام ایجاد میکند
ارتباط برقرار میکند
خطاها را مدیریت میکند
نتیجه را برمیگرداند
اما در ظاهر:
تو همچنان داری یک تابع ساده صدا میزنی.
فرض کنید «سرویس سبد خرید» باید قیمت یک محصول را بگیرد.
ظاهر کد:
price = ProductService.GetPrice(productId)
اما در واقع:
این درخواست روی شبکه ارسال شده
به سرویس محصولات رسیده
پردازش شده
نتیجه بازگشته
RPC تمام این جزئیات را مخفی کرده و فقط یک «رابط ساده» ارائه داده.
این یعنی انتزاع در شبکه.
برای اینکه مدلهای سازگاری را درست بفهمیم، باید ابتدا ببینیم چرا اصلاً مسئلهای به نام «ناسازگاری داده» در سیستمهای توزیعشده وجود دارد.
اگر این قدم را خوب بفهمی، تمام مدلهای بعدی کاملاً شفاف و قابلدرک میشوند.
در یک سیستم ساده که فقط یک سرور دارد، داده همیشه یکنسخه است.
هر تغییری که انجام شود، در همان لحظه برای تمام عملیاتها قابل مشاهده است.
اما امروزه تقریباً هیچ سیستم بزرگی تکسروری نیست.
سیستمها برای سرعت، پایداری و تحمل خطا، داده را روی چندین سرور ذخیره میکنند.
همین موضوع بهظاهر ساده، مهمترین علت تفاوت نسخههای داده است.
حال چند دلیل اصلی را بهصورت کامل و با مثالهای واضح توضیح میدهیم:
وقتی داده روی چند سرور پخش شده باشد، هر تغییری باید در شبکه پخش شود تا همهٔ سرورها آن تغییر را ببینند.
اما شبکه همیشه سریع و تضمینی نیست.
مثال راحت:
سرور شماره ۱ اطلاعات کاربر را بهروز کرد.
سرور شماره ۲ هنوز اطلاع ندارد.
سرور شماره ۳ به دلیل شلوغی شبکه چند ثانیه دیرتر متوجه میشود.
در همین فاصلهٔ زمانی کوتاه،
سه نسخهٔ متفاوت از یک داده وجود دارد.
در یک سیستم واقعی با میلیونها کاربر، این اتفاق دائمی است.
فرض کن اینستاگرام برای لایک کردن یک پست، بهجای اینکه فوراً عدد لایک را نمایش دهد،
اول باید مطمئن شود:
همهٔ سرورها نسخهٔ جدید را دارند
همهٔ نسخهها بررسی و هماهنگ شدهاند
هیچ سروری عقب نیست
این کار چندین میلیثانیه یا حتی ثانیه طول میکشد.
در مقیاس اینستاگرام، همین تأخیر کوچک باعث فروپاشی تجربهٔ کاربر میشود.
پس سیستم تصمیم میگیرد:
«ابتدا سرعت را تضمین کنیم، هماهنگی بعداً انجام شود.»
اینجا است که نسخههای مختلف داده ایجاد میشود.
در یک سیستم توزیعشده،
خرابی یک «استثنا» نیست؛
خرابی یک «حالت طبیعی» است.
مثلاً:
یک سرور خاموش میشود
یک لینک شبکه برای چند ثانیه قطع میشود
پردازش یک سرور کند میشود
پیام بین دو سرور دیر میرسد
در چنین شرایطی، یک بخش از سیستم نسخهٔ جدید را دریافت میکند،
اما بخش دیگر تا مدتی نسخهٔ قدیمی را نگه میدارد.
تا وقتی داده روی چند سرور وجود دارد،
ناسازگاری داده «نه یک خطا»،
بلکه «یک واقعیت اجتنابناپذیر» است.
برای همین سیستمهای توزیعشده مجبورند یک قانوننامه داشته باشند که بگوید:
«اگر کاربر داده را خواند، اجازه داریم چه نسخهای را به او نشان دهیم؟»
این قانوننامه همان مدل سازگاری (Consistency Model) است.
حالا که علت اختلاف نسخهها را کاملاً فهمیدیم،
میتوانیم سراغ انواع مدلهای سازگاری برویم.
این مدل سادهترین شکل سازگاری است،
و بسیاری از سرویسهای بزرگ دنیا از آن استفاده میکنند،
بهخصوص سیستمهایی که تعداد خواندن بسیار زیاد است
و دقت لحظهای اهمیت زیادی ندارد.
در این مدل، سیستم میگوید:
«ممکن است الآن نسخهها یکی نباشند،
اما در نهایت همهٔ سرورها به نسخهٔ درست و مشترک خواهند رسید.»
یعنی:
در لحظه = احتمال تفاوت
بعد از کمی زمان = هماهنگی کامل
اگر بخواهیم همهٔ سرورها همیشه هماهنگ باشند،
سرعت سیستم بهشدت پایین میآید.
Eventual Consistency با خلاص شدن از این محدودیت سرعت،
به ما اجازه میدهد سیستمهای بسیار سریع بسازیم.
وقتی رکورد IP یک دامنه تغییر میکند:
بعضی سرورها فوراً نسخهٔ جدید را میبینند
بعضیها چند دقیقه دیرتر
اما نهایتاً همه یک نسخه خواهند داشت
هیچکس از DNS انتظار ندارد «در لحظهٔ تغییر»، همهٔ سرورها هماهنگ شوند.
در واتساپ، وقتی پیام ارسال میکنی:
تیک یک خاکستری یعنی «سرور دریافت کرده»
تیک دو خاکستری یعنی «به دستگاه طرف رسیده»
تیک دو آبی یعنی «خوانده شده»
این مدل رفتار یعنی «هماهنگی مرحلهبهمرحله»
که کاملاً با Eventual Consistency هماهنگ است.
اگر رویدادها به هم مربوط باشند،
سیستم باید ترتیب آنها را برای همه حفظ کند.
اما اگر هیچ رابطهای نداشته باشند،
نیازی به حفظ ترتیب نیست.
به زبان سادهتر:
«اگر یک اتفاق نتیجهٔ اتفاق دیگری باشد،
باید همیشه اولی دیده شود و بعد دومی.»
در شبکهٔ اجتماعی:
کاربر A مینویسد:
«کسی فیلم X را دیده؟»
کاربر B پاسخ میدهد:
«بله، خیلی خوبه.»
این دو رویداد به هم «وابستهاند».
پس سیستم باید همیشه این ترتیب را رعایت کند:
سؤال
پاسخ
اما اگر کاربر C پست دیگری بنویسد،
هیچ ارتباطی بین آن پست و سؤال A وجود ندارد،
پس ترتیب نمایش اهمیتی ندارد.
اگر سیستم ترتیب رویدادهای مرتبط را رعایت نکند،
رفتار سیستم نامفهوم و آزاردهنده میشود.
در این مدل،
سیستم تضمین میکند عملیات هر کاربر دقیقاً به همان ترتیبی دیده شود که خودش انجام داده است.
اما:
سیستم تضمین نمیکند ترتیب واقعی «زمان» برای همه یکی باشد.
کاربر A سه پست میگذارد:
سلام
امروز هوا خوبه
دارم میرم بیرون
حتی اگر این پستها در شبکه با تأخیر پراکنده شوند،
سیستم باید همیشه آنها را به همان ترتیب اصلی نمایش دهد.
اما اگر کاربر B همزمان پست بگذارد،
رعایت ترتیب بین پستهای A و B مهم نیست.
در شبکههای اجتماعی، انجمنها و اپلیکیشنهایی که رفتار کاربر مهمتر از ترتیب زمانی کامل است.
این قویترین، دقیقترین،
اما سنگینترین مدل سازگاری است.
در این مدل:
هر خواندن باید جدیدترین مقدار واقعی را برگرداند
حتی اگر تغییر، کمتر از یک میلیثانیه قبل انجام شده باشد
هیچ سروری نباید نسخهٔ قدیمی را برگرداند
به این ترتیب، سیستم مانند یک «دادهٔ یکتا و مرکزی» رفتار میکند،
حتی اگر داده روی دهها سرور ذخیره شده باشد.
اگر کاربر رمز عبور خود را تغییر دهد:
هیچ بخش سیستم نباید حتی یک لحظه رمز قبلی را قبول کند
هیچ نسخهٔ قدیمی نباید وجود داشته باشد
امنیت کامل باید تضمین شود
برای همین، این مدل در سیستمهای بانکی، احراز هویت، پرداخت و بخشهای بسیار حساس استفاده میشود.
برای حصول این سطح از دقت:
ارتباط بین سرورها کندتر میشود
نیاز به هماهنگی لحظهای وجود دارد
برخی عملیات قابل پردازش موازی نیستند
پس این مدل بسیار «گران» است و فقط در بخشهایی استفاده میشود که چارهای جز آن وجود ندارد.
در دنیای سیستمهای توزیعشده، خرابی یک استثنا نیست؛ بلکه یک وضعیت عادی است.
در سیستمی که صدها یا هزاران سرور دارد:
همیشه یک سرور در حال خاموش شدن است
همیشه یک بخش از شبکه کمی کندتر از بقیه است
همیشه تعدادی از پیامها دیر میرسند
همیشه احتمال خرابی نرمافزار، اشکالات کدنویسی یا حملهٔ بیرونی وجود دارد
اگر سیستم فقط زمانی درست کار کند که «هیچچیز خراب نشود»،
آن سیستم عملاً قابل استفاده نیست.
طراح سیستم باید بتواند بگوید:
چه نوع خرابیای رخ داده؟
چه تاثیری روی سیستم میگذارد؟
چگونه باید با آن برخورد کرد؟
هروقت بتوانیم خرابیها را روشن و دقیق دستهبندی کنیم،
میتوانیم برای هر دسته یک استراتژی دفاعی طراحی کنیم.
این دستهبندی همان مدلهای خرابی (Failure Models) است.
مدل خرابی کمک میکند رفتار درست یک سیستم را «قبل از خرابی، حین خرابی، و بعد از خرابی» تعریف کنیم.
برای مثال:
اگر یک سرور خاموش شود، آیا سیستم باید منتظر بماند؟
اگر پیامی نرسید، باید دوباره ارسال شود؟
اگر یک بخش خراب شد، آیا بقیهٔ سیستم باید ادامه دهند؟
اگر دادهٔ ناهماهنگ تولید شد، سیستم باید چه کند؟
بدون مدل خرابی، هیچکس دقیق نمیداند سیستم چه کند.
و نتیجهاش، رفتارهای عجیب و غیرقابلپیشبینی است.
بنابراین شناخت مدلهای خرابی، پایهٔ مقاومت یک سیستم است.
خرابی قابلتشخیص (Fail-stop Failure)
خرابی خاموش (Crash Failure)
خرابی در ارسال/دریافت پیام (Omission Failure)
حالت «ارسال نشدن پیام» (Send Omission)
حالت «دریافت نشدن پیام» (Receive Omission)
خرابی زمانی (Temporal Failure)
خرابی نامطمئن و غیرقابلپیشبینی (Byzantine Failure) – پیچیدهترین حالت
در ادامه، هر کدام از این مدلها را جداگانه و با جزئیات بررسی میکنیم.
در این نوع خرابی:
سرور از کار میافتد
اما بقیهٔ سرورها متوجه این خرابی میشوند
سیستم میداند آن سرور «خاموش» است
این سادهترین نوع خرابی است.
چون سیستم میتواند بلافاصله تصمیم بگیرد:
«این سرور دیگر ماشین حساب نیست. پس درخواستها را نفرستید.»
تصور کن یک سرور دیتابیس سختافزارش میسوزد.
سیستم مانیتورینگ فوراً گزارش میدهد: «سرور پاسخ نمیدهد».
در این حالت:
درخواستها دیگر به آن سرور ارسال نمیشوند
سیستم میتواند از replica دیگری استفاده کند
Fail-stop سادهترین خرابی است چون تشخیص آن قطعی است.
در این نوع خرابی:
سرور ناگهان متوقف میشود
اما سیستم نمیتواند با قطعیت تشخیص دهد که مشکل از خرابی سرور است یا فقط یک تأخیر موقت
چون سیستم باید بین دو احتمال تصمیم بگیرد:
آیا سرور مرده است؟
یا زنده است، اما فقط دیر جواب میدهد؟
و این تشخیص همیشه سخت است.
فرض کن یک میکروسرویس بهطور ناگهانی برای چند ثانیه پاسخ نمیدهد:
شاید واقعا crash شده
شاید CPU پر شده
شاید شبکه برای چند ثانیه کند شده
سیستم از بیرون فقط «بیپاسخی» میبیند
و باید تصمیم بگیرد:
صبر کند؟
یا آن را مرده فرض کند؟
این تصمیم اگر اشتباه گرفته شود، مشکلات جدی ایجاد میکند.
این مدل از خرابی زمانی رخ میدهد که «پیام رد و بدل نمیشود»
اما سرور لزوماً کاملاً خراب نشده.
دو نوع دارد:
سرور میخواهد پیامی بفرستد،
اما پیام در شبکه گم میشود
یا اصلاً خارج نمیشود.
در یک سیستم پرداخت:
سرویس فروشگاه به سرویس پرداخت درخواست میفرستد
پیام از شبکه عبور نمیکند
پرداخت انجام نشده
اما فروشگاه خبر ندارد
نتیجه؟
ممکن است کاربر پول پرداخت کرده باشد اما سیستم ثبت نکرده باشد.
پیام به مقصد میرسد،
اما سرور دریافتکننده:
به دلیل شلوغی
اشکال نرمافزار
یا تاخیر زیاد
پیام را پردازش نمیکند.
در یک صف پیام (Message Queue):
سرویس A پیام را ارسال میکند
سرویس B پیام را دریافت نمیکند
اما پیام واقعاً رسیده بوده
در چنین شرایطی، سیستم ممکن است پیام را «پردازش نشده» تشخیص دهد و دوباره ارسال کند
و این چند بار پردازش شدن یک پیام میتواند مشکلساز شود.
در این نوع خرابی:
سرور کار درست انجام میدهد
اما آنقدر دیر که دیگر پاسخ به درد نمیخورد
این یکی از دردناکترین خرابیهاست چون سیستم در ظاهر کار میکند
اما از نظر کاربر، «خراب» دیده میشود.
در اپلیکیشن اسنپ یا اوبر:
راننده موقعیت خود را ۱۰ ثانیه دیرتر ارسال میکند
مسافر فکر میکند راننده «ایستاده»
درحالیکه راننده در حال حرکت است
اطلاعات درست است
اما “بهموقع” نیست
پس سیستم عملاً مفید نیست.
در سامانهٔ بورس،
ارائهٔ قیمت سهام با تأخیر باعث میشود معاملهگر تصمیم اشتباه بگیرد.
در چنین سیستمهایی حتی تأخیر ۵۰۰ میلیثانیه فاجعه است.
این نوع خرابی کابوس تمام طراحان سیستمهای بزرگ است.
در این خرابی:
سرور ممکن است رفتار تصادفی نشان دهد
ممکن است اطلاعات اشتباه، قدیمی یا متناقض ارسال کند
ممکن است گاهی درست و گاهی غلط کار کند
ممکن است پیامهای ساختگی بفرستد
ممکن است نتایج اشتباه تولید کند
و بدتر از همه:
هیچکس نمیفهمد که کدام بخش راست میگوید.
فرض کن سه سرور باید بگویند:
«موجودی حساب چقدر است؟»
سرور ۱: موجودی = ۵۰۰
سرور ۲: موجودی = ۵۰۰
سرور ۳: موجودی = ۱۳ میلیون
کدام درست میگوید؟
آیا سرور ۳ خراب است؟
آیا ۱ و ۲ هماهنگ نشدهاند؟
آیا حملهٔ هکری شده؟
هیچچیز مشخص نیست.
چون سرور خراب ساکت نمیماند
بلکه «مشغول خرابکاری» است.
برخلاف Fail-stop که سرور کامل خاموش میشود
و مشکل ساده و قابل تشخیص است،
در Byzantine Failure
سرور زنده است
اما قابل اعتماد نیست.
در بلاکچین،
باید همیشه فرض کرد که برخی نودها:
خراب شدهاند
یا دستکاری شدهاند
یا تحت کنترل مهاجماند
برای همین بلاکچین مجبور است از الگوریتمهای بسیار سنگین مثل:
PoW
PoS
استفاده کند تا جلوی رفتارهای Byzantine گرفته شود.
Fail-stop ساده است
چون «خرابی قابل تشخیص» است
Crash سختتر است
چون «بیپاسخی» همیشه نشانهٔ خرابی نیست
Omission دردسرساز است
چون پیامها گم میشوند و سیستم باید تصمیم بگیرد چه کند
Temporal بهظاهر کار میکند
اما «دیر بودن» سیستم را بیاستفاده میکند
Byzantine خطرناکترین است
چون «سرور خراب میتواند دروغ بگوید»
و تشخیص حقیقت از دروغ تقریباً غیرممکن است
طراح سیستم باید برای هر کدام استراتژی مناسب بسازد
وگرنه سیستم در دنیای واقعی پایدار نخواهد بود.
انتزاع (Abstraction) شاید در ظاهر یک مفهوم ساده باشد، اما در عمل ستونِ اصلی طراحی سیستمهای بزرگ است. تقریباً تمام شرکتهای بزرگ دنیا—از اینستاگرام و اوبر گرفته تا یوتیوب و گوگل—تمام معماری سیستم خود را روی شانهٔ همین مفهوم ساختهاند. حالا باید ببینیم دقیقاً چرا انتزاع چنین جایگاهی دارد و نبود آن چه نتایجی دارد.
در ادامه این بخش را در پنج محور اصلی جمعبندی میکنیم.
هدف اصلی انتزاع این است که پیچیدگی را قابل مدیریت کند.
وقتی یک سیستم فقط از یک سرور تشکیل شده باشد، فهم آن ساده است. اما وقتی:
صدها سرویس دارید
صدها پایگاه داده در مناطق مختلف
میلیونها کاربر فعال
دهها سیستم ذخیرهسازی
کش، صف پیام، CDN، کنترل ترافیک، load balancer
آن وقت حتی «فهمیدن اینکه سیستم دقیقاً چهکار میکند» مشکل میشود.
در چنین شرایطی انتزاع کاری میکند که بتوانید:
به جای نگاه کردن به هزار جزئیات، یک تصویر قابل فهم داشته باشید
رفتار هر بخش را فقط از طریق یک رابط ساده تحلیل کنید
سیستم را به بخشهای مستقلتر تقسیم کنید
در حقیقت انتزاع برای طراح سیستمِ بزرگ مثل «نقشهٔ راه» است.
بدون آن، معمار سیستم نمیتواند دید کلی داشته باشد و نمیتواند دلیل درستی برای انتخابهای طراحی ارائه دهد.
وقتی سیستم را در لایههای انتزاعی میبینیم، چند مزیت مهم ایجاد میشود:
وقتی هر لایه فقط یک وظیفهٔ مشخص دارد، میتوان یک بخش را عوض کرد بدون اینکه بقیه آسیب ببینند.
مثال:
دیتابیس را تغییر میدهیم
کش را عوض میکنیم
load balancer را ارتقا میدهیم
و سیستم همچنان کار میکند چون قراردادهای بین این اجزا ساده و ثابت ماندهاند.
مغز انسان قادر نیست هزاران ارتباط پیچیده را مدیریت کند.
انتزاع این پیچیدگی را «سطحبندی» میکند.
مثلاً:
از نگاه کلان: فقط چند سرویس اصلی میبینیم
از نگاه میانی: جزئیات ارتباطات سرویسها
از نگاه دقیق: نحوهٔ ذخیرهسازی، صف، کش، پیام و…
این تفکیک باعث میشود بتوانید سیستم بسیار بزرگ را طراحی، عیبیابی و توسعه دهید.
وقتی توسعهدهنده مجبور نباشد تمام جزئیات شبکه، پایگاه داده، مدل خرابی، مدل سازگاری و… را هر بار از نو حل کند، توسعه سریعتر میشود.
انتزاع یعنی:
RPC مثل فراخوانی تابع محلی است
transaction مثل یک بستهٔ «همه یا هیچ» دیده میشود
consistency model مثل قانون قابلپیشبینی رفتار میکند
failure model مثل یک دستهبندی شفاف است
این یعنی سرعت واقعی توسعه.
در شرکتهای بزرگ ۲۰۰ تیم همزمان روی یک محصول کار میکنند.
انتزاع باعث میشود هر تیم فقط «جهان خودش» را ببیند.
مثلاً:
تیم A مسئول دیتابیس است
تیم B مسئول لاگگیری است
تیم C مسئول پیامرسانی است
هر تیم تنها API و رفتار لایهٔ خودش را میبیند، و نیازی نیست از جزئیات داخلی دیگر تیمها خبر داشته باشد.
انتزاع فقط فهم مفهومی نیست، بلکه مستقیماً روی کیفیت سیستم تأثیر دارد.
برای بزرگ شدن یک سیستم، باید بتوانید:
بخشها را مستقل مقیاس دهید
ترافیک را بین سرویسها تقسیم کنید
منابع را جداگانه افزایش دهید
این کار فقط زمانی ممکن است که هر بخش «یک انتزاع واضح» داشته باشد.
مثال:
یک سرویس stateless را میتوان با اضافه کردن چند replica مقیاس داد
یک صف پیام را میتوان shard کرد
یک کش را میتوان توزیعشده کرد
اینها بدون انتزاع امکانپذیر نیست.
خرابی همیشه اتفاق میافتد.
اما اگر سیستم بر اساس مدلهای انتزاعی خرابی طراحی شده باشد:
خرابی قابل پیشبینی میشود
رفتار قابل کنترل میشود
کنارآمدن با خرابی امکانپذیر میشود
مثلاً اگر بدانید:
Omission Failure یعنی پیام ممکن است گم شود
Crash Failure یعنی سیستم نمیداند مشکل از کجاست
Byzantine Failure یعنی لازم است سرورها همدیگر را تایید کنند
میتوانید معماری مقاوم بسازید.
وقتی هزاران جزئیات نامرتبط را حذف میکنید،
تفکر شما واضحتر میشود.
در طراحی سیستمهای بزرگ، شفاف فکر کردن مهمتر از داشتن جزئیات زیاد است.
همهٔ سیستمهای بزرگ دنیا از انتزاع استفاده میکنند.
بیایید چند مثال واقعی ببینیم:
لایک و کامنت با Eventual Consistency انجام میشود چون سرعت مهمتر است.
انتقال تصویرها توسط CDN انجام میشود؛ یک لایهٔ انتزاعی از شبکهٔ جهانی.
هر درخواست HTTP توسط Load Balancer به نزدیکترین سرور هدایت میشود.
دیتابیسها شارد شدهاند و هر shard یک انتزاع از «بخشی از کاربرها» است.
موقعیت راننده با مدلهای مخصوص خرابی زمانی (Temporal Failure) مدیریت میشود.
سرویسهای راننده و مسافر با RPC با هم صحبت میکنند و این پیچیدگی شبکه پنهان شده.
سیستم Matching راننده و مسافر به چند لایه تقسیم شده است:
الگوریتم → موتور جستجو → سرویسها → cache → پایگاه داده.
سیستم ویدیوها روی چند CDN جهانی با مدل Eventual Consistency منتشر میشود.
تعداد بازدید ابتدا در cache ذخیره میشود، سپس بهصورت batch در دیتابیس ثبت میگردد (بهخاطر مدل consistency).
صف پیام Kafka برای پردازش ویدیو استفاده میشود؛ یعنی یک انتزاع کامل از جریان داده.
در تمام اینها یک الگوی واضح دیده میشود:
هیچکدام کل سیستم را یکجا نمیبینند؛ هر بخش فقط انتزاع لایهٔ خودش را میبیند.
تمام مفاهیم اصلی طراحی سیستم—
پایگاه داده
کش
load balancer
صف پیام
CDN
replicaton
sharding
consistency
failure models
—همگی بر پایهٔ انتزاع ساخته شدهاند.
اگر انتزاع را نفهمیم:
نمیدانیم چرا باید Shard کنیم
متوجه نمیشویم چرا consistency مدل دارد
نمیدانیم cache باید چه رفتاری داشته باشد
نمیفهمیم چرا load balancer فقط یک «جعبه» در دیاگرام است
نمیدانیم failure model چه تاثیری دارد
نمیتوانیم معماری را بهدرستی تحلیل کنیم
انتزاع اولین قدم است، چون:
این موضوع شبیه معماری ساختمان است:
قبل از طراحی برق، لولهکشی، تهویه، سازه، آسانسور
باید «نقشهٔ کلی» وجود داشته باشد
انتزاع همان نقشهٔ کلی است.
انتزاع ابزار اصلی مهندسان نرمافزار برای فکر کردن به سیستمهای بزرگ است.
بدون آن سیستمهای امروزی—از فروشگاه آنلاین گرفته تا یوتیوب و اینستاگرام—اصلاً قابل طراحی نیستند.
انتزاع:
پیچیدگی را حذف نمیکند، مدیریتپذیر میکند
بخشها را جدا میکند تا قابل توسعه شوند
مسیر طراحی صحیح را مشخص میکند
پایهٔ مباحث مهمی مثل consistency، failure، replication و scalability است
به همین دلیل است که در آموزش طراحی سیستم،
حتماً باید انتزاع اولین درس باشد.
نویسنده : امیر معصوم بیگی