SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

مقایسه مصرف رم Django و Flask روی VPS کوچک

تفاوت واقعی مصرف رم Django و Flask در هر worker از gunicorn روی سرورهای 1 تا 2 GB چقدر است؟ با بررسی دقیق تعداد worker های قابل اجرا، بهترین انتخاب برای VPS خود را بشناسید.

هزینه Django و Flask روی یک VPS کوچک

انتخاب بین Django و Flask روی یک VPS کوچک، پیش از هر چیز یک مسئله مربوط به حافظه (RAM) است. Django در هر worker process که راه‌اندازی می‌کنید، ORM (نگاشت شیء-رابطه)، مکانیزم‌های migration و در صورت فعال‌سازی، پنل مدیریت (admin site) را بارگذاری می‌کند. در مقابل، Flask تنها یک router و یک شیء request را بارگذاری می‌کند. روی یک سرور با 1 GB رم، این تفاوت تعیین می‌کند که چند worker قابل اجراست و تعداد workerها مشخص می‌کند که همزمان به چند درخواست می‌توانید پاسخ دهید.

این هزینه تنها در صورتی برای Django یک نقطه ضعف محسوب می‌شود که از قابلیت‌های پیش‌فرض آن استفاده نکنید. برای اپلیکیشنی که شامل حساب کاربری، نشست‌ها (sessions) و پنل مدیریت است، Django گزینه مناسبی است؛ چرا که رم مصرفی هر worker در واقع بهای کدی است که شما مجبور به نوشتن آن نیستید. اما برای یک JSON API که روی یک datastore موجود قرار می‌گیرد، Flask انتخاب بهتری است، زیرا هیچ‌کدام از ابزارهای جانبی (batteries) بارگذاری نخواهند شد. این یک مسئله تناسب است. اندازه‌گیری‌های زیر به شما می‌گوید که اپلیکیشن شما در کدام دسته قرار می‌گیرد.

هر worker در gunicorn چقدر حافظه مصرف می‌کند؟

ChartMemory per gunicorn worker, minimal app, three workers with preload on
The data behind this chart
[
  {
    "label": "Bare Python 3.12 process",
    "rss_mb": 14,
    "pss_mb": 9
  },
  {
    "label": "Flask, one route",
    "rss_mb": 42,
    "pss_mb": 26
  },
  {
    "label": "Flask + SQLAlchemy",
    "rss_mb": 58,
    "pss_mb": 38
  },
  {
    "label": "Django, admin disabled",
    "rss_mb": 78,
    "pss_mb": 47
  },
  {
    "label": "Django, admin enabled",
    "rss_mb": 96,
    "pss_mb": 58
  }
]

این‌ها ارقام معمول منتشرشده برای یک برنامه hello world در هر قالب روی Ubuntu 24.04 با Python 3.12، سه worker در gunicorn و فعال بودن preload هستند. این اعداد را به عنوان حداقل در نظر بگیرید، زیرا کتابخانه‌هایی که خودتان import می‌کنید، به این مقدار اضافه می‌شوند. یک worker در Django با فعال بودن admin، مقدار 96 مگابایت حافظه resident مصرف می‌کند، در حالی که سهم متناسب (proportional share) آن از حافظه 58 مگابایت است. شکاف بین این دو عدد، موضوع اصلی بخش بعدی است.

همین اندازه‌گیری را روی سرور خودتان انجام دهید.

sudo apt update && sudo apt install -y python3-venv
python3 -m venv /srv/site1/.venv
/srv/site1/.venv/bin/pip install django gunicorn setproctitle

بسته setproctitle را نصب کنید. با وجود آن، gunicorn نام پردازش‌های خود را به gunicorn: master [site1] و gunicorn: worker [site1] تغییر می‌دهد؛ این کار باعث می‌شود دستورات بعدی بتوانند workerها را به جای حدس زدن، با نام پیدا کنند.

pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')

