Midnight Commit
105 subscribers
3 photos
1 file
41 links
Beyond Syntax.
The fundamentals behind great software.
Download Telegram
ایندکس در PostgreSQL دقیقاً یعنی چی؟ 🤔
وقتی درباره‌ی 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ها مقاوم‌تر بشن.


#️⃣ #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، و بر اساس نیاز واقعی سیستم تصمیم بگیریم.

#️⃣ #backend #system_design


🌙 CHANNEL | GROUP
جنایات جنگی توی برنامه نویسی، اشتباه ترین کارای ممکن به فان ترین شکل ممکن.

یه جاهایی انقدر ترسناک میشه که آدم بجای خندیدن فقط خیره میشه به صفحه


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 فقط ابزاری برای تقسیم درخواست‌ها نیست.
نحوه‌ی توزیع درخواست‌ها می‌تونه روی سرعت پاسخ‌گویی، استفاده از منابع و حتی پایداری کل سیستم تأثیر مستقیمی داشته باشه.
به همین دلیل، انتخاب الگوریتم مناسب یکی از تصمیم‌های مهم در طراحی سیستم‌های مقیاس‌پذیره و معمولاً هیچ پاسخ یکسانی برای همه‌ی پروژه‌ها وجود نداره.

#️⃣ #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 تعادل برقرار کنیم.

#️⃣ #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 پرداخت می‌کنیم.

#️⃣ #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 با پسوند .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 انتخاب کنیم.
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 رو داشته باشیم:
‏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 همزمان داخل سیستم ایجاد بشه.
به همین خاطر هم تنظیم 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