رفتن به محتوا
LoopX

محدودسازی درخواست‌ها

توی این درس یاد می‌گیری تعداد درخواست‌هایی که هر کاربر می‌تواند بفرستد را محدود کنی (rate limiting)، تا یک ربات یا مهاجم نتواند با هزاران درخواست رمز عبور حدس بزند یا سرور را از پا بیندازد. با limit_req_zone و limit_req کار می‌کنی و می‌فهمی Nginx با الگوریتم «سطل نشتی» چطور می‌شمارد، با burst و nodelay برای کاربرهای واقعی جا باز می‌کنی، صفحه‌ی ورود را به ۵ درخواست در دقیقه محدود می‌کنی، با limit_conn تعداد اتصال‌های همزمان را می‌بندی، و IP های مورد اعتماد را از محدودیت معاف می‌کنی.

مسئله: یک نفر، هزار درخواست

Section titled “مسئله: یک نفر، هزار درخواست”

سایت تو برای آدم‌ها ساخته شده: یک آدم در ثانیه چند کلیک می‌کند، نه صدها. ولی اینترنت پر از برنامه است:

  • حدس رمز (brute force): رباتی که در دقیقه هزار رمز را روی صفحه‌ی ورود امتحان می‌کند.
  • اسکرپر (scraper): برنامه‌ای که همه‌ی صفحه‌های فروشگاه را با سرعت هر چه بیشتر کپی می‌کند.
  • حمله‌ی ساده‌ی از کار انداختن: چند ماشین که فقط درخواست سنگین (جستجو، گزارش) می‌فرستند تا CPU اپ پر شود.

Nginx جلوی اپ ایستاده؛ پس بهترین جاست که بگوید «از این IP بیش از این مقدار نمی‌پذیرم»، قبل از اینکه درخواست اصلاً به اپ (و دیتابیس) برسد.

Nginx برای هر کاربر (مثلاً هر IP) یک سطل در نظر می‌گیرد که از ته آن، آب با سرعت ثابت نشت می‌کند (rate، مثلاً ۲ قطره در ثانیه). هر درخواست یک قطره است که در سطل ریخته می‌شود:

  • اگر سطل جا داشته باشد، قطره قبول است.
  • ظرفیت سطل همان burst است (پیش‌فرض: صفر؛ یعنی فقط همان یک قطره‌ای که در حال نشت است).
  • اگر سطل پر باشد، قطره‌ی تازه بیرون می‌ریزد: درخواست رد می‌شود.

پس rate سرعت بلندمدت را تعیین می‌کند و burst اینکه یک هجوم کوتاه چقدر تحمل شود.

سطل نشتی: درخواست‌ها با هر سرعتی برسند، با سرعت ثابت rate از سطل خارج و پردازش می‌شوند. تا burst درخواست در سطل صبر می‌کنند (یا با nodelay فوراً به اپ می‌رسند ولی جایشان در سطل می‌ماند)؛ بیشتر از آن رد می‌شود.

دو بخش: تعریف zone و استفاده از آن

Section titled “دو بخش: تعریف zone و استفاده از آن”

محدودسازی درخواست دو قسمت دارد:

# 1) in http context: define the bucket store
limit_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 (در دقیقه).

این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24، Node.js 18) با کاربر root اجرا شده. دامنه‌ی آزمایشی shop.test در /etc/hosts به 127.0.0.1 اشاره می‌کند.

مثال ۱: اولین محدودیت، و یک غافلگیری

Section titled “مثال ۱: اولین محدودیت، و یک غافلگیری”

یک سایت ساده و محدودیت ۲ درخواست در ثانیه برای هر IP:

Terminal window
mkdir -p /var/www/shop.test
echo '<h1>shop</h1>' > /var/www/shop.test/index.html
dd if=/dev/zero of=/var/www/shop.test/big.bin bs=1M count=2 status=none
cat > /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;
}
}
EOF
ln -sf /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/; done; echo
sleep 2
echo "--- 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; echo
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
200 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 است. ابزارهای مانیتورینگ هم بین «سرور خراب است» و «کسی را محدود کردیم» فرق می‌گذارند:

