Midnight Commit
105 subscribers
3 photos
1 file
41 links
Beyond Syntax.
The fundamentals behind great software.
Download Telegram
چرا 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 همزمان داخل سیستم ایجاد بشه.
به همین خاطر هم تنظیم 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
‏NGINX چطور هزاران Connection رو همزمان مدیریت می‌کنه؟ 🤔
توی پست قبلی درباره‌ی مدل مدیریت Request توی NGINX صحبت کردیم و دیدیم که NGINX به جای ساختن Worker جدید برای هر Request، از تعداد محدودی Worker Process استفاده می‌کنه.
اما یه سؤال مهم باقی می‌مونه:
چطور همین تعداد محدود Worker می‌تونن هزاران Connection همزمان رو مدیریت کنن؟
جواب این سؤال به مفهومی به اسم Event Loop برمی‌گرده.

مشکل اصلی کجاست؟ 🧠
وقتی یه Request وارد NGINX میشه، تمام زمان صرف پردازش CPU نمی‌شه. بخش زیادی از زمان یک Request صرف منتظر موندن برای اتفاق‌های دیگه میشه.
مثلاً:
- منتظر دریافت کامل اطلاعات از Client
- منتظر پاسخ یک سرویس Backend
- منتظر خواندن اطلاعات از Disk
- منتظر آماده شدن Socket برای ارسال داده
یعنی توی خیلی از مواقع، NGINX کاری برای انجام دادن نداره و فقط منتظر یک Event هست. یه راه ساده اینه که Event Loop خودش مدام تمام Connectionها رو بررسی کنه، اما این روش وقتی تعداد Connectionها زیاد بشه، مشکل‌ساز میشه. چون باید دائماً تعداد زیادی Connection بررسی بشن، حتی اون‌هایی که هیچ تغییری نکردن. در نتیجه بخشی از زمان CPU صرف چک کردن وضعیت Connectionها میشه، نه انجام کار واقعی.

‏Event Loop چه کاری انجام میده؟ 🔄
ایده‌ی Event Loop اینه که به جای اینکه NGINX خودش دنبال Eventها بگرده، منتظر اطلاع‌رسانی بمونه. یعنی NGINX به جای اینکه مرتب بپرسه:
"کدوم Connection آماده شده؟"
این کار رو به سیستم‌عامل می‌سپاره. سیستم‌عامل خودش مدیریت می‌کنه که کدوم Connectionها آماده‌ی خواندن یا نوشتن هستن و وقتی اتفاقی افتاد، به NGINX اطلاع میده. در نتیجه NGINX فقط زمانی پردازش انجام میده که واقعاً کاری برای انجام دادن وجود داشته باشه.

نقش سیستم‌عامل و epoll ⚙️
برای اینکه برنامه‌ها بتونن تعداد زیادی Connection رو مدیریت کنن، سیستم‌عامل مکانیزم‌هایی برای مدیریت I/O در اختیارشون قرار میده.
توی Linux یکی از این مکانیزم‌ها epoll هست.
ایده‌ی اصلی epoll اینه که NGINX به جای اینکه خودش دائماً وضعیت Connectionها رو بررسی کنه، این کار رو به سیستم‌عامل بسپاره. NGINX Connectionهایی که براش مهم هستن رو به epoll معرفی می‌کنه.
این Connectionها داخل چیزی به اسم Interest List نگهداری میشن. یعنی یک لیست از Eventهایی که NGINX منتظرشونه.
مثلاً NGINX میگه:
- اگر از این Client داده‌ی جدید رسید، من رو خبر کن.
- اگر این Socket آماده‌ی ارسال Response شد، من رو خبر کن.
حالا سیستم‌عامل وضعیت این Connectionها رو زیر نظر می‌گیره.
وقتی یکی از Eventها اتفاق بیفته، اون Connection از حالت "منتظر" خارج میشه و داخل Ready List قرار می‌گیره. ‏Ready List در واقع لیست Connectionهایی هست که الان واقعاً آماده‌ی پردازش هستن. بعد NGINX فقط همین Eventهای آماده رو دریافت می‌کنه و روی اون‌ها کار می‌کنه. در نتیجه به جای اینکه هزاران Connection رو یکی‌یکی بررسی کنه، فقط سراغ Connectionهایی میره که واقعاً اتفاقی براشون افتاده.

چرا این روش Performance بهتری داره؟ 🚀
چون تعداد زیادی Connection ممکنه همزمان باز باشن، اما در هر لحظه فقط تعداد کمی از اون‌ها نیاز به پردازش داشته باشن. Event Loop باعث میشه NGINX انرژی خودش رو روی Connectionهای فعال بذاره، نه اینکه مدام وضعیت Connectionهای بدون تغییر رو بررسی کنه. به همین دلیل NGINX می‌تونه با منابع محدود، تعداد زیادی Connection همزمان رو مدیریت کنه.

جمع‌بندی ✍️
قدرت NGINX فقط به این نیست که چند Worker Process اجرا می‌کنه. بخش مهم معماری اون، استفاده از Event Loop برای مدیریت Connectionهاست. به جای اینکه دائماً وضعیت همه‌ی Requestها رو بررسی کنه، از سیستم‌عامل کمک می‌گیره تا فقط وقتی اتفاق مهمی افتاد، مطلع بشه. توی Linux این کار با مکانیزم‌هایی مثل epoll انجام میشه که باعث میشن NGINX بتونه تعداد زیادی Connection همزمان رو با هزینه‌ی کمتر مدیریت کنه.

Part 02 | Final


#️⃣ #web #backend #system_design


🌙 CHANNEL | GROUP
1
‏Redis چطور با یک Thread تعداد زیادی Request رو مدیریت می‌کنه؟ 🤔
وقتی درباره‌ی Performance صحبت می‌کنیم، معمولاً یکی از اولین چیزهایی که به ذهنمون میاد استفاده از Threadهای بیشتره. منطق هم ساده به نظر میرسه: Thread بیشتر یعنی کار بیشتر، پس سرعت بالاتر.
اما Redis سال‌هاست با یک مدل متفاوت کار می‌کنه. Redis بخش اصلی پردازش Commandهای خودش رو با یک Thread اصلی انجام میده و با همین معماری می‌تونه تعداد زیادی Request رو در مدت زمان کوتاه مدیریت کنه.

مشکل استفاده از چند Thread چیه؟ 🧠
وقتی چند Thread همزمان روی یک داده کار می‌کنن، باید مراقب دسترسی همزمان اون‌ها باشیم. مثلاً وقتی دو Thread همزمان بخوان یک مقدار رو تغییر بدن. اینجا باید از Lock استفاده کنیم تا داده خراب نشه.
اما Lockها خودشون هزینه دارن. Threadها برای دسترسی به منابع مشترک باید منتظر هم بمونن و سیستم‌عامل هم باید بین Threadهای مختلف Context Switch انجام بده. توی سیستم‌هایی که تعداد زیادی عملیات کوچیک و سریع دارن، همین هماهنگی بین Threadها می‌تونه تبدیل به یک سربار قابل توجه بشه.
‏Redis با یک تصمیم معماری متفاوت، بخش زیادی از این پیچیدگی رو حذف می‌کنه.

‏Redis چطور با یک Thread کار میکنه؟ ⚙️
توی Command, Redisهای Clientها وارد مسیر پردازش میشن و Thread اصلی Redis اون‌ها رو یکی‌یکی اجرا می‌کنه. یعنی در یک لحظه، فقط یک Command در حال تغییر State اصلی Redis هست.
نتیجه‌ی این کار اینه که Redis برای داده‌های داخل Memory خودش نیاز زیادی به Locking نداره. چون چند Thread مختلف وجود ندارن که همزمان بخوان یک داده رو تغییر بدن. این موضوع باعث میشه عملیات‌هایی مثل خوندن یا تغییر یک Key با سربار خیلی کمی انجام بشن.

‏Redis چطور همزمان چندین Connection رو مدیریت می‌کنه؟ 🔄
‏Single Thread بودن Redis به این معنی نیست که فقط یک Client می‌تونه به Redis وصل باشه. در واقع Redis می‌تونه هزاران Connection باز داشته باشه، اما نکته اینجاست که Thread اصلی Redis نباید منتظر تک‌تک این Connectionها بمونه.
چون بیشتر Connectionها در هر لحظه کاری برای انجام دادن ندارن. اینجا Redis از یک معماری Event-Driven استفاده می‌کنه. به جای اینکه دائماً همه‌ی Connectionها رو بررسی کنه، این کار رو به سیستم‌عامل می‌سپاره.

توی Redis, Linuxاز مکانیزم‌هایی مثل epoll استفاده می‌کنه. Connection, Redisهای مورد نظرش رو به epoll معرفی می‌کنه و سیستم‌عامل وضعیت اون‌ها رو دنبال می‌کنه. وقتی یک Eventها اتفاق بیفته، سیستم‌عامل اون Connection رو توی لیست Eventهای آماده قرار میده و Redis فقط همون مواردی که واقعاً نیاز به پردازش دارن رو بررسی می‌کنه. در نتیجه Redis مجبور نیست هزاران Connection رو یکی‌یکی بررسی کنه و فقط زمانی CPU مصرف می‌کنه که واقعاً کاری برای انجام دادن وجود داشته باشه.

چرا این معماری برای Redis جواب میده؟ 🚀
‏Redis بیشتر برای عملیات‌های سریع روی داده‌های داخل Memory طراحی شده. پس به جای اینکه زمان زیادی صرف مدیریت تعداد زیادی Thread و هماهنگی بین اون‌ها کنه، یک مدل ساده‌تر انتخاب کرده:
یک مسیر پردازش اصلی، بدون Lockهای زیاد، همراه با مدیریت هوشمند Connectionها.
همین باعث شده Redis با وجود Single Thread بودن، بتونه Performance بسیار بالایی ارائه بده.

جمع‌بندی ✍️
با حذف پیچیدگی Lockها و استفاده از Event Loop و قابلیت‌هایی مثل Redis, epoll می‌تونه تعداد زیادی Connection همزمان رو مدیریت کنه، بدون اینکه برای هر Connection یک Thread جدا ایجاد کنه.
درک این معماری کمک می‌کنه بهتر بفهمیم چرا Redis برای عملیات‌های سریع و In-Memory یکی از محبوب‌ترین انتخاب‌هاست.

#️⃣ #system_design #databsae #backend