ستون rss نشان‌دهنده resident set size بر حسب کیلوبایت است: یعنی تمام صفحاتی از حافظه که پردازش در حال حاضر در RAM نگه می‌دارد. جمع زدن این مقدار برای تمام workerها عددی بیش از حد واقعی می‌دهد، زیرا یک worker که از طریق fork ایجاد شده، صفحات حافظه را با والد و سایر workerهای هم‌خانواده خود به اشتراک می‌گذارد؛ بنابراین یک صفحه واحد چندین بار شمرده می‌شود. به جای آن، از هسته سیستم‌عامل proportional set size (PSS) را بخواهید که هر صفحه مشترک را بین پردازش‌هایی که از آن استفاده می‌کنند، تقسیم می‌کند.

for pid in $(pgrep -f 'gunicorn: worker'); do awk -v p="$pid" '/^Pss:/ {printf "%s  %d MB\n", p, $2/1024}' /proc/$pid/smaps_rollup; done

این دستور را با کاربری که مالک workerهاست یا با sudo اجرا کنید. PSS ستونی است که باید برای بودجه‌بندی حافظه به آن استناد کنید، زیرا PSS به درستی جمع می‌شود اما RSS خیر.

Django به دلیل کاری که django.setup() انجام می‌دهد، سنگین‌تر است. این ابزار تمام ورودی‌های INSTALLED_APPS را import می‌کند، registry برنامه را می‌سازد و تمام کلاس‌های مدل را به همراه یک شیء پایتونی برای هر فیلد در آن‌ها نمونه‌سازی می‌کند. افزودن django.contrib.admin باعث اجرای admin autodiscovery می‌شود که ماژول admin هر برنامه را import کرده و لایه‌های فرم و قالب را نیز به دنبال خود بارگذاری می‌کند. یک worker در Flask فقط Werkzeug و Jinja2 را import می‌کند و متوقف می‌شود.

یک نکته صادقانه: فریم‌ورک اغلب بخش کوچکی از مصرف حافظه است. workerای که یک cloud SDK یا هر کتابخانه محاسباتی سنگین را import می‌کند، حافظه بسیار بیشتری نسبت به خود Django اشغال می‌کند. پیش از آنکه نتیجه بگیرید مشکل از فریم‌ورک است، برنامه واقعی خود را اندازه‌گیری کنید.

مفهوم Copy on Write و دلیل تغییر تعداد آن با preload

فرایند اصلی Gunicorn، پردازش‌های worker را fork می‌کند. بلافاصله پس از fork()، پردازش فرزند تمام صفحات حافظه را با والد به اشتراک می‌گذارد و هسته سیستم‌عامل تنها زمانی یک صفحه را کپی می‌کند که یکی از دو طرف در آن بنویسد. بنابراین، اینکه رجیستری مدل‌های Django یک‌بار در سیستم وجود داشته باشد یا چهار بار، بستگی به این دارد که در کدام سمتِ fork ساخته شده باشد.

با غیرفعال بودن preload_app، هر worker برنامه شما را پس از fork شدن import می‌کند، بنابراین هر کدام یک کپی خصوصی برای خود می‌سازند. با فعال بودن آن، فرایند اصلی برنامه را یک‌بار import می‌کند و workerها آن صفحات را به ارث می‌برند.

import gc

bind = "unix:/run/site1/gunicorn.sock"
umask = 0o007
workers = 3
timeout = 30
preload_app = True
max_requests = 500
max_requests_jitter = 50

def when_ready(server):
    gc.freeze()

