طرح‌چه

لود بالانس |‌ Nginx from stratch

11 روز پیش

لود بالانس |‌ Nginx from stratch
با لود بالانس آشنا میشیم و یاد میگیریم چطور از Nginx به عنوان لود بالانسر استفاده کنیم.

لود بالانس (Load Balancing) به معنی توزیعِ ترافیکِ ورودی (برای مثال درخواست‌های کاربران) بین چندین سرور است تا هیچ سروری بیش از حد زیر فشار قرار نگیرد و کارایی سیستم حفظ شود.

توزیع ترافیک بین چندین سرور باعث افزایش در دسترس بودن و همچنین مقیاس پذیری بیشتر سرویس های ما میشود.

  • افزایش در دسترس بودن (High Availability): اگر یکی از سرورها از کار بیفتد، درخواست‌ها به سرورهای دیگر هدایت می‌شوند.

  • افزایش مقیاس‌پذیری (Scalability): می‌توان با اضافه کردن سرورهای جدید به سادگی به حجم بیشتری از درخواست‌ها پاسخ داد.

ابزارهای مختلفی برای لود بالانس (توزیع ترافیک) وجود دارد که یکی از رایج‌ترین آن‌ها nginx است. با استفاده از nginx میتوان ترافیک HTTP, TCP و حتی UDP را لود بالانس کرد.

لود بالانسینگ درخواست های HTTP

در قسمت های قبل گفتیم که فایل /etc/nginx/nginx.conf جاییه که nginx از آنجا باقی تنظیمات رو لود میکنه. بیاید یه نگاهی به این فایل بندازیم:

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

میتونیم ببینیم که یک بلاک http وجود دارد. بلاک http تنظیمات مرتبط با درخواست های http را انجام میدهد. در آخرین خط این بلاک با استفاده از دستور include تمامی فایل هایی که دارای پسوند conf باشند را به عنوان تنظیمات از مسیر /etc/nginx/conf.d/ لود میکند. این یعنی هر فایلی که با پسوند conf در مسیر /etc/nginx/conf.d/ قرار دهیم, به عنوان تنظیماتی برای درخواست های http مورد استفاده قرار میگیرد.

با استفاده از دانشی که تا اینجا کسب کردیم، می‌توانیم یک لود بالانسر با nginx پیاده‌سازی کنیم تا درخواست‌ها را بین چندین سرور توزیع کند. فرض کنید چند نسخه فعال از یک اپلیکیشن در حال اجرا هستند و قصد داریم ترافیک ورودی را بین آن‌ها لود بالانس کنیم. برای این کار، می‌توان سه سرور را شبیه‌سازی کرده و از یک nginx به عنوان لود بالانسر برای مدیریت و توزیع ترافیک استفاده کرد.

برای شبیه سازی , با استفاده از خود nginx سه تا سرور ایجاد کنیم. برای اینکار میتوانید یک فایل با نام mock-server.conf در مسیر /etc/nginx/conf.d/ ایجاد کنید و تنظیمات زیر را درون آن قرار دهید:

# service 1
server {
    listen 8080 default_server;

    location / {
        default_type text/html;

        return 200 '
            <!DOCTYPE html>
            <html>
                <head>
                    <link rel="icon" href="data:image/png;base64,iVBORw0KGgo=">
                </head>
                <body style="background-color: #ff6b6b; color: white; text-align: center;">
                    <h1>Hello from Server 8080</h1>
                </body>
            </html>
        ';
    }
}

# service 2
server {
    listen 8081 default_server;

    location / {
        default_type text/html;

        return 200 '
            <!DOCTYPE html>
            <html>
                <head>
                    <link rel="icon" href="data:image/png;base64,iVBORw0KGgo=">
                </head>
                <body style="background-color: #4facfe; color: white; text-align: center;">
                    <h1>Hello from Server 8081</h1>
                </body>
            </html>
        ';
    }
}