🌙 CHANNEL | GROUP
1
‏چطور تعداد Requestها رو کنترل می‌کنیم؟ 🤔
وقتی یک API یا یک سرویس در اختیار کاربرا قرار می‌دیم، یکی از مشکلاتی که دیر یا زود باهاش مواجه می‌شیم، حجم زیاد Requestهاست. این حجم زیاد می‌تونه دلایل مختلفی داشته باشه؛ از استفاده‌ی عادی کاربرا گرفته تا یک Client که به دلیل خطا تعداد زیادی درخواست ارسال می‌کنه یا حتی یک حمله‌ی عمدی که هدفش مصرف منابع سیستم هست.
اگر هیچ محدودیتی روی تعداد Requestها وجود نداشته باشه، یک Client می‌تونه بخش زیادی از منابع سیستم مثل CPU، Memory یا Connectionهای Database رو مصرف کنه و روی عملکرد کل سرویس تأثیر بذاره.
اینجاست که مفهومی به اسم Rate Limiting وارد میشه. Rate Limiting مشخص می‌کنه یک Client در یک بازه‌ی زمانی مشخص، چه تعداد Request می‌تونه ارسال کنه. اما نکته‌ی مهم اینجاست که روش پیاده‌سازی این محدودیت اهمیت زیادی داره. الگوریتم‌های مختلفی برای Rate Limiting وجود دارن که هرکدوم رفتار متفاوتی در برابر حجم‌های مختلف Traffic دارن.

‏Fixed Window Counter 📦
‏Fixed Window یکی از ساده‌ترین روش‌های پیاده‌سازی Rate Limiting هست. توی این الگوریتم، زمان به بازه‌های مشخص تقسیم میشه و برای هر بازه یک Counter نگهداری میشه. هر Request که وارد سیستم میشه، Counter مربوط به Window فعلی افزایش پیدا می‌کنه و وقتی تعداد Requestها از مقدار مشخص شده بیشتر بشه، درخواست‌های بعدی تا پایان اون Window محدود میشن.
سادگی این روش یکی از بزرگ‌ترین مزیت‌هاشه، چون پیاده‌سازی و نگهداری اون خیلی راحت هست. اما مشکل اصلی Fixed Window اینه که مرز بین Windowها می‌تونه باعث رفتار غیرقابل پیش‌بینی بشه. چون الگوریتم فقط داخل یک بازه‌ی مشخص رو بررسی می‌کنه و اطلاعی از توزیع دقیق Requestها در اطراف مرز دو Window نداره.
به همین دلیل ممکنه توی بعضی شرایط، تعداد زیادی Request در زمان کوتاهی وارد سیستم بشه، در حالی که از نظر هر Window جداگانه، محدودیت نقض نشده.

‏Sliding Window 📊
‏Sliding Window برای حل مشکل اصلی Fixed Window طراحی شده. توی این روش به جای تقسیم کردن زمان به بازه‌های ثابت، همیشه یک بازه‌ی زمانی متحرک نسبت به لحظه‌ی فعلی بررسی میشه. یعنی برای هر Request، سیستم بررسی می‌کنه در مدت زمان مشخص گذشته چند درخواست ثبت شده و بر اساس همین مقدار تصمیم می‌گیره که Request جدید اجازه‌ی عبور داشته باشه یا نه.
این روش باعث میشه محدودیت دقیق‌تری داشته باشیم، چون دیگه وابسته به مرزهای ثابت زمانی نیستیم. اما در مقابل، پیاده‌سازی Sliding Window پیچیده‌تره. چون سیستم باید اطلاعات بیشتری درباره‌ی Requestهای قبلی نگه داره و محاسبه‌ی تعداد Requestها در یک بازه‌ی متحرک هزینه‌ی بیشتری نسبت به Fixed Window داره.
برای سیستم‌هایی که دقت Rate Limiting اهمیت بیشتری از سادگی داره، این روش انتخاب مناسب‌تری محسوب میشه.

‏Token Bucket 🪣
‏Token Bucket یکی از محبوب‌ترین الگوریتم‌های Rate Limiting توی سیستم‌های واقعی هست. ایده‌ی اصلی این الگوریتم اینه که به جای شمردن مستقیم Requestها، یک Bucket از Tokenها داشته باشیم. هر Request برای پردازش شدن باید یک Token مصرف کنه و Tokenها با یک سرعت مشخص دوباره به Bucket اضافه میشن. اگه Bucket پر باشه، Tokenهای جدید اضافه نمی‌شن و اگه Request جدیدی وارد بشه ولی Token کافی وجود نداشته باشه، اون Request باید منتظر بمونه یا Reject بشه.
مزیت مهم Token Bucket اینه که برخلاف محدودیت‌های ساده، می‌تونه Burstهای کوتاه رو مدیریت کنه.
یعنی اگر برای مدتی Request کمی وجود داشته باشه، Token داخل Bucket ذخیره میشه و بعداً سیستم می‌تونه برای مدت کوتاهی حجم بیشتری از Requestها رو تحمل کنه.
به همین دلیل Token Bucket توی بسیاری از APIها و سرویس‌های بزرگ استفاده میشه، چون بین محدود کردن Traffic و حفظ تجربه‌ی کاربر تعادل خوبی ایجاد می‌کنه.

#️⃣ #system_design #web #backend


🌙 CHANNEL | GROUP
🔥1
‏Leaky Bucket 🪣
‏Leaky Bucket از نظر ایده شباهت‌هایی به Token Bucket داره، اما هدف اصلی اون کنترل نرخ خروجی Requestهاست. توی این الگوریتم، Requestهای ورودی وارد یک Queue میشن و با یک سرعت ثابت پردازش میشن. یعنی حتی اگه تعداد زیادی Request در یک لحظه وارد سیستم بشه، خروجی با یک Rate مشخص انجام میشه. این رفتار باعث میشه فشار ناگهانی روی بخش‌های دیگر سیستم کاهش پیدا کنه و Traffic ورودی به شکل کنترل‌شده‌تری پردازش بشه.
اما محدودیت اصلی Leaky Bucket اینه که اگه سرعت ورود Requestها برای مدت طولانی بیشتر از سرعت پردازش باشه، Queue پر میشه و Requestهای جدید باید حذف یا Reject بشن.
به همین دلیل این الگوریتم بیشتر برای سیستم‌هایی مناسبه که کنترل ثابت روی نرخ پردازش اهمیت زیادی داره.

‏انتخاب الگوریتم مناسب 💡

هیچ‌کدوم از این الگوریتم‌ها همیشه بهترین انتخاب نیستن.
‏Fixed Window زمانی مناسب هست که سادگی و Performance پیاده‌سازی اهمیت زیادی داشته باشه.
‏Sliding Window محدودیت دقیق‌تری ایجاد می‌کنه، اما هزینه‌ی بیشتری برای نگهداری اطلاعات Requestها داره.
‏Token Bucket برای بیشتر APIهای عمومی انتخاب محبوبیه، چون هم محدودیت ایجاد می‌کنه و هم اجازه‌ی Burstهای کوتاه رو میده.
‏Leaky Bucket هم زمانی مفیده که بخوایم جریان پردازش Requestها کاملاً کنترل‌شده و ثابت باشه.
انتخاب درست Rate Limiting Algorithm به رفتار Traffic، نیاز سیستم و میزان حساسیت روی Consistency بستگی داره.

جمع‌بندی ✍️
‏Rate Limiting یکی از بخش‌های مهم طراحی سیستم‌های مقیاس‌پذیر هست. هدف اون فقط جلوگیری از Requestهای زیاد نیست، بلکه کمک می‌کنه منابع سیستم پایدار بمونن و یک Client نتونه روی عملکرد کل سرویس تأثیر منفی بذاره.
الگوریتم‌های مختلف Rate Limiting هرکدوم یک Trade-off بین سادگی، دقت و کنترل ایجاد می‌کنن و انتخاب درست اون‌ها بخشی از طراحی یک سیستم قابل اعتماد محسوب میشه.

#️⃣ #system_design #web #backend


🌙 CHANNEL | GROUP
2
‏TCP Connection چطور ساخته و بسته میشه؟ 🤔
وقتی یک Client می‌خواد با یک Server ارتباط برقرار کنه، معمولاً اولین چیزی که به ذهنمون میاد اینه که: "یک Connection ساخته میشه و بعد داده‌ها رد و بدل میشن."
اما پشت این Connection ساده، یک فرآیند دقیق وجود داره که TCP برای ایجاد یک ارتباط قابل اعتماد انجام میده. TCP فقط یک کانال برای ارسال داده نیست؛ بلکه مسئولیت‌هایی مثل اطمینان از رسیدن داده‌ها، ترتیب درست Packetها و مدیریت وضعیت Connection رو هم برعهده داره. برای همین، قبل از اینکه حتی یک Byte داده ارسال بشه، Client و Server باید با هم هماهنگ بشن.

‏TCP Three-Way Handshake 🔄
برای ایجاد یک اتصال، TCP از فرآیندی به اسم Three-Way Handshake استفاده می‌کنه. دلیل وجود این مرحله اینه که دو طرف مطمئن بشن:
- طرف مقابل آماده‌ی برقراری ارتباط هست.
- هر دو طرف شماره‌های Sequence مناسب برای ارسال و دریافت داده رو دارن.
- ‏Connection از هر دو سمت به درستی ایجاد شده.

این فرآیند با ارسال یک Packet از سمت Client شروع میشه:
‏ 1 - Client یک Packet با Flag به نام SYN ارسال می‌کنه. SYN مخفف Synchronize هست و در واقع درخواست شروع یک Connection محسوب میشه.
‏ 2 - Server بعد از دریافت این درخواست، اگر آماده باشه، یک Packet با دو Flag یعنی SYN + ACK ارسال می‌کنه. SYN یعنی Server هم آماده‌ی شروع ارتباطه و ACK یعنی درخواست Client رو دریافت کرده.
3 - در مرحله‌ی آخر، Client یک ACK ارسال می‌کنه و بعد از این مرحله، Connection از هر دو سمت برقرار شده و انتقال داده شروع میشه.

چرا TCP به سه مرحله نیاز داره؟ 🧠
شاید این سؤال پیش بیاد که چرا فقط یک پیام برای ایجاد Connection کافی نیست؟ چون TCP باید مطمئن بشه هر دو طرف واقعاً توانایی ارسال و دریافت داده رو دارن.
اگر فقط Client یک درخواست ارسال کنه، Server نمی‌دونه Client آماده‌ی دریافت پاسخ هست یا نه. این هماهنگی اولیه باعث میشه TCP بتونه روی Connection ایجاد شده، ویژگی‌هایی مثل ترتیب Packetها و Reliable بودن انتقال داده رو تضمین کنه.