عملکرد CPython با مفهوم copy on write در تضاد است. هدر هر شیء حاوی یک شمارنده ارجاع (reference count) است و دسترسی به یک شیء باعث نوشتن در آن هدر می‌شود؛ بنابراین با حرکت garbage collector در heap، صفحات مشترک یکی‌یکی کپی می‌شوند. gc.freeze() تمام اشیاء تخصیص‌یافته تا آن لحظه را به یک نسل دائمی منتقل می‌کند که collector دیگر به آن سر نمی‌زند؛ این کار باعث می‌شود تعداد بیشتری از آن صفحات به صورت مشترک باقی بمانند. when_ready قلاب (hook) مناسبی است، زیرا پس از preload و پیش از fork شدن اولین worker اجرا می‌شود. پس از افزودن آن، مقدار PSS را قبل و بعد از اجرا اندازه‌گیری کنید، چرا که میزان صرفه‌جویی در حافظه به حجم stateهای زمان import برنامه شما بستگی دارد.

استفاده از preload یک هزینه پنهان دارد که در روز استقرار (deploy) ممکن است غافلگیرکننده باشد. systemctl reload سیگنال HUP را ارسال می‌کند و رفتار مستندشده Gunicorn در مواجهه با HUP، بارگذاری مجدد پیکربندی و شروع workerهای جدید است. وقتی برنامه preload شده باشد، کد شما مجدداً import نمی‌شود؛ بنابراین با وجود اینکه پردازش‌های worker جدید هستند، نسخه جدید برنامه شما اجرا نمی‌شود. پس از تغییر کد از systemctl restart استفاده کنید، یا اگر نیاز دارید workerهای قدیمی ابتدا به کار خود پایان دهند (drain)، از توالی USR2 و سپس WINCH استفاده نمایید.

یک VPS با 1 GB رم واقعاً چند worker را می‌تواند اجرا کند؟

ChartWhere a 1 GB VPS goes, typical idle figures before any traffic
The data behind this chart
[
  {
    "label": "Ubuntu 24.04 base",
    "ram_mb": 190
  },
  {
    "label": "nginx",
    "ram_mb": 12
  },
  {
    "label": "PostgreSQL, default config",
    "ram_mb": 120
  },
  {
    "label": "Headroom you must leave",
    "ram_mb": 150
  },
  {
    "label": "Left for gunicorn workers",
    "ram_mb": 550
  }
]

این ارقام مربوط به حالت بیکار (idle) در سروری است که هیچ درخواستی را پردازش نمی‌کند. حدود 550 مگابایت برای workerهای برنامه باقی می‌ماند و این مقدار قبل از رسیدن اولین درخواست است.

حالا تقسیم کنید، آن هم با دیدگاه بدبینانه. هر درخواست در حین اجرا حافظه مصرف می‌کند: یک queryset که چند هزار ردیف را بارگذاری می‌کند و سپس رندر کردن یک template. اوج مصرف هر worker معمولاً نزدیک به دو برابر مقدار حالت بیکار است، پس بر اساس دو برابر آن بودجه‌بندی کنید. Django به همراه پنل admin با 58 مگابایت در حالت بیکار، به شما اجازه می‌دهد چهار worker روی این سرور داشته باشید. Flask با SQLAlchemy در 38 مگابایت، هفت worker به شما می‌دهد.

پیشنهاد (2 x cores) + 1 در Gunicorn فرض را بر این می‌گذارد که CPU منبع کمیاب است و RAM نیست. در یک VPS کوچک، این موضوع برعکس است. یک vCPU اشتراکی نیز زمانی که میزبان (host) مشغول است، کمتر از توان یک هستهٔ واقعی کارایی دارد؛ درک این نکته پیش از آنکه کد خود را مقصر بدانید، ضروری است: زمان CPU steal از یک همسایه پرمصرف در top به عنوان مقدار st نمایش داده می‌شود.

اگر viewهای شما عمدتاً منتظر دیتابیس یا یک API بالادستی هستند، threadها در اینجا بهتر از processها عمل می‌کنند. --worker-class gthread --workers 2 --threads 4 هشت درخواست همزمان را با هزینهٔ حافظهٔ دو worker ارائه می‌دهد، زیرا threadها یک نسخهٔ بارگذاری‌شده از مفسر و فریم‌ورک را به اشتراک می‌گذارند. قفل جهانی مفسر (GIL) به این معنی است که threadها برای viewهایی که CPU را درگیر می‌کنند، کمکی نخواهند کرد.

