پروتکل 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 T
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 برای مدت طولانی درگیر سرویس خراب نشن. در همین مدت، سرویس مقصد هم فرصت پیدا میکنه بدون
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 میکنه.
پس میتونیم یه جورایی
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 هر دو همچنان بیشتر از صفر باقی بمونه.
در نتیجه Referen
حالا 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
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 او
پس 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
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 به تنهایی مشکل خاصی رو حل
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
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 De
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 هست.
جمعبندی 🧠
State
روز برنامه نویس مبارک به همهی اونایی که یه جایی بین ارورهای عجیب، تحریم های عجیب تر و با اینترنتی که میدونن نمیشه بهش گفت "اینترنت"، هنوز دارن کد میزنن.
به اونایی که هر روز یه چیز جدید یاد میگیرن، هر هفته یه تکنولوژی جدید میبینن و هر روز یه بار هم به این فکر میکنن که اصلاً چرا وارد این حوزه شدن؛ مخصوصاً وقتی وضعیت بازار کار و بیکاری رو میبینن و با خودشون میگن «من دقیقاً دارم برای چی اینهمه چیز یاد میگیرم؟» :)
برنامه نویسی فقط نوشتن کد نیست؛ یه جور عادت کردنه، عادت کردن به اینکه هر مشکلی، هرچقدر هم مسخره، ساده یا پیچیده باشه، بشه نشست و با تیکه تیکه کردنش حلش کرد.
روز برنامه نویس مبارک.
به امید روزایی که حداقل دیباگ کردن کدمون سخت ترین چیزی باشه که تجربش میکنیم.
🌙 CHANNEL | GROUP
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 میتونه خی
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 منتقل میشه و تغییرات روی همون نسخه انجام میشه. در نهایت هم لایه ی پایین دستنخورده ب
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 فرق داره ⚠️
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 ا
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 استفاده میکنه و سعی میک
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 میتونه اطل
یه 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 با یه فایل، 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 Descr
شبکه ی 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 بر
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 لزوماً مزاحم همدیگه نم
موقع 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 بررسی میکنیم که داده از زمانی که خوندیم تغییر کرده یا نه.
مثلاً میشه یک Version Number کنار داده داشت. Transaction موقع خوندن Version فعلی رو نگه میداره و موقع Update فقط وقتی تغییر رو اعمال میکنه که Version هنوز همون