انتقال داده در TCP 📦
بعد از Handshake، اتصال وارد مرحله‌ی انتقال داده میشه. TCP داده‌ها رو به بخش‌های کوچک‌تری به اسم Segment تقسیم می‌کنه و برای هر Segment اطلاعاتی مثل Sequence Number قرار میده. Sequence Number به TCP کمک می‌کنه که اگر Packetها با تأخیر یا خارج از ترتیب رسیدن، دوباره اون‌ها رو مرتب کنه.
همچنین Receiver با ارسال ACK به Sender اعلام می‌کنه که چه داده‌ای رو دریافت کرده. حالا اگر یک Packet به مقصد نرسه، TCP می‌تونه متوجه بشه و دوباره اون داده رو ارسال کنه.

‏TCP Connection چطور بسته میشه؟ 👋
بستن Connection هم مثل ساختنش یک فرآیند ساده‌ی قطع کردن نیست. TCP برای این کار از Four-Way Handshake استفاده می‌کنه. دلیل چهار مرحله‌ای بودن اینه که ارتباط TCP دوطرفه است و هر سمت باید جداگانه اعلام کنه که دیگر داده‌ای برای ارسال ندارد.
وقتی یکی از طرفین می‌خواد Connection رو ببنده، یک Packet با Flag FIN ارسال می‌کنه. طرف مقابل با ACK دریافت این درخواست رو تأیید می‌کنه. بعد از اینکه خودش هم ارسال داده‌ها رو تمام کرد، یک FIN ارسال می‌کنه و طرف دیگر با ACK نهایی Connection رو می‌بنده.

چرا TIME_WAIT وجود داره؟
یکی از بخش‌های جالب چرخه‌ی TCP، مرحله‌ای به نام TIME_WAIT هست. بعد از بسته شدن اتصال، سمتی که آخرین ACK رو ارسال کرده، برای مدتی اتصال رو در وضعیت TIME_WAIT نگه می‌داره.
هدف این کار اینه که اگر آخرین Packetها با تأخیر در شبکه وجود داشته باشن، باعث ایجاد مشکل برای اتصال جدید نشن.
فرض کنید یک اتصال قدیمی هنوز Packetهایی در شبکه داشته باشه و بعد خیلی سریع یک اتصال جدید با همان IP و Port ساخته بشه. این Packetهای قدیمی ممکنه با اتصال جدید اشتباه گرفته بشن. TIME_WAIT کمک می‌کنه قبل از استفاده‌ی دوباره از آن مشخصات، مطمئن بشیم Packetهای قبلی از شبکه خارج شدن.


جمع‌بندی ✍️
‏TCP برای ایجاد یک ارتباط قابل اعتماد، فقط داده‌ها رو ارسال نمی‌کنه؛ بلکه قبل از شروع ارتباط، وضعیت دو طرف رو هماهنگ می‌کنه و بعد از پایان کار هم Connection رو به شکل کنترل‌شده می‌بنده.
‏Three-Way Handshake، Sequence Number، ACK و TIME_WAIT همگی بخشی از همین مکانیزم هستن که باعث میشن TCP یک پروتکل قابل اعتماد برای ارتباطات شبکه باشه.

#️⃣ #web #network


🌙 CHANNEL | GROUP
‏Database Replication دقیقاً یعنی چی؟ 🧠
‏Replication یعنی ایجاد چند نسخه از داده‌ها روی چند Database Server مختلف. توی این معماری، معمولاً یه Node اصلی داریم که مسئول دریافت تغییرات داده هست و Nodeهای دیگه یک کپی از داده‌ها رو نگهداری می‌کنن. هدف اصلی Replication این نیست که فضای بیشتری برای ذخیره‌سازی داشته باشیم. هدف اصلی اینه که بتوانیم تعدا Request بیشتری رو مدیریت کنیم و فشار روی Database اصلی رو کم کنیم. به زبان ساده، به جای اینکه همه‌ی کاربرا به یه Database متصل بشن، کار بین چند Database تقسیم می‌شه.

‏Primary-Replica Architecture 🔄
یکی از رایج‌ترین مدل‌های Replication، معماری Primary-Replica هست. توی این مدل، یک Database به عنوان Primary انتخاب میشه و تمام عملیات های نوشتن(INSERT, UPDATE, DELETE) روی اون انجام میشن. بعد تغییرات ایجاد شده به Replicaها منتقل میشن تا اونا هم نسخه جدیدی از داده هارو داشته باشن. الان میتونیم از Replicaها برای عملیات های Read استفاده کنیم و اونارو به Replicaها ارسال کنیم تا فشار روی Primary کمتر بشه.

‏Replication چطور داده‌ها رو منتقل می‌کنه؟ ⚙️
برای انتقال تغییرات، Database معمولاً تغییرات انجام شده رو توی یه Log ثبت می‌کنه. تو PostgreSQL، این کار با استفاده از WAL (Write-Ahead Log) انجام میشه. قبل از اینکه تغییرات اصلی روی Data Fileها ذخیره بشن، اطلاعات داخل WAL ثبت میشه.
‏Replicaها این Logها رو دریافت می‌کنن و با اجرای دوباره‌ی اون ها، Database خودشون رو به وضعیت Primary نزدیک می‌کنن. به همین دلیل Replication معمولاً یک فرآیند Continuous هست که دائماً تغییرات جدید رو دنبال می‌کنه.

‏Synchronous و Asynchronous Replication 🔄
یکی از تصمیم‌های مهم در طراحی Replication اینه که تغییرات چطور به Replicaها منتقل بشن.
توی Primary, Synchronous Replication قبل از تأیید یه عملیات Write باید مطمئن بشه که Replica هم اون تغییر را دریافت کرده. مزیت این روش اینه که احتمال از دست رفتن داده خیلی کم میشه. اما مشکل اینجاست که سرعت Write کاهش پیدا می‌کنه، چون Primary باید منتظر Replicaها بمونه.
توی Primary, Asynchronous Replication بعد از ذخیره‌ی تغییر خودش پاسخ رو برمی‌گردونه و انتقال تغییرات به Replica تو پس‌زمینه انجام میشه. این روش Performance بهتری داره، اما ممکنه برای مدت کوتاهی Replicaها داده‌ی جدید را نداشته باشند. به این فاصله، Replication Lag گفته میشه.

مشکل Replication Lag چیه؟
توی معماری Replica، همیشه این احتمال وجود داره که یه Replica کمی عقب‌تر از Primary باشه.
مثلاً یه کاربر اطلاعات خودش رو تغییر میده. ‏Write روی Primary انجام میشه، اما قبل از اینکه Replica این تغییر رو دریافت کنه، درخواست Read به Replica ارسال میشه. توی این حالت ممکنه کاربر هنوز مقدار قدیمی داده‌ی خودش رو ببینه. این مشکل یکی از چالش‌های مهم سیستم‌های توزیع شده هست، چون افزایش Performance معمولاً با هزینه‌ی پیچیده‌تر شدن Consistency همراه میشه.

‏Replication چه مشکلاتی رو حل می‌کنه؟ 💡
‏Replication بیشتر برای حل دو مشکل استفاده میشه:
اول، Scaling Readها: وقتی تعداد Readها زیاد باشه، می‌تونیم اون ها رو بین چند Replica تقسیم کنیم و فشار روی Primary رو کاهش بدیم.
دوم، High Availability: اگر Primary از دسترس خارج بشه، میشه یه Replica رو به عنوان Primary جدید انتخاب کرد تا سرویس ادامه پیدا کنه.
البته این فرآیند معمولاً نیاز به مکانیزم‌هایی مثل Failover و مدیریت دقیق وضعیت Nodeها داره.

‏Replication همیشه جواب مناسبیه؟ 🤔
‏Replication یک راهکار قدرتمند برای افزایش مقیاس‌پذیری و دسترس‌پذیریه، اما بدون هزینه نیست.
با اضافه شدن Replicaها، مدیریت سیستم پیچیده‌تر میشه و مشکلاتی مثل Replication Lag, انتخاب Primary جدید و Consistency بین Nodeها به سیستم اضافه میشن و باید درست مدیریت بشن.
به همین دلیل Replication بیشتر یه Trade-off هست، نه یi راه‌حل جادویی برای همه‌ی مشکلات Database.

جمع‌بندی ✍️
‏Database Replication یکی از تکنیک‌های اصلی برای ساخت سیستم‌های مقیاس‌پذیر و قابل اعتماده. با نگهداری چند نسخه از داده‌ها، می‌تونیم فشار Readها رو تقسیم کنیم و در برابر خرابی‌ها مقاوم‌تر باشیم.
اما در کنار این مزایا، باید چالش‌هایی مثل Consistency و Replication Lag رو هم توی طراحی سیستم در نظر بگیریم.


#️⃣ #system_design #databsae #backend


🌙 CHANNEL | GROUP
3
‏Redis چطور کلیدهای منقضی شده رو حذف می‌کنه؟ 🤔
یکی از قابلیت‌های مهم Redis، امکان تعیین زمان انقضا برای کلیدهاست. مثلاً می‌تونیم یک کلید بسازیم که فقط برای چند دقیقه معتبر باشه. این قابلیت برای چیزهایی مثل Sessionها، Cacheها، OTPها و... خیلی استفاده میشه.
اما یک سؤال جالب وجود داره:
وقتی برای یک کلید مقدار TTL تعیین می‌کنیم، Redis چطور متوجه میشه زمانش تمام شده؟

‏Redis چطور زمان انقضا رو ذخیره می‌کنه؟ 🧠
وقتی برای یک کلید مقدار Expire تعیین می‌کنیم، Redis خود مقدار اصلی کلید رو تغییر نمی‌ده. در واقع Redis یک زمان انقضا جداگانه برای اون کلید نگه می‌داره. یعنی کنار داده‌ی اصلی، اطلاعات مربوط به زمان منقضی شدن هم ذخیره میشه. این زمان معمولاً به شکل Timestamp نگهداری میشه تا Redis بتونه موقع بررسی کلید، متوجه بشه که زمانش گذشته یا نه.
‏نکته مهم اینه که Redis برای هر کلید یک Thread یا Timer جدا ایجاد نمی‌کنه که منتظر رسیدن زمان حذف اون کلید باشه. همچین کاری برای میلیون‌ها کلید می‌تونست هزینه‌ی خیلی زیادی داشته باشه. به جای اون، Redis از دو روش اصلی برای مدیریت Expiration استفاده می‌کنه: ‏Passive Expiration و Active Expiration