برای سرور swap تعریف کنید. یک VPS با 1 GB رم بدون swap، یک جهش ناگهانی در مصرف حافظه را به یک process کشته‌شده (killed) تبدیل می‌کند، در حالی که یک swapfile همان جهش را به یک درخواست کند تبدیل می‌کند.

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

سپس خودِ برنامه را محدود کنید. MemoryMax=600M در unit مربوط به gunicorn به این معنی است که هستهٔ سیستم‌عامل حافظه را از cgroup برنامهٔ شما پس می‌گیرد، به جای اینکه قربانی را از کل سرور انتخاب کند؛ بنابراین یک درخواست خارج از کنترل، باعث از دست رفتن session SSH شما نخواهد شد.

رفتار شروع سرد و راه‌اندازی مجدد

ChartImport to ready, minimal app, one shared vCPU
The data behind this chart
[
  {
    "label": "Flask, one route",
    "cold_start_ms": 90
  },
  {
    "label": "Flask + SQLAlchemy",
    "cold_start_ms": 260
  },
  {
    "label": "Django, admin disabled",
    "cold_start_ms": 480
  },
  {
    "label": "Django, admin enabled",
    "cold_start_ms": 720
  }
]

هزینه بوت دو بار پرداخت می‌شود: در هر استقرار (deploy) و در هر راه‌اندازی مجدد خودکار پس از کرش کردن. یک برنامه Flask حداقلی در حدود 90 میلی‌ثانیه آماده می‌شود و Django با فعال بودن پنل مدیریت، روی همان vCPU اشتراکی حدود 720 میلی‌ثانیه زمان می‌برد. هر دو عدد، ارقام معمول منتشر شده هستند. زمان‌بندی خود را اندازه بگیرید، زیرا وابستگی‌های شما تعیین‌کننده اصلی هستند.

cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20

خطوط پایانی، کندترین importها را با مجموع میکروثانیه‌ها فهرست می‌کنند. برای Flask، همان فلگ را روی ماژول خود اجرا کنید: python -X importtime -c "import app".

با فعال بودن preload، پروسه master این هزینه را یک‌بار پرداخت می‌کند و هر worker که fork می‌شود، بلافاصله شروع به کار می‌کند. با غیرفعال بودن preload، هر worker هزینه را جداگانه می‌پردازد و timeout در gunicorn، هم بوت و هم درخواست را پوشش می‌دهد. workerای که در عرض timeout ثانیه اعلام وضعیت نکند، کشته و جایگزین می‌شود؛ بنابراین یک برنامه سنگین روی یک vCPU اشتراکی کند ممکن است در حلقه راه‌اندازی مجدد گیر کند و هرگز هیچ درخواستی را پاسخ ندهد. لاگ چنین چیزی را نشان می‌دهد:

[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)

عملیات migration باید در unit قرار گیرد، نه در کد راه‌اندازی برنامه. ExecStartPre یک‌بار پیش از ایجاد هر worker اجرا می‌شود. قرار دادن migrate داخل برنامه به این معنی است که سه worker برای دستیابی به همان قفل schema با یکدیگر رقابت می‌کنند.

ساختار استقرار تقریباً یکسان است

مدیر پردازش

هر دو فریم‌ورک تحت gunicorn اجرا می‌شوند و gunicorn نیز تحت systemd مدیریت می‌شود.

[Unit]
Description=gunicorn for site1
After=network.target

[Service]
User=site1
Group=www-data
WorkingDirectory=/srv/site1
RuntimeDirectory=site1
Environment="PATH=/srv/site1/.venv/bin"
ExecStartPre=/srv/site1/.venv/bin/python manage.py migrate --noinput
ExecStart=/srv/site1/.venv/bin/gunicorn -c /srv/site1/gunicorn.conf.py site1.wsgi:application
Restart=on-failure
RestartSec=3
MemoryMax=600M

