故障:博客网站突然变慢
今天下午,我的博客网站(挂在阿里云 ECS 上)访问变得非常慢。直觉告诉我:大概率是被爬虫盯上了。
和以往不同的是,这次我没有像往常一样打开终端敲命令、翻日志、查配置。而是打开阿里云控制台的 Workbench Agent,在对话框里用中文描述故障现象,Agent 自动接管了后续所有排查工作。
四轮排查:层层抽丝剥茧
整个排查过程分四轮,全程通过自然语言对话完成。我只需要描述现象、确认执行建议,Agent 自动生成命令、分析日志、配置防护。
第一轮:发现恶意爬虫
| 爬虫名称 | 请求次数 | 风险说明 |
|---|---|---|
| MJ12bot | 1892 | 超高频,无公司背书,典型内容采集器 |
| YisouSpider | 617 | 疑似伪造 User-Agent,不实为神马搜索 |
| ris-bot | 少量 | 伪造本地域名 ris.local,恶意扫描 |
Agent 没有简单地建议封 IP,而是指出关键思路:封 IP 不是好办法,因为攻击 IP 会变化,要根据特征来封。于是基于 User-Agent 特征创建了 Apache Rewrite 规则,返回 403 Forbidden。
第二轮:伪造的浏览器
拦截爬虫后 CPU 仍然高达 84%。Agent 排除已拦截对象,扫描日志中的异常 User-Agent:
| User-Agent | 请求次数 | 破绽 |
|---|---|---|
| Chrome/142 (Win10) | 5053 | Chrome 真实最新稳定版 ~125 |
| Chrome/145 (Win10) | 4916 | 同上,版本号明显伪造 |
| Edge/145 (Win10) | 4922 | Edge 也没有 145 版本 |
每批 4900~5000 次请求,伪装成正常浏览器,但在 Agent 眼里无所遁形。
第三轮:Slowloris 慢速攻击
CPU 仍未降低。Agent 换了个角度——直接检查 TCP 连接层的状态。用 ss -tnp 检查 80 端口时,发现关键证据:某个 IP 的 Recv-Q = 939, Send-Q = 0。客户端连接上了,但只发了一部分请求头就卡住了。
这就是经典的 Slowloris 慢速攻击。靠”挂” Apache 进程——大量 IP 每个建 2 个连接保持半开,慢慢把进程池占满。
Agent 给出方案:iptables 限制单 IP 并发 ≤20,Apache 层面关闭 KeepAlive。
同时发现 Facebook 爬虫群(Meta AS 网段 69.171.x.x)在反复探测 /opt/gforge5/www/robots.txt。每条 403 响应本身也在消耗 CPU。
一个老系统的天花板
Agent 建议启用 Apache 的 mod_reqtimeout 模块来原生防御 Slowloris。检查后发现当前系统是 CentOS 6.3,2020 年就已 EOL,最高只支持 httpd 2.2.x,装不了 mod_reqtimeout 或 mod_evasive。
AI 总结道:当前多层防护已经是这个系统 Apache 层面能做到的天花板了。
短期策略:现有防护基础上,开启阿里云 DDoS 基础防护。长期策略:迁移至 AlmaLinux 8+ 获得 httpd 2.4 原生防护能力。
最终生效的五层防护
| 层 | 措施 | 状态 |
|---|---|---|
| UA 特征拦截 | MJ12bot / YisouSpider / ris-bot / Chrome/14x → 403 | [OK] |
| 路径拦截 | /opt/gforge5/ → 403 | [OK] |
| 连接限制 | iptables 单 IP ≤20 并发 | [OK] |
| 静态黑名单 | 高频攻击 IP → DROP | [OK] |
| 服务调优 | MaxClients=20 / Timeout=30 / KeepAlive Off | [OK] |
感受:运维方式的范式变化
这次从发现故障到完全解决,全程通过自然语言对话完成。
传统模式:SSH 登录 → tail 日志 → 分析 IP → awk 排序 → 写配置 → 校验 → 重载 → 再观察 → 循环。每个循环五到十分钟,四到五轮下来至少半小时,还容易遗漏。
对话模式:看到什么说什么。Agent 自动决定分析逻辑,自动写出正确的命令,自动配置规则,甚至主动发现用户没想到的问题。操作者只管做决策和确认。
第三部分:性能优化建议导致的崩溃事故
前文介绍了四层防御体系的搭建过程。然而在排查过程中,一起性能优化建议导致的崩溃事故让情况雪上加霜。
问题:MaxClients 被错误调高到 50
当时正常访问变慢,为了提升并发性能,AI 助手提出建议:MaxClients 20 → 50。这个数字看起来只是乘以 2.5 的变化,但对于一台内存仅 1.9GB 的老服务器来说,这是致命的:
MaxClients 50 的内存账:
50 × 50MB (空闲) = 2.5 GB → 已超物理内存
50 × 150MB (PHP) = 7.5 GB → 直接炸
可用内存:1.9 GB + 0 GB Swap = 1.9 GB
需求 2.5 GB > 1.9 GB → OOM
系统反复 OOM 崩溃
MaxClients=50 导致系统内存快速耗尽,OOM Killer 反复杀 httpd 进程。新请求进来 → 起新进程 → 内存不足 → 杀进程 → 再来请求 → 循环。最终操作系统完全无法启动:
阿里云诊断报告:Linux 实例因内存溢出(OOM),操作系统无法正常启动。错误代码:1684829582。
关键时刻的正确决策:果断停掉了 80 端口 httpd 服务,切断了 OOM 循环。系统内存压力解除后,才通过阿里云 VNC 进入系统修复。
Workbench Agent 再次出手:精准定位 OOM 根因
进入系统后,通过 Workbench Agent 继续分析,精准定位了 OOM 根因。从 /var/log/messages 提取到 OOM 记录:
Jul 7 16:48:16 kernel: Out of memory: Kill process 1983 (httpd) score 27
Jul 7 16:48:23 kernel: Out of memory: Kill process 1985 (httpd) score 27
Jul 7 16:48:28 kernel: Out of memory: Kill process 1987 (httpd) score 27
...(每 3 秒被杀一个,连续 10+ 个)
Workbench Agent 正确判断出:每个 PHP 请求时进程内存飙升到 120~150MB,根本解决方案是降低 MaxClients + 增加 Swap。Workbench Agent 将 MaxClients 紧急降到 8,创建了 2GB Swap。后续在 8 和 20 之间做了平衡测试,最终确认为 MaxClients=20 + 2GB Swap。
最终修复结果
MaxClients: 50 (错误建议) → 8 (紧急) → 20 (平衡值)
Swap: 0 GB → 2 GB
Timeout: 60 → 30
KeepAlive: On → Off
最终防护体系(七层)
| 层级 | 措施 | 防御目标 |
|---|---|---|
| Apache UA 特征拦截 | facebook / MJ12bot / YisouSpider / ris-bot / Chrome/14x / Go-http-client → 403 | 已知恶意爬虫 + 伪造浏览器 |
| Apache 路径拦截 | /opt/gforge5/ → 403 | 敏感目录探测 |
| Apache 服务调优 | MaxClients=20 / Timeout=30 / KeepAlive Off | 防资源耗尽 + 防 Slowloris |
| 网络层并发限制 | iptables 单 IP ≤ 20 并发 | 防分布式 CC |
| 自动封禁 | cron 每 5 分钟扫描空 UA 高频 IP → DROP | 动态封禁新攻击源 |
| 内存保护 | 2GB Swap + MaxClients=20 | 防 OOM 崩溃 |
| 目录列表禁用 | Options -Indexes | 防路径枚举 |
核心教训
教训一:优化建议必须算资源账
AI 助手在不知道服务器具体内存的情况下,直接建议 MaxClients 20 → 50,犯了想当然的错误。正确做法是先 free -m 查看内存,再计算:MaxClients = (可用内存 – 预留) / 单进程峰值内存。
教训二:错误的优化建议比攻击更危险
今天这个案例中,攻击者只是造成了访问变慢,而错误的优化建议直接导致了系统崩溃。性能调优必须先看资源余量,不能凭感觉给数字。
后续维护
所有爬虫规则统一维护在 /etc/httpd/conf.d/block-bots.conf。发现新威胁只需在该文件追加 RewriteCond 行并重启 Apache。
自动封禁脚本
通过 cron 每 5 分钟扫描一次,发现空 UA 或 4xx/5xx 高频探测的 IP,自动 DROP 封禁:
*/5 * * * * /usr/local/bin/block-bots.sh
脚本源码:
#!/bin/bash
# 防护:高频空UA + 高频4xx/5xx探测IP自动封禁
# 配置区
LOG_FILE="/var/log/httpd/access_log"
BLOCK_LOG="/var/log/block-bots.log"
TIME_SEC=600 # 统计窗口:10分钟
LIMIT=10 # 阈值:10次即封禁
# 日志不存在直接退出
[ ! -f "$LOG_FILE" ] && exit 0
# 临时文件
TMP_FILE="/tmp/suspicious_ips_$(date +%s).txt"
NEW_BLOCK=0
# 筛选规则:10分钟内 空UA 或 4xx/5xx错误请求
awk -v sec="$TIME_SEC" -v limit="$LIMIT" '
BEGIN{
now = systime();
cutoff = now - sec;
}
{
# 解析Apache日志时间 [01/Jul/2026:15:30:22 +0800]
split($4, t, /[:\/\[]/);
year = t[4]; mon = t[2]; day = t[3];
h = t[5]; mi = t[6]; s = t[7];
ts = mktime(year " " mon " " day " " h ":" mi ":" s);
# 条件:时间达标 且 (空UA 或 4xx/5xx状态码)
if (ts >= cutoff && ($6 == ""-"" || $9 ~ /^[45][0-9]{2}/)) {
print $1
}
}' "$LOG_FILE" | sort | uniq -c | awk -v lim="$LIMIT" '$1 > lim {print $2}' > "$TMP_FILE"
# 批量封禁IP
while read -r ip; do
# 过滤非法IP字符串
if [[ ! $ip =~ ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ ]]; then
continue
fi
# 判断是否已存在DROP规则,避免重复添加
if ! iptables -C INPUT -s "$ip" -j DROP 2>/dev/null; then
iptables -A INPUT -s "$ip" -j DROP
echo "[$(date '+%Y-%m-%d %H:%M:%S')] BLOCK $ip 触发:空UA/4xx5xx高频探测,阈值${LIMIT}次/${TIME_SEC/60}分钟" >> "$BLOCK_LOG"
NEW_BLOCK=1
fi
done < "$TMP_FILE"
# 只有新增封禁IP时,才持久化保存iptables规则,减少IO
if [ $NEW_BLOCK -eq 1 ]; then
# CentOS7+/Rocky Linux 保存方式
# iptables-save > $IPTABLES_CONF
# 如果你是CentOS6,替换上面一行用下面这条:
service iptables save
fi
# 清理临时文件
rm -f "$TMP_FILE"
# 日志超过10M自动轮转,防止磁盘占满
LOG_SIZE=$(du -k "$BLOCK_LOG" 2>/dev/null | awk '{print $1}')
if [ -n "$LOG_SIZE" ] && [ "$LOG_SIZE" -gt 10240 ]; then
mv "$BLOCK_LOG" "${BLOCK_LOG}.$(date +%Y%m%d)"
> "$BLOCK_LOG"
fi
注意:脚本中修复了 NEW_BLOCK 变量未初始化的 bug(原脚本缺少 NEW_BLOCK=0 初始化和循环内的 NEW_BLOCK=1 赋值),确保封禁动作能正确触发持久化。
日志巡检命令
awk '$9 ~ /^(40[0-9]|50[0-9])$/ && !/MJ12bot|YisouSpider|ris-bot|Chrome\/14[0-9]|facebookexternalhit|meta-externalagent|Go-http-client|DotBot|facebot/' /var/log/httpd/access_log | tail -n 20
筛出”被拦截但不在已知名单里”的请求,就是新威胁线索。
最终 block-bots.conf
RewriteEngine On
# 1. 优先拦截空User-Agent(扫描器常用无UA请求)
RewriteCond %{HTTP_USER_AGENT} "^$"
RewriteRule .* - [F,L]
# 2. 合并所有恶意爬虫UA,一条规则搞定,消除重复
RewriteCond %{HTTP_USER_AGENT} "(facebookexternalhit|meta-externalagent|Facebot|facebook|MJ12bot|YisouSpider|ris-bot|Chrome/14[0-9]|Go-http-client|DotBot)" [NC]
RewriteRule .* - [F,L]
# 3. 拦截频繁HEAD探测 /blog/ 路径
RewriteCond %{REQUEST_METHOD} HEAD
RewriteRule ^/blog/ - [F,L]
# 4. 拦截SQL注入特征(同时匹配URI + QUERY_STRING)
RewriteCond %{REQUEST_URI} (\%27|'|AND|OR|UNION|SELECT|--|#|;) [NC,OR]
RewriteCond %{QUERY_STRING} (\%27|'|AND|OR|UNION|SELECT|--|#|;) [NC]
RewriteRule ^ - [F,L]
# 5. 高危扫描目录拦截
RewriteRule ^/cgi-bin/ - [F,L]
# 6. 业务敏感目录拦截
RewriteCond %{REQUEST_URI} "^/opt/gforge5/" [NC]
RewriteRule .* - [F,L]
修改后执行 service httpd restart 生效。