ده اشتباه در پیکربندی NGINX


در این مقاله به ۱۰ اشتباه رایج در پیکربندی nginx و راه حل آنها پرداخته میشود.

اشتباه ۱ : Not Enough File Descriptors per Worker

دستور worker_connections حداکثر تعداد کانکشن های باز همزمانی که یک پردازنده (worker proccess)میتواند داشته باشد (پیش فرض ۵۱۲) را مشخص میکند. همه انواع کانکشن ها (برای مثال، کانکشن های پروکسی سرورها) نیز در این مقدار حداکثر شمرده میشوند نه فقط کانکشنهای کلاینت ها!. اما این نکته را باید در نظر داشته باشیم در نهایت یک محدودیت دیگه در تعداد کانکشن های همزمان در هر worker وجود داره :‌ محدودیت سیستم عامل بر روی جداکثر تعداد File Descriptor هایی که به هر process اختصاص داده شده است. در توزیع های UNIX جدید، این محدودیت پیش فرض ۱۰۲۴ است.

برای اکثر استقرار های NGINX به جز پروژه های کوچک، مقدار ۵۱۲ خیلی کم هست. در واقع در فایل پیش فرض nginx.conf برای نسخه رایگان و plus این مقدار به ۱۰۲۴ افزایش داده است.

اشتباه رایج پیکربندی این است که محدودیت file descriptor ها را به حداقل دو برابر مقدار worker_connections افزایش نمی دهید. راه حل این است که مقدار آن را با دستور worker_rlimit_nofile در فایل nginx.conf اصلی تنظیم کنید.

چرا به تعداد file descriptor های بیشتری نیاز است ؟ هر کانکشن از یک worker process در انجینکس به یک کلاینت یا upstream server، یک file descriptor مصرف میکند. وقتی nginx به عنوان یک وب سرور عمل میکند، از یک FD برای کانکشن کلاینت و یک FD برای فایلی که آن را serve میکند استفاده میکند و این برای حالت حداقلی است اما اکثر صفحات وب از تعداد زیادی فایل ساخته شده اند و نیاز به تعداد بیشتر FD است. زمانی nginx که به عنوان proxy server استفاده میشود، از یک FD برای هر کانکشن و یک FD برای upstream و یک FD سوم برای فایلی که به صورت موقت response را نگه داری میکند استفاده میشود و زمانی که از nginx به عنوان یک caching server استفاده می شود، رفتار آن مشابه وب سرور برای response های cache شده و شبیه proxy server در زمانی cache وجود ندارد و یا منقضی شده است.

انجینکس همچنین از یک FD به ازای هر Log file و یک جفت FD برای ارتباط با master process استفاده میکند اما معمولا این تعداد در برابر تعداد FD هایی که در کانکشن ها و فایلها استفاده میشود ناچیز است.

یونیکس (UNIX) معمولا راه های مختلفی برای تنظیم تعداد FD ها در هر Process ارائه میدهد.

  • دستور ulimit در صورتی که nginx در shell اجرا شده باشد.
  • اسکریپت init یا متغیرهای سرویس systemd در صورتی که nginx به صورت service اجرا شده باشد.
  • فایل etc/security/limits.conf/

به هرحال روش قابل استفاده، بستگی به نوع اجرای nginx دارد در حالی که دستور worker_rlimit_nofile ارتباطی با نوع اجرای انجینکس ندارد.

همچنین یک محدودیت در سطح سیستم عامل برای تعداد File Descriptor ها وجود دارد که میتوان آن را به وسیله دستور sysctl fs.file-max تغییر داد. مقدار پیش فرض معمولا به اندازه کافی بزرگ هست اما بهتره یک محاسبه در این مورد با فرمول زیر انجام شود.

(worker_rlimit_nofile * worker_processes)

که معمولا به طور قابل ملاحظه ای کمتر از fs.file‑max است. اگر به هر دلیلی مثلا یک حمله ی DoS باعث استفاده از تمام آن شود دیگه حتی امکان ورود به سیستم عامل برای فیکس کردن اون هم نداریم 🙂




