Codoloper

آموزش برنامه‌نویسی به فارسی

یاد بگیر، پیشرفت کن، بساز.

منبع آموزشی برنامه‌نویسی به زبان فارسی - مستندات، دوره‌ها و مطالب کاربردی برای همه سطوح.

داکیومنت ها:

JavascriptHTMLCSSReact JSبیشتر
1// برنامه‌نویسی به فارسی
2import { learn } from 'codoloper'
3
4const developer = learn({
5"lang": "فارسی"
6"level": "همه سطوح"
7free: true
8})
بلاگ

مطالب جدید

تازه‌ترین مطالب آموزشی و خبرهای مرتبط.

ایندکس دیتابیس: چرا یک کوئری ساده گاهی چند ثانیه طول میکشه نوشته شده توسط عرفان دهقانی

ایندکس دیتابیس: چرا یک کوئری ساده گاهی چند ثانیه طول میکشه

فرض کن یک جدول داری با یک میلیون ردیف، و میخوای کاربری با یک ایمیل مشخص رو پیدا کنی. بدون هیچ کمکی، دیتابیس مجبوره ردیف به ردیف کل جدول رو بگرده تا برسه به همونی که دنبالشی — به این کار میگن full table scan. روی یک میلیون ردیف، این یعنی دیتابیس تا آخرین لحظه هم نمیدونه جواب رو پیدا کرده یا نه، و باید همه‌چیز رو ببینه. حالا اگه همین جدول یک ایندکس روی ستون ایمیل داشته باشه، دیتابیس دیگه لازم نیست همه‌جا رو بگرده؛ مستقیم میره سراغ جایی که احتمال جواب هست. تفاوتش میتونه از چند ثانیه به چند میلی‌ثانیه برسه.

ایندکس چیه واقعاً

ساده‌ترین راه برای فهمیدن ایندکس، مقایسه‌اش با فهرست آخر یک کتابه. اگه بخوای بفهمی کلمه‌ی «recursion» توی کدوم صفحه‌ی کتاب اومده، دو راه داری: یا کتاب رو صفحه به صفحه بخونی تا پیداش کنی (همون full scan)، یا بری سراغ فهرست آخر کتاب که مرتب‌شده‌ست و سریع بگی صفحه‌ی ۲۴۳. ایندکس دیتابیس دقیقاً همین نقشو بازی میکنه: یک ساختار داده‌ی جدا و مرتب‌شده که به ازای هر مقدار توی یک ستون، میگه دقیقاً کجا (کدوم ردیف فیزیکی) باید بری دنبالش.

اکثر دیتابیس‌های رابطه‌ای مثل PostgreSQL و MySQL از یک ساختار داده به اسم B-tree برای ایندکس‌ها استفاده میکنن. B-tree یک درخت متوازنه که جستجو، درج، و حذف توش با پیچیدگی زمانی لگاریتمی انجام میشه — یعنی حتی روی یک جدول با میلیون‌ها ردیف، پیدا کردن یک مقدار فقط چند مرحله طول میکشه، نه میلیون‌ها مقایسه.

چرا رایگان نیست

اگه ایندکس این‌قدر خوبه، چرا همه‌ی ستون‌ها رو ایندکس نمیکنیم؟ چون هزینه داره، و این هزینه دو جا خودش رو نشون میده.

اول، فضا. هر ایندکس یک کپی جداگانه از داده‌ی اون ستون (به‌شکل ساختار درختی) نگه میداره. روی جدول‌های بزرگ، این میتونه چندین گیگابایت فضای اضافی مصرف کنه.

دوم و مهم‌تر، سرعت نوشتن. هر بار که یک ردیف جدید insert میکنی یا یک ردیف موجود رو update میکنی، دیتابیس باید همزمان با آپدیت کردن جدول اصلی، تمام ایندکس‌های مرتبط با اون ستون‌ها رو هم آپدیت کنه. اگه یک جدول پنج تا ایندکس داشته باشه، هر insert عملاً شش تا عملیات نوشتن رو تریگر میکنه، نه یکی. برای همین توصیه‌ی رایج اینه که فقط روی ستون‌هایی ایندکس بذاری که واقعاً توی WHERE، JOIN، یا ORDER BY زیاد ازشون استفاده میکنی — نه هر ستونی که «شاید یک روز» لازم بشه.

clustered در مقابل non-clustered