‏Passive Expiration (Lazy Expiration) 💤
توی این روش، Redis فقط زمانی یک کلید رو بررسی می‌کنه که قرار باشه به اون دسترسی پیدا کنه. یعنی وقتی یک Client یک کلید رو درخواست می‌کنه، Redis قبل از برگردوندن مقدار، Timestamp انقضا رو بررسی می‌کنه. اگه زمان کلید تمام شده باشه، Redis اون رو حذف می‌کنه و نتیجه‌ای برنمی‌گردونه.
مزیت این روش اینه که Redis برای کلیدهایی که هیچ‌وقت استفاده نمی‌شن، انرژی و CPU مصرف نمی‌کنه.
اما یک مشکل وجود داره. اگه یک کلید هیچ‌وقت خونده نشه، همچنان توی Memory باقی می‌مونه. یعنی ممکنه داده‌ای که مدت‌هاست منقضی شده، فضای Redis رو اشغال کرده باشه.
برای حل این مشکل، Redis از یه روش دیگه هم استفاده می‌کنه.

‏Active Expiration 🔄
اینجا Redis به صورت دوره‌ای کلیدهایی که دارای TTL هستن رو بررسی می‌کنه. تو این فرآیند، Redis تعدادی از کلیدهای دارای TTL رو انتخاب می‌کنه و بررسی می‌کنه که آیا زمان اون ها تمام شده یا نه. اگه کلید منقضی شده باشه، از Memory حذف میشه.
اما تمام کلیدهای منقضی شده رو یک‌باره بررسی نمی‌کنه. چون اگه تعداد کلیدهای دارای TTL خیلی زیاد باشه، بررسی همه‌ی آن‌ها می‌تونه باعث مصرف زیاد CPU و ایجاد Latency بشه. به همین دلیل Redis این کار رو به صورت تدریجی انجام میده و بین مصرف CPU و آزادسازی Memory تعادل ایجاد می‌کنه.

‏Expiration چه تأثیری روی Memory داره؟ 💡
یکی از نکات مهم درباره‌ی Expiration اینه که تنظیم TTL به معنی آزاد شدن فوری Memory نیست. اگه یک کلید منقضی بشه، ممکنه تا زمانی که Redis اون رو بررسی نکنه، همچنان تو Memory باقی مونده باشه. البته Redis با Active Expiration تلاش می‌کنه این فاصله رو کم کنه. اما تو سیستم‌هایی با تعداد زیادی کلید، باید این رفتار رو توی طراحی Cache در نظر گرفت.

جمع‌بندی ✍️
‏Redis برای مدیریت منقضی شدن کلیدها، از یک Timer جدا برای هر کلید استفاده نمی‌کنه. به جاش، با ترکیب دو مکانیزم ‏Passive Expiration برای حذف موقع دسترسی، و ‏Active Expiration برای پاکسازی دوره‌ای بین Performance و مصرف Memory تعادل ایجاد می‌کنه.
این یکی از مثال‌های جالب معماری Redis هست که نشون میده برای رسیدن به Performance بالا، همیشه لازم نیست دقیق‌ترین روش رو انتخاب کنیم؛ گاهی یک طراحی هوشمندانه با Trade-off مناسب، نتیجه‌ی بهتری میده.


#️⃣ #system_design #databsae #backend


🌙 CHANNEL | GROUP
🔥2👏1
‏connection Pooling چطور ارتباط با Database رو بهینه می‌کنه؟ 🤔
وقتی یه درخواست وارد یه Backend Service میشه، معمولاً نیاز داره برای گرفتن یا تغییر دادن داده‌ها با Database ارتباط برقرار کنه. در نگاه اول شاید ساده به نظر برسه:
‏درخواست میاد → Connection ساخته میشه → Query اجرا میشه → Connection بسته میشه
اما توی یه سیستم واقعی که صدها یا هزاران درخواست در ثانیه دریافت می‌کنه، این مدل می‌تونه تبدیل به یه مشکل بزرگ بشه. چون ساختن یه Database Connection فقط یه عملیات ساده نیست.
برای ایجاد یه اتصال، Client باید مراحلی مثل برقراری ارتباط TCP، احراز هویت و آماده‌سازی Session با Database رو انجام بده. اگه برای هر درخواست یه Connection جدید ساخته بشه، بخش زیادی از منابع سیستم صرف ساختن و از بین بردن Connectionها میشه، نه پردازش واقعی Queryها.
اینجاست که مفهومی به اسم Connection Pooling وارد میشه.

‏Connection Pooling دقیقاً یعنی چی؟ 🧠
‏Connection Pooling یعنی به جای اینکه برای هر درخواست یه Connection جدید بسازیم، تعدادی Connection از قبل ایجاد کنیم و اونها رو بین درخواستهای مختلف به اشتراک بذاریم. این Connectionها داخل یه Pool نگهداری میشن و هر زمان یه درخواست نیاز به Database داشته باشه، یکی از Connectionهای آزاد Pool رو دریافت می‌کنه.
بعد از تموم شدن Queryها، Connection بسته نمیشه؛ بلکه دوباره به Pool برمی‌گرده تا درخواست بعدی بتونه از اون استفاده کنه. در واقع Connection Pool مثل یه لایه‌ی مدیریت بین Application و Database عمل می‌کنه و چرخه‌ی عمر Connectionها رو کنترل می‌کنه.

بدون Connection Pool چه اتفاقی میفته؟ ⚠️
فرض کنیم یه API داریم که در هر درخواست یه Connection جدید به PostgreSQL ایجاد می‌کنه. اگه در یه لحظه 1000 درخواست وارد سیستم بشه، Application ممکنه تلاش کنه 1000 اتصال جداگانه بسازه. اما Database هم محدودیت داره.
هر Connection معمولاً منابعی مثل Memory, Thread, Process, Session State مصرف میکنه. در نتیجه تعداد زیاد Connectionها می‌تونه باعث بشه Database به جای اجرای Queryها، بیشتر درگیر مدیریت Connectionها بشه.
به همین دلیل داشتن Connection بیشتر همیشه به معنی Performance بهتر نیست.

‏Connection Pool چطور کار می‌کنه؟ ⚙️
یه Pool معمولاً با چند مقدار مهم مدیریت میشه.
مثلاً:
‏Minimum Connections:
حداقل تعداد Connectionهایی که همیشه آماده نگه داشته میشن.
‏Maximum Connections:
بیشترین تعداد Connectionهایی که Pool اجازه ایجادشون رو میده.
‏Idle Timeout:
مدتی که یه Connection بدون استفاده می‌تونه در Pool باقی بمونه.
‏Connection Lifetime:
حداکثر عمر یه Connection قبل از اینکه دوباره ساخته بشه.


وقتی تعداد درخواستها بیشتر از تعداد Connectionهای آزاد Pool بشه، بسته به تنظیمات سیستم یا درخواست منتظر آزاد شدن یه Connection می‌مونه یا بعد از یه زمان مشخص خطا دریافت می‌کنه.

‏Connection Pool و Database چه رابطه‌ای دارن؟ 🗄
یکی از اشتباهات رایج اینه که فکر کنیم هرچی Pool بزرگ‌تر باشه، سیستم سریع‌تر میشه. اما Database هم ظرفیت محدودی داره. اگه Application چندین Worker داشته باشه و هر Worker تعداد زیادی Connection باز کنه، ممکنه خیلی سریع تعداد Connectionها از توان Database بیشتر بشه.
برای مثال: یه سرویس با 10 Worker داریم. اگه هر Worker یه Pool با حداکثر 100 Connection داشته باشه، در بدترین حالت ممکنه 1000 Connection به Database ارسال بشه. در حالی که شاید Database برای چنین تعداد Connectionهایی طراحی نشده باشه.
به همین دلیل اندازه‌ی Pool باید با توجه به ظرفیت Database، نوع Queryها و حجم Traffic تنظیم بشه.

‏Connection Pooling چه مشکلی رو حل می‌کنه؟ 🤔
‏Connection Pooling باعث میشه هزینه ساخت Connection های تکراری کم بشه، Database کمتر درگیر مدیریت Connection ها بشه، Latency درخواست ها کمتر باشن و در نهایت منابع سیستم بهتر استفاده بشن.
اما در کنارش باید درست تنظیم بشه. یه Pool خیلی کوچک باعث میشه Requestها منتظر Connection بمونن و یه Pool خیلی بزرگ، می‌تونه Database رو تحت فشار قرار بده.

جمع‌بندی ✍️
‏Connection Pooling یکی از بخش‌های مهم طراحی Backendهای مقیاس‌پذیره. ایده‌ی اصلی ساده هست:
به جای ساختن و نابود کردن مداوم Connectionها، اونها رو مدیریت و دوباره استفاده می‌کنیم.
اما پیاده‌سازی درستش نیاز به شناخت رفتار Application و Database داره، چون تعداد Connectionهای بیشتر همیشه به معنی Performance بهتر نیست.


#️⃣ #system_design #databsae #backend


🌙 CHANNEL | GROUP
🔥1
There are only two hard things in Computer Science: cache invalidation and naming things.

- Phil Karlton
😁2🗿1
چرا Cache Invalidation یکی از سخت‌ترین مشکلات سیستم‌هاست؟ 🤔
‏Cache یکی از مهم‌ترین ابزارها برای افزایش Performance در سیستم‌هاست. با نگهداری داده‌های پرتکرار توی یه لایه‌ی سریعتر، می‌تونیم تعداد درخواست‌ها به Database یا سرویس‌های اصلی رو کمتر کنیم، Latency رو کاهش بدیم و سیستم رو بهتر Scale کنیم.
اما یک مشکل مهم وجود داره:
وقتی داده‌ی اصلی تغییر می‌کنه، از کجا بفهمیم نسخه‌ای که داخل Cache داریم هنوز معتبره؟
اینجاست که Cache Invalidation تبدیل به یکی از سخت‌ترین بخش‌های طراحی سیستم‌ها میشه.

‏Cache Invalidation یعنی چی؟ 🧠
‏Cache Invalidation یعنی وقتی داده‌ی اصلی تغییر می‌کنه، نسخه‌ای که داخل Cache ذخیره شده رو هم مدیریت کنیم. این کار می‌تونه با حذف کردن داده از Cache یا Update کردن مقدار جدید انجام بشه.
مشکل اینجاست که Cache معمولاً فقط یک نسخه‌ی موقت از داده‌هاست و منبع اصلی اطلاعات نیست. پس باید همیشه مطمئن بشیم داده‌ای که به کاربر برمی‌گردونیم، با داده‌ی اصلی هماهنگه. اگه این هماهنگی درست انجام نشه، کاربرا ممکنه داده‌ی قدیمی یا اشتباه دریافت کنن.