# service 3
server {
    listen 8082 default_server;

    location / {
        default_type text/html;

        return 200 '
            <!DOCTYPE html>
            <html>
                <head>
                    <link rel="icon" href="data:image/png;base64,iVBORw0KGgo=">
                </head>
                <body style="background-color: #56ab2f; color: white; text-align: center;">
                    <h1>Hello from Server 8082</h1>
                </body>
            </html>
        ';
    }
}

با استفاده از دستورات بالا سه سرور nginx ایجاد کردیم که به ترتیب روی پورت های 8080 , 8081 , 8082 به درخواست های دریافتی پاسخ میدهند. با استفاده از هشتک یا # میتوان توضیحات (comment) گذاشت. کامنت یا توضیحات برای توسعه دهنده ها گذاشته میشود تا بعدا با خواندن آنها, تنظیمات نوشته شده را بهتر درک کنند.

حالا یک لود بالانسر ایجاد میکنیم و آن را طوری تنظیم میکنیم که درخواست ها را بین آنها توزیع کند (من فایل تنظیماتم را load-balancer.conf نامگذاری میکنم.):

upstream backend01 {
    server localhost:8080 weight=1;
    server 127.0.0.1:8081 weight=1;
    server 127.0.0.1:8082 weight=1;
}

server {
    listen 8015;

    location / {
        proxy_pass http://backend01;
    }
}

در این مثال، با استفاده از upstream گروهی از سرورها با نام backend01 تعریف می‌کنیم. آدرس سرورها را با کلمه کلیدی server مشخص کرده و با استفاده از weight تعیین می‌کنیم که بار ترافیک به چه نسبتی میان آن‌ها توزیع شود. 

در بلاک upstream در هنگام تعریف server ها میتوان از IP و یا URL برای مشخص کردن آدرس مورد نظر استفاده کرد.

در مثال بالا weight برای همه سرورهای مشخص شده برابر است بدین صورت درخواست ها به صورت یکسان بین آنها توزیع میشود. در صورتی که weight زیاد تر شود درخواست ها به همان میزان نیز بر روی یک سرور بیشتر توزیع میشوند.

در ادامه یک بلاک server ایجاد کرده‌ایم که روی پورت 8015 تمامی درخواست‌های ورودی را دریافت می‌کند. اگر آدرس درخواست با مسیر تعریف‌شده در بخش location مطابقت داشته باشد، درخواست به سرورهای گروه backend01 پروکسی می‌شود. این انتقال با استفاده از دستور proxy_pass انجام می‌شود.

برای تطبیق تمامی درخواست‌ها از علامت / در ابتدای بخش location استفاده می‌کنیم. در این حالت، اگر یکی از آدرس‌های localhost:8015 یا 127.0.0.1:8015 را در مرورگر باز کنیم، با هر بار ارسال درخواست، به یکی از سه سروری که در ابتدا تنظیم کرده‌ایم متصل می‌شویم و پاسخ از همان سرور برای ما ارسال می‌شود.

در برخی موارد ممکن است یکی از سرورها توان پردازشی بالاتری داشته باشد و قادر باشد به درخواست‌های بیشتری پاسخ دهد. در این حالت می‌توان با افزایش مقدار weight در بلاک upstream، سهم بیشتری از ترافیک را به آن سرور اختصاص داد.

upstream backend02 {
    server localhost:8080 weight=1;
    server 127.0.0.1:8081 weight=1;
    server localhost:8082 weight=3;
}

server {
    listen 8016;

    location / {
        proxy_pass http://backend02;
    }
}

لود بالانس TCP و UDP

برای لود بالانس TCP و UDP بلاک های upstream و server را همانند قبل میتوان استفاده کرد با این تفاوت که این بلاک ها باید درون بلاک دیگری به نام stream قرار بگیرند. برای اینکار به انتهای /etc/nginx/nginx.conf میتوان دستورات زیر را اضافه کرد:

stream {
	include /etc/nginx/stream_conf.d/*.conf;
}

با اینکار تمامی تنظیماتی که در مسیر ‍/etc/nginx/stream_conf.d/ تعریف کنیم برای مدیریت درخواست های TCP و UDP استفاده خواهند شد.

به عنوان مثال برای لود بالانس TCP فرض کنید دو سرور از دیتابیس MySQL داریم و درخواست ها را بین این دو سرور میخواهیم توزیع کنیم. برای اینکار میتونیم یک فایل با نام mysql-db-load-balancer.conf در مسیر stream_conf.d تعریف کنیم و سپس تنظیمات زیر رو درون آن قرار دهیم:

upstream mysql-nodes {
    server mysql-node-01:3306 weight=1;
    server mysql-node-02:3306 weight=1;
}

server {
    listen 3305;

    proxy_pass mysql-nodes;
}

در مثال بالا از location استفاده ای نکردیم چون در درخواست های TCP دیگر مسیر درخواست معنایی ندارد.

برای لود بالانس در حالت udp کافیست در listen از کلمه udp استفاده کنیم تا Nginx متوجه بشه باید درخواست های روی پروتکل UDP را دریافت کند.

upstream my-nodes {
    server 127.0.0.1:2200 weight=1;
    server 127.0.0.1:3200 weight=1;
}

server {
    listen 3200 udp;

    proxy_pass my-nodes;
}

در لود بالانس TCP و UDP باید در نظر داشته باشیم که Nginx به صورت پیش فرض رفتار زیر را دارد:

  • هر کانکشن TCP یا UDP که باز شود تا وقتی که کلاینت خودش کانکشن رو نبندد، به همان صورت به سروری که بعد از لود بالانس متصل شده است ارتباط خود را حفظ میکند.

  • کانکشن های TCP یا UDP به خاطر stateful  بودن، بعد از باز شدن به صورت persistent می‌مونن. یعنی اگر برنامه‌ی کلاینت (یا مثلا mysql client) connection pooling انجام بده یا connection رو باز نگه داره، عملا همون connection همیشه به یک backend وصل است چون Nginx تا وقتی connection بازه است لود بالانس دیگری انجام نمی شود.

الگوریتم های لود بالانس

الگوریتم هایی که nginx روی ورژن رایگان خود ساپورت میکند به صورت زیر است:

  • راند رابین (Round robin)

  • کمترین کانکشن (Least connection)

  • جنریک هش (Generic hash)

  • تصادفی (Random)

  • آیپی هش (IP hash)

لود بالانس با الگوریتم Round robin

در روش Round Robin، درخواست‌ها به ترتیب و به صورت چرخشی بین سرورهای موجود توزیع میشوند؛ اینکار بدون توجه به اینکه چه مقدار بار روی هر سرور توزیع شده یا چقدر سریع به درخواست ها پاسخ میدهد انجام میشود.

در nginx به صورت پیش فرض (default) از روش راند رابین استفاده میشود. بنابراین اگر روش دیگری را انتخاب نکرده باشید لود به صورت round robin روی سرورهای شما توزیع میشود.

upstream backend02 {
    server localhost:8080 weight=1;
    server localhost:8082 weight=3;
}

server {
    listen 8016;

    location / {
        proxy_pass http://backend02;
    }
}

لود بالانس با الگوریتم Least connection

در این روش، هر درخواست جدید به سروری فرستاده می‌شود که در حال حاضر کمترین تعداد اتصال فعال (Active Connections) را دارد. یعنی Nginx ابتدا تعداد درخواست فعال روی هر سرور را بررسی میکند و سپس درخواست جدید را به سروری که کمترین بار فعلی روی اون هست ارسال میکند.

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

upstream backend02 {
	least_conn;

    server localhost:8080 weight=1;
    server localhost:8082 weight=3;
}

server {
    listen 8016;

    location / {
        proxy_pass http://backend02;
    }
}

لود بالانس با الگوریتم Generic hash

روش Generic Hash Load Balancing (یا hash-based load balancing) بر اساس یک الگوی هش (Hash Function) کار می‌کند. این روش بیشتر زمانی کاربرد دارد که بخواهیم درخواست‌های مشابه همیشه به یک سرور مشخص هدایت شوند.

در این روش، Nginx یک مقدار مشخص از هر درخواست (مثل IP کلاینت، URL، یا هر پارامتر دلخواه) را دریافت می کند و نتیجه‌ی حاصل از هش شدن آن مقدار, تعیین میکند که درخواست به کدام سرور ارسال شود.

upstream backend02 {
	hash $remote_addr;

    server localhost:8080 weight=1;
    server localhost:8082 weight=3;
}

server {
    listen 8016;

    location / {
        proxy_pass http://backend02;
    }
}

وقتی از الگوریتم Hash برای لود بالانس استفاده می‌کنیم، یه نکته مهم اینه که اگر یکی از سرورها به هر دلیلی به جمع سرورها اضافه یا حذف بشه، ساختار هش کاملاً به‌ هم می‌ریزه و مسیر بیشتر درخواست‌ها تغییر می‌کنه. این یعنی کاربرهایی که قبلاً به یک سرور مشخص هدایت می‌شدن، حالا ممکنه به سرورهای دیگه منتقل بشن. برای اینکه این اتفاق کمتر پیش بیاد، می‌تونیم از پارامتر consistent استفاده کنیم. این پارامتر باعث می‌شه توزیع درخواست‌ها در زمان اضافه یا حذف شدن سرورها حداقل تغییر ممکن رو داشته باشه.

upstream backend02 {
	hash $remote_addr consistent;

    server localhost:8080 weight=1;
    server localhost:8082 weight=3;
}

server {
    listen 8016;

    location / {
        proxy_pass http://backend02;
    }
}

لود بالانس با الگوریتم Random

در این روش، Nginx به‌طور تصادفی یک سرور از لیست سرورهای موجود در upstream انتخاب می‌کند و درخواست را به آن ارسال می‌کند. این الگوریتم بدون در نظر گرفتن تعداد اتصالات فعال یا سرعت پاسخگویی صرفاً به صورت کاملاً اتفاقی عمل می‌کند. در این روش میتوان برای سرورهای تعریف شده weight مشخص کرد.

upstream backend02 {
	random;

    server localhost:8080 weight=1;
    server localhost:8082 weight=3;
}

server {
    listen 8016;

    location / {
        proxy_pass http://backend02;
    }
}

در روش رندم میتوان در جلوی random از یکسری متد برای مشخص کردن رندم سرورها استفاده کرد. تنها متدی که در ورژن رایگان nginx وجود دارد two است. در صورتی که به صورت random two; نحوه توزیع شدن درخواست ها را مشخص کنیم, nginx از بین لیست سرورهایی که در upstream مشخص کرده ایم ابتدا دو سرور را به صورت تصادفی انتخاب میکند و سپس ترافیک را به صورت least_conn بین این دو سرور پخش میکند. بدین صورت همیشه دو سرور انتخاب میشوند و ترافیک به سروری که کانکشن فعال کمتری دارد ارسال میشود.

لود بالانس با الگوریتم IP hash

در هنگام استفاده از روش آیپی هش, nginx بر اساس آدرسِ IP کلاینت, یک مقدار هش تولید می‌کند و همیشه درخواست‌های کلاینت با همان IP را به یک سرور ثابت هدایت می‌کند.

این روش تضمین می‌کند که کلاینت‌ها تا زمانی که سرور بالادستی یکسانی در دسترس باشد، به آن سرور پروکسی می‌شوند.

این روش معمولا زمانی کاربرد دارد که نیاز داریم یک کلاینت همیشه با یک سرور خاص در ارتباط باشد. برای مثال زمانی که کش سمت سرور وجود دارد یا session های (معمولا برای اهراز هویت استفاده میشود) ذخیره شده به صورت محلی در هر سرور نگهداری میشوند.

همچنین باید در نظر داشته باشیم که اگر بیشتر کلاینت‌ها از یک بازه IP خاص باشند، ممکن است ترافیک ورودی به صورت ناعادلانه روی سرورها توزیع شود.

در Nginx روش IP Hash فقط برای HTTP قابل استفاده است. همچنین میتوان برای سرورهای تعریف شده weight مشخص کرد.

upstream backend02 {
	ip_hash;

    server localhost:8080 weight=1;
    server localhost:8082 weight=3;
}

server {
    listen 8016;

    location / {
        proxy_pass http://backend02;
    }
}

پایش سلامت (Health check)

در nginx دو روش passive و active برای بررسی سلامت سرورها وجود دارد. روش passive در ورژن رایگان nginx موجود است.

  • در روش Active، لود بالانسر به صورت دوره‌ای و مستقیم درخواست‌هایی برای بررسی وضعیت سرورها ارسال می‌کند. یعنی nginx به‌طور فعال سرورها را بررسی می‌کند تا ببیند آیا هنوز در دسترس و سالم هستند یا نه.

  • در روش Passive، لود بالانسر به‌صورت مستقیم وضعیت سرورها را بررسی نمی‌کند، بلکه فقط زمانی متوجه بروز مشکل می‌شود که در حین پردازش درخواست‌های واقعی کاربران، خطایی رخ دهد یا به بیان دیگر nginx فقط زمانی متوجه خرابی سرور می‌شود که پاسخ‌های خطا از سمت سرور در جریان درخواست‌های واقعی کاربران دریافت شود.

ورژن رایگان Nginx تنها روش Passive را پشتیبانی میکند. برای تنظیم health check به صورت passive میتونیم تنظیمات زیر رو انجام بدیم:

upstream backend {
  server backend1.example.com:1234 max_fails=3 fail_timeout=3s;
  server backend2.example.com:1234 max_fails=3 fail_timeout=3s;
}

با استفاده از تنظیمات بالا, سلامت سرور بالادستی به صورت غیر فعال و از طریق نظارت بر پاسخ های دریافتی به درخواست های کلایند بررسی می شود. در صورتی که درخواست های ارسال شده به سرور بالادستی (در اینجا backend نامگذاری شده) با خطا مواجهه شوند یا با timeout مواجهه شوند, nginx متوجه میشود که سرور بالادستی تعریف شده در وضعیت سلامت قرار ندارد و درخواستی به آن ارسال نمیکند.

پارامترهای max_fails و fail_timeout مربوط به تشخیص خطا در (Passive Health Check) سرورهای بالادستی (upstream) هستند. این دو کمک می‌کنند که Nginx بفهمد کِی یک سرور backend موقتاً از دسترس خارج شده.

  • max_fails=3 یعنی اگر Nginx طی بازه زمانی مشخص شده (که همون fail_timeout هست) سه بار پشت سر هم از این سرور پاسخی نگیره یا خطا دریافت کنه (مثل timeouts یا خطای 5xx)، اون سرور رو "failed" یا موقتاً غیرفعال فرض می‌کنه.

  •  fail_timeout=3s یعنی بازه زمانی که توش Nginx این خطاها رو می‌شماره. همچنین زمانی هست که بعد از شناسایی failure، Nginx اون سرور رو از مدار خارج می‌کنه و سعی می‌کنه در درخواست‌های بعدی به سرورهای سالم وصل بشه. بعد از گذشت این زمان، Nginx دوباره تلاش می‌کنه به اون سرور وصل بشه.

فرض کنید Nginx طی ۳ ثانیه سه بار تلاش می‌کنه به backend1 وصل بشه و هر سه بار خطا می‌گیره. Nginx نتیجه می‌گیره که backend1 فعلاً خرابه و دیگه به اون سرور درخواست نمی‌فرسته. بعد از ۳ ثانیه دوباره بهش یه درخواست تستی می‌فرسته. اگه موفق بشه، دوباره اون سرور رو فعال می‌کنه.

میتوانید به نمونه کدِ مثال هایی که در این قسمت وجود داشت از طریق گیت هاب  دسترسی داشته باشید کنید.


قسمت قبل: وب سرور برای فایل های استاتیک | Nginx from scratch

قسمت بعد: مدیریت ترافیک | Nginx from scratch


دیدگاه ها