یک تمایز مهم که خیلی وقت‌ها گیج‌کننده‌ست، فرق ایندکس clustered و non-clustered است. توی ایندکس clustered، ترتیب فیزیکی داده روی دیسک دقیقاً همون ترتیب ایندکسه — یعنی خود جدول، به شکل ایندکس مرتب شده. هر جدول فقط میتونه یک ایندکس clustered داشته باشه، چون داده فقط یک ترتیب فیزیکی روی دیسک میتونه داشته باشه. توی MySQL با InnoDB، این معمولاً همون primary key است.

ایندکس non-clustered یک ساختار جداست که فقط اشاره‌گر به ردیف اصلی نگه میداره، نه خود داده رو. میتونی تعداد دلخواهی از این نوع ایندکس روی یک جدول داشته باشی. تفاوتش توی عمل اینه که خوندن از ایندکس clustered یک مرحله کمتر داره (چون داده همونجاست)، ولی non-clustered برای ستون‌های غیر primary key انعطاف بیشتری میده.

ایندکس ترکیبی و ترتیب ستون‌ها

وقتی کوئری‌هات معمولاً چند شرط با هم دارن — مثلاً WHERE user_id = ? AND status = 'active' — میتونی یک composite index روی هر دو ستون بسازی. نکته‌ی مهمی که خیلی از دولوپرها نادیده میگیرن اینه که ترتیب ستون‌ها توی ایندکس ترکیبی مهمه. یک ایندکس روی (user_id, status) برای کوئری‌هایی که با user_id فیلتر میکنن (با یا بدون status) مفیده، ولی برای کوئری‌هایی که فقط روی status فیلتر میکنن و اصلاً کاری با user_id ندارن، عملاً بی‌فایده‌ست — چون درست مثل فهرست کتابیه که بر اساس فصل مرتب شده، نه بر اساس موضوع؛ اگه فصل رو ندونی، فهرست کمکت نمیکنه.

CREATE INDEX idx_user_status ON orders (user_id, status);

-- این کوئری از ایندکس بالا خوب استفاده میکنه
SELECT * FROM orders WHERE user_id = 42 AND status = 'active';

-- این یکی عملاً از این ایندکس استفاده نمیکنه
SELECT * FROM orders WHERE status = 'active';

چطور بفهمیم دیتابیس واقعاً از ایندکس استفاده میکنه

خیلی وقت‌ها فرض میکنیم چون ایندکس ساختیم، کوئری هم ازش استفاده میکنه، درحالی‌که همیشه این‌طور نیست. ابزار اصلی برای فهمیدن این موضوع دستور EXPLAIN (یا EXPLAIN ANALYZE توی PostgreSQL) است که نشون میده دیتابیس دقیقاً چه plan ای برای اجرای کوئری انتخاب کرده — آیا از ایندکس استفاده کرده یا رفته سراغ full table scan.

EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 42;

اگه توی خروجی این دستور عبارتی مثل Seq Scan ببینی به‌جای Index Scan، یعنی دیتابیس تصمیم گرفته ایندکس رو نادیده بگیره — که میتونه چند دلیل داشته باشه: شاید جدول اونقدر کوچیکه که دیتابیس تشخیص داده full scan سریع‌تره، شاید آمار (statistics) دیتابیس قدیمی شده، یا شاید شرط کوئری طوری نوشته شده که ایندکس اصلاً قابل استفاده نیست (مثلاً استفاده از تابع روی ستون ایندکس‌شده، مثل WHERE LOWER(email) = ...).

وقتی ایندکس ضرر میزنه

جالبه که بدونی ایندکس همیشه سریع‌تر نیست. روی جدول‌های خیلی کوچیک، یا وقتی یک شرط WHERE درصد بزرگی از جدول رو برمیگردونه (مثلاً بیش از ۲۰-۳۰ درصد ردیف‌ها)، دیتابیس معمولاً ترجیح میده مستقیم برگرده به full scan، چون رفتن سراغ ایندکس و بعد پرش بین ردیف‌های پراکنده روی دیسک، از یک خوندن پیوسته و ساده کندتره. این یکی از نکاتی‌یه که خیلی از مهندس‌های تازه‌کار تعجب میکنن وقتی برای اولین بار میبینن دیتابیس با وجود ایندکس، ازش استفاده نکرده — درحالی‌که این تصمیم دیتابیس اتفاقاً درسته، نه باگ.

جمع‌بندی

