CC攻击把服务器CPU跑满了怎么办?排查与降压实战

网站突然变得极慢、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 打满,本质是动态请求的高成本被反复触发;止血靠限流与封禁,治本靠缓存与边缘拦截,两条腿都要走。