حالا
در واقع Reference با اسم
اگه این آخرین Reference باشه، در CPython معمولاً Reference Count به صفر میرسه و Object قابل Deallocation میشه. اما همونطور که گفتیم، Memory آزادشده ممکنه همچنان توسط Python Allocator نگه داشته بشه و فوراً به سیستمعامل برنگرده.
Memory Leak توی Python هم داریم؟ ⚠️
پایتون Garbage Collector داره، اما این به معنی غیرممکن بودن Memory Leak نیست. اگه برنامه به Objectهایی Reference نگه داره که دیگه واقعاً بهشون نیاز نداره، Garbage Collector نمیتونه اونها رو حذف کنه، چون از دید Python هنوز قابل دسترس هستن.
مثلاً یک Cache بدون محدودیت، یک Global Collection که دائماً بزرگتر میشه یا نگه داشتن Reference به Objectهای قدیمی میتونه باعث بشه Memory مصرفی برنامه به مرور افزایش پیدا کنه.
پس Garbage Collector فقط Objectهایی رو جمع میکنه که واقعاً دیگه قابل دسترسی نیستن. اگه خود برنامه همچنان Reference رو نگه داشته باشه، GC نمیتونه تشخیص بده که ما دیگه به اون Object احتیاج نداریم.
جمعبندی ✍️
Memory Management تو Python ترکیبی از چند بخش مختلفه. Objectها روی Python Heap قرار میگیرن، Reference Counting بیشتر Objectهای بدون Reference رو مدیریت میکنه و Garbage Collector برای پیدا کردن Reference Cycleها وارد عمل میشه.
از طرف دیگه، CPython با Memory Allocator خودش Allocationهای کوچک رو مدیریت میکنه و Memory آزادشده رو لزوماً بلافاصله به سیستمعامل برنمیگردونه.
به همین دلیل وقتی دربارهی Memory حرف میزنیم، فقط اینکه «Python خودش Garbage Collection داره» تصویر کاملی بهمون نمیده. باید بدونیم Referenceها، Objectها، Allocator و Garbage Collector چطور کنار هم کار میکنن.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
del دقیقاً چی کار میکنه؟ 🗑
del مستقیماً به معنی «این Object رو از Memory پاک کن» نیست. وقتی مینویسیم:del users
در واقع Reference با اسم
users رو حذف میکنیم. اگه Reference دیگهای به Object وجود داشته باشه، Object همچنان باقی میمونه.اگه این آخرین Reference باشه، در CPython معمولاً Reference Count به صفر میرسه و Object قابل Deallocation میشه. اما همونطور که گفتیم، Memory آزادشده ممکنه همچنان توسط Python Allocator نگه داشته بشه و فوراً به سیستمعامل برنگرده.
Memory Leak توی Python هم داریم؟ ⚠️
پایتون Garbage Collector داره، اما این به معنی غیرممکن بودن Memory Leak نیست. اگه برنامه به Objectهایی Reference نگه داره که دیگه واقعاً بهشون نیاز نداره، Garbage Collector نمیتونه اونها رو حذف کنه، چون از دید Python هنوز قابل دسترس هستن.
مثلاً یک Cache بدون محدودیت، یک Global Collection که دائماً بزرگتر میشه یا نگه داشتن Reference به Objectهای قدیمی میتونه باعث بشه Memory مصرفی برنامه به مرور افزایش پیدا کنه.
پس Garbage Collector فقط Objectهایی رو جمع میکنه که واقعاً دیگه قابل دسترسی نیستن. اگه خود برنامه همچنان Reference رو نگه داشته باشه، GC نمیتونه تشخیص بده که ما دیگه به اون Object احتیاج نداریم.
جمعبندی ✍️
Memory Management تو Python ترکیبی از چند بخش مختلفه. Objectها روی Python Heap قرار میگیرن، Reference Counting بیشتر Objectهای بدون Reference رو مدیریت میکنه و Garbage Collector برای پیدا کردن Reference Cycleها وارد عمل میشه.
از طرف دیگه، CPython با Memory Allocator خودش Allocationهای کوچک رو مدیریت میکنه و Memory آزادشده رو لزوماً بلافاصله به سیستمعامل برنمیگردونه.
به همین دلیل وقتی دربارهی Memory حرف میزنیم، فقط اینکه «Python خودش Garbage Collection داره» تصویر کاملی بهمون نمیده. باید بدونیم Referenceها، Objectها، Allocator و Garbage Collector چطور کنار هم کار میکنن.
🧩Part02
➖➖➖➖➖➖➖➖➖➖
#️⃣ #programming #backend #python
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
Python Import System
وقتی توی Python مینویسیم:
خیلی راحت میشه تصور کرد Python فقط میره فایل
اما
Python از کجا Module رو پیدا میکنه؟ 🔎
وقتی Python با یه
مثلاً اگه
ترتیب این مسیرها هم مهمه، چون Python اونها رو برای پیدا کردن Module بررسی میکنه و اولین مورد مناسب میتونه همونی باشه که Load میشه.
فقط دنبال فایل
نه. Python میتونه Module رو از منابع مختلفی پیدا کنه. یک Module ممکنه یک فایل Python، یک Package، یک Extension Module نوشتهشده با C یا حتی نوع دیگهای از Import Source باشه.
اینجاست که مفهوم Importer و Finder وارد میشه. Python یک سیستم قابل توسعه برای پیدا کردن و Load کردن Moduleها داره که بهش Import Machinery میگیم. این سیستم تصمیم میگیره Module موردنظر از کجا پیدا بشه و چطور Load بشه.
Finder و Loader چه کار میکنن؟ 🧩
به شکل ساده میتونیم فرآیند رو به دو قسمت تقسیم کنیم.
Finder وظیفه داره بفهمه Module موردنظر کجاست و آیا اصلاً میتونه اون رو پیدا کنه یا نه. نتیجهی این مرحله چیزی به اسم Module Spec هست که اطلاعاتی دربارهی Module و نحوهی Load کردنش رو در اختیار Python قرار میده.
بعد Loader وارد میشه و بر اساس همون Spec، ماژول رو Load میکنه. برای یه فایل Python، این مرحله در نهایت باعث میشه Code مربوط به Module اجرا بشه و یک Module Object ساخته بشه.
وقتی یه ماژول Import میشه چه اتفاقی میفته؟ ⚙️
فرض کنین داریم:
Python اول بررسی میکنه که Module قبلاً Import شده یا نه. اگه پیدا نشه، سیستم Import اون رو پیدا میکنه، یک Module Object برایش ایجاد میکنه و Code داخل Module رو اجرا میکنه. این موضوع خیلی مهمه، چون Code سطح Module معمولاً موقع Import اجرا میشه.
یکی از مهمترین قسمتهای Import System، دیکشنری
اگه Module قبلاً Import شده باشه، Python معمولاً همون Module Object موجود رو دوباره در اختیار Import کننده قرار میده. به همین دلیله که وقتی یک Module رو چند بار Import میکنیم، Code داخلش قرار نیست هر بار از اول اجرا بشه.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
وقتی توی Python مینویسیم:
import my_module
خیلی راحت میشه تصور کرد Python فقط میره فایل
my_module.py رو پیدا میکنه و کدهای داخلش رو اجرا میکنه.اما
import در واقع یه سیستم نسبتاً پیچیده پشت خودش داره. Python باید اول بفهمه Module موردنظر کجاست، بعد اون رو پیدا کنه، Load و Execute کنه و در نهایت نتیجه رو طوری نگه داره که Importهای بعدی دوباره همهچیز رو از اول انجام ندن.Python از کجا Module رو پیدا میکنه؟ 🔎
وقتی Python با یه
import مواجه میشه، یکی از اولین چیزهایی که بررسی میکنه sys.path هست. sys.path در واقع لیستی از مسیرهاییه که Python برای پیدا کردن Moduleها و Packageها بررسی میکنه. مسیر فعلی پروژه، مسیرهای مربوط به Standard Library و مسیرهایی که Packageها داخلشون نصب شدن، میتونن بخشی از این لیست باشن.مثلاً اگه
my_module.py توی یکی از مسیرهای موجود در sys.path باشه، Python میتونه اون رو پیدا کنه.import sys
print(sys.path)
ترتیب این مسیرها هم مهمه، چون Python اونها رو برای پیدا کردن Module بررسی میکنه و اولین مورد مناسب میتونه همونی باشه که Load میشه.
فقط دنبال فایل
.py میگرده؟ 📦نه. Python میتونه Module رو از منابع مختلفی پیدا کنه. یک Module ممکنه یک فایل Python، یک Package، یک Extension Module نوشتهشده با C یا حتی نوع دیگهای از Import Source باشه.
اینجاست که مفهوم Importer و Finder وارد میشه. Python یک سیستم قابل توسعه برای پیدا کردن و Load کردن Moduleها داره که بهش Import Machinery میگیم. این سیستم تصمیم میگیره Module موردنظر از کجا پیدا بشه و چطور Load بشه.
Finder و Loader چه کار میکنن؟ 🧩
به شکل ساده میتونیم فرآیند رو به دو قسمت تقسیم کنیم.
Finder وظیفه داره بفهمه Module موردنظر کجاست و آیا اصلاً میتونه اون رو پیدا کنه یا نه. نتیجهی این مرحله چیزی به اسم Module Spec هست که اطلاعاتی دربارهی Module و نحوهی Load کردنش رو در اختیار Python قرار میده.
بعد Loader وارد میشه و بر اساس همون Spec، ماژول رو Load میکنه. برای یه فایل Python، این مرحله در نهایت باعث میشه Code مربوط به Module اجرا بشه و یک Module Object ساخته بشه.
وقتی یه ماژول Import میشه چه اتفاقی میفته؟ ⚙️
فرض کنین داریم:
import my_module
Python اول بررسی میکنه که Module قبلاً Import شده یا نه. اگه پیدا نشه، سیستم Import اون رو پیدا میکنه، یک Module Object برایش ایجاد میکنه و Code داخل Module رو اجرا میکنه. این موضوع خیلی مهمه، چون Code سطح Module معمولاً موقع Import اجرا میشه.
sys.modules چه نقشی داره؟ 🗃یکی از مهمترین قسمتهای Import System، دیکشنری
sys.modules هست. پایتون Moduleهایی که Import شدن رو داخل این Dictionary نگه میداره. قبل از اینکه Python دوباره وارد فرآیند پیدا کردن و Load کردن Module بشه، sys.modules رو بررسی میکنه.import sys
print(sys.modules["my_module"])
اگه Module قبلاً Import شده باشه، Python معمولاً همون Module Object موجود رو دوباره در اختیار Import کننده قرار میده. به همین دلیله که وقتی یک Module رو چند بار Import میکنیم، Code داخلش قرار نیست هر بار از اول اجرا بشه.
🧩Part01
➖➖➖➖➖➖➖➖➖➖
#️⃣ #programming #backend #python
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
پس
در حالت معمول، Code سطح یک Module فقط در اولین Import اجرا میشه. مثلاً اگر چند جای مختلف Application بنویسیم:
Python برای Importهای بعدی از Module موجود در
این دو کد شبیه هم به نظر میان:
و:
اما چیزی که وارد Namespace فعلی میشه متفاوته.
در حالت اول، اسم
در هر دو حالت، Python همچنان از Import System و
Circular Import چطور به وجود میاد؟ 🔄
یکی از مشکلات معروف Circular Import, Import System هست.
فرض کنین
این موضوع میتونه باعث بشه یک Module قبل از اینکه کامل اجرا بشه، توسط Module دیگه مورد استفاده قرار بگیره و در نتیجه Attribute یا Object موردنظر هنوز ساخته نشده باشه.
به همین دلیل Circular Importها معمولاً نشونهای از وابستگی نامناسب بین بخشهای مختلف یک پروژه هستن و بهتره ساختار Dependencyها بررسی بشه.
جمعبندی ✍️
وقتی مینویسیم
اول مسیرهایی مثل
پس پشت همین کلمهی سادهی
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
import فقط یک بار اجرا میشه؟ 🔄در حالت معمول، Code سطح یک Module فقط در اولین Import اجرا میشه. مثلاً اگر چند جای مختلف Application بنویسیم:
import my_module
Python برای Importهای بعدی از Module موجود در
sys.modules استفاده میکنه. نکتهی مهم اینه که این Cache مربوط به همون Python Process هست. اگر Process جدیدی اجرا بشه، sys.modules جدیدی داره و Moduleها دوباره Import میشن.
import با from import چه فرقی داره؟ 🤔این دو کد شبیه هم به نظر میان:
import math
math.sqrt(16)
و:
from math import sqrt
sqrt(16)
اما چیزی که وارد Namespace فعلی میشه متفاوته.
در حالت اول، اسم
math وارد Namespace فعلی میشه و از طریق اون به sqrt دسترسی داریم. در حالت دوم، خود sqrt به Namespace فعلی اضافه میشه.در هر دو حالت، Python همچنان از Import System و
sys.modules استفاده میکنه و Module مربوط به math رو جداگانه دوباره Load نمیکنه.Circular Import چطور به وجود میاد؟ 🔄
یکی از مشکلات معروف Circular Import, Import System هست.
فرض کنین
module_aبیاد module_b رو Import کنه و module_b هم module_a رو Import کنه. حالا Python باید Moduleهایی رو Load کنه که به شکل چرخهای به هم وابستهان.این موضوع میتونه باعث بشه یک Module قبل از اینکه کامل اجرا بشه، توسط Module دیگه مورد استفاده قرار بگیره و در نتیجه Attribute یا Object موردنظر هنوز ساخته نشده باشه.
به همین دلیل Circular Importها معمولاً نشونهای از وابستگی نامناسب بین بخشهای مختلف یک پروژه هستن و بهتره ساختار Dependencyها بررسی بشه.
جمعبندی ✍️
وقتی مینویسیم
import something، پایتون فقط دنبال یه فایل نمیگرده.اول مسیرهایی مثل
sys.path برای پیدا کردن Module بررسی میشن، بعد Finder و Loader وارد فرآیند میشن و Module ساخته و اجرا میشه. بعد از Import موفق، Module Object داخل sys.modules قرار میگیره تا Importهای بعدی بتونن از همون Object استفاده کنن.پس پشت همین کلمهی سادهی
import، یه سیستم کامل برای پیدا کردن، Load کردن، اجرای Code و Cache کردن Moduleها وجود داره.🧩Part02
➖➖➖➖➖➖➖➖➖➖
#️⃣ #programming #backend #python
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤2🔥1
WSGI vs ASGI: دو مدل ارتباط بین Web Server و Application 🌐
وقتی یه Request به یه Application میرسه، معمولاً Web Server مستقیماً با کد اصلی Application صحبت نمیکنه. بین این دو، یه Interface قرار میگیره که مشخص میکنه Requestها چطور دریافت، پردازش و Responseها چطور برگردونده بشن.
دو تا از معروفترین استانداردها توی اکوسیستم پایتون، WSGI و ASGI هستن.
WSGI چیه؟ 🧠
WSGI یه استاندارد برای ارتباط بین Web Server و Application هست که برای مدل سنتی Web Applicationها طراحی شد. توی این مدل، هر Request به Application داده میشه و Application باید تا آماده شدن Response، پردازش مربوط به اون Request رو انجام بده.
این مدل برای خیلی از Applicationهای معمولی کاملاً مناسب بود، چون بیشتر درخواستها شامل پردازش کوتاه و چند عملیات ساده مثل خوندن از Database بودن. اما با رشد Applicationهای Real-time و سرویسهایی که تعداد زیادی Connection همزمان دارن، محدودیتهای این مدل بیشتر مشخص شد.
ASGI چیه؟ ⚡️
ASGI نسخهی جدیدتری از این ایده هست که برای نیازهای مدرنتر طراحی شده. برخلاف WSGI که بیشتر روی مدل سادهی Request/Response تمرکز داشت، ASGI امکان استفاده از مدلهای Asynchronous و Connectionهای طولانی مثل WebSocket رو هم فراهم میکنه.
توی این مدل، Application میتونه موقع انتظار برای عملیاتهایی مثل Network یا I/O، اجرای خودش رو متوقف کنه و اجازه بده کارهای دیگه هم انجام بشن.
تفاوت اصلی WSGI و ASGI چیه؟ ⚖️
تفاوت اصلی این دوتا توی مدل اجرای Requestهاست.
WSGI برای اجرای Synchronous طراحی شده؛ یعنی هر Request معمولاً یه مسیر مشخص رو طی میکنه و تا زمانی که پردازش اون کامل نشه، نتیجه برنمیگرده.
اما ASGI برای محیطهایی ساخته شده که تعداد زیادی عملیات همزمان و انتظارهای طولانی وجود داره. با استفاده از Async، برنامه میتونه منابع رو بهتر مدیریت کنه و مواقعی که منتظر پاسخ یه عملیات I/O هست، بلاک نشه.
به همین دلیل ASGI بیشتر برای سیستمهایی مناسبتره که با Connectionهای زیاد، WebSocket یا عملیاتهای I/O-heavy سروکار دارن.
حالا ASGI همیشه بهتره؟ 🤔
نه. Async بودن به معنی سریعتر بودن همهی برنامهها نیست. اگه یه Application بیشتر درگیر پردازشهای سنگین CPU باشه، ASGI به تنهایی مشکل خاصی رو حل نمیکنه.
مزیت اصلی ASGI زمانی مشخص میشه که برنامه بیشتر زمان خودش رو صرف انتظار برای منابع خارجی مثل Database، APIهای دیگه یا Network میکنه.
WSGI و ASGI در Python 🐍
تو اکوسیستم Python این دو استاندارد خیلی معروف شدن. فریمورک هایی مثل Django، Flask و FastAPI بسته به نیاز میتونن با یکی از این مدلها اجرا بشن.
مثلاً FastAPI معمولاً با ASGI Serverهایی مثل Uvicorn استفاده میشه و Django هم توی نسخههای جدیدتر پشتیبانی از ASGI رو اضافه کرده.
اما خود مفهوم WSGI و ASGI فقط مخصوص یه Framework خاص نیست؛ این ها استانداردهایی برای نحوهی ارتباط Server و Application هستن.
جمعبندی ✍️
WSGI برای نسل قدیمیتر Web Applicationها ساخته شد؛ جایی که مدل اصلی ارتباط، Request و Response ساده بود.
ASGI برای دنیایی طراحی شد که Applicationها باید Connectionهای بیشتر، عملیاتهای همزمان و ارتباطات Real-time رو مدیریت کنن.
تفاوت اصلی این دو در اینه که WSGI روی مدل Synchronous تمرکز داره، ولی ASGI امکان استفاده از مدل Asynchronous و Event-driven رو فراهم میکنه.
➖➖➖➖➖➖➖➖➖➖
وقتی یه Request به یه Application میرسه، معمولاً Web Server مستقیماً با کد اصلی Application صحبت نمیکنه. بین این دو، یه Interface قرار میگیره که مشخص میکنه Requestها چطور دریافت، پردازش و Responseها چطور برگردونده بشن.
دو تا از معروفترین استانداردها توی اکوسیستم پایتون، WSGI و ASGI هستن.
WSGI چیه؟ 🧠
WSGI یه استاندارد برای ارتباط بین Web Server و Application هست که برای مدل سنتی Web Applicationها طراحی شد. توی این مدل، هر Request به Application داده میشه و Application باید تا آماده شدن Response، پردازش مربوط به اون Request رو انجام بده.
این مدل برای خیلی از Applicationهای معمولی کاملاً مناسب بود، چون بیشتر درخواستها شامل پردازش کوتاه و چند عملیات ساده مثل خوندن از Database بودن. اما با رشد Applicationهای Real-time و سرویسهایی که تعداد زیادی Connection همزمان دارن، محدودیتهای این مدل بیشتر مشخص شد.
ASGI چیه؟ ⚡️
ASGI نسخهی جدیدتری از این ایده هست که برای نیازهای مدرنتر طراحی شده. برخلاف WSGI که بیشتر روی مدل سادهی Request/Response تمرکز داشت، ASGI امکان استفاده از مدلهای Asynchronous و Connectionهای طولانی مثل WebSocket رو هم فراهم میکنه.
توی این مدل، Application میتونه موقع انتظار برای عملیاتهایی مثل Network یا I/O، اجرای خودش رو متوقف کنه و اجازه بده کارهای دیگه هم انجام بشن.
تفاوت اصلی WSGI و ASGI چیه؟ ⚖️
تفاوت اصلی این دوتا توی مدل اجرای Requestهاست.
WSGI برای اجرای Synchronous طراحی شده؛ یعنی هر Request معمولاً یه مسیر مشخص رو طی میکنه و تا زمانی که پردازش اون کامل نشه، نتیجه برنمیگرده.
اما ASGI برای محیطهایی ساخته شده که تعداد زیادی عملیات همزمان و انتظارهای طولانی وجود داره. با استفاده از Async، برنامه میتونه منابع رو بهتر مدیریت کنه و مواقعی که منتظر پاسخ یه عملیات I/O هست، بلاک نشه.
به همین دلیل ASGI بیشتر برای سیستمهایی مناسبتره که با Connectionهای زیاد، WebSocket یا عملیاتهای I/O-heavy سروکار دارن.
حالا ASGI همیشه بهتره؟ 🤔
نه. Async بودن به معنی سریعتر بودن همهی برنامهها نیست. اگه یه Application بیشتر درگیر پردازشهای سنگین CPU باشه، ASGI به تنهایی مشکل خاصی رو حل نمیکنه.
مزیت اصلی ASGI زمانی مشخص میشه که برنامه بیشتر زمان خودش رو صرف انتظار برای منابع خارجی مثل Database، APIهای دیگه یا Network میکنه.
WSGI و ASGI در Python 🐍
تو اکوسیستم Python این دو استاندارد خیلی معروف شدن. فریمورک هایی مثل Django، Flask و FastAPI بسته به نیاز میتونن با یکی از این مدلها اجرا بشن.
مثلاً FastAPI معمولاً با ASGI Serverهایی مثل Uvicorn استفاده میشه و Django هم توی نسخههای جدیدتر پشتیبانی از ASGI رو اضافه کرده.
اما خود مفهوم WSGI و ASGI فقط مخصوص یه Framework خاص نیست؛ این ها استانداردهایی برای نحوهی ارتباط Server و Application هستن.
جمعبندی ✍️
WSGI برای نسل قدیمیتر Web Applicationها ساخته شد؛ جایی که مدل اصلی ارتباط، Request و Response ساده بود.
ASGI برای دنیایی طراحی شد که Applicationها باید Connectionهای بیشتر، عملیاتهای همزمان و ارتباطات Real-time رو مدیریت کنن.
تفاوت اصلی این دو در اینه که WSGI روی مدل Synchronous تمرکز داره، ولی ASGI امکان استفاده از مدل Asynchronous و Event-driven رو فراهم میکنه.
#️⃣ #programming #backend #python
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤3
Bulkhead Pattern چطور منابع رو نجات میده؟ 🛡
توی یه سیستم توزیعشده، همیشه این احتمال وجود داره که یکی از سرویسها کند بشه یا از دسترس خارج بشه.
مشکل از جایی شروع میشه که این Failure فقط روی همون سرویس باقی نمونه و بتونه منابع سرویسهای دیگه رو هم مصرف کنه. مثلاً یه Dependency که Responseهاش خیلی دیر برمیگرده، میتونه Connection Pool یا Threadهای Application رو اشغال کنه و در نهایت باعث بشه Requestهای مربوط به بخشهای کاملاً متفاوت هم نتونن پردازش بشن.
اینجاست که Bulkhead Pattern وارد میشه.
Bulkhead Pattern چیه؟ 🧱
ایدهی اصلی Bulkhead خیلی سادهست: منابع سیستم رو به چند بخش مستقل تقسیم کنیم تا Failure یک بخش، منابع بخشهای دیگه رو مصرف نکنه.
اسم Bulkhead از دیوارههای جداکنندهی داخل کشتی گرفته شده. اگه یه قسمت از کشتی آسیب ببینه، این دیوارهها جلوی پخش شدن آب به کل کشتی رو میگیرن. توی نرم افزار هم دقیقاً دنبال همین اتفاقیم. به جای اینکه همهی Requestها و Dependencyها از یه Pool مشترک استفاده کنن، منابع رو به چند محدودهی جدا تقسیم میکنیم.
مشکل منابع اشتراکی🔗
فرض کنین Application ما برای چند Dependency مختلف از یه Connection Pool مشترک استفاده میکنه. اگه یکی از این Dependencyها کند بشه، Requestهای مربوط به اون سرویس مدت بیشتری Connection رو باز نگه میدارن. کمکم Pool پر میشه و Requestهای مربوط به Dependencyهای سالم هم دیگه Connection آزاد برای استفاده ندارن.
در نتیجه یه مشکل توی فقط یه Dependency میتونه باعث بشه بخشهایی که هیچ ارتباطی با اون ندارن هم از کار بیفتن. این همون چیزیه که بهش Cascading Failure میگیم.
Bulkhead چطور جلوی این اتفاق رو میگیره؟ ⚙️
Bulkhead منابع رو به چند Pool یا محدودهی مستقل تقسیم میکنه. مثلاً به جای اینکه همه ی Requestها از یه Thread Pool یا Connection Pool مشترک استفاده کنن، برای بخشهای مختلف ظرفیت جدا تعریف میکنیم.
حالا اگه Dependency A کند بشه و تمام ظرفیت اختصاص دادهشده به خودش رو مصرف کنه، فقط همون بخش تحت تأثیر قرار میگیره. Dependency B هنوز منابع خودش رو داره و میتونه به Requestهای مربوط به خودش جواب بده.
در واقع Bulkhead جلوی Failure رو نمیگیره؛ کاری میکنه که Failure نتونه آزادانه پخش بشه.
Resource Isolation 🔒
یکی از رایجترین شکلهای Bulkhead، جدا کردن منابع محاسباتیه. میتونیم برای بخشهای مختلف سیستم Thread Poolهای جدا داشته باشیم یا تعداد Connectionهایی که هر Dependency میتونه از Pool دریافت کنه رو محدود کنیم.با این کار، یه Dependency نمیتونه تمام منابع مشترک رو مصرف کنه.
این موضوع مخصوصاً توی سیستمهایی مهمه که چند Dependency با رفتار متفاوت دارن. مثلاً اگه یه سرویس معمولاً سریع جواب بده ولی یه سرویس دیگه ممکنه چند ثانیه یا حتی بیشتر طول بکشه، منطقی نیست هردوشون بدون هیچ محدودیتی از یه Resource Pool استفاده کنن.
Semaphore Bulkhead 🚦
یه روش دیگه برای پیادهسازی Bulkhead، محدود کردن تعداد عملیات همزمان با استفاده از Semaphore هست.
فرض کنین برای ارتباط با یه سرویس خارجی فقط اجازه بدیم حداکثر ۲۰ درخواست به صورت همزمان در حال اجرا باشن. وقتی ظرفیت پر شد، Requestهای جدید دیگه وارد اون بخش نمیشن یا طبق سیاست سیستم Fail Fast میشن.
در نتیجه حتی اگه اون Dependency کاملاً کند یا Unresponsive بشه، نمیتونه تعداد نامحدودی از Requestهای Application رو درگیر خودش کنه.
Bulkhead و Circuit Breaker 🔄
Bulkhead و Circuit Breaker معمولاً کنار هم استفاده میشن، ولی یه کار رو انجام نمیدن. Bulkhead منابع رو محدود و Failure Domainها رو از هم جدا میکنه. Circuit Breaker وقتی Failureها از یه حد مشخص بیشتر شدن، موقتاً جلوی ارسال Requestهای جدید به Dependency مشکلدار رو میگیره.
Bulkhead فقط برای Microserviceها نیست 🌐
هرچند Bulkhead Pattern رو زیاد توی معماری Microservice میبینیم، ولی ایدهی اصلیش به Microservice وابسته نیست. هرجایی که چند بخش مختلف سیستم برای استفاده از منابع مشترک با هم رقابت میکنن، میشه از Isolation استفاده کرد. Thread Pool، Connection Pool، Queue، Rate Limit و حتی منابع یک Process میتونن طوری جدا بشن که مشکل یک بخش، کل سیستم رو تحت تأثیر قرار نده.
جمعبندی ✍️
Bulkhead Pattern قرار نیست Failure رو حذف کنه. هدفش اینه که Failure رو محدود کنه. با جدا کردن منابع و تعیین ظرفیت مستقل برای بخشهای مختلف، اجازه نمیدیم یه Dependency خراب یا کند، تمام منابع سیستم رو مصرف کنه و باعث Cascading Failure بشه.
➖➖➖➖➖➖➖➖➖➖
توی یه سیستم توزیعشده، همیشه این احتمال وجود داره که یکی از سرویسها کند بشه یا از دسترس خارج بشه.
مشکل از جایی شروع میشه که این Failure فقط روی همون سرویس باقی نمونه و بتونه منابع سرویسهای دیگه رو هم مصرف کنه. مثلاً یه Dependency که Responseهاش خیلی دیر برمیگرده، میتونه Connection Pool یا Threadهای Application رو اشغال کنه و در نهایت باعث بشه Requestهای مربوط به بخشهای کاملاً متفاوت هم نتونن پردازش بشن.
اینجاست که Bulkhead Pattern وارد میشه.
Bulkhead Pattern چیه؟ 🧱
ایدهی اصلی Bulkhead خیلی سادهست: منابع سیستم رو به چند بخش مستقل تقسیم کنیم تا Failure یک بخش، منابع بخشهای دیگه رو مصرف نکنه.
اسم Bulkhead از دیوارههای جداکنندهی داخل کشتی گرفته شده. اگه یه قسمت از کشتی آسیب ببینه، این دیوارهها جلوی پخش شدن آب به کل کشتی رو میگیرن. توی نرم افزار هم دقیقاً دنبال همین اتفاقیم. به جای اینکه همهی Requestها و Dependencyها از یه Pool مشترک استفاده کنن، منابع رو به چند محدودهی جدا تقسیم میکنیم.
مشکل منابع اشتراکی🔗
فرض کنین Application ما برای چند Dependency مختلف از یه Connection Pool مشترک استفاده میکنه. اگه یکی از این Dependencyها کند بشه، Requestهای مربوط به اون سرویس مدت بیشتری Connection رو باز نگه میدارن. کمکم Pool پر میشه و Requestهای مربوط به Dependencyهای سالم هم دیگه Connection آزاد برای استفاده ندارن.
در نتیجه یه مشکل توی فقط یه Dependency میتونه باعث بشه بخشهایی که هیچ ارتباطی با اون ندارن هم از کار بیفتن. این همون چیزیه که بهش Cascading Failure میگیم.
Bulkhead چطور جلوی این اتفاق رو میگیره؟ ⚙️
Bulkhead منابع رو به چند Pool یا محدودهی مستقل تقسیم میکنه. مثلاً به جای اینکه همه ی Requestها از یه Thread Pool یا Connection Pool مشترک استفاده کنن، برای بخشهای مختلف ظرفیت جدا تعریف میکنیم.
حالا اگه Dependency A کند بشه و تمام ظرفیت اختصاص دادهشده به خودش رو مصرف کنه، فقط همون بخش تحت تأثیر قرار میگیره. Dependency B هنوز منابع خودش رو داره و میتونه به Requestهای مربوط به خودش جواب بده.
در واقع Bulkhead جلوی Failure رو نمیگیره؛ کاری میکنه که Failure نتونه آزادانه پخش بشه.
Resource Isolation 🔒
یکی از رایجترین شکلهای Bulkhead، جدا کردن منابع محاسباتیه. میتونیم برای بخشهای مختلف سیستم Thread Poolهای جدا داشته باشیم یا تعداد Connectionهایی که هر Dependency میتونه از Pool دریافت کنه رو محدود کنیم.با این کار، یه Dependency نمیتونه تمام منابع مشترک رو مصرف کنه.
این موضوع مخصوصاً توی سیستمهایی مهمه که چند Dependency با رفتار متفاوت دارن. مثلاً اگه یه سرویس معمولاً سریع جواب بده ولی یه سرویس دیگه ممکنه چند ثانیه یا حتی بیشتر طول بکشه، منطقی نیست هردوشون بدون هیچ محدودیتی از یه Resource Pool استفاده کنن.
Semaphore Bulkhead 🚦
یه روش دیگه برای پیادهسازی Bulkhead، محدود کردن تعداد عملیات همزمان با استفاده از Semaphore هست.
فرض کنین برای ارتباط با یه سرویس خارجی فقط اجازه بدیم حداکثر ۲۰ درخواست به صورت همزمان در حال اجرا باشن. وقتی ظرفیت پر شد، Requestهای جدید دیگه وارد اون بخش نمیشن یا طبق سیاست سیستم Fail Fast میشن.
در نتیجه حتی اگه اون Dependency کاملاً کند یا Unresponsive بشه، نمیتونه تعداد نامحدودی از Requestهای Application رو درگیر خودش کنه.
Bulkhead و Circuit Breaker 🔄
Bulkhead و Circuit Breaker معمولاً کنار هم استفاده میشن، ولی یه کار رو انجام نمیدن. Bulkhead منابع رو محدود و Failure Domainها رو از هم جدا میکنه. Circuit Breaker وقتی Failureها از یه حد مشخص بیشتر شدن، موقتاً جلوی ارسال Requestهای جدید به Dependency مشکلدار رو میگیره.
Bulkhead فقط برای Microserviceها نیست 🌐
هرچند Bulkhead Pattern رو زیاد توی معماری Microservice میبینیم، ولی ایدهی اصلیش به Microservice وابسته نیست. هرجایی که چند بخش مختلف سیستم برای استفاده از منابع مشترک با هم رقابت میکنن، میشه از Isolation استفاده کرد. Thread Pool، Connection Pool، Queue، Rate Limit و حتی منابع یک Process میتونن طوری جدا بشن که مشکل یک بخش، کل سیستم رو تحت تأثیر قرار نده.
جمعبندی ✍️
Bulkhead Pattern قرار نیست Failure رو حذف کنه. هدفش اینه که Failure رو محدود کنه. با جدا کردن منابع و تعیین ظرفیت مستقل برای بخشهای مختلف، اجازه نمیدیم یه Dependency خراب یا کند، تمام منابع سیستم رو مصرف کنه و باعث Cascading Failure بشه.
#️⃣ #system_design #backend #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
Graceful Degradation چطور سیستم رو هنگام Failure زنده نگه میداره؟ 🛡
توی یه سیستم واقعی، این احتمال همیشه وجود داره که یه Dependency از دسترس خارج بشه، یه سرویس کند بشه یا یه بخش از سیستم نتونه کار خودش رو انجام بده. چطور میشه با همچین موضوعی برخورد کرد؟
سؤال این نیست که چطور کاری کنیم هیچوقت Failure اتفاق نیفته، چون همچین چیزی عملاً غیرممکنه. سؤال مهمتر اینه که وقتی یه بخش خراب شد، چه کار میتونیم بکنیم تا خرابی بقیه ی بخش هارو هم درگیر نکنه؟
Graceful Degradation چیه؟ 🧩
Graceful Degradation یعنی وقتی بخشی از سیستم دچار مشکل میشه، به جای اینکه کل برنامه Fail بشه، سیستم به یه حالت محدودتر ولی همچنان قابل استفاده میره.
یعنی سیستم تشخیص میده یه قابلیت خاص دیگه قابل ارائه نیست و به جای اینکه Failure اون رو به کل Request یا کل Application منتقل کنه، قابلیتهای غیرضروری رو حذف یا ساده میکنه و بخشهای اصلی رو همچنان فعال نگه میداره.
هدف این نیست که کاربر هیچ تفاوتی متوجه نشه. هدف اینه که Failure یه بخش، باعث Failure کل سیستم نشه.
چرا این موضوع مهمه؟ ⚠️
فرض کنین یه Application چندین قابلیت مختلف داره و یکی از Dependencyهای اون برای یه قابلیت فرعی از دسترس خارج میشه.
اگه Application برای کامل کردن Response به اون Dependency وابسته باشه، ممکنه یه Failure کوچیک باعث بشه کل Request شکست بخوره. اما اگه سیستم طوری طراحی شده باشه که اون قابلیت رو موقتاً حذف کنه و Response اصلی رو بدون اون برگردونه، کاربر همچنان میتونه از بخشهای مهم Application استفاده کنه.
توی این حالت، به جای Total Failure با یه Reduced Functionality روبهرو میشیم.
Degradation چطور اتفاق میفته؟ 🔧
برای پیادهسازی Graceful Degradation باید مشخص کنیم کدوم قابلیتها برای عملکرد اصلی سیستم ضروری هستن و کدومها رو میشه موقتاً کنار گذاشت.
وقتی یه Dependency مشکل پیدا میکنه، Application میتونه برای قابلیت وابسته به اون یه Fallback داشته باشه. این Fallback ممکنه یه مقدار پیشفرض باشه، اطلاعات Cache شده باشه، یه Response سادهتر باشه یا حتی حذف موقت اون قابلیت.
نکتهی مهم اینه که سیستم نباید صرفاً بعد از Failure تصمیم بگیره چه کاری انجام بده. رفتار Degraded باید از قبل بخشی از طراحی سیستم باشه.
Graceful Degradation و Bulkhead چه فرقی دارن؟ 🧩
هر دو Pattern برای این استفاده میشن که Failure یه بخش باعث از کار افتادن کل سیستم نشه، ولی از دو زاویهی متفاوت به این مسئله نگاه میکنن. Bulkhead روی منابع تمرکز داره و با جدا کردن Resourceها بین بخشهای مختلف، جلوی این رو میگیره که یه بخش بتونه همه ی منابع مشترک رو مصرف کنه و Failure خودش رو به بقیه منتقل کنه.
اما Graceful Degradation روی رفتار خود سیستم تمرکز میکنه. وقتی یه قابلیت یا Dependency در دسترس نیست، سیستم به جای اینکه کل Request یا Application رو Fail کنه، میتونه با قابلیتهای محدودتر به کارش ادامه بده. پس Bulkhead میگه «چطور نذاریم Failure پخش بشه؟» و Graceful Degradation میگه «حالا که یه بخش از دسترس خارجه، چطور همچنان سرویس بدیم؟»
Degradation به معنی خراب کردن سیستم نیست 🎯
یه نکتهی مهم اینه که Degraded State نباید یه نسخهی تصادفی و ناقص از سیستم باشه. باید از قبل مشخص شده باشه که در شرایط مختلف Failure، چه قابلیتهایی حذف میشن و چه دادهای جایگزین میشه.
از طرف دیگه، Degradation نباید باعث Silent Failure بشه. حتی اگه سیستم بتونه با قابلیتهای محدودتر به کارش ادامه بده، خود Failure باید برای Monitoring و Logging قابل مشاهده باشه تا مشکل پنهان نمونه.
جمعبندی 🧠
توی یه سیستم مقاوم، نمیتونیم Failure رو حذف کنیم، ولی میتونیم اثرش رو کنترل کنیم.
Bulkhead منابع رو ایزوله میکنه تا Failure پخش نشه، Circuit Breaker ارتباط با Dependency مشکلدار رو موقتاً قطع میکنه، و Graceful Degradation باعث میشه سیستم با حذف یا محدود کردن بعضی قابلیتها همچنان به سرویس دادن ادامه بده.
یعنی موقع Failure، یکی Blast Radius رو محدود میکنه، یکی Dependency رو متوقف میکنه و یکی کمک میکنه خود سیستم همچنان قابل استفاده بمونه.
➖➖➖➖➖➖➖➖➖➖
توی یه سیستم واقعی، این احتمال همیشه وجود داره که یه Dependency از دسترس خارج بشه، یه سرویس کند بشه یا یه بخش از سیستم نتونه کار خودش رو انجام بده. چطور میشه با همچین موضوعی برخورد کرد؟
سؤال این نیست که چطور کاری کنیم هیچوقت Failure اتفاق نیفته، چون همچین چیزی عملاً غیرممکنه. سؤال مهمتر اینه که وقتی یه بخش خراب شد، چه کار میتونیم بکنیم تا خرابی بقیه ی بخش هارو هم درگیر نکنه؟
Graceful Degradation چیه؟ 🧩
Graceful Degradation یعنی وقتی بخشی از سیستم دچار مشکل میشه، به جای اینکه کل برنامه Fail بشه، سیستم به یه حالت محدودتر ولی همچنان قابل استفاده میره.
یعنی سیستم تشخیص میده یه قابلیت خاص دیگه قابل ارائه نیست و به جای اینکه Failure اون رو به کل Request یا کل Application منتقل کنه، قابلیتهای غیرضروری رو حذف یا ساده میکنه و بخشهای اصلی رو همچنان فعال نگه میداره.
هدف این نیست که کاربر هیچ تفاوتی متوجه نشه. هدف اینه که Failure یه بخش، باعث Failure کل سیستم نشه.
چرا این موضوع مهمه؟ ⚠️
فرض کنین یه Application چندین قابلیت مختلف داره و یکی از Dependencyهای اون برای یه قابلیت فرعی از دسترس خارج میشه.
اگه Application برای کامل کردن Response به اون Dependency وابسته باشه، ممکنه یه Failure کوچیک باعث بشه کل Request شکست بخوره. اما اگه سیستم طوری طراحی شده باشه که اون قابلیت رو موقتاً حذف کنه و Response اصلی رو بدون اون برگردونه، کاربر همچنان میتونه از بخشهای مهم Application استفاده کنه.
توی این حالت، به جای Total Failure با یه Reduced Functionality روبهرو میشیم.
Degradation چطور اتفاق میفته؟ 🔧
برای پیادهسازی Graceful Degradation باید مشخص کنیم کدوم قابلیتها برای عملکرد اصلی سیستم ضروری هستن و کدومها رو میشه موقتاً کنار گذاشت.
وقتی یه Dependency مشکل پیدا میکنه، Application میتونه برای قابلیت وابسته به اون یه Fallback داشته باشه. این Fallback ممکنه یه مقدار پیشفرض باشه، اطلاعات Cache شده باشه، یه Response سادهتر باشه یا حتی حذف موقت اون قابلیت.
نکتهی مهم اینه که سیستم نباید صرفاً بعد از Failure تصمیم بگیره چه کاری انجام بده. رفتار Degraded باید از قبل بخشی از طراحی سیستم باشه.
Graceful Degradation و Bulkhead چه فرقی دارن؟ 🧩
هر دو Pattern برای این استفاده میشن که Failure یه بخش باعث از کار افتادن کل سیستم نشه، ولی از دو زاویهی متفاوت به این مسئله نگاه میکنن. Bulkhead روی منابع تمرکز داره و با جدا کردن Resourceها بین بخشهای مختلف، جلوی این رو میگیره که یه بخش بتونه همه ی منابع مشترک رو مصرف کنه و Failure خودش رو به بقیه منتقل کنه.
اما Graceful Degradation روی رفتار خود سیستم تمرکز میکنه. وقتی یه قابلیت یا Dependency در دسترس نیست، سیستم به جای اینکه کل Request یا Application رو Fail کنه، میتونه با قابلیتهای محدودتر به کارش ادامه بده. پس Bulkhead میگه «چطور نذاریم Failure پخش بشه؟» و Graceful Degradation میگه «حالا که یه بخش از دسترس خارجه، چطور همچنان سرویس بدیم؟»
Degradation به معنی خراب کردن سیستم نیست 🎯
یه نکتهی مهم اینه که Degraded State نباید یه نسخهی تصادفی و ناقص از سیستم باشه. باید از قبل مشخص شده باشه که در شرایط مختلف Failure، چه قابلیتهایی حذف میشن و چه دادهای جایگزین میشه.
از طرف دیگه، Degradation نباید باعث Silent Failure بشه. حتی اگه سیستم بتونه با قابلیتهای محدودتر به کارش ادامه بده، خود Failure باید برای Monitoring و Logging قابل مشاهده باشه تا مشکل پنهان نمونه.
جمعبندی 🧠
توی یه سیستم مقاوم، نمیتونیم Failure رو حذف کنیم، ولی میتونیم اثرش رو کنترل کنیم.
Bulkhead منابع رو ایزوله میکنه تا Failure پخش نشه، Circuit Breaker ارتباط با Dependency مشکلدار رو موقتاً قطع میکنه، و Graceful Degradation باعث میشه سیستم با حذف یا محدود کردن بعضی قابلیتها همچنان به سرویس دادن ادامه بده.
یعنی موقع Failure، یکی Blast Radius رو محدود میکنه، یکی Dependency رو متوقف میکنه و یکی کمک میکنه خود سیستم همچنان قابل استفاده بمونه.
#️⃣ #system_design #backend #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
Stateless و Stateful چه فرقی دارن؟ 🤔
وقتی یه Client چندتا Request به یه Server میفرسته، Server باید چیزی از Requestهای قبلی رو به خاطر داشته باشه یا نه؟
همین سؤال ساده، ما رو به دو مدل مختلف برای طراحی سیستم میرسونه: Stateful و Stateless.
Stateful یعنی چی؟ 🧠
توی یه سیستم Stateful، سرور بین Requestهای مختلف، State مربوط به Client رو نگه میداره. مثلاً بعد از Login، سرور میتونه یه Session برای کاربر ایجاد کنه و اطلاعات مربوط به اون رو ذخیره کنه. Requestهای بعدی با استفاده از همون Session پردازش میشن و Server میتونه Context قبلی Client رو در اختیار داشته باشه.
Stateless یعنی چی؟ 📦
توی یه سیستم Stateless، سرور State مربوط به Client رو بین Requestها نگه نمیداره. هر Request باید اطلاعات لازم برای پردازش خودش رو داشته باشه. مثلاً یه API میتونه اطلاعات Authentication رو همراه هر Request دریافت کنه و بدون Session ذخیرهشده، هویت Client رو مشخص کنه.
تفاوت اصلی کجاست؟ ⚖️
تفاوت اصلی اینه که Context بین Requestها کجا نگهداری میشه.
توی Stateful، بخشی از این Context روی Server باقی میمونه. توی Stateless، کلاینت باید اطلاعات لازم رو با هر Request ارائه بده. این تفاوت وقتی چند Instance از Application داشته باشیم اهمیت بیشتری پیدا میکنه.
وقتی چند Server داریم چه اتفاقی میفته؟ 🖥
فرض کنین Application ما چند Instance داره و Requestهای Client بین اونها تقسیم میشن. توی Stateful، اگه State روی خود Instance نگه داشته شده باشه، Request بعدی ممکنه به Instance دیگهای برسه که State قبلی رو نداره. برای حل این مشکل میشه از Session Store مشترک یا Sticky Session استفاده کرد.
اما توی Stateless، چون هر Request مستقل پردازش میشه، مهم نیست Request به کدوم Instance برسه. این موضوع باعث میشه Load Balancing و Scale کردن Application سادهتر بشه.
Stateless یعنی بدون هیچ Stateای؟ 🤔
نه. Stateless بودن به این معنی نیست که Application هیچ Stateای نداره یا مثلاً Database استفاده نمیکنه.
مثلاً یه Stateless API همچنان میتونه اطلاعات User، Order و هر دادهی دیگهای رو توی Database ذخیره کنه. چیزی که نگه داشته نمیشه، State مربوط به Session و Context بین Requestهای Client هست.
جمعبندی 🧠
Stateful یعنی Server بین Requestها Context مربوط به Client رو نگه میداره.
Stateless یعنی هر Request اطلاعات لازم خودش رو داره و به State ذخیرهشده روی Server وابسته نیست.
پس انتخاب بین این دو بیشتر یه تصمیم معماریه که باید بر اساس نیاز سیستم، نحوهی مدیریت Session و مدل Scale کردن Application انجام بشه.
➖➖➖➖➖➖➖➖➖➖
وقتی یه Client چندتا Request به یه Server میفرسته، Server باید چیزی از Requestهای قبلی رو به خاطر داشته باشه یا نه؟
همین سؤال ساده، ما رو به دو مدل مختلف برای طراحی سیستم میرسونه: Stateful و Stateless.
Stateful یعنی چی؟ 🧠
توی یه سیستم Stateful، سرور بین Requestهای مختلف، State مربوط به Client رو نگه میداره. مثلاً بعد از Login، سرور میتونه یه Session برای کاربر ایجاد کنه و اطلاعات مربوط به اون رو ذخیره کنه. Requestهای بعدی با استفاده از همون Session پردازش میشن و Server میتونه Context قبلی Client رو در اختیار داشته باشه.
Stateless یعنی چی؟ 📦
توی یه سیستم Stateless، سرور State مربوط به Client رو بین Requestها نگه نمیداره. هر Request باید اطلاعات لازم برای پردازش خودش رو داشته باشه. مثلاً یه API میتونه اطلاعات Authentication رو همراه هر Request دریافت کنه و بدون Session ذخیرهشده، هویت Client رو مشخص کنه.
تفاوت اصلی کجاست؟ ⚖️
تفاوت اصلی اینه که Context بین Requestها کجا نگهداری میشه.
توی Stateful، بخشی از این Context روی Server باقی میمونه. توی Stateless، کلاینت باید اطلاعات لازم رو با هر Request ارائه بده. این تفاوت وقتی چند Instance از Application داشته باشیم اهمیت بیشتری پیدا میکنه.
وقتی چند Server داریم چه اتفاقی میفته؟ 🖥
فرض کنین Application ما چند Instance داره و Requestهای Client بین اونها تقسیم میشن. توی Stateful، اگه State روی خود Instance نگه داشته شده باشه، Request بعدی ممکنه به Instance دیگهای برسه که State قبلی رو نداره. برای حل این مشکل میشه از Session Store مشترک یا Sticky Session استفاده کرد.
اما توی Stateless، چون هر Request مستقل پردازش میشه، مهم نیست Request به کدوم Instance برسه. این موضوع باعث میشه Load Balancing و Scale کردن Application سادهتر بشه.
Stateless یعنی بدون هیچ Stateای؟ 🤔
نه. Stateless بودن به این معنی نیست که Application هیچ Stateای نداره یا مثلاً Database استفاده نمیکنه.
مثلاً یه Stateless API همچنان میتونه اطلاعات User، Order و هر دادهی دیگهای رو توی Database ذخیره کنه. چیزی که نگه داشته نمیشه، State مربوط به Session و Context بین Requestهای Client هست.
جمعبندی 🧠
Stateful یعنی Server بین Requestها Context مربوط به Client رو نگه میداره.
Stateless یعنی هر Request اطلاعات لازم خودش رو داره و به State ذخیرهشده روی Server وابسته نیست.
پس انتخاب بین این دو بیشتر یه تصمیم معماریه که باید بر اساس نیاز سیستم، نحوهی مدیریت Session و مدل Scale کردن Application انجام بشه.
#️⃣ #system_design #backend #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
روز برنامه نویس مبارک به همهی اونایی که یه جایی بین ارورهای عجیب، تحریم های عجیب تر و با اینترنتی که میدونن نمیشه بهش گفت "اینترنت"، هنوز دارن کد میزنن.
به اونایی که هر روز یه چیز جدید یاد میگیرن، هر هفته یه تکنولوژی جدید میبینن و هر روز یه بار هم به این فکر میکنن که اصلاً چرا وارد این حوزه شدن؛ مخصوصاً وقتی وضعیت بازار کار و بیکاری رو میبینن و با خودشون میگن «من دقیقاً دارم برای چی اینهمه چیز یاد میگیرم؟» :)
برنامه نویسی فقط نوشتن کد نیست؛ یه جور عادت کردنه، عادت کردن به اینکه هر مشکلی، هرچقدر هم مسخره، ساده یا پیچیده باشه، بشه نشست و با تیکه تیکه کردنش حلش کرد.
روز برنامه نویس مبارک.
به امید روزایی که حداقل دیباگ کردن کدمون سخت ترین چیزی باشه که تجربش میکنیم.
به اونایی که هر روز یه چیز جدید یاد میگیرن، هر هفته یه تکنولوژی جدید میبینن و هر روز یه بار هم به این فکر میکنن که اصلاً چرا وارد این حوزه شدن؛ مخصوصاً وقتی وضعیت بازار کار و بیکاری رو میبینن و با خودشون میگن «من دقیقاً دارم برای چی اینهمه چیز یاد میگیرم؟» :)
برنامه نویسی فقط نوشتن کد نیست؛ یه جور عادت کردنه، عادت کردن به اینکه هر مشکلی، هرچقدر هم مسخره، ساده یا پیچیده باشه، بشه نشست و با تیکه تیکه کردنش حلش کرد.
روز برنامه نویس مبارک.
به امید روزایی که حداقل دیباگ کردن کدمون سخت ترین چیزی باشه که تجربش میکنیم.
🌙 CHANNEL | GROUP
❤9
Database Crash Recovery چطور کار میکنه؟ 💥
فرض کنین Database وسط اجرای چندین Transaction یهو Crash کنه. بعضی تغییرات ممکنه کامل روی Storage نوشته شده باشن، بعضیها هنوز توی Memory باشن و بعضی Transactionها هم ممکنه وسط کار معلق مونده باشن.
حالا وقتی Database دوباره بالا میاد، یه سؤال مهم وجود داره: از کجا بفهمه کدوم تغییرات باید باقی بمونن و کدومها باید برگردونده بشن؟
اینجاست که Crash Recovery وارد میشه.
Crash چه مشکلی ایجاد میکنه؟ ⚠️
Database همیشه Data رو به شکل لحظهای و کامل روی Storage نمینویسه. برای Performance، بخشی از Data ممکنه یه مدتی روی Memory یا Cache باقی بمونه و حتی نوشتن روی Storage هم ممکنه توی چند مرحله انجام بشه.
پس اگه سیستم دقیقاً وسط این فرآیند Crash کنه، وضعیت Storage میتونه با چیزی که Transactionها انتظار داشتن متفاوت باشه. ممکنه یه Transaction کامل شده باشه ولی همهی تغییراتش هنوز روی Storage نوشته نشده باشن، یا یه Transaction که هنوز Commit نشده بخشی از تغییراتش رو نوشته باشه.
Recovery باید بعد از Restart این وضعیت ناقص رو پیدا کنه و Database رو به یه State معتبر برگردونه.
Database بعد از Restart چه کار میکنه؟ 🔄
وقتی Database دوباره بالا میاد، اطلاعات مربوط به تغییراتی که قبل از Crash اتفاق افتادن رو بررسی میکنه و وضعیت Transactionها رو از روی همون اطلاعات بازسازی میکنه.
مثلاً اگه مشخص باشه یه Transaction به مرحلهی Commit رسیده، تغییراتش باید باقی بمونن، حتی اگه همهی اون تغییرات هنوز روی Data File نوشته نشده باشن. برعکس، اگه Transaction هنوز Commit نشده باشه، تغییراتش نباید جزو وضعیت نهایی Database محسوب بشن و Recovery باید اونها رو کنار بذاره.
برای انجام این کار، Database تغییرات ثبتشده رو با وضعیت فعلی Data روی Storage تطبیق میده. تغییراتی که لازم باشه دوباره اعمال بشن Redo میشن و تغییراتی که مربوط به عملیات ناتمام باشن با Undo برگردونده میشن.
به همین دلیله که Commit فقط یه علامت ساده برای Application نیست؛ بخشی از اطلاعاتیه که Database برای حفظ Consistency بعد از Crash بهش نیاز داره.
Checkpoint چه کمکی میکنه؟ 📍
اگه Database مجبور باشه برای هر Crash تمام تاریخچهی عملیات گذشته رو از اول بررسی کنه، Recovery میتونه خیلی زمانبر بشه.
برای همین Databaseها معمولاً در بازههای مختلف Checkpoint ایجاد میکنن. Checkpoint یه نقطهی مشخص از وضعیت Database رو ثبت میکنه که Recovery میتونه از اون به عنوان نقطهی شروع استفاده کنه.
در نتیجه به جای اینکه Database تمام اتفاقات رو از اول عمر خودش رو بررسی کنه، میتونه روی محدودهی مربوط به آخرین Checkpoint و تغییرات بعد از اون تمرکز کنه.
Recovery فقط برای Crash نیست 🛠
Crash Recovery بیشتر برای Failureهایی مثل قطع ناگهانی برق، Crash شدن Process یا Restart غیرمنتظرهی سیستم مطرح میشه.
اما Database ممکنه با Failureهای دیگهای هم روبهرو بشه، مثل خراب شدن بخشی از Storage یا از بین رفتن کامل یک Node. اینجا دیگه Recovery معمولی کافی نیست و معمولاً باید از Backup، Replication یا روشهای دیگه برای برگردوندن Data استفاده بشه.
پس Crash Recovery بیشتر دربارهی برگردوندن Database به یه State معتبر بعد از یه توقف ناگهانیه، نه بازیابی کامل Data بعد از هر نوع فاجعه.
چرا Recovery باید قابل اعتماد باشه؟ 🧠
اینکه Database بعد از Crash صرفا Restart بشه و دوباره بالا بیاد کافی نیست؛ باید طوری بالا بیاد که قوانین مربوط به Transactionها و Consistency همچنان برقرار باشن.
به همین دلیل Crash Recovery یکی از بخشهای بنیادی Database Engineهاست و خیلی از قابلیتهایی که از یه Database انتظار داریم، مثل Durability و Atomicity، بدون یه مکانیزم Recovery قابل اعتماد عملاً معنی خودشون رو از دست میدن.
جمعبندی ✍️
Crash Recovery یعنی Database بعد از یه توقف ناگهانی، وضعیت خودش رو بررسی کنه و دوباره به یه State معتبر برگردونه.
با استفاده از اطلاعات ثبتشده، Checkpointها و مکانیزمهایی مثل Redo و Undo، تغییرات لازم حفظ میشن و تغییرات ناقص کنار گذاشته میشن.
در نهایت هدف سادهست: Crash نباید باعث بشه Database ندونه آخرین وضعیت معتبرش چی بوده.
➖➖➖➖➖➖➖➖➖➖
فرض کنین Database وسط اجرای چندین Transaction یهو Crash کنه. بعضی تغییرات ممکنه کامل روی Storage نوشته شده باشن، بعضیها هنوز توی Memory باشن و بعضی Transactionها هم ممکنه وسط کار معلق مونده باشن.
حالا وقتی Database دوباره بالا میاد، یه سؤال مهم وجود داره: از کجا بفهمه کدوم تغییرات باید باقی بمونن و کدومها باید برگردونده بشن؟
اینجاست که Crash Recovery وارد میشه.
Crash چه مشکلی ایجاد میکنه؟ ⚠️
Database همیشه Data رو به شکل لحظهای و کامل روی Storage نمینویسه. برای Performance، بخشی از Data ممکنه یه مدتی روی Memory یا Cache باقی بمونه و حتی نوشتن روی Storage هم ممکنه توی چند مرحله انجام بشه.
پس اگه سیستم دقیقاً وسط این فرآیند Crash کنه، وضعیت Storage میتونه با چیزی که Transactionها انتظار داشتن متفاوت باشه. ممکنه یه Transaction کامل شده باشه ولی همهی تغییراتش هنوز روی Storage نوشته نشده باشن، یا یه Transaction که هنوز Commit نشده بخشی از تغییراتش رو نوشته باشه.
Recovery باید بعد از Restart این وضعیت ناقص رو پیدا کنه و Database رو به یه State معتبر برگردونه.
Database بعد از Restart چه کار میکنه؟ 🔄
وقتی Database دوباره بالا میاد، اطلاعات مربوط به تغییراتی که قبل از Crash اتفاق افتادن رو بررسی میکنه و وضعیت Transactionها رو از روی همون اطلاعات بازسازی میکنه.
مثلاً اگه مشخص باشه یه Transaction به مرحلهی Commit رسیده، تغییراتش باید باقی بمونن، حتی اگه همهی اون تغییرات هنوز روی Data File نوشته نشده باشن. برعکس، اگه Transaction هنوز Commit نشده باشه، تغییراتش نباید جزو وضعیت نهایی Database محسوب بشن و Recovery باید اونها رو کنار بذاره.
برای انجام این کار، Database تغییرات ثبتشده رو با وضعیت فعلی Data روی Storage تطبیق میده. تغییراتی که لازم باشه دوباره اعمال بشن Redo میشن و تغییراتی که مربوط به عملیات ناتمام باشن با Undo برگردونده میشن.
به همین دلیله که Commit فقط یه علامت ساده برای Application نیست؛ بخشی از اطلاعاتیه که Database برای حفظ Consistency بعد از Crash بهش نیاز داره.
Checkpoint چه کمکی میکنه؟ 📍
اگه Database مجبور باشه برای هر Crash تمام تاریخچهی عملیات گذشته رو از اول بررسی کنه، Recovery میتونه خیلی زمانبر بشه.
برای همین Databaseها معمولاً در بازههای مختلف Checkpoint ایجاد میکنن. Checkpoint یه نقطهی مشخص از وضعیت Database رو ثبت میکنه که Recovery میتونه از اون به عنوان نقطهی شروع استفاده کنه.
در نتیجه به جای اینکه Database تمام اتفاقات رو از اول عمر خودش رو بررسی کنه، میتونه روی محدودهی مربوط به آخرین Checkpoint و تغییرات بعد از اون تمرکز کنه.
Recovery فقط برای Crash نیست 🛠
Crash Recovery بیشتر برای Failureهایی مثل قطع ناگهانی برق، Crash شدن Process یا Restart غیرمنتظرهی سیستم مطرح میشه.
اما Database ممکنه با Failureهای دیگهای هم روبهرو بشه، مثل خراب شدن بخشی از Storage یا از بین رفتن کامل یک Node. اینجا دیگه Recovery معمولی کافی نیست و معمولاً باید از Backup، Replication یا روشهای دیگه برای برگردوندن Data استفاده بشه.
پس Crash Recovery بیشتر دربارهی برگردوندن Database به یه State معتبر بعد از یه توقف ناگهانیه، نه بازیابی کامل Data بعد از هر نوع فاجعه.
چرا Recovery باید قابل اعتماد باشه؟ 🧠
اینکه Database بعد از Crash صرفا Restart بشه و دوباره بالا بیاد کافی نیست؛ باید طوری بالا بیاد که قوانین مربوط به Transactionها و Consistency همچنان برقرار باشن.
به همین دلیل Crash Recovery یکی از بخشهای بنیادی Database Engineهاست و خیلی از قابلیتهایی که از یه Database انتظار داریم، مثل Durability و Atomicity، بدون یه مکانیزم Recovery قابل اعتماد عملاً معنی خودشون رو از دست میدن.
جمعبندی ✍️
Crash Recovery یعنی Database بعد از یه توقف ناگهانی، وضعیت خودش رو بررسی کنه و دوباره به یه State معتبر برگردونه.
با استفاده از اطلاعات ثبتشده، Checkpointها و مکانیزمهایی مثل Redo و Undo، تغییرات لازم حفظ میشن و تغییرات ناقص کنار گذاشته میشن.
در نهایت هدف سادهست: Crash نباید باعث بشه Database ندونه آخرین وضعیت معتبرش چی بوده.
#️⃣ #system_design #backend #database
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
OverlayFS توی Docker چطور کار میکنه؟ 🐳
وقتی از یه Docker Image چندتا Container میسازیم، منطقی نیست Docker برای هر Container کل فایلهای Image رو دوباره کپی کنه. پس چطور چندتا Container میتونن از یه فایل سیستم مشترک استفاده کنن، ولی هرکدوم تغییرات خودشون رو داشته باشن؟
اینجاست که OverlayFS وارد میشه.
OverlayFS چیه؟ 🧩
OverlayFS یه Filesystem تو Linux هست که میتونه چندتا Directory رو روی هم قرار بده و از دید Process، اونها رو مثل یه Filesystem واحد نمایش بده.
به این Directoryها معمولاً Layer میگیم. در سادهترین حالت، یه لایه پایینی داریم که فقط Read-Only هست و یه لایه بالایی داریم که قابلیت Write داره. وقتی این دو لایه روی هم قرار میگیرن، Application یه Filesystem یکپارچه میبینه، بدون اینکه بدونه فایلها واقعاً توی کدوم لایه قرار دارن.
Docker چطور ازش استفاده میکنه؟ 🐳
Docker Image خودش از چندین لایه تشکیل میشه. هر دستوری که توی Dockerfile باعث ایجاد فایل میشه، یک لایه در نظر گرفته میشه.
مثلاً فرض کنین Image این لایه ها رو داشته باشه:
این لایه ها معمولاً Read-Only هستن و قابلیت اشتراک گذاری بین Containerهای مختلف رو بهمون میدن. وقتی Docker یه Container جدید میسازه، به جای کپی کردن تمام این فایلها، یه Writable Layer مخصوص همون Container بالای Image Layer ها قرار میده.
در نتیجه ساختار کلی چیزی شبیه این میشه:
OverlayFS این لایه هارو روی هم Mount میکنه و Container یه Filesystem واحد میبینه.
وقتی Container یه فایل رو تغییر میده چی میشه؟ ✏️
فرض کنین
در عوض، OverlayFS از مکانیزمی به اسم Copy-on-Write استفاده میکنه. یعنی وقتی Container بخواد یه فایل از لایه های پایینی رو تغییر بده، یه نسخه از اون فایل به Writable Layer منتقل میشه و تغییرات روی همون نسخه انجام میشه. در نهایت هم لایه ی پایین دستنخورده باقی میمونه.
اگه یه فایل جدید بسازیم چی؟ 📄
اگه Container فایلی بسازه که توی لایه های پایین وجود نداشته باشه، دیگه نیازی به Copy-on-Write نیست. فایل مستقیماً داخل Writable Layer همون Container ساخته میشه. پس Writable Layer فقط تغییرات مربوط به همون Container رو نگه میداره، نه یه کپی کامل از Image.
اگه یه فایل رو Delete کنیم چی؟ 🗑
اینجا یه نکته ی جالب وجود داره. اگه فایلی توی لایه های پایین وجود داشته باشه، Container نمیتونه واقعاً اون فایل رو از Layer Read-Only پاک کنه. پس OverlayFS از مکانیزمهایی مثل Whiteout استفاده میکنه تا مشخص بشه اون فایل نباید توی View نهایی وجود داشته باشه. یعنی فایل ممکنه هنوز توی Layer پایین وجود داشته باشه، ولی از دید Container انگار حذف شده.
حالا Container که پاک بشه، تغییراتش چی میشن؟ 💥
Writable Layer به خود Container وابسته هست. پس اگه Container رو حذف کنیم، تغییرات داخل Writable Layer هم معمولاً همراه اون از بین میرن.
برای همین برای اطلاعاتی که باید مستقل از عمر Container باقی بمونن، از Volume یا Bind Mount استفاده میکنیم. تو این حالت Data از Writable Layer خارج میشه و روی Storage مستقل نگه داشته میشه.
جمعبندی ✍️
OverlayFS به Docker اجازه میده چند لایه از فایلسیستم رو روی هم قرار بده و اونها رو به شکل یه Filesystem واحد در اختیار Container بذاره.
Image Layerها Read-Only و قابل اشتراکن، در حالی که هر کانتینر، Writable Layer مربوط به خودش خودش رو داره.
وقتی Container چیزی رو تغییر میده، Copy-on-Write باعث میشه تغییرات روی لایه ی خودش انجام بشن و Image اصلی دستنخورده باقی بمونه.
در نهایت هم چیزی که از داخل Container شبیه یه Filesystem معمولی به نظر میاد، پشت صحنه میتونه ترکیبی از چند لایه باشه که OverlayFS اونها رو برای ما یکپارچه کرده.
➖➖➖➖➖➖➖➖➖➖
وقتی از یه Docker Image چندتا Container میسازیم، منطقی نیست Docker برای هر Container کل فایلهای Image رو دوباره کپی کنه. پس چطور چندتا Container میتونن از یه فایل سیستم مشترک استفاده کنن، ولی هرکدوم تغییرات خودشون رو داشته باشن؟
اینجاست که OverlayFS وارد میشه.
OverlayFS چیه؟ 🧩
OverlayFS یه Filesystem تو Linux هست که میتونه چندتا Directory رو روی هم قرار بده و از دید Process، اونها رو مثل یه Filesystem واحد نمایش بده.
به این Directoryها معمولاً Layer میگیم. در سادهترین حالت، یه لایه پایینی داریم که فقط Read-Only هست و یه لایه بالایی داریم که قابلیت Write داره. وقتی این دو لایه روی هم قرار میگیرن، Application یه Filesystem یکپارچه میبینه، بدون اینکه بدونه فایلها واقعاً توی کدوم لایه قرار دارن.
Docker چطور ازش استفاده میکنه؟ 🐳
Docker Image خودش از چندین لایه تشکیل میشه. هر دستوری که توی Dockerfile باعث ایجاد فایل میشه، یک لایه در نظر گرفته میشه.
مثلاً فرض کنین Image این لایه ها رو داشته باشه:
Base OS → Dependencies → Applicationاین لایه ها معمولاً Read-Only هستن و قابلیت اشتراک گذاری بین Containerهای مختلف رو بهمون میدن. وقتی Docker یه Container جدید میسازه، به جای کپی کردن تمام این فایلها، یه Writable Layer مخصوص همون Container بالای Image Layer ها قرار میده.
در نتیجه ساختار کلی چیزی شبیه این میشه:
Container Writable Layer
Application Layer
Dependencies Layer
Base Layer
OverlayFS این لایه هارو روی هم Mount میکنه و Container یه Filesystem واحد میبینه.
وقتی Container یه فایل رو تغییر میده چی میشه؟ ✏️
فرض کنین
/app/config.json داخل Image وجود داره، و Container میخواد محتویاتش رو تغییر بده. این فایل تو یکی از لایه های پایینی Read-Only قرار داره. Docker نمیاد فایل اصلی Image رو تغییر بده، چون اون لایه ممکنه همزمان توسط چند Container دیگه هم استفاده بشه.در عوض، OverlayFS از مکانیزمی به اسم Copy-on-Write استفاده میکنه. یعنی وقتی Container بخواد یه فایل از لایه های پایینی رو تغییر بده، یه نسخه از اون فایل به Writable Layer منتقل میشه و تغییرات روی همون نسخه انجام میشه. در نهایت هم لایه ی پایین دستنخورده باقی میمونه.
اگه یه فایل جدید بسازیم چی؟ 📄
اگه Container فایلی بسازه که توی لایه های پایین وجود نداشته باشه، دیگه نیازی به Copy-on-Write نیست. فایل مستقیماً داخل Writable Layer همون Container ساخته میشه. پس Writable Layer فقط تغییرات مربوط به همون Container رو نگه میداره، نه یه کپی کامل از Image.
اگه یه فایل رو Delete کنیم چی؟ 🗑
اینجا یه نکته ی جالب وجود داره. اگه فایلی توی لایه های پایین وجود داشته باشه، Container نمیتونه واقعاً اون فایل رو از Layer Read-Only پاک کنه. پس OverlayFS از مکانیزمهایی مثل Whiteout استفاده میکنه تا مشخص بشه اون فایل نباید توی View نهایی وجود داشته باشه. یعنی فایل ممکنه هنوز توی Layer پایین وجود داشته باشه، ولی از دید Container انگار حذف شده.
حالا Container که پاک بشه، تغییراتش چی میشن؟ 💥
Writable Layer به خود Container وابسته هست. پس اگه Container رو حذف کنیم، تغییرات داخل Writable Layer هم معمولاً همراه اون از بین میرن.
برای همین برای اطلاعاتی که باید مستقل از عمر Container باقی بمونن، از Volume یا Bind Mount استفاده میکنیم. تو این حالت Data از Writable Layer خارج میشه و روی Storage مستقل نگه داشته میشه.
جمعبندی ✍️
OverlayFS به Docker اجازه میده چند لایه از فایلسیستم رو روی هم قرار بده و اونها رو به شکل یه Filesystem واحد در اختیار Container بذاره.
Image Layerها Read-Only و قابل اشتراکن، در حالی که هر کانتینر، Writable Layer مربوط به خودش خودش رو داره.
وقتی Container چیزی رو تغییر میده، Copy-on-Write باعث میشه تغییرات روی لایه ی خودش انجام بشن و Image اصلی دستنخورده باقی بمونه.
در نهایت هم چیزی که از داخل Container شبیه یه Filesystem معمولی به نظر میاد، پشت صحنه میتونه ترکیبی از چند لایه باشه که OverlayFS اونها رو برای ما یکپارچه کرده.
#️⃣ #docker #devops #linux
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
Deadlock واقعا چه شکلیه و چطوری مدیریت میشه؟ 🔄
فرض کنین دوتا رکورد داریم. تراکنش اول میاد رکورد اول رو Lock میکنه و برای Lock کردن رکورد دوم منتظر میمونه. تراکنش دوم هم رکورد دوم رو Lock میکنه و برای Lock کردن رکورد اول منتظر میمونه. حالا یه همچین شرایطی داریم: تراکنش اول منتظر آزاد شدن Lock تراکنش دومه، تراکنش دوم هم منتظر آزاد شدن Lock تراکنش اوله!
درواقع هر تراکنش یه منبعی رو در اختیار داره که تراکنش دیگه بهش نیاز داره. تراکنش اول منبعی رو گرفته که تراکنش دوم منتظرشه و تراکنش دوم هم منبعی رو گرفته که تراکنش اول منتظرشه. اینطوری یه Circular Wait یا چرخهی انتظار شکل میگیره که یکی از شرایط لازم برای ایجاد Deadlock هست.
چرا Database نمیزاره این چرخه ادامه پیدا کنه؟ 🧠
اگه Database فقط تراکنش ها رو منتظر نگه داره، Deadlock میتونه باعث بشه منابع برای همیشه درگیر بمونن. پس Database باید بتونه تشخیص بده که یه چرخه ی انتظار ایجاد شده.
برای این کار میتونه وضعیت Lockها و Transactionهایی که منتظر اون Lockها هستن رو بررسی کنه و ارتباط بین اونها رو به شکل یه Wait-For Graph در نظر بگیره.
مثلاً:
وقتی Graph دارای Cycle باشه، یعنی یه Deadlock وجود داره.
Database چطور Deadlock رو میشکنه؟ 💥
پیدا کردن Deadlock به تنهایی کافی نیست. Database باید یکی از Transactionها رو متوقف کنه تا Lockهاش آزاد بشن و Transaction دیگه بتونه ادامه بده.
به طور ساده، معمولاً یکی از Transactionها به عنوان قربانی انتخاب میشه و Rollback میشه. با Rollback شدن اون تراکنش، Lockهایی که گرفته آزاد میشن و Cycle از بین میره. در نهایت هم تراکنش باقیمونده میتونه ادامه پیدا کنه.
حالا قربانی چطوری انتخاب میشه؟
حالا Database باید یکی از تراکنشها رو بهعنوان قربانی انتخاب کنه و با Rollback کردنش، Lockهاش رو آزاد کنه تا چرخه شکسته بشه.
ولی این انتخاب لزوماً تصادفی نیست. دیتابیس بسته به الگوریتم و پیادهسازی خودش میتونه معیارهایی مثل مدت زمانی که تراکنش اجرا شده، مقدار کاری که انجام داده، تعداد Lockهایی که در اختیار داره و هزینهی Rollback کردنش رو در نظر بگیره.
درواقع هدف اینه که تراکنشی قربانی بشه که برگشت دادنش هزینهی کمتری برای سیستم داشته باشه.
Deadlock با Lock Contention فرق داره ⚠️
این دو تا خیلی راحت با هم اشتباه گرفته میشن. اگه چند Transaction منتظر یه Lock مشترک باشن، لزوماً Deadlock نداریم.مثلاً:
وقتی T1 کارش تموم بشه و Lock رو آزاد کنه، T2 ادامه میده.
اینجا فقط Lock Contention داریم. Deadlock زمانی اتفاق میفته که انتظارها به شکل یک Cycle به هم برگردن.
Deadlock چه بلایی سر تراکنش میاره؟
وقتی Database یکی از تراکنش ها رو به عنوان قربانی انتخاب میکنه، عملیات اون تراکنش Rollback میشه. یعنی تغییراتی که تا اون لحظه انجام داده، طبق قواعد Transaction از بین میرن.
Application باید این وضعیت رو مدیریت کنه و در صورت مناسب بودن شرایط، Transaction رو دوباره اجرا کنه. به همین دلیل Deadlock فقط یه مشکل Database نیست و میتونه روی منطق Application و نحوهی Retry کردن هم تأثیر بذاره.
جمعبندی ✍️
Deadlock وقتی اتفاق میفته که چند Transaction به شکلی منتظر Lockهای همدیگه باشن که یه Cycle ایجاد بشه. Database با بررسی رابطهی بین Transactionها و Lockها میتونه این Cycle رو تشخیص بده و با Rollback کردن یکی از Transactionها، Deadlock رو بشکنه؛ و مشکل اصلی Deadlock اینجوری نیست که: "یه تراکنش منتظر Lock مونده". مشکل اینه که هیچکدوم از تراکنش ها نمیتونن بدون آزاد شدن Lock دیگری ادامه بدن.
➖➖➖➖➖➖➖➖➖➖
فرض کنین دوتا رکورد داریم. تراکنش اول میاد رکورد اول رو Lock میکنه و برای Lock کردن رکورد دوم منتظر میمونه. تراکنش دوم هم رکورد دوم رو Lock میکنه و برای Lock کردن رکورد اول منتظر میمونه. حالا یه همچین شرایطی داریم: تراکنش اول منتظر آزاد شدن Lock تراکنش دومه، تراکنش دوم هم منتظر آزاد شدن Lock تراکنش اوله!
درواقع هر تراکنش یه منبعی رو در اختیار داره که تراکنش دیگه بهش نیاز داره. تراکنش اول منبعی رو گرفته که تراکنش دوم منتظرشه و تراکنش دوم هم منبعی رو گرفته که تراکنش اول منتظرشه. اینطوری یه Circular Wait یا چرخهی انتظار شکل میگیره که یکی از شرایط لازم برای ایجاد Deadlock هست.
چرا Database نمیزاره این چرخه ادامه پیدا کنه؟ 🧠
اگه Database فقط تراکنش ها رو منتظر نگه داره، Deadlock میتونه باعث بشه منابع برای همیشه درگیر بمونن. پس Database باید بتونه تشخیص بده که یه چرخه ی انتظار ایجاد شده.
برای این کار میتونه وضعیت Lockها و Transactionهایی که منتظر اون Lockها هستن رو بررسی کنه و ارتباط بین اونها رو به شکل یه Wait-For Graph در نظر بگیره.
مثلاً:
T1 → T2T2 → T1وقتی Graph دارای Cycle باشه، یعنی یه Deadlock وجود داره.
Database چطور Deadlock رو میشکنه؟ 💥
پیدا کردن Deadlock به تنهایی کافی نیست. Database باید یکی از Transactionها رو متوقف کنه تا Lockهاش آزاد بشن و Transaction دیگه بتونه ادامه بده.
به طور ساده، معمولاً یکی از Transactionها به عنوان قربانی انتخاب میشه و Rollback میشه. با Rollback شدن اون تراکنش، Lockهایی که گرفته آزاد میشن و Cycle از بین میره. در نهایت هم تراکنش باقیمونده میتونه ادامه پیدا کنه.
حالا قربانی چطوری انتخاب میشه؟
حالا Database باید یکی از تراکنشها رو بهعنوان قربانی انتخاب کنه و با Rollback کردنش، Lockهاش رو آزاد کنه تا چرخه شکسته بشه.
ولی این انتخاب لزوماً تصادفی نیست. دیتابیس بسته به الگوریتم و پیادهسازی خودش میتونه معیارهایی مثل مدت زمانی که تراکنش اجرا شده، مقدار کاری که انجام داده، تعداد Lockهایی که در اختیار داره و هزینهی Rollback کردنش رو در نظر بگیره.
درواقع هدف اینه که تراکنشی قربانی بشه که برگشت دادنش هزینهی کمتری برای سیستم داشته باشه.
Deadlock با Lock Contention فرق داره ⚠️
این دو تا خیلی راحت با هم اشتباه گرفته میشن. اگه چند Transaction منتظر یه Lock مشترک باشن، لزوماً Deadlock نداریم.مثلاً:
T1 → Lock AT2 → Wait for Aوقتی T1 کارش تموم بشه و Lock رو آزاد کنه، T2 ادامه میده.
اینجا فقط Lock Contention داریم. Deadlock زمانی اتفاق میفته که انتظارها به شکل یک Cycle به هم برگردن.
Deadlock چه بلایی سر تراکنش میاره؟
وقتی Database یکی از تراکنش ها رو به عنوان قربانی انتخاب میکنه، عملیات اون تراکنش Rollback میشه. یعنی تغییراتی که تا اون لحظه انجام داده، طبق قواعد Transaction از بین میرن.
Application باید این وضعیت رو مدیریت کنه و در صورت مناسب بودن شرایط، Transaction رو دوباره اجرا کنه. به همین دلیل Deadlock فقط یه مشکل Database نیست و میتونه روی منطق Application و نحوهی Retry کردن هم تأثیر بذاره.
جمعبندی ✍️
Deadlock وقتی اتفاق میفته که چند Transaction به شکلی منتظر Lockهای همدیگه باشن که یه Cycle ایجاد بشه. Database با بررسی رابطهی بین Transactionها و Lockها میتونه این Cycle رو تشخیص بده و با Rollback کردن یکی از Transactionها، Deadlock رو بشکنه؛ و مشکل اصلی Deadlock اینجوری نیست که: "یه تراکنش منتظر Lock مونده". مشکل اینه که هیچکدوم از تراکنش ها نمیتونن بدون آزاد شدن Lock دیگری ادامه بدن.
#️⃣ #backend #database
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
Query Optimizer چطور تصمیم میگیره؟ 🧠
وقتی یه Query مثل این مینویسیم:
ما فقط به Database میگیم چه دادهای میخوایم. نگفتیم برای پیدا کردن این داده باید دقیقاً چه کاری انجام بده.
Database میتونه کل جدول
Query Optimizer دقیقاً چیکار میکنه؟ ⚙️
Database قبل از اجرای Query، بررسی میکنه چه روشهایی برای اجرای اون وجود داره. برای یه Query ساده ممکنه چند Execution Plan مختلف وجود داشته باشه که همگی در نهایت نتیجهی یکسانی تولید کنن، ولی هزینهی اجرای اونها متفاوت باشه. مثلاً برای پیدا کردن Rowها، Database ممکنه بین Sequential Scan و Index Scan انتخاب کنه.
Sequential Scan یعنی Database دادههای جدول رو به شکل ترتیبی بررسی کنه و ببینه کدوم Row شرط Query رو داره. Index Scan مسیر متفاوتی داره. Database اول از ساختار Index برای پیدا کردن موقعیت داده استفاده میکنه و بعد سراغ Pageهایی میره که دادهی موردنظر داخلشون قرار داره.
هر دو روش میتونن جواب یکسانی بدن، ولی بسته به اندازهی جدول، تعداد Rowهای موردنیاز و شکل داده، یکی میتونه خیلی ارزون تر از اون یکی باشه. Optimizer وظیفه داره بین این انتخابها تصمیم بگیره.
Execution Plan چیه؟ 🔍
Execution Plan در واقع برنامهای هست که Database برای اجرای Query انتخاب کرده. مثلاً ممکنه تصمیم بگیره جدول رو با Index بخونه، بعد روی نتیجه یه Filter اعمال کنه. یا ممکنه تشخیص بده استفاده از Index ارزشش رو نداره و کل جدول رو Sequential Scan کنه.
وقتی Query پیچیدهتر میشه، Execution Plan هم پیچیدهتر میشه. ممکنه داخلش چند Scan، چند Join، Sort، Aggregate و عملیات دیگه وجود داشته باشه. نکتهی مهم اینه که SQL ای که ما مینویسیم، بیشتر توصیف نتیجهی موردنظره. Execution Plan اون چیزیه که مشخص میکنه Database برای رسیدن به اون نتیجه، واقعاً چه عملیاتی انجام بده.
خب حالا Optimizer از کجا میفهمه کدوم Plan بهتره؟ 🧮
اینجا Statistics اهمیت پیدا میکنن. Database معمولاً اطلاعات آماری مختلفی درباره ی دادههای جدول نگه میداره. مثلاً تخمین میزنه یه ستون چند مقدار متمایز داره، مقادیر چطور بین Rowها پخش شدن و یه شرط مشخص تقریباً چند Row رو برمیگردونه. فرض کنین یه جدول با ۱۰ میلیون Row داشته باشیم و Query این باشه:
اگه Optimizer تخمین بزنه این شرط فقط ۱۰ رکورد برمیگردونه، استفاده از Index میتونه منطقی باشه. ولی اگه تخمین بزنه چند میلیون Row باید برگرده، ممکنه Sequential Scan انتخاب مناسب تری بنظر برسه. چون وقتی بخش بزرگی از جدول موردنیازه، استفاده از Index میتونه باعث دسترسیهای بیشتری بشه و مزیت Index رو کم کنه.
Cost یعنی چی؟ 💰
Optimizer برای روشهای مختلف اجرای Query یه Cost تخمینی محاسبه میکنه. این Cost قرار نیست بگه Query دقیقاً چند میلیثانیه طول میکشه. بیشتر یه مدل داخلی برای مقایسهی Planهاست که چیزهایی مثل I/O و CPU و مقدار دادهای که باید پردازش بشه رو در نظر میگیره. فرض کنین Optimizer دو انتخاب داشته باشه:
Optimizer بر اساس مدل خودش Plan اول رو ترجیح میده. اما این عددها به معنی «۱۲۰ میلیثانیه در برابر ۸۰۰ میلیثانیه» نیستن. فقط نشون میدن Optimizer بر اساس اطلاعاتی که داره، اجرای Plan اول رو کمهزینهتر تخمین زده.
چرا Index همیشه بهتر نیست؟ 🤔
چون Index قرار نیست هر Queryای رو سریعتر کنه. برای مثال، توی حالتی که جدول ۱۰ میلیون Row داره ولی Query قراره ۸ میلیون Row رو برگردونه. Database ممکنه مجبور بشه از Index به تعداد زیادی Page مختلف برسه و بعد دادهی اصلی رو از Table بخونه.
گاهی Sequential Scan سادهتره. Database میتونه Pageهای جدول رو به صورت ترتیبی بخونه و بخش بزرگی از داده رو با I/O نسبتاً مناسب پردازش کنه. برای همین وجود Index به تنهایی باعث نمیشه Optimizer ازش استفاده کنه.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
وقتی یه Query مثل این مینویسیم:
SELECT * FROM orders WHERE user_id = 42;
ما فقط به Database میگیم چه دادهای میخوایم. نگفتیم برای پیدا کردن این داده باید دقیقاً چه کاری انجام بده.
Database میتونه کل جدول
orders رو از اول تا آخر بخونه و دنبال user_id = 42 بگرده. میتونه از یه Index استفاده کنه و مستقیم بره سراغ Rowهای مرتبط. اگه چندتا Table هم درگیر باشن، باید تصمیم بگیره کدوم Table رو اول بخونه و Join رو با چه روشی انجام بده. این دقیقا جاییه که Query Optimizer وارد میشه.Query Optimizer دقیقاً چیکار میکنه؟ ⚙️
Database قبل از اجرای Query، بررسی میکنه چه روشهایی برای اجرای اون وجود داره. برای یه Query ساده ممکنه چند Execution Plan مختلف وجود داشته باشه که همگی در نهایت نتیجهی یکسانی تولید کنن، ولی هزینهی اجرای اونها متفاوت باشه. مثلاً برای پیدا کردن Rowها، Database ممکنه بین Sequential Scan و Index Scan انتخاب کنه.
Sequential Scan یعنی Database دادههای جدول رو به شکل ترتیبی بررسی کنه و ببینه کدوم Row شرط Query رو داره. Index Scan مسیر متفاوتی داره. Database اول از ساختار Index برای پیدا کردن موقعیت داده استفاده میکنه و بعد سراغ Pageهایی میره که دادهی موردنظر داخلشون قرار داره.
هر دو روش میتونن جواب یکسانی بدن، ولی بسته به اندازهی جدول، تعداد Rowهای موردنیاز و شکل داده، یکی میتونه خیلی ارزون تر از اون یکی باشه. Optimizer وظیفه داره بین این انتخابها تصمیم بگیره.
Execution Plan چیه؟ 🔍
Execution Plan در واقع برنامهای هست که Database برای اجرای Query انتخاب کرده. مثلاً ممکنه تصمیم بگیره جدول رو با Index بخونه، بعد روی نتیجه یه Filter اعمال کنه. یا ممکنه تشخیص بده استفاده از Index ارزشش رو نداره و کل جدول رو Sequential Scan کنه.
وقتی Query پیچیدهتر میشه، Execution Plan هم پیچیدهتر میشه. ممکنه داخلش چند Scan، چند Join، Sort، Aggregate و عملیات دیگه وجود داشته باشه. نکتهی مهم اینه که SQL ای که ما مینویسیم، بیشتر توصیف نتیجهی موردنظره. Execution Plan اون چیزیه که مشخص میکنه Database برای رسیدن به اون نتیجه، واقعاً چه عملیاتی انجام بده.
خب حالا Optimizer از کجا میفهمه کدوم Plan بهتره؟ 🧮
اینجا Statistics اهمیت پیدا میکنن. Database معمولاً اطلاعات آماری مختلفی درباره ی دادههای جدول نگه میداره. مثلاً تخمین میزنه یه ستون چند مقدار متمایز داره، مقادیر چطور بین Rowها پخش شدن و یه شرط مشخص تقریباً چند Row رو برمیگردونه. فرض کنین یه جدول با ۱۰ میلیون Row داشته باشیم و Query این باشه:
SELECT * FROM orders WHERE status = pending;
اگه Optimizer تخمین بزنه این شرط فقط ۱۰ رکورد برمیگردونه، استفاده از Index میتونه منطقی باشه. ولی اگه تخمین بزنه چند میلیون Row باید برگرده، ممکنه Sequential Scan انتخاب مناسب تری بنظر برسه. چون وقتی بخش بزرگی از جدول موردنیازه، استفاده از Index میتونه باعث دسترسیهای بیشتری بشه و مزیت Index رو کم کنه.
Cost یعنی چی؟ 💰
Optimizer برای روشهای مختلف اجرای Query یه Cost تخمینی محاسبه میکنه. این Cost قرار نیست بگه Query دقیقاً چند میلیثانیه طول میکشه. بیشتر یه مدل داخلی برای مقایسهی Planهاست که چیزهایی مثل I/O و CPU و مقدار دادهای که باید پردازش بشه رو در نظر میگیره. فرض کنین Optimizer دو انتخاب داشته باشه:
Index Scan Cost: 120
Sequential Scan Cost: 800
Optimizer بر اساس مدل خودش Plan اول رو ترجیح میده. اما این عددها به معنی «۱۲۰ میلیثانیه در برابر ۸۰۰ میلیثانیه» نیستن. فقط نشون میدن Optimizer بر اساس اطلاعاتی که داره، اجرای Plan اول رو کمهزینهتر تخمین زده.
چرا Index همیشه بهتر نیست؟ 🤔
چون Index قرار نیست هر Queryای رو سریعتر کنه. برای مثال، توی حالتی که جدول ۱۰ میلیون Row داره ولی Query قراره ۸ میلیون Row رو برگردونه. Database ممکنه مجبور بشه از Index به تعداد زیادی Page مختلف برسه و بعد دادهی اصلی رو از Table بخونه.
گاهی Sequential Scan سادهتره. Database میتونه Pageهای جدول رو به صورت ترتیبی بخونه و بخش بزرگی از داده رو با I/O نسبتاً مناسب پردازش کنه. برای همین وجود Index به تنهایی باعث نمیشه Optimizer ازش استفاده کنه.
🧩Part 01
➖➖➖➖➖➖➖➖➖➖
#️⃣ #backend #database #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
Joinها تصمیمگیری رو سختتر میکنن 🔄
حالا یه Query مثل این رو بررسی کنیم:
اینجا دیگه Optimizer فقط دربارهی Scan تصمیم نمیگیره. باید مشخص کنه دادههای دو جدول رو چطوری به هم Join کنه.
یکی از انتخابهای مهم میتونه Nested Loop باشه. توی این روش Database از Rowهای یه سمت استفاده میکنه و برای هرکدوم دنبال Match در سمت دیگه میگرده. وقتی ورودی کوچیک باشه و دسترسی به سمت دوم مناسب باشه، این روش میتونه کارآمد باشه.
Hash Join هم یه روش دیگه هست. Database معمولاً از یکی از ورودیها یه ساختار Hash میسازه و بعد Rowهای ورودی دیگه رو با اون مقایسه میکنه. این روش مخصوصاً برای Equality Joinها میتونه مناسب باشه.
Merge Join هم از مرتب بودن دو ورودی استفاده میکنه و اونها رو مثل دو لیست مرتب شده با هم جلو میبره.
هیچکدوم ذاتاً «بهترین Join» نیستن. انتخاب مناسب به اندازهی داده، تخمین تعداد Rowها، مرتب بودن داده و عوامل دیگه بستگی داره.
اینجا حتی Cardinality هم مهمه 🎯
یکی از مهمترین چیزهایی که Optimizer باید تخمین بزنه، Cardinality یا تعداد Rowهاییه که هر بخش از Query تولید میکنه. مثلاً Database ممکنه قبل از اجرای Query تخمین بزنه که فقط 100 رکورد نتیجه برمیگرده، ولی درواقع Query یه نتیجه با یک میلیون رکورد داشته باشه.
اینجا یه مشکل جدی به وجود میاد. ممکنه Optimizer یه Plan رو انتخاب کرده باشه که برای ۱۰۰ رکورد عالیه، ولی برای یک میلیون رکورد انتخاب مناسبی نباشه.
به همین دلیله که Statistics نقش خیلی مهم تری توی Query Optimization دارن. Optimizer تصمیمش رو بر اساس تخمین میگیره، نه اینکه قبل از اجرا دقیقاً بدونه چه تعداد Row قراره تولید بشه.
چرا Optimizer همهی حالتها رو امتحان نمیکنه؟ 🧠
چون تعداد Planهای ممکن میتونه خیلی سریع زیاد بشه. یه Query ساده شاید فقط چند انتخاب داشته باشه، ولی وقتی تعداد Tableها، Joinها، Filterها، Indexها و عملیات مختلف زیاد بشه، تعداد ترکیبهای ممکن هم بهشدت افزایش پیدا میکنه.
اگه Database بخواد تمام Planهای ممکن رو بررسی کنه، ممکنه خود فرآیند Optimization تبدیل به یه گلوگاه بشه. برای همین Optimizer از الگوریتمها و روشهای مختلفی برای محدود کردن Search Space استفاده میکنه و سعی میکنه با هزینهی منطقی به یه Plan مناسب برسه.
بعضی وقتا Optimizer تصمیم بدی میگیره
بعضی وقتا ممکنه Optimizer تصمیم بدی بگیره. چون اطلاعاتش ممکنه دقیق نباشه. اگه Statistics قدیمی باشن، توزیع داده غیرمعمول باشه، Cardinality اشتباه تخمین زده بشه یا شرایط Query پیچیده باشه، Cost Model هم ممکنه به نتیجهای برسه که با رفتار واقعی Query فاصله داشته باشه.
EXPLAIN چه چیزی بهمون میگه؟ 🔬
با EXPLAIN، میتونیم ببینیم Database چه Execution Planای برای Query انتخاب کرده. مثلاً میتونیم ببینیم از Index استفاده شده یا Sequential Scan، چه Joinای انتخاب شده، Optimizer چند Row رو تخمین زده و Cost هر بخش چقدره.
با
این مقایسه خیلی مهمه. چون یکی از بهترین راهها برای فهمیدن اینکه چرا یه Query کند شده، اینه که ببینیم Database فکر میکرده چه اتفاقی میفته و واقعا چه اتفاقی افتاده.
جمعبندی ✍️
Query Optimizer یه بخشی از Database هست که سعی میکنه یه روش اجرای مناسب برای Query ما پیدا کنه.
برای این کار به چیزهایی مثل Statistics، Cardinality Estimates، Indexها، Join Strategy و Cost Model نگاه میکنه و از بین Execution Planهای مختلف بهترین رو انتخاب میکنه.
البته «بهترین» اینجا یعنی بهترین Plan بر اساس اطلاعات و Cost Modelای که Database در اختیار داره، نه لزوماً بهترین Planی که بعد از اجرای واقعی مشخص میشه.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
حالا یه Query مثل این رو بررسی کنیم:
SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
اینجا دیگه Optimizer فقط دربارهی Scan تصمیم نمیگیره. باید مشخص کنه دادههای دو جدول رو چطوری به هم Join کنه.
یکی از انتخابهای مهم میتونه Nested Loop باشه. توی این روش Database از Rowهای یه سمت استفاده میکنه و برای هرکدوم دنبال Match در سمت دیگه میگرده. وقتی ورودی کوچیک باشه و دسترسی به سمت دوم مناسب باشه، این روش میتونه کارآمد باشه.
Hash Join هم یه روش دیگه هست. Database معمولاً از یکی از ورودیها یه ساختار Hash میسازه و بعد Rowهای ورودی دیگه رو با اون مقایسه میکنه. این روش مخصوصاً برای Equality Joinها میتونه مناسب باشه.
Merge Join هم از مرتب بودن دو ورودی استفاده میکنه و اونها رو مثل دو لیست مرتب شده با هم جلو میبره.
هیچکدوم ذاتاً «بهترین Join» نیستن. انتخاب مناسب به اندازهی داده، تخمین تعداد Rowها، مرتب بودن داده و عوامل دیگه بستگی داره.
اینجا حتی Cardinality هم مهمه 🎯
یکی از مهمترین چیزهایی که Optimizer باید تخمین بزنه، Cardinality یا تعداد Rowهاییه که هر بخش از Query تولید میکنه. مثلاً Database ممکنه قبل از اجرای Query تخمین بزنه که فقط 100 رکورد نتیجه برمیگرده، ولی درواقع Query یه نتیجه با یک میلیون رکورد داشته باشه.
اینجا یه مشکل جدی به وجود میاد. ممکنه Optimizer یه Plan رو انتخاب کرده باشه که برای ۱۰۰ رکورد عالیه، ولی برای یک میلیون رکورد انتخاب مناسبی نباشه.
به همین دلیله که Statistics نقش خیلی مهم تری توی Query Optimization دارن. Optimizer تصمیمش رو بر اساس تخمین میگیره، نه اینکه قبل از اجرا دقیقاً بدونه چه تعداد Row قراره تولید بشه.
چرا Optimizer همهی حالتها رو امتحان نمیکنه؟ 🧠
چون تعداد Planهای ممکن میتونه خیلی سریع زیاد بشه. یه Query ساده شاید فقط چند انتخاب داشته باشه، ولی وقتی تعداد Tableها، Joinها، Filterها، Indexها و عملیات مختلف زیاد بشه، تعداد ترکیبهای ممکن هم بهشدت افزایش پیدا میکنه.
اگه Database بخواد تمام Planهای ممکن رو بررسی کنه، ممکنه خود فرآیند Optimization تبدیل به یه گلوگاه بشه. برای همین Optimizer از الگوریتمها و روشهای مختلفی برای محدود کردن Search Space استفاده میکنه و سعی میکنه با هزینهی منطقی به یه Plan مناسب برسه.
بعضی وقتا Optimizer تصمیم بدی میگیره
بعضی وقتا ممکنه Optimizer تصمیم بدی بگیره. چون اطلاعاتش ممکنه دقیق نباشه. اگه Statistics قدیمی باشن، توزیع داده غیرمعمول باشه، Cardinality اشتباه تخمین زده بشه یا شرایط Query پیچیده باشه، Cost Model هم ممکنه به نتیجهای برسه که با رفتار واقعی Query فاصله داشته باشه.
EXPLAIN چه چیزی بهمون میگه؟ 🔬
با EXPLAIN، میتونیم ببینیم Database چه Execution Planای برای Query انتخاب کرده. مثلاً میتونیم ببینیم از Index استفاده شده یا Sequential Scan، چه Joinای انتخاب شده، Optimizer چند Row رو تخمین زده و Cost هر بخش چقدره.
با
EXPLAIN ANALYZE علاوه بر Plan، اجرای واقعی Query هم بررسی میشه و میتونیم Estimated Rows رو با Actual Rows مقایسه کنیم.این مقایسه خیلی مهمه. چون یکی از بهترین راهها برای فهمیدن اینکه چرا یه Query کند شده، اینه که ببینیم Database فکر میکرده چه اتفاقی میفته و واقعا چه اتفاقی افتاده.
جمعبندی ✍️
Query Optimizer یه بخشی از Database هست که سعی میکنه یه روش اجرای مناسب برای Query ما پیدا کنه.
برای این کار به چیزهایی مثل Statistics، Cardinality Estimates، Indexها، Join Strategy و Cost Model نگاه میکنه و از بین Execution Planهای مختلف بهترین رو انتخاب میکنه.
البته «بهترین» اینجا یعنی بهترین Plan بر اساس اطلاعات و Cost Modelای که Database در اختیار داره، نه لزوماً بهترین Planی که بعد از اجرای واقعی مشخص میشه.
🧩Part 02 | Final
➖➖➖➖➖➖➖➖➖➖
#️⃣ #backend #database #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
Page های Database چطور کار میکنن؟ 💾
وقتی توی Database یه رکورد ذخیره میکنیم، ممکنه تصور کنیم Database همون رکورد رو به شکل یه تیکه ی مستقل روی Disk نگه میداره. ولی معمولاً اینطوری نیست.
Database دادهها رو تو واحدهایی به اسم Page سازماندهی میکنه. Page یه بلوک با اندازهی مشخصه که Database معمولاً دادهها رو در قالب اون از Storage میخونه و روی Storage مینویسه.
مثلا اگه یه Table چند میلیون رکورد داشته باشه. Database قرار نیست برای هر رکورد یه عملیات جداگانه Disk انجام بده. رکوردها داخل Pageها قرار میگیرن و Database معمولاً Page موردنیاز رو وارد Memory میکنه و بعد از داخل اون Page به رکورد موردنظر دسترسی پیدا میکنه.
Page دقیقاً چیه؟ 📄
Page رو میتونیم به عنوان یه واحد مشخص از Storage در نظر بگیریم که Database برای مدیریت داده ازش استفاده میکنه. اندازهی Page بسته به Database متفاوته. مثلاً ممکنه 8KB باشه، ولی این عدد یک قانون عمومی برای همهی Databaseها نیست.
داخل Page چه خبره؟ 🔍
Page فقط یه فضای خالی برای انباشته کردن Rowها نیست. Database باید علاوه بر خود داده، اطلاعاتی هم دربارهی اون دادهها نگه داره تا بتونه بفهمه چه Rowهایی داخل Page قرار دارن، موقعیتشون کجاست و چه مقدار فضای خالی باقی مونده.
ساختار دقیقش به Database و Storage Engine بستگی داره، ولی معمولاً بخشی از Page برای Metadata و اطلاعات مدیریتی استفاده میشه و بخش دیگه برای خود Tupleها یا Rowها. بعضی Databaseها هم از ساختاری مثل Slot یا Pointer استفاده میکنن تا موقعیت Rowها رو مدیریت کنن. این کار باعث میشه جابهجایی یا تغییر یک Row لزوماً به معنی جابهجا کردن تمام Rowهای دیگه داخل Page نباشه.
چرا رکوردها رو مستقیم پشت سر هم نمیچینیم؟ 🤔
چون Database باید بتونه رکوردها رو مدیریت کنه. ممکنه یه رکورد حذف بشه، یه رکورد جدید اضافه بشه یا اندازهی یه رکورد تغییر کنه. اگه ساختار Page فقط یه آرایهی ساده از رکوردهای پشت سر هم بود، تغییرات میتونستن باعث جابهجایی حجم زیادی از داده بشن.
برای همین خیلی از Databaseها از ساختارهایی استفاده میکنن که موقعیت رکوردها داخل Page قابل مدیریت باشه. یکی از الگوهای رایج، استفاده از Slot یا Pointer برای اشاره به محل رکوردهاست. توی این مدل، Database میتونه اطلاعات مربوط به محل Tupleها رو مدیریت کنه، بدون اینکه برای هر تغییر مجبور باشه کل Page رو از نو سازماندهی کنه.
وقتی یه رکورد رو میخونیم چه اتفاقی میوفته؟ 🧠
فرض کنین Query اینه:
اگه Database از Index استفاده کنه، Index میتونه به محدود کردن موقعیت داده کمک کنه. ولی در نهایت باید به Pageای برسه که رکورد موردنظر داخلشه. اگه اون Page از قبل داخل Memory باشه، Database میتونه بدون رفتن دوباره به Storage از همون نسخه استفاده کنه.
ولی اگه Page تو Memory نباشه، باید از Storage خونده بشه و وارد Buffer Pool بشه. بنابراین هزینهی دسترسی به یه رکورد فقط به خود رکورد مربوط نیست. اینکه رکورد داخل چه Pageای قرار داره و اون Page در Memory هست یا نه هم اهمیت داره.
Page Size چطور اهمیت پیدا میکنه؟ 📐
اگه Page خیلی کوچک باشه، برای دسترسی به حجم مشخصی از داده ممکنه مجبور بشیم Pageهای بیشتری بخونیم. از طرفی هم Page خیلی بزرگ میتونه باعث بشه برای پیدا کردن یه مقدار کوچیک، دادهی بیشتری وارد Memory بشه.
البته انتخاب Page Size فقط یه مسئلهی سادهی «کوچیک بهتره یا بزرگ؟» نیست. Database باید بین چیزهایی مثل I/O، Memory، Locality و نحوهی دسترسی به داده تعادل ایجاد کنه. برای همینه که Page یکی از مهمترین واحدهای فیزیکی توی معماری Database Storage محسوب میشه.
چرا Page Layout روی Performance تأثیر داره؟ ⚡️
چون Storage معمولاً با رکورد به عنوان واحد اصلی I/O کار نمیکنه. Database باید Pageها رو مدیریت کنه. اگه دادههایی که معمولاً با هم استفاده میشن داخل Pageهای نزدیک به هم قرار داشته باشن، Database میتونه با I/O کمتر دادهی بیشتری رو در اختیار Query قرار بده.
ولی اگه دادهها پراکنده باشن، دسترسی به اونها میتونه باعث Page Readهای بیشتری بشه. برای همین مفاهیمی مثل Data Locality، Page I/O، Buffer Pool و Cache Hit Ratio مستقیماً با نحوهی سازماندهی داده روی Pageها ارتباط دارن.
جمعبندی ✍️
Database دادهها رو مستقیماً به شکل مجموعهای از رکوردهای مستقل روی Storage مدیریت نمیکنه. رکوردها داخل Pageها قرار میگیرن و Page تبدیل میشه به یکی از واحدهای اصلی مدیریت داده و I/O. Database برای مدیریت Page باید چیزهایی مثل محل Tupleها، فضای آزاد و Metadata مربوط به دادهها رو هم کنترل کنه.
➖➖➖➖➖➖➖➖➖➖
وقتی توی Database یه رکورد ذخیره میکنیم، ممکنه تصور کنیم Database همون رکورد رو به شکل یه تیکه ی مستقل روی Disk نگه میداره. ولی معمولاً اینطوری نیست.
Database دادهها رو تو واحدهایی به اسم Page سازماندهی میکنه. Page یه بلوک با اندازهی مشخصه که Database معمولاً دادهها رو در قالب اون از Storage میخونه و روی Storage مینویسه.
مثلا اگه یه Table چند میلیون رکورد داشته باشه. Database قرار نیست برای هر رکورد یه عملیات جداگانه Disk انجام بده. رکوردها داخل Pageها قرار میگیرن و Database معمولاً Page موردنیاز رو وارد Memory میکنه و بعد از داخل اون Page به رکورد موردنظر دسترسی پیدا میکنه.
Page دقیقاً چیه؟ 📄
Page رو میتونیم به عنوان یه واحد مشخص از Storage در نظر بگیریم که Database برای مدیریت داده ازش استفاده میکنه. اندازهی Page بسته به Database متفاوته. مثلاً ممکنه 8KB باشه، ولی این عدد یک قانون عمومی برای همهی Databaseها نیست.
داخل Page چه خبره؟ 🔍
Page فقط یه فضای خالی برای انباشته کردن Rowها نیست. Database باید علاوه بر خود داده، اطلاعاتی هم دربارهی اون دادهها نگه داره تا بتونه بفهمه چه Rowهایی داخل Page قرار دارن، موقعیتشون کجاست و چه مقدار فضای خالی باقی مونده.
ساختار دقیقش به Database و Storage Engine بستگی داره، ولی معمولاً بخشی از Page برای Metadata و اطلاعات مدیریتی استفاده میشه و بخش دیگه برای خود Tupleها یا Rowها. بعضی Databaseها هم از ساختاری مثل Slot یا Pointer استفاده میکنن تا موقعیت Rowها رو مدیریت کنن. این کار باعث میشه جابهجایی یا تغییر یک Row لزوماً به معنی جابهجا کردن تمام Rowهای دیگه داخل Page نباشه.
چرا رکوردها رو مستقیم پشت سر هم نمیچینیم؟ 🤔
چون Database باید بتونه رکوردها رو مدیریت کنه. ممکنه یه رکورد حذف بشه، یه رکورد جدید اضافه بشه یا اندازهی یه رکورد تغییر کنه. اگه ساختار Page فقط یه آرایهی ساده از رکوردهای پشت سر هم بود، تغییرات میتونستن باعث جابهجایی حجم زیادی از داده بشن.
برای همین خیلی از Databaseها از ساختارهایی استفاده میکنن که موقعیت رکوردها داخل Page قابل مدیریت باشه. یکی از الگوهای رایج، استفاده از Slot یا Pointer برای اشاره به محل رکوردهاست. توی این مدل، Database میتونه اطلاعات مربوط به محل Tupleها رو مدیریت کنه، بدون اینکه برای هر تغییر مجبور باشه کل Page رو از نو سازماندهی کنه.
وقتی یه رکورد رو میخونیم چه اتفاقی میوفته؟ 🧠
فرض کنین Query اینه:
SELECT * FROM users WHERE id = 42;
اگه Database از Index استفاده کنه، Index میتونه به محدود کردن موقعیت داده کمک کنه. ولی در نهایت باید به Pageای برسه که رکورد موردنظر داخلشه. اگه اون Page از قبل داخل Memory باشه، Database میتونه بدون رفتن دوباره به Storage از همون نسخه استفاده کنه.
ولی اگه Page تو Memory نباشه، باید از Storage خونده بشه و وارد Buffer Pool بشه. بنابراین هزینهی دسترسی به یه رکورد فقط به خود رکورد مربوط نیست. اینکه رکورد داخل چه Pageای قرار داره و اون Page در Memory هست یا نه هم اهمیت داره.
Page Size چطور اهمیت پیدا میکنه؟ 📐
اگه Page خیلی کوچک باشه، برای دسترسی به حجم مشخصی از داده ممکنه مجبور بشیم Pageهای بیشتری بخونیم. از طرفی هم Page خیلی بزرگ میتونه باعث بشه برای پیدا کردن یه مقدار کوچیک، دادهی بیشتری وارد Memory بشه.
البته انتخاب Page Size فقط یه مسئلهی سادهی «کوچیک بهتره یا بزرگ؟» نیست. Database باید بین چیزهایی مثل I/O، Memory، Locality و نحوهی دسترسی به داده تعادل ایجاد کنه. برای همینه که Page یکی از مهمترین واحدهای فیزیکی توی معماری Database Storage محسوب میشه.
چرا Page Layout روی Performance تأثیر داره؟ ⚡️
چون Storage معمولاً با رکورد به عنوان واحد اصلی I/O کار نمیکنه. Database باید Pageها رو مدیریت کنه. اگه دادههایی که معمولاً با هم استفاده میشن داخل Pageهای نزدیک به هم قرار داشته باشن، Database میتونه با I/O کمتر دادهی بیشتری رو در اختیار Query قرار بده.
ولی اگه دادهها پراکنده باشن، دسترسی به اونها میتونه باعث Page Readهای بیشتری بشه. برای همین مفاهیمی مثل Data Locality، Page I/O، Buffer Pool و Cache Hit Ratio مستقیماً با نحوهی سازماندهی داده روی Pageها ارتباط دارن.
جمعبندی ✍️
Database دادهها رو مستقیماً به شکل مجموعهای از رکوردهای مستقل روی Storage مدیریت نمیکنه. رکوردها داخل Pageها قرار میگیرن و Page تبدیل میشه به یکی از واحدهای اصلی مدیریت داده و I/O. Database برای مدیریت Page باید چیزهایی مثل محل Tupleها، فضای آزاد و Metadata مربوط به دادهها رو هم کنترل کنه.
#️⃣ #backend #database #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
یه Process چطور Process جدیدی میسازه؟ 🐧
وقتی توی Linux یه برنامه رو اجرا میکنیم، معمولاً یه Process جدید داریم. ولی یه سؤال جالب این وسط وجود داره: وقتی یه Process بخواد یه Process دیگه بسازه، دقیقاً چه اتفاقی میوفته؟
مثلاً وقتی توی Terminal یه Command اجرا میکنیم، Shell چطوری باعث میشه اون برنامه اجرا بشه؟ اینجا دو تا System Call مهم به کار میان: fork و exec
اول یه Process جدید ساخته میشه 🧬
فرض کنین Shell میخواد یه برنامه مثل
البته این به معنی این نیست که Linux همون لحظه کل Memory رو واقعاً کپی میکنه. اینجا Copy-on-Write وارد ماجرا میشه. صفحات Memory تا وقتی یکی از Processها بخواد چیزی رو تغییر بده، میتونن بین Parent و Child مشترک بمونن. وقتی یکی بخواد صفحهای رو تغییر بده، Linux اون صفحه رو برای Process مربوطه جدا میکنه.
پس
حالا Child باید برنامهی خودش رو اجرا کنه ⚙️
تا اینجا Child هنوز عملاً همون برنامهی Parent رو داره. اگر والد Shell بوده، Child هم هنوز Shell هست. پس چطوری تبدیلش کنیم به
اینجاست که کار
این نکته خیلی مهمه:
ولی خب، تا اینجا که ما Parent Process رو
یه نکتهی عجیب دربارهی fork 👀
بعد از
جمعبندی ✍️
مدل کلاسیک اجرای Process توی Linux رو میشه با دو مفهوم اصلی فهمید: fork برای ساختن Child Process و exec برای جایگزین کردن Program Image داخل اون Process.
Process,
➖➖➖➖➖➖➖➖➖➖
وقتی توی Linux یه برنامه رو اجرا میکنیم، معمولاً یه Process جدید داریم. ولی یه سؤال جالب این وسط وجود داره: وقتی یه Process بخواد یه Process دیگه بسازه، دقیقاً چه اتفاقی میوفته؟
مثلاً وقتی توی Terminal یه Command اجرا میکنیم، Shell چطوری باعث میشه اون برنامه اجرا بشه؟ اینجا دو تا System Call مهم به کار میان: fork و exec
اول یه Process جدید ساخته میشه 🧬
فرض کنین Shell میخواد یه برنامه مثل
ls رو اجرا کنه. Shell مستقیماً تبدیل به ls نمیشه. اول fork رو صدا میزنه.
fork باعث میشه Linux یه Process جدید بسازه که به عنوان Child Process شناخته میشه. نکته ی جالب اینه که Child اول یه کپی از وضعیت Process والد داره. یعنی از دید برنامه، انگار یه نسخه ی جدید از همون Process ساخته شده که از همون نقطهی اجرای کد ادامه میده.البته این به معنی این نیست که Linux همون لحظه کل Memory رو واقعاً کپی میکنه. اینجا Copy-on-Write وارد ماجرا میشه. صفحات Memory تا وقتی یکی از Processها بخواد چیزی رو تغییر بده، میتونن بین Parent و Child مشترک بمونن. وقتی یکی بخواد صفحهای رو تغییر بده، Linux اون صفحه رو برای Process مربوطه جدا میکنه.
پس
fork نسبت به چیزی که از اسم «کپی کردن Process» ممکنه تصور کنیم، خیلی هوشمندانهتر عمل میکنه.حالا Child باید برنامهی خودش رو اجرا کنه ⚙️
تا اینجا Child هنوز عملاً همون برنامهی Parent رو داره. اگر والد Shell بوده، Child هم هنوز Shell هست. پس چطوری تبدیلش کنیم به
ls؟اینجاست که کار
exec شروع میشه. یکی از System Callهای خانوادهی exec، یعنی execve، به Linux میگه Program Image فعلی این Process رو با یه Executable جدید جایگزین کن.این نکته خیلی مهمه:
exec یه Process جدید نمیسازه. همون Child Process باقی میمونه، همون PID رو داره، ولی برنامهای که داخلش اجرا میشه عوض میشه. یعنی Child که تا این لحظه کد Shell رو اجرا میکرده، بعد از execve دیگه Program Image مربوط به ls رو داره.ولی خب، تا اینجا که ما Parent Process رو
fork کردیم همه چی بین Parent و Child یکسان بوده. پس Linux از کجا میفهمه که execve رو باید توی کدوم Process اجرا کنه؟یه نکتهی عجیب دربارهی fork 👀
بعد از
fork، هر دو Process از همون نقطهی کد ادامه میدن. پس Linux چطوری Parent رو از Child تشخیص میده؟ جواب شاید یکم غیرمنتظره باشه: Return Value خود fork. توی fork, Parent مقدار مربوط به PID فرزند رو برمیگردونه. ولی توی Child، مقدار برگشتی fork برابر 0 هست. به همین دلیل یه برنامه میتونه بفهمه الان داخل Parent هست یا Child و برای هرکدوم رفتار متفاوتی داشته باشه.جمعبندی ✍️
مدل کلاسیک اجرای Process توی Linux رو میشه با دو مفهوم اصلی فهمید: fork برای ساختن Child Process و exec برای جایگزین کردن Program Image داخل اون Process.
Process,
fork جدید میسازه و Child معمولاً با Copy-on-Write از وضعیت Parent شروع میکنه. Process, exec جدیدی نمیسازه، بلکه برنامه ی در حال اجرای همون Process رو عوض میکنه. همین ترکیب ساده، یکی از پایههای مدل Process توی سیستم های Unix/Linux هست.#️⃣ #linux #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
برنامه ها توی لینوکس چطوری به منابع دسترسی میگیرن؟ 🐧
وقتی توی Linux با یه فایل، Socket، Terminal یا حتی Pipe کار میکنیم، معمولاً تصور میکنیم برنامه مستقیماً با اون Resource در ارتباطه. ولی اینطوری نیست.
Applicationها معمولاً مستقیماً با خود Resource صحبت نمیکنن. وقتی یه Process چیزی رو باز میکنه، Kernel یه شناسهی عددی در اختیار Process میزاره که بهش File Descriptor گفته میشه.
از این به بعد برنامه به جای کار کردن مستقیم با فایل یا Socket، از همین عدد برای درخواست عملیات مختلف استفاده میکنه. ایدهی اصلی سادهست: برنامه فقط یه Reference به چیزی که Kernel مدیریت میکنه نگه میداره.
File Descriptor واقعا چیه؟ 🔍
File Descriptor در واقع یه عدد کوچیکه که داخل هر Process به یک Resource بازشده تو Kernel اشاره میکنه.
مثلاً وقتی یه فایل رو باز میکنیم، Kernel اطلاعات مختلفی درباره ی اون فایل نگه میداره؛ مثل موقعیت فعلی خوندن و نوشتن، Permissionها، نوع Resource و اطلاعات مربوط به Storage.
طبیعتا Process لازم نیست این جزئیات رو بدونه. فقط همون File Descriptor رو نگه میداره و هر وقت بخواد عملیاتی مثل Read یا Write انجام بده، همون شناسه رو به Kernel میده. Kernel از روی اون میفهمه باید با کدوم Resource کار کنه.
حالا چرا File Descriptor؟ 📁
اسمش یذره گمراه کننده هست، چون فقط برای فایلهای معمولی استفاده نمیشه. همونطور که میدونین توی Linux تقریباً همه چیز مثل فایل دیده میشه. Terminal، یه Socket شبکه، یه Pipe بین دو Process و حتی بعضی Deviceها میتونن File Descriptor داشته باشن.
یعنی سیستم عامل سعی میکنه برای انواع مختلف Resourceها یک Interface مشابه ارائه بده. برای همین یک برنامه میتونه تقریباً با یک مدل مشابه، هم از فایل بخونه، هم از Socket شبکه داده دریافت کنه.
stdin، stdout و stderr چی هستن؟ 🖥
هر Process معمولاً از شروع اجرا چند File Descriptor استاندارد داره. سه موردی که اسمشون رو همیشه میشنویم: stdin, stdout, stderr.
مثلاً وقتی یه برنامه از Terminal ورودی میگیره یا چیزی رو چاپ میکنه، در واقع داره با همین Descriptorهای استاندارد کار میکنه. نکته ی جالب اینه که برنامه لزوماً نمیدونه این ورودی از Keyboard اومده یا از یک فایل Redirect شده. چون از دید برنامه، فقط یک File Descriptor وجود داره که میشه ازش خوند.
Shell میتونه قبل از اجرای یه Process این Descriptorها رو تغییر بده و باعث بشه خروجی یک برنامه به جای Terminal داخل یک فایل ذخیره بشه. خود برنامه همچنان فکر میکنه داره روی خروجی استاندارد مینویسه.
File Descriptor و Process چه رابطهای دارن؟ 🧠
هر Process جدول مخصوص خودش برای File Descriptorها داره. یعنی یه Process میتونه یه فایل رو باز کنه و Descriptor شمارهی خاصی دریافت کنه، در حالی که یه Process دیگه ممکنه برای همون فایل Descriptor متفاوتی داشته باشه.
این Kernel هست که ارتباط بین Descriptor و Resource واقعی رو مدیریت میکنه. به همین دلیل Processها لازم نیست اطلاعات داخلی یکدیگر رو بدونن. هرکدوم فقط با Descriptorهای خودش کار میکنه.
File Descriptor Leak چیه؟ ⚠️
از اونجایی که File Descriptor یه Resource محدود توی Kernel محسوب میشه، باز نگه داشتن تعداد زیادی Descriptor میتونه مشکل ایجاد کنه. اگر یه برنامه فایل یا یه Socket رو باز کنه ولی هیچوقت نبنده، تعداد Descriptorهای مصرفشده به مرور زیاد میشه.
به این مشکل File Descriptor Leak گفته میشه. توی یه برنامه ی کوچک شاید فقط باعث مصرف منابع بشه، ولی تو یک Server که هزاران Connection رو مدیریت میکنه، میتونه باعث بشه برنامه دیگه نتونه فایل یا Connection جدید باز کنه.
جمع بندی ✍️
File Descriptor یک شناسه هست که Linux به Processها میده تا بتونن با Resourceهای مختلف مثل فایل، Socket، Pipe و Terminal کار کنن.
برنامه مستقیماً با خود Resource صحبت نمیکنه، بلکه از طریق Descriptor با Kernel و در نهایت Resource ارتباط برقرار میکنه. همین مفهوم ساده پایهی خیلی از قابلیتهای Linux مثل Redirect کردن خروجی، ارتباط بین Processها و مدیریت Connectionهای شبکه میشه.
➖➖➖➖➖➖➖➖➖➖
وقتی توی Linux با یه فایل، Socket، Terminal یا حتی Pipe کار میکنیم، معمولاً تصور میکنیم برنامه مستقیماً با اون Resource در ارتباطه. ولی اینطوری نیست.
Applicationها معمولاً مستقیماً با خود Resource صحبت نمیکنن. وقتی یه Process چیزی رو باز میکنه، Kernel یه شناسهی عددی در اختیار Process میزاره که بهش File Descriptor گفته میشه.
از این به بعد برنامه به جای کار کردن مستقیم با فایل یا Socket، از همین عدد برای درخواست عملیات مختلف استفاده میکنه. ایدهی اصلی سادهست: برنامه فقط یه Reference به چیزی که Kernel مدیریت میکنه نگه میداره.
File Descriptor واقعا چیه؟ 🔍
File Descriptor در واقع یه عدد کوچیکه که داخل هر Process به یک Resource بازشده تو Kernel اشاره میکنه.
مثلاً وقتی یه فایل رو باز میکنیم، Kernel اطلاعات مختلفی درباره ی اون فایل نگه میداره؛ مثل موقعیت فعلی خوندن و نوشتن، Permissionها، نوع Resource و اطلاعات مربوط به Storage.
طبیعتا Process لازم نیست این جزئیات رو بدونه. فقط همون File Descriptor رو نگه میداره و هر وقت بخواد عملیاتی مثل Read یا Write انجام بده، همون شناسه رو به Kernel میده. Kernel از روی اون میفهمه باید با کدوم Resource کار کنه.
حالا چرا File Descriptor؟ 📁
اسمش یذره گمراه کننده هست، چون فقط برای فایلهای معمولی استفاده نمیشه. همونطور که میدونین توی Linux تقریباً همه چیز مثل فایل دیده میشه. Terminal، یه Socket شبکه، یه Pipe بین دو Process و حتی بعضی Deviceها میتونن File Descriptor داشته باشن.
یعنی سیستم عامل سعی میکنه برای انواع مختلف Resourceها یک Interface مشابه ارائه بده. برای همین یک برنامه میتونه تقریباً با یک مدل مشابه، هم از فایل بخونه، هم از Socket شبکه داده دریافت کنه.
stdin، stdout و stderr چی هستن؟ 🖥
هر Process معمولاً از شروع اجرا چند File Descriptor استاندارد داره. سه موردی که اسمشون رو همیشه میشنویم: stdin, stdout, stderr.
مثلاً وقتی یه برنامه از Terminal ورودی میگیره یا چیزی رو چاپ میکنه، در واقع داره با همین Descriptorهای استاندارد کار میکنه. نکته ی جالب اینه که برنامه لزوماً نمیدونه این ورودی از Keyboard اومده یا از یک فایل Redirect شده. چون از دید برنامه، فقط یک File Descriptor وجود داره که میشه ازش خوند.
Shell میتونه قبل از اجرای یه Process این Descriptorها رو تغییر بده و باعث بشه خروجی یک برنامه به جای Terminal داخل یک فایل ذخیره بشه. خود برنامه همچنان فکر میکنه داره روی خروجی استاندارد مینویسه.
File Descriptor و Process چه رابطهای دارن؟ 🧠
هر Process جدول مخصوص خودش برای File Descriptorها داره. یعنی یه Process میتونه یه فایل رو باز کنه و Descriptor شمارهی خاصی دریافت کنه، در حالی که یه Process دیگه ممکنه برای همون فایل Descriptor متفاوتی داشته باشه.
این Kernel هست که ارتباط بین Descriptor و Resource واقعی رو مدیریت میکنه. به همین دلیل Processها لازم نیست اطلاعات داخلی یکدیگر رو بدونن. هرکدوم فقط با Descriptorهای خودش کار میکنه.
File Descriptor Leak چیه؟ ⚠️
از اونجایی که File Descriptor یه Resource محدود توی Kernel محسوب میشه، باز نگه داشتن تعداد زیادی Descriptor میتونه مشکل ایجاد کنه. اگر یه برنامه فایل یا یه Socket رو باز کنه ولی هیچوقت نبنده، تعداد Descriptorهای مصرفشده به مرور زیاد میشه.
به این مشکل File Descriptor Leak گفته میشه. توی یه برنامه ی کوچک شاید فقط باعث مصرف منابع بشه، ولی تو یک Server که هزاران Connection رو مدیریت میکنه، میتونه باعث بشه برنامه دیگه نتونه فایل یا Connection جدید باز کنه.
جمع بندی ✍️
File Descriptor یک شناسه هست که Linux به Processها میده تا بتونن با Resourceهای مختلف مثل فایل، Socket، Pipe و Terminal کار کنن.
برنامه مستقیماً با خود Resource صحبت نمیکنه، بلکه از طریق Descriptor با Kernel و در نهایت Resource ارتباط برقرار میکنه. همین مفهوم ساده پایهی خیلی از قابلیتهای Linux مثل Redirect کردن خروجی، ارتباط بین Processها و مدیریت Connectionهای شبکه میشه.
#️⃣ #linux #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
شبکه ی Docker چطور کار میکنه؟ 🐳🌐
وقتی یه Container اجرا میکنیم، معمولاً انتظار داریم مثل یه Process معمولی بتونه با بقیه سیستم ارتباط داشته باشه، بتونه به اینترنت وصل بشه، با Containerهای دیگه صحبت کنه و حتی از بیرون درخواست دریافت کنه. ولی یه مشکل اساسی وجود داره: Containerها مثل Processهای عادی روی همون Network سیستم اجرا نمیشن.
چرا اصلاً شبکه ی یه Container به مشکل میخوره؟ 🤔
روی یه سیستم معمولی، همهی Processها از Network Stack مشترک سیستم استفاده میکنن. اگه یه برنامه بخواد به اینترنت وصل بشه یا روی یه Port گوش بده، مستقیماً با Interfaceهای شبکهی سیستم در ارتباطه. ولی توی Container این قضیه فرق میکنه.
وقتی Docker یه Network Namespace جدا میسازه، Container دیگه به شکل مستقیم Interfaceهای سیستم اصلی رو نمیبینه. از دید Container، انگار یه سیستم شبکه ی جدا وجود داره. این ایزوله شدن باعث میشه چند Container بتونن حتی Port های مشابه داشته باشن، بدون اینکه با هم تداخل ایجاد کنن.
حالا اینجا یه مشکلی هست. اگه Container از سیستم اصلی جداست، چطور با بیرون ارتباط برقرار میکنه؟
Docker چطور مشکل ارتباط شبکه رو حل میکنه؟ 🔗
Docker برای حل این مشکل از یه لایهی شبکه بین Host و Container استفاده میکنه. در حالت رایج، Docker یه Interface مجازی برای Container ایجاد میکنه و اون رو به یه Bridge Network روی Host متصل میکنه.
حالا Container از دید خودش یه کارت شبکه داره. Host هم یه Interface مربوط به همین شبکه داره. Docker با استفاده از این ارتباط مجازی باعث میشه Packetها بین Container و سیستم اصلی جا به جا بشن. یعنی Container همچنان ایزوله باقی میمونه، ولی یه مسیر کنترل شده برای ارتباط با دنیای بیرون داره.
ترافیک Container چطور به اینترنت میرسه؟ 🌍
معمولاً Container یه IP داخلی تو شبکه ی Docker دریافت میکنه. طبیعتا این IP برای اینترنت قابل مشاهده نیست. وقتی Container میخواد به بیرون درخواست بفرسته، Docker از یه مکانیزمی مثل NAT استفاده میکنه.
یعنی قبل از خروج Packet از سیستم، آدرس داخلی Container به IP سیستم اصلی ترجمه میشه. از دید اینترنت، درخواست انگار از خود Host ارسال شده. وقتی جواب برمیگرده، سیستم با استفاده از اطلاعات NAT متوجه میشه این پاسخ باید به کدوم Container برگرده. به همین دلیل یه Container میتونه بدون داشتن یه IP عمومی مستقل، به اینترنت دسترسی داشته باشه.
وقتی یه Port رو Bind میکنیم چه اتفاقی میوفته؟ 🔍
تصور رایج اینه که Docker فقط ی عدد Port رو "باز" میکنه. ولی پشت صحنه، اتفاق اصلی اینه که Docker یه مسیر عبور برای Traffic ایجاد میکنه. درخواست اول به Network Interface سیستم اصلی میرسه. بعد قوانین شبکهای که Docker ایجاد کرده بررسی میشن. اگه Port مربوطه، به یه Container متصل شده باشه، Packet به سمت Network Namespace اون Container هدایت میشه.
در نهایت سرویس داخل Container، همون Port داخلی خودش رو میبینه و درخواست رو پردازش میکنه. خود برنامه داخل Container معمولاً اصلاً نمیدونه این درخواست مستقیم اومده یا از طریق Port Mapping وارد شده.
حالا ارتباط بین Containerها چطور انجام میشه؟ 🐳
Containerها هم میتونن با هم ارتباط داشته باشن. Docker برای این کار Networkهای داخلی ایجاد میکنه که Containerهای عضو یه Network بتونن همدیگه رو پیدا کنن. توی این حالت معمولاً به جای استفاده از IP ثابت، Containerها با اسم سرویس یا Container Name با هم ارتباط برقرار میکنن.
Docker یه لایهی Service Discovery ساده فراهم میکنه تا سرویسها بتونن مقصد موردنظر خودشون رو پیدا کنن. مثلاً یه Container که برنامه ی Backend توش اجرا میشه، میتونه به جای دونستن IP دیتابیس، با اسم سرویس Database بهش وصل بشه.
جمعبندی 🎯
Containerها به دلیل استفاده از Network Stack, Network Namespace جداگونه خودشون رو دارن و همین باعث ایزوله شدنشون میشه. Docker با استفاده از Bridge Network، Virtual Interface و NAT مسیر ارتباطی بین Container، Host و اینترنت رو ایجاد میکنه.
برای ارتباط ورودی هم Port Binding یه مسیر کنترل شده ایجاد میکنه تا Traffic از Host به داخل Container منتقل بشه.
➖➖➖➖➖➖➖➖➖➖
وقتی یه Container اجرا میکنیم، معمولاً انتظار داریم مثل یه Process معمولی بتونه با بقیه سیستم ارتباط داشته باشه، بتونه به اینترنت وصل بشه، با Containerهای دیگه صحبت کنه و حتی از بیرون درخواست دریافت کنه. ولی یه مشکل اساسی وجود داره: Containerها مثل Processهای عادی روی همون Network سیستم اجرا نمیشن.
چرا اصلاً شبکه ی یه Container به مشکل میخوره؟ 🤔
روی یه سیستم معمولی، همهی Processها از Network Stack مشترک سیستم استفاده میکنن. اگه یه برنامه بخواد به اینترنت وصل بشه یا روی یه Port گوش بده، مستقیماً با Interfaceهای شبکهی سیستم در ارتباطه. ولی توی Container این قضیه فرق میکنه.
وقتی Docker یه Network Namespace جدا میسازه، Container دیگه به شکل مستقیم Interfaceهای سیستم اصلی رو نمیبینه. از دید Container، انگار یه سیستم شبکه ی جدا وجود داره. این ایزوله شدن باعث میشه چند Container بتونن حتی Port های مشابه داشته باشن، بدون اینکه با هم تداخل ایجاد کنن.
حالا اینجا یه مشکلی هست. اگه Container از سیستم اصلی جداست، چطور با بیرون ارتباط برقرار میکنه؟
Docker چطور مشکل ارتباط شبکه رو حل میکنه؟ 🔗
Docker برای حل این مشکل از یه لایهی شبکه بین Host و Container استفاده میکنه. در حالت رایج، Docker یه Interface مجازی برای Container ایجاد میکنه و اون رو به یه Bridge Network روی Host متصل میکنه.
حالا Container از دید خودش یه کارت شبکه داره. Host هم یه Interface مربوط به همین شبکه داره. Docker با استفاده از این ارتباط مجازی باعث میشه Packetها بین Container و سیستم اصلی جا به جا بشن. یعنی Container همچنان ایزوله باقی میمونه، ولی یه مسیر کنترل شده برای ارتباط با دنیای بیرون داره.
ترافیک Container چطور به اینترنت میرسه؟ 🌍
معمولاً Container یه IP داخلی تو شبکه ی Docker دریافت میکنه. طبیعتا این IP برای اینترنت قابل مشاهده نیست. وقتی Container میخواد به بیرون درخواست بفرسته، Docker از یه مکانیزمی مثل NAT استفاده میکنه.
یعنی قبل از خروج Packet از سیستم، آدرس داخلی Container به IP سیستم اصلی ترجمه میشه. از دید اینترنت، درخواست انگار از خود Host ارسال شده. وقتی جواب برمیگرده، سیستم با استفاده از اطلاعات NAT متوجه میشه این پاسخ باید به کدوم Container برگرده. به همین دلیل یه Container میتونه بدون داشتن یه IP عمومی مستقل، به اینترنت دسترسی داشته باشه.
وقتی یه Port رو Bind میکنیم چه اتفاقی میوفته؟ 🔍
تصور رایج اینه که Docker فقط ی عدد Port رو "باز" میکنه. ولی پشت صحنه، اتفاق اصلی اینه که Docker یه مسیر عبور برای Traffic ایجاد میکنه. درخواست اول به Network Interface سیستم اصلی میرسه. بعد قوانین شبکهای که Docker ایجاد کرده بررسی میشن. اگه Port مربوطه، به یه Container متصل شده باشه، Packet به سمت Network Namespace اون Container هدایت میشه.
در نهایت سرویس داخل Container، همون Port داخلی خودش رو میبینه و درخواست رو پردازش میکنه. خود برنامه داخل Container معمولاً اصلاً نمیدونه این درخواست مستقیم اومده یا از طریق Port Mapping وارد شده.
حالا ارتباط بین Containerها چطور انجام میشه؟ 🐳
Containerها هم میتونن با هم ارتباط داشته باشن. Docker برای این کار Networkهای داخلی ایجاد میکنه که Containerهای عضو یه Network بتونن همدیگه رو پیدا کنن. توی این حالت معمولاً به جای استفاده از IP ثابت، Containerها با اسم سرویس یا Container Name با هم ارتباط برقرار میکنن.
Docker یه لایهی Service Discovery ساده فراهم میکنه تا سرویسها بتونن مقصد موردنظر خودشون رو پیدا کنن. مثلاً یه Container که برنامه ی Backend توش اجرا میشه، میتونه به جای دونستن IP دیتابیس، با اسم سرویس Database بهش وصل بشه.
جمعبندی 🎯
Containerها به دلیل استفاده از Network Stack, Network Namespace جداگونه خودشون رو دارن و همین باعث ایزوله شدنشون میشه. Docker با استفاده از Bridge Network، Virtual Interface و NAT مسیر ارتباطی بین Container، Host و اینترنت رو ایجاد میکنه.
برای ارتباط ورودی هم Port Binding یه مسیر کنترل شده ایجاد میکنه تا Traffic از Host به داخل Container منتقل بشه.
#️⃣ #docker #devops #linux
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
Database Lock چطور کار میکنه؟ 🔒
وقتی چندتا Transaction همزمان روی یه داده کار میکنن، Database باید یه جوری مشخص کنه کدوم Transaction اجازه داره داده رو تغییر بده و کدوم یکی باید صبر کنه. اینجا از Lock استفاده میشه.
Lock در اصل یه مکانیزم برای کنترل دسترسی همزمان به Resourceهاست. Database با استفاده از Lockها میتونه جلوی بعضی از تغییرات ناسازگار و Race Conditionهای مربوط به دسترسی همزمان رو بگیره. اما Lock فقط به معنی «قفل کردن یه دونه رکورد» نیست. Database میتونه روی Resourceهای مختلف Lock بگیره و همین موضوع باعث میشه رفتار Lockها کمی پیچیده تر از چیزی باشه که توی نگاه اول بنظر میاد.
چرا اصلا همچین چیزی وجود داره؟ 🤔
فرض کنین دو Transaction تقریباً همزمان بخوان مقدار موجودی یه حسابی رو تغییر بدن. اگه هیچ مکانیزم کنترلی ای وجود نداشته باشه، هر دو Transaction ممکنه داده رو بخونن و بر اساس همون مقدار قبلی تصمیم بگیرن. بعد هرکدوم تغییر خودش رو اعمال کنه و نتیجهی نهایی با چیزی که انتظار داشتیم فرق داشته باشه.
Database برای جلوگیری از اینطور Race Conditionهایی باید بتونه دسترسی همزمان Transactionها به Resourceهای مشترک رو کنترل کنه. Lock یکی از ابزارهای اصلی برای همین کاره. Transaction میتونه برای مدتی روی یک Resource لاک بگیره تا Transactionهای دیگه نتونن عملیات ناسازگار با اون Lock رو همزمان انجام بدن.
انواع Lock 🔐
وقتی درباره Lock حرف میزنیم، فقط با یه نوع Lock طرف نیستیم. Databaseها میتونن Lockها رو توی سطوح مختلفی اعمال کنن و همچنین برای نوع دسترسیهای مختلف، Lockهای متفاوتی داشته باشن.
از نظر سطح، دو مورد مهم Row-Level Lock و Table-Level Lock هستن. از نظر نوع دسترسی هم معمولاً مفاهیمی مثل Shared Lock و Exclusive Lock مطرح میشن.
البته جزئیات دقیق اسم و رفتار Lockها بین Databaseها فرق میکنه، ولی ایدهی کلی تقریباً همینه: Database باید مشخص کنه چه Transactionهایی میتونن همزمان به یه Resource دسترسی داشته باشن.
Row-Level Lock 🧩
توی Row-Level Lock، محدودهی Lock به یک یا چند رکورد محدود میشه. مثلاً دو Transaction میخوان دو User متفاوت رو تغییر بدن. اگه Database بتونه فقط رکوردهای مربوط به هر User رو Lock کنه، این دو Transaction لزوماً مزاحم همدیگه نمیشن.
این یکی از مزیتهای Lockهایی با سطح درگیری کوچیک تره: Concurrency بیشتر. ولی Lock ریزتر رایگان نیست. Database باید تعداد بیشتری Lock و Metadata مربوط به اونهارو مدیریت کنه و توی بعضی شرایط همین موضوع میتونه Overhead ایجاد کنه.
Table-Level Lock 🗄
توی Table-Level Lock، محدودهی Lock بزرگ تره و ممکنه کل Table تحت تأثیر قرار بگیره. این نوع Lock میتونه تو بعضی عملیات ها مفید باشه، ولی طبیعتا احتمال اینکه Transactionهای بیشتری مجبور به انتظار بشن بالاتره.
مثلاً اگه یک Transaction روی کل جدول Lock گرفته باشه، Transaction دیگه ای ممکنه حتی برای کار کردن روی یک Row کاملاً مستقل هم مجبور بشه منتظر بمونه، البته رفتار دقیقش به نوع Lock و Database هم خیلی بستگی داره.
Lock Compatibility یعنی چی؟ 🧠
داشتن دوتا Lock روی یه دونه Resource الزاماً به معنی Conflict نیست. Database باید بدونه چه ترکیبهایی از Lockها میتونن همزمان وجود داشته باشن. مثلاً دو Shared Lock میتونن توی بسیاری از سیستمها به شکل همزمان وجود داشته باشن، چون هر دو Transaction فقط قصد خوندن دارن. ولی یه Exclusive Lock معمولاً با Lockهای متعارض دیگه سازگار نیست، چون Transaction دارنده ی اون Lock میخواد Resource رو به شکل انحصاری تغییر بده.
اینجاست که Database برای هر Resource باید بتونه تشخیص بده که Lock جدید با Lockهای موجود سازگار هست یا نه. اگه سازگار باشه، Transaction میتونه ادامه بده. اگه نباشه، میره توی وضعیت انتظار.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
وقتی چندتا Transaction همزمان روی یه داده کار میکنن، Database باید یه جوری مشخص کنه کدوم Transaction اجازه داره داده رو تغییر بده و کدوم یکی باید صبر کنه. اینجا از Lock استفاده میشه.
Lock در اصل یه مکانیزم برای کنترل دسترسی همزمان به Resourceهاست. Database با استفاده از Lockها میتونه جلوی بعضی از تغییرات ناسازگار و Race Conditionهای مربوط به دسترسی همزمان رو بگیره. اما Lock فقط به معنی «قفل کردن یه دونه رکورد» نیست. Database میتونه روی Resourceهای مختلف Lock بگیره و همین موضوع باعث میشه رفتار Lockها کمی پیچیده تر از چیزی باشه که توی نگاه اول بنظر میاد.
چرا اصلا همچین چیزی وجود داره؟ 🤔
فرض کنین دو Transaction تقریباً همزمان بخوان مقدار موجودی یه حسابی رو تغییر بدن. اگه هیچ مکانیزم کنترلی ای وجود نداشته باشه، هر دو Transaction ممکنه داده رو بخونن و بر اساس همون مقدار قبلی تصمیم بگیرن. بعد هرکدوم تغییر خودش رو اعمال کنه و نتیجهی نهایی با چیزی که انتظار داشتیم فرق داشته باشه.
Database برای جلوگیری از اینطور Race Conditionهایی باید بتونه دسترسی همزمان Transactionها به Resourceهای مشترک رو کنترل کنه. Lock یکی از ابزارهای اصلی برای همین کاره. Transaction میتونه برای مدتی روی یک Resource لاک بگیره تا Transactionهای دیگه نتونن عملیات ناسازگار با اون Lock رو همزمان انجام بدن.
انواع Lock 🔐
وقتی درباره Lock حرف میزنیم، فقط با یه نوع Lock طرف نیستیم. Databaseها میتونن Lockها رو توی سطوح مختلفی اعمال کنن و همچنین برای نوع دسترسیهای مختلف، Lockهای متفاوتی داشته باشن.
از نظر سطح، دو مورد مهم Row-Level Lock و Table-Level Lock هستن. از نظر نوع دسترسی هم معمولاً مفاهیمی مثل Shared Lock و Exclusive Lock مطرح میشن.
البته جزئیات دقیق اسم و رفتار Lockها بین Databaseها فرق میکنه، ولی ایدهی کلی تقریباً همینه: Database باید مشخص کنه چه Transactionهایی میتونن همزمان به یه Resource دسترسی داشته باشن.
Row-Level Lock 🧩
توی Row-Level Lock، محدودهی Lock به یک یا چند رکورد محدود میشه. مثلاً دو Transaction میخوان دو User متفاوت رو تغییر بدن. اگه Database بتونه فقط رکوردهای مربوط به هر User رو Lock کنه، این دو Transaction لزوماً مزاحم همدیگه نمیشن.
این یکی از مزیتهای Lockهایی با سطح درگیری کوچیک تره: Concurrency بیشتر. ولی Lock ریزتر رایگان نیست. Database باید تعداد بیشتری Lock و Metadata مربوط به اونهارو مدیریت کنه و توی بعضی شرایط همین موضوع میتونه Overhead ایجاد کنه.
Table-Level Lock 🗄
توی Table-Level Lock، محدودهی Lock بزرگ تره و ممکنه کل Table تحت تأثیر قرار بگیره. این نوع Lock میتونه تو بعضی عملیات ها مفید باشه، ولی طبیعتا احتمال اینکه Transactionهای بیشتری مجبور به انتظار بشن بالاتره.
مثلاً اگه یک Transaction روی کل جدول Lock گرفته باشه، Transaction دیگه ای ممکنه حتی برای کار کردن روی یک Row کاملاً مستقل هم مجبور بشه منتظر بمونه، البته رفتار دقیقش به نوع Lock و Database هم خیلی بستگی داره.
Lock Compatibility یعنی چی؟ 🧠
داشتن دوتا Lock روی یه دونه Resource الزاماً به معنی Conflict نیست. Database باید بدونه چه ترکیبهایی از Lockها میتونن همزمان وجود داشته باشن. مثلاً دو Shared Lock میتونن توی بسیاری از سیستمها به شکل همزمان وجود داشته باشن، چون هر دو Transaction فقط قصد خوندن دارن. ولی یه Exclusive Lock معمولاً با Lockهای متعارض دیگه سازگار نیست، چون Transaction دارنده ی اون Lock میخواد Resource رو به شکل انحصاری تغییر بده.
اینجاست که Database برای هر Resource باید بتونه تشخیص بده که Lock جدید با Lockهای موجود سازگار هست یا نه. اگه سازگار باشه، Transaction میتونه ادامه بده. اگه نباشه، میره توی وضعیت انتظار.
🧩Part 01
➖➖➖➖➖➖➖➖➖➖
#️⃣ #backend #database #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
🔥1
موقع Blocking دقیقاً چه اتفاقی میفته؟ ⏳
فرض کنین Transaction اول روی یه رکوردی یک Lock گرفته و هنوز Commit نکرده. حالا Transaction دوم میخواد عملیاتی انجام بده که با Lock اول سازگار نیست. Database معمولاً نمیتونه اجازه بده Transaction دوم همزمان اون عملیات رو انجام بده. پس Transaction دوم Block میشه و تا زمانی که Lock آزاد بشه منتظر میمونه.
این انتظار یه بخش طبیعی از سیستمهای Concurrent هست. اما مشکل از جایی شروع میشه که Transaction اول مدت زیادی Lock رو نگه داره. اونموقع هست که Transactionهای بیشتری پشت اون قرار میگیرن و یک Lock کوچیک میتونه تبدیل به یه زنجیره ی بلند از Transaction های Block شده بشه.
Lock Timeout ⏱️
خب، Transaction دومی که داشتیم نمیتونه برای همیشه منتظر Lockای که Transaction اول گرفته بمونه. اینجا Lock Timeout اهمیت پیدا میکنه. Database یا Application میتونه برای یه مدت زمان مشخص منتظر آزاد شدن Lock بمونه. اگه این زمان تموم بشه و Resource همچنان Locked باشه، عملیات با خطا متوقف میشه.
این موضوع باعث میشه یک Transaction گیر کرده یا یک Lock طولانی مدت نتونه برای همیشه Requestهای دیگه رو معطل کنه. البته Timeout با Deadlock یکی نیست. توی Deadlock، تراکنش ها به شکل چرخهای منتظر Lockهای یکدیگه هستن و Database معمولاً باید خودش این وضعیت رو تشخیص بده و یکی از Transactionها رو قربانی کنه. Timeout صرفاً میگه «من تا این مدت صبر کردم و دیگه قرار نیست بیشتر از این ادامه بدم».
Optimistic vs Pessimistic Locking ⚖️
تا اینجا بیشتر درباره Pessimistic Locking صحبت کردیم. تو این مدل، فرض اصلی اینه که احتمال Conflict وجود داره، پس Transaction قبل یا هنگام دسترسی به داده Lock میگیره تا Transactionهای دیگه نتونن همزمان تغییر ناسازگار ایجاد کنن.
ولی تو Optimistic Locking فرضمون از وضعیت داده هامون متفاوته. اینجا معمولاً فرض میکنیم Conflict زیاد اتفاق نمیوفته، پس از اول Resource رو برای مدت طولانی Lock نمیکنیم. به جای اون، موقع Update بررسی میکنیم که داده از زمانی که خوندیم تغییر کرده یا نه.
تفاوت اصلی این دوتا مدل اینه که Pessimistic Locking سعی میکنه Conflict رو قبل از رخ دادن کنترل کنه، در حالی که Optimistic Locking بیشتر روی تشخیص Conflict در زمان تغییر حساب میکنه.
جمعبندی ✍️
Lock یکی از ابزارهای اصلی Database برای کنترل دسترسی همزمان Transactionها به Resourceهای مشترکه. این Lock میتونه توی سطح های مختلفی از Row تا Table اعمال بشه و بسته به نوع Lock، بعضی دسترسیها میتونن همزمان انجام بشن و بعضی دیگه باعث Blocking بشن.
وقتی Lockها با هم سازگار نباشن، Transaction ممکنه مجبور بشه منتظر بمونه و در صورت طولانی شدن این انتظار، Lock Timeout یا حتی Deadlock میتونه وارد فرآیند بشه.
از طرف دیگه، همیشه لازم نیست برای کنترل Concurrency از Lockهای مستقیم استفاده کنیم. Optimistic Locking میتونه به جای جلوگیری از Conflict، اون رو موقع Update تشخیص بده.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
فرض کنین Transaction اول روی یه رکوردی یک Lock گرفته و هنوز Commit نکرده. حالا Transaction دوم میخواد عملیاتی انجام بده که با Lock اول سازگار نیست. Database معمولاً نمیتونه اجازه بده Transaction دوم همزمان اون عملیات رو انجام بده. پس Transaction دوم Block میشه و تا زمانی که Lock آزاد بشه منتظر میمونه.
این انتظار یه بخش طبیعی از سیستمهای Concurrent هست. اما مشکل از جایی شروع میشه که Transaction اول مدت زیادی Lock رو نگه داره. اونموقع هست که Transactionهای بیشتری پشت اون قرار میگیرن و یک Lock کوچیک میتونه تبدیل به یه زنجیره ی بلند از Transaction های Block شده بشه.
Lock Timeout ⏱️
خب، Transaction دومی که داشتیم نمیتونه برای همیشه منتظر Lockای که Transaction اول گرفته بمونه. اینجا Lock Timeout اهمیت پیدا میکنه. Database یا Application میتونه برای یه مدت زمان مشخص منتظر آزاد شدن Lock بمونه. اگه این زمان تموم بشه و Resource همچنان Locked باشه، عملیات با خطا متوقف میشه.
این موضوع باعث میشه یک Transaction گیر کرده یا یک Lock طولانی مدت نتونه برای همیشه Requestهای دیگه رو معطل کنه. البته Timeout با Deadlock یکی نیست. توی Deadlock، تراکنش ها به شکل چرخهای منتظر Lockهای یکدیگه هستن و Database معمولاً باید خودش این وضعیت رو تشخیص بده و یکی از Transactionها رو قربانی کنه. Timeout صرفاً میگه «من تا این مدت صبر کردم و دیگه قرار نیست بیشتر از این ادامه بدم».
Optimistic vs Pessimistic Locking ⚖️
تا اینجا بیشتر درباره Pessimistic Locking صحبت کردیم. تو این مدل، فرض اصلی اینه که احتمال Conflict وجود داره، پس Transaction قبل یا هنگام دسترسی به داده Lock میگیره تا Transactionهای دیگه نتونن همزمان تغییر ناسازگار ایجاد کنن.
ولی تو Optimistic Locking فرضمون از وضعیت داده هامون متفاوته. اینجا معمولاً فرض میکنیم Conflict زیاد اتفاق نمیوفته، پس از اول Resource رو برای مدت طولانی Lock نمیکنیم. به جای اون، موقع Update بررسی میکنیم که داده از زمانی که خوندیم تغییر کرده یا نه.
مثلاً میشه یک Version Number کنار داده داشت. Transaction موقع خوندن Version فعلی رو نگه میداره و موقع Update فقط وقتی تغییر رو اعمال میکنه که Version هنوز همون مقدار قبلی باشه. حالا اگه Version تغییر کرده باشه، یعنی Transaction دیگه ای قبل از ما داده رو تغییر داده و باید Conflict رو مدیریت کنیم.
تفاوت اصلی این دوتا مدل اینه که Pessimistic Locking سعی میکنه Conflict رو قبل از رخ دادن کنترل کنه، در حالی که Optimistic Locking بیشتر روی تشخیص Conflict در زمان تغییر حساب میکنه.
جمعبندی ✍️
Lock یکی از ابزارهای اصلی Database برای کنترل دسترسی همزمان Transactionها به Resourceهای مشترکه. این Lock میتونه توی سطح های مختلفی از Row تا Table اعمال بشه و بسته به نوع Lock، بعضی دسترسیها میتونن همزمان انجام بشن و بعضی دیگه باعث Blocking بشن.
وقتی Lockها با هم سازگار نباشن، Transaction ممکنه مجبور بشه منتظر بمونه و در صورت طولانی شدن این انتظار، Lock Timeout یا حتی Deadlock میتونه وارد فرآیند بشه.
از طرف دیگه، همیشه لازم نیست برای کنترل Concurrency از Lockهای مستقیم استفاده کنیم. Optimistic Locking میتونه به جای جلوگیری از Conflict، اون رو موقع Update تشخیص بده.
🧩Part 02 | EOF
➖➖➖➖➖➖➖➖➖➖
#️⃣ #backend #database #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
Database Sharding چطور کار میکنه؟ 🗄
فرض کنین یه Database داریم که حجم دادههاش هر ثانیه داره بیشتر میشه. اول کار شاید یه Server خیلی راحت از پسش بربیاد، ولی با بزرگتر شدن سیستم فقط تعداد رکورد ها زیاد نمیشه. Storage بیشتری لازم داریم، Indexها بزرگتر میشن، Queryها باید حجم بیشتری از داده رو بررسی کنن و فشار روی CPU، Memory به دنبال I/O بالا میرن و کم کم سیستم دیگه انتظاراتمون رو برآورده نمیکنه.
اینجا Scale کردن عمودی هم بالاخره یه جایی به محدودیت میرسه. نمیتونیم برای همیشه فقط با خریدن یه سرور قوی تر مشکل رو حل کنیم. حتی اگه از نظر سختافزار هم بتونیم، هزینه و محدودیتهای یه دونه Node واحد بالاخره خودشون رو نشون میدن. یکی از راه هایی که برای عبور از این محدودیت استفاده میشه Sharding هست.
Sharding چیه؟ 🧩
توی Sharding، به جای اینکه کل داده هامون رو روی یه Database نگه داریم، داده ها رو بین چند Database یا Node تقسیم میکنیم. هر قسمت از دادهها یک Shard محسوب میشه و مجموع این Shardها Dataset کامل سیستم رو تشکیل میدن.
نکته مهم اینه که Shardها معمولاً کپی همدیگه نیستن. مثلاً ممکنه یک Shard اطلاعات بخشی از Userها رو نگه داره و Shard دیگه بخش متفاوتی از Userها رو. بنابراین با اضافه کردن Node، ظرفیت Storage و پردازش کل سیستم هم میتونه افزایش پیدا کنه.
ولی همینجا یک مشکل جدید داریم: وقتی یه Record جدید وارد سیستم میشه، Database باید بدونه این Record دقیقاً باید روی کدوم Shard قرار بگیره.
Shard Key 🔑
برای تصمیم گیری درباره محل قرار گرفتن داده، معمولاً یه فیلد یا مجموعهای از فیلدها به عنوان Shard Key انتخاب میشه. این Key ورودی اصلی الگوریتم Sharding به حساب میاد و بر اساس مقدارش مشخص میشه Record جدید به کدوم Shard بره.
مثلاً
اما انتخاب Shard Key فقط به این بستگی نداره که یک مقدار برای تقسیم کردن داده مناسب باشه. باید ببینیم Queryهای اصلی سیستم چطور به داده دسترسی پیدا میکنن و آیا این Key باعث میشه دادهها و ترافیک به شکل متعادلی بین Shardها پخش بشن یا نه.
Hash Sharding 🎲
توی Hash Sharding مقدار Shard Key وارد یه Hash Function میشه و نتیجه برای تعیین Shard موردنظر استفاده میشه. هدف اصلی اینه که مقدارهای مختلف Key تا حد ممکن به شکل یکنواخت بین Shardها پخش بشن. مثلاً اگه
ولی Hash Sharding یک مشکل مهم داره: Mapping بین Key و Shard شدیداً به تعداد Shardها وابسته میشه. اگه الگوریتم سادهای مثل تقسیم Hash بر تعداد Shardها داشته باشیم و تعداد Nodeها از ۴ به ۵ تغییر کنه، تعداد زیادی از Keyها ممکنه مقصد جدیدی پیدا کنن. یعنی اضافه کردن یک Shard میتونه Migration بسیار بزرگی ایجاد کنه.
Consistent Hashing 🌀
حالا Consistent Hashing میاد یه ایده ی متفاوت ارائه میده. Keyها روی یک فضای Hash قرار میگیرن و هر Key معمولاً به نزدیکترین Shard توی اون فضای منطقی اختصاص پیدا میکنه. بنابراین اضافه یا حذف شدن یه Shard قرار نیست Mapping همه ی Keyها رو از نو تغییر بده. در نتیجه وقتی یک Node جدید اضافه میشه، معمولاً فقط بخشی از دادهها که تحت تأثیر اون Node قرار گرفتن باید جا به جا بشن. این ویژگی برای سیستمهایی که تعداد Nodeهاشون در طول زمان تغییر میکنه اهمیت زیادی داره.
Range Sharding 📏
تو Range Sharding، دادهها بر اساس محدودهای از Shard Key تقسیم میشن. مثلاً میتونیم یک محدوده از IDها رو روی یک Shard و محدودهی بعدی رو روی Shard دیگه قرار بدیم.
مزیت بزرگ این روش اینه که ارتباط بین مقدار Key و محل داده خیلی قابل پیشبینیه. اگه Query یک Range مشخص رو بخواد، سیستم میتونه بفهمه این دادهها دقیقاً در کدوم Shard قرار دارن و فقط همون Shard رو بررسی کنه. این ویژگی برای Queryهای Range-based خیلی مفیده. مثلاً اگه دادهها بر اساس Timestamp تقسیم شده باشن، Query مربوط به یک بازهی زمانی مشخص میتونه فقط به Shardهایی ارسال بشه که اون بازه رو نگه میدارن.
➖➖➖➖➖➖➖➖➖➖
➖➖➖➖➖➖➖➖➖➖
فرض کنین یه Database داریم که حجم دادههاش هر ثانیه داره بیشتر میشه. اول کار شاید یه Server خیلی راحت از پسش بربیاد، ولی با بزرگتر شدن سیستم فقط تعداد رکورد ها زیاد نمیشه. Storage بیشتری لازم داریم، Indexها بزرگتر میشن، Queryها باید حجم بیشتری از داده رو بررسی کنن و فشار روی CPU، Memory به دنبال I/O بالا میرن و کم کم سیستم دیگه انتظاراتمون رو برآورده نمیکنه.
اینجا Scale کردن عمودی هم بالاخره یه جایی به محدودیت میرسه. نمیتونیم برای همیشه فقط با خریدن یه سرور قوی تر مشکل رو حل کنیم. حتی اگه از نظر سختافزار هم بتونیم، هزینه و محدودیتهای یه دونه Node واحد بالاخره خودشون رو نشون میدن. یکی از راه هایی که برای عبور از این محدودیت استفاده میشه Sharding هست.
Sharding چیه؟ 🧩
توی Sharding، به جای اینکه کل داده هامون رو روی یه Database نگه داریم، داده ها رو بین چند Database یا Node تقسیم میکنیم. هر قسمت از دادهها یک Shard محسوب میشه و مجموع این Shardها Dataset کامل سیستم رو تشکیل میدن.
نکته مهم اینه که Shardها معمولاً کپی همدیگه نیستن. مثلاً ممکنه یک Shard اطلاعات بخشی از Userها رو نگه داره و Shard دیگه بخش متفاوتی از Userها رو. بنابراین با اضافه کردن Node، ظرفیت Storage و پردازش کل سیستم هم میتونه افزایش پیدا کنه.
ولی همینجا یک مشکل جدید داریم: وقتی یه Record جدید وارد سیستم میشه، Database باید بدونه این Record دقیقاً باید روی کدوم Shard قرار بگیره.
Shard Key 🔑
برای تصمیم گیری درباره محل قرار گرفتن داده، معمولاً یه فیلد یا مجموعهای از فیلدها به عنوان Shard Key انتخاب میشه. این Key ورودی اصلی الگوریتم Sharding به حساب میاد و بر اساس مقدارش مشخص میشه Record جدید به کدوم Shard بره.
مثلاً
user_id رو به عنوان Shard Key انتخاب کردیم. حالا وقتی Request مربوط به یک User وارد میشه، سیستم میتونه از روی user_id تشخیص بده داده ی این User روی کدوم Shard قرار داره و Query رو مستقیماً به همون Node بفرسته. اینکه درخواست ما فقط به Shard مورد نیاز برسه یکی از مهم ترین دغدغه های معماری های Sharded هست.اما انتخاب Shard Key فقط به این بستگی نداره که یک مقدار برای تقسیم کردن داده مناسب باشه. باید ببینیم Queryهای اصلی سیستم چطور به داده دسترسی پیدا میکنن و آیا این Key باعث میشه دادهها و ترافیک به شکل متعادلی بین Shardها پخش بشن یا نه.
Hash Sharding 🎲
توی Hash Sharding مقدار Shard Key وارد یه Hash Function میشه و نتیجه برای تعیین Shard موردنظر استفاده میشه. هدف اصلی اینه که مقدارهای مختلف Key تا حد ممکن به شکل یکنواخت بین Shardها پخش بشن. مثلاً اگه
user_id Shard Key باشه، دو User با IDهای نزدیک لزوماً روی یک Shard قرار نمیگیرن. این ویژگی باعث میشه برخلاف Range Sharding، توزیع داده معمولاً کمتر تحت تأثیر ترتیب خود Key قرار بگیره و احتمال Hotspot شدن یک محدوده کمتر بشه.ولی Hash Sharding یک مشکل مهم داره: Mapping بین Key و Shard شدیداً به تعداد Shardها وابسته میشه. اگه الگوریتم سادهای مثل تقسیم Hash بر تعداد Shardها داشته باشیم و تعداد Nodeها از ۴ به ۵ تغییر کنه، تعداد زیادی از Keyها ممکنه مقصد جدیدی پیدا کنن. یعنی اضافه کردن یک Shard میتونه Migration بسیار بزرگی ایجاد کنه.
Consistent Hashing 🌀
حالا Consistent Hashing میاد یه ایده ی متفاوت ارائه میده. Keyها روی یک فضای Hash قرار میگیرن و هر Key معمولاً به نزدیکترین Shard توی اون فضای منطقی اختصاص پیدا میکنه. بنابراین اضافه یا حذف شدن یه Shard قرار نیست Mapping همه ی Keyها رو از نو تغییر بده. در نتیجه وقتی یک Node جدید اضافه میشه، معمولاً فقط بخشی از دادهها که تحت تأثیر اون Node قرار گرفتن باید جا به جا بشن. این ویژگی برای سیستمهایی که تعداد Nodeهاشون در طول زمان تغییر میکنه اهمیت زیادی داره.
Range Sharding 📏
تو Range Sharding، دادهها بر اساس محدودهای از Shard Key تقسیم میشن. مثلاً میتونیم یک محدوده از IDها رو روی یک Shard و محدودهی بعدی رو روی Shard دیگه قرار بدیم.
مزیت بزرگ این روش اینه که ارتباط بین مقدار Key و محل داده خیلی قابل پیشبینیه. اگه Query یک Range مشخص رو بخواد، سیستم میتونه بفهمه این دادهها دقیقاً در کدوم Shard قرار دارن و فقط همون Shard رو بررسی کنه. این ویژگی برای Queryهای Range-based خیلی مفیده. مثلاً اگه دادهها بر اساس Timestamp تقسیم شده باشن، Query مربوط به یک بازهی زمانی مشخص میتونه فقط به Shardهایی ارسال بشه که اون بازه رو نگه میدارن.
🧩Part 01
➖➖➖➖➖➖➖➖➖➖
#️⃣ #backend #database #programming
➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1