ایندکس یک معامله‌ست، نه یک ترفند رایگان: سرعت خوندن رو در ازای سرعت نوشتن و فضای اضافی میخری. تصمیم درست این نیست که «همه‌چیز رو ایندکس کن»، بلکه اینه که بفهمی کدوم کوئری‌ها واقعاً توی سیستم پرتکرارن، دقیقاً چه ستون‌هایی رو فیلتر یا مرتب میکنن، و بر همون اساس ایندکس بسازی — و بعد با EXPLAIN چک کنی که فرضت درست بوده.

Cache: کش چیه؟ چرا کش کنیم؟ یک لایه با سرعت بسیار بیشتر نوشته شده توسط عرفان دهقانی

Cache: کش چیه؟ چرا کش کنیم؟ یک لایه با سرعت بسیار بیشتر

فرض کن یک صفحه داری که برای ساختنش باید بری سراغ دیتابیس، چند تا جدول رو join کنی، نتیجه رو پردازش کنی، و بعد HTML بسازی. این کار شاید ۲۰۰ میلی‌ثانیه طول بکشه. حالا اگه این صفحه رو هزار نفر در ثانیه ببینن، و محتواش هر بار عوض نشه، داری همون کار سنگین رو هزار بار در ثانیه تکرار میکنی، درحالی‌که جواب همیشه یکیه. کش دقیقاً همین اتلاف رو حل میکنه: جواب رو یک بار حساب میکنی، جایی نگهش میداری که دسترسی بهش خیلی سریع‌تره، و دفعه‌های بعد به‌جای محاسبه‌ی دوباره، همون جواب آماده رو برمیگردونی.

این ایده به‌قدری پایه‌ایه که توی تقریباً هر لایه‌ای از یک سیستم، از مرورگر گرفته تا CDN تا سرور تا دیتابیس، یک نسخه‌ی متفاوت از همین ترفند رو میبینی.

کش توی مرورگر و سمت کاربر

اولین لایه‌ای که هر کاربر باهاش برخورد میکنه، کش مرورگره. وقتی یک فایل CSS یا تصویر یا فونت رو دانلود میکنی، مرورگر بر اساس هدرهای HTTP مثل Cache-Control و ETag تصمیم میگیره که آیا لازمه دوباره از سرور بگیرتش یا همون نسخه‌ی محلی رو نشون بده. برای همین دفعه‌ی دوم که یک سایت رو باز میکنی، خیلی سریع‌تر لود میشه — چون مرورگر اصلاً درخواست جدیدی نمیفرسته، یا اگه بفرسته، سرور فقط میگه «چیزی عوض نشده» (کد وضعیت ۳۰۴) و دوباره کل فایل رو نمیفرسته.

کش توی CDN

لایه‌ی بعدی که خیلی وقت‌ها دیده نمیشه، CDN است — سرویس‌هایی مثل Cloudflare یا ArvanCloud که یک نسخه از فایل‌های استاتیک سایتت (تصویر، CSS، JS، و حتی گاهی HTML) رو روی سرورهای پخش‌شده در نقاط مختلف جغرافیایی نگه میدارن. وقتی کاربری از یک شهر دیگه به سایتت وصل میشه، به‌جای این‌که درخواستش تا سرور اصلی بره، نزدیک‌ترین edge server جواب رو از کشش برمیگردونه. این هم سرعت رو بالا میبره و هم فشار روی سرور اصلی رو کم میکنه.

کش سمت سرور (Application Cache)

توی خود اپلیکیشن، معمولاً از ابزارهایی مثل Redis یا Memcached استفاده میشه تا نتیجه‌ی محاسبات سنگین یا کوئری‌های تکراری رو نگه دارن. مثلاً اگه یک API داری که لیست محصولات پرفروش رو برمیگردونه، به‌جای این‌که هر بار این لیست رو از دیتابیس محاسبه کنی، یک بار حسابش میکنی و توی Redis با یک TTL (زمان انقضا) ذخیره‌اش میکنی:

async function getTopProducts() {
  const cached = await redis.get('top_products');
  if (cached) return JSON.parse(cached);

  const products = await db.query('SELECT ... ORDER BY sales DESC LIMIT 10');
  await redis.set('top_products', JSON.stringify(products), 'EX', 300); // 5 دقیقه
  return products;
}

این الگو رو cache-aside میگن: اول کش رو چک میکنی، اگه خالی بود از منبع اصلی میگیری و توی کش میذاری. رایج‌ترین الگوی کش توی دنیای بک‌اند همینه، چون منطق ساده‌ست و کنترل کامل دستته.

کش توی دیتابیس

