Nginx چیست؟ آموزش کامل Reverse Proxy و Load Balancing برای FastAPI

در این آموزش عملی یاد می‌گیرید Nginx چیست و چگونه یک برنامه FastAPI را با Reverse Proxy، Load Balancing، Docker و Streaming در محیط واقعی اجرا کنید. پروژه کامل اتصال به API هوش مصنوعی درواره نیز ارائه شده است.

Share
آموزش Nginx، Reverse Proxy و Load Balancing برای FastAPI

وقتی یک برنامه FastAPI، سرویس هوش مصنوعی یا REST API را روی سرور اجرا می‌کنید، معمولاً برنامه مستقیماً برای کاربران اینترنت قابل دسترسی نیست. در محیط توسعه ممکن است دستور زیر کافی باشد:

uvicorn app.main:app --host 0.0.0.0 --port 8000

اما در محیط عملیاتی یا Production، باید مسائل بیشتری را مدیریت کنید:

  • اتصال دامنه به برنامه
  • دریافت درخواست روی پورت‌های استاندارد
  • توزیع ترافیک بین چند نمونه برنامه
  • مدیریت اتصال‌های طولانی
  • پشتیبانی از پاسخ جریانی یا Streaming
  • ثبت لاگ و زمان پاسخ
  • محدود کردن اندازه درخواست
  • تحویل فایل‌های استاتیک
  • انتقال اطلاعات واقعی کاربر به Backend
  • جداسازی برنامه داخلی از اینترنت

یکی از رایج‌ترین ابزارها برای انجام این کارها Nginx است.

در این آموزش ابتدا با معماری و مفاهیم اصلی Nginx آشنا می‌شویم و سپس یک پروژه واقعی می‌سازیم که شامل دو نمونه FastAPI، یک Nginx Load Balancer، Docker Compose و اتصال به API هوش مصنوعی درواره است.

Nginx چیست؟

Nginx که معمولاً «انجین‌اکس» تلفظ می‌شود، یک وب‌سرور و Reverse Proxy پرکاربرد است. این نرم‌افزار می‌تواند درخواست‌های ورودی کاربران را دریافت کند و آن‌ها را به برنامه‌هایی مانند FastAPI، Django، Laravel، Node.js یا Spring Boot بفرستد.

Nginx فقط یک وب‌سرور برای نمایش فایل‌های HTML نیست. از آن می‌توان برای کاربردهای زیر استفاده کرد:

  • Web Server
  • Reverse Proxy
  • HTTP Load Balancer
  • سرو فایل‌های Static
  • مدیریت Cache
  • مسیریابی درخواست‌ها
  • مدیریت اتصال‌های طولانی
  • پشتیبانی از WebSocket
  • قرار گرفتن جلوی REST API
  • مدیریت پاسخ‌های Streaming و SSE

طبق مستندات رسمی، Nginx می‌تواند درخواست‌ها را به یک سرور دیگر منتقل کند و با تعریف گروه‌های upstream، ترافیک را میان چند نمونه برنامه توزیع کند. مستندات Reverse Proxy در Nginx

Nginx در معماری API چه جایگاهی دارد؟

یک معماری ساده بدون Nginx ممکن است به شکل زیر باشد:

Client
  |
  v
FastAPI :8000

در این حالت پورت برنامه مستقیماً روی اینترنت باز است و همه درخواست‌ها به یک نمونه FastAPI می‌رسند.

معماری مناسب‌تر به این شکل است:

Client
  |
  v
Nginx :80/:443
  |
  +----> FastAPI Instance 1 :8000
  |
  +----> FastAPI Instance 2 :8000

در این معماری، کاربر فقط با Nginx ارتباط دارد. Nginx درخواست را دریافت می‌کند و آن را به یکی از نمونه‌های FastAPI می‌فرستد.

سرویس FastAPI می‌تواند برای انجام پردازش‌های هوش مصنوعی به API درواره متصل شود:

کاربر
  |
  v
Nginx
  |
  v
FastAPI
  |
  v
API درواره
  |
  v
مدل هوش مصنوعی

این جداسازی باعث می‌شود بتوانید تعداد نمونه‌های برنامه را افزایش دهید، پیکربندی شبکه را متمرکز کنید و سرویس Backend را مستقیماً در معرض اینترنت قرار ندهید.

تفاوت Forward Proxy و Reverse Proxy چیست؟

مفهوم Proxy معمولاً به دو دسته تقسیم می‌شود.

Forward Proxy

در Forward Proxy، پروکسی به نمایندگی از کاربر با سرویس مقصد ارتباط برقرار می‌کند:

User -> Forward Proxy -> Website

وب‌سایت مقصد معمولاً پروکسی را به‌عنوان مبدأ اتصال مشاهده می‌کند.

Reverse Proxy

در Reverse Proxy، پروکسی جلوی سرورهای برنامه قرار می‌گیرد:

User -> Reverse Proxy -> Backend Server

کاربر معمولاً نمی‌داند کدام نمونه Backend درخواست را پردازش کرده است.

در این مقاله از Nginx به‌عنوان Reverse Proxy استفاده می‌کنیم.

چرا FastAPI را مستقیماً روی اینترنت منتشر نکنیم؟

Uvicorn یک ASGI Server مناسب برای اجرای FastAPI است، اما قرار دادن مستقیم آن در لبه عمومی شبکه معمولاً انعطاف‌پذیری معماری را کاهش می‌دهد.

