Midnight Commit
121 subscribers
3 photos
1 file
63 links
Beyond Syntax.
The fundamentals behind great software.
Download Telegram
تولدت مبارک پیرمرد قابل اعتماد دنیای لینوکس!
از مرداد ۱۳۷۲ تا امروز Stable مثل همیشه

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

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

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

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

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

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

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

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

#️⃣ #network #web #devops

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

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

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

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

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

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

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

#️⃣ #network #web #devops

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

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

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

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

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

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

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

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


#️⃣ #network

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

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

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

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

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

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

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

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


#️⃣ #system_design #backend #programming

➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤4
Please open Telegram to view this post
VIEW IN TELEGRAM
‏Reverse Proxy چطور کار می‌کنه؟ 🤔
وقتی یه Client می‌خواد به یه Server وصل بشه، لزوماً قرار نیست مستقیماً با خود Application Server ارتباط داشته باشه. ممکنه بین Client و Server یه سرویس دیگه قرار گرفته باشه که Requestها رو دریافت کنه و بعد اون‌ها رو به Serverهای پشت خودش Forward کنه.
به این سرویس Reverse Proxy می‌گیم. اما چرا اصلاً باید چنین چیزی وسط مسیر قرار بگیره و چه تفاوتی با حالتی داره که Client مستقیماً به Application Server وصل بشه؟

‏Reverse Proxy دقیقاً چیه؟ 🧠
‏Reverse Proxy سرویسیه که جلوی Application Server قرار می‌گیره و Requestهای Client رو دریافت می‌کنه. Client در واقع با Reverse Proxy ارتباط برقرار می‌کنه و Reverse Proxy تصمیم می‌گیره Request رو به کدوم Server یا Service پشت خودش Forward کنه.
مثلاً ممکنه Client به example.com درخواست بفرسته، اما به جای اینکه مستقیماً به Django یا FastAPI وصل بشه، Request اول به NGINX برسه و NGINX اون رو به Application Server منتقل کنه. در این حالت Client لازم نیست بدونه پشت Reverse Proxy چه Serverهایی وجود دارن.

چرا Reverse Proxy استفاده می‌کنیم؟ 🚦
یکی از مهم‌ترین دلایل استفاده از Reverse Proxy اینه که می‌تونیم یه نقطه‌ی ورودی مشترک برای چندین Service داشته باشیم. مثلاً ممکنه یه سیستم همزمان یه Frontend، یه API، یه سرویس Authentication و یه سرویس Media داشته باشه. Reverse Proxy می‌تونه بر اساس Domain یا مسیر Request تشخیص بده هر Request باید به کدوم سرویس ارسال بشه.
علاوه بر Routing میشه کارهایی مثل TLS Termination، Load Balancing، Compression و Caching رو هم انجام داد. در نتیجه Application Server لازم نیست همه‌ی این وظایف رو خودش مدیریت کنه.

‏Reverse Proxy چه فرقی با Forward Proxy داره؟ 🔄
تفاوت اصلی این دو تا توی اینه که Proxy به نمایندگی از چه کسی Request رو ارسال می‌کنه.
توی Proxy, Forward Proxyمعمولاً بین Client و Internet قرار می‌گیره و به نمایندگی از Client به Serverهای مختلف Request می‌فرسته. خود Server مقصد ممکنه Client واقعی رو نبینه و فقط Forward Proxy رو ببینه.
اما تو Proxy, Reverse Proxy جلوی Serverها قرار می‌گیره و به نمایندگی از اون‌ها Requestهای Client رو دریافت و Forward می‌کنه.
پس می‌تونیم یه جورایی بگیم ‏Forward Proxy نماینده‌ی Client و Reverse Proxy نماینده‌ی Server هستن.

