Midnight Commit
105 subscribers
3 photos
1 file
41 links
Beyond Syntax.
The fundamentals behind great software.
Download Telegram
چرا همه‌ی 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
۱۰۰تایی شدییمممم🎉🥳
🔥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/>