Nginx می‌تواند لایه ورودی متمرکزی ایجاد کند و امکانات زیر را در اختیار برنامه قرار دهد:

  • مسیریابی چند سرویس روی یک دامنه
  • توزیع بار میان چند Backend
  • مدیریت Timeout
  • ثبت لاگ دسترسی
  • تنظیم Headerهای Proxy
  • پشتیبانی از پاسخ‌های Streaming
  • تحویل فایل‌های استاتیک
  • مدیریت دامنه و پورت ورودی

برای مثال می‌توانید مسیرهای مختلف را به سرویس‌های جداگانه بفرستید:

location /api/ {
    proxy_pass http://api_backend;
}

location /admin/ {
    proxy_pass http://admin_backend;
}

location /static/ {
    root /var/www/app;
}

اجزای اصلی تنظیمات Nginx

فایل پیکربندی Nginx معمولاً از چند Context اصلی تشکیل می‌شود:

events {
}

http {
    upstream backend {
        server app1:8000;
        server app2:8000;
    }

    server {
        listen 80;

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

بخش events

این بخش تنظیمات مرتبط با اتصال‌ها و Workerهای Nginx را در بر می‌گیرد:

events {
    worker_connections 1024;
}

بخش http

تنظیمات مربوط به ترافیک HTTP، لاگ‌ها، Upstreamها و سرورها در این بخش قرار می‌گیرند.

بخش upstream

در upstream مجموعه‌ای از Backendها تعریف می‌شود:

upstream fastapi_backend {
    server app1:8000;
    server app2:8000;
}

بخش server

هر بلوک server مشخص می‌کند Nginx روی چه پورت یا دامنه‌ای درخواست دریافت کند:

server {
    listen 80;
    server_name api.example.com;
}

بخش location

بلوک location برای مسیریابی URL استفاده می‌شود:

location /api/ {
    proxy_pass http://fastapi_backend;
}

دستور proxy_pass چه کاری انجام می‌دهد؟

دستور proxy_pass مقصد درخواست را مشخص می‌کند:

location / {
    proxy_pass http://fastapi_backend;
}

در این مثال، درخواست دریافتی به گروه fastapi_backend فرستاده می‌شود.

یکی از نکات مهم، تفاوت استفاده از / در انتهای آدرس است.

نمونه اول:

location /api/ {
    proxy_pass http://backend;
}

درخواست زیر معمولاً با همان URI به Backend منتقل می‌شود:

/api/users

نمونه دوم:

location /api/ {
    proxy_pass http://backend/;
}

در این حالت بخش متناظر با location جایگزین می‌شود و Backend ممکن است مسیر زیر را دریافت کند:

/users

این تفاوت کوچک یکی از دلایل رایج خطاهای 404 هنگام تنظیم Nginx است. رفتار دقیق proxy_pass و نحوه انتقال URI در مستندات رسمی ماژول Proxy توضیح داده شده است.

انتقال Headerهای واقعی کاربر

Backend باید بتواند اطلاعاتی مانند Host، IP و پروتکل درخواست اصلی را تشخیص دهد. برای این منظور Headerهای زیر تنظیم می‌شوند:

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;

کاربرد آن‌ها:

Headerکاربرد
Hostدامنه‌ای که کاربر درخواست کرده است
X-Real-IPIP اتصال ورودی به Nginx
X-Forwarded-Forزنجیره Proxyها و IPهای قبلی
X-Forwarded-Protoپروتکل اصلی مانند HTTP یا HTTPS

اگر برنامه از این Headerها برای تصمیم‌گیری مهم استفاده می‌کند، فقط باید Proxyهای مورد اعتماد را معتبر در نظر بگیرد. دریافت مستقیم X-Forwarded-For از اینترنت لزوماً به معنی معتبر بودن آن نیست.

Load Balancing چیست؟

Load Balancing یا توزیع بار یعنی درخواست‌ها بین چند نمونه از یک برنامه تقسیم شوند:

Request 1 -> App 1
Request 2 -> App 2
Request 3 -> App 1
Request 4 -> App 2

این کار می‌تواند ظرفیت پاسخ‌گویی، دسترس‌پذیری و امکان توسعه افقی برنامه را بهبود دهد.

در Nginx یک گروه Upstream می‌سازیم:

upstream ai_backend {
    server app1:8000;
    server app2:8000;
}

اگر روش مشخصی انتخاب نشود، Nginx به‌صورت پیش‌فرض از Round Robin استفاده می‌کند.

روش‌های توزیع بار در Nginx

Round Robin

روش پیش‌فرض Nginx است و درخواست‌ها را به‌ترتیب میان Backendها توزیع می‌کند:

upstream backend {
    server app1:8000;
    server app2:8000;
}

این روش برای درخواست‌هایی با زمان پردازش تقریباً مشابه مناسب است.

Least Connections

در این روش درخواست جدید به سروری فرستاده می‌شود که اتصال فعال کمتری دارد:

upstream backend {
    least_conn;

    server app1:8000;
    server app2:8000;
}

برای APIهای هوش مصنوعی، بعضی درخواست‌ها ممکن است چند ثانیه و بعضی دیگر مدت بیشتری طول بکشند. به همین دلیل least_conn در بسیاری از این سناریوها از Round Robin منطقی‌تر است.

Weighted Load Balancing

اگر یک سرور قدرت بیشتری دارد، می‌توان وزن بالاتری به آن اختصاص داد:

upstream backend {
    server app1:8000 weight=3;
    server app2:8000 weight=1;
}

در این مثال سهم تقریبی app1 از درخواست‌ها بیشتر است.

IP Hash

در این روش IP کاربر برای انتخاب Backend استفاده می‌شود:

upstream backend {
    ip_hash;

    server app1:8000;
    server app2:8000;
}

این روش می‌تواند کاربر را تا حدی به یک Backend ثابت متصل کند، اما برای برنامه‌های Stateless معمولاً بهتر است وابستگی به یک نمونه خاص ایجاد نشود.

Nginx روش‌هایی مانند Round Robin، Least Connections و IP Hash را برای توزیع بار ارائه می‌کند. جزئیات این روش‌ها در راهنمای رسمی HTTP Load Balancing آمده است.

معماری پروژه عملی

در پروژه این مقاله، اجزای زیر را اجرا می‌کنیم:

  • یک Nginx روی پورت 8080
  • دو نمونه یکسان FastAPI
  • اتصال Async به API درواره
  • یک مسیر معمولی برای دریافت پاسخ کامل
  • یک مسیر Streaming برای دریافت تدریجی پاسخ
  • یک مسیر تشخیص نمونه پردازش‌کننده
  • Health Check برای Containerها
  • لاگ زمان پاسخ Upstream

ساختار پروژه:

nginx-fastapi-ai/
├── app/
│   ├── __init__.py
│   └── main.py
├── nginx/
│   └── nginx.conf
├── .env
├── .gitignore
├── compose.yaml
├── Dockerfile
└── requirements.txt

مرحله اول: ساخت محیط پروژه

پوشه‌ها را ایجاد کنید:

mkdir -p nginx-fastapi-ai/app nginx-fastapi-ai/nginx
cd nginx-fastapi-ai
touch app/__init__.py

مرحله دوم: تعریف وابستگی‌ها

فایل requirements.txt را بسازید:

fastapi>=0.115,<1.0
uvicorn[standard]>=0.34,<1.0
openai>=1.68,<3.0
pydantic>=2.10,<3.0

SDK مورد استفاده با APIهای سازگار با OpenAI کار می‌کند. در این پروژه base_url روی آدرس API درواره تنظیم می‌شود.

مرحله سوم: ساخت برنامه FastAPI

فایل app/main.py را ایجاد کنید:

import json
import os
from contextlib import asynccontextmanager
from typing import AsyncIterator

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
from pydantic import BaseModel, Field


DARVAREH_API_KEY = os.environ["DARVAREH_API_KEY"]
DARVAREH_MODEL_ID = os.environ["DARVAREH_MODEL_ID"]
INSTANCE_NAME = os.getenv("INSTANCE_NAME", "unknown")


class ChatRequest(BaseModel):
    message: str = Field(min_length=1, max_length=8000)
    temperature: float = Field(default=0.3, ge=0, le=2)
    max_tokens: int = Field(default=800, ge=1, le=4000)


class ChatResponse(BaseModel):
    answer: str
    instance: str


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.ai_client = AsyncOpenAI(
        api_key=DARVAREH_API_KEY,
        base_url="https://api.darvareh.ir/v1",
        timeout=90.0,
        max_retries=2,
    )

    yield

    await app.state.ai_client.close()


app = FastAPI(
    title="Nginx FastAPI AI Demo",
    version="1.0.0",
    lifespan=lifespan,
)


@app.get("/health")
async def health():
    return {
        "status": "ok",
        "instance": INSTANCE_NAME,
    }


@app.get("/instance")
async def instance():
    return {
        "instance": INSTANCE_NAME,
    }


@app.post("/chat", response_model=ChatResponse)
async def chat(payload: ChatRequest, request: Request):
    response = await request.app.state.ai_client.chat.completions.create(
        model=DARVAREH_MODEL_ID,
        messages=[
            {
                "role": "system",
                "content": (
                    "شما یک دستیار فارسی دقیق هستید. "
                    "پاسخ را روشن، کاربردی و بدون ادعاهای اثبات‌نشده بنویس."
                ),
            },
            {
                "role": "user",
                "content": payload.message,
            },
        ],
        temperature=payload.temperature,
        max_tokens=payload.max_tokens,
    )

    answer = response.choices[0].message.content or ""

    return ChatResponse(
        answer=answer,
        instance=INSTANCE_NAME,
    )


@app.post("/chat/stream")
async def chat_stream(payload: ChatRequest, request: Request):
    async def generate() -> AsyncIterator[str]:
        try:
            stream = await request.app.state.ai_client.chat.completions.create(
                model=DARVAREH_MODEL_ID,
                messages=[
                    {
                        "role": "system",
                        "content": (
                            "شما یک دستیار فارسی دقیق هستید. "
                            "پاسخ را روشن، مرحله‌به‌مرحله و کاربردی بنویس."
                        ),
                    },
                    {
                        "role": "user",
                        "content": payload.message,
                    },
                ],
                temperature=payload.temperature,
                max_tokens=payload.max_tokens,
                stream=True,
            )

            async for chunk in stream:
                if await request.is_disconnected():
                    break

                content = chunk.choices[0].delta.content

                if not content:
                    continue

                data = json.dumps(
                    {
                        "type": "token",
                        "content": content,
                        "instance": INSTANCE_NAME,
                    },
                    ensure_ascii=False,
                )

                yield f"data: {data}\n\n"

            done = json.dumps(
                {
                    "type": "done",
                    "instance": INSTANCE_NAME,
                },
                ensure_ascii=False,
            )

            yield f"event: done\ndata: {done}\n\n"

        except Exception:
            error = json.dumps(
                {
                    "type": "error",
                    "message": "پردازش درخواست با خطا مواجه شد.",
                    "instance": INSTANCE_NAME,
                },
                ensure_ascii=False,
            )

            yield f"event: error\ndata: {error}\n\n"

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
        },
    )

