Django 與 Flask:1 至 2 GB VPS 能跑幾個 worker?
比較 Django 與 Flask 在 1 至 2 GB VPS 的實際成本,查看每個 gunicorn worker 的 RSS、PSS 記憶體,以及小型主機能誠實執行的 worker 數量。
Django 與 Flask 在小型 VPS 上的資源成本
在小型 VPS 上比較 Django 與 Flask,首先要考量記憶體。Django 會將物件關聯對映器(ORM)、migration 機制,以及啟用後的管理介面載入至每個啟動的 worker process。Flask 則只會載入 router 和 request object。在 1 GB 的主機上,這項差異會決定可容納多少 worker,而 worker 數量則決定同時可處理多少請求。
只有在你不重新實作 Django 所提供的功能時,這項成本才算是 Django 的額外負擔。具備使用者帳戶、工作階段和管理面板的應用程式適合使用 Django:每個 worker 的記憶體用量,就是你不必自行撰寫程式碼所付出的成本。若是建置在既有資料儲存服務前方的 JSON API,則適合使用 Flask,因為那些內建功能根本不會載入。這是適配性問題。以下測量結果會告訴你應用程式屬於哪一邊。
單一 gunicorn worker 會使用多少記憶體?
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
}
]以下是 Ubuntu 24.04 搭配 Python 3.12、3 個 gunicorn workers 且啟用 preload 時,各類型 hello world 應用程式常見的公開數據。請將這些數值視為下限,因為你自己的匯入內容還會增加記憶體用量。啟用 admin 的 Django worker,其常駐記憶體為 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]。因此,下一個指令可以依名稱找出 workers,不必猜測程序。
pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')rss 欄位是以 kilobytes 為單位的 resident set size:也就是程序目前持有於 RAM 中的所有記憶體頁面。將所有 workers 的數值相加會得到過高的結果,因為 fork 出來的 worker 會與其 parent 及其他 siblings 共用頁面,同一個頁面因此會被計算多次。請改向 kernel 取得 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請以擁有這些 workers 的使用者身分執行,或搭配 sudo 執行。預估記憶體需求時應使用 PSS,因為 PSS 能正確加總,而 RSS 不能。
Django 較大的原因在於 django.setup() 的作用。它會匯入 INSTALLED_APPS 中的每個項目、建立 application registry,並初始化每個 model class,以及該 model 中每個 field 對應的 Python object。加入 django.contrib.admin 後,會執行 admin autodiscovery,匯入每個 app 的 admin module,並連帶載入 forms 和 template layers。Flask worker 會匯入 Werkzeug 和 Jinja2,然後停止載入其他內容。
有一點需要如實說明:framework 通常只占較小的部分。匯入 cloud SDK 或任何 numeric library 的 worker,載入的內容可能比 Django 還多。在判定 framework 是否造成問題前,請先測量實際應用程式。
Copy on write,以及 preload 為何會改變數量
Gunicorn 的 master process 會 fork workers。緊接在 fork() 之後,子程序會與 parent 共用所有記憶體頁面;只有其中一方寫入時,kernel 才會複製該頁面。因此,主機上 Django 的 model registry 是存在一份還是 4 份,取決於它是在 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 的運作方式不利於 copy on write。每個物件標頭都包含參照計數;存取物件時會寫入該標頭,因此 garbage collector 掃描 heap 時,共用頁面會逐一被複製。gc.freeze() 會將目前已配置的所有內容移至 permanent generation,collector 不再掃描該區域,因此能讓更多頁面維持共用。when_ready 是正確的 hook,因為它會在 preload 後、第一個 worker fork 前執行。加入這項設定前後都應測量 PSS,因為節省的記憶體取決於應用程式在 import 時建立了多少狀態。
preload 有一項部署時容易令人意外的成本。systemctl reload 會傳送 HUP,而 gunicorn 在 HUP 上的文件化行為,是重新載入設定並啟動新的 workers。啟用 preload 時,它不會重新匯入程式碼,因此即使 worker processes 是新的,執行的仍不是新版本。程式碼變更後請使用 systemctl restart;若需要先讓舊 workers 排空,請依序執行 USR2,再執行 WINCH。
1 GB VPS 誠實來說能執行多少個 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
}
]這些是伺服器未提供任何服務時的閒置數值。約有 550 MB 可供應用程式 worker 使用,而且這還未計入第一個請求抵達後的需求。
接著進行除法計算,而且要採用保守估算。請求執行期間會耗用記憶體:例如載入數千筆資料的 queryset,接著進行範本渲染。每個 worker 的記憶體峰值通常接近閒置數值的兩倍,因此應以兩倍容量編列預算。Django 搭配 admin 時,58 MB 的閒置用量代表這台伺服器可執行 4 個 worker。Flask 搭配 SQLAlchemy 時,38 MB 的用量代表可執行 7 個 worker。
Gunicorn 的 (2 x cores) + 1 建議假設 CPU 是稀缺資源,而 RAM 不是。在小型 VPS 上,實際情況正好相反。當主機負載很高時,1 個共用 vCPU 能提供的工作量甚至不到 1 個核心的效能。在責怪程式碼之前,應先了解這一點:來自高噪音鄰居的 CPU steal time 會在 top 中以 st 數值呈現。
如果 view 主要等待資料庫或上游 API 的回應,這裡使用 threads 會比 processes 更有效。--worker-class gthread --workers 2 --threads 4 可用相當於 2 個 worker 的記憶體成本,同時處理 8 個請求,因為 threads 會共用一份已載入的 interpreter 與 framework。global interpreter lock 代表 threads 無法改善會消耗 CPU 的 view。
為伺服器配置 swap。沒有 swap 的 1 GB VPS 會在記憶體突增時終止程序;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 unit 上,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
}
]啟動成本會支付 2 次:每次部署時 1 次,以及每次當機後自動重新啟動時 1 次。最小的 Flask 應用程式約需 90 ms 才能就緒;在相同的共用 vCPU 上,啟用管理介面的 Django 約需 720 ms。這些都是常見的公開數據。請測量自己的環境,因為相依套件才是主要影響因素。
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,請對你的模組使用相同的 flag:python -X importtime -c "import app"。
啟用 preload 後,master 只需支付 1 次成本,每個 fork 出的 worker 都能立即啟動。停用 preload 後,每個 worker 都必須支付這項成本,而 gunicorn 的 timeout 同時涵蓋啟動與請求處理。若 worker 在 timeout 秒內未回報,便會遭到終止並由新的 worker 取代。因此,在速度緩慢的共用 vCPU 上執行重量級應用程式時,可能持續進入重新啟動迴圈,始終無法處理任何請求。日誌如下:
[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)資料庫遷移應放在 unit 中,不應放在應用程式的啟動程式碼內。ExecStartPre 會在任何 worker 建立前執行 1 次。若將 migrate 放入應用程式內,3 個 worker 會互相競爭相同的 schema lock。
部署架構幾乎相同
Process manager
兩個框架都在 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.targetRuntimeDirectory=site1 會在啟動時建立 /run/site1,並在停止時移除它,因此 socket 路徑總是存在,且擁有者正確。gunicorn 設定中的 umask = 0o007 行會讓該 socket 對 www-data 群組可寫入,nginx 便能透過它連線。
Flask 的 unit 是相同檔案,只修改一行:ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app,且沒有 ExecStartPre。app:app 引數依序是模組與 callable,因此錯誤 Failed to find attribute 'app' in 'app'. 表示你的模組未定義該名稱的變數。排程工作也採用相同模式;使用 systemd timer 取代 cron 執行 Django management command,不必在這種規模的主機上加入 task queue。
Static files
Django 搭配 DEBUG = False 完全不會提供 static files。設定 STATIC_ROOT,執行 python manage.py collectstatic,再將 web server 指向輸出目錄。略過這個步驟後,管理介面會在沒有樣式的情況下載入,而日誌會持續出現 Not Found: /static/admin/css/base.css。
提供這些檔案有兩種合理方式。nginx 的 alias 區塊不會消耗應用程式資源。將 WhiteNoise 加入 middleware 後,worker 便能提供檔案,省去 nginx 區塊,但每個檔案會多消耗一些 worker 時間。Flask 在開發環境會提供自己的 static/ 資料夾;在 production 中,則基於相同原因,將 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")如果主機已執行容器,在多個 Docker Compose 應用程式前方使用 Traefik 也能完成相同工作;它使用容器上的 labels,而不是每個網站各自一個檔案。
Which database
對於單一應用程式伺服器而言,如果寫入速率僅為每秒數次,SQLite 確實足以應付,還能從記憶體預算中省下一個完整的 daemon。啟用 write ahead logging(WAL),並為 driver 設定 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 mode 會在寫入期間阻擋讀取,而預設 timeout 幾乎會立即放棄。較完整的說明,包括 SQLite 何時不再適用,請參閱 在 VPS production 環境中執行 SQLite。
在同一台 1 GB 主機上執行 PostgreSQL,會使用上述預算中的 120 MB,此外每個 persistent connection 還會建立一個 backend process。Django 的 CONN_MAX_AGE 會讓每個 worker 保持一個連線,因此 4 個 worker 就代表 4 個 backend。這通常是合理的取捨,但設定 worker 數量前應先計算資源。如果你希望將 database 放在應用程式旁的容器中,在 VPS 上執行 Docker 也是相同的取捨,只是最低資源需求更高,因為 daemon 與每個容器都會增加在此規模下不可忽略的額外負擔。
Django 額外占用的記憶體,換來的是一組已存在且能協同運作的功能:支援 migrations 的 ORM、session 與 authentication system、permission model、具備 CSRF protection 的 form layer、template engine、management commands,以及 admin。admin 是最常被低估的部分。它是可直接編輯模型資料的資料庫管理介面,具備搜尋與篩選功能,只需在 INSTALLED_APPS 中加入一行設定。
Flask 則完全相反。你會取得 routing、request object、Jinja2 templates 和 config object。其他功能都由你自行選擇。應用程式規模較小時,這確實有其價值,因為如果從未匯入 ORM,就不會載入 ORM。
陷阱在於中間地帶。為 models 加入 SQLAlchemy、為 migrations 加入 Alembic、為 sessions 加入 Flask-Login、為 forms 與 CSRF 加入 Flask-WTF,再加入後台管理用的 admin extension,最後組成的系統會有接近 Django 的記憶體占用,卻沒有 Django 的一致性。每個元件都有自己的發行週期,也各自對應用程式的組裝方式抱持不同看法。到了這個階段,Django 會是更省成本的選擇,不只節省 RAM,也減少升級所需的工時。
Django 與 Flask:決策準則
如果應用程式具備使用者帳號、可編輯內容、持續變動的 schema,以及實際會有人開啟的後台,請使用 Django。如果應用程式只是建立在既有資料儲存區之上的 JSON 介面,或是不包含 HTML 的 webhook 接收器,請使用 Flask。
決勝方法是列出書面清單。寫下你會在 Flask 中安裝的每個套件,直到具備所需的功能。如果清單包含 ORM 和 migration tool,就表示你其實已經選擇 Django,只是額外付出成本,花更長時間才能達到相同結果。
在小型硬體上,有一種情況確實比較適合 Flask:在同一台主機上執行多個小型服務。每個 Flask 服務都是獨立的低成本程序,並由自己的 unit 管理。在一台 1 GB VPS 上執行 3 個 Django 網站,代表同一時間必須常駐 3 份 framework,前面的計算方式也就不再成立。如果計算結果持續顯示資源不足,升級到更大的方案通常才是誠實的解決方式,而 VPS 每月實際成本 比重寫一個能正常運作的應用程式更容易討論。
你會看到的錯誤訊息與故障模式
Worker 消失後又重新出現。 Gunicorn 顯示 [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?。工作程序因逾時未回報而被終止,以及程序遭 kernel 終止時,都會顯示這則訊息。使用 dmesg -T | grep -i "killed process" 區分這兩種情況。若其中出現一行訊息,表示記憶體不足;請減少 worker 數量或加入 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 驗證失敗。這通常發生在執行 TLS termination 的 proxy 後方:應用程式看到的是純 HTTP,建立了 http:// origin,卻拿它與透過 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) 表示該 unit 未執行;(13: Permission denied) 表示 socket 存在,但 nginx 無法開啟,原因是 umask 與群組設定不正確。
管理介面沒有樣式。 尚未執行 collectstatic,或 alias 路徑與 STATIC_ROOT 不一致。存取日誌會在 /static/admin/ 下顯示 404。
低負載下寫入仍失敗。 SQLite 的 database is locked 表示 WAL 已停用,或 busy timeout 太短,導致兩個 worker 同時寫入時發生衝突。
FAQ
Django 對 1 GB VPS 來說太耗資源嗎?
不會。Django 搭配少量 worker、前方使用 nginx,後端使用 SQLite,在 1 GB 上可以穩定執行。如果在同一台伺服器上加入採用預設設定的 PostgreSQL、快取、背景 worker 和 Docker,記憶體就會變得緊張。測量單一 worker 的 proportional set size,乘以 2 以預留請求尖峰所需的記憶體,再與作業系統和資料庫占用後的剩餘容量比較。
在單一 vCPU 上應執行多少個 gunicorn worker?
先從 3 個開始,再進行測量。在小型 VPS 上,記憶體通常是主要限制,因此將作業系統和資料庫占用後的剩餘 RAM,除以單一 worker proportional set size 的 2 倍。如果你的 view 大多在等待資料庫或上游 API,請改用 gthread worker class,設定少量 worker 並為每個 worker 啟用多個 thread。因為 thread 會共用一份已載入的 framework,所需記憶體遠低於增加處理程序。
我需要 PostgreSQL,還是 SQLite 就足夠?
對於寫入速率適中的單一應用程式伺服器,SQLite 就足夠,而且不需要將完整 daemon 納入記憶體預算。啟用 write ahead logging 並設定 busy timeout,否則並行寫入會因 database is locked 而失敗。當超過一台機器需要寫入,或需要 SQLite 不提供的功能時,請改用 PostgreSQL,例如大量 writer 並行寫入或依角色進行存取控制。
我應該改用 uvicorn,而不是 gunicorn 嗎?
只有在你使用 async view,且確實有需要等待的工作時才需要。Flask 是 WSGI 應用程式,因此 async view 會在 worker thread 中建立新的 event loop,並在下一個請求開始前完成,無法增加並行處理能力。Django async view 必須搭配 ASGI server 才能發揮作用。近期的 uvicorn releases 已將 gunicorn worker class 移至獨立套件,因此請閱讀目前的 uvicorn 文件,不要從較舊的教學複製過時的 worker class flag。
為什麼我的 worker 沒有 traceback 就消失了?
如果處理程序遭 kernel 的 out of memory killer 終止,會收到 SIGKILL,結束前無法記錄任何訊息,因此應用程式日誌會直接停止。Gunicorn 會注意到這個中斷,並輸出 Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?。使用 dmesg -T | grep -i "killed process" 確認原因。解決方式是減少 worker 數量,或建立 swapfile,讓記憶體尖峰造成請求變慢,而不是導致處理程序終止。