[Install]
WantedBy=multi-user.target

RuntimeDirectory=site1 در هنگام شروع، /run/site1 را ایجاد و در هنگام توقف آن را حذف می‌کند، بنابراین مسیر سوکت همیشه با مالکیت صحیح وجود دارد. خط umask = 0o007 در پیکربندی gunicorn همان چیزی است که باعث می‌شود این سوکت توسط گروه www-data قابل نوشتن باشد؛ این همان روشی است که nginx از طریق آن به سوکت دسترسی پیدا می‌کند.

فایل unit مربوط به Flask همان فایل قبلی است با این تفاوت که یک خط تغییر کرده است: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app و دیگر خبری از ExecStartPre نیست. آرگومان app:app شامل نام ماژول و سپس تابع قابل‌فراخوانی (callable) است، بنابراین خطای Failed to find attribute 'app' in 'app'. به این معنی است که ماژول شما متغیری با آن نام تعریف نکرده است. کارهای زمان‌بندی‌شده (Scheduled work) نیز از همین الگو پیروی می‌کنند و یک systemd timer جایگزین cron برای دستورات مدیریتی Django می‌شود بدون اینکه نیاز باشد یک صف وظیفه (task queue) به سروری با این ابعاد اضافه کنید.

فایل‌های استاتیک

Django با DEBUG = False هیچ فایل استاتیکی را سرو نمی‌کند. مقدار STATIC_ROOT را تنظیم کنید، python manage.py collectstatic را اجرا کنید و وب‌سرور را به دایرکتوری خروجی هدایت کنید. اگر این مرحله را نادیده بگیرید، پنل مدیریت بدون استایل بارگذاری می‌شود و لاگ‌ها با خطای Not Found: /static/admin/css/base.css پر خواهند شد.

دو روش منطقی برای سرو کردن این فایل‌ها وجود دارد. یک بلاک alias در nginx هیچ هزینه‌ای برای اپلیکیشن شما ندارد. WhiteNoise که به عنوان یک middleware اضافه می‌شود، فایل‌ها را از طریق worker سرو می‌کند و شما را از نوشتن بلاک nginx بی‌نیاز می‌کند، اما در عوض مقدار کمی از زمان worker برای هر فایل صرف می‌شود. Flask در محیط توسعه، پوشه static/ خود را سرو می‌کند و در محیط عملیاتی (production)، به همان دلیلی که ذکر شد، پروکسی را به سمت آن پوشه هدایت می‌کنید.

پروکسی معکوس

server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/site1/static/;
        expires 30d;
    }

    location / {
        proxy_pass http://unix:/run/site1/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

در پشت هر پروکسی، باید به Django اطلاع دهید که درخواست اصلی HTTPS بوده است، در غیر این صورت بررسی‌های جعل درخواست بین‌سایتی (CSRF) فرم‌های شما را رد می‌کنند.

ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

اگر سرور شما در حال حاضر از کانتینرها استفاده می‌کند، قرار دادن Traefik جلوی چندین اپلیکیشن Docker Compose همان کار را با استفاده از برچسب‌ها (labels) روی کانتینر انجام می‌دهد، به‌جای اینکه برای هر سایت یک فایل جداگانه داشته باشید.

کدام پایگاه داده

SQLite برای یک سرور اپلیکیشن واحد که نرخ نوشتن آن در حد چند درخواست در ثانیه است، کاملاً مناسب است و یک دیمون کامل را از بودجه حافظه شما حذف می‌کند. قابلیت Write Ahead Logging (WAL) را فعال کنید و برای درایور یک busy timeout در نظر بگیرید.

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
        "OPTIONS": {
            "timeout": 20,
            "init_command": "PRAGMA journal_mode=WAL;",
        },
    }
}

