Django hay Flask trên VPS nhỏ: tốn bao nhiêu RAM?
So sánh Django và Flask trên VPS 1 đến 2 GB: RAM thực tế mỗi worker gunicorn, ảnh hưởng của preload và số worker máy nhỏ có thể chạy ổn định.
Chi phí của Django và Flask trên một VPS nhỏ
Django và Flask trên một VPS nhỏ trước hết là câu chuyện về bộ nhớ. Django nạp object relational mapper (ORM), cơ chế migration và, nếu bạn bật, trang admin vào mọi worker process mà bạn khởi chạy. Flask chỉ nạp router và request object. Trên một máy 1 GB, khác biệt này quyết định có thể chạy bao nhiêu worker, còn số worker quyết định bạn có thể xử lý bao nhiêu request cùng lúc.
Chi phí đó chỉ là bất lợi của Django nếu bạn không bao giờ tự xây lại những gì framework này cung cấp. Một ứng dụng có tài khoản người dùng, session và admin panel nên dùng Django: RAM cho mỗi worker là chi phí cho phần code bạn không phải tự viết. Một JSON API đứng trước datastore mà bạn đã vận hành sẵn nên dùng Flask, vì những thành phần dựng sẵn đó sẽ không bao giờ được nạp. Đây là câu hỏi về mức độ phù hợp. Các phép đo bên dưới sẽ cho biết ứng dụng của bạn phù hợp với lựa chọn nào.
Một worker gunicorn dùng bao nhiêu memory?
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
}
]Đây là các số liệu thường được công bố cho một ứng dụng hello world theo từng dạng trên Ubuntu 24.04 với Python 3.12, 3 worker gunicorn và bật preload. Hãy xem đây là mức tối thiểu, vì các import của ứng dụng bạn sẽ cộng thêm vào đó. Một worker Django có bật admin sử dụng 96 MB resident, trong khi phần memory phân bổ theo worker là 58 MB. Khoảng chênh giữa hai con số này là nội dung chính của phần tiếp theo.
Hãy thực hiện cùng phép đo trên máy của bạn.
sudo apt update && sudo apt install -y python3-venv
python3 -m venv /srv/site1/.venv
/srv/site1/.venv/bin/pip install django gunicorn setproctitleCài đặt setproctitle. Khi có thành phần này, gunicorn đổi tên process của mình thành gunicorn: master [site1] và gunicorn: worker [site1]. Nhờ đó, các lệnh tiếp theo có thể tìm worker theo tên thay vì phải đoán.
pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')Cột rss là resident set size tính bằng kilobyte: tổng số page memory mà process hiện đang giữ trong RAM. Cộng cột này giữa các worker sẽ cho kết quả cao hơn thực tế, vì một worker được fork chia sẻ page với process cha và các worker khác, nên cùng một page bị tính nhiều lần. Thay vào đó, hãy yêu cầu kernel cung cấp proportional set size (PSS). PSS chia mỗi shared page cho các process đang map page đó.
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; doneChạy lệnh này bằng user sở hữu các worker hoặc chạy với sudo. PSS là cột cần dùng để lập ngân sách memory, vì PSS cộng đúng phần memory còn RSS thì không.
Django lớn hơn vì những gì django.setup() thực hiện. Nó import mọi entry trong INSTALLED_APPS, tạo application registry và khởi tạo mọi model class cùng với một Python object cho từng field của model. Thêm django.contrib.admin sẽ chạy admin autodiscovery. Quá trình này import module admin của từng app và kéo theo cả forms layer lẫn template layer. Một worker Flask import Werkzeug và Jinja2 rồi dừng ở đó.
Một lưu ý quan trọng: framework thường chỉ chiếm phần nhỏ. Một worker import cloud SDK hoặc thư viện xử lý số sẽ mang theo nhiều memory từ các thư viện đó hơn là từ Django. Hãy đo ứng dụng thực tế trước khi kết luận framework là nguyên nhân.
Copy on write và vì sao preload thay đổi con số
Master process của Gunicorn fork các worker. Ngay sau fork(), process con chia sẻ mọi memory page với process cha, và kernel chỉ copy một page khi một bên ghi vào page đó. Vì vậy, việc Django model registry tồn tại một lần hay bốn lần trên máy phụ thuộc vào việc nó được khởi tạo ở phía nào của fork.
Khi tắt preload_app, mỗi worker import application sau khi đã được fork, nên mỗi worker tạo một bản copy riêng. Khi bật tùy chọn này, master import application một lần, rồi các worker thừa hưởng những page đó.
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 hoạt động không thuận lợi với copy on write. Header của mỗi object chứa reference count. Mỗi lần truy cập object sẽ ghi vào header đó, khiến các page dùng chung bị copy dần từng page khi garbage collector duyệt heap. gc.freeze() chuyển mọi thứ đã được cấp phát đến thời điểm đó vào một generation cố định mà collector không còn quét, nhờ đó giữ được nhiều page dùng chung hơn. when_ready là hook phù hợp vì nó chạy sau preload và trước khi worker đầu tiên được fork. Hãy đo PSS trước và sau khi thêm tùy chọn này, vì mức tiết kiệm phụ thuộc vào lượng state được tạo trong lúc import application.
Preload có một chi phí thường khiến mọi người bất ngờ khi deploy. systemctl reload gửi HUP, và hành vi được gunicorn ghi rõ đối với HUP là reload cấu hình rồi khởi động worker mới. Khi application đã được preload, process không import lại code của bạn. Vì vậy, release mới chưa chạy dù các worker process đã là process mới. Dùng systemctl restart sau khi thay đổi code, hoặc dùng chuỗi USR2 rồi WINCH nếu cần cho các worker cũ xử lý hết request trước.
Một VPS 1 GB thực tế chạy được bao nhiêu worker?
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
}
]Đó là mức khi máy gần như không phục vụ gì. Còn khoảng 550 MB cho các application worker, và đó là trước khi có request đầu tiên.
Bây giờ hãy chia, và giả định theo hướng thận trọng. Một request tiêu tốn memory trong lúc chạy: một queryset tải vài nghìn dòng, sau đó render template. Peak memory trên mỗi worker thường gần gấp đôi mức idle, vì vậy hãy dự trù gấp đôi. Django có admin ở mức 58 MB khi idle sẽ cho bạn 4 worker trên máy này. Flask với SQLAlchemy ở mức 38 MB sẽ cho bạn 7 worker.
Khuyến nghị (2 x cores) + 1 của Gunicorn giả định CPU là tài nguyên khan hiếm còn RAM thì không. Trên VPS nhỏ, thực tế ngược lại. Một vCPU dùng chung cũng cho bạn ít hơn năng lực xử lý của một core khi host bận. Bạn cần hiểu điều này trước khi đổ lỗi cho code: CPU steal time từ một máy láng giềng gây ồn xuất hiện trong top dưới dạng chỉ số st.
Nếu view của bạn chủ yếu chờ database hoặc upstream API, threads phù hợp hơn processes trong trường hợp này. --worker-class gthread --workers 2 --threads 4 cho phép 8 request chạy đồng thời với chi phí memory tương đương 2 worker, vì các thread dùng chung một bản interpreter và framework đã được load. Global interpreter lock khiến threads không giúp ích cho view tiêu tốn CPU.
Hãy bật swap cho máy. VPS 1 GB không có swap sẽ biến một đợt memory tăng đột biến thành process bị kill, còn swapfile biến cùng đợt tăng đó thành một request chậm.
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 --systemSau đó hãy giới hạn chính application. MemoryMax=600M trên gunicorn unit khiến kernel thu hồi memory từ cgroup của app thay vì chọn một process bất kỳ trên toàn máy. Nhờ vậy, một request chạy mất kiểm soát sẽ không làm mất phiên SSH của bạn.
Chi phí khởi động lạnh và hành vi khởi động lại
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
}
]Chi phí boot phát sinh hai lần: sau mỗi lần deploy và sau mỗi lần tự động khởi động lại do crash. Một ứng dụng Flask tối thiểu sẵn sàng sau khoảng 90 ms, còn Django khi đã bật admin mất khoảng 720 ms trên cùng một vCPU dùng chung. Đây đều là các số liệu thường được công bố. Hãy tự đo vì dependency của bạn mới là yếu tố quyết định.
cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20Các dòng cuối liệt kê những import chậm nhất cùng tổng số microsecond tích lũy. Với Flask, chạy cùng flag này với module của bạn: python -X importtime -c "import app".
Khi bật preload, master chịu chi phí đó một lần và mỗi worker được fork sẽ khởi động ngay. Khi tắt preload, mỗi worker phải chịu chi phí này, còn timeout của gunicorn bao quát cả quá trình boot lẫn request. Worker không gửi tín hiệu trong vòng timeout giây sẽ bị kill và thay thế. Vì vậy, một ứng dụng nặng chạy trên vCPU dùng chung chậm có thể rơi vào vòng lặp khởi động lại và không bao giờ phục vụ được request nào. Log sẽ ghi:
[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)Migration phải nằm trong unit, không nằm trong code khởi động ứng dụng. ExecStartPre chạy một lần trước khi có worker nào được tạo. Đặt migrate bên trong ứng dụng khiến 3 worker tranh chấp cùng một schema lock.
Mô hình triển khai gần như giống nhau
Trình quản lý tiến trình
Cả hai framework đều chạy dưới gunicorn, và gunicorn chạy dưới 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.targetRuntimeDirectory=site1 tạo /run/site1 khi khởi động và xóa nó khi dừng, nên đường dẫn socket luôn tồn tại với đúng owner. Dòng umask = 0o007 trong cấu hình gunicorn làm cho socket đó có thể được ghi bởi group www-data. Đây là cách nginx truy cập socket.
Unit của Flask dùng cùng một file, chỉ thay đổi một dòng: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app, và không có ExecStartPre. Đối số app:app có dạng module rồi callable, nên lỗi Failed to find attribute 'app' in 'app'. có nghĩa là module của bạn không định nghĩa biến có tên đó. Công việc định kỳ cũng dùng cùng mô hình này. systemd timer thay thế cron cho Django management command mà không cần thêm task queue vào một máy có quy mô như vậy.
File tĩnh
Django với DEBUG = False hoàn toàn không phục vụ file tĩnh. Đặt STATIC_ROOT, chạy python manage.py collectstatic, rồi trỏ web server vào thư mục đầu ra. Bỏ qua bước này thì trang admin sẽ tải mà không có CSS, còn log sẽ đầy lỗi Not Found: /static/admin/css/base.css.
Có hai cách hợp lý để phục vụ các file này. Một block nginx alias không làm app tốn thêm tài nguyên. WhiteNoise, được thêm dưới dạng middleware, phục vụ file từ worker và giúp bạn không cần block nginx, nhưng mỗi file sẽ tốn thêm một ít thời gian của worker. Trong môi trường development, Flask phục vụ thư mục static/ của chính nó. Trong production, bạn trỏ proxy vào thư mục đó vì cùng lý do.
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;
}
}Khi nằm sau bất kỳ proxy nào, Django cần biết request gốc dùng HTTPS. Nếu không, các kiểm tra cross site request forgery (CSRF) sẽ từ chối các form của chính bạn.
ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")Nếu máy đã chạy container, Traefik đứng trước nhiều ứng dụng Docker Compose sẽ làm cùng công việc bằng labels trên container thay vì dùng một file cho từng site.
Chọn database
SQLite thực sự phù hợp với một application server duy nhất có tốc độ ghi chỉ vài lần mỗi giây. Nó cũng loại bỏ hẳn một daemon khỏi ngân sách bộ nhớ. Bật write ahead logging (WAL) và đặt busy timeout cho driver.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"timeout": 20,
"init_command": "PRAGMA journal_mode=WAL;",
},
}
}Nếu thiếu hai tùy chọn này, bạn sẽ gặp django.db.utils.OperationalError: database is locked ngay lần đầu hai worker cùng ghi, vì journal mode mặc định chặn reader trong lúc ghi và timeout mặc định bỏ cuộc gần như ngay lập tức. Phần giải thích đầy đủ hơn, bao gồm thời điểm SQLite không còn là lựa chọn phù hợp, có trong chạy SQLite trong production trên VPS.
PostgreSQL trên cùng máy 1 GB sẽ chiếm 120 MB trong ngân sách nêu trên, cộng thêm một backend process cho mỗi persistent connection. CONN_MAX_AGE của Django giữ một connection mở cho mỗi worker, nên 4 worker sẽ tạo 4 backend. Đây thường là một đánh đổi hợp lý. Chỉ cần tính trước khi đặt số worker. Nếu muốn để database trong một container cạnh app, chạy Docker trên VPS cũng là cùng một đánh đổi nhưng có mức tài nguyên tối thiểu cao hơn, vì daemon và mỗi container đều tạo overhead đáng kể ở quy mô này.
Những gì bạn nhận được từ phần “pin”, và chi phí phải trả
Các megabyte bổ sung của Django là một tập hợp những thành phần đã tồn tại và hoạt động cùng nhau: ORM kèm migrations, hệ thống session và authentication, mô hình permission, lớp form kèm CSRF protection, template engine, management commands và admin. Admin là phần thường bị đánh giá thấp. Đây là trình chỉnh sửa database hoạt động được cho các model của bạn, có search và filter, chỉ với một dòng trong INSTALLED_APPS.
Flask là hình ảnh đối lập. Bạn có routing, request object, template Jinja2 và config object. Mọi thứ còn lại là lựa chọn của bạn. Đây là giá trị thực khi app nhỏ, vì ORM sẽ không được load nếu bạn không import nó.
Điểm dễ mắc bẫy là khoảng giữa. Nếu thêm SQLAlchemy cho model, Alembic cho migration, Flask-Login cho session, Flask-WTF cho form và CSRF, cùng một extension admin cho back office, bạn sẽ lắp ghép một hệ thống có mức sử dụng memory tương đương Django nhưng không có tính nhất quán của Django. Mỗi thành phần có release cycle riêng và quan điểm riêng về cách kết nối app. Đó là lúc Django trở thành lựa chọn rẻ hơn, cả về RAM lẫn số giờ bạn phải dành cho việc nâng cấp.
Django so với Flask: quy tắc quyết định
Dùng Django khi ứng dụng có tài khoản, nội dung có thể chỉnh sửa, schema sẽ tiếp tục thay đổi và có một back office mà thực sự sẽ có người mở. Dùng Flask khi ứng dụng là một JSON interface chạy trên datastore đã có sẵn, hoặc là một webhook receiver không có HTML.
Yếu tố phân định là một danh sách được viết ra. Ghi lại mọi package bạn sẽ cài trong Flask để đạt được bộ tính năng cần thiết. Nếu danh sách đó có ORM và migration tool, thì bạn đã chọn Django và đang trả thêm chi phí để đi đến cùng kết quả theo cách chậm hơn.
Có một trường hợp Flask thực sự phù hợp hơn trên phần cứng nhỏ: chạy nhiều service nhỏ trên cùng một máy. Mỗi Flask service là một process nhẹ riêng, chạy dưới unit riêng. Ba Django site trên một VPS 1 GB có nghĩa là có ba bản sao framework cùng resident, và phép tính ở trên không còn đúng. Nếu kết quả vẫn luôn thiếu, chọn plan lớn hơn thường là cách xử lý thực tế, và VPS thực sự tốn bao nhiêu mỗi tháng là một cuộc trao đổi ngắn hơn so với việc viết lại một ứng dụng đang chạy ổn định.
Các dạng lỗi thường gặp và chuỗi log bạn sẽ thấy
Worker biến mất rồi xuất hiện lại. Gunicorn ghi [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Nó ghi chuỗi này cả khi tự dừng một worker không gửi heartbeat trước thời hạn timeout và khi kernel đã dừng process. Dùng dmesg -T | grep -i "killed process" để phân biệt hai trường hợp. Nếu có dòng này thì nguyên nhân là thiếu memory; hãy giảm số worker hoặc thêm swap.
Mọi page đều trả về 400 và log ghi Invalid HTTP_HOST header. Thông báo đầy đủ là Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.. Django từ chối request trước khi request đến code của bạn, vì ALLOWED_HOSTS đang trống hoặc không chứa hostname mà proxy truyền trong Host.
Form lỗi với Origin checking failed. Page hiển thị lỗi xác minh CSRF. Lỗi này xảy ra phía sau proxy thực hiện TLS termination: app thấy request là HTTP thuần, tạo origin http://, rồi so sánh với request đến qua https://. Hãy đặt SECURE_PROXY_SSL_HEADER và CSRF_TRUSTED_ORIGINS, đồng thời xác nhận proxy thực sự gửi X-Forwarded-Proto.
nginx trả về 502 ngay lập tức. Error log nêu rõ nguyên nhân: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) nghĩa là unit chưa chạy, còn (13: Permission denied) nghĩa là socket tồn tại nhưng nginx không mở được socket đó do thiết lập umask và group.
Admin không có CSS. collectstatic chưa chạy, hoặc path alias không khớp với STATIC_ROOT. Access log hiển thị các response 404 dưới /static/admin/.
Ghi dữ liệu lỗi khi tải nhẹ. database is locked từ SQLite nghĩa là WAL đang tắt hoặc busy timeout quá ngắn để 2 worker cùng ghi tại một thời điểm.
FAQ
Django có quá nặng đối với VPS 1 GB không?
Không. Django với một số worker ít, nginx ở phía trước và SQLite ở phía sau vẫn chạy ổn trên 1 GB. Tình hình sẽ căng khi bạn thêm PostgreSQL với thiết lập mặc định, một cache, một background worker và Docker trên cùng máy. Đo proportional set size của một worker, nhân đôi để dự phòng lúc lưu lượng request tăng đột biến, rồi so tổng đó với phần RAM còn lại sau khi trừ hệ điều hành và database.
Nên chạy bao nhiêu gunicorn worker trên một vCPU?
Bắt đầu với 3 worker rồi đo thực tế. Trên VPS nhỏ, RAM thường là giới hạn chính, vì vậy hãy lấy RAM còn lại sau hệ điều hành và database chia cho hai lần proportional set size của một worker. Nếu các view của bạn chủ yếu chờ database hoặc upstream API, hãy chuyển sang worker class gthread với ít worker và mỗi worker có vài thread. Các thread dùng chung một bản framework đã được load nên tốn ít RAM hơn nhiều so với việc thêm process.
Tôi có cần PostgreSQL không, hay SQLite là đủ?
SQLite phù hợp với một application server có write rate vừa phải và giúp loại bỏ hẳn một daemon khỏi ngân sách RAM. Bật write-ahead logging và đặt busy timeout, nếu không các lần ghi đồng thời sẽ fail với database is locked. Chuyển sang PostgreSQL khi có nhiều hơn một máy cần ghi dữ liệu, hoặc khi bạn cần tính năng SQLite không cung cấp, chẳng hạn concurrent heavy writers hoặc access control theo role.
Tôi có nên chạy uvicorn thay cho gunicorn không?
Chỉ khi bạn có async view và có tác vụ thực sự phải chờ. Flask là ứng dụng WSGI, nên async view chạy trong một event loop mới bên trong worker thread và phải hoàn tất trước khi request tiếp theo bắt đầu. Cách này không tăng concurrency. Django async view cần ASGI server thì mới có ích. Các bản uvicorn gần đây đã chuyển gunicorn worker class sang một package riêng, vì vậy hãy đọc tài liệu uvicorn hiện tại thay vì sao chép flag worker class cũ từ một tutorial cũ.
Tại sao worker của tôi biến mất mà không có traceback?
Process bị kernel out-of-memory killer kill sẽ nhận SIGKILL và không thể ghi log trước khi thoát, nên application log chỉ dừng lại. Gunicorn nhận thấy khoảng trống này và in Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Xác nhận bằng dmesg -T | grep -i "killed process". Cách khắc phục là giảm số worker hoặc tạo swapfile để một đợt tăng đột biến mức dùng RAM trở thành request chậm thay vì process bị kill.