这台 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 的峰值风险。
先定四条原则
- 不为「先进」上 K8s。 现状是 LEMP 与容器混合,先把并发和缓存做对,收益比换架构大得多。
- 不为省资源降 PHP 版本。 省不出多少内存,插件兼容性风险却实打实。真正省资源靠缓存和并发控制。
- 避免面板看不见的「野改」。 能在 1Panel 里做的就在面板里做,减少升级时被覆盖的可能。
- 先保命再加速。 4G 机器没有 swap,一次突发的内存峰值就能直接 OOM。
第一步:看清现状
优化前先确认两件事:容器把端口暴露到了哪里,以及数据库到底是什么状态。
端口与容器通信
docker ps --format 'table {{.Names}}\t{{.Ports}}'当时的结果:
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/),默认是这样的:
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3dynamic 模式下,即使没有任何访问,FPM 也会保持一定数量的待命进程。对一个访问量不大的个人站来说,这些进程纯粹在占内存。
ondemand 的逻辑相反:平时不创建任何子进程,第一个请求到达时才启动,空闲超过一定时间就销毁。代价是第一个请求会慢一点,但对低频站点完全可以接受。
第一批参数
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 站点的池配置重复修改,然后:
systemctl restart php-fpm重启后用 free -h 看,闲时可用内存明显上升。这一刀挤出来的空间足够再放一个站。
后来收紧的参数
7 月这轮只改了模式,12 月重做时把并发上限也收紧了:
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 文件:
- 在文件管理器找到应用目录下的
conf/uploads.ini - 把
memory_limit、upload_max_filesize等核心参数与老站点对齐 - 在应用管理界面执行「重建」使配置生效
容器镜像本身的进程管理模式已经为容器环境调过,不需要也无法直接干预。
数据库:先诊断,再动手
盲目改 MySQL 参数既危险又低效。
把 MySQLTuner 送进容器
MySQLTuner 是个成熟的开源诊断脚本,但它默认跑在宿主机上,需要 mysqladmin 等工具。我的数据库在容器里,直接跑会报错找不到命令。
解决思路是把「医生」请进「屋里」:
docker ps
docker exec -it 1Panel-mysql-nA60 /bin/bash容器是精简镜像,连 apt-get 都没有,最后用内置的 microdnf 装依赖:
microdnf install -y wget perl然后在容器内运行:
perl mysqltuner.pl报告给出的两条建议
整体评价很健康(MySQL 8.4.5 本身已经相当智能),只有末尾两条:
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:
[mysqld]
# ... 原有配置 ...
innodb_redo_log_capacity = 32M
innodb_log_buffer_size = 16M保存后在 1Panel 应用列表对 MySQL 执行「重建」。
换成 MariaDB 之后的参数收敛
后来数据库换成了 MariaDB,重做时目标变了:不是跑分,而是减少无效占用、压低峰值、减少临时表落盘。
数据库体量只有几十 MB,又有页面缓存和 Redis 顶着,压力本来就不大。调整集中在四项:
- 降低
key_buffer_size:MyISAM 用得少,128MB 基本是白占,WordPress 主要用 InnoDB - 提高
innodb_buffer_pool_size:从过小值提到 256–512MB 区间,避免频繁读盘 - 提高
tmp_table_size,同时确保max_heap_table_size不低于它,否则前者会被后者截断 - 降低
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:
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 是否开启:
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:
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再调两个内核参数:
printf '\nvm.swappiness=10\nvm.vfs_cache_pressure=50\n' | sudo tee -a /etc/sysctl.conf
sudo sysctl -pswappiness=10 的意思是尽量不碰 swap,只在内存压力大时使用。swap 在这里的角色是安全气囊,不是日常跑道。
上线第 4 个站
有了上面的底子,加站就是复制现有栈:
- 复制 Redis 与 WP Super Cache 配置,OPcache 默认已开
- 回归评论与搜索:发评论后文章页能看到新内容(说明缓存清理正确),搜索正常且不出现异常缓存结果
- 观察一到两天
观察三个数:PHP-FPM 的 queue 和 max_children_reached 不增长,docker stats 里 PHP 与 MariaDB 的内存不持续爬升,swap 使用量长期接近 0。
巡检命令
这几个命令只读,随时可以跑:
docker stats --no-stream
free -h
swapon --show
docker ps --format 'table {{.Names}}\t{{.Ports}}'OPcache 状态:
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 内存撑爆。
相关内容
宝塔面板下 WordPress 的全套优化清单:Nginx、MySQL 性能调整、PHP 参数与扩展、Swap,以及注释掉面板自带 404 页面。
讲清页面缓存与对象缓存分别处在请求链路的哪个位置,并记录 1Panel 环境下启用 Redis、多站隔离与常见报错的排查流程。