طرح‌چه

مدیریت ترافیک | Nginx from scratch

۱۵ تیر ۱۴۰۴

مدیریت ترافیک | Nginx from scratch
در این قسمت یاد میگیریم چطور می توان از nginx برای کنترل و مدیریت ترافیک استفاده کرد.

با استفاده از nginx بر اساس یکسری پارامتر می توان جریانِ درخواست های ورودی را کنترل و یا به مسیر های مختلف هدایت کرد. در این قسمت یکسری از روش ها برای مدیریت و کنترل درخواست ها آشنا می شویم.

تست A/B یا A/B testing

فرض کنید دو یا چندین نسخه از یک اپلیکیشن را ساخته اید یا اینکه تغییری در اپلیکیشن داده اید و میخواهید ابتدا آن را توسط درصد کمی از کاربرها تست کنید و ببینید که آیا تغییرات به درستی کار میکنند یا اینکه تغییرات چیزی هستند که کاربردی باشند یا نه! در این حالت به نسخه فعلی اپلیکیشن A و به نسخه جدیدی که شامل تغییرات است B میگوییم. 

در A/B Testing، معمولاً بخش عمده‌ای از ترافیک (مثلاً 90٪) به نسخه A و بخش کوچکی (مثلاً 10٪) به نسخه B هدایت می‌شود. با مانیتورینگ (monitoring) رفتار کاربران و تحلیل داده‌ها در نسخه B، می‌توان بررسی کرد که آیا تغییرات عملکرد مورد انتظار را دارند یا خیر.

اگر نسخه B نتایج مثبت داشت، می‌توان ترافیک بیشتری به سمت آن هدایت کرد یا آن را به نسخه اصلی تبدیل نمود. در غیر این صورت، تغییرات به راحتی قابل بازگشت هستند. به این فرآیند A/B Testing گفته می‌شود که هدف آن ارزیابی عملی و کنترل‌شده تغییرات در محصول پیش از اعمال گسترده آن برای تمام کاربران است.

split_clients "basket-rollout-${remote_addr}" $basket_rollout {
	20% "backendv2"
	* "backendv1"
}

در Nginx با استفاده از split_clients میتوان ترافیک ورودی را به چندین قسمت تقسیم کرد. این دستور دو پارامتر میگیرد. پارامتر اول یک پارامتر متنی است که بر اساس آن ترافیک تقسیم میشود و پارامتر دوم یک آبجکت است که نحوه تقسیم شدن ترافیک را مشخص میکند. در مثال بالا ما ترافیک را بر اساس IP کاربران تقسیم میکنیم. به ابتدای آیپی کلمه ای که خواستیم رو اضافه کردیم. این بدین معنی است که شما میتوانید به پارامتر اول متن یا متغیرهایی را بر حسب نیاز اضافه کنید. با توجه به پارامتر دوم 20 درصد از ترافیک به backendv2 و باقی ترافیک به backendv1 ارسال میشود. 

دستور split_clients بدین صورت عمل میکند که به ازای هر درخواست با استفاده از پارامتر اول یک مقدار عددی که مقدار آن میتواند بین 0 تا 100 باشد ساخته می شود. سپس با توجه به پارامتر دوم این ترافیک بین سرورهایی که مشخص کرده ایم تقسیم میشود. 

  • نکته تکمیلی: برای ساخته شدن مقدار عددی در حال حاضر ابتدا از تابع هش CRC32 استفاده میشود و سپس نتیجه آن به عددی در بازه 0-100 نرمال سازی می شود.

فرض کنید دو نسخه از فایل index.html داریم و میخواهیم ترافیک خود را بین این دو نسخه تقسیم کنیم:

split_clients "${remote_addr}" $site_root_folder {
  33.3% "/var/www/sitev2/";
  * "/var/www/sitev1/";
}

server {
  listen 80 _;
  root $site_root_folder;
  
  location / {
    index index.html;
  }
}

همچنین میتوان دو سرور مختلف داشت و ترافیک را با proxy_pass بین آنها تقسیم کرد.

split_clients "${remote_addr}" $ab_testing {
  33.3% "localhost:8080";
  * "localhost:8081";
}

server {
  listen 80;
  
  location / {
    proxy_pass $ab_testing
  }
}

میتوان از split_clients برای استقرار (deploy) به روش Blue/Green یا Canary استفاده کرد.

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

  • Canary: در این روش, نسخه شامل تغییرات, ابتدا به بخش کوچکی از کاربران ارائه می‌شود تا عملکرد و پایداری آن در محیط واقعی به دقت مانیتور و ارزیابی شود. در صورت موفقیت‌آمیز بودن، به تدریج درصد بیشتری از ترافیک به نسخه جدید هدایت می‌شود تا نهایتاً جایگزین نسخه قدیمی شود.

محدود کردن تعداد کانکشن ها (connection limit)

برای محدود کردن تعداد کانکشن های فعال از طرف هر کلاینت, میتوان از دستور limit_conn‍ استفاده کرد. این دستور به همراه دستور های limit_conn_zone و limit_conn_status مورد استفاده قرار میگیرد.