‏Reverse Proxy و Load Balancing ⚖️
‏Reverse Proxy الزاماً Load Balancer نیست، اما می‌تونه نقش Load Balancer رو هم داشته باشه. فرض کنین سه Instance از یه Application Server داریم. Reverse Proxy می‌تونه Requestهای ورودی رو بین این سه Instance تقسیم کنه.
تو این حالت Client همچنان فقط یه Endpoint می‌بینه، ولی پشت اون Endpoint چندین Server وجود دارن و Reverse Proxy مسئول انتخاب مقصد هر Requestه.
این موضوع باعث میشه بتونیم تعداد Instanceها رو افزایش بدیم و Load رو بین اون‌ها پخش کنیم، بدون اینکه Client نیاز داشته باشه چیزی درباره‌ی ساختار داخلی سیستم بدونه.

چرا Application Server رو مستقیم در معرض اینترنت نذاریم؟ 🛡
قرار دادن Reverse Proxy جلوی Application Server فقط برای Routing نیست. این لایه می‌تونه به عنوان یه نقطه‌ی کنترل برای Requestهای ورودی هم عمل کنه.
مثلاً می‌تونیم محدودیت‌هایی برای اندازه‌ی Request، تعداد Connectionها یا بعضی الگوهای Request تعریف کنیم و TLS رو هم همین‌جا Terminate کنیم.
در نتیجه Application Server بیشتر روی اجرای Logic مربوط به Application تمرکز می‌کنه و Reverse Proxy بخشی از وظایف مربوط به Traffic ورودی رو بر عهده می‌گیره.

جمع‌بندی ✍️
‏Reverse Proxy یه لایه بین Client و Application Server قرار میده و Requestهای ورودی رو به Service یا Server مناسب Forward می‌کنه. این لایه می‌تونه علاوه بر Routing، وظایفی مثل TLS Termination، Load Balancing، Caching و کنترل Traffic رو هم انجام بده.
به همین دلیل توی خیلی از معماری‌های Backend، چیزی مثل NGINX جلوی Application Server قرار می‌گیره و Client به جای ارتباط مستقیم با Application، اول با Reverse Proxy صحبت می‌کنه.

#️⃣ #system_design #backend #network

➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤2
‏Python Memory Management 🧠
وقتی توی Python یه Object می‌سازیم، مثلاً یه List یا یه Instance از یک Class، این Object باید جایی توی Memory قرار بگیره. Python خودش مسئول مدیریت این Memory هست و برخلاف زبان‌هایی مثل C، معمولاً لازم نیست ما دستی مشخص کنیم چه زمانی Memory Allocate یا Free بشه.
اما اینکه Python خودش این کارها رو انجام میده به این معنی نیست که Memory Management ساده هست یا یسری مفاهیم رو از داده. پشت این رفتار، مفاهیمی مثل Reference Counting، Garbage Collection، Memory Allocator و Heap وجود دارن که با هم مشخص می‌کنن Objectها کجا قرار بگیرن، چه زمانی دیگه مورد استفاده نیستن و چه زمانی Memory مربوط به اون‌ها آزاد بشه.

‏Python Objectها کجا قرار می‌گیرن؟ 📦
توی CPython، آبجکت که توی برنامه می‌سازیم معمولاً روی Heap قرار می‌گیرن. وقتی مثلاً می‌نویسیم users = []، پایتون برای ساختن List یک Object ایجاد می‌کنه و Memory مورد نیازش رو از Allocator خودش دریافت می‌کنه.
این Memory با Stack اشتباه گرفته نشه. Variableای که اسم users رو نگه داشته، در واقع یک Reference به Objectهست و خود List یک Object مستقل روی Heap محسوب میشه. به همین دلیل چند Variable می‌تونن همزمان به یک Object اشاره کنن.