چرا از AsyncOpenAI استفاده کردیم؟

FastAPI یک فریم‌ورک Async است. استفاده از Client ناهمگام باعث می‌شود برنامه هنگام انتظار برای پاسخ مدل، Thread اصلی را بی‌دلیل مسدود نکند.

این موضوع به‌خصوص زمانی اهمیت دارد که چند کاربر به‌طور هم‌زمان درخواست ارسال می‌کنند.

چرا Client را در lifespan ساختیم؟

ساخت Client برای هر درخواست باعث ایجاد سربار غیرضروری می‌شود. در این پیاده‌سازی، یک Client هنگام شروع هر نمونه برنامه ساخته و هنگام خاموش شدن آن بسته می‌شود.

چرا کلید API در Backend قرار دارد؟

کلید API نباید داخل JavaScript مرورگر، اپلیکیشن عمومی یا مخزن Git قرار بگیرد. کاربر با Backend شما ارتباط برقرار می‌کند و Backend درخواست را به درواره می‌فرستد.

مرحله چهارم: ساخت Dockerfile

فایل Dockerfile را بسازید:

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /code

COPY requirements.txt /code/requirements.txt

RUN pip install \
    --no-cache-dir \
    --upgrade \
    -r /code/requirements.txt

COPY app /code/app