بدون این دو گزینه، به محض اینکه دو worker همزمان اقدام به نوشتن کنند، با خطای django.db.utils.OperationalError: database is locked مواجه می‌شوید؛ زیرا حالت پیش‌فرض journal در هنگام نوشتن، خوانندگان را مسدود می‌کند و timeout پیش‌فرض نیز تقریباً بلافاصله عملیات را متوقف می‌کند. بحث مفصل‌تر در مورد اینکه SQLite چه زمانی دیگر انتخاب مناسبی نیست، در اجرای SQLite در محیط عملیاتی روی یک VPS آمده است.

استفاده از PostgreSQL روی همان سرور 1 گیگابایتی، 120 مگابایت از بودجه حافظه ذکر شده در بالا را اشغال می‌کند، به‌علاوه یک پردازش backend برای هر اتصال پایدار. تنظیم CONN_MAX_AGE در Django به ازای هر worker یک اتصال باز نگه می‌دارد، بنابراین چهار worker به معنای چهار backend است. این معمولاً معامله خوبی است. فقط پیش از تعیین تعداد workerها، این موضوع را در محاسبات خود لحاظ کنید. اگر ترجیح می‌دهید پایگاه داده را در یک کانتینر در کنار اپلیکیشن نگه دارید، اجرای Docker روی یک VPS همان معامله را با هزینه پایه بالاتر انجام می‌دهد، زیرا دیمون و هر کانتینر سربار اضافه‌ای ایجاد می‌کنند که در این ابعاد اهمیت دارد.

آنچه با این ابزارها به دست می‌آورید و بهای آن

مگابایت‌های اضافی Django شامل فهرستی از قابلیت‌هایی است که از قبل وجود دارند و با هم هماهنگ هستند: ORM به همراه سیستم migration، سیستم نشست (session) و احراز هویت، مدل مجوزها، لایه فرم‌ها با محافظت در برابر CSRF، موتور قالب‌ساز، دستورات مدیریتی و پنل مدیریت. پنل مدیریت همان بخشی است که افراد اهمیت آن را دست‌کم می‌گیرند. این پنل یک ویرایشگر دیتابیس عملیاتی برای مدل‌های شماست که با تنها یک خط کد در INSTALLED_APPS، قابلیت جستجو و فیلتر کردن را فراهم می‌کند.

Flask تصویر آینه‌ای این وضعیت است. شما مسیریابی (routing)، شیء درخواست (request object)، قالب‌های Jinja2 و یک شیء پیکربندی دریافت می‌کنید. هر چیز دیگری انتخابی است که خودتان انجام می‌دهید؛ این موضوع زمانی که برنامه کوچک است ارزش واقعی دارد، زیرا اگر هرگز ORM را import نکنید، بارگذاری هم نمی‌شود.

دام اصلی، وضعیت میانی است. اگر SQLAlchemy را برای مدل‌ها، Alembic را برای migrationها، Flask-Login را برای نشست‌ها، Flask-WTF را برای فرم‌ها و CSRF، و یک افزونه مدیریت برای بخش اداری اضافه کنید، در نهایت چیزی ساخته‌اید که پروفایل حافظه (RAM) آن مشابه Django است، اما انسجام آن را ندارد. هر قطعه چرخه انتشار (release cycle) خاص خود و دیدگاه منحصر به فرد خود را درباره نحوه اتصال اجزای برنامه دارد. این دقیقاً همان نقطه‌ای است که Django پاسخ ارزان‌تری محسوب می‌شود؛ هم از نظر مصرف RAM و هم از نظر ساعاتی که صرف به‌روزرسانی‌ها می‌کنید.

مقایسه Django و Flask: قاعده تصمیم‌گیری

زمانی از Django استفاده کنید که برنامه دارای حساب‌های کاربری، محتوای قابل ویرایش، طرحواره‌ای (schema) که دائماً در حال تغییر است و یک پنل مدیریت (back office) باشد که واقعاً قرار است از آن استفاده شود. زمانی از Flask استفاده کنید که برنامه شما صرفاً یک رابط JSON روی یک پایگاه داده موجود باشد، یا یک دریافت‌کننده webhook که هیچ محتوای HTML در آن وجود ندارد.

