2026-09-01 12:02:19 +08:00
我租了幾台虛擬主機,其中 loading 最重的一台是 8 Core, 16GB Ram 規格的 Linux VM 。這台 VM 跑一般的動態網站其實已經非常夠用了,不過「硬體夠用」不代表把每個服務的上限都往上調,網站就一定會變快。因為每一個環節都有自己關於效能的設定,如果沒調整好 (都依照預設值) 很可能在流量一高就會一起搶光記憶體,接著開始使用 Swap、回應時間變長,最糟的情況則是被 OOM Killer 終止服務。
以下我針對這台 VM 的狀況稍微整理在調校系統時進行的方向:
在開始改設定以前,我會先把 16GB RAM 大致分配給各個服務,例如可以從下面這組配置開始:
| 用途 | 起始配置 |
|---|---|
| 作業系統、systemd、監控及檔案快取 | 2–3GB |
| Apache | 0.5–1GB |
| PHP-FPM | 6–7GB |
| MariaDB | 4–5GB |
| 安全餘裕 | 1–2GB |
這裡一定要留一些安全餘裕,因為每個 request 使用的記憶體並不固定。圖片處理、產生報表、資料匯入、備份或複雜 SQL,都可能讓用量在短時間內突然升高。
另外,Linux 的 free 很低,不一定就是記憶體不夠。Linux
會盡量把空閒 RAM 拿來做 file cache,所以我通常會先看
available:
free -h如果 available 長時間接近零、Swap
持續增加,而且網站也同時變慢,這時才是比較明確的記憶體壓力。
確認主機規格、軟體版本和目前服務狀態:
cat /etc/os-release
nproc
free -h
df -hT /
/usr/sbin/apache2ctl -v
php -v
mariadb --version
systemctl --no-pager status apache2 php8.2-fpm mariadbPHP 的 service name 要依實際版本調整。不要只看到某個設定檔存在,就認為它一定有生效,因為系統裡可能同時留著不同 PHP 版本,或是沒有啟用的 Apache MPM 範例設定。
sudo /usr/sbin/apache2ctl -M 2>&1 \
| grep -E 'mpm_|php|proxy_fcgi'正常情況下,我會希望看到:
mpm_event_module (shared)
proxy_fcgi_module (shared)
mpm_event 使用 threads 處理連線,面對 KeepAlive
連線時,通常會比 mpm_prefork
節省資源;proxy_fcgi 則負責把 PHP request 交給
PHP-FPM。如果查到的是 mpm_prefork_module 和 Apache 的
php_module,我建議先處理架構切換,換成 event 架構。
Debian 通常在 /etc/apache2/mods-enabled/mpm_event.conf
載入設定:
<IfModule mpm_event_module>
StartServers 4
MinSpareThreads 50
MaxSpareThreads 150
ThreadLimit 64
ThreadsPerChild 25
ServerLimit 16
MaxRequestWorkers 400
MaxConnectionsPerChild 5000
</IfModule>這幾個設定的意思如下:
StartServers:Apache 啟動時建立的 child processes
數量。MinSpareThreads:可立即處理新連線的閒置 threads
下限。MaxSpareThreads:閒置 threads 上限;過多時 Apache
會減少 child processes。ThreadLimit:每個 child process 可配置的 threads
硬上限。ThreadsPerChild:每個 child process 建立的 threads
數量。ServerLimit:child processes 上限。MaxRequestWorkers:可同時處理的請求上限。MaxConnectionsPerChild:每個 child
處理指定數量的連線後重建,可回收長期累積的記憶體;0
代表永不因連線數重建。這些參數必須符合以下關係:
MaxRequestWorkers ≤ ServerLimit × ThreadsPerChild
範例中 16 × 25 = 400。
不過 MaxRequestWorkers = 400 不代表主機可以同時跑 400 個
PHP 程式。靜態檔案、KeepAlive 和其他 HTTP 工作一樣會用到 Apache
workers;真正比較吃資源的 PHP 平行數量,還是由 PHP-FPM 的
pm.max_children 控制。
Apache 的全域設定可以先從這組開始:
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 2
Timeout 60KeepAlive On:允許同一 TCP 連線處理多個 HTTP
請求,減少重複握手成本。MaxKeepAliveRequests:單一連線最多處理多少請求;100
是合理起點。KeepAliveTimeout:等待同一連線下一個請求的時間。動態網站通常可從
2 秒開始;設得過長會保留大量閒置連線。Timeout:Apache
等待某些網路操作完成的全域時間。過長可能讓異常請求長時間佔用資源。如果網站前面還有 CDN、reverse proxy 或 Load Balancer,也要一起檢查前一層的 timeout,不然只改 Apache 可能看不出效果。
ps --no-headers -C apache2 -o rss= \
| awk '{sum+=$1; n++} END {
if(n) printf "Processes: %d, Average: %.1f MB, Total: %.1f MB\n",
n, sum/n/1024, sum/1024
}'Event MPM 的 threads 會共用 process 記憶體,因此不能直接用「單一 process RSS × MaxRequestWorkers」估算總量。RSS(Resident Set Size)代表 process 目前常駐於實體 RAM 的記憶體,但其中可能包含和其他 process 共用的部分。我會分別在平常和尖峰流量時,觀察實際 RSS、Apache scoreboard、連線數和回應時間。
動態網站裡面,PHP-FPM 往往是最容易把 RAM
吃完的服務。最常見的問題是直接把 pm.max_children
設得很高,卻沒有先量過每個 PHP worker 實際用了多少記憶體。
grep -RnsE \
'^[[:space:]]*(pm|pm\.(max_children|start_servers|min_spare_servers|max_spare_servers|max_requests)|request_terminate_timeout)[[:space:]]*=' \
/etc/php/*/fpm/pool.d 2>/dev/nullDebian 的預設 pool 通常位於:
/etc/php/8.2/fpm/pool.d/www.conf
這個指令最好在網站有正常流量,甚至接近尖峰的時候執行:
ps --no-headers -eo rss,cmd \
| awk '/php-fpm: pool/ {
sum+=$1; n++
} END {
if(n) printf "Processes: %d, Average: %.1f MB, Total: %.1f MB\n",
n, sum/n/1024, sum/1024;
else print "No PHP-FPM pool processes found"
}'簡化估算公式為:
pm.max_children ≈ 分配給 PHP-FPM 的 RAM ÷ 尖峰時單一 worker 的平均 RSS
假設 16GB 主機分配 6.5GB 給 PHP-FPM,而尖峰期間每個 worker 平均使用 130MB:
6.5GB × 1024 ÷ 130MB ≈ 51
因為還要保留突發用量和量測誤差,可以先抓大約 80%:
51 × 0.8 ≈ 40
所以先用 pm.max_children = 40 當作比較保守的起點。
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 16
pm.max_requests = 500
request_terminate_timeout = 120spm = dynamic:依流量增減
workers,同時保留一定數量的閒置 workers。pm.max_children:可同時執行的 PHP 請求上限,也是
PHP-FPM 最重要的記憶體保護線。pm.start_servers:PHP-FPM 啟動時建立的 workers
數量。pm.min_spare_servers:閒置 workers 太少時,PHP-FPM
會建立更多 workers。pm.max_spare_servers:閒置 workers 太多時,PHP-FPM
會回收多餘 workers。pm.max_requests:每個 worker
處理指定數量的請求後重建,有助於控制第三方套件或程式造成的緩慢記憶體增長。request_terminate_timeout:單一請求允許執行的最長時間,可阻止失控程式永久佔用
worker;需要長時間匯入的應用必須另外評估。pm.start_servers 和 spare servers
影響的是啟動速度與常駐記憶體,沒有必要直接設到接近
pm.max_children。
memory_limit
不等於平均用量例如:
memory_limit = 256M這只是單一 PHP request 可以配置的上限,不代表每個 worker 平常都會使用 256MB;反過來說,也不能假設 workers 永遠只會使用量測到的平均值。圖片處理、試算表匯出、備份和大型 API payload 都可能逼近上限。
如果 pm.max_children = 40 且
memory_limit = 256M,最壞情況的 PHP 配置量可能接近
10GB,仍需透過應用層限制、背景工作分離及流量控制降低風險。
檢查 PHP-FPM 的 OPcache:
/usr/sbin/php-fpm8.2 -i 2>/dev/null \
| grep -E '^opcache\.(enable|memory_consumption|max_accelerated_files) '起始範例:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000opcache.enable:啟用 PHP bytecode
快取,避免每次請求重新解析與編譯 PHP 檔案。opcache.memory_consumption:OPcache
可使用的共享記憶體,單位為 MB。opcache.max_accelerated_files:可快取的 PHP
檔案數量。小型網站可能只需要 128MB/10,000 files。如果是大型網案或是安裝了很多套件,可以先從 256MB/20,000 files 開始,後續再依 OPcache 使用率調整。
sudo mariadb -NBe "
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1)
FROM information_schema.tables
WHERE engine = 'InnoDB';
"查看核心變數:
sudo mariadb -NBe "
SHOW VARIABLES WHERE Variable_name IN (
'innodb_buffer_pool_size',
'max_connections',
'tmp_table_size',
'max_heap_table_size',
'slow_query_log',
'long_query_time'
);
"innodb_buffer_pool_size 用來快取 InnoDB
的資料頁和索引,也是 MariaDB 最重要的效能設定之一。
如果是單純的資料庫主機,buffer pool 常見的起始比例是實體 RAM 的 60–75%;不過 Apache、PHP-FPM 和 MariaDB 全部共用同一台 16GB 主機時,就不能直接照這個比例設定。
這種共用型主機,可以先從 4GB 開始:
[mariadb]
innodb_buffer_pool_size = 4G如果資料庫工作量高、PHP 實際用量低,而且主機還有足夠的
available,可以再慢慢測試 5–6GB;如果 PHP worker
本身很重,可能就只適合分配 3–4GB。資料庫總容量比 buffer pool
大也不代表設定錯誤,因為真正經常用到的 hot
data,通常只是全部資料的一部分。
查看連線狀況:
sudo mariadb -NBe "
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Threads_connected',
'Threads_running',
'Max_used_connections'
);
"Threads_connected:目前已建立的用戶端連線數。Threads_running:目前正在執行工作的連線數。Max_used_connections:MariaDB
啟動以來同時連線數的最高紀錄。假設 PHP-FPM 上限是 40,再加上 cron job、管理工具和背景工作,可以先從下面這個設定開始:
max_connections = 80不要因為看過一次「Too many connections」,就直接把數值加到幾百。每個 MariaDB connection 都可能配置 thread、sort buffer、join buffer 和 temporary table;連線太多時,CPU、Disk I/O 和 lock contention 也可能一起惡化。
如果 Max_used_connections 長期低於 40,那麼設定 80
已經留了不少餘裕。如果它經常接近 80,我會先確認到底是正常流量、slow
query、connection 沒有釋放,還是 PHP-FPM 平行數量太高。
我建議可以另外開一個設定檔,例如
/etc/mysql/mariadb.conf.d/87-tuning.cnf:
[mariadb]
innodb_buffer_pool_size = 4G
max_connections = 80
thread_cache_size = 64
table_open_cache = 4000
tmp_table_size = 64M
max_heap_table_size = 64M
slow_query_log = ON
long_query_time = 2thread_cache_size:快取已使用過的連線
threads,降低頻繁建立連線的成本。table_open_cache:快取已開啟 table
handles。過高會消耗記憶體與檔案描述符。tmp_table_size:內部記憶體 temporary table
上限之一。max_heap_table_size:MEMORY table
上限,也會共同限制內部 memory temporary table。slow_query_log:記錄超過門檻的
SQL,讓調校從證據出發。long_query_time:慢查詢秒數門檻;2
秒是保守起點,調校期間可視需求降至 1 秒。tmp_table_size 和 max_heap_table_size
都有「每個 connection 可能使用」的特性。如果直接設成幾百
MB,再乘上大量連線,很容易造成嚴重的記憶體壓力。
Swap 是突發狀況的安全網,而不是平常拿來工作的記憶體。16GB 的 Web/DB 主機可以考慮配置 2–4GB Swap,實際容量還是要看作業政策、磁碟空間,以及主機是否需要 hibernation。
查看狀態:
free -h
/usr/sbin/sysctl vm.swappiness共用型網站主機可以先從下面這個設定開始:
vm.swappiness = 10儲存在 /etc/sysctl.d/99-webserver.conf 後套用:
sudo /usr/sbin/sysctl --system較低的 swappiness 可以降低 kernel 主動把 application pages 換出去的機率,但無法阻止記憶體真的不夠時使用 Swap。如果 Swap 持續增加,根本的解法還是降低平行數量、修正記憶體用量,或增加 RAM。
不要只看 load average 就判斷主機太忙,CPU、RAM、Swap、Disk I/O、資料庫、PHP queue 和網站回應時間都要綜合考量。
uptime
top8 核心主機的 load average 短暫超過 8 不一定有問題;但如果長時間都高於 8,而且 CPU idle 很低,或 I/O wait 很高,就要再往下查。
%wa:通常表示 CPU 正在等待磁碟
I/O,可能與慢儲存、資料庫掃描或 Swap 有關。free -h
vmstat 1觀察重點:
available 是否長時間過低vmstat 的 si/so
是否持續出現數值sudo journalctl -k --since "7 days ago" --no-pager \
| grep -Ei 'out of memory|oom-killer|killed process'若已安裝 sysstat:
iostat -xz 1可以看到裝置延遲、queue size、utilization 和 await。MariaDB buffer pool 太小、缺少 index、大量 temporary tables 寫入磁碟或使用 Swap,都可能造成 I/O 壓力。
查看服務日誌:
sudo journalctl -u php8.2-fpm --since "24 hours ago" --no-pager \
| grep -Ei 'max_children|warning|error'如果 log 反覆出現
server reached pm.max_children setting,可以先確認:
除了連線數,還應檢查 slow query、temporary tables、buffer pool 命中情況:
SHOW GLOBAL STATUS LIKE 'Slow_queries';
SHOW GLOBAL STATUS LIKE 'Created_tmp_tables';
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';Created_tmp_disk_tables 很高,不一定只要增加 temporary
table 上限就能解決。問題也可能出在 SQL、欄位型別、排序、分組或 index
設計。
我會一次只改一個服務,照著「備份、驗證、reload/restart、觀察」的順序處理。這樣出問題時,比較容易知道是哪個設定造成的。
sudo /usr/sbin/php-fpm8.2 -t
sudo systemctl reload php8.2-fpm
systemctl is-active php8.2-fpmsudo /usr/sbin/apache2ctl configtest
sudo systemctl reload apache2
systemctl is-active apache2MariaDB 的 innodb_buffer_pool_size
等參數,要先確認目前版本是否支援動態修改。不過為了確保重新開機後仍會套用相同設定,我通常還是會寫進設定檔,然後安排在維護時段
restart。
先確認設定檔有被讀取:
sudo mariadbd --print-defaults 2>/dev/null \
| tr ' ' '\n' \
| grep -E '^--(innodb_buffer_pool_size|max_connections)='再於可接受短暫中斷的時段執行:
sudo systemctl restart mariadb
systemctl is-active mariadbMariaDB
正在運作時,不要為了檢查設定,又直接啟動另一個沒有正確「只驗證」能力的
mariadbd process。第二個 process 會嘗試 lock
同一批資料檔,然後啟動失敗。比較安全的方式是使用套件提供的設定檢查功能,或先用
--print-defaults 確認載入結果,再查看 service log。
套用後確認實際值:
sudo mariadb -NBe "
SHOW VARIABLES WHERE Variable_name IN (
'innodb_buffer_pool_size',
'max_connections'
);
"設定改完而且服務能啟動,只代表語法和基本運作正常,不代表這組設定已經是最佳狀態。我會至少觀察一個完整的流量週期,通常是 7–14 天,而且一定要包含尖峰時段。
每天手動查看,或透過監控系統收集下面這些資料:
調整原則:
pm.max_children,每次約 10–20%。整理一下,下面是我會給這類共用型網站主機的保守起始值:
Apache Event MPM
MaxRequestWorkers: 400
ThreadsPerChild: 25
ServerLimit: 16
KeepAliveTimeout: 2s
PHP-FPM
pm: dynamic
pm.max_children: 40(必須依實測 RSS 重算)
pm.start_servers: 10
pm.min_spare_servers: 8
pm.max_spare_servers: 16
pm.max_requests: 500
MariaDB
innodb_buffer_pool_size: 4GB
max_connections: 80
slow_query_log: ON
long_query_time: 2s
Kernel
vm.swappiness: 10
重點還是要釐清它們彼此之間的限制關係:
總記憶體
> 作業系統與安全餘裕
+ Apache 實際用量
+ PHP-FPM 尖峰用量
+ MariaDB 全域與每連線用量
+ 其他服務用量
效能調校的重點我覺得是在資源可控的情況下,讓真正的工作順利完成。先量測、再推算、也不太可能一次到位,基本上都是一次改一點,然後持續觀察,雖然花時間,但對系統實際運作的狀況掌握度會更高,也會更可靠。