EXPOSE 8000

CMD [
    "uvicorn",
    "app.main:app",
    "--host",
    "0.0.0.0",
    "--port",
    "8000"
]

ترتیب کپی فایل‌ها مهم است. ابتدا requirements.txt کپی می‌شود تا در صورت تغییر نکردن وابستگی‌ها، Docker بتواند از Cache لایه قبلی استفاده کند.

همچنین CMD به فرم Exec نوشته شده است. مستندات FastAPI این فرم را برای خاموش شدن صحیح برنامه و اجرای رویدادهای Lifespan توصیه می‌کنند. راهنمای رسمی اجرای FastAPI در Docker

مرحله پنجم: تنظیم Nginx

فایل nginx/nginx.conf را ایجاد کنید:

events {
    worker_connections 2048;
}

http {
    log_format upstream_timing
        '$remote_addr - $request_id '
        '"$request" $status $body_bytes_sent '
        'request_time=$request_time '
        'upstream_addr=$upstream_addr '
        'upstream_status=$upstream_status '
        'upstream_connect_time=$upstream_connect_time '
        'upstream_header_time=$upstream_header_time '
        'upstream_response_time=$upstream_response_time';

    access_log /var/log/nginx/access.log upstream_timing;
    error_log /var/log/nginx/error.log warn;

    upstream ai_backend {
        least_conn;

        server app1:8000 max_fails=3 fail_timeout=15s;
        server app2:8000 max_fails=3 fail_timeout=15s;

        keepalive 32;
    }

    server {
        listen 80;
        server_name _;

        client_max_body_size 1m;

        location = /nginx-health {
            access_log off;
            default_type application/json;
            return 200 '{"status":"ok","service":"nginx"}';
        }

        location /chat/stream {
            proxy_pass http://ai_backend;

            proxy_http_version 1.1;
            proxy_set_header Connection "";

            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 X-Request-ID $request_id;

            proxy_buffering off;
            proxy_cache off;

            proxy_connect_timeout 5s;
            proxy_send_timeout 120s;
            proxy_read_timeout 300s;

            add_header X-Request-ID $request_id always;
            add_header X-Accel-Buffering no always;
        }

        location / {
            proxy_pass http://ai_backend;

            proxy_http_version 1.1;
            proxy_set_header Connection "";

            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 X-Request-ID $request_id;

            proxy_connect_timeout 5s;
            proxy_send_timeout 60s;
            proxy_read_timeout 120s;

            add_header X-Request-ID $request_id always;
        }
    }
}

تحلیل تنظیمات Upstream

قسمت زیر دو نمونه FastAPI را معرفی می‌کند:

upstream ai_backend {
    least_conn;

    server app1:8000 max_fails=3 fail_timeout=15s;
    server app2:8000 max_fails=3 fail_timeout=15s;

    keepalive 32;
}

least_conn

درخواست جدید به نمونه‌ای فرستاده می‌شود که اتصال فعال کمتری دارد.

max_fails

تعداد خطاهایی را تعیین می‌کند که در بازه fail_timeout می‌تواند باعث شود Nginx یک Backend را موقتاً ناموفق در نظر بگیرد.

fail_timeout

بازه بررسی خطا و مدت کنار گذاشته شدن موقت Backend را کنترل می‌کند.

در نسخه متن‌باز Nginx، این رفتار یک Health Check غیرفعال یا Passive است؛ یعنی وضعیت Backend از نتیجه ترافیک واقعی تشخیص داده می‌شود. نباید آن را با Active Health Check دوره‌ای اشتباه گرفت.

keepalive

تعدادی اتصال آماده میان Nginx و Backendها نگه می‌دارد تا برای هر درخواست مجبور به ایجاد اتصال کاملاً جدید نباشیم.

چرا برای Streaming تنظیم جداگانه داریم؟

در پاسخ معمولی، Nginx می‌تواند پاسخ Backend را Buffer کند. اما در Streaming می‌خواهیم هر بخش از پاسخ بلافاصله به کاربر برسد.

به همین دلیل از تنظیم زیر استفاده شده است:

proxy_buffering off;

وقتی Buffering غیرفعال باشد، Nginx داده دریافتی از Backend را بدون انتظار برای تکمیل کل پاسخ به Client منتقل می‌کند. این رفتار در مستندات proxy_buffering توضیح داده شده است.