اشتباه ۲: دستور error_log off

یک اشتباه رایج این است که فکر میکنیم دستور error_log off، لاگ گیری را غیرفعال میکند. در واقع، برخلاف دستور access_log ، دستور error_log پارامتر off نمیگیرد.در صورتی که شما دستور error_log off را در پیکربندی ها استفاده کنید، nginx یک فایل لاگ خطا به نام off در دایرکتوری پیش فرض خودش ( etc/nginx/ ) ایجاد میکند

به دلیل این که error log ها حاوی اطلاعات حیاتی در مورد هر مساله در nginx و یا سرویس هستند. ما به شما پیشنهاد نمیکنیم که error log ها را غیرفعال کنید. به هر حال فضای ذخیره سازی محدود است و اگر ممکن است داده های لاگ تمام فضای شما را اشتغال کند و تصمیم به غیرفعال کردن آن گرفته اید. از دستور العمل زیر استفاده نمایید.

error_log /dev/null emerg;

بعد از این دستور نیاز است یک بار وب سرور reload گردد تا تغییرات اعمال شوند.




اشتباه ۳ :فعال نکردن Keepalive Connections در Upstream Servers

به صورت پیش فرض، nginx به ازای هر درخواست یک کانکشن جدید به یک upstream(backend) server باز میکند. این کار ایمن ولی ناکارمد است! چون nginx و server باید ۳ پکت برای برقراری کانکشن و ۳ یا ۴ پکت برای خاتمه دادن به آن مبادله کنند.

در حجم ترافیک بالا،‌باز کردن یک کانکشن جدید برای هر درخواست میتواند تهدیدی برای منابع سیستم باشد و حتی میتواند باز کردن کانکشن جدید دیگر غیرممکن شود. دلیل آن هم این است که برای هر کانکشن، ۴ جفت اطلاعات شامل آدرس مبدا، پورت مبدا، آدرس مقصد، پورت مقصد باید به صورت unique باشد.

برای کانکشن ها از nginx به یک upstream server، سه عدد از این المنت ها (اول، سوم و چهارم) ثابت است ولی پورت مبدا، به صورت متغیر است. زمانی که یک کانکشن بسته میشود، سوکت لینوکس به مدت دو دقیقه در حالت TIME-WAIT قرار میگیرد که در حجم ترافیک بالا، امکان تکمیل پورت های داخل pool وجود دارد که در صورت این اتفاق، nginx توانایی باز کردن کانکشن جدید به upstream server را ندارد.

راه حل این است که keepalive connection را بین nginx و upstream server فعال کنیم. این روش احتمال تمام شدن پورت های pool را کاهش می دهد و هم عملکرد را بهبود می بخشد.

به منظور فعال کردن keepalive connection:

  • اضافه کردن دستور keepalive در هریلاک {}upstream
    • این نکته را در نظر داشته باشید که این دستور تعداد کانکشن ها به upstream server به ازای هر worker process را محدود نمیکنه. این یک تصور غلط رایج است. پس پارامتر keepalived نباید اونقدر که فکر میکنید عدد بزرگی باشه.
    • ما پیشنهاد میکنیم پارامتر را به مقدار ۲ برار تعداد سرورهای لیست شده در بلاک {}upstream قرار بدید. این مقدار به اندازه ای کافی برای nginx بزرگ است که بتونه تمام کانکشن های keepalived را نگه داری کنه وهمچنین به اندازی کافی کوچک هست که upstream server ها بتوانند کانکشن های ورودی جدید را نیز پردازش کنند.
  • در نظر داشته باشید هنگامی که یک الگوریتم load balacer برای بلاک {}upstream مشخص میکنیم (منظورhash, ip_hash, least_conn, least_time, یا random است ) باید در بالای دستور keepalived قرار گیرند. این یکی از استثناهای نادر در قاعده کلی است و ترتیب دستورات در پیکربندی NGINX مهم نیست.
  • بلاک {}location که درخواست ها را به سمت upstream هدایت میکند باید همراه با این دو دستور در کنار proxy_pass باشد:
proxy_http_version 1.1;
proxy_set_header "Connection" "";

به صورت پیش فرض nginx از HTTP/1.0 برای کانکشن های upstream server استفاده میکند و بر همین اساس یک Connection: close به header درخواست هایی که به سرور می فرستد اضافه میکند و در نتیجه زمانی که درخواست کامل شود، کانکشن بسته میشود و keepalived در {}upstream نادیده گرفته میشود.

دستور proxy_http_version به nginx میگه که از HTTP/1.1 استفاده کنه و دستور proxy_set_header مقدار close را از هدر Connection حذف میکنه.




اشتباه ۴:‌ فراموش کردن نحوه عملکرد وراثت دستورات

دستورالعمل های NGINX به صورت رو به پایین یا outside-in به ارث برده می شوند: یعنی یک context فرزند از دستورات والد خود به ارث میبرد. برای مثال، تمام بلاک های {}server و {}location در {}http مقادیر دستوراتی که داخل بلاک {}http اضافه شده است را به ارث میبرند و تمام دستورات در بلاک {}server به بلاک های فرزند {}location به ارث میرسد. به هر حال اگر یک دستوری در هردوی آنها موجود باشد دیگر از والد خود به ارث نمی برند و دستور و مقدار همان بلاک مورد استفاده قرار میگیرد. اشتباه اونجایی هست که این قانون در دستورات array فراموش میشه که نه تنها میتونه در چندین context اضافه بشه بلکه متیونه چندی بار در یک context استفاده بشه. برای مثال دستورات proxy_set_header و add_header. همین واژه ی add در دستور میتونه باعث فراموش شدن این قانون بشه.

در مثال زیر نحوه عملکرد وراثت را برای دستور add_header نشون میدیم

برای سروری که به پورت 8080 گوش میده هیچ دستور add_header در هیچ یک از بلاک های {}server و {}location وجود ندارد ولی به دلیل وراثت ما هر دوی آن را در header داریم.

برای سروری که به پورت 8081 گوش میده یک دستور add_header در بلاک {}server وجود داره ولی در بلاک {}location وجود نداره. Header هایی که در بلاک{}server وجود دارد دو header تعریف شده در بلاک {}http را نادیده میگیرد.

در بلاک location /test یک دستور add_header وجود دارد و این دستور باعث میشود دو header در والد خود یعنی {}server و دو header از {}http را نادیده بگیرد.

اگر بخواهیم یک بلاک {}location هدر های تعریف شده در والد خود را به همراه هدرهای تعریف شده در بلاک خود حفظ کند باید تمام آنها را مجدد تعریف کنیم. دقیقا چیزی که در بلاک location /correct تعریف شده است.




اشتباه ۵ : دستور proxy_buffering off

بافر پروکسی به صورت پیش فرض در nginx فعال است. یعنی پارامتر دستور proxy_buffering مقدار on است.بافر پروکسی به این معنی است که NGINX پاسخ یک سرور را در بافرهای داخلی ذخیره می کند و تا زمانی که کل پاسخ بافر نشود، شروع به ارسال داده به client نمی کند. سرورهای پروکسی شده میتوانند در سریع ترین زمان ممکن پاسخ خود را برگردانند تا برای ارائه ی درخواست های دیگر در دسترس باشند. بافر کردن به بهینه سازی عملکرد کلاینت های کند (slow) کمک میکنه چون nginx پاسخ را تا زمانی که کلاینت نیاز داره تا کل آن را دریافت کنه، بافر میکنه.

زمانی که بافر پروکسی غیرفعال است، nginx فقط بخش اول پاسخ سرور را قبل از شروع ارسال به کلاینت در یک بافر که به صورت پیش فرض به اندازه یک memery page است بافر میکند. (بسته به نوع سیستم عامل 4KB یا 8KB). معمولا این مقدار برای response header کافی است. سپس NGINX همزمان با دریافت پاسخ، پاسخ را برای مشتری ارسال می‌کند و سرور را مجبور می‌کند تا زمانی که NGINX بتواند بخش پاسخ بعدی را accept کند به صورت idle قرار بگیرد.