خود دیتابیس‌ها هم لایه‌های کش داخلی دارن که معمولاً کمتر بهشون فکر میکنیم. مثلاً PostgreSQL یک بخش به اسم shared buffers داره که صفحات پرکاربرد دیتابیس رو توی رم نگه میداره تا لازم نباشه هر بار از دیسک بخونه. MySQL هم مکانیزم‌های مشابهی داره. این یعنی حتی وقتی فکر میکنی داری «مستقیم از دیتابیس» میخونی، احتمالاً بخشی از اون داده از قبل توی حافظه‌ی خود دیتابیس کش شده و دیسک اصلاً درگیر نشده.

کش توی اپلیکیشن‌های موبایل

توی اپ‌های موبایل هم همین منطق تکرار میشه، فقط با اسم‌های دیگه. مثلاً یک اپلیکیشن ممکنه لیست محصولات یا فید خبری رو یک بار از سرور بگیره و توی حافظه‌ی محلی گوشی (یا یک دیتابیس سبک مثل SQLite) نگه داره، تا کاربر بدون اینترنت هم بتونه محتوای قبلی رو ببینه، و تا وقتی داده‌ی جدیدی نیومده، دوباره از سرور درخواست نده. این هم برای تجربه‌ی کاربری بهتره (لود سریع‌تر) و هم برای مصرف داده و باتری صرفه‌جویی میکنه.

چیزی که واقعاً برای یک مهندس نرم‌افزار جذابه: invalidation

جمله‌ی معروفی توی دنیای برنامه‌نویسی هست که میگه فقط دو چیز سخت توی علوم کامپیوتر وجود داره: invalidate کردن کش، نام‌گذاری متغیرها، و off-by-one errors. این جمله شوخیه، ولی نکته‌ی جدی پشتش اینه: نگه‌داشتن داده توی کش کار سختی نیست؛ سخت اینه که بفهمی دقیقاً کِی اون داده دیگه معتبر نیست و باید پاکش کنی یا آپدیتش کنی.

فرض کن قیمت یک محصول توی دیتابیس عوض بشه، ولی نسخه‌ی کش‌شده‌اش هنوز قیمت قدیمی رو نشون بده. اگه TTL طولانی گذاشته باشی، این عدم‌تطابق میتونه دقیقه‌ها یا حتی ساعت‌ها طول بکشه. راه‌حل‌های رایج برای این مشکل چندتاست: یکی این‌که هر وقت داده توی دیتابیس آپدیت شد، مستقیماً همون کلید رو از کش پاک کنی (invalidate on write). یکی دیگه اینه که از TTL کوتاه‌تر استفاده کنی تا حداکثر «قدیمی‌بودن» داده محدود باشه. روش سوم که پیچیده‌تره ولی قوی‌تره، استفاده از event-based invalidation است: وقتی یک تغییر توی سیستم اتفاق میفته، یک event منتشر میشه و هر سرویسی که کش مرتبط داره، خودش رو آپدیت میکنه.

یک مشکل کمتر شناخته‌شده: cache stampede

این یکی از اون چیزهاییه که وقتی برای اولین بار باهاش برخورد میکنی، واقعاً غافلگیرت میکنه. فرض کن یک کلید کش با TTL پنج دقیقه‌ای منقضی بشه، و دقیقاً همون لحظه هزار درخواست هم‌زمان برسه. چون کش خالیه، هر هزار درخواست میرن سراغ منبع اصلی (مثلاً دیتابیس) تا همون مقدار رو دوباره محاسبه کنن — درحالی‌که فقط یکی از این محاسبات کافی بود. این پدیده رو cache stampede یا thundering herd میگن، و میتونه یک دیتابیس سالم رو در عرض چند ثانیه از پا دربیاره.

راه‌حل رایج برای این مشکل یک قفل موقته: اولین درخواستی که میبینه کش خالیه، یک lock میگیره و شروع میکنه به محاسبه؛ بقیه‌ی درخواست‌ها یا منتظر میمونن تا اون قفل آزاد بشه، یا موقتاً همون مقدار قدیمی (حتی اگه منقضی شده) رو برمیگردونن تا محاسبه‌ی جدید تموم بشه. به این تکنیک دوم گاهی stale-while-revalidate میگن، و توی خیلی از CDN ها و کتابخانه‌های کش هم پیاده‌سازی شده.

سیاست‌های حذف: چه چیزی رو نگه داریم، چه چیزی رو دور بریزیم