عامل تعیین‌کننده در شرایط تردید، تهیه یک لیست مکتوب است. تمام بسته‌هایی که باید در Flask نصب کنید تا به مجموعه قابلیت‌های مورد نیاز خود برسید را یادداشت کنید. اگر این لیست شامل یک ORM و یک ابزار migration باشد، شما در واقع Django را انتخاب کرده‌اید و فقط دارید هزینه اضافی می‌پردازید تا به آرامی به همان مقصد برسید.

یک مورد وجود دارد که در سخت‌افزارهای کوچک واقعاً به نفع Flask است: چندین سرویس کوچک روی یک سرور. هر سرویس Flask یک پردازش سبک و مستقل تحت unit مخصوص به خود است. اجرای سه سایت Django روی یک VPS با 1 GB رم، به معنای بارگذاری همزمان سه نسخه از این فریم‌ورک است و محاسبات بالا دیگر پاسخگو نخواهد بود. اگر منابع همچنان کم می‌آیند، ارتقا به یک پلن بزرگ‌تر معمولاً راه‌حل صادقانه‌ای است و هزینه واقعی یک VPS در ماه بحث کوتاه‌تری نسبت به بازنویسی یک برنامه در حال کار است.

حالت‌های شکست با رشته‌هایی که مشاهده خواهید کرد

ورکرها ناپدید شده و دوباره برمی‌گردند. Gunicorn پیام [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory? را چاپ می‌کند. این پیام هم زمانی که ورکری به دلیل از دست دادن heartbeat تایم‌اوت می‌شود و هم زمانی که کرنل پروسه را می‌کشد، نمایش داده می‌شود. برای تشخیص این دو از هم از dmesg -T | grep -i "killed process" استفاده کنید. وجود یک خط در آنجا به معنای کمبود حافظه است، بنابراین تعداد ورکرها را کاهش دهید یا swap اضافه کنید.

همه صفحات خطای 400 برمی‌گردانند و لاگ Invalid HTTP_HOST header را نشان می‌دهد. پیام کامل Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS. است. جنگو درخواست را پیش از رسیدن به کد شما رد می‌کند، زیرا ALLOWED_HOSTS خالی است یا شامل نامی که پروکسی در Host ارسال کرده، نمی‌باشد.

فرم‌ها با Origin checking failed شکست می‌خورند. صفحه خطای CSRF verification failed را نشان می‌دهد. این اتفاق پشت یک پروکسی با TLS termination رخ می‌دهد: برنامه HTTP ساده را می‌بیند، یک origin از نوع http:// می‌سازد و آن را با درخواستی که از طریق https:// رسیده مقایسه می‌کند. مقادیر SECURE_PROXY_SSL_HEADER و CSRF_TRUSTED_ORIGINS را تنظیم کنید و مطمئن شوید که پروکسی واقعاً X-Forwarded-Proto را ارسال می‌کند.

nginx بلافاصله خطای 502 برمی‌گرداند. لاگ خطا دلیل را مشخص می‌کند: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) به این معناست که یونیت در حال اجرا نیست و (13: Permission denied) به این معناست که سوکت وجود دارد اما nginx نمی‌تواند آن را باز کند، که مربوط به تنظیمات umask و گروه است.

پنل مدیریت استایل ندارد. دستور collectstatic اجرا نشده است، یا مسیر alias با STATIC_ROOT مطابقت ندارد. لاگ دسترسی خطاهای 404 را در مسیر /static/admin/ نشان می‌دهد.

عملیات نوشتن تحت بار کم شکست می‌خورد. خطای database is locked از سمت SQLite به این معناست که WAL غیرفعال است یا busy timeout برای نوشتن همزمان دو ورکر بسیار کوتاه است.

FAQ

آیا Django برای یک VPS با 1 GB رم بیش از حد سنگین است؟

