🧐فکر میکردم System Design فقط کشیدن چند تا باکسه...
تا قبل از اینکه خودم بخوام یک System Design انجام بدم، از دور همهچیز خیلی ساده به نظر میرسید.
چند تا مستطیل، چند تا فلش بینشون و تمام!
اما وقتی چند وقت پیش اولین System Design جدی خودم رو شروع کردم، فهمیدم اصلاً نمیدونم باید از کجا شروع کنم.
❓سؤالهایی مثل این مدام توی ذهنم میچرخید:
کش رو کجا باید قرار بدم؟
اصلاً به Queue نیاز دارم؟
CDN لازمه؟
Database چی انتخاب کنم؟
Load Balancer از همون اول لازمه؟
این سیستم اصلاً قراره Scale بشه یا نه؟
کمکم فهمیدم اشتباه من این بود که از آخر مسیر شروع کرده بودم.
⚖️قبل از اینکه اصلاً به System Design فکر کنیم، باید بدونیم هر کدوم از این ابزارها و مفاهیم دقیقاً چه مشکلی رو حل میکنن.
مثلاً:
تا وقتی ندونیم هر کدوم چه کاربردی دارن، کنار هم قرار دادنشون فقط تبدیل میشه به کشیدن چند تا باکس روی یک صفحه.
📌بعد از کمی مطالعه، فهمیدم روند طراحی سیستم معمولاً خیلی منطقیتر از چیزیه که اول به نظر میاد.
تقریباً میشه اینطوری بهش نگاه کرد:
1️⃣ نیازمندیهای سیستم رو مشخص کن
سیستم قراره چه کاری انجام بده؟ چه قابلیتهایی باید داشته باشه؟
2️⃣ محدودیتها و انتظارات رو مشخص کن
چند کاربر همزمان خواهیم داشت؟ میزان ترافیک چقدره؟ اولویت با Performance هست یا Cost؟ Availability مهمتره یا Consistency؟
3️⃣ یک طراحی ساده ارائه بده
بدون Cache، Queue یا هر چیز اضافهای. فقط اجزای اصلی سیستم.
4️⃣ گلوگاهها رو پیدا کن
حالا کمکم سؤالها خودشون به وجود میان:
اگر ترافیک زیاد شد چی؟
اگر دیتابیس پاسخگو نبود چی؟
اگر فایلها زیاد شدن چی؟
و دقیقاً توی همین نقطه هست که ابزارهایی مثل Cache، Queue، CDN و... وارد طراحی میشن؛ نه از همون اول.
🧠برای من، بزرگترین درس این بود که System Design حفظ کردن ابزارها نیست؛ یاد گرفتن نحوهی فکر کردنه.
شاید یکی از دلایلی که این روزها بیشتر از قبل به یادگیری System Design علاقمند شدم، پیشرفت سریع ابزارهای هوش مصنوعیه. هرچی نوشتن کد ساده تر و در دسترس تر میشه، ارزش مهارت هایی مثل تحلیل نیازمندیها، شناخت Trade-offها و طراحی معماری مناسب بیشتر خودش رو نشون میده.
به نظرم توی سالهای آینده، این مهارتها بیش از هر زمان دیگری افراد رو از هم متمایز خواهند کرد.
من تازه اول این مسیرم، اما فکر میکنم همین تغییر طرز فکر، مهمترین چیزی بود که تا الان یاد گرفتم. 🚀
➖➖➖➖➖➖➖➖➖➖
تا قبل از اینکه خودم بخوام یک System Design انجام بدم، از دور همهچیز خیلی ساده به نظر میرسید.
چند تا مستطیل، چند تا فلش بینشون و تمام!
اما وقتی چند وقت پیش اولین System Design جدی خودم رو شروع کردم، فهمیدم اصلاً نمیدونم باید از کجا شروع کنم.
❓سؤالهایی مثل این مدام توی ذهنم میچرخید:
کش رو کجا باید قرار بدم؟
اصلاً به Queue نیاز دارم؟
CDN لازمه؟
Database چی انتخاب کنم؟
Load Balancer از همون اول لازمه؟
این سیستم اصلاً قراره Scale بشه یا نه؟
کمکم فهمیدم اشتباه من این بود که از آخر مسیر شروع کرده بودم.
⚖️قبل از اینکه اصلاً به System Design فکر کنیم، باید بدونیم هر کدوم از این ابزارها و مفاهیم دقیقاً چه مشکلی رو حل میکنن.
مثلاً:
- Cache
- CDN
- Load Balancer
- Message Queue
- Reverse Proxy
- Object Storage
- Database Replication
- Database Sharding
- Consistency
- CAP Theorem
تا وقتی ندونیم هر کدوم چه کاربردی دارن، کنار هم قرار دادنشون فقط تبدیل میشه به کشیدن چند تا باکس روی یک صفحه.
📌بعد از کمی مطالعه، فهمیدم روند طراحی سیستم معمولاً خیلی منطقیتر از چیزیه که اول به نظر میاد.
تقریباً میشه اینطوری بهش نگاه کرد:
1️⃣ نیازمندیهای سیستم رو مشخص کن
سیستم قراره چه کاری انجام بده؟ چه قابلیتهایی باید داشته باشه؟
2️⃣ محدودیتها و انتظارات رو مشخص کن
چند کاربر همزمان خواهیم داشت؟ میزان ترافیک چقدره؟ اولویت با Performance هست یا Cost؟ Availability مهمتره یا Consistency؟
3️⃣ یک طراحی ساده ارائه بده
بدون Cache، Queue یا هر چیز اضافهای. فقط اجزای اصلی سیستم.
4️⃣ گلوگاهها رو پیدا کن
حالا کمکم سؤالها خودشون به وجود میان:
اگر ترافیک زیاد شد چی؟
اگر دیتابیس پاسخگو نبود چی؟
اگر فایلها زیاد شدن چی؟
و دقیقاً توی همین نقطه هست که ابزارهایی مثل Cache، Queue، CDN و... وارد طراحی میشن؛ نه از همون اول.
🧠برای من، بزرگترین درس این بود که System Design حفظ کردن ابزارها نیست؛ یاد گرفتن نحوهی فکر کردنه.
شاید یکی از دلایلی که این روزها بیشتر از قبل به یادگیری System Design علاقمند شدم، پیشرفت سریع ابزارهای هوش مصنوعیه. هرچی نوشتن کد ساده تر و در دسترس تر میشه، ارزش مهارت هایی مثل تحلیل نیازمندیها، شناخت Trade-offها و طراحی معماری مناسب بیشتر خودش رو نشون میده.
به نظرم توی سالهای آینده، این مهارتها بیش از هر زمان دیگری افراد رو از هم متمایز خواهند کرد.
من تازه اول این مسیرم، اما فکر میکنم همین تغییر طرز فکر، مهمترین چیزی بود که تا الان یاد گرفتم. 🚀
#️⃣ #SystemDesign #Backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
چرا Read و Write در PostgreSQL همدیگه رو متوقف نمیکنن؟🐘
وقتی برای اولین بار دربارهی PostgreSQL مطالعه میکردم، یکی از مفاهیمی که زیاد باهاش برخورد میکردم MVCC (Multi-Version Concurrency Control) بود.
اوایل فکر میکردم فقط اسم یک قابلیت دیگه از PostgreSQL باشه، اما هرچی بیشتر دربارهش خوندم، بیشتر متوجه شدم که بخش بزرگی از رفتار این دیتابیس بر پایهی همین مفهوم طراحی شده.
بیاید اول یک سناریوی ساده رو تصور کنیم.
فرض کنید یک API در حال خواندن اطلاعات یک کاربره.
همون لحظه، درخواست دیگهای میاد و اطلاعات همون کاربر رو تغییر میده.
حالا چند سؤال پیش میاد:
🤔 آیا عملیات خواندن باید متوقف بشه تا عملیات نوشتن تموم بشه؟
🤔 یا عملیات نوشتن باید منتظر بمونه تا عملیات خواندن به پایان برسه؟
اگر جوابمون «بله» باشه، با افزایش تعداد کاربران، دیتابیس به مرور زمان پر از Lock میشه و تعداد زیادی از Queryها فقط منتظر آزاد شدن منابع باقی میمونن.
نتیجه؟
❌ کاهش Concurrency
❌ افزایش زمان پاسخگویی
❌ افت Performance
اینجاست که PostgreSQL از MVCC استفاده میکنه.💡
ایدهی اصلی MVCC اینه که به جای بازنویسی مستقیم یک رکورد، یک نسخهی جدید از اون رکورد ایجاد بشه.
در نتیجه، هر Transaction تصویری از دیتابیس رو میبینه که در لحظهی شروع خودش معتبر بوده. یعنی اگر یک Query قبل از تغییر یک رکورد شروع شده باشه، همچنان همون نسخهی قدیمی رو مشاهده میکنه؛ حتی اگر در همین فاصله Transaction دیگهای اطلاعات رو تغییر داده باشه. از طرف دیگه، Transactionهای جدید نسخهی جدید رکورد رو خواهند دید.
به همین دلیل، در بسیاری از سناریوها عملیات Read و Write بدون اینکه همدیگه رو Block کنن، به صورت همزمان انجام میشن.
این روش چه مزیتی داره؟ 🚀
به جای اینکه دیتابیس دائماً درگیر Lock کردن رکوردها باشه، میتونه تعداد بسیار بیشتری Transaction همزمان رو مدیریت کنه.
نتیجهی این تصمیم:
✅ Concurrency بالاتر
✅ Lockهای کمتر
✅ Response Time بهتر
✅ مقیاسپذیری بیشتر در سیستمهای پرترافیک
به همین دلیله که PostgreSQL حتی تحت بارهای سنگین هم عملکرد بسیار خوبی از خودش نشون میده.
اما این روش رایگان نیست... ⚠️
وقتی برای هر تغییر، نسخهی جدیدی از رکورد ایجاد میشه، نسخههای قدیمی همچنان داخل دیتابیس باقی میمونن.
البته این نسخهها برای همیشه نگهداری نمیشن. PostgreSQL سرویسی به نام VACUUM داره که وظیفهی شناسایی و پاکسازی نسخههای قدیمی و بلااستفاده رو بر عهده داره. اگر این فرآیند به درستی انجام نشه، حجم دیتابیس به مرور افزایش پیدا میکنه و حتی ممکنه روی Performance هم تأثیر منفی بذاره.
به همین دلیل، MVCC و VACUUM دو مفهوم کاملاً وابسته به هم هستن.
آیا MVCC یعنی هیچ Lockای وجود نداره؟
نه.
یکی از برداشتهای اشتباه اینه که MVCC باعث حذف کامل Lockها میشه. در واقع PostgreSQL همچنان در بعضی عملیات، مثل تغییر همزمان یک رکورد توسط چند Transaction یا بعضی تغییرات ساختاری (DDL)، از Lock استفاده میکنه.
اما تفاوت اصلی اینجاست که عملیات خواندن معمولی، در اکثر مواقع نیازی به منتظر موندن برای عملیات نوشتن نداره.
همین تفاوت، یکی از دلایل اصلی عملکرد خوب PostgreSQL در سیستمهای پرترافیکه.
MVCC فقط یک قابلیت برای افزایش سرعت نیست.💡
در واقع، یک تصمیم معماریه که روی نحوهی اجرای Transactionها، مدیریت همزمانی و حتی طراحی بخشهای مختلف PostgreSQL تأثیر گذاشته.
شناخت مفاهیمی مثل MVCC، VACUUM، WAL و Query Planner کمک میکنه دلیل خیلی از رفتارهای دیتابیس رو بهتر درک کنیم و تصمیمهای بهتری در طراحی سیستم بگیریم.
➖➖➖➖➖➖➖➖➖➖
وقتی برای اولین بار دربارهی PostgreSQL مطالعه میکردم، یکی از مفاهیمی که زیاد باهاش برخورد میکردم MVCC (Multi-Version Concurrency Control) بود.
اوایل فکر میکردم فقط اسم یک قابلیت دیگه از PostgreSQL باشه، اما هرچی بیشتر دربارهش خوندم، بیشتر متوجه شدم که بخش بزرگی از رفتار این دیتابیس بر پایهی همین مفهوم طراحی شده.
بیاید اول یک سناریوی ساده رو تصور کنیم.
فرض کنید یک API در حال خواندن اطلاعات یک کاربره.
همون لحظه، درخواست دیگهای میاد و اطلاعات همون کاربر رو تغییر میده.
حالا چند سؤال پیش میاد:
🤔 آیا عملیات خواندن باید متوقف بشه تا عملیات نوشتن تموم بشه؟
🤔 یا عملیات نوشتن باید منتظر بمونه تا عملیات خواندن به پایان برسه؟
اگر جوابمون «بله» باشه، با افزایش تعداد کاربران، دیتابیس به مرور زمان پر از Lock میشه و تعداد زیادی از Queryها فقط منتظر آزاد شدن منابع باقی میمونن.
نتیجه؟
❌ کاهش Concurrency
❌ افزایش زمان پاسخگویی
❌ افت Performance
اینجاست که PostgreSQL از MVCC استفاده میکنه.💡
ایدهی اصلی MVCC اینه که به جای بازنویسی مستقیم یک رکورد، یک نسخهی جدید از اون رکورد ایجاد بشه.
در نتیجه، هر Transaction تصویری از دیتابیس رو میبینه که در لحظهی شروع خودش معتبر بوده. یعنی اگر یک Query قبل از تغییر یک رکورد شروع شده باشه، همچنان همون نسخهی قدیمی رو مشاهده میکنه؛ حتی اگر در همین فاصله Transaction دیگهای اطلاعات رو تغییر داده باشه. از طرف دیگه، Transactionهای جدید نسخهی جدید رکورد رو خواهند دید.
به همین دلیل، در بسیاری از سناریوها عملیات Read و Write بدون اینکه همدیگه رو Block کنن، به صورت همزمان انجام میشن.
این روش چه مزیتی داره؟ 🚀
به جای اینکه دیتابیس دائماً درگیر Lock کردن رکوردها باشه، میتونه تعداد بسیار بیشتری Transaction همزمان رو مدیریت کنه.
نتیجهی این تصمیم:
✅ Concurrency بالاتر
✅ Lockهای کمتر
✅ Response Time بهتر
✅ مقیاسپذیری بیشتر در سیستمهای پرترافیک
به همین دلیله که PostgreSQL حتی تحت بارهای سنگین هم عملکرد بسیار خوبی از خودش نشون میده.
اما این روش رایگان نیست... ⚠️
وقتی برای هر تغییر، نسخهی جدیدی از رکورد ایجاد میشه، نسخههای قدیمی همچنان داخل دیتابیس باقی میمونن.
البته این نسخهها برای همیشه نگهداری نمیشن. PostgreSQL سرویسی به نام VACUUM داره که وظیفهی شناسایی و پاکسازی نسخههای قدیمی و بلااستفاده رو بر عهده داره. اگر این فرآیند به درستی انجام نشه، حجم دیتابیس به مرور افزایش پیدا میکنه و حتی ممکنه روی Performance هم تأثیر منفی بذاره.
به همین دلیل، MVCC و VACUUM دو مفهوم کاملاً وابسته به هم هستن.
آیا MVCC یعنی هیچ Lockای وجود نداره؟
نه.
یکی از برداشتهای اشتباه اینه که MVCC باعث حذف کامل Lockها میشه. در واقع PostgreSQL همچنان در بعضی عملیات، مثل تغییر همزمان یک رکورد توسط چند Transaction یا بعضی تغییرات ساختاری (DDL)، از Lock استفاده میکنه.
اما تفاوت اصلی اینجاست که عملیات خواندن معمولی، در اکثر مواقع نیازی به منتظر موندن برای عملیات نوشتن نداره.
همین تفاوت، یکی از دلایل اصلی عملکرد خوب PostgreSQL در سیستمهای پرترافیکه.
MVCC فقط یک قابلیت برای افزایش سرعت نیست.💡
در واقع، یک تصمیم معماریه که روی نحوهی اجرای Transactionها، مدیریت همزمانی و حتی طراحی بخشهای مختلف PostgreSQL تأثیر گذاشته.
شناخت مفاهیمی مثل MVCC، VACUUM، WAL و Query Planner کمک میکنه دلیل خیلی از رفتارهای دیتابیس رو بهتر درک کنیم و تصمیمهای بهتری در طراحی سیستم بگیریم.
#️⃣ #Database #Backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
تا وقتی پروژههای کوچیک مینویسیم، معمولاً فقط چند متد معروف مثل append() یا pop() رو استفاده میکنیم.
اما list و tuple متدها و قابلیتهای بیشتری دارن که بعضی از اونها کمتر شناخته شدن، در حالی که میتونن کد رو خواناتر و حتی سادهتر کنن.
یه Cheat Sheet از متدهای مهم list و tuple آماده کردم که امیدوارم به دردتون بخوره. 🚀
➖➖➖➖➖➖➖➖➖➖
اما list و tuple متدها و قابلیتهای بیشتری دارن که بعضی از اونها کمتر شناخته شدن، در حالی که میتونن کد رو خواناتر و حتی سادهتر کنن.
یه Cheat Sheet از متدهای مهم list و tuple آماده کردم که امیدوارم به دردتون بخوره. 🚀
#️⃣ #Python
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
اگر درست وسط یک Transaction برق سرور قطع بشه، PostgreSQL چیکار میکنه؟🐘
فرض کنید یک Transaction در حال انتقال پول بین دو حسابه.
ابتدا موجودی حساب اول کم میشه و بعد موجودی حساب دوم افزایش پیدا میکنه.
حالا تصور کنید درست بعد از انجام مرحلهی اول، برق سرور قطع بشه یا سیستم Crash کنه.
سؤال اینجاست:
🤔 از کجا معلوم دیتابیس بعد از روشن شدن دوباره، وضعیت درستی داشته باشه؟
اگر اطلاعات مستقیماً روی دیسک نوشته بشن، ممکنه فقط بخشی از تغییرات ذخیره شده باشه و دیتابیس در یک وضعیت ناسازگار (Inconsistent) قرار بگیره.
اینجاست که WAL (Write-Ahead Logging) وارد ماجرا میشه.💡
ایدهی اصلی WAL خیلی سادهست:
قبل از اینکه هر تغییری روی فایلهای اصلی دیتابیس نوشته بشه، اطلاعات مربوط به اون تغییر ابتدا داخل یک Log ثبت میشه.
یعنی PostgreSQL اول تغییرات رو در فایلهای WAL ذخیره میکنه و فقط بعد از اون سراغ اعمال تغییر روی فایلهای اصلی دیتابیس میره.
به همین دلیل بهش میگن Write-Ahead Logging؛ یعنی «اول Log رو بنویس، بعد دادهی اصلی رو تغییر بده.»
چرا این کار مهمه؟ 📝
فرض کنید بعد از ثبت شدن Log، اما قبل از بهروزرسانی فایلهای اصلی، سرور Crash کنه.
در نگاه اول شاید به نظر برسه اطلاعات از بین رفتن.
اما اینطور نیست.
وقتی PostgreSQL دوباره اجرا میشه، فایلهای WAL رو بررسی میکنه و تمام تغییراتی که ثبت شده اما هنوز روی دیتابیس اعمال نشدن رو دوباره اجرا (Replay) میکنه.
در نتیجه، دیتابیس میتونه خودش رو به آخرین وضعیت صحیح برگردونه.
WAL فقط برای Crash Recovery نیست 🚀
خیلیها WAL رو فقط ابزاری برای بازیابی بعد از Crash میدونن.
در حالی که کاربردهای مهم دیگهای هم داره.
از جمله:
✅ Crash Recovery
✅ Streaming Replication
✅ Point-in-Time Recovery (PITR)
تقریباً تمام قابلیتهای مربوط به بازیابی اطلاعات و ساخت Replica در PostgreSQL، به نوعی به WAL وابسته هستن.
آیا این یعنی هر تغییر دوبار نوشته میشه؟🤔
تقریباً بله.
ابتدا تغییر داخل فایل WAL ثبت میشه.
بعد همون تغییر روی فایلهای اصلی دیتابیس اعمال میشه.
شاید در نگاه اول این کار هزینهی اضافه به نظر برسه، اما مزیتی که در حفظ یکپارچگی دادهها و امکان بازیابی سیستم ایجاد میکنه، ارزش این هزینه رو داره.
به همین دلیل WAL یکی از مهمترین دلایل قابل اعتماد بودن PostgreSQL در سیستمهای Production محسوب میشه.
ارتباط WAL با Transactionها🖇
وقتی یک Transaction رو Commit میکنیم، PostgreSQL فقط زمانی موفق بودن اون رو اعلام میکنه که رکوردهای مربوط به اون Transaction با موفقیت داخل WAL ثبت شده باشن.
این یعنی حتی اگر چند میلیثانیه بعد سرور خاموش بشه، PostgreSQL هنوز اطلاعات لازم برای بازیابی اون Transaction رو در اختیار داره.
به همین خاطر WAL یکی از ستونهای اصلی تضمین Durability در ویژگیهای ACID به حساب میاد.
WAL فقط یک فایل Log نیست.
در واقع، یکی از مهمترین تصمیمهای معماری PostgreSQLه که قابلیتهایی مثل Crash Recovery، Replication و Backupهای پیشرفته بر پایهی اون ساخته شدن.
➖➖➖➖➖➖➖➖➖➖
فرض کنید یک Transaction در حال انتقال پول بین دو حسابه.
ابتدا موجودی حساب اول کم میشه و بعد موجودی حساب دوم افزایش پیدا میکنه.
حالا تصور کنید درست بعد از انجام مرحلهی اول، برق سرور قطع بشه یا سیستم Crash کنه.
سؤال اینجاست:
🤔 از کجا معلوم دیتابیس بعد از روشن شدن دوباره، وضعیت درستی داشته باشه؟
اگر اطلاعات مستقیماً روی دیسک نوشته بشن، ممکنه فقط بخشی از تغییرات ذخیره شده باشه و دیتابیس در یک وضعیت ناسازگار (Inconsistent) قرار بگیره.
اینجاست که WAL (Write-Ahead Logging) وارد ماجرا میشه.💡
ایدهی اصلی WAL خیلی سادهست:
قبل از اینکه هر تغییری روی فایلهای اصلی دیتابیس نوشته بشه، اطلاعات مربوط به اون تغییر ابتدا داخل یک Log ثبت میشه.
یعنی PostgreSQL اول تغییرات رو در فایلهای WAL ذخیره میکنه و فقط بعد از اون سراغ اعمال تغییر روی فایلهای اصلی دیتابیس میره.
به همین دلیل بهش میگن Write-Ahead Logging؛ یعنی «اول Log رو بنویس، بعد دادهی اصلی رو تغییر بده.»
چرا این کار مهمه؟ 📝
فرض کنید بعد از ثبت شدن Log، اما قبل از بهروزرسانی فایلهای اصلی، سرور Crash کنه.
در نگاه اول شاید به نظر برسه اطلاعات از بین رفتن.
اما اینطور نیست.
وقتی PostgreSQL دوباره اجرا میشه، فایلهای WAL رو بررسی میکنه و تمام تغییراتی که ثبت شده اما هنوز روی دیتابیس اعمال نشدن رو دوباره اجرا (Replay) میکنه.
در نتیجه، دیتابیس میتونه خودش رو به آخرین وضعیت صحیح برگردونه.
WAL فقط برای Crash Recovery نیست 🚀
خیلیها WAL رو فقط ابزاری برای بازیابی بعد از Crash میدونن.
در حالی که کاربردهای مهم دیگهای هم داره.
از جمله:
✅ Crash Recovery
✅ Streaming Replication
✅ Point-in-Time Recovery (PITR)
تقریباً تمام قابلیتهای مربوط به بازیابی اطلاعات و ساخت Replica در PostgreSQL، به نوعی به WAL وابسته هستن.
آیا این یعنی هر تغییر دوبار نوشته میشه؟🤔
تقریباً بله.
ابتدا تغییر داخل فایل WAL ثبت میشه.
بعد همون تغییر روی فایلهای اصلی دیتابیس اعمال میشه.
شاید در نگاه اول این کار هزینهی اضافه به نظر برسه، اما مزیتی که در حفظ یکپارچگی دادهها و امکان بازیابی سیستم ایجاد میکنه، ارزش این هزینه رو داره.
به همین دلیل WAL یکی از مهمترین دلایل قابل اعتماد بودن PostgreSQL در سیستمهای Production محسوب میشه.
ارتباط WAL با Transactionها🖇
وقتی یک Transaction رو Commit میکنیم، PostgreSQL فقط زمانی موفق بودن اون رو اعلام میکنه که رکوردهای مربوط به اون Transaction با موفقیت داخل WAL ثبت شده باشن.
این یعنی حتی اگر چند میلیثانیه بعد سرور خاموش بشه، PostgreSQL هنوز اطلاعات لازم برای بازیابی اون Transaction رو در اختیار داره.
به همین خاطر WAL یکی از ستونهای اصلی تضمین Durability در ویژگیهای ACID به حساب میاد.
WAL فقط یک فایل Log نیست.
در واقع، یکی از مهمترین تصمیمهای معماری PostgreSQLه که قابلیتهایی مثل Crash Recovery، Replication و Backupهای پیشرفته بر پایهی اون ساخته شدن.
#️⃣ #Database #Backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
چرا بعضی object ها در Python hashable هستن و بعضیها نه؟ 🤔
اگر تا حالا با خطایی شبیه این مواجه شده باشین:
احتمالاً براتون سؤال پیش اومده که اصلاً hashable بودن یعنی چی؟
و مهمتر از اون، چرا پایتون برای بعضی object ها این محدودیت رو گذاشته؟
Hash چیه؟ 🧠
Hash یک عدد صحیحه که از روی وضعیت یک object تولید میشه.
توی پایتون میتونیم با تابع
نکته مهم اینجاست که Hash قرار نیست محتوای object رو ذخیره کنه یا بتونیم از روش داده اصلی رو بازسازی کنیم.
هدف Hash اینه که یک شناسه سریع و قابل اعتماد از وضعیت فعلی یک object تولید کنه.
Hashable بودن یعنی چی؟ 📦
یک object زمانی Hashable محسوب میشه که:
1. دارای مقدار Hash باشه.
2. مقدار Hash اون در طول عمر object تغییر نکنه.
3. رفتارش با متد
در واقع اگر دو object برابر باشن:
باید Hash یکسانی هم داشته باشن:
چرا همه object ها Hashable نیستند؟
اینجا مفهوم Immutable بودن اهمیت پیدا میکنه.
فرض کنید یک List قابل Hash شدن بود:
اگر Hash این object محاسبه بشه و بعد محتویات List تغییر کنه:
دیگر Hash قبلی نماینده وضعیت فعلی object نخواهد بود.
به همین دلیل اکثر object های Mutable مثل:
- list
- dict
- set
به صورت پیشفرض Hashable نیستن.
در مقابل Objectهای Immutable مثل:
- int
- str
- bytes
- tuple
معمولاً Hashable هستن.
چطور یک آبجکت Hashable بسازیم؟ 🛠
برای ساخت یک آبجکت Hashable باید دو متد رو تعریف کنیم:
مثلاً:
توی این مثال Hash بر اساس
نکته مهم اینه که فیلدی که برای Hash استفاده میکنیم نباید بعداً تغییر کنه؛ در غیر این صورت رفتار object غیرقابل پیشبینی میشه.
چرا Hashable بودن مهمه ؟ 💡
بخش بزرگی از ساختارهای داده و مکانیزمهای پرکاربرد پایتون بر پایه Hash ساخته شدن.
از جمله:
- dict
- set
- frozenset
- بسیاری از سیستمهای Cache و Memoization
وقتی یه hashable, object باشه، پایتون میتونه اون رو به سرعت پیدا، مقایسه و ذخیره کنه.
به همین دلیل Hashable بودن فقط یک ویژگی کوچیک توی Python Data Model نیست؛ بلکه یکی از مفاهیم بنیادیای هست که بخش بزرگی از Performance ساختارهای داده پایتون بر پایه اون شکل گرفته.
➖➖➖➖➖➖➖➖➖➖
اگر تا حالا با خطایی شبیه این مواجه شده باشین:
TypeError: unhashable type: 'list'
احتمالاً براتون سؤال پیش اومده که اصلاً hashable بودن یعنی چی؟
و مهمتر از اون، چرا پایتون برای بعضی object ها این محدودیت رو گذاشته؟
Hash چیه؟ 🧠
Hash یک عدد صحیحه که از روی وضعیت یک object تولید میشه.
توی پایتون میتونیم با تابع
hash() ببینیمش:hash("hello")
hash((1, 2, 3))نکته مهم اینجاست که Hash قرار نیست محتوای object رو ذخیره کنه یا بتونیم از روش داده اصلی رو بازسازی کنیم.
هدف Hash اینه که یک شناسه سریع و قابل اعتماد از وضعیت فعلی یک object تولید کنه.
Hashable بودن یعنی چی؟ 📦
یک object زمانی Hashable محسوب میشه که:
1. دارای مقدار Hash باشه.
2. مقدار Hash اون در طول عمر object تغییر نکنه.
3. رفتارش با متد
__eq__ سازگار باشه.در واقع اگر دو object برابر باشن:
a == b
باید Hash یکسانی هم داشته باشن:
hash(a) == hash(b)
چرا همه object ها Hashable نیستند؟
اینجا مفهوم Immutable بودن اهمیت پیدا میکنه.
فرض کنید یک List قابل Hash شدن بود:
items = [1, 2, 3]
اگر Hash این object محاسبه بشه و بعد محتویات List تغییر کنه:
items.append(4)
دیگر Hash قبلی نماینده وضعیت فعلی object نخواهد بود.
به همین دلیل اکثر object های Mutable مثل:
- list
- dict
- set
به صورت پیشفرض Hashable نیستن.
در مقابل Objectهای Immutable مثل:
- int
- str
- bytes
- tuple
معمولاً Hashable هستن.
چطور یک آبجکت Hashable بسازیم؟ 🛠
برای ساخت یک آبجکت Hashable باید دو متد رو تعریف کنیم:
__eq__
__hash__مثلاً:
class User:
def __init__(self, username):
self.username = username
def __eq__(self, other):
return self.username == other.username
def __hash__(self):
return hash(self.username)
توی این مثال Hash بر اساس
username تولید میشه.>>> denver = User("denver")
>>> hash(denver)
6843638685520863834
>>> نکته مهم اینه که فیلدی که برای Hash استفاده میکنیم نباید بعداً تغییر کنه؛ در غیر این صورت رفتار object غیرقابل پیشبینی میشه.
چرا Hashable بودن مهمه ؟ 💡
بخش بزرگی از ساختارهای داده و مکانیزمهای پرکاربرد پایتون بر پایه Hash ساخته شدن.
از جمله:
- dict
- set
- frozenset
- بسیاری از سیستمهای Cache و Memoization
وقتی یه hashable, object باشه، پایتون میتونه اون رو به سرعت پیدا، مقایسه و ذخیره کنه.
به همین دلیل Hashable بودن فقط یک ویژگی کوچیک توی Python Data Model نیست؛ بلکه یکی از مفاهیم بنیادیای هست که بخش بزرگی از Performance ساختارهای داده پایتون بر پایه اون شکل گرفته.
#️⃣ #Python #Programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
چرا در Python میتونیم Functionها رو مثل داده جابهجا کنیم؟ 🤔
بعضی از قابلیتهای پایتون اونقدر طبیعی به نظر میرسن که اصلاً بهشون فکر نمیکنیم.
مثلاً اینکه یک Function رو به Function دیگه پاس بدیم، داخل یک متغیر ذخیره کنیم یا حتی از یک Function برگردونیم.
اما پشت این سادگی، یکی از مهمترین ویژگیهای طراحی Python قرار گرفته.
Higher-Order Function یعنی چی؟ 🧠
یک Function زمانی Higher-Order محسوب میشه که حداقل یکی از این دو ویژگی رو داشته باشه:
- یک Function دیگه رو به عنوان آرگومان دریافت کنه.
- یک Function دیگه رو به عنوان خروجی برگردونه.
برای مثال:
در اینجا
بنابراین
چرا این امکان توی Python وجود داره؟ ⚙️
دلیلش اینه که توی پایتون، فانکشن ها First-Class Object هستن.
یعنی Functionها تقریباً مثل هر Object دیگهای رفتار میکنن:
میتونن داخل متغیر ذخیره بشن.
میتونن داخل Collectionها قرار بگیرن.
میتونن به Functionهای دیگه پاس داده بشن.
میتونن از Functionها برگردونده بشن.
مثلاً:
اینجا
این موضوع چه فایدهای داره؟ 💡
در نگاه اول ممکنه صرفاً یک قابلیت جالب به نظر برسه.
اما بخش بزرگی از قابلیتهای مدرن Python بر پایهی همین ایده ساخته شدن:
- Decoratorها
- Callbackها
- Strategy Pattern
- Event Handling
- Functional Programming Tools
در واقع وقتی Functionها هم مثل داده قابل جابهجایی باشن، میتونیم رفتار برنامه رو به شکل بسیار انعطافپذیرتری طراحی کنیم.
Decoratorها از کجا میان؟ 🎭
یکی از معروفترین کاربردهای Higher-Order Functionها، Decoratorها هستن.
Decoratorها در اصل Functionهایی هستن که یک Function دیگه رو دریافت میکنن و نسخهی جدیدی از اون رو برمیگردونن.
به همین دلیل فهم Decoratorها بدون درک Higher-Order Functionها معمولاً باعث میشه فقط نحوهی استفاده از اونها رو یاد بگیریم، نه دلیل کار کردنشون رو.
جمعبندی ✍️
Higher-Order Function ها نتیجه مسقتیم این هستن که فانشکن ها توی پایتون، First-Class Object محسوب میشن.
و همین ویژگی پایهی بسیاری از مفاهیم مهم زبان، از جمله Decoratorها، محسوب میشه.
گاهی پشت یک قابلیت ظاهراً ساده، یکی از بنیادیترین ایدههای طراحی زبان قرار گرفته.
📚 منبع این مطلب فصل Higher-Order Functions از کتاب Fluent Python هست.
➖➖➖➖➖➖➖➖➖➖
بعضی از قابلیتهای پایتون اونقدر طبیعی به نظر میرسن که اصلاً بهشون فکر نمیکنیم.
مثلاً اینکه یک Function رو به Function دیگه پاس بدیم، داخل یک متغیر ذخیره کنیم یا حتی از یک Function برگردونیم.
اما پشت این سادگی، یکی از مهمترین ویژگیهای طراحی Python قرار گرفته.
Higher-Order Function یعنی چی؟ 🧠
یک Function زمانی Higher-Order محسوب میشه که حداقل یکی از این دو ویژگی رو داشته باشه:
- یک Function دیگه رو به عنوان آرگومان دریافت کنه.
- یک Function دیگه رو به عنوان خروجی برگردونه.
برای مثال:
sorted(users, key=len)
در اینجا
len خودش یک فانکشن هست که به عنوان آرگومان به sorted پاس داده شده.بنابراین
sorted یک Higher-Order Function محسوب میشه.چرا این امکان توی Python وجود داره؟ ⚙️
دلیلش اینه که توی پایتون، فانکشن ها First-Class Object هستن.
یعنی Functionها تقریباً مثل هر Object دیگهای رفتار میکنن:
میتونن داخل متغیر ذخیره بشن.
میتونن داخل Collectionها قرار بگیرن.
میتونن به Functionهای دیگه پاس داده بشن.
میتونن از Functionها برگردونده بشن.
مثلاً:
def greet():
print("Hello")
handler = greet
اینجا
handler و greet به یک Function اشاره میکنن.این موضوع چه فایدهای داره؟ 💡
در نگاه اول ممکنه صرفاً یک قابلیت جالب به نظر برسه.
اما بخش بزرگی از قابلیتهای مدرن Python بر پایهی همین ایده ساخته شدن:
- Decoratorها
- Callbackها
- Strategy Pattern
- Event Handling
- Functional Programming Tools
در واقع وقتی Functionها هم مثل داده قابل جابهجایی باشن، میتونیم رفتار برنامه رو به شکل بسیار انعطافپذیرتری طراحی کنیم.
Decoratorها از کجا میان؟ 🎭
یکی از معروفترین کاربردهای Higher-Order Functionها، Decoratorها هستن.
Decoratorها در اصل Functionهایی هستن که یک Function دیگه رو دریافت میکنن و نسخهی جدیدی از اون رو برمیگردونن.
به همین دلیل فهم Decoratorها بدون درک Higher-Order Functionها معمولاً باعث میشه فقط نحوهی استفاده از اونها رو یاد بگیریم، نه دلیل کار کردنشون رو.
جمعبندی ✍️
Higher-Order Function ها نتیجه مسقتیم این هستن که فانشکن ها توی پایتون، First-Class Object محسوب میشن.
و همین ویژگی پایهی بسیاری از مفاهیم مهم زبان، از جمله Decoratorها، محسوب میشه.
گاهی پشت یک قابلیت ظاهراً ساده، یکی از بنیادیترین ایدههای طراحی زبان قرار گرفته.
📚 منبع این مطلب فصل Higher-Order Functions از کتاب Fluent Python هست.
#️⃣ #Python #Programming #FluentPython
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
اگر جدول PostgreSQL حجیم بشه و عملکردش افت کنه، PostgreSQL چیکار میکنه؟🐘
فرض کنید داریم روی یک جدول حساب کاربری کار میکنیم. هر روز کلی UPDATE و DELETE انجام میشه. موجودی تغییر میکنه، سفارش لغو میشه، رکورد پاک میشه...
تو PostgreSQL به خاطر MVCC، وقتی یه رکورد آپدیت یا حذف میشه، نسخهی قدیمیش فوراً حذف نمیشه. یه نسخهی جدید ساخته میشه و نسخهی قدیمی به عنوان تاپل مرده (Dead Tuple) تو جدول میمونه. با گذشت زمان این تاپلهای مرده جمع میشن و جدول و ایندکسها حجیم میشن، حجمشون زیاد میشه ولی دادهی واقعی زیاد نشده.
حالا تصور کنیم بعد از چند ماه جدولمون خیلی بزرگتر از چیزی که باید باشه شده، کوئریها کند شدن، حافظه بیشتر مصرف میشه و سیستم سنگین شده.
سؤال اینجاست:
🤔 PostgreSQL چطور این بهمریختگی رو تمیز میکنه و فضا رو پس میگیره؟
اینجاست که VACUUM وارد ماجرا میشه. 💡
ایدهی VACUUM خیلی سادهست:
قبل از اینکه جدول کاملاً حجم بلا استفاده و فضای مفید رو از دست بدیم، PostgreSQL با VACUUM میاد تاپلهای مرده رو پیدا میکنه، علامت میزنه که فضاشون آزاد شده و قابل استفاده مجدد هست. یعنی فضایی که قبلاً توسط رکوردهای قدیمی اشغال شده بود رو دوباره در اختیار دیتابیس قرار میده.
به همین دلیل بهش میگن Vacuum؛ یعنی «جاروبرقی کشیدن» دیتابیس!
چرا این کار مهمه؟ 📝
بدون VACUUM، جدول و ایندکسها به مرور متورم میشن (Table Bloat). این تورم باعث میشه اسکن جدول کندتر بشه، حافظه بیشتر مصرف بشه و در نهایت عملکرد کلی سیستم افت کنه. VACUUM با تمیز کردن این فضاهای مرده، جدول رو سبک و سریع نگه میداره.
هرچند وظیفهی اصلیش بازیابی فضاست، اما مزایای مهم دیگهای هم داره:
✅ آزاد کردن فضای اشغالشده توسط نسخههای قدیمی رکوردها
✅ کمک به بهروزرسانی آمار جدول برای اینکه پلنر کوئری بهتر تصمیم بگیره
✅ حفظ سلامت دیتابیس در بلندمدت
VACUUM فقط برای تمیزکاری نیست 🚀
تقریباً همه سیستمهای بزرگ PostgreSQL به VACUUM وابسته هستن تا بتونن سالها بدون افت عملکرد کار کنن. PostgreSQL حتی یه مکانیزم هوشمند به اسم Autovacuum داره که خودش به صورت خودکار تشخیص میده کی باید بیاد تمیزکاری کنه.
ارتباط VACUUM با Transactionها 🖇
VACUUM خیلی خوب با مکانیزم تراکنش و MVCC کار میکنه. وقتی تراکنشها تموم بشن و دیگه به نسخههای قدیمی نیاز نباشه، VACUUM میاد و اون فضاها رو جمع میکنه. این یعنی حتی تو محیطهای پرتراکنش هم دیتابیس میتونه خودش رو تمیز نگه داره.
➖➖➖➖➖➖➖➖➖➖
فرض کنید داریم روی یک جدول حساب کاربری کار میکنیم. هر روز کلی UPDATE و DELETE انجام میشه. موجودی تغییر میکنه، سفارش لغو میشه، رکورد پاک میشه...
تو PostgreSQL به خاطر MVCC، وقتی یه رکورد آپدیت یا حذف میشه، نسخهی قدیمیش فوراً حذف نمیشه. یه نسخهی جدید ساخته میشه و نسخهی قدیمی به عنوان تاپل مرده (Dead Tuple) تو جدول میمونه. با گذشت زمان این تاپلهای مرده جمع میشن و جدول و ایندکسها حجیم میشن، حجمشون زیاد میشه ولی دادهی واقعی زیاد نشده.
حالا تصور کنیم بعد از چند ماه جدولمون خیلی بزرگتر از چیزی که باید باشه شده، کوئریها کند شدن، حافظه بیشتر مصرف میشه و سیستم سنگین شده.
سؤال اینجاست:
🤔 PostgreSQL چطور این بهمریختگی رو تمیز میکنه و فضا رو پس میگیره؟
اینجاست که VACUUM وارد ماجرا میشه. 💡
ایدهی VACUUM خیلی سادهست:
قبل از اینکه جدول کاملاً حجم بلا استفاده و فضای مفید رو از دست بدیم، PostgreSQL با VACUUM میاد تاپلهای مرده رو پیدا میکنه، علامت میزنه که فضاشون آزاد شده و قابل استفاده مجدد هست. یعنی فضایی که قبلاً توسط رکوردهای قدیمی اشغال شده بود رو دوباره در اختیار دیتابیس قرار میده.
به همین دلیل بهش میگن Vacuum؛ یعنی «جاروبرقی کشیدن» دیتابیس!
چرا این کار مهمه؟ 📝
بدون VACUUM، جدول و ایندکسها به مرور متورم میشن (Table Bloat). این تورم باعث میشه اسکن جدول کندتر بشه، حافظه بیشتر مصرف بشه و در نهایت عملکرد کلی سیستم افت کنه. VACUUM با تمیز کردن این فضاهای مرده، جدول رو سبک و سریع نگه میداره.
هرچند وظیفهی اصلیش بازیابی فضاست، اما مزایای مهم دیگهای هم داره:
✅ آزاد کردن فضای اشغالشده توسط نسخههای قدیمی رکوردها
✅ کمک به بهروزرسانی آمار جدول برای اینکه پلنر کوئری بهتر تصمیم بگیره
✅ حفظ سلامت دیتابیس در بلندمدت
VACUUM فقط برای تمیزکاری نیست 🚀
تقریباً همه سیستمهای بزرگ PostgreSQL به VACUUM وابسته هستن تا بتونن سالها بدون افت عملکرد کار کنن. PostgreSQL حتی یه مکانیزم هوشمند به اسم Autovacuum داره که خودش به صورت خودکار تشخیص میده کی باید بیاد تمیزکاری کنه.
ارتباط VACUUM با Transactionها 🖇
VACUUM خیلی خوب با مکانیزم تراکنش و MVCC کار میکنه. وقتی تراکنشها تموم بشن و دیگه به نسخههای قدیمی نیاز نباشه، VACUUM میاد و اون فضاها رو جمع میکنه. این یعنی حتی تو محیطهای پرتراکنش هم دیتابیس میتونه خودش رو تمیز نگه داره.
#️⃣ #Database #Backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
Caching Patternها دقیقاً چی هستن؟ 🧠
وقتی صحبت از Cache میشه، معمولاً ذهنمون میره سمت ابزارهایی مثل Redis یا Memcached.
اما انتخاب تکنولوژی فقط بخشی از ماجراست.
سؤال مهمتر اینه که: Application قراره چطور با Cache و Database تعامل داشته باشه؟
پاسخ این سؤال رو Caching Patternها مشخص میکنن.
در واقع، Caching Patternها استراتژیهایی هستن که نحوهی خواندن و نوشتن داده بین Application، Cache و Database رو تعیین میکنن.
انتخاب Pattern مناسب میتونه روی Performance، Data Consistency، Latency و حتی پیچیدگی سیستم تأثیر زیادی داشته باشه.
1.Cache-Aside (Lazy Loading) 📦
رایجترین الگوی کشینگ.
توی این الگو Application خودش مسئول مدیریت ارتباط بین Cache و Database هست.
- Read Flow
اول Cache بررسی میشه.
اگه داده وجود داشته باشه (Cache Hit)، همون برگردونده میشه.
اگه وجود نداشته باشه (Cache Miss)، داده از Database خونده میشه، داخل Cache قرار میگیره و سپس به کاربر برگردونده میشه.
- Write Flow
داده مستقیماً در Database بهروزرسانی میشه.
سپس مقدار مربوطه توی Cache حذف یا Invalid میشه.
اولین درخواست بعدی دوباره داده رو از Database میخونه و Cache رو بهروز میکنه.
- مناسب برای
سیستمهایی که تعداد عملیات Read بسیار بیشتر از Write هست.
2.Read-Through 📖
توی این الگو Application مستقیماً با Database ارتباطی نداره.
اگر داده داخل Cache نباشه، خود Cache مسئول دریافت اون از Database، ذخیره کردن و برگردوندن داده به Application هست.
در نتیجه منطق بارگذاری داده از دوش Application برداشته میشه.
- مناسب برای
سیستمهایی که میخوان مدیریت Cache تا حد ممکن از منطق برنامه جدا باشه.
3.Write-Through ✍️
توی این الگو، هر بار که دادهای نوشته میشه، هم Cache و هم Database بهصورت همزمان بهروزرسانی میشن.
به همین دلیل، دادهی موجود در Cache همیشه با Database همگام باقی میمونه.
- مناسب برای
سیستمهایی که تحمل خواندن دادهی قدیمی (Stale Data) رو ندارن.
4.Write-Behind (Write-Back) 🚀
توی این Pattern، اول داده داخل Cache نوشته میشه و پاسخ بلافاصله به Application برگردونده میشه.
توی Cache توی پسزمینه (Asynchronously) تغییرات رو در Database ذخیره میکنه.
این کار باعث میشه عملیات Write با سرعت بسیار بیشتری انجام بشه.
- مناسب برای
سیستمهای Write-Heavy که Performance عملیات نوشتن در اونها اهمیت زیادی داره.
5.Write-Around 🔄
توی این روش، عملیات Write کاملاً Cache رو دور میزنه.
داده مستقیماً داخل Database ذخیره میشه و فقط زمانی وارد Cache میشه که بعداً یک عملیات Read روی اون انجام بشه.
این کار از پر شدن Cache با دادههایی که احتمالاً فقط یکبار نوشته میشن جلوگیری میکنه.
- مناسب برای
دادههایی که بهندرت بعد از نوشتن دوباره خوانده میشن.
Cache Eviction Policy 🗑
هر Cache ظرفیت محدودی داره.
وقتی این ظرفیت پر بشه، باید تصمیم بگیره کدوم داده حذف بشه تا برای دادههای جدید جا باز بشه.
رایجترین Eviction Policyها:
1. TTL (Time-To-Live): هر داده مدت زمان مشخصی اعتبار داره و بعد از اون منقضی میشه.
2. LRU (Least Recently Used): دادهای حذف میشه که مدت بیشتری استفاده نشده.
3. LFU (Least Frequently Used): دادهای حذف میشه که کمترین تعداد دفعات استفاده رو داشته.
4. FIFO (First In, First Out): قدیمیترین داده، بدون توجه به میزان استفاده، حذف میشه.
جمعبندی 💡
هیچ Caching Patternی بهترین انتخاب برای همهی سیستمها نیست.
هر کدوم برای حل یک مسئله طراحی شدن و انتخاب بین اونها به نیازهای سیستم، میزان Read و Write، حساسیت به Consistency و محدودیتهای Performance بستگی داره.
درک این Patternها کمک میکنه قبل از انتخاب ابزارهایی مثل Redis، بدونیم دقیقاً چه رفتاری از سیستم انتظار داریم.
📚 این مطلب مروری بر رایجترین Caching Patternها و Cache Eviction Policyها در طراحی سیستمهای Backend بود.
➖➖➖➖➖➖➖➖➖➖
وقتی صحبت از Cache میشه، معمولاً ذهنمون میره سمت ابزارهایی مثل Redis یا Memcached.
اما انتخاب تکنولوژی فقط بخشی از ماجراست.
سؤال مهمتر اینه که: Application قراره چطور با Cache و Database تعامل داشته باشه؟
پاسخ این سؤال رو Caching Patternها مشخص میکنن.
در واقع، Caching Patternها استراتژیهایی هستن که نحوهی خواندن و نوشتن داده بین Application، Cache و Database رو تعیین میکنن.
انتخاب Pattern مناسب میتونه روی Performance، Data Consistency، Latency و حتی پیچیدگی سیستم تأثیر زیادی داشته باشه.
1.Cache-Aside (Lazy Loading) 📦
رایجترین الگوی کشینگ.
توی این الگو Application خودش مسئول مدیریت ارتباط بین Cache و Database هست.
- Read Flow
اول Cache بررسی میشه.
اگه داده وجود داشته باشه (Cache Hit)، همون برگردونده میشه.
اگه وجود نداشته باشه (Cache Miss)، داده از Database خونده میشه، داخل Cache قرار میگیره و سپس به کاربر برگردونده میشه.
- Write Flow
داده مستقیماً در Database بهروزرسانی میشه.
سپس مقدار مربوطه توی Cache حذف یا Invalid میشه.
اولین درخواست بعدی دوباره داده رو از Database میخونه و Cache رو بهروز میکنه.
- مناسب برای
سیستمهایی که تعداد عملیات Read بسیار بیشتر از Write هست.
2.Read-Through 📖
توی این الگو Application مستقیماً با Database ارتباطی نداره.
اگر داده داخل Cache نباشه، خود Cache مسئول دریافت اون از Database، ذخیره کردن و برگردوندن داده به Application هست.
در نتیجه منطق بارگذاری داده از دوش Application برداشته میشه.
- مناسب برای
سیستمهایی که میخوان مدیریت Cache تا حد ممکن از منطق برنامه جدا باشه.
3.Write-Through ✍️
توی این الگو، هر بار که دادهای نوشته میشه، هم Cache و هم Database بهصورت همزمان بهروزرسانی میشن.
به همین دلیل، دادهی موجود در Cache همیشه با Database همگام باقی میمونه.
- مناسب برای
سیستمهایی که تحمل خواندن دادهی قدیمی (Stale Data) رو ندارن.
4.Write-Behind (Write-Back) 🚀
توی این Pattern، اول داده داخل Cache نوشته میشه و پاسخ بلافاصله به Application برگردونده میشه.
توی Cache توی پسزمینه (Asynchronously) تغییرات رو در Database ذخیره میکنه.
این کار باعث میشه عملیات Write با سرعت بسیار بیشتری انجام بشه.
- مناسب برای
سیستمهای Write-Heavy که Performance عملیات نوشتن در اونها اهمیت زیادی داره.
5.Write-Around 🔄
توی این روش، عملیات Write کاملاً Cache رو دور میزنه.
داده مستقیماً داخل Database ذخیره میشه و فقط زمانی وارد Cache میشه که بعداً یک عملیات Read روی اون انجام بشه.
این کار از پر شدن Cache با دادههایی که احتمالاً فقط یکبار نوشته میشن جلوگیری میکنه.
- مناسب برای
دادههایی که بهندرت بعد از نوشتن دوباره خوانده میشن.
Cache Eviction Policy 🗑
هر Cache ظرفیت محدودی داره.
وقتی این ظرفیت پر بشه، باید تصمیم بگیره کدوم داده حذف بشه تا برای دادههای جدید جا باز بشه.
رایجترین Eviction Policyها:
1. TTL (Time-To-Live): هر داده مدت زمان مشخصی اعتبار داره و بعد از اون منقضی میشه.
2. LRU (Least Recently Used): دادهای حذف میشه که مدت بیشتری استفاده نشده.
3. LFU (Least Frequently Used): دادهای حذف میشه که کمترین تعداد دفعات استفاده رو داشته.
4. FIFO (First In, First Out): قدیمیترین داده، بدون توجه به میزان استفاده، حذف میشه.
جمعبندی 💡
هیچ Caching Patternی بهترین انتخاب برای همهی سیستمها نیست.
هر کدوم برای حل یک مسئله طراحی شدن و انتخاب بین اونها به نیازهای سیستم، میزان Read و Write، حساسیت به Consistency و محدودیتهای Performance بستگی داره.
درک این Patternها کمک میکنه قبل از انتخاب ابزارهایی مثل Redis، بدونیم دقیقاً چه رفتاری از سیستم انتظار داریم.
📚 این مطلب مروری بر رایجترین Caching Patternها و Cache Eviction Policyها در طراحی سیستمهای Backend بود.
#️⃣ #SystemDesign #Backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
چرا نباید از Mutableها به عنوان Default در Python استفاده کنیم؟ 🤔
توی Python، مقدارهای default آرگومانهای تابع فقط یکبار و در زمان تعریف تابع ساخته میشن، نه هر بار که تابع اجرا بشه. این یعنی اگر از یک object mutable مثل list یا dict به عنوان مقدار پیشفرض استفاده کنیم، اون object بین تمام callهای تابع shared باقی میمونه.
این موضوع در ظاهر شاید مشکلی ایجاد نکنه، اما وقتی تابع چند بار صدا زده میشه، همون state قبلی حفظ میشه و روی نتیجهی callهای بعدی تأثیر میذاره. در واقع تابع به جای اینکه مستقل و بدون وابستگی به گذشته باشه، وارد یک وضعیت stateful ناخواسته میشه.
ریشهی این رفتار به این برمیگرده که پایتون، default argumentها رو در زمان تعریف تابع evaluate میکنه، نه در زمان اجرا. برای همین object مربوط به default، ثابت میمونه و فقط mutate میشه، نه اینکه دوباره ساخته بشه.
به همین دلیل استفاده از mutableها به عنوان default میتونه باعث bugهای خیلی subtle و سختدیباگ بشه. راه استاندارد اینه که به جای اونها از None استفاده کنیم و داخل تابع object جدید بسازیم تا هر بار یک state کاملاً مستقل داشته باشیم.
➖➖➖➖➖➖➖➖➖➖
توی Python، مقدارهای default آرگومانهای تابع فقط یکبار و در زمان تعریف تابع ساخته میشن، نه هر بار که تابع اجرا بشه. این یعنی اگر از یک object mutable مثل list یا dict به عنوان مقدار پیشفرض استفاده کنیم، اون object بین تمام callهای تابع shared باقی میمونه.
این موضوع در ظاهر شاید مشکلی ایجاد نکنه، اما وقتی تابع چند بار صدا زده میشه، همون state قبلی حفظ میشه و روی نتیجهی callهای بعدی تأثیر میذاره. در واقع تابع به جای اینکه مستقل و بدون وابستگی به گذشته باشه، وارد یک وضعیت stateful ناخواسته میشه.
ریشهی این رفتار به این برمیگرده که پایتون، default argumentها رو در زمان تعریف تابع evaluate میکنه، نه در زمان اجرا. برای همین object مربوط به default، ثابت میمونه و فقط mutate میشه، نه اینکه دوباره ساخته بشه.
به همین دلیل استفاده از mutableها به عنوان default میتونه باعث bugهای خیلی subtle و سختدیباگ بشه. راه استاندارد اینه که به جای اونها از None استفاده کنیم و داخل تابع object جدید بسازیم تا هر بار یک state کاملاً مستقل داشته باشیم.
#️⃣ #Python #Programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
ایندکس در PostgreSQL دقیقاً یعنی چی؟ 🤔
وقتی دربارهی performance دیتابیس صحبت میکنیم، یکی از اولین مفاهیمی که وسط میاد Index هست.
ایندکس رو میشه شبیه یه ساختار کمکی در نظر گرفت که به دیتابیس کمک میکنه به جای اینکه برای پیدا کردن یک رکورد کل جدول رو scan کنه، سریعتر به دادهی مورد نظر برسه.
اما نکته مهم اینه که Index فقط “سرعت بیشتر” نیست؛ در واقع یک trade-off بین سرعت خواندن و هزینه نوشتن + نگهداری ساختار داده هست.
هر ایندکس باعث میشه عملیات SELECT سریعتر بشه، اما در عوض INSERT و UPDATE و DELETE سنگینتر بشن، چون دیتابیس باید اون ساختار کمکی رو هم بهروز نگه داره.
B-Tree Index 📚
رایجترین و پیشفرضترین نوع ایندکس در PostgreSQL، B-Tree هست.
این نوع ایندکس برای دادههایی طراحی شده که قابل مرتبسازی هستن (مثل عدد، تاریخ یا حتی string). ساختارش به شکلیه که جستجو، range query و sorting رو خیلی سریع میکنه.
به همین دلیل، وقتی روی یک ستون ساده مثل
اما هزینهاش اینه که هر تغییر روی داده باید در ساختار درختی ایندکس هم اعمال بشه، که توی سیستم های write-heavy میتونه فشار ایجاد کنه.
Hash Index 🔑
Hash Index بیشتر برای equality check طراحی شده؛ یعنی وقتی دقیقاً دنبال یک مقدار مشخص هستیم.
در این حالت، به جای ساختار درختی، از یک hash function استفاده میشه تا مستقیم به موقعیت داده برسیم.
اما محدودیت مهمش اینه که برای range query یا مرتبسازی کاربرد نداره، و به همین دلیل در عمل کمتر از B-Tree استفاده میشه. حتی در PostgreSQL هم تا مدتها محدودیتهای خاص خودش رو داشت و هنوز هم use-caseهای محدودی داره.
GIN Index (Generalized Inverted Index) 🔍
GIN یکی از جذابترین ایندکسهای PostgreSQL هست، مخصوصاً برای دادههای غیرساختیافته.
این نوع ایندکس بیشتر برای حالتهایی استفاده میشه که یک رکورد شامل چندین value هست؛ مثل JSONB، arrayها یا full-text search.
به جای اینکه کل row رو index کنه، GIN بر اساس “اجزای داخلی داده” عمل میکنه. یعنی هر token یا key رو جداگانه index میکنه و همین باعث میشه برای search روی دادههای پیچیده خیلی سریع باشه.
اما هزینهاش اینه که ساخت و update کردنش نسبت به B-Tree سنگینتره، چون ساختار داخلی پیچیدهتری داره.
GiST Index 🌳
GiST یک framework برای ساخت ایندکسهای سفارشیتره، نه یک ساختار ثابت.
PostgreSQL ازش برای دادههایی استفاده میکنه که ساختار غیرمعمول دارن؛ مثل geometry data، range types یا حتی full-text search در برخی سناریوها.
ویژگی مهم GiST اینه که انعطاف پذیر هست، یعنی میتونه برای انواع مختلف داده گسترش داده بشه، اما در عوض دقت و performance اون کاملاً به implementation و نوع داده بستگی داره.
BRIN Index 📦
BRIN برای جدولهای خیلی بزرگ طراحی شده؛ جایی که دادهها به صورت طبیعی روی disk مرتب هستن (مثلاً بر اساس زمان).
به جای اینکه تکتک rowها رو index کنه، BRIN فقط summary از blockهای داده نگه میداره. همین باعث میشه بسیار سبک و کمهزینه باشه.
اما trade-off اینه که دقتش کمتره و بیشتر برای queryهای range روی دادههای بزرگ و append-only مناسب هست، نه جستجوی دقیق.
جمعبندی 🧩
Indexها در PostgreSQL فقط ابزار سرعت نیستن؛ در واقع یک انتخاب طراحی هستن.
هر نوع ایندکس یک trade-off مشخص داره:
B-Tree برای general use
Hash برای equality ساده
GIN برای دادههای پیچیده و full-text
GiST برای سناریوهای قابل توسعه و خاص
BRIN برای دیتاستهای خیلی بزرگ و ordered
درک درست این تفاوتها باعث میشه قبل از اینکه “ایندکس بذاریم”، دقیق بدونیم داریم چه هزینهای رو از سیستم میگیریم و چه چیزی به دست میاریم.
➖➖➖➖➖➖➖➖➖➖
وقتی دربارهی performance دیتابیس صحبت میکنیم، یکی از اولین مفاهیمی که وسط میاد Index هست.
ایندکس رو میشه شبیه یه ساختار کمکی در نظر گرفت که به دیتابیس کمک میکنه به جای اینکه برای پیدا کردن یک رکورد کل جدول رو scan کنه، سریعتر به دادهی مورد نظر برسه.
اما نکته مهم اینه که Index فقط “سرعت بیشتر” نیست؛ در واقع یک trade-off بین سرعت خواندن و هزینه نوشتن + نگهداری ساختار داده هست.
هر ایندکس باعث میشه عملیات SELECT سریعتر بشه، اما در عوض INSERT و UPDATE و DELETE سنگینتر بشن، چون دیتابیس باید اون ساختار کمکی رو هم بهروز نگه داره.
B-Tree Index 📚
رایجترین و پیشفرضترین نوع ایندکس در PostgreSQL، B-Tree هست.
این نوع ایندکس برای دادههایی طراحی شده که قابل مرتبسازی هستن (مثل عدد، تاریخ یا حتی string). ساختارش به شکلیه که جستجو، range query و sorting رو خیلی سریع میکنه.
به همین دلیل، وقتی روی یک ستون ساده مثل
id یا created_at کوئری میزنیم، معمولاً B-Tree اولین انتخاب دیتابیس هست. چون هم برای equality search خوبه، هم برای rangeها.اما هزینهاش اینه که هر تغییر روی داده باید در ساختار درختی ایندکس هم اعمال بشه، که توی سیستم های write-heavy میتونه فشار ایجاد کنه.
Hash Index 🔑
Hash Index بیشتر برای equality check طراحی شده؛ یعنی وقتی دقیقاً دنبال یک مقدار مشخص هستیم.
در این حالت، به جای ساختار درختی، از یک hash function استفاده میشه تا مستقیم به موقعیت داده برسیم.
اما محدودیت مهمش اینه که برای range query یا مرتبسازی کاربرد نداره، و به همین دلیل در عمل کمتر از B-Tree استفاده میشه. حتی در PostgreSQL هم تا مدتها محدودیتهای خاص خودش رو داشت و هنوز هم use-caseهای محدودی داره.
GIN Index (Generalized Inverted Index) 🔍
GIN یکی از جذابترین ایندکسهای PostgreSQL هست، مخصوصاً برای دادههای غیرساختیافته.
این نوع ایندکس بیشتر برای حالتهایی استفاده میشه که یک رکورد شامل چندین value هست؛ مثل JSONB، arrayها یا full-text search.
به جای اینکه کل row رو index کنه، GIN بر اساس “اجزای داخلی داده” عمل میکنه. یعنی هر token یا key رو جداگانه index میکنه و همین باعث میشه برای search روی دادههای پیچیده خیلی سریع باشه.
اما هزینهاش اینه که ساخت و update کردنش نسبت به B-Tree سنگینتره، چون ساختار داخلی پیچیدهتری داره.
GiST Index 🌳
GiST یک framework برای ساخت ایندکسهای سفارشیتره، نه یک ساختار ثابت.
PostgreSQL ازش برای دادههایی استفاده میکنه که ساختار غیرمعمول دارن؛ مثل geometry data، range types یا حتی full-text search در برخی سناریوها.
ویژگی مهم GiST اینه که انعطاف پذیر هست، یعنی میتونه برای انواع مختلف داده گسترش داده بشه، اما در عوض دقت و performance اون کاملاً به implementation و نوع داده بستگی داره.
BRIN Index 📦
BRIN برای جدولهای خیلی بزرگ طراحی شده؛ جایی که دادهها به صورت طبیعی روی disk مرتب هستن (مثلاً بر اساس زمان).
به جای اینکه تکتک rowها رو index کنه، BRIN فقط summary از blockهای داده نگه میداره. همین باعث میشه بسیار سبک و کمهزینه باشه.
اما trade-off اینه که دقتش کمتره و بیشتر برای queryهای range روی دادههای بزرگ و append-only مناسب هست، نه جستجوی دقیق.
جمعبندی 🧩
Indexها در PostgreSQL فقط ابزار سرعت نیستن؛ در واقع یک انتخاب طراحی هستن.
هر نوع ایندکس یک trade-off مشخص داره:
B-Tree برای general use
Hash برای equality ساده
GIN برای دادههای پیچیده و full-text
GiST برای سناریوهای قابل توسعه و خاص
BRIN برای دیتاستهای خیلی بزرگ و ordered
درک درست این تفاوتها باعث میشه قبل از اینکه “ایندکس بذاریم”، دقیق بدونیم داریم چه هزینهای رو از سیستم میگیریم و چه چیزی به دست میاریم.
#️⃣ #Backend #Database
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
🔥1
Idempotency در API یعنی چی؟ 🤔
اگر بخوایم خیلی ساده شروع کنیم، Idempotency یعنی:
یعنی مهم نیست این عملیات یک بار اجرا بشه یا ده بار؛ در نهایت باید سیستم به یک state یکسان برسه.
این مفهوم بیشتر از اینکه “ویژگی خوشگل API” باشه، یک ضرورت در سیستمهای واقعیه؛ مخصوصاً جایی که شبکه unreliable هست و retry اتفاق میافته.
مشکل از کجا شروع میشه؟ 🧠
در دنیای واقعی، درخواستها همیشه “یک بار و دقیق” اجرا نمیشن.
ممکنه:
درخواست به سرور برسه ولی response برنگرده
کلاینت retry کنه بدون اینکه بدونه عملیات قبلی موفق بوده یا نه
یا حتی شبکه وسط کار قطع بشه
حالا اگر API ما idempotent نباشه، هر retry میتونه یک side effect جدید بسازه. مثلاً یک پرداخت دوبار انجام بشه، یا یک رکورد چند بار ایجاد بشه.
Idempotency چطور این مشکل رو حل میکنه؟ ⚙️
ایدهی اصلی اینه که سرور باید بتونه تشخیص بده این درخواست قبلاً انجام شده یا نه.
اگر قبلاً انجام شده باشه، به جای اجرای دوبارهی logic، همون نتیجه قبلی رو برمیگردونه.
برای همین خیلی از سیستمها از چیزی مثل idempotency key استفاده میکنن؛ یک شناسه که کلاینت همراه request میفرسته تا سرور بفهمه این عملیات تکراریه یا جدید.
همهی عملیاتها idempotent نیستن 💡
بعضی عملیاتها ذاتاً idempotent هستن، بعضیها نه.
مثلاً عملیاتهایی مثل GET معمولاً idempotent هستن چون فقط داده رو میخونن.
اما عملیاتهایی مثل POST به صورت پیشفرض idempotent نیستن، چون هر بار میتونن یک resource جدید بسازن.
برای همین در API design باید حواسمون باشه کجا داریم state رو تغییر میدیم و چطور از duplicate execution جلوگیری کنیم.
چرا این موضوع مهمه؟ 🚧
Idempotency یکی از اون مفاهیمیه که شاید اولش خیلی ساده به نظر بیاد، اما توی سیستمهای واقعی حیاتی میشه.
چون بدونش:
retry logic خطرناک میشه
پرداختها ممکنه دوبار انجام بشن
دادهها duplicate میشن
و debugging سیستم خیلی سختتر میشه
در واقع هر جا پای شبکه وسیستم های توزیع شده وسط باشه، idempotency تبدیل به یک پیش نیاز جدی میشه، نه یک انتخاب.
جمع بندی ✍️
Idempotency یعنی اینکه تکرار یک operation، نتیجهی نهایی سیستم رو تغییر نده.
این مفهوم کمک میکنه سیستمها در برابر retry، خطاهای شبکه و عدم قطعیت در اجرای requestها مقاومتر بشن.
➖➖➖➖➖➖➖➖➖➖
اگر بخوایم خیلی ساده شروع کنیم، Idempotency یعنی:
اگر یک درخواست رو چند بار پشت سر هم اجرا کنیم، نتیجه نهایی سیستم تغییر نکنه.
یعنی مهم نیست این عملیات یک بار اجرا بشه یا ده بار؛ در نهایت باید سیستم به یک state یکسان برسه.
این مفهوم بیشتر از اینکه “ویژگی خوشگل API” باشه، یک ضرورت در سیستمهای واقعیه؛ مخصوصاً جایی که شبکه unreliable هست و retry اتفاق میافته.
مشکل از کجا شروع میشه؟ 🧠
در دنیای واقعی، درخواستها همیشه “یک بار و دقیق” اجرا نمیشن.
ممکنه:
درخواست به سرور برسه ولی response برنگرده
کلاینت retry کنه بدون اینکه بدونه عملیات قبلی موفق بوده یا نه
یا حتی شبکه وسط کار قطع بشه
حالا اگر API ما idempotent نباشه، هر retry میتونه یک side effect جدید بسازه. مثلاً یک پرداخت دوبار انجام بشه، یا یک رکورد چند بار ایجاد بشه.
Idempotency چطور این مشکل رو حل میکنه؟ ⚙️
ایدهی اصلی اینه که سرور باید بتونه تشخیص بده این درخواست قبلاً انجام شده یا نه.
اگر قبلاً انجام شده باشه، به جای اجرای دوبارهی logic، همون نتیجه قبلی رو برمیگردونه.
برای همین خیلی از سیستمها از چیزی مثل idempotency key استفاده میکنن؛ یک شناسه که کلاینت همراه request میفرسته تا سرور بفهمه این عملیات تکراریه یا جدید.
همهی عملیاتها idempotent نیستن 💡
بعضی عملیاتها ذاتاً idempotent هستن، بعضیها نه.
مثلاً عملیاتهایی مثل GET معمولاً idempotent هستن چون فقط داده رو میخونن.
اما عملیاتهایی مثل POST به صورت پیشفرض idempotent نیستن، چون هر بار میتونن یک resource جدید بسازن.
برای همین در API design باید حواسمون باشه کجا داریم state رو تغییر میدیم و چطور از duplicate execution جلوگیری کنیم.
چرا این موضوع مهمه؟ 🚧
Idempotency یکی از اون مفاهیمیه که شاید اولش خیلی ساده به نظر بیاد، اما توی سیستمهای واقعی حیاتی میشه.
چون بدونش:
retry logic خطرناک میشه
پرداختها ممکنه دوبار انجام بشن
دادهها duplicate میشن
و debugging سیستم خیلی سختتر میشه
در واقع هر جا پای شبکه وسیستم های توزیع شده وسط باشه، idempotency تبدیل به یک پیش نیاز جدی میشه، نه یک انتخاب.
جمع بندی ✍️
Idempotency یعنی اینکه تکرار یک operation، نتیجهی نهایی سیستم رو تغییر نده.
این مفهوم کمک میکنه سیستمها در برابر retry، خطاهای شبکه و عدم قطعیت در اجرای requestها مقاومتر بشن.
#️⃣ #Backend #SystemDesign
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
🔥1
چرا در سیستمهای توزیعشده نمیتونیم همه چیز رو همزمان داشته باشیم؟ 🤔
وقتی یک سیستم کوچیک داریم، معمولاً همهچیز ساده به نظر میرسه. یک Application داریم، یک Database و همهی درخواستها هم به همان نقطه ارسال میشن. اما وقتی سیستم بزرگتر میشه و برای مقیاسپذیری از چندین سرور، چندین دیتابیس یا Regionهای مختلف استفاده میکنیم، یه مشکل اساسی به وجود میاد:
چطور مطمئن بشیم همهی بخشهای سیستم همیشه با هم هماهنگ هستن؟
اینجاست که مفهومی به اسم CAP Theorem وارد میشه.
CAP Theorem دقیقاً چی میگه؟ 🧠
CAP Theorem که توسط Eric Brewer مطرح شد، بیان میکنه که یک سیستم توزیعشده نمیتونه بهصورت همزمان هر سه ویژگی زیر رو به طور کامل تضمین کنه:
1. Consistency
2. Availability
3. Partition Tolerance
اما نکته مهم اینه که این سه مفهوم دقیقاً چه معنیای دارن؟
Consistency یعنی چی؟ 📚
Consistency یعنی هر درخواست Read، جدیدترین مقدار نوشتهشده را دریافت کنه.
فرض کنید یه کاربر اطلاعات حسابش رو توی یه Node تغییر میده.
اگر سیستم Consistent باشه، وقتی همون لحظه از Node دیگری اطلاعات رو بخوانیم، باید همون مقدار جدید رو ببینیم.
در واقع، تمام Nodeها باید یک view یکسان از داده داشته باشند.
Availability یعنی چی؟ ⚡️
Availability یعنی سیستم حتی توی شرایط مشکل هم بتونه پاسخ درخواستها رو بده. یعنی اگر یک درخواست به سیستم ارسال شد، نباید صرفاً به خاطر مشکل داخلی یا از دسترس خارج شدن بخشی از سیستم، هیچ پاسخی دریافت نکنه.
توی سیستمهایی که Availability اولویت بالایی داره، معمولاً ترجیح داده میشه سیستم پاسخ بده، حتی اگر دادهی برگشتی آخرین نسخه نباشه.
Partition Tolerance یعنی چی؟ 🌐
Partition یعنی ارتباط بین بخشهای مختلف یک سیستم توزیعشده قطع بشه.
مثلاً فرض کنید یک دیتابیس توی دو ریجن مختلف دارید:
Region A
Region B
اگه ارتباط شبکه بین این دو قطع بشه، هر کدوم از اونها دیگر نمیتونن تغییرات دیگری رو دریافت کنن. Partition Tolerance یعنی سیستم بتونه با وجود این مشکل شبکه همچنان کار کنه.
چرا نمیتوانیم هر سه تا رو داشته باشیم؟ 🤔
مشکل اصلی زمانی اتفاق میوفته که Partition وارد بحث میشه.
توی یه سیستم توزیعشده، قطع شدن شبکه یک اتفاق غیرممکن نیست؛ بلکه چیزیه که باید براش طراحی کنیم.
حالا فرض کن ارتباط بین دو Node قطع شده:
یه کاربر دادهای رو در Node اول تغییر میده.
Node دوم هنوز از این تغییر خبر نداره.
حالا یه درخواست Read به Node دوم میرسه.
اینجا سیستم باید یک انتخاب انجام بده:
اگر مقدار جدید را برنگردونه، Consistency رو از دست داده.
گر درخواست رو رد کنه تا مطمئن شود داده درسته، Availability رو از دست داده.
و این همون trade-off اصلی CAP است.
سیستمهای CP و AP 🧩
به همین دلیل، سیستمهای توزیعشده معمولاً یکی از این دو مسیر را انتخاب میکنند.
1- CP (Consistency + Partition Tolerance)
توی این مدل، سیستم ترجیح میدهه داده همیشه درست باشه، حتی اگه مجبور بشه بعضی درخواستها رو رد کنه یا منتظر بمونه. این مدل برای سیستمهایی مناسبه که صحیح و معتبر بودن داده اهمیت بالایی داره.
مثلاً سیستمهای مالی که نمیتونن اطلاعات اشتباه برگردونن.
2- AP (Availability + Partition Tolerance)
توی این مدل، سیستم ترجیح میدهه همیشه پاسخ بده، حتی اگر بعضی Nodeها موقتاً دادهی قدیمی داشته باشن. بعداً وقتی ارتباط برقرار شد، دادهها با هم هماهنگ میشن.
این مدل توی سیستمهایی که Availability اهمیت زیادی داره، مثل بعضی شبکههای اجتماعی یا سیستمهای توزیعشده بزرگ، استفاده میشه.
پس CA چی میشه؟ 🤔
از نظر تئوری، یه سیستم میتونه Consistency و Availability رو همزمان داشته باشه. اما فقط وقتی که Partition اتفاق نیوفته. توی دنیای واقعی، مخصوصاً در سیستمهای توزیعشده بزرگ، Partition اجتنابناپذیره.
برای همینه که در عمل بیشتر دربارهی انتخاب بین CP و AP صحبت میکنیم.
یه سوءتفاهم رایج درباره CAP ⚠️
این برداشت که "یه سیستم فقط میتونه دو مورد از این سه مورد رو داشته باشه"، یه برداشت ساده شده هست و خیلی دقیق مفهوم رو بیان نمیکنه. درواقع ایده اصلی درمورد تضمین کردن صحبت میکنه: "وقتی Partition داشته باشیم، دیگه نمیتونیم همزمان Consistency و Availability رو هم کامل تضمین کنیم."
جمعبندی 💡
CAP Theorem یکی از پایهایترین مفاهیم توی طراحی سیستمهای توزیعشده هست.
به ما یاد میده که توی مقیاس بالا، بعضی تصمیمها دیگه فقط تصمیم فنی نیستند؛ بلکه انتخاب بین trade-offهای مختلف هستند.
هیچ سیستم توزیعشدهای “بهترین حالت مطلق” نداره.
طراحی خوب یعنی بدونیم کجا Consistency مهمتره، کجا Availability، و بر اساس نیاز واقعی سیستم تصمیم بگیریم.
➖➖➖➖➖➖➖➖➖➖
وقتی یک سیستم کوچیک داریم، معمولاً همهچیز ساده به نظر میرسه. یک Application داریم، یک Database و همهی درخواستها هم به همان نقطه ارسال میشن. اما وقتی سیستم بزرگتر میشه و برای مقیاسپذیری از چندین سرور، چندین دیتابیس یا Regionهای مختلف استفاده میکنیم، یه مشکل اساسی به وجود میاد:
چطور مطمئن بشیم همهی بخشهای سیستم همیشه با هم هماهنگ هستن؟
اینجاست که مفهومی به اسم CAP Theorem وارد میشه.
CAP Theorem دقیقاً چی میگه؟ 🧠
CAP Theorem که توسط Eric Brewer مطرح شد، بیان میکنه که یک سیستم توزیعشده نمیتونه بهصورت همزمان هر سه ویژگی زیر رو به طور کامل تضمین کنه:
1. Consistency
2. Availability
3. Partition Tolerance
اما نکته مهم اینه که این سه مفهوم دقیقاً چه معنیای دارن؟
Consistency یعنی چی؟ 📚
Consistency یعنی هر درخواست Read، جدیدترین مقدار نوشتهشده را دریافت کنه.
فرض کنید یه کاربر اطلاعات حسابش رو توی یه Node تغییر میده.
اگر سیستم Consistent باشه، وقتی همون لحظه از Node دیگری اطلاعات رو بخوانیم، باید همون مقدار جدید رو ببینیم.
در واقع، تمام Nodeها باید یک view یکسان از داده داشته باشند.
Availability یعنی چی؟ ⚡️
Availability یعنی سیستم حتی توی شرایط مشکل هم بتونه پاسخ درخواستها رو بده. یعنی اگر یک درخواست به سیستم ارسال شد، نباید صرفاً به خاطر مشکل داخلی یا از دسترس خارج شدن بخشی از سیستم، هیچ پاسخی دریافت نکنه.
توی سیستمهایی که Availability اولویت بالایی داره، معمولاً ترجیح داده میشه سیستم پاسخ بده، حتی اگر دادهی برگشتی آخرین نسخه نباشه.
Partition Tolerance یعنی چی؟ 🌐
Partition یعنی ارتباط بین بخشهای مختلف یک سیستم توزیعشده قطع بشه.
مثلاً فرض کنید یک دیتابیس توی دو ریجن مختلف دارید:
Region A
Region B
اگه ارتباط شبکه بین این دو قطع بشه، هر کدوم از اونها دیگر نمیتونن تغییرات دیگری رو دریافت کنن. Partition Tolerance یعنی سیستم بتونه با وجود این مشکل شبکه همچنان کار کنه.
چرا نمیتوانیم هر سه تا رو داشته باشیم؟ 🤔
مشکل اصلی زمانی اتفاق میوفته که Partition وارد بحث میشه.
توی یه سیستم توزیعشده، قطع شدن شبکه یک اتفاق غیرممکن نیست؛ بلکه چیزیه که باید براش طراحی کنیم.
حالا فرض کن ارتباط بین دو Node قطع شده:
یه کاربر دادهای رو در Node اول تغییر میده.
Node دوم هنوز از این تغییر خبر نداره.
حالا یه درخواست Read به Node دوم میرسه.
اینجا سیستم باید یک انتخاب انجام بده:
اگر مقدار جدید را برنگردونه، Consistency رو از دست داده.
گر درخواست رو رد کنه تا مطمئن شود داده درسته، Availability رو از دست داده.
و این همون trade-off اصلی CAP است.
سیستمهای CP و AP 🧩
به همین دلیل، سیستمهای توزیعشده معمولاً یکی از این دو مسیر را انتخاب میکنند.
1- CP (Consistency + Partition Tolerance)
توی این مدل، سیستم ترجیح میدهه داده همیشه درست باشه، حتی اگه مجبور بشه بعضی درخواستها رو رد کنه یا منتظر بمونه. این مدل برای سیستمهایی مناسبه که صحیح و معتبر بودن داده اهمیت بالایی داره.
مثلاً سیستمهای مالی که نمیتونن اطلاعات اشتباه برگردونن.
2- AP (Availability + Partition Tolerance)
توی این مدل، سیستم ترجیح میدهه همیشه پاسخ بده، حتی اگر بعضی Nodeها موقتاً دادهی قدیمی داشته باشن. بعداً وقتی ارتباط برقرار شد، دادهها با هم هماهنگ میشن.
این مدل توی سیستمهایی که Availability اهمیت زیادی داره، مثل بعضی شبکههای اجتماعی یا سیستمهای توزیعشده بزرگ، استفاده میشه.
پس CA چی میشه؟ 🤔
از نظر تئوری، یه سیستم میتونه Consistency و Availability رو همزمان داشته باشه. اما فقط وقتی که Partition اتفاق نیوفته. توی دنیای واقعی، مخصوصاً در سیستمهای توزیعشده بزرگ، Partition اجتنابناپذیره.
برای همینه که در عمل بیشتر دربارهی انتخاب بین CP و AP صحبت میکنیم.
یه سوءتفاهم رایج درباره CAP ⚠️
این برداشت که "یه سیستم فقط میتونه دو مورد از این سه مورد رو داشته باشه"، یه برداشت ساده شده هست و خیلی دقیق مفهوم رو بیان نمیکنه. درواقع ایده اصلی درمورد تضمین کردن صحبت میکنه: "وقتی Partition داشته باشیم، دیگه نمیتونیم همزمان Consistency و Availability رو هم کامل تضمین کنیم."
جمعبندی 💡
CAP Theorem یکی از پایهایترین مفاهیم توی طراحی سیستمهای توزیعشده هست.
به ما یاد میده که توی مقیاس بالا، بعضی تصمیمها دیگه فقط تصمیم فنی نیستند؛ بلکه انتخاب بین trade-offهای مختلف هستند.
هیچ سیستم توزیعشدهای “بهترین حالت مطلق” نداره.
طراحی خوب یعنی بدونیم کجا Consistency مهمتره، کجا Availability، و بر اساس نیاز واقعی سیستم تصمیم بگیریم.
#️⃣ #backend #system_design
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
جنایات جنگی توی برنامه نویسی، اشتباه ترین کارای ممکن به فان ترین شکل ممکن.
https://youtube.com/playlist?list=PLr1FHxz1zgsFdBXnE-uQ9_29GVNtPdqj5&si=JqgG_sGEIqQKKYRM
➖➖➖➖➖➖➖➖➖➖
یه جاهایی انقدر ترسناک میشه که آدم بجای خندیدن فقط خیره میشه به صفحه
https://youtube.com/playlist?list=PLr1FHxz1zgsFdBXnE-uQ9_29GVNtPdqj5&si=JqgG_sGEIqQKKYRM
#️⃣ #reference
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
آیا Load Balancer فقط درخواستها رو بین سرورها تقسیم میکنه؟ 🤔
وقتی برای اولین بار اسم Load Balancer رو میشنویم، معمولاً این تصور به وجود میاد که فقط درخواستها رو به ترتیب بین چند سرور پخش میکنه.
تا حدی هم درسته، اما فقط بخشی از ماجراست.
نحوهی انتخاب سروری که قرار هست درخواست بعدی رو پردازش کنه، میتونه روی Performance، Latency، استفاده از منابع و حتی تجربهی کاربر تأثیر زیادی بذاره.
به همین دلیل، Load Balancerها از الگوریتمهای مختلفی برای توزیع درخواستها استفاده میکنن و هر کدوم برای سناریوی خاصی طراحی شدن.
Round Robin 🔄
سادهترین و شناختهشدهترین الگوریتم، Round Robin هست.
در این روش، درخواستها به ترتیب بین سرورها تقسیم میشن. اگر سه سرور داشته باشیم، درخواست اول به سرور اول، درخواست دوم به سرور دوم، درخواست سوم به سرور سوم و بعد دوباره همین چرخه تکرار میشه.
این الگوریتم پیادهسازی سادهای داره و زمانی عملکرد خوبی داره که همهی سرورها تقریباً قدرت پردازشی یکسانی داشته باشن و مدت زمان پردازش درخواستها هم نزدیک به هم باشه.
اما اگر یکی از سرورها کندتر باشه یا درخواستها زمان پردازش متفاوتی داشته باشن، ممکنه بعضی سرورها خیلی شلوغ بشن و بعضی دیگه تقریباً بیکار بمونن.
Weighted Round Robin ⚖️
گاهی همهی سرورها توان پردازشی یکسانی ندارن.
ممکنه یکی از سرورها CPU و RAM بیشتری داشته باشه یا روی سختافزار قویتری اجرا بشه. در این شرایط، استفاده از Round Robin معمولی منطقی نیست.
Weighted Round Robin به هر سرور یک وزن اختصاص میده و سرورهایی که وزن بیشتری دارن، سهم بیشتری از درخواستها رو دریافت میکنن.
به این ترتیب منابع قویتر بهتر استفاده میشن و فشار روی سرورهای ضعیفتر کمتر میشه.
Least Connections 🔗
این الگوریتم به جای اینکه فقط تعداد درخواستهای ورودی رو در نظر بگیره، تعداد Connectionهای فعال هر سرور رو بررسی میکنه.
هر درخواست جدید به سروری ارسال میشه که در اون لحظه کمترین Connection فعال رو داشته باشه.
این روش برای سرویسهایی که مدت زمان پردازش درخواستها متغیره، معمولاً انتخاب مناسبتری نسبت به Round Robin محسوب میشه، چون بار واقعی هر سرور رو بهتر منعکس میکنه.
Least Response Time ⚡️
بعضی Load Balancerها علاوه بر تعداد Connectionها، زمان پاسخدهی هر سرور رو هم اندازهگیری میکنن.
در این حالت، درخواستها به سروری ارسال میشن که هم پاسخ سریعتری داره و هم بار کمتری روی اون وجود داره.
این الگوریتم میتونه Latency سیستم رو کاهش بده، اما در عوض نیاز به مانیتورینگ مداوم وضعیت سرورها داره و نسبت به الگوریتمهای سادهتر، هزینهی بیشتری برای تصمیمگیری ایجاد میکنه.
IP Hash 🌐
در بعضی سیستمها مهمه که درخواستهای یک کاربر همیشه به یک سرور مشخص ارسال بشه.
مثلاً زمانی که Sessionها روی خود سرورها نگهداری میشن.
در الگوریتم IP Hash، بر اساس IP کاربر یک Hash محاسبه میشه و نتیجهی اون مشخص میکنه درخواست به کدوم سرور بره.
در نتیجه تا زمانی که تعداد سرورها تغییر نکنه، یک کاربر معمولاً همیشه به همان سرور قبلی هدایت میشه.
البته اگر سروری از دسترس خارج بشه یا سرور جدیدی به سیستم اضافه بشه، این رویه میتونه تغییر کنه و بخشی از کاربران به سرورهای دیگه منتقل بشن.
پس کدوم الگوریتم بهتره؟ 💡
واقعیت اینه که هیچ الگوریتمی بهترین انتخاب برای همهی سیستمها نیست.
اگر همهی سرورها تقریباً یکسان باشن، Round Robin معمولاً کافی خواهد بود.
اگر سختافزار سرورها متفاوت باشه، Weighted Round Robin انتخاب منطقیتریه.
اگر مدت زمان پردازش درخواستها متغیر باشه، Least Connections یا Least Response Time معمولاً عملکرد بهتری دارن.
و اگر حفظ Session یا اتصال یک کاربر به یک سرور خاص اهمیت داشته باشه، IP Hash میتونه گزینهی مناسبی باشه.
در نهایت، انتخاب الگوریتم بیشتر از اینکه به خود Load Balancer بستگی داشته باشه، به رفتار سیستم و نوع ترافیکی برمیگرده که قراره مدیریت بشه.
Load Balancer فقط ابزاری برای تقسیم درخواستها نیست.
نحوهی توزیع درخواستها میتونه روی سرعت پاسخگویی، استفاده از منابع و حتی پایداری کل سیستم تأثیر مستقیمی داشته باشه.
به همین دلیل، انتخاب الگوریتم مناسب یکی از تصمیمهای مهم در طراحی سیستمهای مقیاسپذیره و معمولاً هیچ پاسخ یکسانی برای همهی پروژهها وجود نداره.
➖➖➖➖➖➖➖➖➖➖
وقتی برای اولین بار اسم Load Balancer رو میشنویم، معمولاً این تصور به وجود میاد که فقط درخواستها رو به ترتیب بین چند سرور پخش میکنه.
تا حدی هم درسته، اما فقط بخشی از ماجراست.
نحوهی انتخاب سروری که قرار هست درخواست بعدی رو پردازش کنه، میتونه روی Performance، Latency، استفاده از منابع و حتی تجربهی کاربر تأثیر زیادی بذاره.
به همین دلیل، Load Balancerها از الگوریتمهای مختلفی برای توزیع درخواستها استفاده میکنن و هر کدوم برای سناریوی خاصی طراحی شدن.
Round Robin 🔄
سادهترین و شناختهشدهترین الگوریتم، Round Robin هست.
در این روش، درخواستها به ترتیب بین سرورها تقسیم میشن. اگر سه سرور داشته باشیم، درخواست اول به سرور اول، درخواست دوم به سرور دوم، درخواست سوم به سرور سوم و بعد دوباره همین چرخه تکرار میشه.
این الگوریتم پیادهسازی سادهای داره و زمانی عملکرد خوبی داره که همهی سرورها تقریباً قدرت پردازشی یکسانی داشته باشن و مدت زمان پردازش درخواستها هم نزدیک به هم باشه.
اما اگر یکی از سرورها کندتر باشه یا درخواستها زمان پردازش متفاوتی داشته باشن، ممکنه بعضی سرورها خیلی شلوغ بشن و بعضی دیگه تقریباً بیکار بمونن.
Weighted Round Robin ⚖️
گاهی همهی سرورها توان پردازشی یکسانی ندارن.
ممکنه یکی از سرورها CPU و RAM بیشتری داشته باشه یا روی سختافزار قویتری اجرا بشه. در این شرایط، استفاده از Round Robin معمولی منطقی نیست.
Weighted Round Robin به هر سرور یک وزن اختصاص میده و سرورهایی که وزن بیشتری دارن، سهم بیشتری از درخواستها رو دریافت میکنن.
به این ترتیب منابع قویتر بهتر استفاده میشن و فشار روی سرورهای ضعیفتر کمتر میشه.
Least Connections 🔗
این الگوریتم به جای اینکه فقط تعداد درخواستهای ورودی رو در نظر بگیره، تعداد Connectionهای فعال هر سرور رو بررسی میکنه.
هر درخواست جدید به سروری ارسال میشه که در اون لحظه کمترین Connection فعال رو داشته باشه.
این روش برای سرویسهایی که مدت زمان پردازش درخواستها متغیره، معمولاً انتخاب مناسبتری نسبت به Round Robin محسوب میشه، چون بار واقعی هر سرور رو بهتر منعکس میکنه.
Least Response Time ⚡️
بعضی Load Balancerها علاوه بر تعداد Connectionها، زمان پاسخدهی هر سرور رو هم اندازهگیری میکنن.
در این حالت، درخواستها به سروری ارسال میشن که هم پاسخ سریعتری داره و هم بار کمتری روی اون وجود داره.
این الگوریتم میتونه Latency سیستم رو کاهش بده، اما در عوض نیاز به مانیتورینگ مداوم وضعیت سرورها داره و نسبت به الگوریتمهای سادهتر، هزینهی بیشتری برای تصمیمگیری ایجاد میکنه.
IP Hash 🌐
در بعضی سیستمها مهمه که درخواستهای یک کاربر همیشه به یک سرور مشخص ارسال بشه.
مثلاً زمانی که Sessionها روی خود سرورها نگهداری میشن.
در الگوریتم IP Hash، بر اساس IP کاربر یک Hash محاسبه میشه و نتیجهی اون مشخص میکنه درخواست به کدوم سرور بره.
در نتیجه تا زمانی که تعداد سرورها تغییر نکنه، یک کاربر معمولاً همیشه به همان سرور قبلی هدایت میشه.
البته اگر سروری از دسترس خارج بشه یا سرور جدیدی به سیستم اضافه بشه، این رویه میتونه تغییر کنه و بخشی از کاربران به سرورهای دیگه منتقل بشن.
پس کدوم الگوریتم بهتره؟ 💡
واقعیت اینه که هیچ الگوریتمی بهترین انتخاب برای همهی سیستمها نیست.
اگر همهی سرورها تقریباً یکسان باشن، Round Robin معمولاً کافی خواهد بود.
اگر سختافزار سرورها متفاوت باشه، Weighted Round Robin انتخاب منطقیتریه.
اگر مدت زمان پردازش درخواستها متغیر باشه، Least Connections یا Least Response Time معمولاً عملکرد بهتری دارن.
و اگر حفظ Session یا اتصال یک کاربر به یک سرور خاص اهمیت داشته باشه، IP Hash میتونه گزینهی مناسبی باشه.
در نهایت، انتخاب الگوریتم بیشتر از اینکه به خود Load Balancer بستگی داشته باشه، به رفتار سیستم و نوع ترافیکی برمیگرده که قراره مدیریت بشه.
Load Balancer فقط ابزاری برای تقسیم درخواستها نیست.
نحوهی توزیع درخواستها میتونه روی سرعت پاسخگویی، استفاده از منابع و حتی پایداری کل سیستم تأثیر مستقیمی داشته باشه.
به همین دلیل، انتخاب الگوریتم مناسب یکی از تصمیمهای مهم در طراحی سیستمهای مقیاسپذیره و معمولاً هیچ پاسخ یکسانی برای همهی پروژهها وجود نداره.
#️⃣ #backend #system_design
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
🆒1
چرا همهی Replicaها همیشه یک دادهی یکسان ندارن؟ 🤔
وقتی برای اولین بار با سیستمهای توزیعشده آشنا میشیم، معمولاً انتظار داریم اگر دادهای روی یک سرور تغییر کرد، بلافاصله روی همهی Replicaها هم تغییر کنه. اما در عمل همیشه این اتفاق نمیافته.
دلیلش این نیست که سیستم درست کار نمیکنه، بلکه به مدلی برمیگرده که برای Consistency انتخاب شده.
Consistency دقیقاً یعنی چی؟ 🧠
Consistency مشخص میکنه که بعد از انجام یک عملیات Write، چه زمانی همهی Clientها نسخهی جدید داده رو مشاهده میکنن. هرچی Consistency قویتر باشه، احتمال دیدن دادهی قدیمی کمتره؛ اما معمولاً باید هزینهی بیشتری از نظر Latency یا Availability پرداخت کنیم.
در مقابل، اگر هدف اصلی پاسخگویی سریع و در دسترس بودن سیستم باشه، ممکنه برای مدت کوتاهی نسخههای متفاوتی از یک داده روی Nodeهای مختلف وجود داشته باشه.
به همین دلیل، Consistency بیشتر از اینکه یک ویژگی ثابت باشه، یک trade-off توی طراحی سیستم محسوب میشه.
Strong Consistency 🔒
توی Strong Consistency، بعد از اینکه عملیات Write با موفقیت انجام شد، هر عملیات Read باید آخرین مقدار نوشتهشده رو برگردونه. به عبارت دیگه، تمام Clientها همیشه یک دید یکسان از داده دارن و مهم نیست درخواستشون به کدوم Node ارسال بشه.
این مدل سادهترین رفتار رو برای توسعهدهنده ایجاد میکنه، اما هماهنگ نگه داشتن همهی Nodeها معمولاً باعث افزایش Latency میشه و در زمان بروز مشکلات شبکه هم میتونه Availability سیستم رو کاهش بده.
به همین دلیل، Strong Consistency بیشتر در سیستمهایی استفاده میشه که صحت داده از هر چیز دیگهای مهمتره؛ مثل سیستمهای بانکی یا مالی.
Eventual Consistency ⏳
توی Eventual Consistency، بعد از انجام یک Write، ممکنه بعضی Replicaها هنوز دادهی قدیمی رو برگردونن. اما اگر تغییر جدیدی روی داده انجام نشه، در نهایت تمام Replicaها به یک وضعیت مشترک میرسن و داده بین اونها همگام میشه.
این مدل باعث میشه سیستم Availability بالاتری داشته باشه و توی مقیاس بزرگ راحتتر توسعه پیدا کنه، اما توسعهدهنده باید این احتمال رو در نظر بگیره که بعضی Readها برای مدت کوتاهی آخرین نسخهی داده رو نمایش ندن.
به همین دلیل، Eventual Consistency در بسیاری از سرویسهای بزرگ، شبکههای اجتماعی و سیستمهای توزیعشده استفاده میشه.
Causal Consistency 🔗
گاهی لازم نیست همهی عملیاتها فوراً روی تمام Nodeها دیده بشن، اما ترتیب منطقی اتفاقها باید حفظ بشه. Causal Consistency دقیقاً همین مسئله رو حل میکنه. اگه یک عملیات به عملیات دیگری وابسته باشه، همهی Clientها باید این وابستگی رو به همان ترتیب مشاهده کنن.
برای مثال، اگه کاربری ابتدا یک پست منتشر کنه و چند ثانیه بعد ویرایشش کنه، منطقی نیست که کاربر دیگری نسخهی ویرایششده رو ببینه اما اصل پست هنوز براش وجود نداشته باشه.
این مدل تعادلی بین Strong Consistency و Eventual Consistency ایجاد میکنه و برای بسیاری از سیستمهای تعاملی انتخاب مناسبیه.
Read-Your-Writes Consistency 👤
یکی از مشکلات رایج توی سیستمهای توزیعشده اینه که کاربر اطلاعات خودش رو تغییر میده، اما بلافاصله بعد از Refresh هنوز نسخهی قدیمی رو میبینه. Read-Your-Writes Consistency تضمین میکنه که حداقل همون کاربری که عملیات Write رو انجام داده، از اون لحظه به بعد همیشه آخرین تغییر خودش رو مشاهده کنه.
این مدل الزاماً به این معنی نیست که همهی کاربران همزمان دادهی جدید رو ببینن، اما تجربهی کاربری بسیار بهتری ایجاد میکنه و توی بسیاری از سرویسهای مدرن مورد استفاده قرار میگیره.
Monotonic Reads 📈
این سناریو رو در نظر بگیرید:
یک کاربر اول نسخهی جدید یک داده رو مشاهده کنه، اما توی درخواست بعدی دوباره نسخهی قدیمیتر همون داده بهش نشون داده بشه.
همچین رفتاری میتونه باعث سردرگمی بشه. Monotonic Reads تضمین میکنه که اگه یک Client نسخهای از داده رو دید، توی درخواستهای بعدی هرگز نسخهای قدیمیتر از اون رو نمیبینه.
این مدل بیشتر روی حفظ تجربهی کاربر تمرکز داره.
جمعبندی ✍️
Consistency فقط یک ویژگی برای Database نیست؛ بلکه یکی از مهمترین تصمیمها توی طراحی سیستمهای توزیعشده هست.
اینکه کاربرا چه زمانی تغییرات رو ببینن، Replicaها چگونه با هم همگام بشن و سیستم در زمان بروز مشکلات شبکه چه رفتاری داشته باشه، همگی به Consistency Model انتخابشده بستگی دارن.
درک این مدلها کمک میکند هنگام طراحی یک سیستم، آگاهانه بین Latency، Availability و Consistency تعادل برقرار کنیم.
➖➖➖➖➖➖➖➖➖➖
وقتی برای اولین بار با سیستمهای توزیعشده آشنا میشیم، معمولاً انتظار داریم اگر دادهای روی یک سرور تغییر کرد، بلافاصله روی همهی Replicaها هم تغییر کنه. اما در عمل همیشه این اتفاق نمیافته.
دلیلش این نیست که سیستم درست کار نمیکنه، بلکه به مدلی برمیگرده که برای Consistency انتخاب شده.
Consistency دقیقاً یعنی چی؟ 🧠
Consistency مشخص میکنه که بعد از انجام یک عملیات Write، چه زمانی همهی Clientها نسخهی جدید داده رو مشاهده میکنن. هرچی Consistency قویتر باشه، احتمال دیدن دادهی قدیمی کمتره؛ اما معمولاً باید هزینهی بیشتری از نظر Latency یا Availability پرداخت کنیم.
در مقابل، اگر هدف اصلی پاسخگویی سریع و در دسترس بودن سیستم باشه، ممکنه برای مدت کوتاهی نسخههای متفاوتی از یک داده روی Nodeهای مختلف وجود داشته باشه.
به همین دلیل، Consistency بیشتر از اینکه یک ویژگی ثابت باشه، یک trade-off توی طراحی سیستم محسوب میشه.
Strong Consistency 🔒
توی Strong Consistency، بعد از اینکه عملیات Write با موفقیت انجام شد، هر عملیات Read باید آخرین مقدار نوشتهشده رو برگردونه. به عبارت دیگه، تمام Clientها همیشه یک دید یکسان از داده دارن و مهم نیست درخواستشون به کدوم Node ارسال بشه.
این مدل سادهترین رفتار رو برای توسعهدهنده ایجاد میکنه، اما هماهنگ نگه داشتن همهی Nodeها معمولاً باعث افزایش Latency میشه و در زمان بروز مشکلات شبکه هم میتونه Availability سیستم رو کاهش بده.
به همین دلیل، Strong Consistency بیشتر در سیستمهایی استفاده میشه که صحت داده از هر چیز دیگهای مهمتره؛ مثل سیستمهای بانکی یا مالی.
Eventual Consistency ⏳
توی Eventual Consistency، بعد از انجام یک Write، ممکنه بعضی Replicaها هنوز دادهی قدیمی رو برگردونن. اما اگر تغییر جدیدی روی داده انجام نشه، در نهایت تمام Replicaها به یک وضعیت مشترک میرسن و داده بین اونها همگام میشه.
این مدل باعث میشه سیستم Availability بالاتری داشته باشه و توی مقیاس بزرگ راحتتر توسعه پیدا کنه، اما توسعهدهنده باید این احتمال رو در نظر بگیره که بعضی Readها برای مدت کوتاهی آخرین نسخهی داده رو نمایش ندن.
به همین دلیل، Eventual Consistency در بسیاری از سرویسهای بزرگ، شبکههای اجتماعی و سیستمهای توزیعشده استفاده میشه.
Causal Consistency 🔗
گاهی لازم نیست همهی عملیاتها فوراً روی تمام Nodeها دیده بشن، اما ترتیب منطقی اتفاقها باید حفظ بشه. Causal Consistency دقیقاً همین مسئله رو حل میکنه. اگه یک عملیات به عملیات دیگری وابسته باشه، همهی Clientها باید این وابستگی رو به همان ترتیب مشاهده کنن.
برای مثال، اگه کاربری ابتدا یک پست منتشر کنه و چند ثانیه بعد ویرایشش کنه، منطقی نیست که کاربر دیگری نسخهی ویرایششده رو ببینه اما اصل پست هنوز براش وجود نداشته باشه.
این مدل تعادلی بین Strong Consistency و Eventual Consistency ایجاد میکنه و برای بسیاری از سیستمهای تعاملی انتخاب مناسبیه.
Read-Your-Writes Consistency 👤
یکی از مشکلات رایج توی سیستمهای توزیعشده اینه که کاربر اطلاعات خودش رو تغییر میده، اما بلافاصله بعد از Refresh هنوز نسخهی قدیمی رو میبینه. Read-Your-Writes Consistency تضمین میکنه که حداقل همون کاربری که عملیات Write رو انجام داده، از اون لحظه به بعد همیشه آخرین تغییر خودش رو مشاهده کنه.
این مدل الزاماً به این معنی نیست که همهی کاربران همزمان دادهی جدید رو ببینن، اما تجربهی کاربری بسیار بهتری ایجاد میکنه و توی بسیاری از سرویسهای مدرن مورد استفاده قرار میگیره.
Monotonic Reads 📈
این سناریو رو در نظر بگیرید:
یک کاربر اول نسخهی جدید یک داده رو مشاهده کنه، اما توی درخواست بعدی دوباره نسخهی قدیمیتر همون داده بهش نشون داده بشه.
همچین رفتاری میتونه باعث سردرگمی بشه. Monotonic Reads تضمین میکنه که اگه یک Client نسخهای از داده رو دید، توی درخواستهای بعدی هرگز نسخهای قدیمیتر از اون رو نمیبینه.
این مدل بیشتر روی حفظ تجربهی کاربر تمرکز داره.
جمعبندی ✍️
Consistency فقط یک ویژگی برای Database نیست؛ بلکه یکی از مهمترین تصمیمها توی طراحی سیستمهای توزیعشده هست.
اینکه کاربرا چه زمانی تغییرات رو ببینن، Replicaها چگونه با هم همگام بشن و سیستم در زمان بروز مشکلات شبکه چه رفتاری داشته باشه، همگی به Consistency Model انتخابشده بستگی دارن.
درک این مدلها کمک میکند هنگام طراحی یک سیستم، آگاهانه بین Latency، Availability و Consistency تعادل برقرار کنیم.
#️⃣ #system_design #databsae #backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
چرا دو Transaction همزمان همیشه نتیجهی یکسانی نمیبینن؟ 🤔
وقتی یه سیستم ساده داریم، معمولاً تصورمون اینه که Database فقط دادهها رو ذخیره میکنه. اما توی یه سیستم واقعی، چندین کاربر میتونن همزمان روی یه داده بخونن یا تغییر ایجاد کنن.
اینجاست که یه سؤال مهم به وجود میاد:
اگه دو Transaction همزمان روی یه داده کار کنن، Database باید چه رفتاری داشته باشه؟
جواب این سؤالها توسط مفهومی به اسم Transaction Isolation Level مشخص میشه.
Isolation Level دقیقاً یعنی چی؟ 🧠
توی Database، هر Transaction یه عملیات منطقی مستقل محسوب میشه. Isolation مشخص میکنه که تغییرات یه Transaction تا چه حد باید از Transactionهای دیگه جدا باشه. هرچی Isolation بالاتر باشه، Transactionها رفتار قابل پیشبینیتری دارن، اما معمولاً هزینهی بیشتری از نظر Performance و محدودیت همزمانی ایجاد میکنه.
در واقع Isolation Level یه نقطهی تعادل بین Correctness و Performance هست.
Read Uncommitted 📖
پایینترین سطح Isolation، Read Uncommitted هست. توی این حالت، یه Transaction میتونه تغییراتی رو ببینه که Transaction دیگه هنوز Commit نکرده. یعنی ممکنه دادهای خونده بشه که در نهایت اصلاً ذخیره نشه.
فرض کنین یه Transaction مقدار موجودی حساب رو تغییر داده، اما هنوز Commit نکرده. اگه Transaction دیگهای همون مقدار جدید رو بخونه و بعد Transaction اول Rollback بشه، دادهای که دیده شده اصلاً وجود نداشته.
به این مشکل Dirty Read گفته میشه.
این Level معمولاً توی سیستمهای حساس استفاده نمیشه، چون تضمین زیادی دربارهی صحت داده نمیده.
Read Committed 📚
Read Committed یکی از رایجترین Isolation Levelهاست و توی PostgreSQL هم Default محسوب میشه. توی این حالت، یه Transaction فقط دادههایی رو میبینه که توسط Transactionهای دیگه Commit شدن. یعنی Dirty Read اتفاق نمیافته. اما همچنان ممکنه یه Transaction توی دو Query متفاوت، دو نتیجهی مختلف بگیره.
مثلاً:
توی Query اول تعداد سفارشها رو میخونه.
یه Transaction دیگه سفارش جدیدی ثبت و Commit میکنه.
Query دوم دوباره تعداد سفارشها رو میخونه.
حالا نتیجه تغییر کرده.
این رفتار به Non-Repeatable Read معروفه.
Repeatable Read 🔒
توی Database, Repeatable Read تلاش میکنه یه Snapshot ثابت از دادهها برای Transaction ایجاد کنه. یعنی اگه یه Transaction یه داده رو بخونه، توی ادامهی همون Transaction باید همون نسخه رو ببینه، حتی اگه Transactionهای دیگه اون داده رو تغییر بدن. این Level مشکل Non-Repeatable Read رو حل میکنه.
PostgreSQL با استفاده از MVCC یه Snapshot مشخص از دیتابیس در ابتدای Transaction ایجاد میکنه و تمام Queryهای همون Transaction بر اساس همون Snapshot اجرا میشن.
اما همچنان بعضی سناریوهای خاص مربوط به تغییر همزمان مجموعهای از دادهها ممکنه نیاز به کنترل بیشتری داشته باشن.
Serializable 🛡
بالاترین سطح Serializable, Isolation هست. هدف این Level اینه که نتیجهی اجرای Transactionهای همزمان، دقیقاً مثل حالتی باشه که اونها یکییکی و به ترتیب اجرا شدن. یعنی Database طوری رفتار میکنه که انگار اصلاً هیچ concurrency وجود نداشته.
این مدل بیشترین تضمین رو برای صحت داده فراهم میکنه، اما هزینهی بیشتری هم داره. Database باید Transactionها رو بررسی کنه و اگه تشخیص بده اجرای همزمان اونها باعث رفتار ناسازگار میشه، ممکنه یکی از Transactionها رو Rollback کنه تا Consistency حفظ بشه.
جمعبندی ✍️
Isolation Levelها مشخص میکنن Transactionهای همزمان چطور با هم رفتار کنن.
اونها جلوی مشکلاتی مثل:
Dirty Read
Non-Repeatable Read
و رفتارهای غیرقابل پیشبینی توی concurrency
رو میگیرن.
اما هرچی محدودیت بیشتری ایجاد کنیم، معمولاً هزینهی بیشتری برای Performance پرداخت میکنیم.
➖➖➖➖➖➖➖➖➖➖
وقتی یه سیستم ساده داریم، معمولاً تصورمون اینه که Database فقط دادهها رو ذخیره میکنه. اما توی یه سیستم واقعی، چندین کاربر میتونن همزمان روی یه داده بخونن یا تغییر ایجاد کنن.
اینجاست که یه سؤال مهم به وجود میاد:
اگه دو Transaction همزمان روی یه داده کار کنن، Database باید چه رفتاری داشته باشه؟
جواب این سؤالها توسط مفهومی به اسم Transaction Isolation Level مشخص میشه.
Isolation Level دقیقاً یعنی چی؟ 🧠
توی Database، هر Transaction یه عملیات منطقی مستقل محسوب میشه. Isolation مشخص میکنه که تغییرات یه Transaction تا چه حد باید از Transactionهای دیگه جدا باشه. هرچی Isolation بالاتر باشه، Transactionها رفتار قابل پیشبینیتری دارن، اما معمولاً هزینهی بیشتری از نظر Performance و محدودیت همزمانی ایجاد میکنه.
در واقع Isolation Level یه نقطهی تعادل بین Correctness و Performance هست.
Read Uncommitted 📖
پایینترین سطح Isolation، Read Uncommitted هست. توی این حالت، یه Transaction میتونه تغییراتی رو ببینه که Transaction دیگه هنوز Commit نکرده. یعنی ممکنه دادهای خونده بشه که در نهایت اصلاً ذخیره نشه.
فرض کنین یه Transaction مقدار موجودی حساب رو تغییر داده، اما هنوز Commit نکرده. اگه Transaction دیگهای همون مقدار جدید رو بخونه و بعد Transaction اول Rollback بشه، دادهای که دیده شده اصلاً وجود نداشته.
به این مشکل Dirty Read گفته میشه.
این Level معمولاً توی سیستمهای حساس استفاده نمیشه، چون تضمین زیادی دربارهی صحت داده نمیده.
Read Committed 📚
Read Committed یکی از رایجترین Isolation Levelهاست و توی PostgreSQL هم Default محسوب میشه. توی این حالت، یه Transaction فقط دادههایی رو میبینه که توسط Transactionهای دیگه Commit شدن. یعنی Dirty Read اتفاق نمیافته. اما همچنان ممکنه یه Transaction توی دو Query متفاوت، دو نتیجهی مختلف بگیره.
مثلاً:
توی Query اول تعداد سفارشها رو میخونه.
یه Transaction دیگه سفارش جدیدی ثبت و Commit میکنه.
Query دوم دوباره تعداد سفارشها رو میخونه.
حالا نتیجه تغییر کرده.
این رفتار به Non-Repeatable Read معروفه.
Repeatable Read 🔒
توی Database, Repeatable Read تلاش میکنه یه Snapshot ثابت از دادهها برای Transaction ایجاد کنه. یعنی اگه یه Transaction یه داده رو بخونه، توی ادامهی همون Transaction باید همون نسخه رو ببینه، حتی اگه Transactionهای دیگه اون داده رو تغییر بدن. این Level مشکل Non-Repeatable Read رو حل میکنه.
PostgreSQL با استفاده از MVCC یه Snapshot مشخص از دیتابیس در ابتدای Transaction ایجاد میکنه و تمام Queryهای همون Transaction بر اساس همون Snapshot اجرا میشن.
اما همچنان بعضی سناریوهای خاص مربوط به تغییر همزمان مجموعهای از دادهها ممکنه نیاز به کنترل بیشتری داشته باشن.
Serializable 🛡
بالاترین سطح Serializable, Isolation هست. هدف این Level اینه که نتیجهی اجرای Transactionهای همزمان، دقیقاً مثل حالتی باشه که اونها یکییکی و به ترتیب اجرا شدن. یعنی Database طوری رفتار میکنه که انگار اصلاً هیچ concurrency وجود نداشته.
این مدل بیشترین تضمین رو برای صحت داده فراهم میکنه، اما هزینهی بیشتری هم داره. Database باید Transactionها رو بررسی کنه و اگه تشخیص بده اجرای همزمان اونها باعث رفتار ناسازگار میشه، ممکنه یکی از Transactionها رو Rollback کنه تا Consistency حفظ بشه.
جمعبندی ✍️
Isolation Levelها مشخص میکنن Transactionهای همزمان چطور با هم رفتار کنن.
اونها جلوی مشکلاتی مثل:
Dirty Read
Non-Repeatable Read
و رفتارهای غیرقابل پیشبینی توی concurrency
رو میگیرن.
اما هرچی محدودیت بیشتری ایجاد کنیم، معمولاً هزینهی بیشتری برای Performance پرداخت میکنیم.
#️⃣ #system_design #databsae #backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
ردیس چطور بدون متوقف کردن سرویس از دادهها Snapshot میگیره؟ 🤔
وقتی دربارهی ردیس صحبت میکنیم، معمولاً اولین چیزی که به ذهنمون میاد سرعت بالای اون به خاطر نگهداری دادهها داخل رم هست. اما همین موضوع یه سؤال مهم ایجاد میکنه:
اگه تمام دادههای ردیس داخل رم باشن، بعد از Restart شدن سرویس یا کرش شدن سرور چه اتفاقی برای اطلاعات میفته؟
اینجاست که مفهوم Persistence وارد ردیس میشه. ردیس با اینکه یه دیتابیس In-Memory محسوب میشه، اما برای اینکه دادهها بعد از خاموش شدن یا ریاستارت شدن از بین نرن، مکانیزمهایی برای ذخیره کردن اطلاعات روی دیسک داره.
یکی از این روشها RDB (Redis Database Backup) هست.
قبل از RDB، یه نکته مهم دربارهی معماری ردیس 🧠
یکی از ویژگیهای مهم ردیس اینه که بخش اصلی اجرای Commandها به صورت Single Thread انجام میشه. یعنی ردیس معمولاً یک Thread اصلی داره که Commandهای Clientها رو دریافت میکنه، اجرا میکنه و نتیجه رو برمیگردونه. این طراحی باعث میشه ردیس برای خیلی از عملیاتها Performance خیلی بالایی داشته باشه؛ چون پیچیدگی Lock کردن بین چند Thread مختلف رو نداره.
اما همین موضوع یه چالش ایجاد میکنه:
اگه ردیس بخواد وسط پردازش Requestها، چند گیگابایت داده رو روی Disk بنویسه، نباید این کار باعث متوقف شدن Event Loop اصلی بشه.
اینجاست که ردیس از یک تکنیک جالب استفاده میکنه.
RDB دقیقاً چطور کار میکنه؟ 📦
RDB در واقع یک Snapshot از وضعیت ردیس در یک لحظهی مشخصه. یعنی ردیس تمام دادههای موجود در رم رو گرفته و داخل یک فایل Binary با پسوند
مثلاً میتونیم تنظیم کنیم:
بعد از 10000 تغییر در 60 ثانیه، یه Snapshot جدید ساخته بشه.
وقتی این شرایط اتفاق بیفته، ردیس فرآیند ساخت RDB رو شروع میکنه.
اما نکته مهم اینجاست:
ردیس نمیتونه Thread اصلی خودش رو برای چند ثانیه یا چند دقیقه متوقف کنه تا کل Memory رو روی Disk بنویسه.
پس میاد یه کمک کوچیک از سیستم عامل میگیره.
نقش fork() در RDB ⚙️
برای ساخت فایل RDB، ردیس از یه System Call به اسم ()fork استفاده میکنه. وقتی ()fork اجرا میشه، سیستمعامل یک Process جدید از Process اصلی ردیس ایجاد میکنه.
بعد از این اتفاق:
- Process اصلی ردیس همچنان Requestهای Clientها رو پردازش میکنه.
- Process جدید مسئول نوشتن Snapshot روی Disk میشه.
یعنی عملیات سنگین نوشتن فایل RDB از مسیر اصلی اجرای ردیس جدا میشه.
اما اینجا یه سؤال مهم پیش میاد:
آیا با fork شدن Process، کل دادههای ردیس دوباره داخل RAM کپی میشن؟
اگه این اتفاق بیفته، برای دیتابیسی که چندین گیگابایت داده داره، عملاً غیرممکن میشه.
اینجاست که Copy-On-Write وارد میشه.
Copy-On-Write چطور کار میکنه؟ 📝
بعد از ()Child Process ,fork و Parent Process در ابتدا Memory مشترکی دارن. یعنی سیستمعامل لازم نیست همون لحظه تمام دادهها رو کپی کنه. هر دو Process فعلاً به یک Page از Memory اشاره میکنن. اما اگه بعد از fork، ردیس بخواد یک داده رو تغییر بده، اون Page خاص باید جدا بشه. یعنی:
- نسخهی قدیمی برای Child Process باقی میمونه تا Snapshot درست ساخته بشه.
- نسخهی جدید برای Parent Process استفاده میشه تا ردیس به کار خودش ادامه بده.
به همین دلیل اسم این تکنیک Copy-On-Write هست. کپی فقط زمانی اتفاق میفته که واقعاً Write انجام بشه.
مشکل Copy-On-Write چیه؟ 🤔
با اینکه Copy-On-Write باعث میشه fork خیلی سریع انجام بشه، اما بدون هزینه هم نیست. اگه ردیس موقع ساخت RDB تغییرات زیادی دریافت کنه، تعداد Pageهایی که باید Copy بشن بیشتر میشه. در نتیجه Memory مصرفی ردیس میتونه برای مدتی افزایش پیدا کنه. به همین دلیل توی سیستمهای بزرگ، زمان اجرای BGSAVE و میزان تغییرات دادهها باید به دقت مانیتور بشه.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
وقتی دربارهی ردیس صحبت میکنیم، معمولاً اولین چیزی که به ذهنمون میاد سرعت بالای اون به خاطر نگهداری دادهها داخل رم هست. اما همین موضوع یه سؤال مهم ایجاد میکنه:
اگه تمام دادههای ردیس داخل رم باشن، بعد از Restart شدن سرویس یا کرش شدن سرور چه اتفاقی برای اطلاعات میفته؟
اینجاست که مفهوم Persistence وارد ردیس میشه. ردیس با اینکه یه دیتابیس In-Memory محسوب میشه، اما برای اینکه دادهها بعد از خاموش شدن یا ریاستارت شدن از بین نرن، مکانیزمهایی برای ذخیره کردن اطلاعات روی دیسک داره.
یکی از این روشها RDB (Redis Database Backup) هست.
قبل از RDB، یه نکته مهم دربارهی معماری ردیس 🧠
یکی از ویژگیهای مهم ردیس اینه که بخش اصلی اجرای Commandها به صورت Single Thread انجام میشه. یعنی ردیس معمولاً یک Thread اصلی داره که Commandهای Clientها رو دریافت میکنه، اجرا میکنه و نتیجه رو برمیگردونه. این طراحی باعث میشه ردیس برای خیلی از عملیاتها Performance خیلی بالایی داشته باشه؛ چون پیچیدگی Lock کردن بین چند Thread مختلف رو نداره.
اما همین موضوع یه چالش ایجاد میکنه:
اگه ردیس بخواد وسط پردازش Requestها، چند گیگابایت داده رو روی Disk بنویسه، نباید این کار باعث متوقف شدن Event Loop اصلی بشه.
اینجاست که ردیس از یک تکنیک جالب استفاده میکنه.
RDB دقیقاً چطور کار میکنه؟ 📦
RDB در واقع یک Snapshot از وضعیت ردیس در یک لحظهی مشخصه. یعنی ردیس تمام دادههای موجود در رم رو گرفته و داخل یک فایل Binary با پسوند
.rdb ذخیره میکنه.مثلاً میتونیم تنظیم کنیم:
بعد از 10000 تغییر در 60 ثانیه، یه Snapshot جدید ساخته بشه.
وقتی این شرایط اتفاق بیفته، ردیس فرآیند ساخت RDB رو شروع میکنه.
اما نکته مهم اینجاست:
ردیس نمیتونه Thread اصلی خودش رو برای چند ثانیه یا چند دقیقه متوقف کنه تا کل Memory رو روی Disk بنویسه.
پس میاد یه کمک کوچیک از سیستم عامل میگیره.
نقش fork() در RDB ⚙️
برای ساخت فایل RDB، ردیس از یه System Call به اسم ()fork استفاده میکنه. وقتی ()fork اجرا میشه، سیستمعامل یک Process جدید از Process اصلی ردیس ایجاد میکنه.
بعد از این اتفاق:
- Process اصلی ردیس همچنان Requestهای Clientها رو پردازش میکنه.
- Process جدید مسئول نوشتن Snapshot روی Disk میشه.
یعنی عملیات سنگین نوشتن فایل RDB از مسیر اصلی اجرای ردیس جدا میشه.
اما اینجا یه سؤال مهم پیش میاد:
آیا با fork شدن Process، کل دادههای ردیس دوباره داخل RAM کپی میشن؟
اگه این اتفاق بیفته، برای دیتابیسی که چندین گیگابایت داده داره، عملاً غیرممکن میشه.
اینجاست که Copy-On-Write وارد میشه.
Copy-On-Write چطور کار میکنه؟ 📝
بعد از ()Child Process ,fork و Parent Process در ابتدا Memory مشترکی دارن. یعنی سیستمعامل لازم نیست همون لحظه تمام دادهها رو کپی کنه. هر دو Process فعلاً به یک Page از Memory اشاره میکنن. اما اگه بعد از fork، ردیس بخواد یک داده رو تغییر بده، اون Page خاص باید جدا بشه. یعنی:
- نسخهی قدیمی برای Child Process باقی میمونه تا Snapshot درست ساخته بشه.
- نسخهی جدید برای Parent Process استفاده میشه تا ردیس به کار خودش ادامه بده.
به همین دلیل اسم این تکنیک Copy-On-Write هست. کپی فقط زمانی اتفاق میفته که واقعاً Write انجام بشه.
مشکل Copy-On-Write چیه؟ 🤔
با اینکه Copy-On-Write باعث میشه fork خیلی سریع انجام بشه، اما بدون هزینه هم نیست. اگه ردیس موقع ساخت RDB تغییرات زیادی دریافت کنه، تعداد Pageهایی که باید Copy بشن بیشتر میشه. در نتیجه Memory مصرفی ردیس میتونه برای مدتی افزایش پیدا کنه. به همین دلیل توی سیستمهای بزرگ، زمان اجرای BGSAVE و میزان تغییرات دادهها باید به دقت مانیتور بشه.
Part 01
➖➖➖➖➖➖➖➖➖➖
#️⃣ #system_design #databsae #backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
RDB چه مزایا و معایبی داره؟ 💡
یکی از بزرگترین مزیتهای RDB اینه که فایل خروجی معمولاً حجم کمی داره و ردیس بعد از Restart میتونه خیلی سریعتر دادهها رو دوباره Load کنه.
همچنین چون نوشتن Snapshot به صورت دورهای انجام میشه، فشار کمتری روی دیسک ایجاد میکنه.
اما مشکل اصلی اینجاست:
RDB آخرین وضعیت Memory رو ذخیره نمیکنه، بلکه آخرین Snapshot گرفتهشده رو ذخیره میکنه.
یعنی اگر ردیس Crash بشه، ممکنه تغییراتی که بعد از آخرین Snapshot انجام شدن از بین برن.
مثلاً اگر هر 5 دقیقه Snapshot بگیریم، در بدترین حالت ممکنه 5 دقیقه آخر تغییرات از دست بره.
پس چرا ردیس فقط RDB استفاده نمیکنه؟ 🧩
چون RDB بیشتر برای Performance و Recovery سریع طراحی شده. اما بعضی سیستمها نیاز دارن کوچیکترین تغییر هم از بین نره.
برای این سناریو ردیس روش دیگهای به اسم AOF (Append Only File) داره که به جای Snapshot گرفتن، Commandهای Write رو ذخیره میکنه. که توی پست های بعدی درموردش مینویسم.
جمعبندی 🧠
ردیس با اینکه یک دیتابیس In-Memory هست، با استفاده از Persistence میتونه دادهها رو بعد از Restart هم حفظ کنه. توی روش RDB، ردیس به کمک ()fork یک Process جدا برای ساخت Snapshot ایجاد میکنه و با استفاده از Copy-On-Write بدون متوقف کردن Thread اصلی خودش، دادهها رو روی Disk ذخیره میکنه.
اما مثل هر تصمیم معماری دیگهای، اینجا هم یک Trade-off وجود داره:
RDB سرعت بالا و Recovery سریعتر میده، اما ممکنه بخشی از تغییرات اخیر از بین برن.
برای همین توی سیستمهای واقعی باید با توجه به نیازمون بین Performance، Durability و Complexity انتخاب کنیم.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
یکی از بزرگترین مزیتهای RDB اینه که فایل خروجی معمولاً حجم کمی داره و ردیس بعد از Restart میتونه خیلی سریعتر دادهها رو دوباره Load کنه.
همچنین چون نوشتن Snapshot به صورت دورهای انجام میشه، فشار کمتری روی دیسک ایجاد میکنه.
اما مشکل اصلی اینجاست:
RDB آخرین وضعیت Memory رو ذخیره نمیکنه، بلکه آخرین Snapshot گرفتهشده رو ذخیره میکنه.
یعنی اگر ردیس Crash بشه، ممکنه تغییراتی که بعد از آخرین Snapshot انجام شدن از بین برن.
مثلاً اگر هر 5 دقیقه Snapshot بگیریم، در بدترین حالت ممکنه 5 دقیقه آخر تغییرات از دست بره.
پس چرا ردیس فقط RDB استفاده نمیکنه؟ 🧩
چون RDB بیشتر برای Performance و Recovery سریع طراحی شده. اما بعضی سیستمها نیاز دارن کوچیکترین تغییر هم از بین نره.
برای این سناریو ردیس روش دیگهای به اسم AOF (Append Only File) داره که به جای Snapshot گرفتن، Commandهای Write رو ذخیره میکنه. که توی پست های بعدی درموردش مینویسم.
جمعبندی 🧠
ردیس با اینکه یک دیتابیس In-Memory هست، با استفاده از Persistence میتونه دادهها رو بعد از Restart هم حفظ کنه. توی روش RDB، ردیس به کمک ()fork یک Process جدا برای ساخت Snapshot ایجاد میکنه و با استفاده از Copy-On-Write بدون متوقف کردن Thread اصلی خودش، دادهها رو روی Disk ذخیره میکنه.
اما مثل هر تصمیم معماری دیگهای، اینجا هم یک Trade-off وجود داره:
RDB سرعت بالا و Recovery سریعتر میده، اما ممکنه بخشی از تغییرات اخیر از بین برن.
برای همین توی سیستمهای واقعی باید با توجه به نیازمون بین Performance، Durability و Complexity انتخاب کنیم.
Part 01
➖➖➖➖➖➖➖➖➖➖
#️⃣ #system_design #databsae #backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
Redis چطور تغییرات رو بدون متوقف کردن سرویس روی Disk ذخیره میکنه؟ 🤔
توی پست قبلی دربارهی RDB صحبت کردیم و دیدیم Redis چطور با استفاده از ()fork و Copy-On-Write از دادههای داخل Memory یه Snapshot میگیره. اما RDB یه محدودیت مهم داره:
چون Snapshotها در بازههای زمانی مشخص ساخته میشن، ممکنه تغییراتی که بعد از آخرین Snapshot اتفاق افتادن، در صورت Crash شدن Redis از بین برن.
اینجاست که مکانیزم دیگهی Redis برای Persistence یعنی AOF (Append Only File) وارد میشه.
AOF دقیقاً چیه؟ 🧠
برخلاف RDB که وضعیت نهایی دادهها رو ذخیره میکنه، AOF خود Commandهایی که باعث تغییر داده شدن رو ذخیره میکنه. یعنی به جای اینکه وضعیت فعلی دیتابیس رو بررسی کنه و ازش Snapshot بگیره، Commandها و ترتیب اجرا شدن اونها رو داخل فایل AOF ثبت میکنه. بعد از Restart شدن Redis، با خوندن همین فایل Commandها دوباره اجرا میشن و وضعیت قبلی بازسازی میشه.
مشکل اصلی AOF چیه؟ 💿
اگر Redis بخواد بعد از هر Write، مستقیم روی دیسک بنویسه، سرعتش خراب نمیشه؟ جواب کوتاه: بله.
دیسک نسبت به RAM خیلی کندتره. Redis سرعت بالاش رو از این میگیره که عملیاتها رو داخل Memory انجام میده. پس اگر برای هر Command مجبور باشه منتظر نوشتن روی دیسک بمونه، تمام مزیت In-Memory بودنش زیر سؤال میره. برای همین Redis نوشتن AOF رو از مسیر اصلی اجرای Command جدا میکنه.
Redis چطور Write به Disk رو Non-blocking میکنه؟ ⚙️
Redis یک Thread اصلی داره که Commandهای Clientها رو پردازش میکنه. این Thread نباید برای عملیاتهای کند مثل Disk I/O منتظر بمونه. به همین دلیل، وقتی یه Write اتفاق میفته، Redis اول Command رو داخل یه Buffer توی Memory قرار میده. بعد در زمان مناسب، این دادهها از Buffer به فایل AOF منتقل میشن.
در نتیجه Redis میتونه همچنان Requestهای جدید رو پردازش کنه و منتظر Disk نباشه.
حالت های مختلف AOF
میتونیم یکی از سه حالت AOF رو داشته باشیم:
AOF Rewrite چرا لازم میشه؟ 📝
با گذشت زمان، فایل AOF بزرگ و بزرگتر میشه. مثلاً ممکنه یه Key چند هزار بار تغییر کنه. AOF ممکنه تمام تغییرات قبلی رو نگه داشته باشه و یه فایل خیلی بزرگ ایجاد کرده باشه، در حالی که فقط آخرین مقدار مهمه. برای حل این مشکل Redis از AOF Rewrite استفاده میکنه.
توی Rewrite، Redis یک فایل AOF جدید تولید میکنه که فقط شامل Commandهای لازم برای ساخت وضعیت فعلی دیتابیسه.
مثلاً به جای:
فقط آخرین وضعیت ذخیره میشه:
AOF بهتره یا RDB؟ 🤔
مثل خیلی از تصمیمهای معماری، جواب مطلقی وجود نداره.
RDB بیشتر برای:
- Backup گرفتن
- Recovery سریعتر
- حجم کمتر فایل
مناسبه.
AOF بیشتر برای:
- از دست ندادن تغییرات اخیر
- Durability بالاتر
- ثبت دقیقتر عملیاتها
- استفاده میشه.
به همین دلیل خیلی از سیستمهای واقعی حتی هر دو روش رو همزمان فعال میکنن.
جمعبندی ✍️
AOF یکی از روشهای Redis برای حفظ دادهها بعد از Restart یا Crash شدنه. توی این روش Redis به جای ذخیرهی Command, Snapshotهای تغییر دهندهی داده رو ثبت میکنه.
برای اینکه سرعت Redis کاهش پیدا نکنه، نوشتن روی Disk از مسیر اصلی اجرای Command جدا میشه و با Buffer مدیریت میشه.
اما اینجا هم مثل RDB یک Trade-off وجود داره:
هرچی بخوایم داده امنتر ذخیره بشه، هزینهی بیشتری از نظر Performance پرداخت میکنیم.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
توی پست قبلی دربارهی RDB صحبت کردیم و دیدیم Redis چطور با استفاده از ()fork و Copy-On-Write از دادههای داخل Memory یه Snapshot میگیره. اما RDB یه محدودیت مهم داره:
چون Snapshotها در بازههای زمانی مشخص ساخته میشن، ممکنه تغییراتی که بعد از آخرین Snapshot اتفاق افتادن، در صورت Crash شدن Redis از بین برن.
اینجاست که مکانیزم دیگهی Redis برای Persistence یعنی AOF (Append Only File) وارد میشه.
AOF دقیقاً چیه؟ 🧠
برخلاف RDB که وضعیت نهایی دادهها رو ذخیره میکنه، AOF خود Commandهایی که باعث تغییر داده شدن رو ذخیره میکنه. یعنی به جای اینکه وضعیت فعلی دیتابیس رو بررسی کنه و ازش Snapshot بگیره، Commandها و ترتیب اجرا شدن اونها رو داخل فایل AOF ثبت میکنه. بعد از Restart شدن Redis، با خوندن همین فایل Commandها دوباره اجرا میشن و وضعیت قبلی بازسازی میشه.
مشکل اصلی AOF چیه؟ 💿
اگر Redis بخواد بعد از هر Write، مستقیم روی دیسک بنویسه، سرعتش خراب نمیشه؟ جواب کوتاه: بله.
دیسک نسبت به RAM خیلی کندتره. Redis سرعت بالاش رو از این میگیره که عملیاتها رو داخل Memory انجام میده. پس اگر برای هر Command مجبور باشه منتظر نوشتن روی دیسک بمونه، تمام مزیت In-Memory بودنش زیر سؤال میره. برای همین Redis نوشتن AOF رو از مسیر اصلی اجرای Command جدا میکنه.
Redis چطور Write به Disk رو Non-blocking میکنه؟ ⚙️
Redis یک Thread اصلی داره که Commandهای Clientها رو پردازش میکنه. این Thread نباید برای عملیاتهای کند مثل Disk I/O منتظر بمونه. به همین دلیل، وقتی یه Write اتفاق میفته، Redis اول Command رو داخل یه Buffer توی Memory قرار میده. بعد در زمان مناسب، این دادهها از Buffer به فایل AOF منتقل میشن.
در نتیجه Redis میتونه همچنان Requestهای جدید رو پردازش کنه و منتظر Disk نباشه.
حالت های مختلف AOF
میتونیم یکی از سه حالت AOF رو داشته باشیم:
Always
توی این حالت، بعد از هرRedis, Command مجبور میشه داده رو روی دیسک Sync کنه. این حالت بیشترین میزان durability رو میده، چون احتمال از دست رفتن داده خیلی کمه. اما هزینهی Performance بالایی داره، چون تعداد زیادی عملیات دیسک در هر ثانیه انجام میشه.
Every Second
این حالت پیش فرض رایج Redis هست. Redis دادهها رو تقریبا هر 1 ثانیه یک بار، sync میکنه. توی این حالت، Performance خیلی بهتره و در بدترین حالت ممکنه حدود یک ثانیه از تغییرات از بین بره. برای خیلی از سیستمهای Production این Trade-off منطقیه.
No
توی این حالت Redis کنترل sync رو به سیستمعامل میسپاره. Performance بالاتره، اما تضمین کمتری برای حفظ داده وجود داره.
AOF Rewrite چرا لازم میشه؟ 📝
با گذشت زمان، فایل AOF بزرگ و بزرگتر میشه. مثلاً ممکنه یه Key چند هزار بار تغییر کنه. AOF ممکنه تمام تغییرات قبلی رو نگه داشته باشه و یه فایل خیلی بزرگ ایجاد کرده باشه، در حالی که فقط آخرین مقدار مهمه. برای حل این مشکل Redis از AOF Rewrite استفاده میکنه.
توی Rewrite، Redis یک فایل AOF جدید تولید میکنه که فقط شامل Commandهای لازم برای ساخت وضعیت فعلی دیتابیسه.
مثلاً به جای:
SET counter 1
SET counter 2
SET counter 3
SET counter 4
فقط آخرین وضعیت ذخیره میشه:
SET counter 4
AOF بهتره یا RDB؟ 🤔
مثل خیلی از تصمیمهای معماری، جواب مطلقی وجود نداره.
RDB بیشتر برای:
- Backup گرفتن
- Recovery سریعتر
- حجم کمتر فایل
مناسبه.
AOF بیشتر برای:
- از دست ندادن تغییرات اخیر
- Durability بالاتر
- ثبت دقیقتر عملیاتها
- استفاده میشه.
به همین دلیل خیلی از سیستمهای واقعی حتی هر دو روش رو همزمان فعال میکنن.
جمعبندی ✍️
AOF یکی از روشهای Redis برای حفظ دادهها بعد از Restart یا Crash شدنه. توی این روش Redis به جای ذخیرهی Command, Snapshotهای تغییر دهندهی داده رو ثبت میکنه.
برای اینکه سرعت Redis کاهش پیدا نکنه، نوشتن روی Disk از مسیر اصلی اجرای Command جدا میشه و با Buffer مدیریت میشه.
اما اینجا هم مثل RDB یک Trade-off وجود داره:
هرچی بخوایم داده امنتر ذخیره بشه، هزینهی بیشتری از نظر Performance پرداخت میکنیم.
Part 02 | Final
➖➖➖➖➖➖➖➖➖➖
#️⃣ #system_design #databsae #backend
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤2
چرا Nginx برای هر Request یک Process جدید نمیسازه؟ 🤔
وقتی اسم Web Server میاد، معمولاً اولین چیزی که به ذهنمون میاد اینه که یک Request وارد میشه، سرور اون رو پردازش میکنه و جواب رو برمیگردونه. اما چیزی که کمتر بهش توجه میشه، اینه که خود Web Server چطور تصمیم میگیره این Requestها رو مدیریت کنه.
این تصمیم تأثیر مستقیمی روی Performance، مصرف Memory و تعداد Connectionهایی داره که سرور میتونه همزمان مدیریت کنه.
معماری Worker per Request یعنی چی؟ 🧠
سالها خیلی از Web Serverها از مدلی استفاده میکردن که برای هر Request یا هر Connection، یک Process یا Thread جداگانه ایجاد میشد.
ایدهی این معماری خیلی ساده بود. هر Request یک Worker مخصوص خودش رو داره، اون Worker Request رو پردازش میکنه و بعد از تموم شدن کارش آزاد میشه یا از بین میره. این مدل پیادهسازی نسبتاً سادهای داره و هر Worker کاملاً مستقل از بقیه کار میکنه. اما با زیاد شدن تعداد Requestها، تعداد Workerها هم بیشتر میشه و همین موضوع کمکم به یک مشکل تبدیل میشه.
مشکل این معماری چیه؟ ⚠️
ساختن یک Process یا حتی یک Thread رایگان نیست. سیستمعامل باید برای هر Worker حافظه اختصاص بده، منابع لازم رو آماده کنه و بین Workerهای مختلف Context Switch انجام بده.
شاید برای چند ده Request این هزینه خیلی محسوس نباشه، اما وقتی هزاران Connection همزمان وارد سرور بشن، تعداد Workerها هم به همون نسبت زیاد میشه.
در نتیجه بخش قابل توجهی از منابع سیستم صرف مدیریت خود Workerها میشه، نه پردازش Requestها.
به همین دلیل این مدل برای بارهای سنگین، خیلی مقیاسپذیر نیست.
Nginx چه راهی رو انتخاب کرد؟ ⚙️
Nginx از همون اول مسیر متفاوتی رو انتخاب کرد. به جای اینکه با هر Request یک Worker جدید بسازه، تعداد محدودی Worker Process موقع بالا اومدن سرویس ایجاد میکنه.
این Workerها تا زمانی که Nginx در حال اجراست، زنده میمونن و دیگه برای هر Request جدید، خبری از ساختن Process یا Thread نیست.
در نتیجه هزینهی ایجاد Worker فقط یک بار پرداخت میشه و همهی Requestهای بعدی توسط همون Workerهای موجود مدیریت میشن.
تعداد Workerها چطور مشخص میشه؟ 📌
توی بیشتر سناریوها، Nginx به ازای هر CPU Core یک Worker Process اجرا میکنه. مثلاً روی یک سرور ۸ هستهای، معمولاً ۸ Worker خواهیم داشت.
دلیلش هم اینه که هر Worker میتونه روی یک Core اجرا بشه و از توان پردازشی سیستم به بهترین شکل استفاده کنه، بدون اینکه صدها یا هزاران Process همزمان داخل سیستم ایجاد بشه.
به همین خاطر هم تنظیم
آیا این یعنی Nginx همیشه بهتره؟ 💡
نه لزوماً. مدل Worker per Request هنوز هم توی بعضی سناریوها میتونه انتخاب مناسبی باشه و Web Serverهایی مثل Apache هم در طول سالها مدلهای مختلفی مثل Worker MPM و Event MPM رو برای کاهش محدودیتهای معماری قدیمی معرفی کردن.
اما چیزی که Nginx رو از همون اول متفاوت کرد، این بود که معماری خودش رو بر پایهی تعداد کمی Worker Process طراحی کرد، نه تعداد زیادی Worker برای هر Request.
همین تصمیم باعث شد با مصرف Memory کمتر و سربار پایینتر، بتونه تعداد خیلی بیشتری Connection همزمان رو مدیریت کنه.
جمعبندی ✍️
یکی از مهمترین تفاوتهای Nginx با Web Serverهای دیگه، نحوهی مدیریت Requestهاست.
به جای اینکه برای هر Request یک Process یا Thread جدید ایجاد کنه، از تعداد محدودی Worker Process استفاده میکنه که از زمان بالا اومدن سرویس در حال اجرا هستن.
این تصمیم باعث کاهش مصرف Memory، کم شدن سربار سیستم و افزایش مقیاسپذیری Nginx شده.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
وقتی اسم Web Server میاد، معمولاً اولین چیزی که به ذهنمون میاد اینه که یک Request وارد میشه، سرور اون رو پردازش میکنه و جواب رو برمیگردونه. اما چیزی که کمتر بهش توجه میشه، اینه که خود Web Server چطور تصمیم میگیره این Requestها رو مدیریت کنه.
این تصمیم تأثیر مستقیمی روی Performance، مصرف Memory و تعداد Connectionهایی داره که سرور میتونه همزمان مدیریت کنه.
معماری Worker per Request یعنی چی؟ 🧠
سالها خیلی از Web Serverها از مدلی استفاده میکردن که برای هر Request یا هر Connection، یک Process یا Thread جداگانه ایجاد میشد.
ایدهی این معماری خیلی ساده بود. هر Request یک Worker مخصوص خودش رو داره، اون Worker Request رو پردازش میکنه و بعد از تموم شدن کارش آزاد میشه یا از بین میره. این مدل پیادهسازی نسبتاً سادهای داره و هر Worker کاملاً مستقل از بقیه کار میکنه. اما با زیاد شدن تعداد Requestها، تعداد Workerها هم بیشتر میشه و همین موضوع کمکم به یک مشکل تبدیل میشه.
مشکل این معماری چیه؟ ⚠️
ساختن یک Process یا حتی یک Thread رایگان نیست. سیستمعامل باید برای هر Worker حافظه اختصاص بده، منابع لازم رو آماده کنه و بین Workerهای مختلف Context Switch انجام بده.
شاید برای چند ده Request این هزینه خیلی محسوس نباشه، اما وقتی هزاران Connection همزمان وارد سرور بشن، تعداد Workerها هم به همون نسبت زیاد میشه.
در نتیجه بخش قابل توجهی از منابع سیستم صرف مدیریت خود Workerها میشه، نه پردازش Requestها.
به همین دلیل این مدل برای بارهای سنگین، خیلی مقیاسپذیر نیست.
Nginx چه راهی رو انتخاب کرد؟ ⚙️
Nginx از همون اول مسیر متفاوتی رو انتخاب کرد. به جای اینکه با هر Request یک Worker جدید بسازه، تعداد محدودی Worker Process موقع بالا اومدن سرویس ایجاد میکنه.
این Workerها تا زمانی که Nginx در حال اجراست، زنده میمونن و دیگه برای هر Request جدید، خبری از ساختن Process یا Thread نیست.
در نتیجه هزینهی ایجاد Worker فقط یک بار پرداخت میشه و همهی Requestهای بعدی توسط همون Workerهای موجود مدیریت میشن.
تعداد Workerها چطور مشخص میشه؟ 📌
توی بیشتر سناریوها، Nginx به ازای هر CPU Core یک Worker Process اجرا میکنه. مثلاً روی یک سرور ۸ هستهای، معمولاً ۸ Worker خواهیم داشت.
دلیلش هم اینه که هر Worker میتونه روی یک Core اجرا بشه و از توان پردازشی سیستم به بهترین شکل استفاده کنه، بدون اینکه صدها یا هزاران Process همزمان داخل سیستم ایجاد بشه.
به همین خاطر هم تنظیم
worker_processes auto; یکی از رایجترین تنظیمات Nginx محسوب میشه.آیا این یعنی Nginx همیشه بهتره؟ 💡
نه لزوماً. مدل Worker per Request هنوز هم توی بعضی سناریوها میتونه انتخاب مناسبی باشه و Web Serverهایی مثل Apache هم در طول سالها مدلهای مختلفی مثل Worker MPM و Event MPM رو برای کاهش محدودیتهای معماری قدیمی معرفی کردن.
اما چیزی که Nginx رو از همون اول متفاوت کرد، این بود که معماری خودش رو بر پایهی تعداد کمی Worker Process طراحی کرد، نه تعداد زیادی Worker برای هر Request.
همین تصمیم باعث شد با مصرف Memory کمتر و سربار پایینتر، بتونه تعداد خیلی بیشتری Connection همزمان رو مدیریت کنه.
جمعبندی ✍️
یکی از مهمترین تفاوتهای Nginx با Web Serverهای دیگه، نحوهی مدیریت Requestهاست.
به جای اینکه برای هر Request یک Process یا Thread جدید ایجاد کنه، از تعداد محدودی Worker Process استفاده میکنه که از زمان بالا اومدن سرویس در حال اجرا هستن.
این تصمیم باعث کاهش مصرف Memory، کم شدن سربار سیستم و افزایش مقیاسپذیری Nginx شده.
Part 01
➖➖➖➖➖➖➖➖➖➖
#️⃣ #web #backend #system_design
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
👍4