慢速攻击不像洪水攻击那样靠蛮力冲垮服务器,它更像慢性毒药。攻击者建立连接后,以极慢的速度发送请求或读取响应,刻意维持连接不释放。Python后端服务,尤其是基于WSGI或ASGI的同步/异步框架,其工作进程或线程数量是有限的。当所有工作单元都被这些慢速连接占满时,正常的用户请求根本无法进入处理队列,服务实质上已经瘫痪。解决这个问题的核心,在于精细调整从反向代理、网关接口到应用服务器乃至操作系统层面的各类超时参数,让资源能够及时回收。

反向代理层的超时防线

绝大多数Python后端不会直接暴露在公网,前端通常部署着Nginx或Caddy这类反向代理。这是抵御慢速攻击最理想的第一道关口。攻击者的连接首先到达这里,如果反向代理能快速识别并断开异常连接,后端Python服务就不会受到直接影响。

在Nginx中,有几个核心指令需要重点关注。client_header_timeout用来定义读取客户端请求头的超时时间。如果客户端在这个时间内没有把请求头发送完整,Nginx会返回408状态码并断开连接。通常默认的60秒对于生产环境来说过于宽松,可以调整为10秒甚至5秒。client_body_timeout定义的是读取请求体时,两次连续读操作之间的间隔超时。攻击者可能在发送完请求头后,以每秒一个字节的速度发送请求体,这个参数就是用来切断这种行为的。将其设置为10秒或更短,能有效防御慢速的POST攻击。

http {
    # 读取客户端请求头的超时时间,默认60s,建议设为5-10s
    client_header_timeout 10s;
    # 读取客户端请求体的超时时间,默认60s,建议设为10s
    client_body_timeout 10s;
    # 发送响应给客户端的超时时间,默认60s,建议设为10s
    send_timeout 10s;
    
    server {
        listen 80;
        # ...
    }
}

send_timeout则控制Nginx向客户端发送响应数据时的超时。如果客户端刻意用极低的速度读取响应,导致发送缓冲区持续无法排空,这个参数就能让Nginx主动断开连接。还有一个容易被忽略的参数keepalive_timeout,它决定了长连接保持多久。虽然长连接能提升性能,但过长的保持时间等于变相允许攻击者长时间占用连接槽位。将其从常见的65秒降低到15至30秒,可以加速资源回收。如果服务面向的是纯API,不需要浏览器层面的长连接复用,直接设置为0来关闭keep-alive也是一种极端的防御策略。

网关接口的超时协同

反向代理将请求转发给Python后端时,通常通过HTTP协议或FastCGI、uWSGI等专用协议。这一跳的超时设置同样至关重要,它决定了反向代理愿意等后端多久。如果这一层不设防,即使客户端超时设置得很短,反向代理与后端之间的连接依然会被慢速攻击者通过维持前端连接而间接耗尽。

以Nginx配合uWSGI为例,uwsgi_read_timeout定义Nginx从uWSGI后端读取响应的超时时间。默认值60秒在多数场景下都过长。如果一个请求需要处理超过30秒,通常应该考虑异步任务队列,而不是让Web进程阻塞等待。将其设置为30秒或更短,可以防止后端进程被慢请求长时间占用。uwsgi_send_timeout则是Nginx向uWSGI发送请求体时的超时,同样需要缩短。

location / {
    include uwsgi_params;
    uwsgi_pass 127.0.0.1:8000;
    # 与uWSGI后端通信的超时设置
    uwsgi_read_timeout 30s;
    uwsgi_send_timeout 30s;
    uwsgi_connect_timeout 5s;
}

如果使用的是Gunicorn这类直接通过HTTP代理的WSGI服务器,Nginx的proxy_read_timeout和proxy_send_timeout就对应上述参数。proxy_connect_timeout控制与后端建立连接的超时,这个值可以设得更短,比如3到5秒,因为正常情况下后端就在本地或内网,连接应该瞬间完成。任何连接延迟都意味着后端可能已经过载。

Python应用服务器的内部超时

当请求最终抵达Python应用服务器时,它自身也需要具备超时切断能力。Gunicorn、uWSGI和Waitress等服务器都提供了工作进程级别的超时机制。这是最后一道防线,防止某个工作进程因为代码死循环、外部API调用无响应或慢速客户端而永久挂起。