همچنین Timeout خواندن پاسخ را افزایش داده‌ایم:

proxy_read_timeout 300s;

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

مرحله ششم: ساخت Docker Compose

فایل compose.yaml را ایجاد کنید:

services:
  app1:
    build:
      context: .
    environment:
      DARVAREH_API_KEY: ${DARVAREH_API_KEY}
      DARVAREH_MODEL_ID: ${DARVAREH_MODEL_ID}
      INSTANCE_NAME: app1
    expose:
      - "8000"
    healthcheck:
      test:
        [
          "CMD",
          "python",
          "-c",
          "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
        ]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  app2:
    build:
      context: .
    environment:
      DARVAREH_API_KEY: ${DARVAREH_API_KEY}
      DARVAREH_MODEL_ID: ${DARVAREH_MODEL_ID}
      INSTANCE_NAME: app2
    expose:
      - "8000"
    healthcheck:
      test:
        [
          "CMD",
          "python",
          "-c",
          "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
        ]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      app1:
        condition: service_healthy
      app2:
        condition: service_healthy
    restart: unless-stopped

نکته مهم این است که پورت FastAPI با ports روی Host منتشر نشده است. دو Backend فقط داخل شبکه Docker در دسترس‌اند و ورودی عمومی از Nginx عبور می‌کند.

مرحله هفتم: تنظیم متغیرهای محیطی

فایل .env را بسازید:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

کلید API را بعد از ثبت‌نام در درواره دریافت کنید.

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

فایل .gitignore:

.env
__pycache__/
*.pyc
.venv/

هیچ‌گاه فایل .env واقعی را داخل مخزن عمومی Git قرار ندهید.

مرحله هشتم: اجرای پروژه

پروژه را Build و اجرا کنید:

docker compose up --build

وضعیت Containerها را ببینید:

docker compose ps

خروجی باید نشان دهد هر دو برنامه سالم هستند و Nginx روی پورت 8080 در دسترس است.

آزمایش Health Check

curl http://localhost:8080/health

نمونه خروجی:

{
  "status": "ok",
  "instance": "app1"
}

این درخواست از Nginx عبور کرده و به یکی از Backendها رسیده است.

Health Check خود Nginx:

curl http://localhost:8080/nginx-health

خروجی:

{
  "status": "ok",
  "service": "nginx"
}

تفاوت این دو مسیر مهم است:

  • /nginx-health فقط آماده بودن Nginx را بررسی می‌کند.
  • /health از طریق Nginx به یکی از Backendها می‌رسد.

آزمایش Load Balancing

چند بار مسیر /instance را صدا بزنید:

for i in $(seq 1 10); do
  curl -s http://localhost:8080/instance
  echo
done

خروجی نمونه:

{"instance":"app1"}
{"instance":"app2"}
{"instance":"app1"}
{"instance":"app2"}

به دلیل استفاده از least_conn و کوتاه بودن درخواست‌ها، توزیع دقیق همیشه الزاماً یکی‌درمیان نیست. هدف این است که درخواست‌ها براساس تعداد اتصال‌های فعال میان نمونه‌ها تقسیم شوند.

آزمایش API هوش مصنوعی

درخواست معمولی:

curl -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Reverse Proxy را با یک مثال ساده توضیح بده.",
    "temperature": 0.2,
    "max_tokens": 500
  }'

نمونه ساختار پاسخ:

{
  "answer": "پروکسی معکوس سرویسی است که...",
  "instance": "app2"
}

مقدار instance نشان می‌دهد کدام Backend درخواست را پردازش کرده است.

آزمایش پاسخ Streaming

برای جلوگیری از Buffer شدن خروجی در خود curl از گزینه -N استفاده کنید:

curl -N -X POST http://localhost:8080/chat/stream \
  -H "Content-Type: application/json" \
  -d '{
    "message": "پنج کاربرد Nginx را مرحله‌به‌مرحله توضیح بده.",
    "temperature": 0.3,
    "max_tokens": 700
  }'

خروجی به‌صورت رویدادهای تدریجی دریافت می‌شود:

data: {"type":"token","content":"Nginx","instance":"app1"}

data: {"type":"token","content":" یک وب‌سرور","instance":"app1"}

event: done
data: {"type":"done","instance":"app1"}

اگر همه متن بعد از یک انتظار طولانی و یک‌جا نمایش داده شود، احتمالاً در یکی از لایه‌ها Buffering فعال است.

مشاهده لاگ‌های Nginx

برای مشاهده لاگ زنده:

docker compose logs -f nginx

نمونه لاگ:

172.18.0.1 - abc123 "POST /chat HTTP/1.1" 200 542
request_time=2.418
upstream_addr=172.18.0.3:8000
upstream_status=200
upstream_connect_time=0.001
upstream_header_time=2.416
upstream_response_time=2.416

مقادیر مهم:

مقدارمفهوم
request_timeکل زمان پردازش درخواست در Nginx
upstream_addrنمونه Backend انتخاب‌شده
upstream_statusکد وضعیت دریافتی از Backend
upstream_connect_timeزمان برقراری اتصال با Backend
upstream_header_timeزمان دریافت Header پاسخ
upstream_response_timeزمان پاسخ Upstream

این اطلاعات برای تشخیص اینکه کندی در Nginx، Backend یا سرویس بالادستی رخ داده مفید است.

تست خرابی یک Backend

ابتدا یکی از برنامه‌ها را متوقف کنید:

docker compose stop app1