‏Distributed Systems و تأخیر شبکه 🌐
توی یک سیستم بزرگ، داده‌ها معمولاً فقط داخل یک Cache وجود ندارن. ممکنه چندین Server، Region یا Node مختلف نسخه‌ای از یک داده رو نگهداری کنن. وقتی داده‌ی اصلی تغییر می‌کنه، باید پیام Invalidation به تمام این Nodeها ارسال بشه. اما تو سیستم‌های Distributed، شبکه همیشه سریع و قابل اعتماد نیست.
ممکنه پیام با تأخیر به مقصد برسه یا حتی در مسیر از بین بره. در نتیجه ممکنه بعضی کاربرا داده‌ی جدید رو ببینن و بعضی کاربران هنوز نسخه‌ی قدیمی داده رو دریافت کنن.

‏Concurrency و ترتیب عملیات‌ها 🔄
تو سیستم‌های پرترافیک، چندین Read و Write می‌تونن همزمان اتفاق بیفتن. حالا ترتیب انجام این عملیات‌ها اهمیت زیادی پیدا می‌کنه. فرض کنید یک داده داخل Database تغییر می‌کنه و بعد پیام حذف Cache ارسال میشه. اگه این پیام‌ها با ترتیب درست پردازش نشن، ممکنه Cache دوباره با یک مقدار قدیمی پر بشه. یعنی حتی حذف کردن Cache هم همیشه تضمین نمی‌کنه که داده‌ی درست در اختیار کاربر قرار بگیره.

‏Freshness یا Performance؟ ⚖️
یکی از مهم‌ترین تصمیم‌ها توی طراحی Cache، پیدا کردن تعادل بین تازه بودن داده و Performance هست. اگه با هر تغییر، Cache رو سریع حذف کنیم یا Update کنیم، داده‌ها همیشه جدیدتر هستن. اما این کار باعث میشه درخواست‌های بیشتری به Database ارسال بشن و فشار روی سیستم اصلی افزایش پیدا کنه. از طرف دیگه، اگه اجازه بدیم داده‌ها مدت بیشتری داخل Cache باقی بمونن، Performance بهتر میشه اما احتمال نمایش داده‌ی قدیمی بیشتر میشه.

‏Cache Eviction و محدودیت حافظه 📦
‏Cacheها معمولاً ظرفیت نامحدود ندارن. وقتی فضای Cache پر بشه، باید بعضی داده‌ها حذف بشن تا فضای کافی برای داده‌های جدید ایجاد بشه. اینجاست که الگوریتم‌هایی مثل LRU (Least Recently Used) و LFU (Least Frequently Used) وارد میشن. اما انتخاب اشتباه می‌تونه باعث بشه داده‌ای که هنوز کاربرد داره حذف بشه و سیستم دوباره مجبور بشه اون داده رو از منبع اصلی دریافت کنه.

هزینه‌ی مدیریت Cache 💡
پیاده‌سازی یک سیستم Cache ساده نیست. برای مدیریت درست Cache ممکنه نیاز داشته باشیم اطلاعاتی درباره‌ی داده‌ها نگه داریم، پیام‌های Invalidation ارسال کنیم و وضعیت Cacheها رو بررسی کنیم. تو سیستم‌های بزرگ، حتی یک مقدار پیچیدگی اضافه می‌تونه باعث مصرف بیشتر منابع، افزایش ترافیک شبکه و هزینه‌ی نگهداری بشه.

جمع‌بندی ✍️
‏Cache کردن داده‌ها کار سختی نیست. چالش اصلی زمانی شروع میشه که داده تغییر کنه و باید مطمئن بشیم تمام نسخه‌های Cache شده هم هماهنگ هستن. به همین دلیل Cache Invalidation یکی از معروف‌ترین مشکلات طراحی سیستم‌ها محسوب میشه.
در نهایت هم:

There are only two hard things in Computer Science: cache invalidation and naming things.
- Phil Karlton



#️⃣ #system_design #cache #backend


🌙 CHANNEL | GROUP
3
۱۰۰تایی شدییمممم🎉🥳
🔥42
نگاهی به روش های مختلف Cache invalidation
‏Cache یکی از رایج‌ترین راهکارا برای افزایش Performance توی سیستم‌هاست. به جای اینکه برای هر Request مستقیم به Database مراجعه کنیم، داده‌هایی که زیاد استفاده میشن رو توی یک لایه‌ی سریع‌تر نگهداری می‌کنیم. این کار باعث میشه تعداد Queryها کمتر بشه، Latency کاهش پیدا کنه و فشار روی Database پایین بیاد. اما یک مشکل مهم وجود داره:
اگه داده‌ی اصلی تغییر کنه، چه اتفاقی برای نسخه‌ای که داخل Cache داریم میفته؟
فرض کنیم اطلاعات یه کاربر داخل Cache ذخیره شده. حالا کاربر اسم خودش رو تغییر میده و این تغییر داخل Database ثبت میشه. اما اگه مقدار قدیمی هنوز داخل Cache وجود داشته باشه، درخواست‌های بعدی ممکنه همون داده‌ی قدیمی رو دریافت کنن.
اینجاست که مفهوم Cache Invalidation اهمیت پیدا می‌کنه.

‏Cache Invalidation دقیقاً یعنی چی؟ 🧠
‏Cache Invalidation یعنی مدیریت منقضی شدن یا حذف داده‌هایی که دیگه معتبر نیستند. به زبان ساده، وقتی داده‌ی اصلی تغییر می‌کنه، باید یه تصمیمی بگیریم:
باید Cache رو حذف کنیم؟ یا مقدار جدید رو داخل Cache قرار بدیم؟ یا اجازه بدیم بعد از مدتی خودش منقضی بشه؟
این تصمیم یکی از چالش‌های مهم طراحی سیستم‌هاست. چون Cache و Database معمولاً دو منبع جدا از هم هستن و همیشه باید بین سرعت بالا و Consistency تعادل ایجاد کنیم.

روش اول: Cache Aside (Lazy Loading) 📦
یکی از رایج‌ترین روش‌ها، Cache Aside هست. تو این مدل، Application خودش مسئول مدیریت Cache و Database هست. وقتی یه داده درخواست میشه، اول Cache بررسی میشه. اگه داده وجود داشته باشه، همون مقدار برگردونده میشه. اگه Cache Miss اتفاق بیفته، Application داده رو از Database می‌خونه، داخل Cache قرار میده و بعد جواب رو برمی‌گردونه.
اما بخش مهم این روش، زمان نوشتن هست. وقتی داده تغییر می‌کنه، معمولاً Application اول تغییر رو توی Database ذخیره می‌کنه و بعد مقدار مربوطه رو از Cache حذف می‌کنه. با این کار، درخواست بعدی مجبور میشه داده‌ی جدید رو از Database بخونه و دوباره Cache رو پر کنه. مزیت این روش سادگی و کنترل خوب روی Cache هست. به همین دلیل یکی از رایج‌ترین الگوها توی سیستم‌های واقعی محسوب میشه.

روش دوم: TTL (Time To Live)
یکی از ساده‌ترین روش‌ها برای مدیریت Cache، تعیین یک زمان انقضا برای داده‌هاست. هر داده ای فقط تا زمان مشخصی معتبره. بعد از تموم شدن این زمان، داده حذف میشه و درخواست بعدی مقدار جدید رو دریافت می‌کنه.
مزیت TTL اینه که نیاز نداریم تمام تغییرات Database رو دنبال کنیم. اما مشکلش اینه که ممکنه داده قبل از منقضی شدن، تغییر کرده باشه. یعنی تا زمانی که TTL تمام نشده، کاربرا ممکنه مقدار قدیمی رو ببینن. به همین دلیل TTL بیشتر برای داده‌هایی مناسبه که تغییراتشون حساس نیست یا Stale بودن کوتاه‌مدت براشون قابل قبوله.

روش سوم: Write Through 🔄
توی Write Through، موقع تغییر داده، Application همزمان Cache و Database رو به‌روز می‌کنه.
یعنی هر Write باید در هر دو مکان انجام بشه.
مزیت این روش اینه که Cache همیشه مقدار جدید رو داره و احتمال Stale Data کمتر میشه. اما هزینه‌ی بیشتری داره، چون هر تغییر باید چند عملیات مختلف انجام بده. همچنین اگه یکی از این عملیات‌ها شکست بخوره، باید مکانیزمی برای هماهنگ کردن دوباره‌ی داده‌ها وجود داشته باشه.

روش چهارم: Event-Based Invalidation 📡
تو سیستم‌های بزرگ‌تر، گاهی سرویس‌ها از Event استفاده می‌کنن. یعنی وقتی تغییری داخل Database اتفاق میوفته، یه Event منتشر میشه. سرویس‌های دیگه این Event رو دریافت می‌کنن و Cache مربوط به اون داده رو حذف یا به‌روز می‌کنن.
این روش باعث میشه سرویس‌ها وابستگی مستقیم به هم نداشته باشن. اما در عوض، معماری پیچیده‌تر میشه و باید مواردی مثل Duplicate Event، ترتیب Eventها و خطاهای احتمالی مدیریت بشن.

پس بهترین روش کدومه؟💡
مثل بیشتر تصمیم‌های معماری، یک جواب ثابت وجود نداره.
اگه داده‌ها زیاد تغییر نمی‌کنن و کمی قدیمی بودنشون مشکلی ایجاد نمی‌کنه، TTL می‌تونه کافی باشه.

اگه Performance و سادگی مهم باشه، Cache Aside معمولاً انتخاب مناسبیه.

اگه Consistency اهمیت زیادی داشته باشه، باید سراغ روش‌هایی مثل Write Through یا Event-Based Invalidation رفت.

انتخاب روش مناسب به این بستگی داره که سیستم ما بیشتر به چه چیزی نیاز داره:
‏Performance بیشتر یا داده‌ی همیشه به‌روز؟

جمع‌بندی ✍️
‏Cache کردن داده‌ها بخش ساده‌ی ماجراست. چالش واقعی زمانی شروع میشه که داده تغییر کنه و باید تصمیم بگیریم Cache چه زمانی و چطور به‌روز بشه.

#️⃣ #system_design #cache #backend


🌙 CHANNEL | GROUP
Switch که توی لایه ی دوم شبکه کار می‌کنه و فقط MAC Addressها رو می‌شناسه، چطوری می‌تونه با IP Address کار کنه؟🤔
در واقع Switch خودش کاری با IP Address نداره .وظیفه‌ی Switch اینه که Frameها رو بر اساس MAC Address بین دستگاه‌های داخل شبکه جابه‌جا کنه. اما مشکل اینجاست که اکثر ارتباطات شبکه‌ای با IP شروع میشن.
وقتی یک Application می‌خواد داده‌ای رو به یک IP مشخص ارسال کنه، سیستم عامل اول باید اون IP رو به یک MAC Address تبدیل کنه تا بتونه Frame مربوطه رو داخل شبکه ارسال کنه.
اینجاست که ARP (Address Resolution Protocol) وارد میشه.