Gunicorn的timeout参数是最核心的配置。它指定了工作进程处理一个请求的最长允许时间。一旦超过这个时间,主进程会直接杀死该工作进程并启动一个新的。默认值通常是30秒,但根据业务特性,这个值可能需要大幅下调。对于大多数Web API,处理时间应该在毫秒级,超过5秒已经算异常。将timeout设置为10秒,既能容忍偶尔的网络抖动,又能及时清理僵死的进程。需要注意的是,如果使用了异步工作类型如gevent,这个timeout参数依然有效,它监控的是整个协程的处理周期。

# gunicorn.conf.py
import os

# 工作进程数
workers = os.cpu_count() * 2 + 1
# 工作进程超时,超过此时间无响应则重启进程
timeout = 10
# 优雅重启超时
graceful_timeout = 15
# 保持连接存活时间,如果前端Nginx已经处理了keep-alive,这里可以设短
keepalive = 2

uWSGI的配置更为细粒度。harakiri参数与Gunicorn的timeout类似,是工作进程的硬性超时时间。此外,uWSGI还提供了http-timeout、socket-timeout等参数,用于控制与前端代理通信时的读写超时。如果uWSGI直接暴露在公网(不推荐),这些参数就是抵御慢速客户端的直接武器。即便在前置Nginx的情况下,设置一个较短的socket-timeout也能防止在Nginx与uWSGI之间出现慢速传输问题。

[uwsgi]
http-timeout = 10
socket-timeout = 10
harakiri = 15
harakiri-verbose = true

对于异步框架如FastAPI或Sanic,它们底层依赖uvloop和asyncio,超时控制需要从应用层面和服务器层面双管齐下。Uvicorn作为ASGI服务器,提供了timeout_keep_alive参数。这个参数专门用来处理那些保持连接但不发送任何数据的空闲客户端。当一个连接在完成一个请求后,如果在这个时间内没有发送下一个请求,服务器会主动关闭它。默认值5秒对于公网服务来说比较合理,但如果遭受慢速攻击,可以进一步降低到2到3秒。这个参数直接打击了慢速攻击者试图通过保持空闲连接来耗尽资源的企图。

操作系统内核参数的底层加固

应用层和代理层的超时设置虽然关键,但攻击流量依然会到达服务器并建立TCP连接,消耗内核资源。如果能在操作系统层面减少对这类恶意连接的容忍度,就能在更底层化解压力。Linux内核提供了多个TCP相关的参数,可以缩短对异常连接的生命周期判定。

net.ipv4.tcp_syn_retries决定了服务器在收到SYN包但未完成三次握手时,重试发送SYN-ACK的次数。默认值通常是5或6,对应大约180秒的超时。遭受SYN Flood变种攻击时,将这个值减小到1或2,可以大幅缩短半连接在队列中的存活时间。net.ipv4.tcp_keepalive_time、net.ipv4.tcp_keepalive_intvl和net.ipv4.tcp_keepalive_probes这三个参数共同控制TCP的保活机制。当连接空闲后,系统会按照这个配置发送探测包,确认对方是否仍然存活。默认的保活时间长达7200秒(2小时),这对于防御慢速攻击毫无帮助。将tcp_keepalive_time调整为600秒,tcp_keepalive_intvl调整为30秒,tcp_keepalive_probes调整为3次,意味着系统在空闲10分钟后开始探测,如果90秒内无响应就断开连接。虽然这仍比应用层超时慢,但提供了底层的兜底保障。

# /etc/sysctl.conf 或 /etc/sysctl.d/99-custom.conf
# 减少SYN重试次数
net.ipv4.tcp_syn_retries = 2
# 缩短TCP保活探测开始时间
net.ipv4.tcp_keepalive_time = 600
# 缩短探测间隔
net.ipv4.tcp_keepalive_intvl = 30
# 减少探测次数
net.ipv4.tcp_keepalive_probes = 3
# 启用TIME-WAIT状态连接重用,加速回收
net.ipv4.tcp_tw_reuse = 1