‏Reference Counting 🔢
یکی از مهم‌ترین بخش‌های Memory Management تو پایتون، Reference Counting هست. هر Object یک شمارنده داره که تعداد Referenceهایی که به اون Object اشاره می‌کنن رو دنبال می‌کنه. وقتی یه Reference جدید به Object اضافه میشه، این Count افزایش پیدا می‌کنه و وقتی Reference از بین میره، Count کاهش پیدا می‌کنه.
وقتی Reference Count یه Object به صفر برسه، یعنی دیگه هیچ چیزی از طریق Reference به اون Object دسترسی نداره و CPython می‌تونه Memory مربوط به اون Object رو آزاد کنه.

پس Garbage Collector برای چیه؟ ♻️
اگه Reference Counting داریم، شاید این سؤال پیش بیاد که پس Garbage Collector دیگه چه کاری انجام میده؟
مشکل زمانی ایجاد میشه که Objectها به صورت Circular به هم Reference داشته باشن. مثلاً فرض کنین آبجکت A به B اشاره کنه و B هم به A. حالا ممکنه هیچ Reference خارجی به این دو Object وجود نداشته باشه، اما Reference Count هر دو همچنان بیشتر از صفر باقی بمونه.
در نتیجه Reference Counting به تنهایی نمی‌تونه بفهمه این Objectها دیگه قابل استفاده نیستن. Garbage Collector مخصوصاً برای پیدا کردن و جمع کردن همین Reference Cycleها وارد عمل میشه.

‏Python Memory Allocator ⚙️
‏Python هم برای گرفتن و آزاد کردن Memory مستقیماً برای تک‌تک Objectها با سیستم‌عامل درگیر نمیشه. ‏CPython یک Memory Allocator داره که مدیریت Memory مورد نیاز Objectهای Python رو بهینه‌تر انجام میده. برای Objectهای کوچیک، CPython از مکانیزم‌هایی مثل pymalloc استفاده می‌کنه تا به جای اینکه برای هر Allocation مستقیماً سراغ سیستم‌عامل بره، Memory رو تو بلوک‌های بزرگ‌تر دریافت و خودش مدیریت کنه.
این کار باعث میشه Allocation و Deallocation تعداد زیادی Object کوچیک سریع‌ تر انجام بشه و هزینه‌ی تعامل مستقیم با سیستم‌عامل کمتر بشه.

چرا Memory همیشه به سیستم‌عامل برنمیگرده؟ 🤔
یکی از چیزهایی که ممکنه موقع بررسی Memory یک برنامه‌ی Python گیج‌کننده باشه اینه که وقتی یک Object رو حذف می‌کنیم، ممکنه مقدار Memory مصرفی Process بلافاصله به همون اندازه کاهش پیدا نکنه.
دلیلش اینه که آزاد شدن یک Object لزوماً به معنی پس دادن فوری اون Memory به سیستم‌عامل نیست. Python ممکنه Memory آزاد شده رو داخل Allocator خودش نگه داره تا بعداً برای Objectهای جدید ازش استفاده کنه.
پس ممکنه Object از بین رفته باشه، اما Process همچنان مقدار زیادی Memory از سیستم‌عامل گرفته باشه. این دو موضوع با هم تناقضی ندارن.

🧩Part01

➖➖➖➖➖➖➖➖➖➖
#️⃣ #programming #backend #python

➖➖➖➖➖➖➖➖➖➖
🌙 CHANNEL | GROUP
❤1
حالا ‏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 می‌نویسیم:
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
‏پس 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 رو فراهم می‌کنه.

#️⃣ #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 بشه.

#️⃣ #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 رو متوقف می‌کنه و یکی کمک می‌کنه خود سیستم همچنان قابل استفاده بمونه.

#️⃣ #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 انجام بشه.

#️⃣ #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 ندونه آخرین وضعیت معتبرش چی بوده.

#️⃣ #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 این لایه ها رو داشته باشه:
‏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 در نظر بگیره.
مثلاً:
T1 → T2
T2 → 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 A
T2 → 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 مثل این می‌نویسیم:
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 مثل این رو بررسی کنیم:
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