حالا چند بار درخواست بفرستید:

for i in $(seq 1 5); do
  curl -s http://localhost:8080/instance
  echo
done

درخواست‌های سالم باید به app2 برسند:

{"instance":"app2"}

سپس نمونه اول را دوباره اجرا کنید:

docker compose start app1

پس از آماده شدن برنامه، Nginx می‌تواند دوباره درخواست‌ها را میان نمونه‌های دردسترس توزیع کند.

تفاوت Timeoutهای مهم Nginx

proxy_connect_timeout

حداکثر زمان مورد انتظار برای برقراری اتصال با Backend:

proxy_connect_timeout 5s;

اگر برنامه داخلی در شبکه محلی است، اتصال معمولاً باید سریع برقرار شود.

proxy_send_timeout

فاصله زمانی مجاز میان عملیات ارسال درخواست به Backend:

proxy_send_timeout 60s;

proxy_read_timeout

فاصله زمانی مجاز میان خواندن دو بخش متوالی پاسخ:

proxy_read_timeout 120s;

برای مسیرهای Streaming می‌توان مقدار بیشتری در نظر گرفت:

proxy_read_timeout 300s;

افزایش بی‌حساب Timeout راه‌حل همه مشکلات نیست. اگر پاسخ بیش‌ازحد طول می‌کشد، باید زمان پردازش مدل، طول ورودی، تعداد توکن خروجی، Retryها و بار Backend را نیز بررسی کنید.

مدیریت درخواست‌های POST و Retry

درخواست چت یک عملیات POST است. هنگام تنظیم Failover نباید بدون بررسی، Retry خودکار عملیات غیرقابل تکرار را فعال کنید.

در Nginx تنظیم زیر مشخص می‌کند چه خطاهایی باعث امتحان کردن Upstream بعدی شوند:

proxy_next_upstream error timeout;

از فعال کردن گزینه non_idempotent برای همه درخواست‌ها خودداری کنید، مگر اینکه برنامه شما برای جلوگیری از اجرای تکراری درخواست طراحی شده باشد.

برای عملیات حساس به تکرار می‌توان از شناسه درخواست یا Idempotency Key در سطح برنامه استفاده کرد.

آیا چند Container بهتر است یا چند Worker؟

دو روش رایج برای افزایش ظرفیت FastAPI وجود دارد.

چند Worker در یک Container

uvicorn app.main:app \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4

چند Container جداگانه

Nginx
  ├── App Container 1
  ├── App Container 2
  └── App Container 3

مقایسه کلی:

روشمزیتمحدودیت
چند Workerراه‌اندازی ساده روی یک سرورمدیریت هر Worker مستقل نیست
چند Containerجداسازی و توسعه افقی بهترتنظیمات و عملیات بیشتر
ترکیبیظرفیت بیشتر روی سرور قدرتمندپیچیدگی و مصرف حافظه بیشتر

برای یک سرور ساده، چند Worker می‌تواند کافی باشد. وقتی به جداسازی نمونه‌ها، Restart مستقل و Load Balancing نیاز دارید، چند Container انتخاب انعطاف‌پذیرتری است.

برنامه باید Stateless باشد

وقتی چند نمونه FastAPI دارید، درخواست‌های یک کاربر ممکن است هر بار به Backend متفاوتی برسد.

بنابراین اطلاعاتی مانند موارد زیر نباید فقط در حافظه یک Process نگهداری شوند:

  • Session کاربر
  • تاریخچه مکالمه
  • وضعیت Job
  • محدودیت مصرف
  • نتیجه پردازش موقت مهم
  • اطلاعات احراز هویت سمت سرور

برای داده‌های مشترک می‌توان از پایگاه داده یا Cache مشترک استفاده کرد. در غیر این صورت ممکن است کاربر در درخواست دوم به نمونه‌ای برسد که اطلاعات درخواست اول را ندارد.

تنظیم Nginx برای WebSocket

اگر برنامه شما مسیر WebSocket دارد، Headerهای Upgrade باید منتقل شوند:

map $http_upgrade $connection_upgrade {
    default upgrade;
    "" close;
}

server {
    listen 80;

    location /ws/ {
        proxy_pass http://ai_backend;
        proxy_http_version 1.1;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        proxy_read_timeout 300s;
    }
}

این تنظیم را فقط برای مسیر WebSocket به کار ببرید. مسیرهای HTTP معمولی به Header Upgrade نیاز ندارند.

اضافه کردن دامنه

در سرور واقعی، مقدار server_name را تغییر دهید:

server {
    listen 80;
    server_name api.example.com;

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

رکورد DNS دامنه باید به IP سرور اشاره کند.

برای محیط Production بهتر است HTTPS نیز فعال باشد. نحوه صدور و تمدید گواهی به زیرساخت و روش استقرار شما بستگی دارد و باید براساس مستندات ارائه‌دهنده دامنه، سرور و گواهی انجام شود.

ارائه فایل‌های Static با Nginx

اگر برنامه Frontend یا فایل‌های ثابت دارد، Nginx می‌تواند آن‌ها را مستقیماً ارائه کند:

server {
    listen 80;

    location /static/ {
        alias /var/www/static/;
        expires 7d;
    }

    location /api/ {
        proxy_pass http://ai_backend;
    }
}

این روش مانع مصرف ظرفیت FastAPI برای فایل‌هایی مانند موارد زیر می‌شود:

  • CSS
  • JavaScript
  • تصویر
  • فونت
  • فایل‌های عمومی

خطاهای رایج Nginx و FastAPI

خطای 502 Bad Gateway

معمولاً یعنی Nginx نتوانسته پاسخ معتبر از Backend دریافت کند.

موارد زیر را بررسی کنید:

docker compose ps
docker compose logs app1
docker compose logs app2
docker compose logs nginx

دلایل رایج:

  • FastAPI اجرا نشده است.
  • نام سرویس در upstream اشتباه است.
  • پورت Backend اشتباه تنظیم شده است.
  • برنامه فقط روی 127.0.0.1 گوش می‌دهد.
  • Containerها در شبکه مشترک نیستند.
  • برنامه هنگام شروع Crash کرده است.

داخل Container باید Uvicorn روی این Host اجرا شود:

--host 0.0.0.0

خطای 504 Gateway Timeout

Nginx اتصال را برقرار کرده، اما در زمان مورد انتظار پاسخ مناسب دریافت نکرده است.

موارد زیر را بررسی کنید:

  • زمان پاسخ مدل
  • مقدار max_tokens
  • Timeout کلاینت API
  • proxy_read_timeout
  • بار CPU و RAM
  • تعداد درخواست‌های هم‌زمان
  • Retryهای Backend

خطای 404 بعد از اضافه کردن Reverse Proxy

نحوه نوشتن location و / انتهای proxy_pass را بررسی کنید:

location /api/ {
    proxy_pass http://backend/;
}

ممکن است Backend مسیر /users را دریافت کند، در حالی که شما انتظار /api/users داشته‌اید.

پاسخ Streaming یک‌جا نمایش داده می‌شود

تنظیمات زیر را بررسی کنید:

proxy_buffering off;
proxy_cache off;

در FastAPI نیز Header زیر مفید است:

headers={
    "Cache-Control": "no-cache",
    "X-Accel-Buffering": "no",
}

برای آزمایش با curl نیز از -N استفاده کنید:

curl -N http://localhost:8080/chat/stream

همه درخواست‌ها به یک نمونه می‌روند

این رفتار همیشه نشانه خطا نیست. در روش least_conn ممکن است یک نمونه در لحظه اتصال کمتری داشته باشد.

همچنین موارد زیر را بررسی کنید:

  • هر دو Container واقعاً اجرا شده‌اند.
  • هر دو در upstream تعریف شده‌اند.
  • اتصال‌های Keep-Alive یا Streaming طولانی وجود ندارند.
  • یکی از نمونه‌ها مرتباً خطا نمی‌دهد.
  • یکی از Backendها به دلیل max_fails موقتاً کنار گذاشته نشده است.

بهینه‌سازی برای APIهای هوش مصنوعی

APIهای هوش مصنوعی با APIهای معمولی چند تفاوت مهم دارند:

  • زمان پاسخ می‌تواند متغیر باشد.
  • خروجی ممکن است Streaming باشد.
  • درخواست‌ها ممکن است طولانی‌تر باشند.
  • هزینه هر درخواست یکسان نیست.
  • تعداد توکن خروجی روی زمان و هزینه اثر دارد.
  • لغو اتصال کاربر باید در Backend تشخیص داده شود.

پیشنهادهای عملی:

Timeoutها را براساس داده واقعی تنظیم کنید

ابتدا زمان پاسخ واقعی درخواست‌ها را اندازه‌گیری کنید. سپس Timeout را کمی بیشتر از محدوده منطقی قرار دهید.

خروجی‌های طولانی را Stream کنید

Streaming زمان واقعی تولید پاسخ را لزوماً کاهش نمی‌دهد، اما کاربر زودتر اولین بخش پاسخ را مشاهده می‌کند.

درخواست‌های طولانی را محدود کنید

در مدل Pydantic محدودیت طول ورودی تعریف کردیم:

message: str = Field(min_length=1, max_length=8000)

این مقدار باید براساس نوع برنامه، مدل انتخابی و سیاست محصول تنظیم شود.

شناسه درخواست ثبت کنید

در Nginx از $request_id استفاده کردیم:

proxy_set_header X-Request-ID $request_id;

همین شناسه را می‌توان در لاگ Backend نیز ثبت کرد تا مسیر یک درخواست در لایه‌های مختلف قابل پیگیری باشد.

معیارهای فنی را پایش کنید

حداقل معیارهای زیر را اندازه‌گیری کنید:

  • تعداد درخواست
  • نرخ خطای 4xx و 5xx
  • زمان پاسخ
  • زمان دریافت اولین توکن
  • زمان کامل شدن پاسخ
  • تعداد اتصال فعال
  • Backend انتخاب‌شده
  • تعداد Retry
  • مصرف توکن
  • هزینه تقریبی
  • تعداد قطع اتصال کاربر

Nginx چه تفاوتی با API Gateway دارد؟

Nginx و API Gateway هم‌پوشانی‌هایی دارند، اما همیشه یک مفهوم نیستند.

قابلیتNginxAPI Gateway تخصصی
Reverse Proxyبلهبله
Load Balancingبلهمعمولاً بله
سرو فایل Staticبلهمعمولاً هدف اصلی نیست
مدیریت دامنه و ورودیبلهبله
مدیریت کلید APIنیازمند طراحی و تنظیممعمولاً داخلی
سهمیه‌بندی مصرفمحدود یا نیازمند توسعهمعمولاً داخلی
گزارش مصرف مدلنیازمند توسعهممکن است داخلی باشد
مسیریابی مدل هوش مصنوعینیازمند Backendدر AI Gateway ارائه می‌شود

Nginx می‌تواند ورودی شبکه برنامه شما باشد، در حالی که درواره دسترسی API به مدل‌های هوش مصنوعی را متمرکز می‌کند. این دو می‌توانند در یک معماری کنار هم استفاده شوند:

Client
  |
  v
Nginx
  |
  v
FastAPI Business Logic
  |
  v
Darvareh AI API

چک‌لیست استقرار Nginx و FastAPI

پیش از انتشار نسخه Production این موارد را بررسی کنید:

  • Backend مستقیماً روی اینترنت منتشر نشده باشد.
  • کلید API فقط در Environment Variable قرار گرفته باشد.
  • فایل .env داخل Git نباشد.
  • مسیر Health Check تعریف شده باشد.
  • Timeoutها براساس رفتار واقعی سرویس تنظیم شده باشند.
  • برای Streaming، Buffering غیرفعال شده باشد.
  • Headerهای Proxy به‌درستی منتقل شوند.
  • شناسه درخواست در لاگ ثبت شود.
  • برنامه تا حد امکان Stateless باشد.
  • خطاهای سرویس بالادستی مدیریت شوند.
  • اندازه ورودی و تعداد توکن خروجی محدود شوند.
  • لاگ‌ها اطلاعات حساس را ذخیره نکنند.
  • بازیابی پس از توقف یک Backend آزمایش شده باشد.
  • مصرف CPU، RAM و تعداد اتصال پایش شود.
  • قیمت و محدودیت مدل انتخابی بررسی شده باشد.

پرسش‌های متداول

آیا Nginx برای FastAPI ضروری است؟

برای توسعه محلی ضروری نیست. در محیط عملیاتی، Nginx یکی از گزینه‌های رایج برای Reverse Proxy، مسیریابی، Load Balancing و مدیریت اتصال ورودی است. در بعضی زیرساخت‌های ابری ممکن است Load Balancer مدیریت‌شده جای آن را بگیرد.

آیا Nginx رایگان است؟

نسخه متن‌باز Nginx رایگان است. Nginx Plus یک محصول تجاری با امکانات تکمیلی محسوب می‌شود.

آیا Nginx می‌تواند چند برنامه را روی یک سرور اجرا کند؟

Nginx خود برنامه‌ها را اجرا نمی‌کند، اما می‌تواند درخواست‌ها را براساس دامنه یا مسیر به برنامه‌های مختلف بفرستد.

location /users/ {
    proxy_pass http://user_service;
}

location /chat/ {
    proxy_pass http://chat_service;
}

برای API هوش مصنوعی Round Robin بهتر است یا Least Connections؟

اگر زمان همه درخواست‌ها تقریباً یکسان باشد، Round Robin ساده و مناسب است. اگر زمان پردازش درخواست‌ها تفاوت زیادی دارد، least_conn می‌تواند توزیع متعادل‌تری ایجاد کند.

چرا Streaming پشت Nginx با تأخیر نمایش داده می‌شود؟

رایج‌ترین دلیل فعال بودن Proxy Buffering است. مسیر Streaming را جدا کنید و proxy_buffering off قرار دهید. Buffering احتمالی Client یا لایه‌های واسط دیگر را نیز بررسی کنید.

آیا Nginx تاریخچه مکالمه کاربران را نگه می‌دارد؟

خیر. Nginx یک Reverse Proxy است و مدیریت تاریخچه گفتگو باید در Backend، پایگاه داده یا Cache انجام شود.

آیا می‌توان کلید درواره را در Nginx قرار داد؟

از نظر فنی امکان افزودن Header ثابت وجود دارد، اما برای بیشتر برنامه‌ها بهتر است منطق اتصال و کلید API داخل Backend نگهداری شود. این روش کنترل بیشتری بر ورودی، مدل، هزینه، خطا و سیاست استفاده می‌دهد.

چطور تعداد Backendها را افزایش دهیم؟

یک سرویس جدید اجرا و آن را به گروه Upstream اضافه کنید:

upstream ai_backend {
    least_conn;

    server app1:8000;
    server app2:8000;
    server app3:8000;
}

سپس پیکربندی Nginx را اعتبارسنجی و Reload کنید.

چگونه تنظیمات Nginx را قبل از اجرا بررسی کنیم؟

داخل یک سرور معمولی:

nginx -t

در Docker Compose:

docker compose exec nginx nginx -t

پس از موفق بودن تست:

docker compose exec nginx nginx -s reload

جمع‌بندی

Nginx یکی از کاربردی‌ترین ابزارها برای قرار گرفتن جلوی برنامه‌های FastAPI و سرویس‌های Backend است. این ابزار می‌تواند درخواست‌های ورودی را دریافت کند، آن‌ها را به Backend مناسب بفرستد و ترافیک را میان چند نمونه برنامه توزیع کند.

در پروژه این مقاله موارد زیر را به‌صورت عملی پیاده‌سازی کردیم:

  • ساخت API با FastAPI
  • اتصال Backend به API هوش مصنوعی درواره
  • اجرای دو نمونه برنامه با Docker
  • استفاده از Nginx به‌عنوان Reverse Proxy
  • توزیع ترافیک با least_conn
  • تنظیم Headerهای Forwarded
  • ثبت زمان پاسخ Upstream
  • مدیریت Timeout
  • پشتیبانی از SSE و Streaming
  • آزمایش توقف یک Backend
  • بررسی خطاهای رایج 502 و 504

برای ساخت یک محصول هوش مصنوعی، بهتر است کلید API در Backend نگهداری شود و Nginx در لایه ورودی شبکه قرار بگیرد. Backend نیز می‌تواند با یک API سازگار و متمرکز به مدل مورد نظر متصل شود.

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

منابع

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.