网站突然变得极慢、SSH 都卡、监控里 CPU 一路飙红,很多人的第一反应就是「被 CC 攻击了」。本文用 9 个问答讲清怎么判断 CPU 高到底是不是 CC、用哪些命令能快速定位元凶、Nginx 与系统层怎么限流降压、封禁怎么避免误伤,以及 CPU 被打满时的应急处理顺序。
Q:CPU 突然跑满,一定是被 CC 攻击了吗?
- 不一定。CPU 高可能是 CC,也可能是自身问题:慢 SQL、死循环脚本、备份任务、日志写入过多,甚至是被当成肉鸡跑挖矿。
- 区分看三点:流量是否异常上涨、是否集中在某个动态接口、访问来源是否高度分散。
- CC 的典型特征是:QPS 不算离谱,但请求全部打在 消耗 CPU 的动态页面或接口(搜索、列表、详情、API),后端每次都要算一遍,CPU 自然被吃满。
- 如果 CPU 高但访问日志没涨,先怀疑本机进程,而不是攻击。
Q:怎么用命令快速定位 CPU 被谁吃掉了?
# 1) 先看整体负载与 CPU 占用排行
uptime
top -o %CPU
# 2) 进入 top 后按 1 展开每个核,按 H 展开线程
# 看是 nginx / php-fpm / mysql 哪个进程在烧 CPU
# 3) 按进程统计 CPU 占用
ps -eo pid,ppid,%cpu,%mem,cmd --sort=-%cpu | head -20
# 4) 看 Web 服务当前连接情况
ss -s
ss -antp | awk '{print $1}' | sort | uniq -c
- 重点是区分 用户态高 还是 内核态高:top 里 us 高说明应用在算(多半是动态请求),sy 高说明系统调用与网络栈在扛(多半是连接或转发风暴)。
- 若 nginx 进程本身 CPU 很高,通常是 单 IP 高频请求叠加日志同步写;若 php-fpm 高,则说明动态请求被刷。
Q:怎么从访问日志确认是 CC 而不是真实流量?
# 统计 Top IP 的请求数
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 统计请求最集中的 URL
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 统计每分钟请求数,看是否有明显尖峰
awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1-3 | sort | uniq -c | tail -20
- CC 的三个日志特征:单个 URL 的请求高度集中、来源 IP 数量很大但每 IP 请求量不大、几乎没有静态资源请求(只打
.php、.do、/api/,不拉图片和 CSS)。 - 真实用户的请求顺序是「先 HTML 再一堆静态资源」,比例严重失衡的基本就是脚本流量。
- 如果发现 UA 高度一致、Referer 为空,顺着 UA 再聚合一次,往往能一把揪出批量肉鸡。
Q:Nginx 层怎么限流,先把 CPU 降下来?
# 全局配置:按 IP 限速,10MB 共享内存足够记录大量 IP
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=connip:10m;
server {
# burst 允许一定突发,nodelay 让突发立即处理不排队
limit_req zone=perip burst=20 nodelay;
# 单 IP 并发连接上限,防止连接耗尽
limit_conn connip 30;
limit_req_status 429;
limit_conn_status 429;
}
- rate 要贴着真实峰值定:设太低会误伤正常用户(尤其是 NAT 出口下的办公网),建议先观察线上 P95 建立基线。
- 动态接口单独加一条更严的规则,把
location ~ ^/(search|api)/的 rate 压到 2r/s,静态资源不限。 - 限流只是降载,不是拦截。真正省 CPU 的是让请求根本不进后端。
Q:系统层还有哪些参数必须一起调?
# 后端并发进程数,决定了 CPU 被吃满的速度
# php-fpm.conf:按内存与核数定,不要盲目调大
pm = dynamic
pm.max_children = 30
# MySQL 最大连接数(连接数暴涨常被误判成 CPU 问题)
# /etc/my.cnf
max_connections = 300
# 系统参数:缩短无效连接占用、加大半连接队列
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 4096
- php-fpm 的 max_children 是关键:进程越多,被 CC 打时 CPU 越容易被瞬间拉满,要结合 CPU 核数与内存反推合理值。
- 动态请求里如果有慢查询,会在高并发下被放大成 CPU 雪崩,先开慢查询日志把慢 SQL 揪出来。
Q:为什么说缓存才是最有效的降压手段?
- CC 专打动态页面,因为动态页面必须经过 PHP 与数据库,CPU 成本最高。
- 把不随用户变化的列表页、详情页做缓存(Nginx fastcgi_cache、Redis、页面静态化),命中缓存的请求只消耗极少 CPU,攻击效果被大幅削弱。
- 缓存的意义不只是「省一次查询」,而是把攻击的高成本转嫁给对方:原本每个请求都要算一遍,现在同一页面每秒刷 100 次也只算一次。
- 注意设置合理的缓存时长与回源策略,避免缓存穿透把压力又打回后端。
Q:怎么封禁高频 IP 又不误伤正常用户?
# 1) 系统层限制单 IP 并发连接数(对 CC 很有效)
iptables -I INPUT -p tcp --dport 80 -m connlimit \
--connlimit-above 50 --connlimit-mask 32 -j DROP
# 2) 从日志提取高频 IP 批量短时封禁
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c \
| sort -rn | awk '$1>1000{print $2}' > ban.txt
ipset create ccban hash:ip timeout 3600 maxelem 200000 -exist
while read -r ip; do ipset add ccban "$ip" -exist; done < ban.txt
iptables -I INPUT -p tcp -m multiport --dports 80,443 \
-m set --match-set ccban src -j DROP
- 一定要用短时封禁(timeout),不要写永久黑名单。CC 的 IP 是耗材,秒拨与代理池会不断换新 IP,而误封的真实用户却会一直进不来。
- 封禁前先排除白名单:公司出口 IP、CDN 回源段、合作方接口、监控探针。如果站点在 CDN 后面,务必先确认拿到的是真实客户端 IP,否则封的是回源节点,全站直接挂。
- 对无法确认的 IP,优先用限速与人机验证,而不是直接 DROP。
Q:CPU 已经被打满,应急处理顺序是什么?
- 第一步,先保命:把攻击最集中的接口用 Nginx 返回
return 429;或直接拒绝,先让正常用户能打开首页。 - 第二步,再封禁:按 IP、UA、连接数做短时封禁,压制最猛的来源。
- 第三步,再降载:打开缓存与静态化,把高成本请求挡在后端之外。
- 第四步,看数据:对比攻击前后的 QPS、缓存命中率、回源率,判断处置是否有效。
- 最后才是复盘与固化规则:把这次的特征写进 WAF 规则与监控告警,避免下次再从零救火。
- 顺序很重要:先大规模封禁再降载,容易把正常用户一起挡在外面,反而扩大事故面。
Q:还有什么补充建议?
- 先建基线再谈拦截:把平峰的 QPS、连接数、CPU 使用率记下来,异常时才判断得准。
- 把判断放到边缘:有条件时用 CDN 或云 WAF 在边缘做限频与人机验证,别让所有请求都打到源站烧 CPU。
- 监控要比人快:对 CPU、连接数、429 比例设阈值告警,而不是等用户投诉才发现。
- 慎用「一键封 C 段」:C 段里可能混着正常用户与第三方服务,封之前先看归属与置信度。
- 一句话总结:CC 把 CPU 打满,本质是动态请求的高成本被反复触发;止血靠限流与封禁,治本靠缓存与边缘拦截,两条腿都要走。