چون حافظه‌ای که کش توش نگه‌داری میشه (چه RAM چه هر چیز دیگه) محدوده، وقتی کش پر میشه باید یک چیزی رو حذف کنی تا جا برای داده‌ی جدید باز بشه. رایج‌ترین سیاست LRU (Least Recently Used) است: چیزی که مدت طولانی‌تری استفاده نشده، اول حذف میشه. یک سیاست دیگه LFU (Least Frequently Used) است که به‌جای «آخرین بار کِی استفاده شده» به «چند بار استفاده شده» نگاه میکنه. انتخاب بین این دو به رفتار واقعی ترافیکت بستگی داره — مثلاً برای یک فید خبری که محتوای پرطرفدار مدام عوض میشه، LRU معمولاً منطقی‌تره؛ برای داده‌هایی که یک زیرمجموعه‌ی ثابت همیشه پرتکرارن، LFU میتونه بهتر عمل کنه.

جمع‌بندی

نکته‌ی جالب کش اینه که هیچ‌وقت یک تصمیم یک‌باره نیست. هر لایه‌ای از سیستم —از مرورگر تا CDN تا اپلیکیشن تا دیتابیس— نسخه‌ی خودش رو از همین ایده‌ی ساده پیاده میکنه: یک بار محاسبه کن، سریع نگه دار، و مراقب باش که کِی این نسخه‌ی سریع دیگه با واقعیت هم‌خونی نداره. بخش سخت کش هیچ‌وقت «نگه‌داشتن» نبوده؛ همیشه «دونستن کِی باید فراموشش کنی» بوده.

تفاوت Layout و Template در Next.js: کی و چرا باید از template.tsx استفاده کنیم؟ نوشته شده توسط Haleh Nakisa

تفاوت Layout و Template در Next.js: کی و چرا باید از template.tsx استفاده کنیم؟

اگر با App Router در Next.js کار کرده باشید، احتمالاً بیشتر وقت‌ها سراغ layout.tsx می‌روید. اما Next.js فایل دیگری به نام template.tsx هم در اختیار ما قرار داده که در ظاهر رفتاری بسیار مشابه دارد، ولی یک فرق ساختاری با Layout دارد.

در این مقاله بررسی می‌کنیم که template چیست، چه فرقی با layout دارد و چرا باید در استفاده از آن هوشمندانه عمل کنیم.

تفاوت کلیدی: حفظ State در برابر ساخت مجدد (Remount)

اصلی‌ترین تفاوت این دو کامپوننت، نحوه رفتار آن‌ها هنگام جابه‌جایی بین صفحات (Navigation) است:

  • فایل layout.tsx: هنگام جابه‌جایی بین routeهای هم‌سطح، ناپدید نمی‌شود، State خود را حفظ می‌کند و از اول رندر (Remount) نمی‌شود.

  • فایل template.tsx: با هر بار تغییر Route، کامپوننت template به‌طور کامل Unmount شده و یک نمونه (Instance) جدید از آن ساخته می‌شود. این یعنی تمامی Stateها و Effectهای درون آن ریست می‌شوند.

نکته: هرگاه یک template.tsx در یک پوشه داشته باشید، Next.js آن را دقیقاً به عنوان فرزندِ Layout و والدِ Page رندر می‌کند: Layout ➔ Template ➔ Page

چه زمانی باید از template.tsx استفاده کنیم؟

زمانی که عدم حفظ State و اجرای مجدد کد با هر تغییر صفحه رفتار مطلوب ما باشد!

۱. انیمیشن‌های ورود و خروج (Enter/Exit Animations)

یکی از کاربردی‌ترین سناریوها، صفحاتی مثل ورود و ثبت‌نام (Sign In / Sign Up) است. اگر در این صفحات وکتورها، المان‌های گرافیکی انتزاعی یا انیمیشن‌های موشن (مثلاً با Framer Motion) دارید و می‌خواهید کاربر با رفتن از فرم Sign In به Sign Up، دوباره اجرای کامل انیمیشن‌ها و ریست شدن حالت‌ها را تجربه کند، template.tsx گزینه انتخابی شماست.

۲. لاگ‌برداری و Analytics بر اساس بازدید صفحه

اگر نیاز دارید با هربار ورود کاربر به هرکدام از صفحات فرعی، یک useEffect اجرا شده و اطلاعاتی مثل Page View ارسال شود.

۳. ریست شدن فرم‌ها یا Stateهای وابسته به Route

وقتی می‌خواهید با جابه‌جایی بین مسیرها، حالت‌های ورودی کاربر کلاً پاک شوند.

نمونه کد عملی

برای تعریف پروپ‌های کامپوننت در ری‌اکت و نکست، استفاده از type ساختار تمیزتر و یکدست‌تری ایجاد می‌کند:

