一天重启两次:因为 nginx 看到了错误的 IP,限速形同虚设(CDN 后面的 real_ip)
发布于 2026年7月24日

症状:抓取下的每日重启
一台 EC2 t4g.medium(2 vCPU)在 CDN 后面服务一个 WordPress 多站点,每天重启两次。在负载下,10 个 PHP-FPM worker 在约 100% CPU 饱和,load 升到 4.70(在一台 2 vCPU 的机器上),平均响应时间到 15.77 秒。这是分布式抓取——很多 IP,每个请求很少——而限速什么都没拦住。
原因:nginx 数的是 CDN 的 IP
有一个按 $binary_remote_addr 的 12 req/min 限速区。问题是:在 CDN 后面,nginx 看到的 $remote_addr 是 CDN PoP 的 IP,而非客户端的。由于所有流量都"来自 CDN",限制把成千上万的客户端(和攻击者)聚到了少数几个 IP 下——永远不会正确触发。CDN 新增了 2 个 PoP,其范围不在 set_real_ip_from 里,于是对它们 real_ip 根本没有被重建。
# nginx 必须信任 CDN 的范围,才能从 X-Forwarded-For 读取真实 IP
set_real_ip_from 198.51.100.0/24; # CDN PoP(示例)
set_real_ip_from 203.0.113.0/24; # 缺失的新 PoP → real_ip 未重建
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# 只有这样,限速才会数对客户端:
limit_req_zone $binary_remote_addr zone=perip:10m rate=12r/m;CDN 后面的按 IP 限速,只有当 nginx 先重建真实 IP 才有效。如果 set_real_ip_from 没覆盖所有 PoP,你限的是 CDN——而非攻击者。
我们犯的错:封了 CDN 自己
匆忙之下,一次尝试是封掉"流量"来源的范围——而那是 CDN 的 PoP。这会让整个站点对所有人宕掉。教训是:封一个范围之前,先确认它是攻击者的还是你自己的边缘。real_ip 正确后,真实的源 IP 才显现,封禁才瞄准对的地方。
第二处调整:分页正则
抓取打的是很深的分页 URL。检测该模式的规则用了 {3,}(3 位以上数字),放过了两位数的页码。收紧到 {2,} 抓住了大部分滥用,又不影响正常浏览:
# 之前:只抓 /page/100+;之后:抓 /page/10+
location ~* "/page/[0-9]{2,}" { limit_req zone=perip burst=5 nodelay; }结果
平均响应时间 : 15.77s -> < 1s
Load (2 vCPU): 4.70 -> 0.24
PHP-FPM CPU : ~100% -> 正常
每日重启 : 2 -> 0运维细节:journald 不是持久化的,所以之前几次崩溃的日志丢了。持久化 journal(Storage=persistent)是修复的一部分——没有日志,每次重启都是一个新谜团。
经验教训
1. set_real_ip_from 必须覆盖所有 CDN PoP。列表之外的一个新 PoP 会破坏 real_ip 并让限速失明。
2. 没确认来源前,绝不封某个范围。封掉你自己 CDN 的 IP 会让站点对所有人宕掉。
3. 持久化 journald,否则你丢失证据。没有 Storage=persistent,每次重启都抹掉上一次重启的原因。
结论
限速的好坏取决于它所数的 IP。在 CDN 后面,nginx 看到的是 PoP——按 IP 的限制会形同虚设,直到你用 set_real_ip_from 为每个边缘范围重建真实 IP。有了正确的 IP,12 req/min 又重新意味着每客户端 12 req/min,服务器也不再重启。