Terminal window
sed -i 's|^ limit_req zone=perip;| limit_req zone=perip;\n limit_req_status 429;|' /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
: > /var/log/nginx/error.log
for i in 1 2 3; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/; done; echo
cut -d' ' -f6- /var/log/nginx/error.log
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
200 429 429
limiting 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) و برای هر کدام کد و زمان را چاپ می‌کنیم:

Terminal window
sed -i 's|^ limit_req zone=perip;| limit_req zone=perip burst=5;|' /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
seq 10 | xargs -P 10 -I{} curl -s -o /dev/null -w '%{http_code} after %{time_total}s\n' http://shop.test/ | sort -t' ' -k3
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
200 after 0.000660s
429 after 0.000687s
429 after 0.000719s
429 after 0.000839s
429 after 0.003209s
200 after 0.501748s
200 after 1.001038s
200 after 1.497334s
200 after 1.996407s
200 after 2.496513s

ده درخواست همزمان رسیدند:

  • ۱ فوراً جواب گرفت (200 در حدود ۱ میلی‌ثانیه).
  • ۵ در صف ماندند و با سرعت rate (هر ۵۰۰ میلی‌ثانیه یکی) جواب گرفتند: بعد از ۰٫۵، ۱، ۱٫۵، ۲ و ۲٫۵ ثانیه.
  • ۴ تای باقی‌مانده جا نداشتند و فوراً 429 گرفتند.

برای کاربر واقعی، صبر ۲٫۵ ثانیه‌ای برای آخرین فایل صفحه خیلی زیاد است. راه‌حل در مثال بعد است.

مثال ۴: nodelay، بدون صبر ولی با همان سقف

Section titled “مثال ۴: nodelay، بدون صبر ولی با همان سقف”

صف کردن یعنی کاربر صبر می‌کند. nodelay می‌گوید درخواست‌هایی که در burst جا می‌شوند را فوراً بفرست، ولی جایشان در سطل را نگه دار تا با سرعت rate خالی شود:

Terminal window
sleep 3
sed -i 's|burst=5;|burst=5 nodelay;|' /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
seq 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 -c
echo "--- right after, one more:"
curl -s -o /dev/null -w '%{http_code}\n' http://shop.test/
sleep 1
echo "--- 1s later, three more:"
for i in 1 2 3; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/; done; echo
خروجی
nginx: 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 محدود:

/srv/lx-login/app.js
// 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');
Terminal window
(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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
echo "=== 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/login
done
echo "=== 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/login
echo "=== attacker, 13s later:"
sleep 13
for i in 1 2; do curl -s -o /dev/null -w '%{http_code} ' -X POST http://shop.test/login; done; echo
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
=== attacker, 7 tries:
try 1: 401
try 2: 401
try 3: 401
try 4: 401
try 5: 401
try 6: 429
try 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 (درس عملکرد) پایین می‌آوریم:

Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
: > /var/log/nginx/error.log
seq 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 | sort
cut -d' ' -f6- /var/log/nginx/error.log | cut -d, -f1 | sort | uniq -c
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
200 2097152 bytes after 1.002174s
200 2097152 bytes after 1.002437s
429 178 bytes after 0.000657s
429 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 “مثال ۷: لیست سفید و حالت آزمایشی”

دو نیاز رایج دیگر:

  1. IP های مورد اعتماد (دفتر شرکت، سرور مانیتورینگ) نباید محدود شوند.
  2. قبل از روشن کردن محدودیت روی سایت واقعی، می‌خواهی ببینی چه کسانی رد می‌شدند، بدون اینکه واقعاً ردشان کنی.

برای اولی از یک نکته استفاده می‌کنیم: اگر کلید خالی باشد، Nginx آن درخواست را نمی‌شمارد. با geo (مقدار بر اساس IP) و map (تبدیل مقدار)، کلید را برای IP های مورد اعتماد خالی می‌کنیم. برای دومی، limit_req_dry_run on;:

Terminal window
cat > /etc/nginx/sites-available/shop.test <<'EOF'
# 0 = trusted, 1 = everyone else
geo $limited {
default 1;
127.0.0.2 0;
10.0.0.0/8 0;
}
# empty key = not counted
map $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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
: > /var/log/nginx/error.log
for 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; echo
done
sleep 2
printf '%-10s ' 'dry run'
for i in 1 2 3 4; do curl -s -o /dev/null -w '%{http_code} ' http://shop.test/preview/; done; echo
grep -c 'dry run' /var/log/nginx/error.log
grep 'dry run' /var/log/nginx/error.log | head -n 1 | cut -d' ' -f6- | cut -d, -f1-2
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
127.0.0.1 200 429 429 429
127.0.0.2 200 200 200 200
dry run 200 200 200 200
3
limiting requests, dry run
  • 127.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، چقدر سطل پر است). در هر درخواست:

  1. به اندازه‌ی زمان گذشته از آخرین درخواست، از excess کم می‌کند (نشت).
  2. یک درخواست اضافه می‌کند.
  3. اگر excess از burst بیشتر شود، رد می‌کند.