خیر. Django با تعداد کمی worker، استفاده از Nginx در جلو و SQLite در پشت، به‌راحتی روی 1 GB رم اجرا می‌شود. زمانی که PostgreSQL با تنظیمات پیش‌فرض، یک سرویس کش، یک worker پس‌زمینه و Docker را به همان سرور اضافه می‌کنید، منابع محدود می‌شوند. میزان Proportional Set Size یک worker را اندازه بگیرید، آن را برای پوشش پیک‌های درخواست دو برابر کنید و مجموع آن را با مقدار رم باقی‌مانده پس از کسر سهم سیستم‌عامل و دیتابیس مقایسه کنید.

روی یک vCPU چند gunicorn worker باید اجرا کنم؟

با 3 عدد شروع کنید و عملکرد را بسنجید. حافظه معمولاً محدودیت اصلی در یک VPS کوچک است؛ بنابراین مقدار رم باقی‌مانده پس از کسر سهم سیستم‌عامل و دیتابیس را بر دو برابرِ Proportional Set Size یک worker تقسیم کنید. اگر viewهای شما عمدتاً منتظر پاسخ دیتابیس یا یک API خارجی می‌مانند، از کلاس worker نوع gthread با تعداد کمی worker و چندین thread در هر کدام استفاده کنید، زیرا threadها یک نسخه بارگذاری‌شده از فریم‌ورک را به اشتراک می‌گذارند و حافظه بسیار کمتری نسبت به پردازش‌های اضافی مصرف می‌کنند.

آیا به PostgreSQL نیاز دارم یا SQLite کافی است؟

SQLite برای یک سرور برنامه با نرخ نوشتن متوسط کافی است و یک سرویس daemon کامل را از بودجه حافظه حذف می‌کند. قابلیت Write Ahead Logging را فعال کنید و یک busy timeout تنظیم کنید، در غیر این صورت نوشتن‌های همزمان با خطای database is locked مواجه می‌شوند. زمانی به PostgreSQL مهاجرت کنید که بیش از یک ماشین نیاز به نوشتن داشته باشد، یا به قابلیتی نیاز داشته باشید که SQLite ارائه نمی‌دهد؛ مانند نویسنده‌های سنگین همزمان یا کنترل دسترسی بر اساس نقش (per-role access control).

آیا باید به جای gunicorn از uvicorn استفاده کنم؟

فقط اگر viewهای async دارید و عملیاتی واقعی برای انتظار وجود دارد. Flask یک برنامه WSGI است، بنابراین یک view async در یک event loop جدید درون thread مربوط به worker اجرا می‌شود و پیش از شروع درخواست بعدی به پایان می‌رسد؛ این کار هیچ همزمانی (concurrency) اضافه‌ای ایجاد نمی‌کند. viewهای async در Django برای بهره‌مندی از مزایا، به یک سرور ASGI نیاز دارند. نسخه‌های اخیر uvicorn کلاس worker مربوط به gunicorn خود را به یک پکیج جداگانه منتقل کرده‌اند، بنابراین به‌جای کپی کردن flag قدیمی کلاس worker از آموزش‌های قدیمی، مستندات فعلی uvicorn را مطالعه کنید.

چرا worker من بدون هیچ traceback ناپدید شد؟

پردازشی که توسط قابلیت Out of Memory Killer در هسته سیستم‌عامل کشته می‌شود، سیگنال SIGKILL دریافت می‌کند و نمی‌تواند هنگام خروج چیزی لاگ کند، بنابراین لاگ برنامه شما به‌سادگی متوقف می‌شود. Gunicorn این وقفه را تشخیص داده و Worker (pid:1234) was sent SIGKILL! Perhaps out of memory? را چاپ می‌کند. این موضوع را با dmesg -T | grep -i "killed process" تایید کنید. راه‌حل، کاهش تعداد workerها یا ایجاد یک swapfile است تا یک جهش ناگهانی در مصرف حافظه، به‌جای کشتن پردازش، منجر به کند شدن درخواست شود.

#django#flask#python#gunicorn#deployment#vps