خیلی شگفت انگیزه که بعضیا بافر پروکسی را غیرفعال میکنند. شاید هدفشون کاهش تاخیر تجربه شده توسط کلاینتها باشه، اما تأثیر اون ناچیزه در حالی که عوارض جانبی متعدد داره: با غیرفعال کرد بافر پروکسی، rate limit و caching حتی در صورت پیکربندی، دیگه کار نمیکنه و عملکرد هم ضعیف میشه.

فقط در یک سری موارد بسیار محدود ممکنه غیرفعال کردن بافر پروکسی منطقی باشه (مثل long polling) . بنابراین ما به شدت از تغییر این پیش فرض خودداری می کنیم. پیشنهاد میکنم این مقاله را بخونید (NGINX Plus Admin Guide)




اشتباه ۶ : استفاده نامناسب از دستور if

استفاده از دستورالعمل if بیشتر به صورت تریک است، به خصوص در بلوک های {}location. اغلب اون چیزی که ما انتظار داریم را انجام نمیده و حتی ممکنه باعث خطاهایی هم بشه. در واقع از اونجایی استفاده ازش ترسناکه که یک مقاله در ویکی nginx با عنوان if is Evil وجود داره که پیشنهاد میکنم برای جزییات بیشتر از مشکلات و راه پرهیز از آنها، اونو بخونید.

به طور کلی، دستوراتی که معمولا میتونید به صورت امن در بلاک {}if استفاده کنید return و rewrite هستند. مثال زیر، از دستور if برای تشخیص درخواست هایی که هدر X-Test دارند استفاده میکنه. nginx خطای 430 (Request Header Fields Too Large) را برمیگردونه، آن را از location نامگذاری شده @error_430 میگیره و درخواست را به upstream به نام b پروکسی می کنه.

برای این مورد و بسیاری دیگر از استفاده های if، اغلب امکان اجتناب از دستورالعمل وجود داره. در مثال زیر، زمانی که یک درخواست شامل هدر X-Test است، بلاک map مقدار متغیر upstream$ را b ست میکنه و درخواست به گروه upstream با اون نام پروکسی میشه




اشتباه ۷ : بررسی بیش از اندازه health check

پیکربندی چندین سرور برای درخواست‌های پروکسی در یک upstream group (به عبارت دیگر، اضافه کردن دستور proxy_pass یکسان در بلوک‌های چند {}server) کاملاً معمول هست. اشتباه، استفاده از دستور health_check در همه بلاک های {}server است. این دستور فقط یک بار اضافه بر روی upstream هست و هیچ اطلاعات اضافه ای را ارائه نمیکنه.

راه حل این هست که فقط یک health check به ازای هر {}upstream تعریف بشه. اینجا ما یک health check برای یک upstream group به نام b در یک location نامگذاری شده تعریف کردیم

در پیکربندی‌های پیچیده، می‌تواند مدیریت را برای گروه‌بندی همه location های health check در یک سرور مانند این مثال، ساده‌تر کرد.




اشتباه ۸ :‌ دسترسی نا امن به metric ها

متریک های پایه ای در مورد عملکرد NGINX در ماژول Stub Status در دسترس است.برای فعال سازی metrics collection دستور stub_status یا api به ترتیب در یک بلوک {}server یا {}location اضافه کنید که تبدیل به URLی می شود که برای مشاهده metric ها به آن دسترسی دارید.

برخی از این متریک ها شامل اطلاعات حساسی هستند که میتواند برای حمله به وب سایت یا اپ های شما که توسط nginx پروکسی شده است مورد استفاده قرار گیرد و اشتباهی که رخ میدهد این است که برای این آدرس محدودیت دسترسی تنظیم نشده است. در ادامه چند روش برای امن کردن آدرس متریک ها را ارائه میدیم.

