对象存储S3网关的DDoS防护实战:Nginx+Lua令牌桶限流与熔断,性能损失<5%

背景:为什么对象存储需要应用层DDoS防护?

大多数DDoS防护方案聚焦在网络层(SYN Flood、UDP放大),但对象存储(尤其是S3兼容网关)的软肋在于应用层:攻击者可以用短小精悍的GET/PUT请求填满你的连接池、耗尽CPU。一旦网关反向代理被压垮,用户看到的不是拥塞控制,而是长达数秒的TCP重传和503错误。

我经历过一次真实案例:某业务使用MinIO作为对象存储,生产环境突发大量非法请求(伪造签名、随机Key),单个端口并发飙到20w/s,Nginx worker进程CPU全红,正常用户的PUT操作延迟从2ms飙升到7s。事后排查发现,iptables限制源IP根本无效——攻击IP是分布式的。而传统的rate limiting模块(如limit_req)在遇到短连接频繁断开时效果有限,且容易误伤突发正常流量。

解决方案:令牌桶+熔断,配合轻云互联的高性能服务器

我最终在网关层部署了一套Nginx+Lua+令牌桶的方案,部署在轻云互联的4核4G实例上(其CPU支持AES-NI和RDT,对加密签名校验和锁竞争友好)。核心思路是:

  • 为每个Bucket分配独立的令牌桶(Lua共享字典 + 定时补充),控制每秒最大请求数(TPS)。
  • 对短时间内多次触发限流的IP自动加入临时黑名单(熔断器),避免反复计算签名。
  • 使用ngx.sleep(0.01)实现非阻塞等待令牌,而非直接拒绝(减少客户端重试风暴)。

以下是对生产环境进行精简后的核心代码片段。

1. Nginx配置(关键部分)

# nginx.conf 核心段
http {
    # 共享字典:存储每个bucket的计数器与令牌桶状态
    lua_shared_dict buckets_tokens 10m;
    lua_shared_dict ip_blacklist 5m;

    # 全局超时与连接池优化
    proxy_connect_timeout 3s;
    proxy_send_timeout 5s;
    proxy_read_timeout 10s;
    keepalive_requests 100;

    upstream minio_cluster {
        server 192.168.1.10:9000 weight=5;
        server 192.168.1.11:9000 weight=5;
        keepalive 32;
    }

    server {
        listen 443 ssl;
        # ... SSL基础配置 ...

        location / {
            access_by_lua_block {
                local bucket = ngx.var.bucket or "default"
                local token_ok, err = bucket_limiter.take(bucket, 500) -- 每个bucket默认500 TPS
                if not token_ok then
                    local client_ip = ngx.var.remote_addr
                    -- 触发熔断:加入IP黑名单(60秒)
                    local blk = ip_blacklist:get(client_ip)
                    if not blk then
                        ip_blacklist:set(client_ip, "1", 60)
                    end
                    ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE
                    ngx.say('{"error":"rate limited"}')
                    return ngx.exit(ngx.HTTP_REQUEST_ENTITY_TOO_LARGE) -- 返回413避免重试
                end
                -- 黑名单检查(熔断器)
                if ip_blacklist:get(client_ip) then
                    ngx.status = ngx.HTTP_FORBIDDEN
                    ngx.say('{"error":"blocked"}')
                    return ngx.exit(ngx.HTTP_OK)
                end
            }

            # 代理到MinIO集群
            proxy_pass http://minio_cluster;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            # 传递原始请求体(对象存储需要body)
            proxy_request_buffering off;
            proxy_buffering off;
        }
    }
}

2. Lua限流脚本(bucket_limiter.lua)

-- 令牌桶参数:每秒补充rate个令牌,桶容量burst
local function _token_bucket(bucket, rate, burst)
    local tokens, last_ts = bucket_tokens[bucket]["tokens"], bucket_tokens[bucket]["last_timestamp"]
    local now = ngx.time()
    local elapsed = now - last_ts
    tokens = math.min(tokens + elapsed * rate, burst)  -- 限制最大桶容量
    if tokens >= 1 then
        tokens = tokens - 1
        bucket_tokens[bucket]["tokens"] = tokens
        bucket_tokens[bucket]["last_timestamp"] = now
        return true
    else
        bucket_tokens[bucket]["tokens"] = tokens
        bucket_tokens[bucket]["last_timestamp"] = now
        return false
    end
end

local buckets = ngx.shared.buckets_tokens

-- 对外接口:尝试从bucket中取一个令牌
function _M.take(bucket, rate)
    -- 使用共享字典,避免进程间同步问题
    local k_tokens = bucket .. ":tokens"
    local k_ts = bucket .. ":ts"
    local burst = rate * 2  -- 允许2秒内的突发

    local tokens = buckets:get(k_tokens) or burst
    local last_ts = buckets:get(k_ts) or ngx.time()
    local now = ngx.time()
    local elapsed = now - last_ts
    tokens = math.min(tokens + elapsed * rate, burst)  -- 每秒补充rate个

    if tokens >= 1 then
        tokens = tokens - 1
        buckets:set(k_tokens, tokens, 0)
        buckets:set(k_ts, now, 0)
        return true
    else
        buckets:set(k_tokens, tokens, 0)
        buckets:set(k_ts, now, 0)
        return false, "too many requests"
    end
end

return _M

实战效果与性能损耗

部署后在轻云互联的实例上压测:模拟1000个并发(短连接+随机bucket),极限TPS从8000/s降至7500/s(性能损失不到5%),且CPU负载仅增加约3%。原因在于Lua代码只做内存操作和简单数学运算,且令牌桶状态通过共享字典降低锁竞争。更重要的是,被限流的请求直接返回413,不会向MinIO后端转发,缓解了后端压力。

攻击发生时,恶意IP因连续触发限流会快速进入黑名单,后续请求被ip_blacklist直接拒绝,连Nginx worker都不再处理。正常用户因为令牌桶的存在,可享受秒级突发保护,几乎不受影响。

排错建议:遇到性能瓶颈时检查这些点

  • 共享字典大小:如果bucket数量超过1万,适当增大lua_shared_dict buckets_tokens(每10K bucket约需1MB)。
  • CPU亲和性:在轻云互联的服务器上建议绑定worker到不同物理核心(worker_cpu_affinity),避免Lua代码竞争。
  • SSL解密卸载:如果使用HTTPS,可考虑使用ssl_early_data +硬件加速卡,减少握手消耗——轻云互联部分机型支持QAT加速。
  • 熔断阈值调整:根据业务容忍度,可把ip_blacklist的有效期从60秒改为300秒,或增加计数(连续3次触发限流再熔断)。

这套方案已稳定运行半年,后续我们会把令牌桶迁移到eBPF XDP层以进一步降低延迟,但那是另一个话题了。