您现在的位置: 365建站网 > 365文章 > 网站被 DDoS 怎么应急?PHP 限流与数据库连接池配置实操

网站被 DDoS 怎么应急?PHP 限流与数据库连接池配置实操

文章来源:365jz.com 点击数:10 更新时间:2025-03-01 10:04

小型 PHP 站点被刷时的第一反应往往是“加个限流”。但限流如果只做在应用层,带宽早已被打满,PHP 进程也全被唤醒,等于没做。本文按带宽层 → Web 服务器层 → PHP 应用层 → 数据库连接池四层给出一套可实际落地的应急与长期方案。

网站被DDoS怎么应急 PHP限流与数据库连接池

一、先分清你在挨哪种打

  • 流量型(带宽耗尽):UDP 反射、ICMP 洪水。特征是入带宽打满、源 IP 分散。应用层限流无效,只能靠上游清洗(运营商/CDN 高防)。
  • 连接型:SYN Flood、半连接耗尽。看 netstat -s | grep -i 'SYN' 的重传数,需要内核参数调整。
  • 应用层 CC(HTTP Flood):模拟真实请求打动态接口,如反复访问搜索、发短信接口。特征是 QPS 不高但 php-fpm 全忙、MySQL CPU 100%。这类才轮到 PHP 限流出场。
  • 慢速攻击(Slowloris/慢 POST):连接挂着不发完请求,占满 worker。timeout 参数与 limit_conn 是关键。

判断命令:看 access.log 里 TOP 路径与 TOP UA 即可定位 CC 目标接口;tail -f /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head。

二、Web 服务器层:Nginx 三把闸

http {
    # 1) 按 IP 限制并发连接
    limit_conn_zone $binary_remote_addr zone=perip:10m;

    # 2) 按 IP 限制请求速率:5 r/s,允许 10 的突发
    limit_req_zone $binary_remote_addr zone=reqperip:10m rate=5r/s;

    # 3) 对单个热点接口单独更严的闸(用 map 或单独的 location)
    server {
        location /api/search {
            limit_req zone=reqperip burst=5 nodelay;
            limit_conn perip 10;
            limit_rate 200k;          # 单连接下行限速
        }
        location / {
            limit_req zone=reqperip burst=20 nodelay;
        }
    }
}

要点与坑:

  • rate 是每秒请求数,Nginx 内部按毫秒精度计时;burst 是允许的队列长度,nodelay 表示队列里的请求立即处理而不是排队(不加则超出 rate 的部分被延迟)。
  • 被限流的请求默认返回 503。要防止攻击者拿 503 判断“这里有限流”,可以 limit_req_status 429;(推荐 429 Too Many Requests)。
  • 共享内存大小:10m 约能存 16 万个 IP 状态;IP 多时被打满会开始淘汰旧状态,出现“限流失效”,观察 nginx_error.log 里的 limiting requests, zone ...。
  • 如果有 CDN/WAF,$binary_remote_addr 是所有边缘节点 IP,限流会把正常用户一起限掉。必须改用真实客户端 IP:set_real_ip_from CDN网段; real_ip_header X-Forwarded-For; real_ip_recursive on;。
  • 慢速防护:client_header_timeout 10s; client_body_timeout 10s; send_timeout 10s; keepalive_timeout 30s;,配合 limit_conn。

三、PHP 应用层:不要自己造计数器

常见错误写法是用文件计数(file_put_contents 写一个数字),高并发下会有竞态且疯狂磁盘 I/O。正确姿势是 Redis + Lua 原子操作:

$key = 'rl:' . $ip . ':' . $route;
$lua = <<<'L'
local n = redis.call('INCR', KEYS[1])
if n == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) end
if n > tonumber(ARGV[1]) then return 0 end
return 1
L;
$allowed = $redis->eval($lua, [$key, 60, 30], 2);   // 30 次/60 秒
if (!$allowed) {
    http_response_code(429);
    header('Retry-After: 60');
    exit(json_encode(['code' => 429, 'msg' => '请求过于频繁']));
}