‏ARP دقیقاً یعنی چی؟ 🧠
‏ARP یک پروتکل برای پیدا کردن MAC Address مربوط به یک IP Address داخل شبکه‌ی Local هست. به زبان ساده، ARP این سؤال رو از شبکه می‌پرسه:
«کدوم دستگاه این IP رو داره؟ MAC Address خودش رو بهم بگه.»
برای مثال، فرض کنین یک سیستم با IP 192.168.1.10 می‌خواد برای یه سیستم دیگه با IP 192.168.1.20 داده ارسال کنه. سیستم اول می‌دونه مقصد کجاست، اما MAC Address مقصد رو نمی‌دونه. پس باید قبل از ارسال Frame، این اطلاعات رو پیدا کنه.

‏ARP Request و ARP Reply چطور کار می‌کنن؟ 📡
اول سیستم فرستنده یه پیام ARP Request ارسال می‌کنه. این پیام به صورت Broadcast توی شبکه Local پخش میشه. یعنی تمام دستگاه‌های داخل همون شبکه این پیام رو دریافت می‌کنن. محتوای پیام یه همچین چیزیه:
«چه کسی IP 192.168.1.20 رو داره؟ MAC Address خودش رو بهم بگه.»
تمام دستگاه‌ها این پیام رو دریافت می‌کنن، اما فقط دستگاهی که اون IP رو داره جواب میده. این جواب با نام ARP Reply شناخته میشه. دستگاه مقصد MAC Address خودش رو ارسال می‌کنه و سیستم فرستنده حالا می‌تونه Frame رو مستقیم به اون MAC Address ارسال کنه.

‏ARP Cache چرا وجود داره؟ 💾
اگر برای هر Packet نیاز بود دوباره ARP Request ارسال بشه، شبکه خیلی سریع شلوغ می‌شد. به همین دلیل سیستم‌ها نتیجه‌ی ARP Queryها رو برای مدتی ذخیره می‌کنن. این اطلاعات داخل چیزی به اسم ARP Cache نگهداری میشه.
مثلاً سیستم می‌تونه نگه داره:
192.168.1.20 → AA:BB:CC:DD:EE:FF

دفعه‌ی بعد که بخواد داده‌ای برای این IP ارسال کنه، مستقیم از همین اطلاعات استفاده می‌کنه و نیازی به Broadcast دوباره نیست. البته این اطلاعات دائمی نیستن و بعد از مدتی منقضی میشن تا تغییرات شبکه هم قابل تشخیص باشن.

‏ARP فقط برای ارتباط داخل شبکه است 🌐
یک نکته‌ی مهم اینه که ARP برای پیدا کردن MAC Address داخل همون شبکه‌ی Local استفاده میشه. وقتی می‌خوایم به یک Server توی اینترنت وصل بشیم، سیستم ما MAC Address اون Server رو پیدا نمی‌کنه. چون مقصد توی یه شبکه ی دیگه هست.
توی این حالت، سیستم MAC Address مربوط به Router یا Gateway خودش رو با ARP پیدا می‌کنه و Packet رو به Router تحویل میده. بعد از اون، Router مسئول انتقال Packet در مسیر شبکه است.

ولی ARP یه مشکلی داره ⚠️
‏ARP یک پروتکل ساده است، یکی از مشکلاتش اینه که مکانیزم Authentication نداره. یعنی یک دستگاه می‌تونه خودش رو به عنوان صاحب یک IP معرفی کنه و پیام ARP جعلی ارسال کنه. به این حمله ARP Spoofing گفته میشه. توی این حالت، مهاجم می‌تونه ترافیک شبکه رو به سمت خودش هدایت کنه و اطلاعاتی مثل Packetها رو مشاهده یا تغییر بده. به همین دلیل تو شبکه‌های حساس، مکانیزم‌های امنیتی بیشتری برای کنترل ARP استفاده میشن.

جمع‌بندی ✍️
‏ARP پلی بین دو دنیای مختلفه:
‏IP Address که برای پیدا کردن مقصد در لایه‌ی Network استفاده میشه، و ‏MAC Address که برای ارسال واقعی Frame داخل شبکه‌ی Local استفاده میشه.
هر بار که یک Packet داخل یک شبکه‌ی Ethernet حرکت می‌کنه، پشت صحنه ARP کمک می‌کنه سیستم بدونه باید این داده رو دقیقاً به کدوم دستگاه تحویل بده.

#️⃣ #network #web #devops


🌙 CHANNEL | GROUP
‏DNS Caching چطور کار می‌کنه؟ 🤔
وقتی توی مرورگر example.com رو وارد می‌کنیم، سیستم برای برقراری ارتباط باید IP مربوط به اون Domain رو پیدا کنه. اما اینطور نیست که برای هر درخواست، سیستم دوباره از Root DNS Server شروع کنه و تمام مسیر DNS رو طی کنه. اگه این اتفاق برای هر Request می‌افتاد، هم زمان زیادی صرف DNS Resolution می‌شد و هم فشار خیلی زیادی به DNS Serverها وارد می‌شد.

‏DNS Cache دقیقاً چیه؟ 🧠
‏DNS Cache یعنی ذخیره کردن نتیجه‌ی DNS Query برای یه مدت مشخص. مثلاً فرض میکنیم Resolver برای example.com به IP 93.184.216.34 رسیده. به جای اینکه دفعه‌ی بعد دوباره این Domain رو از اول Resolve کنه، نتیجه رو برای یه مدت نگه می‌داره. این مدت توسط TTL (Time To Live) مشخص میشه. تا وقتی TTL تموم نشده، Resolver می‌تونه از همان نتیجه‌ی Cache شده استفاده کنه و نیازی به Query جدید نداره.

‏DNS Cache کجا نگه داشته میشه؟ 📦
یک DNS Cache فقط در یک نقطه وجود نداره. ممکنه نتیجه‌ی DNS Query توی چندین لایه Cache بشه. مثلاً مرورگر می‌تونه نتیجه‌ی DNS رو نگه داره، سیستم‌عامل هم می‌تونه Cache خودش رو داشته باشه و DNS Resolver مورد استفاده‌ی سیستم هم معمولاً Cache داره. حتی Resolverهای عمومی مثل DNSهای ISP یا سرویس‌های عمومی هم می‌تونن نتیجه‌ی Domainها رو برای مدتی Cache کنن. به همین دلیل وقتی یک Domain رو چندین بار Request می‌کنیم، ممکنه تمام مسیر DNS طی نشه.

‏TTL چه کاری انجام میده؟
‏TTL مشخص می‌کنه یک DNS Record تا چه مدت می‌تونه Cache بشه. فرض کنیم TTL یک Record برابر با 300 ثانیه باشه. ‏Resolver می‌تونه نتیجه رو تا 5 دقیقه Cache کنه. بعد از تموم شدن TTL، اون مقدار دیگه معتبر محسوب نمیشه و Resolver باید برای دریافت اطلاعات جدید دوباره Query انجام بده.
این مقدار خیلی مهمه، چون TTL پایین باعث میشه تغییرات DNS سریع‌تر منتشر بشن، اما تعداد Queryها بیشتر میشه. ‏TTL بالاتر هم باعث کاهش Queryها و افزایش استفاده از Cache میشه، اما تغییرات DNS ممکنه دیرتر دیده بشن.

‏DNS Cache چه مشکلی رو حل می‌کنه؟ ⚡️
بدون DNS Caching، حتی یک درخواست ساده به یک Website می‌تونست نیازمند چندین DNS Query باشه. ‏Resolver ممکن بود مجبور بشه از Root Server شروع کنه، بعد به TLD Server برسه و در نهایت Authoritative DNS Server رو پیدا کنه. اما وقتی نتیجه قبلاً Cache شده باشه، Resolver می‌تونه خیلی سریع جواب رو برگردونه.
این یعنی Latency کمتر، DNS Traffic کمتر و فشار کمتر روی DNS Infrastructure.

‏DNS Cache Poisoning ☠️
‏Cache کردن DNS فقط یک بحث Performance نیست و جنبه‌ی امنیتی هم داره. اگه یک مهاجم بتونه یک DNS Resolver رو فریب بده و یک پاسخ جعلی داخل Cache اون قرار بده، درخواست‌های بعدی ممکنه به IP اشتباه هدایت بشن.
به این حمله DNS Cache Poisoning گفته میشه. به همین دلیل مکانیزم‌هایی مثل تصادفی‌سازی Transaction ID و Source Port و همچنین پروتکل‌هایی مثل DNSSEC برای افزایش امنیت DNS استفاده میشن.

جمع‌بندی ✍️
‏DNS Caching باعث میشه نتیجه‌ی DNS Query برای یه مدتی ذخیره بشه تا برای هر درخواست مجبور نباشیم دوباره مسیر Resolution رو طی کنیم.
TTL هم مشخص می‌کنه این نتیجه تا چه مدت معتبره و همین مقدار، بین سرعت انتشار تغییرات DNS و کاهش DNS Queryها یک Trade-off ایجاد می‌کنه.

#️⃣ #network #web #devops


🌙 CHANNEL | GROUP
🔥2
تولدت مبارک پیرمرد قابل اعتماد دنیای لینوکس!
از مرداد ۱۳۷۲ تا امروز Stable مثل همیشه

@DevTwitter | <MehrdadLinux/>
‏Socket Lifecycle چطور کار می‌کنه؟ 🔌
وقتی یه Application می‌خواد با یه سیستم دیگه از طریق Network ارتباط برقرار کنه، در نهایت به چیزی به اسم Socket نیاز داره. Socket رو می‌تونیم به عنوان نقطه‌ی ارتباطی بین Application و Network در نظر بگیریم. اما Socket از همون لحظه‌ای که ساخته میشه تا زمانی که Connection بسته بشه، چند مرحله‌ی مشخص رو طی می‌کنه. این مراحل رو می‌تونیم به عنوان Socket Lifecycle بشناسیم.

ساختن Socket 🧠
اولین مرحله، ساختن خود Socket هست. Application به سیستم‌عامل اعلام می‌کنه که یه Socket می‌خواد و مشخص می‌کنه از چه Address Family و چه نوع پروتکلی قراره استفاده بشه، مثلاً AF_INET برای IPv4 و SOCK_STREAM برای ارتباط TCP. سیستم‌عامل هم یه File Descriptor برمی‌گردونه که Application از طریق اون بعداً با Socket کار می‌کنه. از اینجا به بعد، Socket در اختیار Application هست ولی هنوز به یه Address یا Connection مشخصی وصل نشده.