调整这些内核参数需要谨慎,过短的保活时间可能会在合法的长连接场景下导致意外断连,比如移动客户端的弱网环境。但面对明确的慢速攻击威胁,这种权衡是必要的。修改后执行sysctl -p使其立即生效。

应用代码中的超时控制与异步处理

超时参数的优化不能只停留在基础设施层。Python代码内部如果存在对外部服务的调用,比如数据库查询、第三方API请求,也必须设置严格的超时时间。攻击者可能不会直接攻击你的服务器,而是攻击你依赖的外部服务,通过拖慢这些调用来间接耗尽你的连接池和工作进程。

在使用requests库时,务必显式传入timeout参数。不设置超时的requests调用是阻塞的,一旦目标服务响应缓慢或刻意拖延,调用线程就会永久挂起。在生产代码中,应该强制要求所有网络请求都设置连接超时和读取超时。

import requests

# 永远不要这样做:requests.get('https://api.example.com')
# 正确的做法是设置timeout,可以分别指定连接和读取超时
try:
    response = requests.get(
        'https://api.example.com/data',
        timeout=(3.0, 10.0)  # 连接超时3秒,读取超时10秒
    )
except requests.exceptions.Timeout:
    # 记录日志并返回降级响应
    pass

对于数据库操作,SQLAlchemy等ORM框架允许在创建引擎时配置连接超时和池回收策略。pool_recycle参数可以防止连接因为数据库端的超时而失效,同时也能避免连接被长时间占用。connect_args中的connect_timeout则控制建立数据库连接时的等待时间。

from sqlalchemy import create_engine

engine = create_engine(
    'postgresql://user:pass@host/db',
    connect_args={'connect_timeout': 5},
    pool_size=20,
    max_overflow=10,
    pool_recycle=3600,  # 连接每小时回收一次
    pool_pre_ping=True  # 每次使用前检查连接有效性
)

异步编程中的超时控制更为灵活。在asyncio中,可以使用asyncio.wait_for为协程执行设置总超时,或者使用asyncio.timeout上下文管理器(Python 3.11+)来包裹可能缓慢的操作。这能防止单个异步任务无限期占用事件循环,导致其他任务饿死。

import asyncio

async def fetch_data():
    try:
        async with asyncio.timeout(5.0):
            # 假设这是一个可能很慢的异步HTTP请求
            result = await slow_async_http_call()
            return result
    except asyncio.TimeoutError:
        # 处理超时,返回默认值或错误
        return None
监控与动态调整的闭环思维

设置好各项超时参数后,工作只完成了一半。这些数值不是一成不变的教条,需要根据实际监控数据持续调整。如果超时设置得过短,会导致正常请求被误杀,用户体验下降,日志中会出现大量499、502或504错误。如果设置得过长,又起不到防御慢速攻击的作用。

需要重点监控的指标包括:Nginx或反向代理的499(客户端主动断开)状态码比例、502/504(后端超时)状态码比例、Gunicorn或uWSGI的工作进程平均存活时间、被harakiri或timeout机制杀死的进程数量、以及操作系统的TCP连接状态分布,特别是SYN_RECV和ESTABLISHED状态的数量。当TIME_WAIT连接数异常飙升时,可能意味着短连接场景下端口回收不及时,需要调整tcp_tw_reuse等参数。当ESTABLISHED连接数持续接近工作进程数上限时,说明并发处理能力已达瓶颈,慢速攻击的威胁会急剧放大。

构建一个从外到内的超时漏斗结构是最佳实践。最外层的Nginx拥有最短的客户端超时,中间层的网关接口超时稍长,内层的应用服务器超时最长。这样能确保连接在离用户最近的地方被切断,最大限度地节省后端资源。例如,Nginx的client_header_timeout设为5秒,proxy_read_timeout设为15秒,Gunicorn的timeout设为20秒。这种梯度设计让每一层都有明确的职责,避免了后端进程还在处理请求,前端代理却已经断开连接的不一致状态。

慢速攻击的防御本质上是一场资源管理战。通过精确控制从客户端到内核每一层的等待时间,Python后端服务能够在恶意连接造成实质损害前将其剥离,保障核心业务逻辑的可用性。这些超时参数就是服务容错能力的刻度尺,刻度越精细,系统的韧性就越强。