同样的攻击流量,有的站纹丝不动,有的一分钟就 502。差距往往不在防护设备,而在源站的单请求成本。本文用 10 个问答讲清源站抗压该改哪几处、怎么算成本、配置怎么落地。
Q:为什么同样的流量,别人扛得住你挂了?
- 承载上限有个朴素公式:可用资源除以单请求成本。分母差三倍,能扛的流量就差三倍。
- 以 8 核 8G 的机器为例:单请求消耗 40 毫秒 CPU,理论上两百 QPS 就顶到天花板;如果优化到 8 毫秒,同样机器能到一千 QPS。
- 单请求成本高通常来自三件事:一次请求查了十几次数据库、每个页面都整页渲染模板、同步调用了外部接口(短信、地图、风控)。
- 防护设备只负责把「量」压低,压不到零。流量总会有一部分落到源站,最后扛住的是源站。
- 所以源站抗压不是锦上添花,它是防护体系的最后一段,也是唯一你能完全掌控的一段。
Q:怎么算出自己的单请求成本?
- 两个动作:阶梯压测找到天花板,日志统计找到最贵的接口。
- 压测要压非缓存接口,压首页得到的是缓存能力,不是源站能力。
# 阶梯加压,看每个档位的 P95 与错误率
for q in 50 100 200 400 800; do
echo "==== concurrency $q"
wrk -t4 -c$q -d30s --latency "https://www.example.com/api/list?page=1" \
| grep -E "Requests/sec|Latency|Non-2xx"
done
- 同时盯住 CPU。CPU 到 70% 的那个 QPS 就是你的实际承接上限,不是压到报错才叫上限。
- 日志侧统计最贵的 URI(要求
log_format末位是$request_time):
awk '{u=$7; t=$NF; s[u]+=t; c[u]++; if(t>m[u])m[u]=t}
END{for(k in s) printf "%-45s n=%-7d avg=%.3f max=%.3f\n", k, c[k], s[k]/c[k], m[k]}
' /var/log/nginx/access.log | sort -k3 -nr | head -20
- 把 avg 和 n 相乘,就是每个接口吃掉的总时间。占用最高的那几个,就是改造清单的第一梯队。
Q:哪些接口应该最先改?
- 排序公式:优先级 = P95 耗时 × 请求占比。不是挑最慢的,是挑「又慢又多」的。
- 五类高危:站内搜索、导出与报表、深度翻页、动态缩图、相关推荐与实时统计。
- 共同特征:用户点一次,源站在后台干好几秒。正常用户少点,攻击者可以连点。
- 改法优先级从高到低:加缓存、砍请求量、限上限、异步化、最后才考虑加机器。
Q:Nginx 层有哪些低成本加固?
- 连接数和请求数要双限。只限 QPS 挡不住慢连接,只限连接数挡不住高频短请求。
- 超时一律收紧。默认的 60 秒超时在攻击期间会白占几十万个连接。
# http 层
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=20r/s;
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 10s;
keepalive_timeout 15s;
reset_timedout_connection on;
open_file_cache max=20000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
server {
limit_conn conn_per_ip 30;
limit_req zone=req_per_ip burst=40 nodelay;
limit_req_status 429;
limit_conn_status 429;
# 静态资源不参与限流,否则页面直接散架
location ~* \.(css|js|jpg|png|webp|woff2)$ {
limit_req off;
limit_conn off;
expires 30d;
access_log off;
try_files $uri =404;
}
if ($request_method !~ ^(GET|HEAD|POST)$) { return 405; }
}
open_file_cache对静态文件多的站效果明显,能省掉每次请求的stat系统调用。access_log关掉静态资源,不是省磁盘,是省 CPU 和磁盘 IO 争抢。
Q:深度翻页和导出为什么是重灾区?
- 翻到第一万页时,数据库要扫十万行再丢掉,耗时随页码线性上涨。攻击者只要反复请求最后一页,就能用极低成本压死数据库。
- 解法是 keyset 分页,用上一页最后一条记录的值做条件,耗时恒定。
-- 坏:越翻越慢
SELECT id,title FROM article ORDER BY id DESC LIMIT 20 OFFSET 100000;
-- 好:keyset 分页,始终走索引
SELECT id,title FROM article WHERE id < 900000 ORDER BY id DESC LIMIT 20;
- 再补两道硬限制:页码超过 100 直接返回 400,导出超过 5 万行改成异步任务。
- 导出走队列:请求只负责入队并返回任务号,后台生成好再通知下载。导出接口是 CC 攻击最爱的目标之一,因为它天然又慢又重。
Q:缓存怎么用才真的减负?
- 优先片段缓存而不是整页缓存。整页缓存遇到登录态、购物车就废,片段缓存可以只缓存不随用户变化的部分。
- 热点数据放 Redis,同时做三件事:空值也缓存(防穿透)、同一 key 只放一个请求去回源(防击穿)、过期时间加随机偏移(防雪崩)。
- 反向代理层的缓存只对匿名 GET 生效,带登录 Cookie 的请求必须绕过,否则会把别人的页面发给下一个人。
proxy_cache_path /data/cache levels=1:2 keys_zone=page:200m inactive=10m max_size=8g;
map $http_cookie $skip_cache {
default 0;
"~*(PHPSESSID|wordpress_logged_in|token)" 1;
}
map $request_method $skip_method {
GET 0;
default 1;
}
location / {
proxy_cache page;
proxy_cache_valid 200 5m;
proxy_cache_bypass $skip_cache $skip_method;
proxy_no_cache $skip_cache $skip_method;
proxy_pass http://backend;
}
- 缓存的价值要量化:改造后回源 QPS 占比下降多少。占比不降,说明缓存键设计有问题。
Q:数据库连接池和进程数怎么设才不出事?
- 估算公式:连接数约等于峰值 QPS 乘平均查询耗时(秒)。一千 QPS、单查询 10 毫秒,理论上十个连接就够,留余量给二十到三十。
- 最常见的坑是乘法陷阱:PHP 进程数设 200,每个进程一个数据库连接,数据库只给 151 个连接上限,攻击一来立刻大面积「连接数超限」。
; php-fpm:按内存算,不是按 CPU 核数算
; max_children = 可用内存 / 单进程常驻内存
pm = dynamic
pm.max_children = 60
pm.start_servers = 10
pm.max_spare_servers = 20
pm.max_requests = 1000
-- MySQL:连接上限与空闲回收
SHOW VARIABLES LIKE 'max_connections';
SET GLOBAL max_connections = 300;
SET GLOBAL wait_timeout = 60;
- 三层要满足一条不等式:应用进程数 × 每进程连接数 ≤ 数据库连接上限的 80%。
Q:慢请求会不会拖死整个站?
- 会,而且比流量更致命。超时必须逐层递减,外层永远比内层宽松。
- 推荐链路:客户端 60 秒,CDN 30 秒,Nginx 20 秒,应用 15 秒,数据库 10 秒。
- 顺序反了就会出现「上游已经超时放弃,下游还在拼命算」,等于白烧资源。
proxy_connect_timeout 5s;
proxy_send_timeout 20s;
proxy_read_timeout 20s;
fastcgi_connect_timeout 5s;
fastcgi_read_timeout 20s;
- 再加一道熔断:某个下游连续错误就快速失败,别再排队。
max_fails加fail_timeout就是最轻量的版本。
Q:改造完怎么验证真的有效?
- 用同一个压测脚本、同一个 URL、同一个并发做前后对比,只改一个变量。
- 看四个数:同 QPS 下的 CPU 占用、P95 延迟、错误率、数据库 QPS。
- 达标线可以定成:承接上限至少翻倍,或者同 QPS 下 CPU 占用降一半。
- 别压首页交差。首页有缓存,压出来漂亮,攻击来了照样挂。要压的是登录、搜索、列表这些真正吃资源的路径。
Q:还有什么补充建议?
- 给非核心功能做一键开关:推荐、评论、实时统计、在线客服,攻击期间先关掉,把资源全留给主流程。
- 图片走对象存储加 CDN,源站不要动态产图。缩图接口是典型的「一个小请求换几秒 CPU」。
- 首页和栏目页定时静态化,攻击期间切静态兜底,这是最便宜的降级手段。
- 日志单独落盘或直接推走,别和网站数据抢同一块盘。
- 最后一句:防护决定你能不能扛住第一波,源站决定你能扛多久。两条腿都要有,缺一条都站不住。