همان عدد excess در لاگ مثال ۲ بود. ببینیم با فاصله‌های مختلف چطور تغییر می‌کند:

Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
: > /var/log/nginx/error.log
pids=""
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.05
done
wait $pids
grep -o 'delaying request, excess: [0-9.]*\|limiting requests, excess: [0-9.]*' /var/log/nginx/error.log
echo "--- 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; echo
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
200 after 0.001077s
429 after 0.000932s
429 after 0.000797s
200 after 0.448859s
200 after 0.894418s
delaying request, excess: 0.894
delaying request, excess: 1.784
limiting requests, excess: 2.686
limiting 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 دارد. دلیلش در کادر زیر است.

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 های مورد اعتماد

۱) limit_req_zone در جای اشتباه

Section titled “۱) limit_req_zone در جای اشتباه”
Terminal window
cp /etc/nginx/sites-available/shop.test /root/shop.test.bak
sed -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.test
nginx -t
cp /root/shop.test.bak /etc/nginx/sites-available/shop.test
خروجی
2026/10/04 14:17:36 [emerg] 38994#38994: "limit_req_zone" directive is not allowed here in /etc/nginx/sites-enabled/shop.test:8
nginx: configuration file /etc/nginx/nginx.conf test failed

limit_req_zone و limit_conn_zone فقط در http (یعنی بیرون از بلوک server، در فایل سایت یا conf.d) مجازند، چون حافظه‌ی مشترک برای کل Nginx ساخته می‌شود.

۲) استفاده از zone ای که تعریف نشده

Section titled “۲) استفاده از zone ای که تعریف نشده”
Terminal window
sed -i 's|limit_req zone=bucket burst=2;|limit_req zone=buckt burst=2;|' /etc/nginx/sites-available/shop.test
nginx -t
cp /root/shop.test.bak /etc/nginx/sites-available/shop.test
خروجی
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
2026/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، یعنی یک سطل برای کل سایت)، ولی اسمش را نه:

Terminal window
: > /var/log/nginx/error.log
sed -i 's|^limit_req_zone $binary_remote_addr zone=bucket|limit_req_zone $server_name zone=bucket|' /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1
systemctl reload nginx; echo "reload exit code: $?"
sleep 1
systemctl is-active nginx
cut -d' ' -f3,5- /var/log/nginx/error.log
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
reload exit code: 0
active
[emerg] limit_req "bucket" uses the "$server_name" key while previously it used the "$binary_remote_addr" key

nginx -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 محدود کن. پیش‌بینی کن از ۶ درخواست همزمان چند تا ۲۰۰ می‌گیرند، بعد آزمایش کن.

دیدن جواب
Terminal window
mkdir -p /var/www/shop.test/api
echo '{"items": []}' > /var/www/shop.test/api/items.json
cat > /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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
seq 6 | xargs -P 6 -I{} curl -s -o /dev/null -w '%{http_code}\n' http://shop.test/api/items.json | sort | uniq -c
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
4 200
2 429