در پیکربندی زیر، هر شخصی در اینترنت میتواند به متریک ها به آدرس http://example.com/basic_status دسترسی داشته باشد.

محافظت از متریک ها با استفاده از HTTP Basic Authentication

به منظور محافظت از متریک ها به وسیله رمز عبور میتوانید از HTTP Basic Authentication استفاده کرد. برای این کار از دو دستور auth_basic و auth_basic_user_file استفاده میشود. فایل htpasswd شامل لیستی از نام کاربری و رمز عبور کاربرانی است که می توانند متریک ها را مشاهده کنند.

محافظت از متریک ها با دستورات allow و deny

اگر نمی‌خواهید کاربران مجاز مجبور به ورود به سیستم شوند، و آدرس‌های IP را که از طریق آنها به متریک ها دسترسی خواهند داشت، می‌دانید، گزینه دیگر دستور allow است.می توانید آدرس های IPv4 و IPv6 و محدوده CIDR را مشخص کنید. دستور deny all مانع از دسترسی از هر آدرس دیگری می شود.

ترکیب دو روش

اگر بخواهیم هر دو روش را با هم ترکیب کنیم چه؟ ما می‌توانیم به کلاینت اجازه دهیم از آدرس‌های خاص بدون رمز عبور به متریک ها دسترسی داشته باشند و همچنان برای کلاینتهایی که از آدرس‌های مختلف وارد می‌شوند، نیاز به ورود به سیستم داشته باشند. برای این ما از دستور satisfy any استفاده می کنیم. این دستور به NGINX می‌گوید که اجازه دسترسی به کلاینت‌هایی را بدهد که یا با HTTP Basic وارد می‌شوند یا از یک آدرس IP از پیش تأیید شده استفاده می‌کنند. برای امنیت بیشتر، می‌توانید satisfy را روی all تنظیم کنید تا حتی افرادی که از آدرس‌های خاصی آمده‌اند نیاز به ورود داشته باشند.

این پیکربندی فقط به کلاینت هایی که از شبکه 96.1.2.23/32 یا localhost می‌آیند، بدون رمز عبور دسترسی دارند. از آنجایی که دستور در سطح {}server تعریف شده‌اند، محدودیت‌های یکسانی برای API و داشبورد اعمال می‌شود. به عنوان یک نکته جانبی، پارامتر write=on به api به این معنی است که این کلاینت ها می توانند از API برای ایجاد تغییرات پیکربندی نیز استفاده کنند.

برای اطلاعات بیشتر درباره پیکربندی API و داشبورد، به NGINX Plus Admin Guide مراجعه کنید.




اشتباه ۹ :‌استفاده از ip_hash در زمانی که همه ی ترافیک از یک بلاک 24/ CIDR می آید.

الگوریتم ip_hash ترافیک را بین سرورهای داخل یک {}upstream براساس hash آدرس ip کلاینت، پخش میکند. کلید hashing، سه اکتت اول آدرس IPV4 یا کل آدرس IPV6 است. این روش پایداری session را ایجاد می کند، به این معنی که درخواست های یک کلاینت همیشه به همان سرور اولیه ارسال می شود، مگر زمانی که آن سرور در دسترس نباشد.

فرض کنید ما nginx را به عنوان یک reverse proxy بر روی یک شبکه خصوصی برای high availability نصب کرده ایم. ما چند فایروال، روتر، لودبالانسر لایه ۴ و gateway های مختلفی را در جلوی NGINX قرار می‌دهیم تا ترافیک از منابع مختلف را بپذیریم و اون را به nginx میدیم تا برای به صورت reverse proxy به upstream server ها بدهد. پیگربندی زیر یک نمونه اولیه است :