// app/auth/template.tsx
import { ReactNode } from 'react';

type TemplateProps = {
  children: ReactNode;
};

export default function AuthTemplate({ children }: TemplateProps) {
  return (
    <div className="auth-container animate-fade-in">
      {/* المان گرافیکی یا وکتور دارای انیمیشن که در هر تغییر مسیر دوباره اجرا می‌شود */}
      <div className="vector-illustration" />
      <main>{children}</main>
    </div>
  );
}

چرا باید با احتیاط از آن استفاده کرد؟

استفاده بی‌مورد از template.tsx به جای layout.tsx می‌تواند روی تجربه کاربری و عملکرد برنامه تأثیر منفی بگذارد:

  • افت فریم و انیمیشن‌های اضافی: اگر المان‌های سنگینی درون Template قرار دهید، با هر جابه‌جایی کوچک فریم‌ها مجدداً ساخته می‌شوند که می‌تواند باعث Perceived Latency (حس کندی) شود.

  • از دست رفتن Scroll Position یا حالت‌های داخلی: فرم‌ها یا کامپوننت‌هایی که انتظار دارید حالتشان حفظ شود، با هر Navigation کلاً ریست خواهند شد.

خلاصه تصمیم‌گیری

  • اگر می‌خواهید هدر، فوتر، Sidebar یا Stateها ثابت بمانند ➔ از layout.tsx استفاده کنید.

  • اگر می‌خواهید با هر تغییر مسیر، انیمیشن‌ها مجدداً اجرا شده و Effectها ریست شوند ➔ از template.tsx استفاده کنید.

لود بالانسر: از یک سرور تنها تا چیزی که هیچ‌وقت down نمیشه نوشته شده توسط عرفان دهقانی

لود بالانسر: از یک سرور تنها تا چیزی که هیچ‌وقت down نمیشه

اکثر پروژه‌ها همین‌طور شروع میشن: یک سرور ساده روی یک VPS معمولی، کاربر کم، همه‌چیز روبه‌راه. مشکل معمولاً از جایی شروع میشه که یک اتفاق باعث میشه ترافیک چند برابر بشه — یک پست وایرال، یک کمپین تبلیغاتی، یا فقط رشد طبیعی کاربر — و همون یک سرور شروع میکنه به کند شدن و timeout دادن. راه‌حلی که خیلی‌ها اول امتحان میکنن اضافه‌کردن منابع به همون سرور است: رم بیشتر، CPU بیشتر. این کار یک‌مدت جواب میده، ولی زیاد طول نمیکشه که همون مشکل دوباره برگرده. نکته اینه که این مسیر یک سقف داره، و لود بالانسر دقیقاً همون‌جایی وارد میشه که این سقف رو کنار میزنه: به‌جای یک سرور بزرگ‌تر، چند سرور کوچک‌تر که ترافیک بینشون تقسیم بشه.

قبل از لود بالانسر: مقیاس عمودی

قبل از این‌که مفهوم load balancing جا بیفته، راه‌حل رایج برای مقابله با ترافیک بالا چیزی بود به اسم مقیاس عمودی یا vertical scaling: سرور موجود رو قوی‌تر میکردی. رم بیشتر، هسته‌ی پردازشی بیشتر، دیسک سریع‌تر. این روش یک مشکل بزرگ داره: یک سقف فیزیکی وجود داره. حتی اگه پول بی‌نهایت هم داشته باشی، یک سرور فیزیکی یا مجازی یک جایی به حداکثر ظرفیتش میرسه. علاوه بر این، این مدل یک نقطه‌ی شکست واحد داره؛ اگه همون یک سرور بخوابه، کل سرویس از دسترس خارج میشه.

بعد از لود بالانسر: مقیاس افقی

لود بالانسر اومد تا یک مسیر جایگزین باز کنه: مقیاس افقی یا horizontal scaling. به‌جای یک سرور قوی، چند سرور معمولی رو کنار هم میذاری و یک لایه‌ی جلویی به اسم load balancer درخواست‌های ورودی رو بینشون پخش میکنه. کاربر اصلاً نمیفهمه پشت صحنه چند سرور داره کار میکنه؛ فقط یک آدرس یا دامنه میبینه.

نتیجه‌ی این تغییر دو چیزه: اول، دیگه سقف فیزیکی وجود نداره — هر وقت ترافیک بالا رفت، یک سرور دیگه اضافه میکنی. دوم، دیگه نقطه‌ی شکست واحد نداری؛ اگه یکی از سرورها بخوابه، لود بالانسر خودش میفهمه و ترافیک رو به بقیه‌ی سرورها هدایت میکنه، بدون این‌که کاربر چیزی متوجه بشه.

