Linux VM Tuning Notes 2026 (Apache, PHP-FPM & MariaDB / 8 Core、16GB RAM)

Roga Lin

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 mariadb

PHP 的 service name 要依實際版本調整。不要只看到某個設定檔存在,就認為它一定有生效,因為系統裡可能同時留著不同 PHP 版本,或是沒有啟用的 Apache MPM 範例設定。

三、Apache:使用 Event MPM 搭配 PHP-FPM

確認實際啟用的模組

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 架構。

Event MPM 起始範例

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>

這幾個設定的意思如下:

這些參數必須符合以下關係:

MaxRequestWorkers ≤ ServerLimit × ThreadsPerChild

範例中 16 × 25 = 400

不過 MaxRequestWorkers = 400 不代表主機可以同時跑 400 個 PHP 程式。靜態檔案、KeepAlive 和其他 HTTP 工作一樣會用到 Apache workers;真正比較吃資源的 PHP 平行數量,還是由 PHP-FPM 的 pm.max_children 控制。

KeepAlive 設定

Apache 的全域設定可以先從這組開始:

KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 2
Timeout 60

如果網站前面還有 CDN、reverse proxy 或 Load Balancer,也要一起檢查前一層的 timeout,不然只改 Apache 可能看不出效果。

量測 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:用實測記憶體推算 children

動態網站裡面,PHP-FPM 往往是最容易把 RAM 吃完的服務。最常見的問題是直接把 pm.max_children 設得很高,卻沒有先量過每個 PHP worker 實際用了多少記憶體。

找出有效的 Pool 設定

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/null

Debian 的預設 pool 通常位於:

/etc/php/8.2/fpm/pool.d/www.conf

量測 PHP worker 平均 RSS

這個指令最好在網站有正常流量,甚至接近尖峰的時候執行:

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 當作比較保守的起點。

Dynamic 模式範例

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 = 120s

pm.start_servers 和 spare servers 影響的是啟動速度與常駐記憶體,沒有必要直接設到接近 pm.max_children

memory_limit 不等於平均用量

例如:

memory_limit = 256M

這只是單一 PHP request 可以配置的上限,不代表每個 worker 平常都會使用 256MB;反過來說,也不能假設 workers 永遠只會使用量測到的平均值。圖片處理、試算表匯出、備份和大型 API payload 都可能逼近上限。

如果 pm.max_children = 40memory_limit = 256M,最壞情況的 PHP 配置量可能接近 10GB,仍需透過應用層限制、背景工作分離及流量控制降低風險。

OPcache

檢查 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=20000

小型網站可能只需要 128MB/10,000 files。如果是大型網案或是安裝了很多套件,可以先從 256MB/20,000 files 開始,後續再依 OPcache 使用率調整。

五、MariaDB:先從 InnoDB Buffer Pool 開始

查看資料量與目前設定

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'
);
"

Buffer Pool 怎麼估算

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,通常只是全部資料的一部分。

Max Connections

查看連線狀況:

sudo mariadb -NBe "
SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Threads_connected',
  'Threads_running',
  'Max_used_connections'
);
"

假設 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 = 2

tmp_table_sizemax_heap_table_size 都有「每個 connection 可能使用」的特性。如果直接設成幾百 MB,再乘上大量連線,很容易造成嚴重的記憶體壓力。

六、Swap 與 Swappiness

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 和網站回應時間都要綜合考量。

CPU 與 Load Average

uptime
top

8 核心主機的 load average 短暫超過 8 不一定有問題;但如果長時間都高於 8,而且 CPU idle 很低,或 I/O wait 很高,就要再往下查。

記憶體與 Swap

free -h
vmstat 1

觀察重點:

sudo journalctl -k --since "7 days ago" --no-pager \
  | grep -Ei 'out of memory|oom-killer|killed process'

磁碟 I/O

若已安裝 sysstat

iostat -xz 1

可以看到裝置延遲、queue size、utilization 和 await。MariaDB buffer pool 太小、缺少 index、大量 temporary tables 寫入磁碟或使用 Swap,都可能造成 I/O 壓力。

PHP-FPM 是否排隊

查看服務日誌:

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,可以先確認:

  1. 主機是否仍有足夠 RAM。
  2. CPU 是否已飽和。
  3. 請求是否因慢 SQL 或外部 API 長時間阻塞。
  4. 是否有爬蟲、攻擊或突發流量。
  5. PHP workers 的尖峰 RSS 是否比平均值高很多。

MariaDB 是否成為瓶頸

除了連線數,還應檢查 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、觀察」的順序處理。這樣出問題時,比較容易知道是哪個設定造成的。

PHP-FPM

sudo /usr/sbin/php-fpm8.2 -t
sudo systemctl reload php8.2-fpm
systemctl is-active php8.2-fpm

Apache

sudo /usr/sbin/apache2ctl configtest
sudo systemctl reload apache2
systemctl is-active apache2

MariaDB

MariaDB 的 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 mariadb

MariaDB 正在運作時,不要為了檢查設定,又直接啟動另一個沒有正確「只驗證」能力的 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 天,而且一定要包含尖峰時段。

每天手動查看,或透過監控系統收集下面這些資料:

調整原則:

十、8 核心、16GB 主機的起始配置摘要

整理一下,下面是我會給這類共用型網站主機的保守起始值:

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 全域與每連線用量
  + 其他服務用量

效能調校的重點我覺得是在資源可控的情況下,讓真正的工作順利完成。先量測、再推算、也不太可能一次到位,基本上都是一次改一點,然後持續觀察,雖然花時間,但對系統實際運作的狀況掌握度會更高,也會更可靠。