Buzeli
buzeliSoluções Digitais
安全

10分钟内131次PHP-FPM崩溃:爬虫如何通过systemd-coredump(而非PHP)锁死服务器

发布于 2026年4月27日

这个高CPU告警不是CPU攻击

Grafana在一个WordPress发布平台上触发了高CPU告警。我连接到服务器并运行了初步诊断。

复制
ps aux --sort=-%cpu | head -12

输出显示7个`systemd-coredump`进程排在顶部,总共消耗47%的CPU。没有CPU占用高的PHP-FPM进程。没有异常的nginx进程。所谓的'CPU攻击'是崩溃处理程序,而不是PHP。

当你看到systemd-coredump高CPU时,事故已经发生了。systemd正在压缩崩溃留下的残骸。真正的问题是:什么在10分钟内产生了131个核心转储文件?

131个核心转储文件和4GB磁盘占用

检查核心转储目录确认了问题的规模:

复制
ls -lh /var/lib/systemd/coredump/ | grep 'php-fpm' | wc -l
# 131

du -sh /var/lib/systemd/coredump/
# 4.0G

ls -lh /var/lib/systemd/coredump/ | grep 'php-fpm' | head -5
# core.php-fpm.33.abc1234.1234567890.xz  31M
# core.php-fpm.33.abc1235.1234567891.xz  31M
# core.php-fpm.33.abc1236.1234567892.xz  31M

131个核心转储文件,经systemd-coredump压缩后每个约31MB,共计4GB。全部来自UID 33——即www-data,PHP-FPM worker用户。不到10分钟,磁盘使用率从39%跳升至50%。

systemd-coredump在后台使用lz4或xz压缩每个核心转储文件。 面对131次同时发生的崩溃,有7个压缩进程并行运行,在尝试处理转储队列的同时消耗了服务器近一半的CPU容量。

根本原因:来自<crawler-range>/24的爬虫

对nginx error.log的调查揭示了规律:

复制
grep '<crawler-prefix>' /var/log/nginx/error.log | head -10
# 2026/03/03 14:20:15 [error] upstream timed out (110) GET /post-about-dogs client: <crawler-IP-1>
# 2026/03/03 14:20:17 [error] upstream timed out (110) GET /another-post client: <crawler-IP-1>
# 2026/03/03 14:20:19 [error] upstream timed out (110) GET /tag/animals client: <crawler-IP-1>
# ...95条来自同一网段的超时记录
# 2026/03/03 14:33:04 [error] ModSecurity: Access denied 403 GET /.env client: <crawler-IP-2>

从14:20开始,`<crawler-range>/24`网段的爬虫对WordPress的文章、标签页(/tag/)和分类页(/categoria/)发起了并行扫描。该站点使用GoCache CDN,但这类URL——标签和分类——缓存是冷的。每个请求都直接到达PHP-FPM,没有缓存。

为什么PHP-FPM会崩溃

WordPress发布平台有一台8核服务器,PHP-FPM配置了多个worker。在对无缓存页面进行并行请求轰炸的情况下,worker开始发生冲突——多个worker同时尝试生成同一个内容繁重的标签页,在完成之前耗尽内存。PHP-FPM worker达到memory_limit时会产生SIGSEGV信号并生成核心转储文件。

14:33对/.env的访问尝试确认了爬虫的特征:这不是合法的索引机器人,而是带有凭据暴露尝试的侦察扫描。

降级循环

导致8核服务器负载达到15.18的序列:

14:20 — 爬虫开始对/tag/和/category/发起并行请求

14:20-14:30 — PHP-FPM worker耗尽内存并崩溃(131次)

14:20+ — systemd-coredump生成131个约31MB的文件 = 磁盘增加4GB

14:30 — 负载达到峰值15.18(是该服务器正常值~0.5的7倍)

14:31 — 爬虫退出 / worker停止崩溃

14:38 — 负载降至0.82

14:44 — 负载恢复正常:0.02

响应操作

1. 清理核心转储文件(优先:释放CPU和磁盘)

7个压缩进程仍在运行,第一个操作是打破循环并释放资源:

复制
# 删除所有php-fpm核心转储文件
sudo rm -f /var/lib/systemd/coredump/core.php-fpm.*

# 结果:
# 磁盘:50% → 39%
# systemd-coredump CPU:47% → 0%
# 负载立即开始下降

2. 封锁爬虫网段