لایه‌ی ۴ در مقابل لایه‌ی ۷

لود بالانسرها معمولاً روی دو سطح کار میکنن. لود بالانسر لایه‌ی ۴ (L4) روی سطح transport کار میکنه، یعنی فقط IP و پورت رو میبینه و بدون این‌که وارد محتوای درخواست بشه، ترافیک رو پخش میکنه. سریع‌تره چون کار کمتری انجام میده، ولی هوشمندی کمتری داره.

لود بالانسر لایه‌ی ۷ (L7) روی سطح application کار میکنه، یعنی میتونه محتوای درخواست HTTP رو ببینه — مسیر URL، هدرها، کوکی‌ها — و بر همون اساس تصمیم بگیره کدوم سرور جواب بده. مثلاً میتونی بگی درخواست‌هایی که مسیرشون با /api شروع میشه برن سراغ یک گروه از سرورها، و درخواست‌های /static برن سراغ یک گروه دیگه. این انعطاف هزینه داره: کمی کندتر از L4 است چون باید محتوای درخواست رو parse کنه.

الگوریتم‌های تقسیم ترافیک

خود لود بالانسر باید یک قانون داشته باشه که تصمیم بگیره کدوم درخواست بره کدوم سرور. رایج‌ترینش round robin است: درخواست‌ها به‌ترتیب و دوره‌ای بین سرورها پخش میشن، بدون توجه به این‌که هر سرور الان چقدر مشغوله. ساده‌ست ولی همیشه منصفانه نیست — اگه یک درخواست سنگین‌تر باشه و روی یک سرور طول بکشه، اون سرور همچنان درخواست بعدی رو هم میگیره.

الگوریتم least connections این مشکل رو حل میکنه: درخواست جدید میره سراغ سروری که الان کمترین تعداد کانکشن فعال رو داره. برای ترافیکی که مدت زمان پردازش درخواست‌هاش خیلی متفاوته (مثلاً بعضی درخواست‌ها سریع جواب داده میشن، بعضی طول میکشن)، این روش عادلانه‌تره.

یک روش دیگه IP hash است، جایی که بر اساس IP کاربر یک هش محاسبه میشه و همیشه همون کاربر به همون سرور فرستاده میشه. این برای موقعیت‌هایی مفیده که session کاربر روی یک سرور خاص ذخیره شده و باید همیشه همون کاربر به همون سرور برسه — البته این خودش یک ضعف داره، چون اگه توزیع IP ها نامتوازن باشه، بعضی سرورها بیشتر از بقیه بار میگیرن.

ابزارهای واقعی

اگه بخوای خودت یک لود بالانسر راه بندازی، دو تا از رایج‌ترین گزینه‌ها Nginx و HAProxy هستن. یک تنظیم ساده‌ی Nginx برای پخش ترافیک بین سه سرور backend این‌شکلیه:

upstream backend_servers {
    least_conn;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend_servers;
    }
}

اینجا least_conn مشخص میکنه از الگوریتم least connections استفاده بشه، و هر درخواستی که به این سرور برسه بین این سه backend پخش میشه. HAProxy هم همین کار رو میکنه، ولی معمولاً برای سناریوهای پیچیده‌تر و ترافیک بسیار بالا انتخاب اول خیلی از تیم‌هاست، چون از ابتدا برای همین کار طراحی شده و قابلیت‌های health check و مانیتورینگش قوی‌تره.

اگه نخوای خودت این لایه رو مدیریت کنی، سرویس‌های ابری مثل Cloudflare یک لود بالانسر مدیریت‌شده ارائه میدن که هم روی لایه‌ی DNS کار میکنه (یعنی حتی قبل از رسیدن ترافیک به سرورهات، بین چند دیتاسنتر جغرافیایی پخشش میکنه) و هم health check خودکار داره؛ اگه یک سرور جواب نده، خودش به‌صورت خودکار از چرخه خارجش میکنه تا برگرده.

چند لود بالانسر مرجع که ارزش دیدن دارن

تا اینجا از Nginx و HAProxy و Cloudflare گفتم، ولی چند تا نمونه‌ی دیگه هست که دیدنشون برای یک مهندس نرم‌افزار واقعاً جالبه، چون هرکدوم یک مسئله‌ی متفاوت رو حل کردن.