پیش‌بینی: یکی (خود درخواست) + ۳ (burst) = ۴ تا ۲۰۰ و ۲ تا ۴۲۹؛ همان شد.

✎ تمرینمتوسط

تمرین اصلی درس، کامل: صفحه‌ی ورود (/login) را به ۵ درخواست در دقیقه برای هر IP محدود کن، به‌طوری که: کد رد 429 باشد، رد شدن‌ها با سطح warn لاگ شوند، و برای کاربر به‌جای صفحه‌ی خالی Nginx یک پیام فارسی ساده (429.html) نمایش داده شود. با ۷ تلاش نشان بده نتیجه درست است.

دیدن جواب
Terminal window
cat > /var/www/shop.test/429.html <<'EOF'
<!doctype html><meta charset="utf-8"><title>429</title>
<p>تعداد تلاش‌ها زیاد بود. یک دقیقه‌ی دیگر دوباره امتحان کن.</p>
EOF
cat > /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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
: > /var/log/nginx/error.log
for 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'
echo
done
curl -s -X POST http://shop.test/login | tail -n 1
cut -d' ' -f3,6- /var/log/nginx/error.log | cut -d, -f1 | sort | uniq -c
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
try 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 در Ubuntu error است، باید error_log ... warn; را هم می‌گذاشتیم، وگرنه این خط‌ها اصلاً نوشته نمی‌شدند.
✎ تمرینسخت

یک API عمومی داری که مشتری‌ها با هدر X-Api-Key صدا می‌زنند. سیاست: هر کلید API حداکثر 2r/s (با burst=2 nodelay)، درخواست بدون کلید حداکثر 1r/m برای هر IP، و هر IP حداکثر ۳ درخواست همزمان. با curl نشان بده دو کلید مختلف از یک IP سطل جدا دارند و درخواست بی‌کلید سفت محدود است.

دیدن جواب
Terminal window
cat > /etc/nginx/sites-available/shop.test <<'EOF'
# requests with a key: one bucket per key; without a key: empty -> not counted here
map $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 here
map $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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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
echo
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
key-A 200 200 200 429 429
key-B 200 200 200 429 429
none 200 429 429 429 429
  • key-A و key-B: هر کدام ۳ تا 200 (۱ + burst=2) و بعد 429. سطل‌ها جدا هستند، با اینکه هر دو از یک IP آمده‌اند؛ چون کلید zone bykey خود کلید API است.
  • بی‌کلید: فقط یکی (1r/m)، بعد 429.

ترفند اصلی: دو map که برای هر درخواست فقط یکی از دو کلید را پر می‌کنند. درخواست با کلید API در zone anon کلید خالی دارد (شمرده نمی‌شود) و برعکس. دو limit_req در یک location مجاز است و هر دو باید قبول کنند. limit_conn conns 3 هم روی همه، بر اساس IP، اعمال می‌شود. (در دنیای واقعی، اول باید معتبر بودن کلید API را چک کرد؛ وگرنه مهاجم با کلیدهای ساختگی سطل‌های تازه می‌سازد. این کار معمولاً در اپ یا با یک map از کلیدهای معتبر انجام می‌شود.)

⚡ بررسی سریع

با rate=2r/s و بدون burst، دو درخواست با فاصله‌ی ۱۰۰ میلی‌ثانیه می‌رسند. دومی چه می‌شود؟

؟ آزمونک
  1. burst=5 بدون nodelay با ۱۰ درخواست همزمان (rate=2r/s) چه می‌کند؟

  2. چرا برای صفحه‌ی ورود با سقف ۵ در دقیقه، rate=5r/m burst=4 nodelay؟

  3. location /x { return 200 "ok"; limit_req zone=z; } چرا محدود نمی‌شود؟

  4. تفاوت limit_req و limit_conn؟

  5. چطور IP دفتر شرکت را از محدودیت معاف می‌کنی؟

  6. پشت یک CDN هستی و با اولین هجوم همه‌ی کاربرها ۴۲۹ می‌گیرند. علت؟

  • دو بخش: 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آزمایش با درخواست همزمان