复制
# 通过iptables封锁攻击网段
sudo iptables -I INPUT -s <CRAWLER_CIDR> -j DROP   -m comment --comment 'crawler-ban: php-fpm crash 2026-03-03'

# 验证规则已生效
sudo iptables -L INPUT -n | grep '<crawler-prefix>'

清理转储文件并封锁网段后,服务器在6分钟内完全恢复正常。负载从15.18降至0.02。PHP-FPM有10个健康worker。站点响应时间0.32秒。

正确诊断与初始诊断的对比

告警显示'高CPU'。自然的初始诊断会是:流量攻击、进程卡死、PHP消耗CPU。这些都不对。

复制
# 事故期间进程状态(从日志重建)
# 峰值时(14:30)CPU排名前5:
#
# PID    USER     %CPU  COMMAND
# 12341  systemd  9.2   systemd-coredump  ← 压缩转储1
# 12342  systemd  8.8   systemd-coredump  ← 压缩转储2
# 12343  systemd  8.7   systemd-coredump  ← 压缩转储3
# 12344  systemd  7.9   systemd-coredump  ← 压缩转储4
# 12345  systemd  7.4   systemd-coredump  ← 压缩转储5
# ...
# 没有高CPU的php-fpm进程——因为它们已经崩溃了

高CPU在崩溃处理程序中,而不在PHP里。 这是一种常见的误诊模式:systemd-coredump在后台处理转储文件,出现在ps/top的顶部,好像是攻击者,而实际上它是清理服务。攻击者已经完成了它的工作。

为什么/tag/和/category/的冷缓存至关重要

GoCache CDN对这些URL配置了透传模式。在正常情况下,标签和分类页面被少数用户有机访问,缓存保持温热。一个并行访问数百个唯一标签URL的爬虫不会找到缓存——每个URL对CDN来说都是新的。

缓存冷时,每个请求都到达PHP-FPM。WordPress标签页可能很繁重——多次数据库查询(标签下的文章、侧边栏、相关内容),PHP渲染完整模板。在对不同标签发出50多个并行请求的情况下,worker的内存压力与并发请求数量成正比。

激进爬虫+冷缓存+启用的systemd-coredump的组合创造了一种看起来像CPU攻击但实际上是崩溃级联的隐性故障。服务器不是被流量压垮的——它是在清理崩溃后果时被压垮的。

缺失的防护措施和实施建议

该服务器有ModSecurity运行(/.env访问被403拦截),但以下防护措施缺失或已禁用:

GoCache机器人防护: 状态为false。启用机器人防护后,爬虫会在边缘被拦截,不会到达源站。

GoCache速率限制: 状态为false。边缘的每IP请求限流是对抗激进爬虫的第一道防线。

CrowdSec bouncer: Docker容器在运行,但主机bouncer未激活。CrowdSec检测扫描模式并自动封禁——没有激活的bouncer,检测不会产生封锁。

PHP-FPM pm.max_children: 未配置限制以防止崩溃循环耗尽资源。在coredump.conf中配置SystemMaxUse也可以限制磁盘影响。

复制
# 限制systemd核心转储文件总大小
# /etc/systemd/coredump.conf
[Coredump]
Storage=external
Compress=yes
ProcessSizeMax=2G
ExternalSizeMax=2G
MaxUse=1G          # /var/lib/systemd/coredump/最大1GB
KeepFree=1G        # 文件系统至少保留1GB空闲

# 无需重启即可应用:
sudo systemctl daemon-reload

设置MaxUse=1G后,当达到限制时systemd-coredump会自动丢弃旧的转储文件——防止131次崩溃的攻击填满磁盘,并通过压缩CPU延长危机。

最终状态与经验教训

识别真实原因6分钟后,服务器恢复正常。处理序列:

rm -f /var/lib/systemd/coredump/core.php-fpm.* — 释放4GB,CPU恢复正常

iptables -I INPUT -s <crawler-range>/24 -j DROP — 爬虫被封锁

服务器无需重启就恢复到事故前状态。在整个响应过程中,未崩溃的健康PHP-FPM worker继续正常处理流量。

当ps/top显示systemd-coredump排在高CPU顶部时,不要尝试杀死systemd-coredump。识别崩溃的进程(转储的UID指向用户),找出崩溃原因,然后再清理转储文件。不理解原因就杀死处理程序,会让磁盘保持占满状态,事故无法被诊断。