‏Bind کردن Socket 📍
اگه Application قراره روی یه Address مشخص به Connectionها گوش بده، باید Socket رو با استفاده از bind به یه IP و Port مشخص متصل کنه. مثلاً یه Web Server می‌تونه Socket خودش رو روی 0.0.0.0:8000 بایند کنه تا روی پورت 8000 منتظر Connectionهای ورودی باشه. این مرحله بیشتر برای Serverها اهمیت داره، چون Client معمولاً لازم نیست خودش Port مشخصی رو انتخاب کنه و سیستم‌عامل می‌تونه یه Ephemeral Port براش اختصاص بده.

‏Listen کردن 👂
بعد از bind، یه TCP Server باید Socket رو وارد حالت Listening کنه. با listen به سیستم‌عامل می‌گه که این Socket قراره Connectionهای ورودی رو دریافت کنه. از اینجا به بعد، Connectionهای جدید می‌تونن به Server ارسال بشن و سیستم‌عامل اون‌ها رو در ساختارهای مربوط به Listening Socket مدیریت می‌کنه. مقدار Backlog هم مشخص می‌کنه چه تعداد Connection می‌تونن در صف مربوط به پذیرش قرار بگیرن.

‏Accept کردن Connection 🤝
وقتی یه Client به Server وصل میشه، Server با accept یهی از Connectionهای آماده رو دریافت می‌کنه. نکته‌ی مهم اینه که accept یه Socket جدید برمی‌گردونه و Socket اصلی که روی اون listen انجام شده همچنان برای دریافت Connectionهای جدید باقی می‌مونه. بنابراین Server یه Listening Socket داره و برای هر Connection پذیرفته‌شده یه Connected Socket جداگانه ایجاد میشه. از اینجا به بعد، Application می‌تونه روی Socket جدید داده ارسال و دریافت کنه.

ارتباط با Server از سمت Client 📡
در سمت Client، معمولاً بعد از ساختن Socket، اپلیکیشن از connect استفاده می‌کنه. در TCP، این عملیات باعث شروع TCP Three-Way Handshake میشه تا Connection بین Client و Server برقرار بشه. بعد از موفق شدن Handshake، هر دو طرف یه Socket متصل دارن که می‌تونن از طریق اون داده ارسال و دریافت کنن. سیستم‌عامل هم جزئیات مربوط به TCP Connection مثل Sequence Number و State Connection رو مدیریت می‌کنه.

ارسال و دریافت داده 📦
بعد از برقرار شدن Connection، اپلیکیشن می‌تونه با استفاده از send و recv یا APIهای مشابه داده ارسال و دریافت کنه. داده‌ای که Application می‌فرسته مستقیماً به شکل یه Packet واحد به مقصد نمی‌رسه، بلکه TCP اون رو به جریان Byteها تبدیل می‌کنه و خودش مسئول مواردی مثل ترتیب، Retransmission و کنترل جریان میشه. به همین دلیل TCP برای Application بیشتر شبیه یه Byte Stream دیده میشه تا مجموعه‌ای از Messageهای جداگانه.

بستن Socket 🔒
وقتی ارتباط دیگه مورد نیاز نیست، Application می‌تونه با close سوکت رو ببنده. در TCP، بسته شدن Connection هم خودش بخشی از Lifecycle هست و معمولاً با تبادل پیام‌های FIN و ACK بین دو طرف انجام میشه. بعد از بسته شدن Connection، سیستم‌عامل منابع مربوط به Socket و File Descriptor رو آزاد می‌کنه. البته بسته شدن TCP Connection لزوماً به معنی این نیست که هر دو طرف دقیقاً در یه لحظه Connection رو ببندن و ممکنه هر سمت Lifecycle خودش رو طی کنه.

#️⃣ #network #web #devops


🌙 CHANNEL | GROUP
‌‏Retry & Backoff چطور جلوی فشار بیشتر روی سیستم رو می‌گیرن؟ 🤔
وقتی یک درخواست به یک سرویس دیگه ارسال می‌کنیم، همیشه قرار نیست موفق بشه. ممکنه سرویس موقتاً در دسترس نباشه، شبکه مشکل داشته باشه یا درخواست به خاطر یک خطای موقتی شکست بخوره.
اینجاست که Retry وارد میشه. به جای اینکه با اولین خطا کار رو تموم‌شده در نظر بگیریم، بعد از یک مدت دوباره درخواست رو ارسال می‌کنیم. این کار برای خطاهای موقتی می‌تونه خیلی مفید باشه، اما یک مشکل مهم داره: اگه تعداد زیادی کلاینت همزمان Retry کنن، خود Retry می‌تونه فشار بیشتری به سرویسی که همین الان مشکل داره وارد کنه.

مشکل Retry ساده چیه؟ 🔄
فرض کنین یک سرویس برای چند ثانیه از دسترس خارج شده و 1000 کلاینت همزمان بهش درخواست می‌فرستن. حالا اگه همه‌ی این کلاینت ها بلافاصله بعد از شکست دوباره درخواست خودشون رو ارسال کنن، تعداد زیادی درخواست جدید دقیقاً همون زمانی وارد سیستم میشه که سرویس هنوز درگیر مشکل قبلیه.
حتی بدتر از اون، اگه Retryها با فاصله‌ی ثابتی مثل یک ثانیه انجام بشن، کلاینت ها ممکنه دوباره دقیقاً همزمان Retry کنن. در نتیجه به جای اینکه مشکل برطرف بشه، یک موج جدید از درخواستها ایجاد میشه و فشار روی سرویس بیشتر میشه.

‏Backoff چه مشکلی رو حل می‌کنه؟
‏Backoff یعنی بعد از شکست یک درخواست، قبل از Retry کردن کمی صبر کنیم. اما معمولاً این زمان ثابت نیست و با هر Retry بیشتر میشه. برای مثال می‌تونیم زمان انتظار رو به شکل Exponential Backoff افزایش بدیم:
1s → 2s → 4s → 8s → 16s
با این روش، اگه سرویس برای مدت کوتاهی مشکل داشته باشه، کلاینت ها به جای اینکه مدام بهش درخواست بفرستن، کم‌کم فاصله‌ی Retryها رو بیشتر می‌کنن. این کار فرصت بیشتری به سرویس میده تا Recovery کنه و دوباره آماده‌ی دریافت درخواست بشه.

چرا Exponential Backoff؟ 📈
اگه زمان انتظار با هر Retry بیشتر بشه، تعداد درخواستهای اضافی خیلی بهتر کنترل میشه. فرض کنین اولین Retry بعد از یک ثانیه انجام بشه، Retry بعدی دو ثانیه بعد و بعدی چهار ثانیه بعد. اگه سرویس همچنان Down باشه، کلاینت به جای ارسال مداوم درخواست، به تدریج کمتر به اون سرویس فشار میاره.
البته معمولاً برای Backoff یک Maximum Delay هم تعیین میشه تا زمان انتظار بی‌نهایت زیاد نشه. همچنین تعداد Retryها هم باید محدود باشه، چون Retry کردن یک درخواست برای همیشه نه تنها مفید نیست، بلکه می‌تونه منابع Application رو هم مصرف کنه.

‏Jitter چرا مهمه؟ 🎲
حتی Exponential Backoff هم یک مشکل داره. فرض کنین 1000 کلاینت تقریباً در یک زمان درخواست فرستادن و همزمان شکست خوردن. اگه همه دقیقاً از یک فرمول استفاده کنن، ممکنه همه‌ی اون‌ها بعد از 1 ثانیه Retry کنن، بعد 2 ثانیه و بعد 4 ثانیه.
یعنی با اینکه Backoff داریم، هنوز درخواستها می‌تونن به صورت گروهی و همزمان ارسال بشن. ‏Jitter با اضافه کردن مقداری تصادفی به زمان انتظار، این کلاینتها رو از هم جدا می‌کنه. در نتیجه به جای اینکه همه با هم Retry کنن، درخواستها توی بازه‌های زمانی مختلف پخش میشن و فشار روی سرویس یکنواخت‌تر میشه.

‏Retry برای هر خطایی مناسب نیست ⚠️
یک نکته‌ی خیلی مهم اینه که هر Errorای نباید باعث Retry بشه. اگه درخواست به خاطر یک خطای موقتی مثل Timeout یا بعضی خطاهای سمت Server شکست خورده باشه، Retry می‌تونه منطقی باشه.
اما اگه درخواست به خاطر یک خطای دائمی مثل Invalid Input شکست خورده، ارسال دوباره‌ی همون درخواست احتمالاً هیچ چیزی رو درست نمی‌کنه.
همچنین برای عملیات‌هایی که Side Effect دارن باید دقت بیشتری داشته باشیم. مثلاً اگه یک درخواست باعث ایجاد Order یا پرداخت بشه و کلاینت بعد از Timeout دوباره همون درخواست رو ارسال کنه، ممکنه عملیات دوبار انجام بشه. اینجاست که مفاهیمی مثل Idempotency اهمیت پیدا می‌کنن.

‏Retry می‌تونه مشکل رو بدتر کنه 🔥
‏Retry در ظاهر یک مکانیزم ساده برای افزایش Reliabilityه، اما اگه درست طراحی نشه می‌تونه یک Failure رو به یک Failure بزرگ‌تر تبدیل کنه.
وقتی یک سرویس تحت فشار قرار می‌گیره و کلاینت ها به جای کاهش درخواست، مرتب Retry می‌کنن، Load بیشتری ایجاد میشه و Recovery سخت‌تر میشه.
به همین دلیل Retry معمولاً در کنار چیزهایی مثل Exponential Backoff، Jitter، Retry Limit و Timeout استفاده میشه تا سیستم هم شانس بیشتری برای Recovery داشته باشه و هم خودش باعث تشدید مشکل نشه.

#️⃣ #network #web #devops


🌙 CHANNEL | GROUP
پروتکل ‏ICMP چطور کار می‌کنه؟ 🤔
وقتی اسم شبکه میاد، معمولاً سریع میریم سراغ TCP و UDP و IP. اما یک پروتکل دیگه هم وجود داره که برای خودِ انتقال معمولی Data ساخته نشده، بلکه بیشتر برای گزارش وضعیت و خطاهای شبکه استفاده میشه.
اسمش ICMP (Internet Control Message Protocol) هست. یکی از معروف‌ترین جاهایی که ICMP رو می‌بینیم، ping هست. اما ICMP خیلی بیشتر از این حرف‌هاست و بخش مهمی از نحوه‌ی عیب‌یابی و مدیریت ارتباطات IP رو تشکیل میده.

