توی این درس یاد میگیری تعداد درخواستهایی که هر کاربر میتواند بفرستد را محدود کنی (rate limiting)، تا یک ربات یا مهاجم نتواند با هزاران درخواست رمز عبور حدس بزند یا سرور را از پا بیندازد. با limit_req_zone و limit_req کار میکنی و میفهمی Nginx با الگوریتم «سطل نشتی» چطور میشمارد، با burst و nodelay برای کاربرهای واقعی جا باز میکنی، صفحهی ورود را به ۵ درخواست در دقیقه محدود میکنی، با limit_conn تعداد اتصالهای همزمان را میبندی، و IP های مورد اعتماد را از محدودیت معاف میکنی.
مسئله: یک نفر، هزار درخواست
Section titled “مسئله: یک نفر، هزار درخواست”سایت تو برای آدمها ساخته شده: یک آدم در ثانیه چند کلیک میکند، نه صدها. ولی اینترنت پر از برنامه است:
- حدس رمز (brute force): رباتی که در دقیقه هزار رمز را روی صفحهی ورود امتحان میکند.
- اسکرپر (scraper): برنامهای که همهی صفحههای فروشگاه را با سرعت هر چه بیشتر کپی میکند.
- حملهی سادهی از کار انداختن: چند ماشین که فقط درخواست سنگین (جستجو، گزارش) میفرستند تا CPU اپ پر شود.
Nginx جلوی اپ ایستاده؛ پس بهترین جاست که بگوید «از این IP بیش از این مقدار نمیپذیرم»، قبل از اینکه درخواست اصلاً به اپ (و دیتابیس) برسد.
تشبیه: سطل نشتی
Section titled “تشبیه: سطل نشتی”Nginx برای هر کاربر (مثلاً هر IP) یک سطل در نظر میگیرد که از ته آن، آب با سرعت ثابت نشت میکند (rate، مثلاً ۲ قطره در ثانیه). هر درخواست یک قطره است که در سطل ریخته میشود:
- اگر سطل جا داشته باشد، قطره قبول است.
- ظرفیت سطل همان
burstاست (پیشفرض: صفر؛ یعنی فقط همان یک قطرهای که در حال نشت است). - اگر سطل پر باشد، قطرهی تازه بیرون میریزد: درخواست رد میشود.
پس rate سرعت بلندمدت را تعیین میکند و burst اینکه یک هجوم کوتاه چقدر تحمل شود.
دو بخش: تعریف zone و استفاده از آن
Section titled “دو بخش: تعریف zone و استفاده از آن”محدودسازی درخواست دو قسمت دارد:
# 1) in http context: define the bucket storelimit_req_zone $binary_remote_addr zone=perip:10m rate=2r/s;
server { location / { # 2) apply it where you need it limit_req zone=perip; }}- کلید (
$binary_remote_addr): بر اساس چه چیزی سطل جدا بسازد.$binary_remote_addrهمان IP است ولی در ۴ بایت (IPv4) بهجای تا ۱۵ کاراکتر؛ حافظهی کمتر. zone=perip:10m: اسم و اندازهی حافظهی مشترک (بین همهی workerها). طبق مستندات Nginx، هر مگابایت حدود ۱۶ هزار IP را نگه میدارد؛ ۱۰ مگابایت یعنی حدود ۱۶۰ هزار.rate=2r/s: سرعت مجاز.r/s(در ثانیه) یاr/m(در دقیقه).
مثالهای عملی
Section titled “مثالهای عملی”این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24، Node.js 18) با کاربر root اجرا شده. دامنهی آزمایشی shop.test در /etc/hosts به 127.0.0.1 اشاره میکند.
مثال ۱: اولین محدودیت، و یک غافلگیری
Section titled “مثال ۱: اولین محدودیت، و یک غافلگیری”یک سایت ساده و محدودیت ۲ درخواست در ثانیه برای هر IP:
mkdir -p /var/www/shop.testecho '<h1>shop</h1>' > /var/www/shop.test/index.htmldd if=/dev/zero of=/var/www/shop.test/big.bin bs=1M count=2 status=nonecat > /etc/nginx/sites-available/shop.test <<'EOF'limit_req_zone $binary_remote_addr zone=perip:10m rate=2r/s;
server { listen 80; server_name shop.test; root /var/www/shop.test;
location / { limit_req zone=perip; }}EOFln -sf /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/; done; echosleep 2echo "--- one request every 0.6s:"for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/; sleep 0.6; done; echonginx: configuration file /etc/nginx/nginx.conf test is successful200 503 503 503 503 503--- one request every 0.6s:200 200 200 200 200 200- پشتسرهم: فقط اولی
200گرفت و بقیه503. شاید انتظار داشتی دو تای اول قبول شوند («۲ در ثانیه»)؛ ولی نشد. دلیلش در پشت پرده است:2r/sیعنی «هر ۵۰۰ میلیثانیه یکی»، نه «هر ثانیه دو تا». - با فاصلهی ۰٫۶ ثانیه: همه
200، چون بین هر دو درخواست بیش از ۵۰۰ میلیثانیه گذشت. - کد پیشفرض رد شدن
503است؛ در مثال بعد عوضش میکنیم.
یک فایل ۲ مگابایتی (big.bin) هم ساختیم که در مثال ۶ لازم میشود.
مثال ۲: 429 بهجای 503، و پیام در لاگ
Section titled “مثال ۲: 429 بهجای 503، و پیام در لاگ”503 Service Unavailable یعنی «سرور مشکل دارد»؛ برای کسی که زیاد درخواست فرستاده، کد درست 429 Too Many Requests است. ابزارهای مانیتورینگ هم بین «سرور خراب است» و «کسی را محدود کردیم» فرق میگذارند:
sed -i 's|^ limit_req zone=perip;| limit_req zone=perip;\n limit_req_status 429;|' /etc/nginx/sites-available/shop.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1: > /var/log/nginx/error.logfor i in 1 2 3; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/; done; echocut -d' ' -f6- /var/log/nginx/error.lognginx: configuration file /etc/nginx/nginx.conf test is successful200 429 429limiting requests, excess: 0.980 by zone "perip", client: 127.0.0.1, server: shop.test, request: "GET / HTTP/1.1", host: "shop.test"limiting requests, excess: 0.966 by zone "perip", client: 127.0.0.1, server: shop.test, request: "GET / HTTP/1.1", host: "shop.test"حالا رد شدنها 429 هستند. هر رد شدن یک خط در error log دارد: limiting requests (رد شد)، excess (چقدر از سقف جلوتر بود؛ در پشت پرده میبینی)، اسم zone و IP کاربر. اگر روزی کاربری گفت «سایت برایم باز نمیشود»، همین خط را با IP اش جستجو کن.
مثال ۳: burst، صف برای هجوم کوتاه
Section titled “مثال ۳: burst، صف برای هجوم کوتاه”یک صفحهی واقعی وقتی باز میشود، مرورگر همزمان چند درخواست میفرستد. با محدودیت سفت مثال ۱، کاربر واقعی هم رد میشود. burst=5 یعنی سطل ۵ جای خالی دارد: درخواستهای اضافه تا ۵ تا در صف میمانند و با همان سرعت rate جلو میروند. ۱۰ درخواست همزمان میفرستیم (xargs -P 10) و برای هر کدام کد و زمان را چاپ میکنیم:
sed -i 's|^ limit_req zone=perip;| limit_req zone=perip burst=5;|' /etc/nginx/sites-available/shop.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1seq 10 | xargs -P 10 -I{} curl -s -o /dev/null -w '%{http_code} after %{time_total}s\n' http://shop.test/ | sort -t' ' -k3nginx: configuration file /etc/nginx/nginx.conf test is successful200 after 0.000660s429 after 0.000687s429 after 0.000719s429 after 0.000839s429 after 0.003209s200 after 0.501748s200 after 1.001038s200 after 1.497334s200 after 1.996407s200 after 2.496513sده درخواست همزمان رسیدند:
- ۱ فوراً جواب گرفت (
200در حدود ۱ میلیثانیه). - ۵ در صف ماندند و با سرعت
rate(هر ۵۰۰ میلیثانیه یکی) جواب گرفتند: بعد از ۰٫۵، ۱، ۱٫۵، ۲ و ۲٫۵ ثانیه. - ۴ تای باقیمانده جا نداشتند و فوراً
429گرفتند.
برای کاربر واقعی، صبر ۲٫۵ ثانیهای برای آخرین فایل صفحه خیلی زیاد است. راهحل در مثال بعد است.
مثال ۴: nodelay، بدون صبر ولی با همان سقف
Section titled “مثال ۴: nodelay، بدون صبر ولی با همان سقف”صف کردن یعنی کاربر صبر میکند. nodelay میگوید درخواستهایی که در burst جا میشوند را فوراً بفرست، ولی جایشان در سطل را نگه دار تا با سرعت rate خالی شود:
sleep 3sed -i 's|burst=5;|burst=5 nodelay;|' /etc/nginx/sites-available/shop.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1seq 10 | xargs -P 10 -I{} curl -s -o /dev/null -w '%{http_code} after %{time_total}s\n' http://shop.test/ | sort | awk '{print $1}' | uniq -cecho "--- right after, one more:"curl -s -o /dev/null -w '%{http_code}\n' http://shop.test/sleep 1echo "--- 1s later, three more:"for i in 1 2 3; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/; done; echonginx: configuration file /etc/nginx/nginx.conf test is successful 6 200 4 429--- right after, one more:429--- 1s later, three more:200 200 429- همان ۱۰ درخواست همزمان: ۶ تا (۱ +
burst=5) فوراً200گرفتند، بدون صف و تأخیر؛ ۴ تا429. - درخواست بعدی، بلافاصله:
429. سطل هنوز پر است؛nodelayفقط صبر را حذف کرد، نه سقف را. - یک ثانیه بعد، سه درخواست: دو تا قبول (در یک ثانیه با
2r/sدو جا خالی شد)، سومی رد.
پس burst ... nodelay برای صفحههای واقعی رفتار طبیعی دارد: هجوم کوتاه (یک صفحه با چند فایل) فوراً جواب میگیرد، ولی کسی که مدام با سرعت بالا درخواست بفرستد، به سقف rate میخورد. برای حالت بینابین، delay=N هم هست: N تای اول فوراً و بقیهی burst در صف (جدول مرجع).
مثال ۵: صفحهی ورود، ۵ تلاش در دقیقه
Section titled “مثال ۵: صفحهی ورود، ۵ تلاش در دقیقه”حالا تمرین اصلی درس. یک اپ ورود ساده (Node) پشت Nginx؛ بقیهی سایت آزاد، فقط /login محدود:
// fake login endpoint: always "wrong password"const http = require('http');http.createServer((req, res) => { res.writeHead(req.method === 'POST' ? 401 : 200, { 'Content-Type': 'text/plain' }); res.end(req.method === 'POST' ? 'wrong username or password\n' : 'login form\n');}).listen(9421, '127.0.0.1');(exec node /srv/lx-login/app.js) > /dev/null 2>&1 &cat > /etc/nginx/sites-available/shop.test <<'EOF'limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;limit_req_status 429;
server { listen 80; server_name shop.test; root /var/www/shop.test;
location / { limit_req zone=perip burst=20 nodelay; }
# 5 attempts per minute per IP: 1 now + 4 in the bucket, then 1 every 12s location = /login { limit_req zone=login burst=4 nodelay; proxy_pass http://127.0.0.1:9421; include proxy_params; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "=== attacker, 7 tries:"for i in 1 2 3 4 5 6 7; do curl -s -o /dev/null -w "try $i: %{http_code}\n" -X POST -d 'user=ali&pass=123456' http://shop.test/logindoneecho "=== same IP, home page:"curl -s -o /dev/null -w '%{http_code}\n' http://shop.test/echo "=== another user (127.0.0.2), login:"curl -s -w '[%{http_code}]\n' -X POST -d 'user=sara&pass=x' --interface 127.0.0.2 http://shop.test/loginecho "=== attacker, 13s later:"sleep 13for i in 1 2; do curl -s -o /dev/null -w '%{http_code} ' -X POST http://shop.test/login; done; echonginx: configuration file /etc/nginx/nginx.conf test is successful=== attacker, 7 tries:try 1: 401try 2: 401try 3: 401try 4: 401try 5: 401try 6: 429try 7: 429=== same IP, home page:200=== another user (127.0.0.2), login:wrong username or password[401]=== attacker, 13s later:401 429خطبهخط:
- دو zone جدا:
peripبا سقف بزرگ برای کل سایت (10r/s،burst=20)، وloginفقط برایlocation = /loginباrate=5r/m(یعنی یکی هر ۱۲ ثانیه). burst=4 nodelay: پنج تلاش اول (۱ + ۴) فوراً به اپ رسیدند (اپ401«رمز اشتباه» داد)، تلاش ۶ و ۷ را Nginx با429رد کرد؛ اپ اصلاً آنها را ندید.- همان IP، صفحهی اصلی:
200. محدودیت ورود روی بقیهی سایت اثری ندارد. - کاربر دیگر (
127.0.0.2):401معمولی؛ هر IP سطل خودش را دارد. - ۱۳ ثانیه بعد: یک جای خالی (یکی هر ۱۲ ثانیه) و دقیقاً یک تلاش دیگر قبول شد. پس مهاجم در بلندمدت حداکثر ۵ رمز در دقیقه امتحان میکند، یعنی ۷۲۰۰ در روز بهجای چند میلیون؛ و تو در لاگها میبینیاش.
مثال ۶: limit_conn، تعداد اتصال همزمان
Section titled “مثال ۶: limit_conn، تعداد اتصال همزمان”limit_req سرعت درخواستها را میشمارد. ولی یک دانلود ۲ گیگابایتی یک درخواست است که ده دقیقه طول میکشد. اگر کسی ۵۰ دانلود همزمان باز کند، با limit_req جلویش گرفته نمیشود. limit_conn تعداد درخواستهای همزمان در حال پردازش برای هر کلید را محدود میکند. برای اینکه دانلودها طول بکشند، سرعت هر کدام را هم با limit_rate (درس عملکرد) پایین میآوریم:
cat > /etc/nginx/sites-available/shop.test <<'EOF'limit_conn_zone $binary_remote_addr zone=conns:10m;limit_conn_status 429;
server { listen 80; server_name shop.test; root /var/www/shop.test;
location /big.bin { limit_conn conns 2; limit_rate 1m; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1: > /var/log/nginx/error.logseq 4 | xargs -P 4 -I{} curl -s -o /dev/null -w '%{http_code} %{size_download} bytes after %{time_total}s\n' http://shop.test/big.bin | sortcut -d' ' -f6- /var/log/nginx/error.log | cut -d, -f1 | sort | uniq -cnginx: configuration file /etc/nginx/nginx.conf test is successful200 2097152 bytes after 1.002174s200 2097152 bytes after 1.002437s429 178 bytes after 0.000657s429 178 bytes after 0.000797s 2 limiting connections by zone "conns"از چهار دانلود همزمان، دو تا کامل شدند (هر کدام ۲ مگابایت، حدود ۱ ثانیه) و دو تای دیگر فوراً 429 گرفتند. error log هم دو خط limiting connections by zone "conns" دارد. (دانلود ۲ مگابایتی با limit_rate 1m حدود یک ثانیه طول کشید، نه دو؛ چون Nginx سهم ثانیهی اول را بلافاصله میفرستد، همان که در درس عملکرد دیدیم.)
limit_conn فقط درخواستهایی را میشمارد که الان در حال پردازشاند؛ بعد از تمام شدن دانلودها، دوباره میشود دو دانلود دیگر شروع کرد. با HTTP/2، چند درخواست روی یک اتصال TCP هم هر کدام جدا شمرده میشوند.
مثال ۷: لیست سفید و حالت آزمایشی
Section titled “مثال ۷: لیست سفید و حالت آزمایشی”دو نیاز رایج دیگر:
- IP های مورد اعتماد (دفتر شرکت، سرور مانیتورینگ) نباید محدود شوند.
- قبل از روشن کردن محدودیت روی سایت واقعی، میخواهی ببینی چه کسانی رد میشدند، بدون اینکه واقعاً ردشان کنی.
برای اولی از یک نکته استفاده میکنیم: اگر کلید خالی باشد، Nginx آن درخواست را نمیشمارد. با geo (مقدار بر اساس IP) و map (تبدیل مقدار)، کلید را برای IP های مورد اعتماد خالی میکنیم. برای دومی، limit_req_dry_run on;:
cat > /etc/nginx/sites-available/shop.test <<'EOF'# 0 = trusted, 1 = everyone elsegeo $limited { default 1; 127.0.0.2 0; 10.0.0.0/8 0;}# empty key = not countedmap $limited $limit_key { 0 ""; 1 $binary_remote_addr;}limit_req_zone $limit_key zone=perip:10m rate=1r/s;limit_req_status 429;
server { listen 80; server_name shop.test; root /var/www/shop.test;
location / { limit_req zone=perip; } location /preview/ { alias /var/www/shop.test/; limit_req zone=perip; limit_req_dry_run on; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1: > /var/log/nginx/error.logfor ip in 127.0.0.1 127.0.0.2; do printf '%-10s ' "$ip" for i in 1 2 3 4; do curl -s -o /dev/null -w '%{http_code} ' --interface "$ip" http://shop.test/; done; echodonesleep 2printf '%-10s ' 'dry run'for i in 1 2 3 4; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/preview/; done; echogrep -c 'dry run' /var/log/nginx/error.loggrep 'dry run' /var/log/nginx/error.log | head -n 1 | cut -d' ' -f6- | cut -d, -f1-2nginx: configuration file /etc/nginx/nginx.conf test is successful127.0.0.1 200 429 429 429127.0.0.2 200 200 200 200dry run 200 200 200 2003limiting requests, dry run127.0.0.1: کاربر عادی،1r/sبدونburst: فقط اولی200.127.0.0.2: درgeoمقدار0دارد، پسmapکلیدش را خالی ("") کرد و هیچوقت شمرده نشد: هر چهار200.dry run: location/preview/همان محدودیت را دارد، ولی همه200گرفتند. در عوض، ۳ خطlimiting requests, dry runدر لاگ آمد: یعنی «اگر محدودیت روشن بود، این ۳ درخواست رد میشدند». روش امن روشن کردن محدودیت روی سایت واقعی: چند روزdry_run، بررسی لاگ (چه کسانی و چقدر رد میشدند)، تنظیمrateوburst، و بعد خاموش کردنdry_run.
geo شبیه map است ولی کلیدش IP است و محدوده (مثل 10.0.0.0/8) میفهمد.
پشت پرده: Nginx دقیقاً چطور میشمارد؟
Section titled “پشت پرده: Nginx دقیقاً چطور میشمارد؟”دو نکته از مثال ۱ شاید عجیب بود: با rate=2r/s، دو درخواست پشتسرهم قبول نشد. چون Nginx «۲ درخواست در هر ثانیهی ساعت» نمیشمارد؛ با دقت میلیثانیه حساب میکند: 2r/s یعنی هر ۵۰۰ میلیثانیه یک درخواست. برای هر کلید فقط دو عدد نگه میدارد: زمان آخرین درخواست و «اضافه» (excess، چقدر سطل پر است). در هر درخواست:
- به اندازهی زمان گذشته از آخرین درخواست، از
excessکم میکند (نشت). - یک درخواست اضافه میکند.
- اگر
excessازburstبیشتر شود، رد میکند.
همان عدد excess در لاگ مثال ۲ بود. ببینیم با فاصلههای مختلف چطور تغییر میکند:
cat > /etc/nginx/sites-available/shop.test <<'EOF'limit_req_zone $binary_remote_addr zone=bucket:10m rate=2r/s;limit_req_status 429;
server { listen 80; server_name shop.test; root /var/www/shop.test;
# delays are logged one level below refusals (warn); Ubuntu's default level is error error_log /var/log/nginx/error.log warn;
location / { limit_req zone=bucket burst=2; } location /fast { return 200 "return runs before limit_req\n"; limit_req zone=bucket; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1: > /var/log/nginx/error.logpids=""for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code} after %{time_total}s\n' http://shop.test/ & pids="$pids $!"; sleep 0.05donewait $pidsgrep -o 'delaying request, excess: [0-9.]*\|limiting requests, excess: [0-9.]*' /var/log/nginx/error.logecho "--- location with return:"for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/fast; done; echonginx: configuration file /etc/nginx/nginx.conf test is successful200 after 0.001077s429 after 0.000932s429 after 0.000797s200 after 0.448859s200 after 0.894418sdelaying request, excess: 0.894delaying request, excess: 1.784limiting requests, excess: 2.686limiting requests, excess: 2.582--- location with return:200 200 200 200 200 200پنج درخواست با فاصلهی ۵۰ میلیثانیه رسیدند (rate=2r/s، یعنی در هر ۵۰ میلیثانیه ۰٫۱ از سطل نشت میکند؛ burst=2):
| درخواست | زمان رسیدن | محاسبهی excess |
نتیجه |
|---|---|---|---|
| ۱ | ۰ | سطل خالی | فوراً 200 |
| ۲ | ۵۰ms | 0 − 0.1 + 1 = 0.9 | ≤ 2، در صف: 0.9 × 500ms ≈ ۴۵۰ms صبر |
| ۳ | ۱۰۰ms | 0.9 − 0.1 + 1 = 1.8 | ≤ 2، در صف: ≈ ۹۰۰ms |
| ۴ | ۱۵۰ms | 1.8 − 0.1 + 1 = 2.7 | > 2، 429 |
| ۵ | ۲۰۰ms | 1.8 − 0.2 + 1 = 2.6 | > 2، 429 (درخواست رد شده در سطل حساب نمیشود) |
عددهای لاگ (0.894، 1.784، 2.686، 2.582) و زمانهای خروجی (0.448s و 0.894s) دقیقاً همیناند (کمی کمتر، چون فاصلهها کمی بیشتر از ۵۰ میلیثانیه بود). خروجی به ترتیب تمام شدن چاپ شده، برای همین ردشدهها قبل از صفشدهها آمدهاند. تأخیرها با پیام delaying request یک سطح پایینتر (warn) لاگ میشوند؛ برای همین در این server گفتیم error_log ... warn;، چون سطح پیشفرض Ubuntu error است.
و location /fast: شش درخواست پشتسرهم، همه 200؛ با اینکه limit_req دارد. دلیلش در کادر زیر است.
جدول مرجع
Section titled “جدول مرجع”| directive | context | معنی |
|---|---|---|
limit_req_zone KEY zone=NAME:SIZE rate=R; |
http |
تعریف سطلها (r/s یا r/m) |
limit_req zone=NAME; |
http، server، location |
اعمال؛ بدون burst یعنی هیچ هجومی |
limit_req zone=NAME burst=N; |
تا N درخواست در صف (با سرعت rate) |
|
limit_req zone=NAME burst=N nodelay; |
تا N درخواست فوراً، بعد سقف | |
limit_req zone=NAME burst=N delay=M; |
M تای اول فوراً، بقیهی burst در صف | |
limit_req_status 429; |
کد رد (پیشفرض 503) |
|
limit_req_dry_run on; |
فقط لاگ، بدون رد (Nginx ۱.۱۷.۱ به بعد) | |
limit_req_log_level warn; |
سطح لاگ ردها (پیشفرض error؛ تأخیرها یک سطح پایینتر) |
|
limit_conn_zone KEY zone=NAME:SIZE; |
http |
تعریف شمارندهی اتصال |
limit_conn NAME N; |
http، server، location |
حداکثر N درخواست همزمان برای هر کلید |
limit_conn_status 429; |
کد رد (پیشفرض 503) |
|
limit_rate 1m; |
سرعت هر پاسخ (نه تعداد) |
کلیدهای رایج:
| کلید | یعنی هر سطل برای | کاربرد |
|---|---|---|
$binary_remote_addr |
هر IP | پیشفرض |
$server_name |
کل سایت | سقف کل درخواستها به یک سایت |
$http_x_api_key |
هر کلید API | محدودیت هر مشتری API |
$limit_key (از map) |
هر IP بهجز لیست سفید | معافیت IP های مورد اعتماد |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) limit_req_zone در جای اشتباه
Section titled “۱) limit_req_zone در جای اشتباه”cp /etc/nginx/sites-available/shop.test /root/shop.test.baksed -i 's|^ root /var/www/shop.test;| root /var/www/shop.test;\n limit_req_zone $binary_remote_addr zone=bad:1m rate=1r/s;|' /etc/nginx/sites-available/shop.testnginx -tcp /root/shop.test.bak /etc/nginx/sites-available/shop.test2026/10/04 14:17:36 [emerg] 38994#38994: "limit_req_zone" directive is not allowed here in /etc/nginx/sites-enabled/shop.test:8nginx: configuration file /etc/nginx/nginx.conf test failedlimit_req_zone و limit_conn_zone فقط در http (یعنی بیرون از بلوک server، در فایل سایت یا conf.d) مجازند، چون حافظهی مشترک برای کل Nginx ساخته میشود.
۲) استفاده از zone ای که تعریف نشده
Section titled “۲) استفاده از zone ای که تعریف نشده”sed -i 's|limit_req zone=bucket burst=2;|limit_req zone=buckt burst=2;|' /etc/nginx/sites-available/shop.testnginx -tcp /root/shop.test.bak /etc/nginx/sites-available/shop.testnginx: the configuration file /etc/nginx/nginx.conf syntax is ok2026/10/04 14:17:36 [emerg] 38997#38997: zero size shared memory zone "buckt"nginx: configuration file /etc/nginx/nginx.conf test failedپیام کمی گمراهکننده است: «zone با اندازهی صفر». یعنی Nginx zone ای به این اسم ندید (اینجا buckt بهجای bucket). اسم را با تعریف limit_req_zone مقایسه کن.
۳) محدودیت پشت CDN یا load balancer
Section titled “۳) محدودیت پشت CDN یا load balancer”اگر جلوی Nginx یک CDN یا proxy دیگر باشد، $binary_remote_addr برای همه IP همان proxy است؛ پس همهی کاربرها یک سطل مشترک دارند و با اولین هجوم، همه رد میشوند. راهحل: IP واقعی را با ماژول realip بازیابی کن (set_real_ip_from + real_ip_header X-Forwarded-For) تا $remote_addr درست شود.
۴) محدودیت سفت بدون burst روی کل سایت
Section titled “۴) محدودیت سفت بدون burst روی کل سایت”مثال ۱: با rate=2r/s و بدون burst، صفحهای که ۱۰ فایل CSS و JS و عکس دارد، برای کاربر واقعی نیمهکاره بار میشود. راهحل: محدودیت سفت فقط روی بخشهای حساس (/login، /api/search)؛ برای کل سایت سقف بزرگ با burst و nodelay (مثل مثال ۵)، یا اصلاً فایلهای استاتیک را محدود نکن.
۵) عوض کردن کلید یک zone و reload بیصدا شکستخورده
Section titled “۵) عوض کردن کلید یک zone و reload بیصدا شکستخورده”این یکی خطرناک است، چون همهچیز «موفق» به نظر میرسد. zone bucket الان با کلید $binary_remote_addr کار میکند. کلیدش را عوض میکنیم (مثلاً به $server_name، یعنی یک سطل برای کل سایت)، ولی اسمش را نه:
: > /var/log/nginx/error.logsed -i 's|^limit_req_zone $binary_remote_addr zone=bucket|limit_req_zone $server_name zone=bucket|' /etc/nginx/sites-available/shop.testnginx -t 2>&1 | tail -n 1systemctl reload nginx; echo "reload exit code: $?"sleep 1systemctl is-active nginxcut -d' ' -f3,5- /var/log/nginx/error.lognginx: configuration file /etc/nginx/nginx.conf test is successfulreload exit code: 0active[emerg] limit_req "bucket" uses the "$server_name" key while previously it used the "$binary_remote_addr" keynginx -t موفق بود، systemctl reload با کد ۰ تمام شد و Nginx هم active است. ولی در error log یک [emerg] نشسته: zone در حال اجرا با کلید دیگری ساخته شده و Nginx نمیتواند حافظهی مشترک موجود را با کلید جدید عوض کند. نتیجه: تنظیم جدید اصلاً اعمال نشد و Nginx با تنظیم قبلی ادامه میدهد (nginx -t این را نمیفهمد، چون از zone های در حال اجرا خبر ندارد). راهحل: وقتی کلید یا اندازهی یک zone را عوض میکنی، اسمش را هم عوض کن (مثلاً bucket_site)، یا یک بار systemctl restart nginx بزن. و عادت کن بعد از هر reload، آخر error log را نگاه کنی.
یک API ساده (هر location ای که فایل برگرداند) را به 1r/s با burst=3 nodelay و کد 429 محدود کن. پیشبینی کن از ۶ درخواست همزمان چند تا ۲۰۰ میگیرند، بعد آزمایش کن.
دیدن جواب
mkdir -p /var/www/shop.test/apiecho '{"items": []}' > /var/www/shop.test/api/items.jsoncat > /etc/nginx/sites-available/shop.test <<'EOF'limit_req_zone $binary_remote_addr zone=api:10m rate=1r/s;
server { listen 80; server_name shop.test; root /var/www/shop.test;
location /api/ { limit_req zone=api burst=3 nodelay; limit_req_status 429; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1seq 6 | xargs -P 6 -I{} curl -s -o /dev/null -w '%{http_code}\n' http://shop.test/api/items.json | sort | uniq -cnginx: configuration file /etc/nginx/nginx.conf test is successful 4 200 2 429پیشبینی: یکی (خود درخواست) + ۳ (burst) = ۴ تا ۲۰۰ و ۲ تا ۴۲۹؛ همان شد.
تمرین اصلی درس، کامل: صفحهی ورود (/login) را به ۵ درخواست در دقیقه برای هر IP محدود کن، بهطوری که: کد رد 429 باشد، رد شدنها با سطح warn لاگ شوند، و برای کاربر بهجای صفحهی خالی Nginx یک پیام فارسی ساده (429.html) نمایش داده شود. با ۷ تلاش نشان بده نتیجه درست است.
دیدن جواب
cat > /var/www/shop.test/429.html <<'EOF'<!doctype html><meta charset="utf-8"><title>429</title><p>تعداد تلاشها زیاد بود. یک دقیقهی دیگر دوباره امتحان کن.</p>EOFcat > /etc/nginx/sites-available/shop.test <<'EOF'limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server { listen 80; server_name shop.test; root /var/www/shop.test;
# log warn and above here (Ubuntu's default level is error) error_log /var/log/nginx/error.log warn;
location = /login { limit_req zone=login burst=4 nodelay; limit_req_status 429; limit_req_log_level warn; error_page 429 /429.html; proxy_pass http://127.0.0.1:9421; include proxy_params; } location = /429.html { internal; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1: > /var/log/nginx/error.logfor i in 1 2 3 4 5 6 7; do printf 'try %s: ' "$i" curl -s -X POST -w ' [%{http_code}]\n' http://shop.test/login | grep -v -e '^<!doctype' -e '^<p>' | tr -d '\n' echodonecurl -s -X POST http://shop.test/login | tail -n 1cut -d' ' -f3,6- /var/log/nginx/error.log | cut -d, -f1 | sort | uniq -cnginx: configuration file /etc/nginx/nginx.conf test is successfultry 1: wrong username or password [401]try 2: wrong username or password [401]try 3: wrong username or password [401]try 4: wrong username or password [401]try 5: wrong username or password [401]try 6: [429]try 7: [429]<p>تعداد تلاشها زیاد بود. یک دقیقهی دیگر دوباره امتحان کن.</p> 3 [warn] limiting requests- پنج تلاش اول به اپ رسیدند (
401)، ششم و هفتم429گرفتند و بدنهشان صفحهی ما بود (در حلقه، خطهای HTML را برای خوانایی حذف کردیم؛ درخواست آخر بیرون حلقه متن کامل پیام فارسی را نشان میدهد). error_page 429 /429.html;صفحهی خطا را عوض کرد وinternalنمیگذارد کسی مستقیم/429.htmlرا باز کند.- در لاگ ۳ خط
[warn] limiting requests(تلاش ۶، ۷ و درخواست آخر).limit_req_log_level warnسطح لاگ ردها را پایین آورد تا error log با هزاران خط «خطا» از رباتها پر نشود؛ ولی چون سطح پیشفرض error log در Ubuntuerrorاست، بایدerror_log ... warn;را هم میگذاشتیم، وگرنه این خطها اصلاً نوشته نمیشدند.
یک API عمومی داری که مشتریها با هدر X-Api-Key صدا میزنند. سیاست: هر کلید API حداکثر 2r/s (با burst=2 nodelay)، درخواست بدون کلید حداکثر 1r/m برای هر IP، و هر IP حداکثر ۳ درخواست همزمان. با curl نشان بده دو کلید مختلف از یک IP سطل جدا دارند و درخواست بیکلید سفت محدود است.
دیدن جواب
cat > /etc/nginx/sites-available/shop.test <<'EOF'# requests with a key: one bucket per key; without a key: empty -> not counted heremap $http_x_api_key $api_key { default $http_x_api_key; "" "";}# requests without a key: one bucket per IP; with a key: empty -> not counted heremap $http_x_api_key $anon_key { default ""; "" $binary_remote_addr;}limit_req_zone $api_key zone=bykey:10m rate=2r/s;limit_req_zone $anon_key zone=anon:10m rate=1r/m;limit_conn_zone $binary_remote_addr zone=conns:10m;
server { listen 80; server_name shop.test; root /var/www/shop.test;
location /api/ { limit_req zone=bykey burst=2 nodelay; limit_req zone=anon; limit_conn conns 3; limit_req_status 429; limit_conn_status 429; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for who in key-A key-B none; do printf '%-6s ' "$who" for i in 1 2 3 4 5; do if [ "$who" = none ]; then curl -s -o /dev/null -w '%{http_code} ' http://shop.test/api/items.json else curl -s -o /dev/null -w '%{http_code} ' -H "X-Api-Key: $who" http://shop.test/api/items.json fi done echodonenginx: configuration file /etc/nginx/nginx.conf test is successfulkey-A 200 200 200 429 429key-B 200 200 200 429 429none 200 429 429 429 429key-Aوkey-B: هر کدام ۳ تا200(۱ +burst=2) و بعد429. سطلها جدا هستند، با اینکه هر دو از یک IP آمدهاند؛ چون کلید zonebykeyخود کلید API است.- بیکلید: فقط یکی (
1r/m)، بعد429.
ترفند اصلی: دو map که برای هر درخواست فقط یکی از دو کلید را پر میکنند. درخواست با کلید API در zone anon کلید خالی دارد (شمرده نمیشود) و برعکس. دو limit_req در یک location مجاز است و هر دو باید قبول کنند. limit_conn conns 3 هم روی همه، بر اساس IP، اعمال میشود. (در دنیای واقعی، اول باید معتبر بودن کلید API را چک کرد؛ وگرنه مهاجم با کلیدهای ساختگی سطلهای تازه میسازد. این کار معمولاً در اپ یا با یک map از کلیدهای معتبر انجام میشود.)
آزمونک
Section titled “آزمونک”با rate=2r/s و بدون burst، دو درخواست با فاصلهی ۱۰۰ میلیثانیه میرسند. دومی چه میشود؟
Nginx با دقت میلیثانیه میشمارد؛ برای تحمل هجوم کوتاه، burst لازم است.
burst=5 بدون nodelay با ۱۰ درخواست همزمان (rate=2r/s) چه میکند؟
صف یعنی تأخیر برای کاربر؛ nodelay همان ۶ تا را فوراً میفرستد.
چرا برای صفحهی ورود با سقف ۵ در دقیقه، rate=5r/m burst=4 nodelay؟
بدون burst، تلاش دوم در کمتر از ۱۲ ثانیه رد میشد.
location /x { return 200 "ok"; limit_req zone=z; } چرا محدود نمیشود؟
ترتیب خطها مهم نیست؛ ترتیب مرحلهها مهم است.
تفاوت limit_req و limit_conn؟
برای دانلودهای طولانی limit_conn (و limit_rate برای سرعت) به کار میآید.
چطور IP دفتر شرکت را از محدودیت معاف میکنی؟
geo $limited {...} و map $limited $limit_key {0 ""; 1 $binary_remote_addr;}
پشت یک CDN هستی و با اولین هجوم همهی کاربرها ۴۲۹ میگیرند. علت؟
set_real_ip_from و real_ip_header.
جمعبندی
Section titled “جمعبندی”- دو بخش:
limit_req_zone $binary_remote_addr zone=NAME:10m rate=R;درhttp، وlimit_req zone=NAME ...;درserverیاlocation. - سطل نشتی:
rateبا دقت میلیثانیه (2r/sیعنی هر ۵۰۰ms یکی)؛burstظرفیت هجوم؛ بدونnodelayاضافهها صف میشوند (تأخیر)، باnodelayفوراً ولی با همان سقف. - کد ۴۲۹:
limit_req_status 429;(پیشفرض ۵۰۳) و صفحهی خطای سفارشی باerror_page 429. - صفحهی ورود:
rate=5r/m+burst=4 nodelayرویlocation = /login؛ بقیهی سایت سقف بزرگ یا بدون محدودیت. limit_connبرای تعداد همزمان (دانلود طولانی)،limit_rateبرای سرعت.- لیست سفید: کلید خالی شمرده نمیشود (
geo+map). آزمایش:limit_req_dry_run on;. - دامها:
returnاز محدودیت رد میشود (مرحلهی rewrite)؛ پشت CDN بدونrealipهمه یک سطلاند.
| دستور | کاری که میکند |
|---|---|
limit_req_zone $binary_remote_addr zone=perip:10m rate=2r/s; | تعریف سطل برای هر IP |
limit_req zone=perip; | اعمال بدون هجوم |
limit_req zone=perip burst=5; | صف تا ۵ درخواست |
limit_req zone=perip burst=5 nodelay; | تا ۵ اضافه فوراً |
limit_req_status 429; | کد رد |
limit_req_dry_run on; | فقط لاگ، بدون رد |
limit_req_log_level warn; | سطح لاگ |
limit_conn_zone $binary_remote_addr zone=conns:10m; | تعریف شمارندهی اتصال |
limit_conn conns 2; | حداکثر ۲ همزمان |
geo $limited { default 1; 10.0.0.0/8 0; } | برچسب IP های مورد اعتماد |
map $limited $limit_key { 0 ""; 1 $binary_remote_addr; } | کلید خالی برای لیست سفید |
seq 10 | xargs -P 10 -I{} curl -s -o /dev/null -w "%{http_code}\n" URL | آزمایش با درخواست همزمان |