سایت بالا میآید ولی همهٔ کاربران با یک IP در لاگ اپلیکیشن ثبت میشوند، یا صفحههای سنگین بعد از ۶۰ ثانیه با 504 Gateway Timeout میافتند. اگر پشت Nginx یک اپلیکیشن Node.js، یک بکاند Python یا یک سرور دیگر نشسته باشد، این دو نشانه تقریباً همیشه یک ریشه دارند: ریورس پروکسی ناقص پیکربندی شده. در این متن همان بلوکی را مینویسیم که در پروداکشن جواب میدهد، و جاهایی که مردم اشتباه میکنند را با علامت مشخص میکنیم.
بلوک پایهٔ ریورس پروکسی که واقعاً کار میکند
فرض کنید اپلیکیشن روی 127.0.0.1:3000 گوش میدهد و Nginx باید ترافیک example.com را به آن پاس بدهد. کمترین کانفیگی که هم درست باشد و هم قابل نگهداری:
upstream app_backend {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name example.com;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
}
سه خط از این بلوک را اگر حذف کنید، مشکل برمیگردد. proxy_http_version 1.1 بدون آن، Nginx با HTTP/1.0 به بکاند وصل میشود و keepalive در upstream عملاً بیاثر میماند. خط Connection "" جلوی ارسال هدر Connection: close را میگیرد؛ اگر بگذارید، هر درخواست یک اتصال TCP تازه میسازد و در بار بالا پورتهای TIME_WAIT جمع میشوند. و X-Forwarded-Proto همان چیزی است که اپلیکیشن از آن میفهمد کاربر روی HTTPS بوده یا نه؛ بدون آن، ریدایرکتهای داخلی به http:// برمیگردند و حلقهٔ ریدایرکت میسازند.
چرا X-Forwarded-For کافی نیست
هدر X-Forwarded-For یک لیست است، نه یک IP. وقتی Cloudflare یا یک لودبالانسر دیگر جلوی Nginx باشد، مقدار این هدر چیزی شبیه 203.0.113.5, 198.51.100.7 میشود و اپلیکیشن باید بداند کدام یکی معتبر است. اگر کد شما فقط split(",")[0] میگیرد، هر کسی میتواند با فرستادن هدر جعلی، IP خودش را جعل کند. راه درست، تعریف محدودهٔ IPهای مطمئن است:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
real_ip_header CF-Connecting-IP;
بعد از این، $remote_addr خودش IP واقعی کاربر است و لازم نیست اپلیکیشن چیزی را پارس کند. اگر این بلوک را نگذارید و در عین حال به هدر اعتماد کنید، لاگهای امنیتیتان بیارزش میشوند و rate limit روی IP اشتباه اعمال میشود.
تایماوتها: عددی که باید آگاهانه انتخاب شود
مقدار پیشفرض proxy_read_timeout در Nginx شصت ثانیه است. برای یک API معمولی این عدد سخاوتمندانه است؛ برای یک گزارشساز که روی دیتابیس سنگین کار میکند، کم است. برای یک endpoint که با stream پاسخ میدهد، بیمعنی است. سه تایماوت جدا وجود دارد و مردم معمولاً فقط اولی را میبینند:
| دایرکتیو | پیشفرض | کجا درد میسازد |
|---|---|---|
proxy_connect_timeout | 60s | بکاند بالا نیامده یا پورت اشتباه است |
proxy_send_timeout | 60s | آپلود بزرگ یا بکاند کند در خواندن بدنه |
proxy_read_timeout | 60s | پاسخ طولانی، گزارش سنگین، صف کار |
برای یک اپلیکیشن وب معمولی، من proxy_connect_timeout 5s میگذارم و proxy_read_timeout را روی ۱۲۰ ثانیه. عدد اتصال را کوتاه نگه دارید چون اگر بکاند مرده باشد، کاربر نباید پنج دقیقه منتظر بماند تا بفهمد سایت خراب است. اما تایماوت خواندن را بلند کنید، چون قطع کردن یک درخواست نیمهکاره معمولاً بدتر از صبر کردن است.
یک نکتهٔ عملی: اگر تایماوت را در Nginx زیاد کردید ولی در لودبالانسر جلویی نه، همان ۵۰۴ برمیگردد و شما دنبال باگ در اپلیکیشن میگردید. تایماوتها را از بیرون به داخل نزولی بچینید.
بافر کردن پاسخ و جایی که اشتباه میکنند
Nginx بهصورت پیشفرض پاسخ بکاند را در حافظه یا روی دیسک بافر میکند. این برای فایلهای استاتیک و پاسخهای کوچک عالی است: بکاند سریع آزاد میشود و کلاینت کند، سرور شما را اشغال نمیکند. اما برای Server-Sent Events، WebSocket یا هر پاسخ streaming، بافر کردن یعنی کاربر هیچ چیزی نمیبیند تا کل پاسخ تمام شود.
اینجا اشتباه میکنند: برای WebSocket فقط proxy_set_header Upgrade $http_upgrade را اضافه میکنند و proxy_buffering off را فراموش میکنند. نتیجه این است که اتصال برقرار میشود، لاگها سالماند، ولی پیامها با تأخیر عجیبی میرسند یا اصلاً نمیرسند. کانفیگ کامل WebSocket این است:
location /ws/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 3600s;
}
توجه کنید که Connection اینجا "upgrade" است، نه رشتهٔ خالی. اگر هر دو بلوک را در یک فایل کپی کنید و Connection "" را برای مسیر /ws/ بگذارید، handshake شکست میخورد و در کنسول مرورگر خطای WebSocket connection failed میبینید.
بافر روی دیسک و پرشدن /var
وقتی پاسخ از حافظهٔ بافر بزرگتر باشد، Nginx آن را در مسیر proxy_temp_path روی دیسک مینویسد. پیشفرض /var/lib/nginx/proxy است. اگر کسی فایلهای بزرگ را از طریق پروکسی دانلود کند و proxy_max_temp_file_size را محدود نکرده باشید، پارتیشن /var پر میشود و بعد از آن نه لاگ مینویسید نه session. علامتش هم این است که همهچیز تا یک نقطه سالم است و بعد یکدفعه 500 میگیرید بدون هیچ خطای اپلیکیشنی.
هدر Host و ریدایرکتهای اشتباه
اگر proxy_set_header Host $host را نگذارید، Nginx مقدار Host را از درخواست اصلی نگه میدارد که در بیشتر موارد درست است، ولی وقتی upstream یک نام داخلی دارد، بکاند ممکن است ریدایرکت به آن نام داخلی بزند و کاربر به آدرس غیرقابلدسترس هدایت شود. اگر اپلیکیشن روی چند دامنه سرو میشود، $host درست است؛ اگر باید همیشه یک دامنهٔ مشخص دیده شود، مقدار ثابت بگذارید.
برای دامنههایی که هنوز DNSشان کامل جا نیفتاده، قبل از هر چیز مطمئن شوید رکورد درست انتخاب شده؛ تفاوت CNAME و A record در همینجا خودش را نشان میدهد و اگر اشتباه باشد، Nginx سالم هم به سایت اشتباهی سرویس میدهد.
تست کردن قبل از اینکه کاربر خبر بدهد
قبل از reload، همیشه nginx -t بزنید. بعد با curl -I -H "Host: example.com" https://127.0.0.1/ پاسخ را از خود سرور ببینید تا مطمئن شوید مشکل از DNS یا CDN نیست. برای دیدن هدرهایی که واقعاً به بکاند میرود، در بکاند یک endpoint موقت بسازید که هدرها را چاپ کند؛ این کار از حدس زدن سریعتر است.
اگر خطای 502 Bad Gateway میگیرید، اول با ss -ltnp | grep 3000 ببینید بکاند روی همان پورت گوش میدهد یا نه. اگر 504 است، تایماوتها را چک کنید. اگر 499 در لاگ Nginx میبینید، یعنی خود کاربر اتصال را بست و مشکل جای دیگری است. معنی دقیق این کدها را در راهنمای کد وضعیت HTTP آوردهایم.
یک trade-off که باید صریح بگویم: بافر کردن پاسخ، throughput را بالا میبرد و مصرف حافظهٔ بکاند را کم میکند، ولی latency اولین بایت را برای پاسخهای کوچک زیاد میکند. اگر اپلیکیشن شما API با پاسخهای چند کیلوبایتی است، بافر را روشن بگذارید. اگر streaming یا SSE دارید، خاموشش کنید و هزینهاش را بپذیرید.
برای پروژههای وردپرسی، معمولاً لازم نیست این بلوک را از صفر بنویسید؛ یک هاست وردپرس مدیریتشده این لایه را از قبل تنظیم کرده و شما فقط باید مطمئن شوید کش و تایماوتها با افزونههایتان تضاد ندارند. اگر روی سرور اختصاصی خودتان کار میکنید، نمونههای آمادهٔ بیشتری در مستندات و پایگاه دانش هست.
پرسشهای پرتکرار
چرا همهٔ کاربران با IP سرور در لاگ اپلیکیشن ثبت میشوند؟
چون اپلیکیشن IP را از اتصال TCP میخواند و آن اتصال از سمت Nginx میآید. باید هدر X-Forwarded-For را در Nginx ست کنید و اپلیکیشن را طوری تنظیم کنید که به آن اعتماد کند. اگر جلوی Nginx هم پروکسی دیگری هست، از set_real_ip_from استفاده کنید تا $remote_addr خودش درست شود.
تفاوت proxy_pass با و بدون اسلش انتهایی چیست؟
در proxy_pass http://backend/ مسیر مکان با اسلش جایگزین میشود، ولی در proxy_pass http://backend کل URI اصلی دستنخورده به بکاند میرود. اگر اپلیکیشن شما زیرمسیر انتظار دارد و این را اشتباه بگذارید، همهٔ درخواستها به 404 میخورند بدون اینکه چیزی در لاگ Nginx دیده شود.
آیا باید proxy_buffering را برای همه خاموش کنم؟
نه. خاموش کردن سراسری، بکاند را در برابر کلاینتهای کند آسیبپذیر میکند و مصرف حافظه را بالا میبرد. فقط برای مسیرهایی که واقعاً streaming هستند خاموشش کنید و بقیه را با بافر پیشفرض بگذارید.
بعد از تغییر کانفیگ چه کاری لازم است؟
اول nginx -t، بعد systemctl reload nginx. از restart استفاده نکنید مگر لازم باشد، چون اتصالهای فعال را میبندد. اگر سایت روی چند سرور پشت لودبالانسر است، reload را یکییکی انجام دهید تا سرویس قطع نشود.
دیدگاهها ۰
هنوز دیدگاهی ثبت نشده؛ اولین نفر باشید!