这台 2 核 4G 的 VPS 上跑了 3 个 WordPress 站点和 2 个静态页面。准备上第 4 个站时,我决定先把资源占用理一遍,而不是直接升级配置。

整个过程分两轮:7 月先处理 PHP-FPM 和数据库,12 月趁加站重做了一轮完整收敛。下面按主题记录,命令和参数都是当时实际用的。

场景与目标

  • 机器:2C4G VPS(Debian)
  • 面板:1Panel
  • Web:OpenResty
  • PHP:8.4.x,容器运行
  • 数据库:早期 MySQL 8.4,后来换成 MariaDB
  • 缓存:Redis(容器)
  • 站点:3 个在跑,准备上第 4 个
  • 特点:无会员、无购物车,动态部分主要是搜索和评论,只有站长自己登录后台

目标很明确:不升级 VPS,在现有资源内把稳定性提上去,压住 502 和 OOM 的峰值风险。

先定四条原则

  1. 不为「先进」上 K8s。 现状是 LEMP 与容器混合,先把并发和缓存做对,收益比换架构大得多。
  2. 不为省资源降 PHP 版本。 省不出多少内存,插件兼容性风险却实打实。真正省资源靠缓存和并发控制。
  3. 避免面板看不见的「野改」。 能在 1Panel 里做的就在面板里做,减少升级时被覆盖的可能。
  4. 先保命再加速。 4G 机器没有 swap,一次突发的内存峰值就能直接 OOM。

第一步:看清现状

优化前先确认两件事:容器把端口暴露到了哪里,以及数据库到底是什么状态。

端口与容器通信

BASH
docker ps --format 'table {{.Names}}\t{{.Ports}}'

当时的结果:

TEXT
MariaDB   127.0.0.1:3306->3306/tcp
PHP       127.0.0.1:9000->9000/tcp
Redis     127.0.0.1:6379->6379/tcp
OpenResty (空,由面板与宿主机层管理 80/443)

结论是数据库、PHP、Redis 都只绑定在宿主机的 127.0.0.1,没有对公网开放。这一点很关键:即使容器内 listen = 0.0.0.0:9000,外部也进不来,安全性由宿主机端口映射决定,而不是容器内的监听地址。

顺便解释一个常见误解:把 PHP-FPM 改成 listen=127.0.0.1 会导致 502。因为容器里的 127.0.0.1 只代表容器自己,OpenResty 是通过宿主机映射过来的,连不上。正确做法是保持容器内 0.0.0.0:9000,由宿主机侧限制只对本地开放。

优化前的资源基线

  • 内存:4 GB
  • 已有优化:WP Super Cache、Redis 对象缓存、静态资源走 CDN

即使做了这些,闲时内存占用依然不低。为了给第 4 个站留空间,得从常驻进程下手。

动手前先用服务商控制台打一个快照。 这是整篇文章里最重要的一步,后面所有操作都有可能把站点弄挂,有快照就能一键回到原点。

PHP-FPM:从 dynamic 换到 ondemand

性能瓶颈的第一嫌疑对象是 PHP-FPM。它是 PHP 与 Web 服务器之间的桥梁,负责管理 PHP 子进程池,运行模式直接决定内存怎么被消耗。

查看各站点对应的池配置(通常在 /etc/php/[版本]/fpm/pool.d/),默认是这样的:

INI
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

dynamic 模式下,即使没有任何访问,FPM 也会保持一定数量的待命进程。对一个访问量不大的个人站来说,这些进程纯粹在占内存。

ondemand 的逻辑相反:平时不创建任何子进程,第一个请求到达时才启动,空闲超过一定时间就销毁。代价是第一个请求会慢一点,但对低频站点完全可以接受。

第一批参数

INI
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 10s

; ondemand 下这三项无效,注释掉避免误判
; pm.start_servers = 2
; pm.min_spare_servers = 1
; pm.max_spare_servers = 3

对服务器上所有 WordPress 站点的池配置重复修改,然后:

BASH
systemctl restart php-fpm

重启后用 free -h 看,闲时可用内存明显上升。这一刀挤出来的空间足够再放一个站。

后来收紧的参数

7 月这轮只改了模式,12 月重做时把并发上限也收紧了:

INI
pm = ondemand
pm.max_children = 6
pm.process_idle_timeout = 10s
pm.max_requests = 500
request_terminate_timeout = 120s
request_slowlog_timeout = 3s

几个参数的作用:

  • max_children = 6:2 核机器并发开太大只会让 CPU 和内存一起雪崩,宁可排队
  • max_requests = 500:进程处理 500 个请求后重建,防止内存越跑越胖
  • request_terminate_timeout = 120s:单个请求超过两分钟直接杀掉,避免一个卡死的请求拖垮整站
  • request_slowlog_timeout = 3s:超过 3 秒的请求记进慢日志,用来抓插件问题

用 FPM status 验收

改完不要凭感觉,看三个指标:

  • listen queue:长期为 0,说明没排队
  • max children reached:长期为 0,说明并发没撞墙
  • slow requests:接近 0,说明没有明显的慢请求堆积

任何一个持续增长,就说明参数还需要调。

容器化站点怎么调

1Panel 一键部署的 WordPress 是容器,改不了内部的 FPM 配置。面板的做法是挂载一个外部 ini 文件:

  1. 在文件管理器找到应用目录下的 conf/uploads.ini
  2. 把 memory_limit、upload_max_filesize 等核心参数与老站点对齐
  3. 在应用管理界面执行「重建」使配置生效

容器镜像本身的进程管理模式已经为容器环境调过,不需要也无法直接干预。

数据库:先诊断,再动手

盲目改 MySQL 参数既危险又低效。

把 MySQLTuner 送进容器

MySQLTuner 是个成熟的开源诊断脚本,但它默认跑在宿主机上,需要 mysqladmin 等工具。我的数据库在容器里,直接跑会报错找不到命令。

解决思路是把「医生」请进「屋里」:

BASH
docker ps
docker exec -it 1Panel-mysql-nA60 /bin/bash

容器是精简镜像,连 apt-get 都没有,最后用内置的 microdnf 装依赖:

BASH
microdnf install -y wget perl

然后在容器内运行:

BASH
perl mysqltuner.pl

报告给出的两条建议

整体评价很健康(MySQL 8.4.5 本身已经相当智能),只有末尾两条:

TEXT
Variables to adjust:
  innodb_redo_log_capacity should be (=32M) if possible
  innodb_log_buffer_size (> 64M)

两条都指向 InnoDB 的日志系统:

  • innodb_redo_log_capacity:当时是 100M,相对 128M 的 buffer pool 比例偏高,建议降到 buffer pool 的 25%,也就是 32M,平衡恢复时间与写入性能
  • innodb_log_buffer_size:建议加到 64M 以上,但服务器内存有限,我取了保守的 16M

改配置并重建

配置文件在 /opt/1panel/apps/mysql/mysql/conf/my.cnf:

INI
[mysqld]
# ... 原有配置 ...

innodb_redo_log_capacity = 32M
innodb_log_buffer_size = 16M

保存后在 1Panel 应用列表对 MySQL 执行「重建」。

换成 MariaDB 之后的参数收敛

后来数据库换成了 MariaDB,重做时目标变了:不是跑分,而是减少无效占用、压低峰值、减少临时表落盘。

数据库体量只有几十 MB,又有页面缓存和 Redis 顶着,压力本来就不大。调整集中在四项:

  1. 降低 key_buffer_size:MyISAM 用得少,128MB 基本是白占,WordPress 主要用 InnoDB
  2. 提高 innodb_buffer_pool_size:从过小值提到 256–512MB 区间,避免频繁读盘
  3. 提高 tmp_table_size,同时确保 max_heap_table_size 不低于它,否则前者会被后者截断
  4. 降低 max_connections:小站 80–100 足够,降低最坏情况下的内存上限