三个细节:INCR + EXPIRE 必须在同一个 Lua 脚本里(否则进程在两步之间被杀,键永不过期,形成“永久封禁”);eval 的第三个参数是 keys 个数(这里为 2 是因为我传了 key+两个参数,实际应按 eval($lua, [$key, $limit, $ttl], 1),keys 数量写错是最常见的 bug,Redis 会报 Error the script attempted to access unused global variable 或直接语义错乱)。

更工程化的做法是令牌桶(允许突发、平均速率恒定),或直接上现成组件:bandwidth-throttle/token-bucket、tagset/session-bucket(Composer 可装)。

别忘了 php-fpm 自身的池配置,这是最后一道背压:

; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 64          ; 按 内存/(单进程RSS) 估算,不要拍脑袋
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 24
pm.max_requests = 2000        ; 定期回收,缓解内存泄漏
request_terminate_timeout = 20s   ; 超过就杀掉,防慢请求堆积

max_children 设太大会直接 OOM 或 swap 打死机器,比被攻击还严重。用 ps --no-headers -o "rss" -C php-fpm8.2 | awk '{s+=$1} END {print s/NR/1024 " MB"}' 测出单进程平均 RSS 再算。

四、数据库连接:CC 打垮站点的真正位置

多数“PHP 站点被打死”实际是 MySQL 被打死:每个请求一条连接、每个连接跑一个慢查询,很快 max_connections 耗尽,报 Too many connections。措施顺序:

  1. 限制来源:GRANT ... ON db.* TO 'app'@'10.0.2.%',并给该账号设资源上限:
    ALTER USER 'app'@'10.0.2.%' WITH MAX_USER_CONNECTIONS 80 MAX_QUERIES_PER_HOUR 0;
  2. 用连接池而不是持久连接裸连:Swoole/RoadRunner 自带协程连接池;传统 FPM 场景优先上 ProxySQL(mysql-monitor_username、后端连接复用、查询缓存、按 hostgroup 分流),或 MySQL Router;
  3. PHP 端避免 mysql_pconnect(FPM 下的持久连接会随子进程数量线性放大后端连接);
  4. 打索引与热点缓存:把 CC 目标接口(搜索、列表)加 Redis 缓存 30~60 秒,即使返回旧数据也比挂掉好;用 SELECT * FROM information_schema.PROCESSLIST WHERE TIME > 2 ORDER BY TIME DESC LIMIT 20 找慢查询;开启 slow_query_log;
  5. 降级策略:对高开销接口做“熔断”,Redis 里放开关位,命中阈值直接返回缓存页或简化版结果。

五、上游与长期方案

  • 接入带 CC 防护的 CDN/WAF(能识别 JS 挑战、指纹、行为模型),把真实源站 IP 换掉并只允许 CDN 回源 IP 访问 80/443(防火墙白名单),否则攻击者会绕过 CDN 直接打源站;
  • 内核层可调:net.ipv4.tcp_syncookies=1、net.core.somaxconn=1024、net.ipv4.tcp_max_syn_backlog=8192(sysctl -w 后写进 /etc/sysctl.conf);
  • 带宽型攻击必须依靠上游清洗,本地无能为力,提前谈好高防方案与切换流程;
  • 演练与监控:把 429 数量、fpm slow.log(配 request_slowlog_timeout = 3s; slowlog = /var/log/php-fpm/slow.log)、MySQL 线程数都接入告警。

六、常见问题 FAQ

问:加了 Nginx 限流为什么还是慢?
答:限流解决的是“请求过多”,不解决“单个请求过重”。用 request_slowlog_timeout 定位到底是哪个接口在慢,通常是一个未加索引的查询。

问:限流会不会误伤公司内网/机房出口同一 IP 的大量用户?
答:会。要按 $binary_remote_addr + http_x_api_key 之类的组合作为限流维度,或把可信网段用 geo 排除。

问:pm = static 更好吗?
答:在内存固定、并发可预测的场景 static 更稳(无进程创建开销、不会突然爆内存),适合被攻击时保底。

应急顺序记一条:先看日志确定打哪个接口 → Nginx 限住 → Redis 缓存兜住 → fpm/DB 连接池压住 → 决定是否上报上游清洗。四层里只补一层,基本挡不住。

生产内容,同时打通搜索引擎(SEO)与AI搜索引擎(GEO)流量入口的软件! 点击进入365建站器 »