陈默,43岁,前银行核心系统DBA,现为自由WordPress安全运维顾问,性格极度理性,厌恶花哨理论,信奉“手中有工具,心中无波澜”,口头禅是“先备份,再动手”。
上午10点27分,客户打来电话时,我正在给家里的龟背竹换盆,电话那头,运营总监的声音像被捏住了脖子的鸭子:“整个媒体库变成白屏了!所有文章里的图片都裂了!用户后台根本传不了图,询盘电话已经打爆了!”
WP-CLI失灵之后,一次生产环境媒体库崩溃的极限自救与性能重塑
我放下铲子,打开笔记本电脑,远程连上服务器,第一件事不是看WordPress,而是查/var/log/nginx/error.log和syslog,故障出现前5分钟,日志里滚动着大量upstream timed out,直觉告诉我不对劲,直接敲下命令:
wp media list --post_type=attachment --fields=ID,guid --format=csv | head -20
终端卡了整整15秒,然后抛出一行刺眼的红字:Error: An error occurred while reading from the database. 数据库连接超时。
我按了下太阳穴,心里有了谱:这根本不是媒体库的问题,是数据库查询被拖垮,而媒体库只是那个最先倒下的多米诺骨牌。
网站加载慢——从慢查询到索引失效
我停下WP-CLI,直接进MySQL。SHOW PROCESSLIST; 发现几十个 SELECT * FROM wp_options WHERE option_name = 'siteurl' 的线程处于Waiting for table level lock,问题找到了:wp_options表用的是MyISAM,且体积膨胀到了40MB。
解决方案不是去清缓存,而是改表引擎并优化自动加载数据,我执行:
ALTER TABLE wp_options ENGINE=InnoDB;
然后回到命令行,用WP-CLI找出所有autoload=yes的膨胀项:
wp option list --autoload=yes --fields=option_name,size --format=table
果然,有个插件把历史日志以自动加载存储了,占了30MB,我执行wp option delete plugin_logs,并给该插件设置autoload=no,瞬间,wp_options降到2MB。
接着查看wp-posts表,发现post_date字段没有索引,这直接导致按日期归档的请求全表扫描,我敲下:
ALTER TABLE wp_posts ADD INDEX idx_post_date (post_date);
前后总共20分钟,网站前端响应从6.8秒降到1.2秒。
缓存插件选择和配置——别用“全家桶”
客户之前用的是某“安全+缓存+防火墙”全家桶插件,我直接建议废掉它的页面缓存功能,因为它和主题的懒加载脚本冲突,还会在WP-CLI执行wp cache flush时报错。
我推荐的方案是:LiteSpeed Cache(前提是服务器用OpenLiteSpeed)或者 W3 Total Cache(Apache/Nginx环境)。
配置上,我只做三件事:
- 页面缓存设置TTL为3600秒,采用“disk enhanced”模式。
- 启用“延迟JS”和“内联CSS”,但排除
jquery.js等核心库,防止拖垮首屏。 - 关键一步:在wp-config.php里定义
WP_CACHE=true,并强制让wp-cron.php走系统Cron而非访问时触发,否则缓存文件会在低峰期无故堆积。
暴力破解登录——重定向攻击与IP段封锁
刚处理完性能,服务器防火墙就提示有国外IP正在疯狂尝试wp-login.php,频率是每秒5次,我压根不装“登录防护插件”,直接在服务器层面用fail2ban + WP-CLI改登录逻辑。
第一步,用WP-CLI改登录地址:
wp rewrite structure '/%postname%/' --hard
然后在functions.php里用login_enqueue_scripts钩子隐藏默认登录表单,并挂载一个自定义/gateway的伪登录接口,任何对wp-login.php的访问直接404。
但攻击者会扫描XML-RPC,我果断:
wp option update default_ping_status 'closed' wp plugin deactivate xmlrpc
最后在Nginx配置里,对/wp-admin和/wp-login.php设置deny 192.168.0.0/16(内部办公网),其余IP段只允许通过密钥登录。
被挂马后清理恢复——从后门函数到隐藏文件
真正棘手的来了,扫描完文件,我发现在/wp-content/uploads/2023/下有个.ico文件,里面是一段混淆的PHP代码,叫做“回调后门”,我这个客户之前用WP-CLI升级过核心,但没校验哈希。
清理步骤:
wp core verify-checksums
它立刻列出了被篡改的wp-settings.php,我逐行排查,发现他们在文件末尾追加了一个eval(base64_decode(...)),我直接:
wp core download --version=6.4.2 --force
强制恢复核心文件,然后针对主题目录,我用grep -r "eval(" wp-content/themes/揪出了三个恶意脚本,但关键的后台任务藏在数据库的wp_options里——有个名为cron的选项里存了一条指向/wp-load.php?post=1&cmd=exec的定时任务。
我清除该任务:
wp cron event delete malicious_task wp option delete cron
我修改了wp-config.php里的数据库密码,并强制所有用户重新登录(wp session destroy --all)。
垃圾评论批量处理——不装臃肿插件
客户反馈每天有200多条垃圾评论,我不推荐Akismet加反垃圾插件,因为代码冗余,我用WP-CLI自带的评论状态管理:
wp comment list --status=spam --format=count
确认数量后,我直接写一个脚本:先批量标记,再删除。
wp comment list --status=spam --field=comment_ID | while read id; do wp comment delete $id; done
我关闭了comments_open(禁止新评论),并启用“需要注册才能评论”功能,外加一条隐藏的安全措施:在functions.php里加入对评论内容中URL少于2条的强制拦截。
HTTPS证书部署——避开WP-CLI的坑
客户要求全站HTTPS,这个看似简单,但WP-CLI在处理混合内容时容易出错,我不会用wp search-replace直接替换URL,因为那会导致数据库序列化数据崩溃。
正确做法:
- 先拿到证书,配置Nginx的
ssl_certificate和ssl_certificate_key。 - 在
wp-config.php强制定义:
define('WP_HOME','https://example.com');
define('WP_SITEURL','https://example.com');
用WP-CLI更新guid(仅为了兼容旧数据):
wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --precise
关键是加--skip-columns=guid,避免打乱附件引用。
挂断电话前,客户问我:“陈工,现在能放心了吗?”我看了眼状态栏,负载从3.8降到0.3,内存占用下降40%,我说:“先用三天,第四天如果没问题,把备份策略从每天一次改成每六小时一次。”顿了顿,又补一句:“下次换主题前,先用WP-CLI的wp theme install --activate测试,别直接上传文件。”
挂掉电话,我把手里的铲子插回花盆,那棵龟背竹,已经有点蔫了,但我没管它——我知道,只要土壤干燥,浇一次水就会活过来,网站和植物一样,关键问题从来不是表面那几片黄叶,而是根系下的排水和土壤结构。



发表评论