اولیش Maglev گوگله. این یک لود بالانسر لایه‌ی ۴ نرم‌افزاریه که گوگل خودش ساخته و یک مقاله‌ی مهندسی دربارش منتشر کرده. نکته‌ی جالب Maglev اینه که به‌جای این‌که از هش معمولی استفاده کنه، از یک روش به اسم consistent hashing با یک جدول lookup بزرگ استفاده میکنه. چرا این مهمه؟ چون وقتی یک backend اضافه یا حذف میشه، اکثر connection های موجود همچنان به همون backend قبلی‌شون میرسن — فقط بخش کوچیکی جابه‌جا میشه. توی یک لود بالانسر ساده با mod hash، اضافه‌کردن یا حذف یک سرور میتونه تقریباً همه‌ی mapping ها رو به‌هم بریزه، که برای سرویس‌هایی که connection طولانی دارن فاجعه‌ست.

دومی Envoy Proxy است، که اولین بار توی Lyft ساخته شد و بعد به یکی از پروژه‌های اصلی CNCF تبدیل شد. چیزی که Envoy رو خاص میکنه اینه که configuration اش رو میشه به‌صورت داینامیک و از راه دور، از طریق یک API به اسم xDS، تغییر داد، بدون این‌که پروسه ری‌استارت بشه. این دقیقاً همون چیزیه که service mesh هایی مثل Istio بهش نیاز دارن: هزاران sidecar proxy که هر کدوم کنار یک سرویس نشستن، و باید بتونن بدون downtime روت‌ها و policy هاشون رو آپدیت کنن.

سومی AWS است، که سه نوع لود بالانسر جدا ارائه میده و این خودش نشونه‌ی خوبیه که چرا تفکیک لایه‌ها مهمه: Network Load Balancer (NLB) برای لایه‌ی ۴ با تأخیر خیلی پایین، Application Load Balancer (ALB) برای لایه‌ی ۷ با قابلیت‌هایی مثل روتینگ بر اساس path یا host header، و Classic Load Balancer که نسخه‌ی قدیمی‌تر و امروز کمتر توصیه‌شده‌ست. این‌که AWS این سه رو جدا از هم نگه داشته، نشون میده هیچ لود بالانسر «همه‌کاره» ای وجود نداره؛ انتخاب درست کاملاً به نوع ترافیک بستگی داره.

چهارمی که شاید برای خیلی از دولوپرها آشناتر باشه، خود Kubernetes است. سرویس‌های Kubernetes از یک کامپوننت به اسم kube-proxy استفاده میکنن که روی هر node اجرا میشه و ترافیک بین Pod ها رو با استفاده از iptables یا IPVS پخش میکنه. جالبیش اینه که این لود بالانسینگ کاملاً decentralized اتفاق میفته — هیچ نقطه‌ی مرکزی وجود نداره که همه‌ی ترافیک از اون رد بشه، برخلاف مدل سنتی که یک لود بالانسر جلوی همه‌چیز میشینه.

اگه بخوای یک قدم عمیق‌تر بری، سراغ Linux Virtual Server (LVS) و IPVS برو — این تکنولوژی مستقیم توی kernel لینوکس پیاده‌سازی شده و پایه‌ی خیلی از لود بالانسرهای بزرگ‌مقیاس دنیا از جمله همون kube-proxy است. چیزی که این رو برای یک مهندس جالب میکنه اینه که کل تصمیم‌گیری لود بالانسینگ توی kernel space اتفاق میفته، نه توی user space، که یعنی سربار (overhead) به‌شدت پایین‌تر از یک reverse proxy معمولیه.

نکته‌ای که معمولاً فراموش میشه

چیزی که خیلی از تیم‌ها موقع راه‌اندازی اولین لود بالانسرشون فراموش میکنن، مدیریت session است. اگه اپلیکیشنت session رو توی حافظه‌ی خود سرور نگه میداره (نه توی یک دیتابیس یا Redis مشترک)، وقتی کاربر بار دوم درخواست بده و لود بالانسر اون رو به یک سرور دیگه بفرسته، session گم میشه و کاربر عملاً logout میشه. راه‌حل درست این نیست که مجبور کنی کاربر همیشه به همون سرور برسه (چون این کار انعطاف لود بالانسر رو از بین میبره)؛ راه‌حل درست اینه که session رو از حافظه‌ی محلی سرور جدا کنی و توی یک storage مشترک بین همه‌ی سرورها نگه داری. این یکی از اون نکاتیه که تا خودت باهاش برخورد نکنی، احتمالاً موقع طراحی اولیه بهش فکر نمیکنی.