اما یه مشکل وجود داره. همه دستگاه‌ها در یک شبکه 10.10.0.0/24 هستند، بنابراین برای NGINX به نظر می‌رسد که تمام ترافیک از آدرس‌هایی در آن رنج CIDR می‌آید. به یاد داشته باشید که الگوریتم ip_hash سه اکتت اول یک آدرس IPv4 را هش می کند. در استقرار ما، سه اکتت اول برای هر کلاینت یکسان است (10.10.0) بنابراین hash برای همه آنها یکسان است و هیچ مبنایی برای توزیع ترافیک در سرورهای مختلف وجود ندارد.

برای حل این موضوع از الگوریتم توزیع بار hash با پارامتر $binary_remote_addr به عنوان hash key استفاده کنید. این متغیر آدرس کامل کلاینت را می گیرد و آن را به یک باینری تبدیل می کند که 4 بایت برای آدرس IPv4 و 16 بایت برای آدرس IPv6 است. اکنون هش برای هر دستگاه متفاوت است و تعادل بار همانطور که انتظار می رود کار می کند.

ما همچنین پارامتر consistent را برای استفاده از روش هش کردن ketama به جای پیش‌فرض در نظر می‌گیریم.این امر تعداد کلیدهایی را که در هنگام تغییر مجموعه سرورها به upstream server ها remap می شوند تا حد زیادی کاهش می دهد که باعث بهبود عملکرد میشود.




اشتباه ۱۰ : عدم استفاده از گروه های upstream

فرض کنید از NGINX برای یکی از ساده‌ترین موارد استفاده، به‌عنوان یک reverse proxy برای یک برنامه backend مبتنی بر NodeJS که در پورت 3000 گوش می‌دهد، استفاده می‌کنید. یک پیکربندی معمول ممکن است به این صورت باشد:

دستورالعمل proxy_pass به NGINX میگه که درخواست‌های کلاینت به کجا را ارسال کند. تنها کاری که NGINX باید انجام دهد این است که hostname را به آدرس IPv4 یا IPv6 تبدیل کند. هنگامی که اتصال برقرار شد NGINX درخواست ها را به آن سرور ارسال می کند.

اشتباه اینجا این است که فکر میکنیم چون فقط یک سرور وجود دارد - و بنابراین دلیلی برای پیکربندی load balancer وجود ندارد - ایجاد یک بلوک {}upstream بی معنی است. در واقع، یک بلوک {}upstream چندین ویژگی را آنلاک می کند که باعث بهبود عملکرد میشه، همانطور که در این پیکربندی نشان داده شده است:
تنظیمات upstream در nginx
تنظیمات upstream در nginx

دستورالعمل zone یک zone حافظه مشترک ایجاد می کند که در آن تمام worker processهای در یک host می توانند به پیکربندی و اطلاعات وضعیت سرورهای upstream دسترسی داشته باشند. چندین گروه upstream می توانند zone را به اشتراک بگذارند.

دستور server دارای چندین پارامتر است که می توانید از آنها برای تنظیم رفتار server استفاده کنید. در این مثال، شرایطی را که NGINX برای تعیین unhealthy بودن سرور و در نتیجه واجد شرایط بودن برای پذیرش درخواست‌ها استفاده می‌کند، تغییر داده‌ایم. در اینجا اگر تلاش ارتباطی حتی یک بار در هر دوره 2 ثانیه ای شکست بخورد (به جای پیش فرض یک بار در دوره 10 ثانیه). سرور را unhealthy در نظر می گیرد. ما این تنظیم را با دستور proxy_next_upstream ترکیب می‌کنیم تا اون چیزی را که NGINX یک تلاش ارتباطی ناموفق در نظر می‌گیرد، پیکربندی کنیم، در این صورت درخواست‌ها را به سرور بعدی در گروه upstream ارسال می‌کند.به شرایط پیش‌فرض خطا و مهلت زمانی، http_500 را اضافه می‌کنیم تا NGINX یک کد HTTP 500 (خطای سرور داخلی) را از یک سرور upstream برای نشان دادن تلاش ناموفق در نظر بگیرد.

دستورالعمل keepalive تعداد کانکشن های بیکار keepalive به سرورهای upstream که در cache هر worker process حفظ می شوند، تنظیم می کند.


منبع