服务器日志里全是同一个页面或接口的请求,数据库连接池的活跃数飙到上限,新的请求全部在排队或者直接拒绝连接,CPU 负载可能并不高,但网站已经打不开了。这就是典型的 CC 攻击打到数据库瓶颈的表现。攻击者不是靠海量垃圾流量撑爆你的带宽,而是用看似正常的请求,专门挑那些最消耗数据库资源的 URL,反复请求,直到数据库连接被耗尽。
处理这类问题,不能只盯着流量清洗,必须从请求入口、中间件、数据库自身三个层面同时下手。下面直接讲具体怎么操作。
第一步:在流量入口做紧急止损当你发现数据库连接数已经耗尽时,第一件事不是分析日志,而是先止血。登录服务器,检查 Web 服务器或应用服务器的当前连接状态。如果是 Nginx,执行以下命令可以看到当前活跃连接和请求数:
# 查看 Nginx 状态,包含活跃连接、读写等待数 curl http://127.0.0.1/nginx_status
如果发现某类 URL 的请求频率异常高,比如某个搜索接口、列表翻页接口,立刻在 Nginx 层面对这类 URL 做频率限制。不要犹豫,直接在 Nginx 配置里加上 limit_req 模块的限制:
http {
# 定义一个共享内存区域,限制每秒每个 IP 只能请求 10 次,突发最多 20 次
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_zone $server_name zone=perserver:10m rate=100r/s;
server {
location /search/ {
# 应用 IP 限制,超过限制返回 503
limit_req zone=perip burst=20 nodelay;
proxy_pass http://backend;
}
location /api/hotlist {
limit_req zone=perip burst=5 nodelay;
proxy_pass http://backend;
}
}
}
这个操作可以立刻把攻击请求拦截在 Web 层,不让它穿透到应用服务器和数据库。nodelay 参数意味着超过突发容量的请求直接返回 503,不会排队等待,避免占用连接资源。如果你用的是 OpenResty 或者 Tengine,还可以用更精细的 lua 脚本做动态频率控制,但紧急情况下,原生 limit_req 最快最有效。
第二步:应用层做请求缓存,避免直接查库CC 攻击能打到数据库,说明你的应用逻辑大概率是每次请求都去实时查询数据库。对于攻击者盯上的那些高消耗接口,必须立刻加上缓存层。这不是优化,是保命。如果你的应用没有集成 Redis 或 Memcached,现在就要加。
对于热点数据,比如首页文章列表、热搜关键词、分类导航,这些数据在短时间内变化极小,完全可以在应用层做内存级缓存或 Redis 缓存,设置一个合理的过期时间,比如 5 到 10 秒。这样即使攻击者疯狂请求同一个 URL,大部分请求都会被缓存挡住,根本不会落到数据库上。
代码层面最简单的实现思路(以 PHP 为例):
// 连接 Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$cacheKey = 'hot_articles_list';
$articles = $redis->get($cacheKey);
if ($articles === false) {
// 缓存未命中,查询数据库
$articles = $db->query("SELECT * FROM articles ORDER BY views DESC LIMIT 20");
// 缓存结果,过期时间 10 秒
$redis->setex($cacheKey, 10, serialize($articles));
} else {
$articles = unserialize($articles);
}
这个逻辑一旦上线,数据库的查询压力会断崖式下降。攻击者请求再频繁,大部分请求都变成了 Redis 读取操作,而 Redis 单机就能扛住每秒数万甚至十万级别的读请求,完全不是一个量级。
第三步:数据库连接池优化与慢查询治理数据库连接被耗尽,除了请求量大,还有一个常见原因是单个请求占用连接时间过长。如果某个 SQL 查询需要执行 2 秒,而你的连接池最大只有 50 个,那么每秒只要来 25 个这种慢请求,连接池就会被占满。所以,必须立刻排查慢查询。
登录 MySQL,执行以下命令查看当前正在执行的查询:
SHOW FULL PROCESSLIST;
你会看到大量处于 Sending data 或 Locked 状态的查询,而且 Info 字段显示的 SQL 语句往往就是攻击者反复请求的那个接口产生的。对于这些 SQL,立刻用 EXPLAIN 分析执行计划,检查是否命中索引。如果没有索引,马上加上。如果表数据量太大,索引优化来不及,就先在应用层把这条 SQL 的查询结果缓存起来,或者直接返回静态数据。
同时,调整数据库连接池的配置。如果你用的是 Tomcat JDBC Pool 或者 HikariCP,把最大连接数调大只是权宜之计,更重要的是设置合理的连接超时和闲置回收时间:
# HikariCP 配置示例 spring.datasource.hikari.maximum-pool-size=50 spring.datasource.hikari.minimum-idle=10 spring.datasource.hikari.connection-timeout=3000 spring.datasource.hikari.idle-timeout=600000 spring.datasource.hikari.max-lifetime=1800000
connection-timeout 设置为 3000 毫秒,意味着如果连接池满了,新的请求等待 3 秒还拿不到连接就直接抛异常,不会无限等待。这能防止请求线程本身被耗尽。同时,应用层要对这种异常做捕获,返回一个友好的提示页面或者 503 状态码,而不是让用户看到白屏或者报错。
第四步:识别攻击特征并自动化拦截手动加限制只能应急,攻击者随时可能更换目标 URL。你需要根据当前的攻击日志,提取出攻击特征,然后写一条通用的拦截规则。CC 攻击的请求通常有非常明显的特征:请求集中在少数几个 URL、User-Agent 固定或者为空、Referer 缺失、请求间隔极其规律。
分析 Nginx 访问日志,用 awk 命令快速找出请求频率最高的 IP 和 URL:
# 统计访问最频繁的 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
如果发现某个 IP 在短时间内请求了上千次同一个接口,直接把它加入黑名单。在 Nginx 里可以这样封禁:
location / {
deny 192.168.1.100;
deny 10.0.0.0/8;
}
但手动封 IP 效率太低,攻击者可能使用代理池。更有效的方式是结合 Nginx 的 map 模块和 limit_req,对没有 Referer 或者使用固定 User-Agent 的请求直接返回 444(关闭连接不返回任何内容):
map $http_user_agent $bad_agent {
default 0;
"~*sqlmap" 1;
"~*acunetix" 1;
"~*nikto" 1;
"" 1; # 空 User-Agent 直接标记
}
server {
if ($bad_agent) {
return 444;
}
}
对于更复杂的攻击,建议接入专业的 WAF(Web 应用防火墙),配置 CC 防护策略。现在主流的 WAF 都支持基于会话的人机识别和频率控制,能够自动区分正常用户和攻击流量。
第五步:架构层面的长期防御方案应急处理完后,必须从架构上解决数据库连接容易被耗尽的问题。核心思路是“读写分离”和“异步化”。
如果还没有做读写分离,尽快搭建主从架构。所有查询类请求(SELECT)全部走从库,写入类请求走主库。这样即使从库连接被打满,主库依然可以正常写入,不影响核心业务。在应用层用中间件如 MyCat 或者 ShardingSphere 来做路由,或者简单地在代码里用不同的数据源。
对于不需要实时返回结果的请求,比如记录日志、统计访问量,全部改成异步处理。用消息队列(RabbitMQ、Kafka)把请求先扔进队列,后端慢慢消费写入数据库。这样数据库的写入压力就变成了平滑的流,而不是突发尖峰。
还有一个容易被忽视的点:前端页面静态化。对于内容型网站,把动态页面生成静态 HTML 文件,直接由 Nginx 返回,完全不经过应用和数据库。攻击者即使请求这些页面,消耗的也只是 Web 服务器的少量资源,数据库零压力。可以用 CMS 自带的静态化功能,或者自己写脚本定期生成。
第六步:监控与预警最后,你必须建立一套监控体系,在数据库连接数达到危险阈值之前就收到告警。不要等到用户反馈网站打不开了才发现问题。
在应用服务器上部署 Prometheus + Grafana,监控数据库连接数、QPS、慢查询数量。对于 MySQL,可以用 mysqld_exporter 采集指标;对于应用层,在代码里暴露连接池的 metrics。设置告警规则,比如当活跃连接数超过最大连接数的 80% 时,立刻发送短信或即时通讯工具告警。
同时,对 Nginx 的错误日志做监控。如果 503 错误率突然飙升,很可能就是限流模块在起作用,说明正在遭受攻击。可以用 ELK(Elasticsearch、Logstash、Kibana)或者简单的脚本分析日志,一旦 503 比例超过 5%,就触发告警。
整套方案从紧急止血到长期防御,核心逻辑是:先限制频率挡住攻击,再用缓存减少数据库请求,然后优化慢查询和连接池配置,最后通过架构调整和监控体系把防御能力固化下来。CC 攻击打到数据库耗尽,本质上是对你系统瓶颈的精准打击,只有把每一层都加固,才能真正扛住。
