لود بالانسر: از یک سرور تنها تا چیزی که هیچوقت 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 مشترک بین همهی سرورها نگه داری. این یکی از اون نکاتیه که تا خودت باهاش برخورد نکنی، احتمالاً موقع طراحی اولیه بهش فکر نمیکنی.