limit_conn_zone $binary_remote_addr zone=limitbyaddr:10m;
limit_conn_status 429;

server {
  limit_conn limitbyaddr 40;
}

دستور limit_conn_zone یک فضای حافظه اشتراکی برای نگهداری اطلاعات مربوط به کانکشن‌های فعال تعریف می‌کند. همچنین در این دستور مشخص می‌شود که شمارش کانکشن‌ها بر اساس چه کلیدی انجام شود؛ برای مثال می‌توان از IP کاربر یا شناسه جلسه (Session ID) به‌عنوان کلید استفاده کرد.

در اینجا از متغیر $binary_remote_addr استفاده شده که آدرس IP کلاینت را به‌صورت دودویی (باینری) در نظر می‌گیرد تا فضای کمتری مصرف شود. با گزینه zone=limitbyaddr نام ناحیه حافظه را تعیین می‌کنیم و اندازه آن را برابر با ۱۰ مگابایت در نظر گرفته‌ایم.

دستور limit_conn_status مشخص می‌کند که در صورتی که تعداد کانکشن‌های یک کلاینت از حد مجاز فراتر رفت، چه کد وضعیت HTTP به او بازگردانده شود. در این مثال از کد 429 (Too Many Requests) استفاده شده که نشان‌دهنده تعداد بیش از حد مجاز درخواست‌ها است.

در نهایت با استفاده از دستور limit_conn محدودیت اعمال می‌شود. در این دستور ابتدا نام ناحیه حافظه‌ای که برای مدیریت کانکشن‌ها تعریف شده است (در اینجا limitbyaddr) را وارد می‌کنیم و سپس حداکثر تعداد کانکشن مجاز برای هر کلاینت را تعیین می‌کنیم که در این مثال 40 کانکشن همزمان است.

دستورات limit_conn و limit_conn_status را میتوان درون بلاک های http , stream و ‍location بکار برد در حالی که دستور limit_conn_zone را تنها درون بلاک http میتوان استفاده کرد.

به صورت پیش فرض مقدار limit_conn_status برابر با 503 (Service Unavailable) است.

محدود کردن نرخ درخواست ها (rate limit)

برای محدود کردن نرخ درخواست های هر کلاینت, میتوان از دستور limit_req استفاده کرد. این دستور به همراه limit_req_zone و limit_req_status مورد استفاده قرار میگیرد.

limit_req_zone $binary_remote_addr zone=limitbyaddr:10m rate=3r/s;
limit_req_status 429;

server {
  limit_req zone=limitbyaddr;
}

در مثال بالا به اندازه 10MB حافظه برای نگهداری اطلاعات مربط با rate limit در نظر گرفتیم. همچنین محدودیت نرخ درخواست ها بر اساس آیپی کاربران خواهد بود که به صورت باینری درون حافظه نگهداری خواهد شد. همچنین مشخص کردیم کاربر 3 درخواست در هر ثانیه میتواند داشته باشد و اگر از این مقدار بیشتر شود به باقی درخواست ها با استاتوس 429 پاسخ داده خواهد شد.

در دستور limit_req مقدار zone به صورت اجباری باید حتما مشخص شود. مقادیر دیگری مانند burst , delay و nodelay هستند که در صورت نیاز میتوانند مورد استفاده قرار بگیرند.

limit_req_zone $binary_remote_addr zone=limitbyaddr:10m rate=3r/s;
limit_req_status 429;

server {
  location / {
    limit_req zone=limitbyaddr burst=12 delay=9;
  }
}
  • burst=12 : این مقدار تعیین می‌کند که یک آی‌پی مجاز است تا چه تعداد درخواست اضافی (فراتر از نرخ مجاز) را به صورت پشت سر هم (بدون رد شدن فوری) ارسال کند قبل از اینکه Nginx شروع به رد درخواست‌ها کند. در اینجا، عدد 12 یعنی هر کاربر با آی‌پی خود می‌تواند در یک بازه کوتاه تا 12 درخواست اضافه بفرستد که موقتا در صف می‌مانند.

  • delay=9 : این یعنی اولین 9 درخواست اضافه (از همان burst) به صورت تأخیری پردازش می‌شوند (یعنی با توجه به نرخ ۳ درخواست بر ثانیه، به آرامی وارد می‌شوند). اما اگر تعداد درخواست‌های پشت صف بیشتر از 9 شود (یعنی به درخواست‌های 10، 11 و 12 برسیم)، این درخواست‌ها بدون تأخیر قبول می‌شوند (اگرچه باز هم در محدودیت burst هستند). اگر بیشتر از ۱۲ درخواست اضافه شوند (یعنی burst تمام شود)، درخواست‌ها با خطای 429 Too Many Requests رد خواهند شد.

