做运维和安全的这些年,半夜被叫起来处理故障,最怕听到的不是“服务挂了”,而是“服务打不开,而且带宽跑满了”。前者多半是程序出了问题,重启一下、回滚版本还能救;后者往往意味着你正在被攻击,而且连日志都看不到——服务器还没来得及处理什么请求,链路已经被塞死了。DDoS攻击(分布式拒绝服务)这个名字听着唬人,拆开看其实就一件事:用远超你资源上限的请求量,让正常用户拿不到服务。而“三种方式”——流量型、连接型、应用层——的本质区别在于,它们分别去抢你身上三种完全不同的资源:带宽、会话表和 CPU。
这篇文章不聊怎么打人,只聊怎么被打以及怎么防。我按自己处理过的几类故障,把三种消耗路径的原理、指标特征、防护配置和排查手法完整拆一遍,包括内核参数怎么算、限速阈值怎么定、监控指标怎么看。适合刚接手线上服务的后端和运维同学,也适合已经挨过打但还没想明白“为什么带宽没满服务也死了”的朋友。看完你至少能做到一件事:监控告警响的时候,五分钟内判断出对方在打哪一层。
1. 先把“消耗”这件事拆开看
1.1 三种攻击路径本质上在抢不同的资源
很多人把 DDoS 当成一个笼统的词,实际上它对应的是三个完全独立的资源池,防护手段也几乎不重叠。流量型打的是链路带宽和网络设备的包处理能力,你的服务器 CPU 可能只跑了 5%,但上行口已经 100% 占满,正常请求的包根本进不来。连接型打的是操作系统的会话表——半连接队列、conntrack 表、文件描述符,带宽占用可能只有几十兆,但新连接一个都建不起来。应用层打的是业务逻辑本身,一个需要查三次数据库、调两个外部接口的接口,被打上几百 QPS 就可能拖垮整个后端。
我用一个生活类比解释这三者的差别:流量型是把小区门口的路堵死,谁都进不来;连接型是门口保安的登记簿写满了,后面的人排着队但没人有空登记;应用层是好不容易进了门,发现每个窗口都在办一件极其麻烦的业务,一个客户能占住柜台半小时。三种堵法,疏通的方式完全不同——第一种要找交警疏导交通,第二种要加开登记窗口或者优化登记流程,第三种要简化业务或者限制单客户办理时长。
这也解释了为什么只买“高防带宽”不一定管用。带宽清洗设备擅长处理第一种和部分第二种,但面对“每个请求都长得像正常用户”的应用层消耗,流量清洗基本无能为力,因为从包的角度看,那些请求完全合法。
1.2 为什么攻击者总是挑最便宜的那条路
从攻击成本角度看,三种方式的“性价比”差异巨大。流量型看起来声势浩大,但对攻击方来说成本最高——他需要真实的出口带宽,要凑够 100Gbps 的流量,就得有相应的资源储备或被控节点。连接型便宜一些,因为一个几十字节的 SYN 包就能在你的内核里占一个表项,几台普通机器就能制造出几十万半连接。应用层最便宜,一个 HTTP 请求几十字节的请求头,可能触发你后端几百毫秒的计算,放大比轻松上百倍,甚至一台机器、一个脚本就能让某个业务接口不可用。
所以真实的攻击往往是混合的:先用小流量探测你的防护策略和业务架构,然后流量型压带宽、连接型压会话表、应用层压最贵的那个接口,三管齐下。我见过的一次是前面几分钟只有零星几个异常请求,像是在摸接口,十几分钟后突然同时出现 UDP 洪水和针对搜索接口的高频请求,一个打网络层,一个打数据库。
注意:判断攻击类型时不要只看总流量曲线。很多团队盯着带宽图,带宽没动就以为没事,实际上那时候连接表和 CPU 已经先撑不住了。
1.3 一个常被忽略的前提:你的服务到底暴露了多少入口
防护之前得先知道自己有多少个“门”。我接手过一个项目,主站做了防护,结果一个测试用的旧接口通过独立域名直接暴露在公网,没有限速、没有鉴权,被打的时候主站监控毫无反应,直到数据库连接池被打满才被发现。类似的还有:对象存储的回源地址、监控上报接口、第三方回调地址、内部服务的临时映射端口。
我的做法是定期做一次资产梳理,把所有对公网可达的域名和端口列出来,逐个确认:是否必须公网可达、是否有限速、是否有鉴权、是否有独立的容量上限。这份清单在被打的时候价值极高,因为你能立刻知道哪些入口可以先临时关掉止血。下表是我自己用的一份简表模板,各位可以照着填:
| 入口类型 | 是否必须公网 | 现有防护 | 可临时关闭 | 关联后端资源 |
|---|---|---|---|---|
| 主站 Web | 是 | 四层清洗 + 七层限速 | 否 | 应用集群、数据库 |
| 开放 API | 是 | 鉴权 + 配额 | 否 | 应用集群 |
| 回调接收 | 是 | 来源白名单 | 视业务 | 队列 |
| 管理后台 | 否 | 内网访问 | 是 | 应用集群 |
| 监控上报 | 是 | 限速 | 是(降级) | 时序库 |
| 测试环境映射 | 否 | 无 | 是 | 同上生产 |
这张表我一般会放在应急文档第一页。出事的时候人容易慌,有清单能少想很多。
2. 流量型消耗:先把管道塞满再说
2.1 从“包大”和“包多”两个方向理解流量冲击
流量型攻击可以粗暴地分成两个方向:把包做大,和把包做多。大包方向的代表是 UDP 洪水和各种反射放大,单包体积大或者放大倍数高,用较少的发起流量换出巨大的响应流量,直接吃满你的入口带宽。小包方向的代表是 SYN 洪水和小包 Flood,单个包只有几十字节,但每秒发包数量极高(Mpps,百万包每秒),考验的是路由器和网卡的处理能力,而不是带宽。
这两个方向对设备的要求不一样。带宽型压力主要看你的上行链路容量和清洗中心的接入能力;包速率型压力看的是网络设备的 PPS 处理上限,一个 10Gbps 的端口如果每秒要处理 2000 万个 64 字节小包,很可能在带宽只用了一半的时候就被打满了转发能力。所以我一直强调,看流量图不能只看 bps,必须同时看 pps。我自己排障时习惯把这两个指标放在同一个面板上,bps 平缓但 pps 陡增,基本就是小包洪水的特征。
还有一个容易忽略的方向是分片攻击:把包切成大量很小的分片,让目标端消耗大量内存和 CPU 去重组,重组队列满了就丢包。这类攻击在流量上看起来并不大,但对防火墙和内核的冲击不小。
2.2 反射放大的倍数是怎么算出来的
反射放大的原理很朴素:伪造源 IP,向互联网上大量开放的公共服务发一个很小的查询请求,服务把较大的响应发回给被伪造的那个 IP——也就是你。攻击者不消耗带宽,只消耗一点点请求流量,放大倍数由“响应大小 ÷ 请求大小”决定。
以常见的几类服务为例,一个几十字节的查询请求,响应可能有几百到几千字节,放大倍数从几倍到几十倍不等;某些协议因为历史设计原因,一次查询能返回远超请求体积的数据,放大倍数会更高。这里不给具体协议的具体倍数,因为不同实现、不同配置下差异很大,照着一个数字做容量规划很容易翻车。我给自己的估算方法是:先确认自己业务的正常入向流量基线,再看攻击时的峰值,用峰值除以基线得到的是压力倍数,而不是理论放大倍数。理论值只能用来判断“为什么这么小的攻击流量能打出这么大的效果”。
反过来说,反射放大能被大规模利用,根本原因是一批服务默认允许任意来源查询,且响应比请求大得多。作为服务提供方,如果你运营着对外服务,限制来源、关闭不必要的查询功能、做响应速率限制,本质上是在替整个互联网降低风险。这一点在合规审计里也经常被提到。
2.3 清洗与限速:防护侧真正能干的事
面对流量型攻击,单机层面能做的事其实有限,因为带宽不是靠调参数解决的。可落地的动作大概分三层。
第一层是接入侧清洗:把域名解析指向带清洗能力的入口,由上游在网络边缘做流量识别和丢弃。这层的关键是提前配置好清洗阈值和回源策略,不要等被打的时候才去开通。清洗阈值定得太低会误杀正常突发流量,定得太高又起不到保护作用,我一般按“历史峰值 × 1.5”作为初始阈值,跑一段时间再校准。
第二层是源站隐藏:确保源站 IP 不通过邮件服务器、子域名、历史 DNS 记录、扫描等方式暴露。这一条听起来像常识,但真正泄露的途径往往非常隐蔽,比如某个忘记关掉的调试接口会把服务器内网地址回显出来。
第三层是单机限速兜底,主要用于压制那些已经进到机器里的流量。比如对新建连接速率做限制:
# 对到 443 端口的新建连接做每源 IP 限速,超过 30/s 即丢弃 iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW \ -m hashlimit --hashlimit-name syn_lim \ --hashlimit-above 30/sec --hashlimit-burst 60 \ --hashlimit-mode srcip --hashlimit-htable-expire 60000 \ -j DROP这里的 30/sec 不是拍脑袋定的。我的算法是:正常用户完成一次页面加载,浏览器对同域名并发连接数通常在 6 到 8 个,页面加载完成后有若干静态资源请求,一个真实用户在一秒内新建连接数极少超过 20。取 30 是留了 50% 的余量,同时把恶意脚本每秒几百上千次的建连压下来。burst 设为 60 是允许短时间的突发,避免正常用户刷新页面时被误伤。
注意:hashlimit 的规则如果放在 INPUT 链最前面,会消耗额外的匹配开销,在 PPS 极高的情况下本身也会成为瓶颈。生产环境我更倾向把这类限速放在清洗设备或者前置代理上,本机规则作为最后一道。
3. 连接型消耗:带宽没满,服务却死了
3.1 半连接队列和全连接队列,瓶颈到底在哪
这是我认为最值得每个后端同学搞清楚的一组概念。TCP 建连分两步:客户端发 SYN,服务端回 SYN-ACK 并把这条连接放入半连接队列(SYN 队列),同时启动重传定时器;客户端回 ACK 后,连接从半连接队列移到全连接队列(Accept 队列),等待应用调用 accept 取走。
半连接队列的长度受net.ipv4.tcp_max_syn_backlog和net.core.somaxconn共同影响,实际生效值还和内核的 SYN Cookie 开关有关。全连接队列的长度则由应用 listen 时传入的 backlog 和net.core.somaxconn中较小的那个决定。这两个队列任何一个是满的,新连接就会被丢弃或者被内核直接拒绝。
SYN 洪水的打法就是:大量伪造源地址的 SYN 包打过来,服务端为每一个都分配表项并回 SYN-ACK,但永远等不到最后的 ACK。半连接队列被这些“僵尸”占满,真实用户的 SYN 包进来直接无处安放。表现出来就是:带宽不高、CPU 不高,但新用户连不上,老连接还能用——这个特征非常典型,几乎可以作为判断依据。
实际排查时我用ss看状态分布,syn-recv数量持续上涨且不回落,基本就锁定了:
# 每秒采样一次,观察已完成连接和半连接的堆积情况 while true; do ts=$(date +%H:%M:%S) estab=$(ss -ant state established | wc -l) synrecv=$(ss -ant state syn-recv | wc -l) echo "$ts established=$estab syn_recv=$synrecv" sleep 1 done正常情况下syn_recv应该是个位数到几十,且波动很快。如果它稳定在几百上千并持续增长,同时established不涨,那就是半连接堆积。如果established涨到几千上万但业务处理不过来,那是另一回事——可能是应用 accept 太慢或者全连接队列溢出。
3.2 内核参数怎么调,调到什么程度算够
调参之前先明确一点:调参只能提高你的承受上限,不能让你免于被打。它的价值在于把“几万台肉鸡就能打死你”变成“需要更多资源才能打死你”,同时减少正常高峰期的误伤。
下面这组是我在多数 Linux 服务上会用的基线配置,注释里写了为什么这么设:
# 半连接队列上限。默认 128/1024 在现代服务面前太小 net.ipv4.tcp_max_syn_backlog = 65535 # 全连接队列硬上限,必须 >= 应用 listen backlog,否则应用设多大都没用 net.core.somaxconn = 65535 # SYN-ACK 重传次数。默认 5 次累计等待约 60 秒以上, # 降到 2 次大约 7 秒,半连接占用时间缩短近一个数量级 net.ipv4.tcp_synack_retries = 2 # 开启 SYN Cookie,队列满时用加密方式生成序号,不占表项 net.ipv4.tcp_syncookies = 1 # 文件描述符上限,连接型攻击经常先打满这个 fs.file-max = 2000000 # TIME_WAIT 上限,避免主动关闭方堆积过多 net.ipv4.tcp_max_tw_buckets = 262144tcp_synack_retries = 2这一条我要单独说。默认值 5 意味着内核为一个半连接要重传 SYN-ACK 5 次,间隔按指数退避,累计占用时间超过 60 秒。攻击者根本不在乎这 60 秒,但你的队列在乎——同样 65535 个表项,占用时间从 60 秒缩到 7 秒,理论上的抗压能力提升了将近 9 倍。这个改动几乎没有副作用,唯一的代价是极端网络条件下的正常用户建连成功率略微下降,实际影响可以忽略。
tcp_syncookies是双刃剑。开启后,队列满时内核不再分配表项,而是把连接信息编码进序列号,等 ACK 回来再还原。好处是半连接洪水基本失效,坏处是开启期间会丢失部分 TCP 选项(比如窗口缩放、SACK),在大带宽高延迟链路上性能会打折。我的建议是:作为兜底手段常开,但不要指望它解决所有问题——它对付的是半连接洪水,对已经完成三次握手的连接洪水完全无效。
3.3 连接数与新建速率,两个必须分开看的指标
很多人把“连接数”和“连接速率”混为一谈,实际上这是两个完全不同的压力维度。连接数考验的是内存和文件描述符:每个连接都要占内核缓冲区、socket 结构体,几十万长连接能把内存吃掉一大块。新建速率考验的是 CPU 和队列处理能力:每秒几万次建连意味着每秒几万次状态机转换、几万次定时器操作。
我之前遇到过一次很典型的混合攻击:对方同时做两件事,一是维持大量慢速长连接占住连接数,二是以极高频率发起新建连。结果是我们既达到了nproc限制,又出现了队列溢出,两个方向的告警同时响,一开始还以为是两拨人在打。后来看时间戳发现完全同步,才确定是同一波。
应对连接数型压力,除了调fs.file-max和进程的ulimit -n,更重要的是设置单 IP 的连接数上限。Nginx 层面可以直接做:
# 按客户端 IP 统计并发连接数,20MB 内存约可容纳 32 万个状态 limit_conn_zone $binary_remote_addr zone=conn_perip:20m; server { # 单 IP 最多 20 个并发连接,超出的按 429 返回 limit_conn conn_perip 20; limit_conn_status 429; }20 这个数字怎么来的?普通浏览器对单个域名并发连接数一般不超过 8,移动端弱网环境下重试会多一些,取 20 留出充足余量。如果你的业务有大文件下载或者长轮询,需要单独给这些 location 放宽,否则会把正常用户误伤。20MB 的内存估算依据是每个状态约 64 字节,20MB ÷ 64B 约等于 32 万个 IP,对中小型站点绰绰有余,大站点按最大并发源 IP 数往上加。
4. 应用层消耗:最省流量,也最难缠
4.1 一个请求拖垮一个后端是怎么发生的
应用层攻击最反直觉的地方在于:从网络层看,它和正常流量几乎没区别。完整的三次握手、合法的 HTTP 请求头、正常的 User-Agent,唯一异常的是频率和请求的目标。真正致命的不是请求本身,而是这个请求触发的后端链条。
我举个自己踩过的例子。有个查询接口,用户传几个筛选条件,后端要做三次关联查询加一次外部服务调用,最慢的情况下单请求耗时 800 毫秒。正常 QPS 只有 20 左右,数据库负载 30%。攻击者发现这个接口不需要登录,于是开始高频调用,QPS 涨到 200 的时候,数据库连接池(大小 50)瞬间被打满,所有依赖这个库的接口一起变慢,最终整个站点雪崩。从流量上看,200 QPS 的请求流量可能只有几兆,带宽毫无压力,但业务已经死了。
这就是应用层消耗的核心逻辑:你的瓶颈不在网络,而在某个昂贵操作的并发上限。任何满足“单请求成本高 + 无需鉴权 + 结果不可缓存”三个条件的接口,都是天然的靶子。搜索结果页、导出接口、详情页的复杂聚合、需要调用第三方计费的接口,都属于这一类。
所以做应用层防护,第一步不是加限速,而是找出你系统里最贵的那些接口,逐个确认:能不能加缓存、能不能异步化、能不能要求鉴权、能不能降级。我在项目里会给每个接口标一个“成本分”,计算方式是单请求耗时 × 平均数据库查询次数 × 是否调用外部服务,分数最高的那几个接口必须单独配置限速策略。
4.2 慢速攻击:把连接挂在那里不放手
慢速类攻击是应用层里比较特殊的一支,它不追求高 QPS,而是追求“占着茅坑”。典型手法是:建立连接后,把 HTTP 请求头拆成很多段,每隔十几秒发一个字节,让服务端一直等待请求头接收完成。因为请求一直没完成,工作进程就被这个连接占着,占满之后正常用户连不上。
这类攻击对 Nginx 这类事件驱动服务器的影响相对小(一个连接只占一点内存,不占线程),但对传统的“一个连接一个线程”模型是致命的——几百个慢连接就能把线程池耗尽。所以判断自己是否受影响,先看你的服务模型。
防护手段主要是设置各类超时。Nginx 的几个关键参数:
# 客户端发送请求头的超时,超时返回 408 client_header_timeout 10s; # 客户端发送请求体的超时 client_body_timeout 10s; # 两次读操作之间的超时,防慢速读 send_timeout 10s; # 长连接保持时间,不要太长,避免连接被长期占用 keepalive_timeout 30s; # 单个连接最多处理多少个请求,到数就关 keepalive_requests 200; # 请求头缓冲区大小,太大容易被慢速攻击占内存 client_header_buffer_size 4k;这几个值要结合业务调。client_header_timeout设 10 秒,对正常用户完全够用(正常请求头在毫秒级就发完了),但对慢速攻击就是硬止损。keepalive_requests设 200 是为了防止单个长连接被反复利用来刷请求,同时也限制了连接被长期占用。
注意:不要把这些超时设得太激进。某些老设备或者弱网环境,请求头分片到达的间隔可能超过 10 秒,设成 3 秒会导致真实用户偶发 408。我一般先在观察模式跑一周,统计 408 的比例再决定。
4.3 限速、指纹与缓存的组合拳
应用层防护我总结成三句话:能缓存的不让它到后端,能识别的不让它进业务,剩下的才限速。顺序很重要,因为限速是最后手段,误伤成本最高。
第一层是缓存。把可缓存的接口结果放到 CDN 或本地缓存,攻击流量打到缓存层就被消化了,根本到不了数据库。这一层的收益极高,很多“被打挂”的站点其实只是几个不常变的接口没做缓存。注意缓存要设置合理的 TTL 和回源策略,否则攻击者用随机参数把缓存全部穿透,等于没做。
第二层是识别。基于请求特征做过滤,比如缺少必要请求头、参数格式明显异常、User-Agent 为脚本默认值、访问路径顺序不符合正常浏览逻辑(正常人不会在 1 秒内从首页直接跳到第 50 页)。这一层需要业务知识,不是通用规则能覆盖的。我一般在 Nginx 里做粗筛,复杂逻辑放到应用层或者专门的防护组件。
第三层是限速。Nginx 的limit_req用令牌桶算法,配置的关键是rate和burst两个值:
# 按 IP 限速:每秒 10 个请求,允许 20 个的突发 limit_req_zone $binary_remote_addr zone=perip:20m rate=10r/s; # 再按 IP + URI 限一层,防止单个接口被打穿 limit_req_zone $binary_remote_addr$uri zone=perurl:30m rate=5r/s; server { location /api/search/ { # nodelay 表示突发额度内的请求立即处理,不排队 limit_req zone=perip burst=20 nodelay; limit_req zone=perurl burst=10; limit_req_status 429; } }rate=10r/s意味着令牌按每 100 毫秒一个的速度补充,burst=20是桶的容量,允许短时间攒 20 个令牌。加nodelay的效果是突发额度内的请求立即放行,而不是排队等待——不加的话请求会被延迟处理,对用户体验影响更大,也更容易积累连接。
两层限速叠加的意义在于:只按 IP 限速,攻击者用少量 IP 分布式打,每个 IP 都在阈值内;只按 URI 限速,攻击者换个接口继续打。两层都加上,才能同时约束“单 IP 总量”和“单接口总量”。至于为什么单接口的阈值更低(5r/s),因为同一个用户对同一个接口的正常调用频率本来就低于整体访问频率。
5. 排查实录:从监控指标倒推攻击类型
5.1 一张对照表,五分钟定位方向
出事的时候最缺的是时间。我把几种典型攻击的指标特征整理成了一张表,贴在应急文档里,值班的人照着对就行:
| 观测指标 | 流量型特征 | 连接型特征 | 应用层特征 |
|---|---|---|---|
| 入口带宽 bps | 接近或打满 | 通常不高 | 增长有限 |
| 包速率 pps | 可能异常高(小包) | 中等 | 接近正常 |
| CPU 使用率 | 软中断升高,业务进程不高 | 内核态升高 | 业务进程占满 |
| 新建连接速率 | 可能正常 | 异常陡增 | 中等 |
| syn-recv 数量 | 不一定高 | 持续堆积不回落 | 正常 |
| 已完成连接数 | 下降 | 可能极高或极低 | 高位且持续 |
| 5xx / 429 比例 | 少量 | 建连失败为主 | 大量 5xx 或超时 |
| 数据库连接池 | 正常 | 正常 | 打满 |
| 典型日志特征 | 大量无业务含义的包 | 无应用日志,连接未完成 | 大量同一路径的重复请求 |
这张表的用法是:先看带宽和 CPU,再看连接状态,最后看业务日志。带宽打满基本就是流量型,优先级最高,因为其他手段这时候都没用,只能靠上游清洗。带宽正常但 CPU 高,看一下是内核态还是用户态,内核态高就往连接型走,用户态高就往应用层走。
5.2 三个典型案例的排查路径
案例一:带宽正常但整站 502。某次告警显示入口带宽只有平时的 20%,但所有用户报 502。查 Nginx 错误日志,大量worker_connections are not enough。用ss一看,established状态的连接有 8 万多,且来源 IP 高度集中。结论是连接型消耗打满了 worker 连接数。处置是临时调大worker_connections、加单 IP 连接限制、对异常来源 IP 段做临时封禁,二十分钟恢复。事后复盘发现源站直接暴露、没有前置防护,才让攻击流量直达。
案例二:CPU 100%,接口全超时。另一套服务,带宽和连接数都正常,但服务器 CPU 跑满,数据库连接池告警。抓取访问日志按 URL 聚合,发现 80% 的请求集中在同一个查询接口,参数高度相似,UA 是脚本默认值。结论是应用层针对固定接口的高频请求。处置是先对该接口上严格限速 + 加短 TTL 缓存,再把该接口的重查询改成异步并加结果复用,半小时内恢复,之后永久加了鉴权和配额。
案例三:间歇性卡顿,没有明显指标异常。这个最难受,每隔一段时间服务就慢几十秒,监控上看不出任何峰值。后来把采样粒度从 1 分钟改成 1 秒才看到真相:短时间极高频的慢连接建立,每个只维持几十毫秒就断开,1 分钟均值完全看不出来。处置是把慢连接相关的超时全部收紧,并对建连速率做限制。这件事给我最大的教训是:监控采样粒度决定了你能看到什么级别的攻击,1 分钟的均值图对短脉冲攻击几乎是失明的。
5.3 常见问题速查
| 现象 | 可能原因 | 快速验证 | 处置方向 |
|---|---|---|---|
| 新连接建不上,老连接正常 | 半连接队列被占满 | ss -ant state syn-recv持续增长 | 开 SYN Cookie、降低 synack 重试、上游清洗 |
| 报 “Too many open files” | 文件描述符耗尽 | lsof -p <pid> | wc -l接近上限 | 调 ulimit、加单 IP 连接上限 |
| 带宽不高但网关丢包 | 小包 PPS 打满转发能力 | 对比 bps 和 pps 曲线 | 上游清洗、启用硬件加速 |
| 特定接口慢,其他正常 | 该接口单请求成本高 | 按 URL 聚合日志看分布 | 缓存 + 单独限速 + 异步化 |
| 大量 408 状态码 | 慢速请求头攻击 | 看 408 比例与连接持续时长 | 收紧 header timeout |
| 429 大量出现但用户抱怨 | 限速阈值过严 | 对比历史正常时段请求分布 | 按接口重新校准 burst |
| 数据库连接池满,QPS 不高 | 慢查询占用连接 | 查慢查询日志、看单请求耗时 | 优化查询 + 独立连接池 |
6. 分层防护怎么落地,钱花在哪最值
6.1 四层与七层的分工边界
四层防护(基于 IP 和端口)速度快、开销小,适合拦流量型和连接型;七层防护(基于 HTTP 语义)识别能力强,适合拦应用层,但自身要解析请求,成本和延迟都更高。正确的做法是分层:四层做粗筛和容量吸收,七层做精细识别,应用层做业务侧兜底。
一个常见的误区是只做七层不做四层。结果是大流量打过来的时候,七层设备自己先被流量压垮,成了新的瓶颈。反过来,只做四层不做七层,面对长得像正常请求的应用层攻击就毫无办法。我一般建议至少两层:网络边缘有四层清洗能力,前置代理做七层限速和规则过滤,应用内部再加业务配额。
还有一点值得说:限速的位置越靠前,成本越低。同样一笔攻击流量,在边缘丢掉和在应用层丢掉,消耗的资源差好几个数量级。所以限速规则优先往上放,不要全堆在业务代码里。
6.2 容量规划与压测:别等被打才知道上限
我在每个项目上线前都会做一次“容量摸底”,具体是三个数:正常峰值的 QPS 和带宽、单机能扛的极限 QPS、从峰值到极限还有多少余量。第三个数最重要,它决定了攻击者需要多大的量才能打死你。
摸底方法很朴素:在预发环境用压测工具从正常流量逐步加压,同时盯住 CPU、连接数、数据库连接池、响应时间四个指标,记录下哪个指标先到 100%。那个先到顶的指标,就是你系统的真实瓶颈。比如压测发现数据库连接池在 300 QPS 就打满,那么即使 CPU 还能撑到 800 QPS,你的实际上限也就是 300。
有了这个数,防护策略就有依据了:限速阈值设在极限值的 60% 到 70%,留出应对正常突发的空间;告警阈值设在 50%,给你留出反应时间。清洗阈值按带宽峰值乘以 1.5 来定。这些数不是拍脑袋,都是从压测结果推出来的。
6.3 应急响应清单
真被打的时候,人是慌的,所以我准备了一份按顺序执行的清单,写清楚每一步做什么、谁来做、多久没效果就升级。
第一步,确认影响面:是全部不可用还是部分接口、是全部地域还是局部、是持续还是脉冲。第二步,判断类型:按第 5 节那张表走一遍,控制在五分钟内。第三步,止血:能关的入口先关(测试环境、后台、非核心接口),能降级的先降级(关闭复杂查询、切静态页)。第四步,升级:流量型立刻联系上游清洗,应用层立刻上更严格的限速。第五步,记录:把时间点、指标变化、处置动作都记下来,事后复盘全靠这个。
注意:止血阶段的任何操作都要留记录,包括临时关闭了哪些功能、调整了哪些阈值。我见过复盘时完全想不起来当时改了什么的,导致问题重复发生。
我个人在实际操作中的体会是,DDoS 防护这件事,最贵的不是带宽和设备,而是提前搞清楚自己系统的瓶颈在哪。绝大多数被打挂的服务,不是因为攻击量有多大,而是因为某个自己都不知道的薄弱点被精准命中了——可能是一个没做缓存的查询接口,可能是一个忘了加超时的连接配置,也可能是一个早就该下线却还挂在公网上的测试入口。把这些摸清楚,比买多少防护都管用。