Django أم Flask على VPS صغير: استهلاك الذاكرة
اكتشف تكلفة Django وFlask الفعلية على VPS بسعة 1 أو 2 GB: ذاكرة كل عامل gunicorn وعدد العمال الذي يستطيع خادم صغير تشغيله بصدق.
تكلفة Django وFlask على VPS صغير
المقارنة بين Django وFlask على VPS صغير هي أولاً مسألة ذاكرة. يحمّل Django مخطِّط العلاقات بين الكائنات (ORM)، وآلية الترحيل، وموقع الإدارة إذا فعّلته، داخل كل عملية عامل تبدأها. أما Flask فيحمّل موجّهًا وكائن طلب. على خادم بسعة 1 GB، يحدد هذا الفرق عدد العمليات العاملة التي يمكن تشغيلها، ويحدد عدد العمليات العاملة عدد الطلبات التي يمكنك خدمتها في الوقت نفسه.
لا تُحتسب هذه التكلفة على Django إلا إذا أعدت بناء الوظائف التي يوفرها لك. التطبيق الذي يتضمن حسابات مستخدمين وجلسات ولوحة إدارة يحتاج إلى Django؛ فمقدار RAM لكل عامل هو تكلفة الشيفرة التي لن تكتبها. أما واجهة JSON أمام مخزن بيانات تشغّله مسبقاً فتناسبها Flask، لأن أياً من هذه المكونات الجاهزة لن يُحمّل أصلاً. هذه مسألة ملاءمة. وتوضح لك القياسات أدناه إلى أي جانب ينتمي تطبيقك.
ما مقدار الذاكرة التي يستخدمها عامل واحد من gunicorn؟
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، وثلاثة عمال من gunicorn، وتفعيل preload. اعتبرها حداً أدنى، لأن عمليات الاستيراد الخاصة بتطبيقك تُضاف إليها. يستهلك عامل Django مع تفعيل لوحة الإدارة 96 MB من الذاكرة المقيمة، بينما تبلغ حصته التناسبية من الذاكرة 58 MB. والفرق بين هذين الرقمين هو موضوع القسم التالي بالكامل.
أنشئ القياس نفسه على خادمك.
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]، وهذا ما يتيح للأوامر التالية العثور على العمال بالاسم بدلاً من التخمين.
pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')يمثل العمود rss حجم مجموعة الذاكرة المقيمة بالكيلوبايت: أي كل صفحة ذاكرة يحتفظ بها process حالياً في RAM. يؤدي جمع هذه القيمة عبر العمال إلى رقم أعلى من اللازم، لأن العامل المتشعب باستخدام fork يشارك الصفحات مع العملية الأصلية ومع العمال الأشقاء، ولذلك تُحتسب الصفحة نفسها عدة مرات. اطلب من kernel حجم المجموعة التناسبي (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شغّله باعتباره المستخدم الذي يملك العمال، أو باستخدام sudo. استخدم PSS لتحديد ميزانية الذاكرة، لأن PSS يجمع القيم بصورة صحيحة، بخلاف RSS.
يكون Django أكبر بسبب ما يفعله django.setup(). فهو يستورد كل إدخال في INSTALLED_APPS، وينشئ سجل التطبيق، ثم ينشئ كلاً من كل فئة model وكل كائن Python لكل field فيها. تؤدي إضافة django.contrib.admin إلى تشغيل الاكتشاف التلقائي للإدارة، الذي يستورد الوحدة admin لكل تطبيق، ويحمّل معه طبقتَي النماذج والقوالب. يستورد عامل Flask Werkzeug وJinja2، ثم يتوقف.
هناك ملاحظة مهمة: غالباً ما يكون framework هو الجزء الأصغر. فالعامل الذي يستورد cloud SDK أو أي مكتبة رقمية يحمّل منها ذاكرة أكبر مما يحمّل من Django. قِس تطبيقك الفعلي قبل أن تقرر أن framework هو المشكلة.
النسخ عند الكتابة، ولماذا يغيّر preload العدد
ينشئ الـmaster process في Gunicorn عمليات workers عبر fork. مباشرة بعد fork()، تشارك العملية الابنة كل صفحة ذاكرة مع العملية الأب، ولا تنسخ النواة الصفحة إلا عندما تكتب إحدى العمليتين فيها. لذلك يعتمد وجود سجل نماذج Django مرة واحدة أو أربع مرات على الخادم على العملية التي أنشأته قبل fork.
عند تعطيل preload_app، تستورد كل worker تطبيقك بعد إنشاء fork لها، ولذلك تنشئ كل واحدة نسخة خاصة بها. وعند تفعيله، يستورد الـmaster التطبيق مرة واحدة، وترث الـworkers تلك الصفحات.
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 بطريقة تتعارض مع النسخ عند الكتابة. يحتوي رأس كل كائن على عدّاد مراجع، ولمس الكائن يكتب في ذلك الرأس، لذلك تُنسخ الصفحات المشتركة من جديد واحدة تلو الأخرى بينما يجتاز جامع البيانات المهملة الكومة. ينقل gc.freeze() كل ما خُصص حتى تلك اللحظة إلى جيل دائم لا يعود الجامع يزوره، وهذا يحافظ على مشاركة عدد أكبر من تلك الصفحات. ويُعد when_ready نقطة الربط الصحيحة لأنه يعمل بعد preload وقبل إنشاء أول worker عبر fork. قِس PSS قبل إضافته وبعده، لأن مقدار التوفير يعتمد على حجم حالة تطبيقك التي أُنشئت وقت الاستيراد.
لـpreload تكلفة تفاجئ بعض المستخدمين يوم النشر. يرسل systemctl reload إشارة HUP، والسلوك الموثق لـgunicorn عند HUP هو إعادة تحميل إعداداته وبدء workers جديدة. عندما يكون التطبيق محمّلاً مسبقاً، لا يعيد استيراد الشيفرة، لذلك لا يعمل الإصدار الجديد رغم أن عمليات workers جديدة قد بدأت. استخدم systemctl restart بعد تغيير الشيفرة، أو تسلسل USR2 ثم WINCH إذا كنت تحتاج إلى منح الـworkers القديمة وقتاً لتصريف الطلبات أولاً.
كم عدد العمال الذين يمكن لخادم VPS بسعة 1 GB تشغيلهم فعلياً؟
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
}
]هذه أرقام الخمول على خادم لا يقدّم أي خدمة. يتبقى نحو 550 MB لعمال التطبيق، وذلك قبل وصول أول طلب.
قسّم الذاكرة، وكن متشائماً في التقدير. يستهلك الطلب الذاكرة أثناء تنفيذه: يبدأ بـqueryset يحمّل بضعة آلاف من الصفوف، ثم يعرض قالباً. يقترب الحد الأقصى لاستهلاك العامل عادةً من ضعف قيمة الخمول، لذلك خطط على أساس الضعف. عند استخدام Django مع لوحة الإدارة، واستهلاك 58 MB في وضع الخمول، يمكنك تشغيل أربعة عمال على هذا الخادم. وعند استخدام Flask مع SQLAlchemy، واستهلاك 38 MB، يمكنك تشغيل سبعة عمال.
يفترض اقتراح (2 x cores) + 1 من Gunicorn أن CPU هو المورد المحدود وأن RAM ليست كذلك. لكن الوضع معكوس على خادم VPS صغير. كما أن vCPU مشتركاً واحداً يوفّر لك عند انشغال المضيف أقل من قدرة نواة واحدة على تنفيذ العمل. افهم ذلك قبل أن تلقي اللوم على شفرتك: يظهر وقت سرقة CPU من جار يستهلك الموارد في top على هيئة قيمة st.
إذا كانت views لديك تنتظر غالباً قاعدة بيانات أو API خارجياً، فستكون threads أفضل من processes هنا. يوفّر --worker-class gthread --workers 2 --threads 4 ثمانية طلبات متزامنة بتكلفة ذاكرة تعادل عاملين، لأن threads تشترك في نسخة واحدة محمّلة من المفسّر والإطار. يعني قفل المفسّر العام أن threads لن تساعد view تستهلك CPU.
أضف swap إلى الخادم. يحوّل خادم VPS بسعة 1 GB ومن دون swap ارتفاع الذاكرة إلى عملية يقتلها النظام، بينما يحوّل 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 في وحدة gunicorn إلى أن يستعيد kernel الذاكرة من cgroup الخاص بتطبيقك، بدلاً من اختيار عملية ضحية من الخادم كله. لذلك لا يتسبب طلب خارج عن السيطرة في فقدان جلسة SSH.
سلوك بدء التشغيل البارد وإعادة التشغيل
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
}
]تُدفع تكلفة الإقلاع مرتين: عند كل نشر، وعند كل إعادة تشغيل تلقائية بعد تعطل. يصبح تطبيق Flask بسيطاً جاهزاً خلال نحو 90 مللي ثانية، بينما يستغرق Django مع تفعيل لوحة الإدارة نحو 720 مللي ثانية على وحدة vCPU افتراضية مشتركة مماثلة. هذه أرقام منشورة نموذجية. قِس زمن تطبيقك، لأن التبعيات هي العامل المؤثر الأكبر.
cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20تعرض الأسطر الأخيرة عمليات الاستيراد الأبطأ مع الزمن التراكمي بالميكروثانية. بالنسبة إلى Flask، شغّل العلم نفسه مع وحدتك: python -X importtime -c "import app".
عند تفعيل preload، يتحمل master هذه التكلفة مرة واحدة، ويبدأ كل worker متشعب فوراً. عند تعطيل preload، يتحمل كل worker هذه التكلفة، كما يغطي timeout في gunicorn عملية الإقلاع والطلب معاً. إذا لم يسجّل worker دخوله خلال timeout ثانية، يُنهى ويُستبدل. لذلك قد يدخل تطبيق كبير على وحدة vCPU افتراضية مشتركة بطيئة في حلقة إعادة تشغيل لا تخدم أي طلب. يعرض السجل ما يلي:
[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)يجب وضع عمليات الترحيل في الوحدة، لا في رمز بدء تشغيل التطبيق. يُشغّل ExecStartPre مرة واحدة قبل إنشاء أي worker. ويؤدي وضع migrate داخل تطبيقك إلى تنافس ثلاثة workers على قفل المخطط نفسه.
يكاد شكل النشر يكون متطابقاً
مدير العمليات
يعمل كلا الإطارين تحت 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 الوصول إليه.
وحدة Flask هي الملف نفسه مع تغيير سطر واحد: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app، ومن دون ExecStartPre. يأخذ وسيط app:app اسم الوحدة ثم اسم الدالة القابلة للاستدعاء، لذلك يعني الخطأ Failed to find attribute 'app' in 'app'. أن وحدتك لا تعرّف متغيراً بهذا الاسم. تتبع المهام المجدولة النمط نفسه، ويمكنك استخدام مؤقت systemd بدلاً من cron لأمر إدارة Django من دون إضافة قائمة مهام إلى خادم بهذا الحجم.
الملفات الثابتة
لا يقدّم Django مع DEBUG = False أي ملفات ثابتة على الإطلاق. اضبط STATIC_ROOT، وشغّل python manage.py collectstatic، ووجّه خادم الويب إلى مجلد الإخراج. إذا تخطيت هذه الخطوة، فستُحمّل لوحة الإدارة من دون تنسيق، بينما تمتلئ السجلات برسائل Not Found: /static/admin/css/base.css.
هناك طريقتان مناسبتان لتقديمها. لا يضيف مقطع alias في nginx أي حمل على تطبيقك. أما WhiteNoise، عند إضافته كوسيط، فيقدّم الملفات من worker ويوفّر عليك مقطع nginx، مقابل استهلاك قدر صغير من وقت worker لكل ملف. يقدّم Flask محتويات مجلد static/ الخاص به في بيئة التطوير، وفي الإنتاج توجّه الـproxy إلى المجلد للسبب نفسه.
الـReverse proxy
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;
}
}عند استخدام أي proxy، يجب إبلاغ 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 مناسبة فعلاً لخادم تطبيق واحد بمعدل كتابة يبلغ بضع عمليات في الثانية، كما أنها تلغي daemon كاملة من ميزانية الذاكرة. فعّل تسجيل الكتابة المسبقة (WAL) واضبط مهلة انتظار الانشغال في برنامج التشغيل.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"timeout": 20,
"init_command": "PRAGMA journal_mode=WAL;",
},
}
}من دون هذين الخيارين، ستواجه django.db.utils.OperationalError: database is locked في أول مرة يكتب فيها worker اثنان في الوقت نفسه، لأن نمط journal الافتراضي يحجب القرّاء أثناء الكتابة، ولأن مهلة الانتظار الافتراضية تنتهي تقريباً فوراً. يشرح تشغيل SQLite في الإنتاج على VPS التفاصيل الموسعة، بما في ذلك متى تتوقف SQLite عن كونها الخيار المناسب.
تستهلك PostgreSQL على الخادم نفسه بسعة 1 GB مقدار 120 MB من الميزانية أعلاه، إضافة إلى عملية backend واحدة لكل اتصال دائم. يحتفظ CONN_MAX_AGE في Django باتصال مفتوح لكل worker، لذلك يعني تشغيل أربعة workers وجود أربعة backends. وهذا غالباً مقابل مناسب. احسبه فقط قبل تحديد عدد workers. إذا كنت تفضّل إبقاء قاعدة البيانات في حاوية بجوار التطبيق، فإن تشغيل Docker على VPS يقدّم المقايضة نفسها عند حد أدنى أعلى، لأن daemon وكل حاوية يضيفان حملاً مؤثراً بهذا الحجم.
ما الذي تشتريه البطاريات، وما تكلفته
الميغابايت الإضافية في Django هي مجموعة من المكونات الموجودة أصلاً والتي تعمل معاً: ORM مع عمليات الترحيل، ونظام الجلسات والمصادقة، ونموذج الصلاحيات، وطبقة النماذج مع حماية CSRF، ومحرك القوالب، وأوامر الإدارة، وواجهة الإدارة. واجهة الإدارة هي الجزء الذي يقلل الناس من قيمته. فهي محرر عملي لقاعدة البيانات الخاصة بنماذجك، مع البحث ومرشحات التصفية، مقابل سطر واحد في INSTALLED_APPS.
Flask هو الصورة المعاكسة. تحصل على التوجيه، وكائن الطلب، وقوالب Jinja2، وكائن الإعدادات. وكل شيء آخر هو خيار تتخذه أنت. وتكون هذه قيمة فعلية عندما يكون التطبيق صغيراً، لأن أي ORM لن يُحمَّل إذا لم تستورد واحداً منه.
تكمن المشكلة في المنطقة الوسطى. إذا أضفت SQLAlchemy للنماذج، وAlembic لعمليات الترحيل، وFlask-Login للجلسات، وFlask-WTF للنماذج وحماية CSRF، وامتداداً إدارياً للواجهة الخلفية، فقد جمّعت شيئاً يستهلك من الذاكرة بقدر ما يستهلكه Django، من دون اتساقه. لكل مكوّن دورة إصدار خاصة به، ورؤية خاصة به حول طريقة ربط التطبيق. عند هذه النقطة يصبح Django الخيار الأقل تكلفة، من حيث RAM ومن حيث الساعات التي تنفقها على الترقية.
Django مقابل Flask: قاعدة اتخاذ القرار
استخدم Django عندما يحتوي التطبيق على حسابات، ومحتوى قابلاً للتحرير، ومخطط قاعدة بيانات سيتغير باستمرار، وواجهة إدارة سيفتحها أحد فعلياً. استخدم Flask عندما يكون التطبيق واجهة JSON فوق مخزن بيانات موجود مسبقاً، أو مستقبلاً لـwebhook لا يحتوي على HTML.
الفاصل الحاسم هو إعداد قائمة مكتوبة. دوّن كل حزمة ستثبّتها في Flask للوصول إلى مجموعة الميزات التي تحتاج إليها. إذا كانت القائمة تتضمن ORM وأداة للترحيلات، فقد اخترت Django فعلياً، وأنت تدفع تكلفة إضافية للوصول إلى النتيجة نفسها ببطء.
توجد حالة ترجّح Flask فعلياً على الأجهزة الصغيرة: تشغيل عدة خدمات صغيرة على جهاز واحد. كل خدمة Flask هي عملية منخفضة التكلفة مستقلة، وتعمل ضمن unit مستقلة. تشغيل 3 مواقع Django على VPS بسعة 1 GB يعني بقاء 3 نسخ من إطار العمل محمّلة في الذاكرة في الوقت نفسه، وعندها تتوقف الحسابات السابقة عن الانطباق. إذا ظل الناتج أقل من المطلوب، فغالباً تكون الخطة الأكبر هي الحل الصريح، كما أن مناقشة التكلفة الفعلية لـ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.. يرفض Django الطلب قبل أن يصل إلى الشيفرة، لأن ALLOWED_HOSTS فارغ أو لا يتضمن الاسم الذي مرّره الـproxy في Host.
تفشل النماذج مع Origin checking failed. تعرض الصفحة رسالة تفيد بفشل التحقق من CSRF. يحدث ذلك خلف proxy ينهي TLS: يرى التطبيق اتصالاً عادياً عبر HTTP، وينشئ origin من http://، ثم يقارنه بطلب وصل عبر https://. اضبط SECURE_PROXY_SSL_HEADER وCSRF_TRUSTED_ORIGINS، وتحقق من أن الـproxy يرسل فعلاً 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 معطّل، أو أن مهلة الانتظار قبل اعتبار قاعدة البيانات مشغولة قصيرة جداً لعمليتي كتابة من عاملين في الوقت نفسه.
FAQ
هل Django ثقيل أكثر من اللازم على VPS بسعة 1 GB؟
لا. يعمل Django بشكل مريح على 1 GB عند استخدام عدد صغير من workers، وnginx أمامه، وSQLite خلفه. تصبح الذاكرة ضيقة عند إضافة PostgreSQL بإعداداته الافتراضية، وcache، وbackground worker، وDocker إلى الخادم نفسه. قِس proportional set size الخاص بـworker واحد، وضاعفه للسماح بقمم الطلبات، ثم قارنه بإجمالي الذاكرة المتبقية بعد نظام التشغيل وقاعدة البيانات.
كم عدد workers الخاص بـgunicorn الذي ينبغي تشغيله على vCPU واحد؟
ابدأ بثلاثة ثم قِس الاستخدام. تكون الذاكرة عادةً العامل المقيِّد على VPS صغير، لذلك اقسم RAM المتبقية بعد نظام التشغيل وقاعدة البيانات على ضعف proportional set size الخاص بـworker واحد. إذا كانت views تنتظر في الغالب قاعدة بيانات أو upstream API، فاستخدم worker class من نوع gthread مع عدد صغير من workers وعدة threads لكل worker، لأن threads تشترك في نسخة واحدة محمّلة من framework وتستهلك ذاكرة أقل بكثير من العمليات الإضافية.
هل أحتاج إلى PostgreSQL، أم تكفي SQLite؟
تكفي SQLite لخادم تطبيق واحد ذي معدل كتابة معتدل، كما أنها تزيل daemon كاملاً من ميزانية الذاكرة. فعّل write ahead logging واضبط busy timeout، وإلا تفشل عمليات الكتابة المتزامنة مع database is locked. انتقل إلى PostgreSQL عندما تحتاج أكثر من آلة إلى الكتابة، أو عندما تحتاج إلى ميزة لا توفرها SQLite، مثل عمليات الكتابة المتزامنة الكثيفة أو access control حسب الدور.
هل ينبغي تشغيل uvicorn بدلاً من gunicorn؟
فقط إذا كانت لديك async views وشيء فعلي تنتظره. Flask هو تطبيق WSGI، لذلك تعمل async view في event loop جديد داخل worker thread وتنتهي قبل بدء الطلب التالي، ولا يوفّر ذلك أي تزامن إضافي. تحتاج async views في Django إلى ASGI server لكي تستفيد من التزامن. نقلت إصدارات uvicorn الحديثة gunicorn worker class إلى package منفصل، لذلك راجع وثائق uvicorn الحالية بدلاً من نسخ flag قديم لـworker class من tutorial أقدم.
لماذا اختفى worker لدي من دون traceback؟
تتلقى العملية التي يقتلها kernel out of memory killer الإشارة SIGKILL، ولا يمكنها تسجيل أي شيء أثناء خروجها، لذلك يتوقف application log ببساطة. يلاحظ Gunicorn الفجوة ويطبع Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. أكّد ذلك باستخدام dmesg -T | grep -i "killed process". يتمثل الإصلاح في تقليل عدد workers، أو إنشاء swapfile بحيث تتحول زيادة الذاكرة المفاجئة إلى طلب بطيء بدلاً من عملية متوقفة.