اگر از nodelay استفاده کنیم درخواست‌ها پشت هم و سریع قبول می‌شن تا وقتی که ظرفیت burst پر شود. فقط وقتی تعداد درخواست‌ها بیشتر از burst شود، درخواست‌ها با خطای 429 رد خواهند شد.

limit_req_zone $binary_remote_addr zone=limitbyaddr:10m rate=3r/s;
limit_req_status 429;

server {
  location / {
    limit_req zone=limitbyaddr burst=12 delay=9;
  }
}

برای درک بهتر حالت burst به همراه delay و nodelay میتوانید از نمودار زیر استفاده کنید. این نمودار 7 ثانیه از تعداد درخواست های قبول شده و ریجکت شده ی کلاینتی که به سمت سرور در هر ثانیه ۲۰ درخواست ارسال میکند را نشان میدهد:

nginx-rate-limiting-burst-delay-vs-no-delay
مقایسه burst در حالت delay و nodelay

همانطور که میبنیم وقتی burst=12 است, اگر nodelay باشد در ثانیه اول به ۱۲ درخواست پاسخ داده میشود و در ثانیه های بعدی به ۳ درخواست در هر ثانیه پاسخ داده میشود و با تمام شدن درخواست ها نرخ پاسخ نیز صفر می شود.

اما در حالتی که delay=9 استفاده شود در ثانیه اول به ۳ درخواست پاسخ داده میشود و 9 درخواست دیگر به درون صف برده میشوند. نرخ پاسخ دهی همیشه 3 می ماند. چون درخواست ها به درون صف برده میشوند زمانی که دیگر کاربر درخواست جدیدی ارسال نکند همچنان nginx تا 3 ثانیه بعد به درخواست هایی که درون صف قرار داده نیاز است تا پاسخ دهد.

  1. در حالت nodelay

    • شما یک‌بار می‌توانید تا burst=12 درخواست را یک‌جا (instant) بپذیرید.

    • به محض مصرف این 12 توکن، حساب‌گر (token bucket) «خالی» می‌شود و از آن به بعد فقط با نرخ rate=3r/s توکن جدید اضافه می‌کند.

    • بنابراین در ثانیهٔ اول 12 درخواست پردازش (instant) می‌شوند، اما در ثانیهٔ دوم و سوم هرکدام فقط 3 تا (به اندازهٔ سرعت refill) قابل پردازش هستند. هیچ‌وقت نمی‌توانید پشت سر هم سه بار 12 درخواست instant بفرستید مگر اینکه بین آن‌ها حداقل 4 ثانیه صبر کنید تا توکن‌ها دوباره پر شوند.

  2. در حالت delay

    • هیچ پردازشی فراتر از rate=3r/s انجام نمی‌شود.

    • تا burst=12 درخواست اول «تأخیر» داده و به صف داخلی NGINX می‌روند، ولی باز هم پاسخ‌ها با همان نرخ 3r/s خارج می‌شوند.

    • بنابراین آن 9 درخواستی که «در صف» مانده‌اند، به‌تدریج (3 تا در ثانیه) پاسخ داده می‌شوند و نه یک‌جای 9 تای یکهو.

خلاصه اینکه:

  • حالت nodelay تنها یک‌بار اجازهٔ burst می‌دهد، بعد از آن refill با rate انجام می‌شود.

  • حالت delay کلاً با نرخ rate پاسخ می‌دهد و burst فقط اندازهٔ queue برای تاخیر است، نه اینکه پاسخ‌ها را یکجا ارسال کند.

محدود کردن پهنای باند (bandwidth limit)

برای ایجاد محدودیت پهنای باند میتوان از ترکیب دستورات limit_rate_after و limit_rate استفاده کرد.

server {
  location /download/ {
    limit_rate_after 10m;
    limit_rate 1m;
  }
}

محدودیت پهنای باند به ازای هر کانکشن اعمال می شود. بدین معنی که اگر کاربر چندین کانکشن داشته باشد محدودیت برای هر کانکشن به صورت مجزا محاسبه خواهد شد.

در مثال بالا, سرعتی که پاسخ به کلاینت ارائه می‌شود، بعد از 10 مگابایت به 1 مگابایت در ثانیه محدود خواهد شد. محدودیت پهنای باند به ازای هر اتصال است، بنابراین می‌توانید در صورت لزوم، علاوه بر محدودیت پهنای باند، محدودیت اتصال نیز اعمال کنید.

بنابراین با توجه به تنظیمات بالا سرعت دانلود تا ۱۰ مگابایت اول نامحدود است. بعد از آن سرعت به ۱ مگابایت بر ثانیه محدود می‌شود. این تنظیمات معمولا به این دلیل اعمال میشود که کاربرانی که فایل‌های خیلی بزرگ دانلود می‌کنند باعث اشغال کل پهنای باند سرور نشوند، اما همچنان کاربران عادی بتوانند با سرعت بالا فایل‌های کوچک را سریع دریافت کنند.


قسمت قبل: لود بالانس | Nginx from scratch

قسمت بعد: کش (cache) | Nginx from scratch

دیدگاه ها