‏ICMP دقیقاً برای چیه؟ 🧠
‏ICMP یک پروتکل در کنار IP هست که برای ارسال پیام‌های کنترلی و خطاهای مربوط به ارتباطات IP استفاده میشه. مثلاً فرض کنین یک Router یک Packet دریافت می‌کنه، اما مقصد اون Packet قابل دسترسی نیست. Router نمی‌تونه Packet رو به مقصد برسونه و در بعضی شرایط می‌تونه با استفاده از ICMP به فرستنده اطلاع بده که چه اتفاقی افتاده.
پس ICMP قرار نیست مثل TCP یا UDP داده‌ی Application رو از یک نقطه به نقطه‌ی دیگه منتقل کنه. بیشتر وظیفه‌ش اینه که اطلاعاتی درباره‌ی وضعیت ارتباط IP در اختیار سیستم‌ها قرار بده.

‏Ping چطور با ICMP کار می‌کنه؟ 📡
وقتی دستور ping رو اجرا می‌کنیم، معمولاً یک ICMP Echo Request به مقصد ارسال میشه. اگه مقصد در دسترس باشه و Echo Request رو قبول کنه، یک ICMP Echo Reply برمی‌گردونه.
از روی این رفت‌ و برگشت می‌تونیم بفهمیم مقصد قابل دسترسی هست یا نه و همچنین زمان رفت‌ و برگشت Packet رو اندازه بگیریم که بهش Round-Trip Time (RTT) میگیم.
پس وقتی ping می‌زنیم، در حالت معمول خبری از TCP Connection یا UDP Port نیست. داریم از ICMP برای بررسی Reachability و اندازه‌گیری زمان رفت‌وبرگشت استفاده می‌کنیم.

‏ICMP فقط برای Ping نیست ⚠️
یکی از اشتباهات رایج اینه که ICMP رو با Ping یکی بدونیم. ‏Ping فقط یکی از کاربردهای ICMP هست. ‏ICMP پیام‌های مختلفی برای شرایط مختلف داره. مثلاً اگه یک Packet به مقصد نرسه یا یک Router نتونه اون رو Forward کنه، ممکنه یک ICMP Error Message برای فرستنده ارسال بشه تا اون رو از مشکل مطلع کنه.
یکی از نمونه‌های معروفش Destination Unreachable هست که می‌تونه نشون بده مقصد یا سرویس موردنظر قابل دسترسی نیست.

یکی دیگه از پیام‌های معروف این پروتکل، Time Exceeded هست. هر IP Packet یک مقدار به اسم TTL (Time To Live) داره. هر بار که Packet از یک Router عبور می‌کنه، TTL کاهش پیدا می‌کنه. اگه TTL قبل از رسیدن Packet به مقصد به صفر برسه، Router اون Packet رو Drop می‌کنه و معمولاً یک ICMP Time Exceeded برای فرستنده ارسال می‌کنه.
این دقیقاً یکی از چیزهاییه که ابزارهایی مثل traceroute ازش استفاده می‌کنن تا بفهمن Packet برای رسیدن به مقصد از چه Routerهایی عبور می‌کنه.

‏ICMP روی TCP و UDP قرار نمی‌گیره 🧩
یک نکته‌ ی مهم درباره‌ی ICMP اینه که مثل HTTP که روی TCP اجرا میشه یا DNS که می‌تونه از UDP و TCP استفاده کنه، ICMP روی TCP یا UDP قرار نگرفته. ‏ICMP بخشی از مجموعه‌ی پروتکل‌های IP محسوب میشه و پیام‌های اون مستقیماً داخل IP Packet قرار می‌گیرن. به همین دلیل وقتی یک ICMP Echo Request ارسال میشه، خبری از Port Number مثل TCP یا UDP وجود نداره. ICMP ساختار پیام و Type و Code خودش رو داره.

حالا ICMP چه نقشی توی شبکه داره؟ 🌐
‏ICMP کمک می‌کنه سیستم‌ها و تجهیزات شبکه درباره‌ی وضعیت ارتباطات IP اطلاعات بیشتری داشته باشن. از بررسی اینکه یک مقصد قابل دسترسی هست یا نه گرفته تا گزارش بعضی خطاهای مربوط به Routing و انتقال Packetها، ICMP یک کانال مهم برای این نوع اطلاعات فراهم می‌کنه.
البته ICMP خودش تضمین نمی‌کنه که یک Host حتماً به یک Application خاص پاسخ میده. مثلاً ممکنه یک Server به Ping جواب نده، ولی سرویس HTTP اون کاملاً در دسترس باشه، چون ممکنه ICMP توسط Firewall فیلتر شده باشه.

‏جمع‌بندی ✍️
‏ICMP یه پروتکل برای انتقال معمولی داده‌های Application نیست. وظیفه‌ی اصلیش ارسال Control Message و Error Message مربوط به ارتباطات IP هست.
ping از Echo Request و Echo Reply استفاده می‌کنه، traceroute از پیام‌های Time Exceeded کمک می‌گیره و Routerها هم می‌تونن برای گزارش بعضی خطاهای شبکه از ICMP استفاده کنن.


#️⃣ #network


🌙 CHANNEL | GROUP
‏Circuit Breaker چطور جلوی Chain Failure رو می‌گیره؟ 🤔
توی یه سیستم ساده، اگه یه سرویس Down بشه، معمولاً فقط همون سرویس دچار مشکل میشه. اما توی یه سیستم توزیع شده و بزرگ که چندین سرویس به هم وابسته‌ان، قضیه می‌تونه خیلی سریع پیچیده بشه.
فرض کنین Service A برای انجام یه Request به Service B نیاز داره و Service B هم به Service C وابسته است. حالا اگه Service C دچار مشکل بشه، Requestهای Service B شروع به شکست خوردن می‌کنن و این شکست‌ها می‌تونن به Service A هم منتقل بشن.
اگه Service A همچنان به Service B درخواست بفرسته و منتظر Timeout بمونه، کم‌کم Connectionها و منابعش هم درگیر میشن و ممکنه در نهایت خود Service A هم از کار بیفته.
اینجاست که Circuit Breaker وارد میشه.

‏Circuit Breaker دقیقاً چیه؟ 🧠
‏Circuit Breaker یه الگوی طراحی برای جلوگیری از ادامه پیدا کردن درخواست‌ها به یه سرویس خراب یا غیرقابل دسترسه. ایده‌ی اصلیش شبیه Circuit Breakerهای برق هست. وقتی سیستم تشخیص بده تعداد زیادی از درخواست‌ها دارن شکست می‌خورن، به جای اینکه همچنان درخواست‌های جدید رو ارسال کنه، مسیر ارتباط رو موقتاً قطع می‌کنه.
توی این حالت Requestها دیگه به سرویس مشکل‌دار ارسال نمیشن و Application می‌تونه سریع‌تر یه Error یا Fallback برگردونه. این کار باعث میشه یه Failure محدود، به Failure بزرگ‌تری در کل سیستم تبدیل نشه.

‏Closed State 🔵
در حالت عادی، Circuit در وضعیت Closed قرار داره. توی این حالت Requestها مثل همیشه به سرویس مقصد ارسال میشن و Circuit Breaker نتیجه‌ی اون‌ها رو بررسی می‌کنه.
اگه Requestها موفق باشن، اتفاق خاصی نمیفته. اما اگه تعداد خطاها یا Timeoutها از یه Threshold مشخص بیشتر بشه، Circuit Breaker متوجه میشه که احتمالاً سرویس مقصد دچار مشکل شده و Circuit رو باز می‌کنه.

‏Open State 🔴
وقتی Circuit وارد حالت Open میشه، Requestهای جدید دیگه به سرویس مقصد ارسال نمیشن. به جای اینکه Application هر بار یه Connection ایجاد کنه، منتظر Timeout بمونه و دوباره شکست بخوره، Circuit Breaker خیلی سریع Request رو Fail می‌کنه.
این موضوع هم از ارسال Requestهای اضافی جلوگیری می‌کنه و هم باعث میشه منابع Application برای مدت طولانی درگیر سرویس خراب نشن. در همین مدت، سرویس مقصد هم فرصت پیدا می‌کنه بدون دریافت حجم زیادی از Requestهای جدید، Recovery کنه.

‏Half-Open State 🟡
اما Circuit نمی‌تونه برای همیشه Open باقی بمونه. بعد از گذشت یه مدت مشخص، Circuit وارد وضعیت Half-Open میشه تا بررسی کنه سرویس مقصد دوباره سالم شده یا نه. توی این حالت معمولاً تعداد محدودی Request اجازه پیدا می‌کنن به سرویس مقصد ارسال بشن.
اگه این Requestها موفق باشن، Circuit دوباره به حالت Closed برمی‌گرده و ارتباط عادی ادامه پیدا می‌کنه. اما اگه دوباره Failure اتفاق بیفته، Circuit دوباره Open میشه و مدتی دیگه Requestها رو متوقف می‌کنه.

‏Circuit Breaker و Timeout چه فرقی دارن؟ ⏱️
‏Timeout فقط مشخص می‌کنه Application تا چه مدت برای دریافت جواب یه Request صبر کنه. اما Circuit Breaker یه قدم جلوتر میره.
اگه یه سرویس مرتب Timeout بشه، بدون Circuit Breaker همچنان Requestهای جدید ارسال میشن و هرکدوم ممکنه تا زمان Timeout منابع Application رو درگیر کنن.
‏Circuit Breaker بعد از تشخیص این الگوی شکست، جلوی Requestهای جدید رو می‌گیره و اجازه نمیده منابع سیستم به خاطر یه سرویس خراب بی‌دلیل مصرف بشن.

‏Fallback همیشه جواب نیست 💡
وقتی Circuit باز میشه، Application معمولاً باید یه رفتار جایگزین داشته باشه. مثلاً ممکنه به جای دریافت اطلاعات از سرویس اصلی، داده‌ی Cache شده رو برگردونه یا یه جواب ساده‌تر به کاربر بده.
اما Fallback نباید فقط برای مخفی کردن Error استفاده بشه. اگه Fallback خودش وابسته به یه سرویس خراب دیگه باشه، ممکنه مشکل فقط از یه سرویس به سرویس دیگه منتقل بشه. پس باید دقت کنیم مسیر جایگزین واقعاً مستقل و قابل استفاده باشه.

جمع‌بندی ✍️
‏Circuit Breaker قرار نیست یه سرویس خراب رو درست کنه. کاری که انجام میده اینه که Failure رو محدود می‌کنه و نمیذاره مشکل یه سرویس به بخش‌های دیگه‌ی سیستم سرایت کنه.
با سه وضعیت اصلی Closed، Open و Half-Open، تشخیص میده چه زمانی درخواست‌ها عادی باشن، چه زمانی متوقف بشن و چه زمانی دوباره سلامت سرویس رو بررسی کنه.


#️⃣ #system_design #backend #programming


🌙 CHANNEL | GROUP
4
Please open Telegram to view this post
VIEW IN TELEGRAM