小站调数据库的核心不是「越大越好」,而是「够用且上限可控」。真遇到慢,先看慢查询日志和插件行为,别急着加 buffer。

Web 服务:OpenResty 基本不用动

OpenResty 的全局配置在 /opt/1panel/apps/openresty/openresty/conf/nginx.conf。

检查下来,1Panel 的默认值已经合适:

  • worker_processes auto;:按 CPU 核心数自动启动
  • worker_connections 1024;:单进程 1024 连接,总并发 2048,对个人站绰绰有余

唯一调整的是 gzip_types,补上 JSON、RSS 和 SVG:

NGINX
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss image/svg+xml;

改完对 OpenResty 执行「重启」。进程配置保持原值——这台机器的瓶颈从来不在 Nginx。

缓存:三层各管一段

缓存体系单独写过一篇,这里只留结论:

  • 页面缓存(WP Super Cache):管匿名访客,直接发 HTML,绕过 PHP
  • 对象缓存(Redis):管后台和动态请求,减少重复查询
  • OPcache:管 PHP 字节码,避免每次请求重新编译

确认 OPcache 是否开启:

BASH
docker exec -it PHP8 sh -lc 'php -i | egrep -i "Zend OPcache =>|opcache.enable =>|opcache.memory_consumption =>|opcache.jit" | head -n 30'

输出里 opcache.enable => On、opcache.memory_consumption => 128 就够用了。opcache.jit => disable 在 WordPress 场景下很正常。

页面缓存的配置要点:搜索页排除、带查询字符串的页面排除、登录用户不缓存、评论后刷新对应文章。这几条没做对,缓存反而会制造问题。

具体配置与多站 Redis 隔离的做法在WordPress 缓存体系那篇里。

swap:给突发峰值兜底

4G 机器最怕的是突发:爬虫扫站、插件内存膨胀、容器瞬间涨内存。没有 swap 的话,内核会直接 OOM 杀掉容器,站点短暂挂掉。

加 2G swap:

BASH
sudo fallocate -l 2G /swapfile || sudo dd if=/dev/zero of=/swapfile bs=1M count=2048
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
swapon --show
free -h
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

再调两个内核参数:

BASH
printf '\nvm.swappiness=10\nvm.vfs_cache_pressure=50\n' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

swappiness=10 的意思是尽量不碰 swap,只在内存压力大时使用。swap 在这里的角色是安全气囊,不是日常跑道。

上线第 4 个站

有了上面的底子,加站就是复制现有栈:

  1. 复制 Redis 与 WP Super Cache 配置,OPcache 默认已开
  2. 回归评论与搜索:发评论后文章页能看到新内容(说明缓存清理正确),搜索正常且不出现异常缓存结果
  3. 观察一到两天

观察三个数:PHP-FPM 的 queue 和 max_children_reached 不增长,docker stats 里 PHP 与 MariaDB 的内存不持续爬升,swap 使用量长期接近 0。

巡检命令

这几个命令只读,随时可以跑:

BASH
docker stats --no-stream
free -h
swapon --show
docker ps --format 'table {{.Names}}\t{{.Ports}}'

OPcache 状态:

BASH
docker exec -it PHP8 sh -lc 'php -i | egrep -i "Zend OPcache =>|opcache.enable =>|opcache.memory_consumption =>|opcache.jit" | head -n 30'

结果

四个站跑在同一台 2C4G 上,闲时内存留出了明显余量,没有再出现 502。

回头看,真正起作用的只有三件事:把 FPM 换成 ondemand、给数据库参数设了上限、加了 swap 兜底。其余调整都是微调。

有一个教训值得记:优化要基于现状,而不是基于别人的配置单。我这台机器的 MySQLTuner 报告整体是健康的,真正该改的只有两行;如果照着网上流传的「高并发 my.cnf」抄一遍,反而会把 4G 内存撑爆。

相关内容

搜索

开始搜索
↑↓
↵
ESC
Ctrl K快捷键