CC防护的核心就是控制访问频率,而滑动窗口和漏桶算法是两种最常用、最有效的限流手段。简单来说,滑动窗口通过维护一个时间区间内的请求计数来判断是否超限,适合应对突发流量;漏桶算法则以固定速率处理请求,把突发流量"削平"成匀速流出,适合保护后端服务不被压垮。实际生产环境中,这两种算法经常组合使用,比如用滑动窗口做粗粒度的频率检测,再用漏桶做细粒度的流量整形,形成多层防护体系。

CC攻击(Challenge Collapsar)本质上是利用大量代理IP或肉鸡对目标网站发起高频HTTP请求,耗尽服务器资源导致正常用户无法访问。防护的关键不在于完全阻断,而在于精准识别异常流量并合理限流。访问频率统计是整个防护链路的第一道关卡,统计得准不准、限流策略合不合理,直接决定防护效果。

一、为什么需要访问频率统计

任何CC防护系统的第一步都是"看见"流量。你得知道谁在访问、访问了多少次、在什么时间段内访问的。没有这个数据基础,后面的规则匹配、行为分析、限流拦截全都是空谈。访问频率统计通常以IP地址、用户Session、设备指纹等维度进行聚合,统计单位时间内的请求次数。常见的统计维度包括:每秒请求数(QPS)、每分钟请求数、每小时请求数,以及针对特定URL路径的访问频率。

统计本身不难,难的是在高并发场景下如何高效、准确地完成统计。传统的计数器方案在分布式环境下会遇到数据不一致的问题,所以生产环境一般采用Redis等内存数据库配合滑动窗口或漏桶算法来实现。Redis的原子操作和过期机制天然适合做这类实时统计。

二、滑动窗口算法详解

滑动窗口是一种动态的时间窗口统计方法。它不像固定窗口那样把时间切成死板的块(比如每分钟一个窗口),而是以当前时间为终点,往前推一个固定时长作为统计区间。比如设定窗口大小为60秒,那么在任意时刻t,统计的都是[t-60s, t]这个区间内的请求总数。

滑动窗口的优势在于解决了固定窗口的"临界突刺"问题。固定窗口在窗口切换的瞬间可能出现两个窗口各通过一半请求的情况,实际QPS翻倍但每个窗口都没超限。滑动窗口因为窗口是连续滑动的,不存在这种边界漏洞。

实现上,滑动窗口有两种主流方案:一种是基于Redis的Sorted Set,用时间戳作为score、请求ID作为member,每次请求时移除窗口外的数据并统计当前score范围内的数量;另一种是将窗口细分为多个小格子(比如60秒分成6个10秒的格子),用多个计数器近似模拟滑动效果。后者实现更简单,精度也足够用。

下面是基于Redis实现滑动窗口限流的核心代码示例:

import redis
import time

class SlidingWindowLimiter:
    def __init__(self, redis_client, window_size=60, max_requests=100):
        self.redis = redis_client
        self.window_size = window_size
        self.max_requests = max_requests
        self.key_prefix = "cc_sliding_window:"

    def is_allowed(self, client_ip):
        key = self.key_prefix + client_ip
        now = time.time()
        window_start = now - self.window_size

        # 移除窗口外的记录
        self.redis.zremrangebyscore(key, 0, window_start)
        # 统计当前窗口内的请求数
        current_count = self.redis.zcard(key)

        if current_count < self.max_requests:
            # 记录本次请求
            self.redis.zadd(key, {str(now) + ":" + str(time.time_ns()): now})
            self.redis.expire(key, self.window_size + 10)
            return True
        return False

这段代码的逻辑很清晰:每次请求进来,先清理过期数据,再统计当前数量,没超限就记录并放行。Redis的zremrangebyscore和zcard都是O(logN)操作,性能非常好,单机轻松支撑数万QPS的统计需求。

三、漏桶算法详解

漏桶算法的名字来源于一个形象的比喻:想象一个底部有孔的桶,水(请求)从上面倒进去,不管倒多快,水都只能以固定的速度从底部漏出去。如果桶满了,多余的水就会溢出(请求被拒绝)。这个机制天然适合流量整形,把突发的请求洪峰削平成稳定的输出流。

漏桶算法的核心参数有两个:桶的容量(bucket size)和漏水速率(leak rate)。桶容量决定了能缓存多少突发请求,漏水速率决定了后端服务能承受的最大处理速度。比如桶容量设为100,漏水速率设为每秒10个请求,那么即使瞬间来了200个请求,前100个会被缓存,后面100个直接拒绝,而缓存的请求会以每秒10个的速度被处理。

漏桶算法的优点是输出流量绝对平稳,后端服务不会被突发流量打懵;缺点是面对持续的突发流量时,大量请求会被直接丢弃,用户体验可能受影响。所以它更适合保护数据库、API接口这类对稳定性要求极高的后端组件。

下面是漏桶算法的简化实现:

import time
import threading

class LeakyBucket:
    def __init__(self, capacity=100, leak_rate=10):
        self.capacity = capacity
        self.leak_rate = leak_rate  # 每秒处理的请求数
        self.current_water = 0
        self.last_leak_time = time.time()
        self.lock = threading.Lock()

    def _leak(self):
        now = time.time()
        elapsed = now - self.last_leak_time
        leaked = elapsed * self.leak_rate
        self.current_water = max(0, self.current_water - leaked)
        self.last_leak_time = now

    def try_acquire(self):
        with self.lock:
            self._leak()
            if self.current_water < self.capacity:
                self.current_water += 1
                return True
            return False

    def get_current_load(self):
        with self.lock:
            self._leak()
            return self.current_water

这个实现用了一个后台线程或者定时调用_leak方法来模拟漏水过程。在实际生产中,通常会结合Redis的原子操作来实现分布式漏桶,避免多节点间的状态不一致问题。

四、滑动窗口与漏桶的对比和选型

很多人纠结到底用滑动窗口还是漏桶,其实这两个算法解决的问题不完全一样。滑动窗口侧重于"统计和判断"——在一段时间内请求是否超限;漏桶侧重于"整形和控制"——以什么速率把请求放出去。从功能定位上看,滑动窗口更像一个检测传感器,漏桶更像一个流量调节器。

具体对比来看:滑动窗口能精确统计任意时间窗口内的请求量,适合做访问频率的实时监控和阈值告警;漏桶能保证输出速率恒定,适合做后端服务的过载保护。滑动窗口对突发流量的容忍度更高(只要窗口内总量不超就放行),漏桶对突发流量更严格(超了桶容量就直接拒绝)。

在CC防护的实际架构中,推荐的做法是分层部署:第一层用滑动窗口做IP级别的粗粒度频率检测,快速识别明显的高频攻击源;第二层用漏桶对通过第一层的流量做整形,确保到达后端的请求速率在安全范围内;第三层再结合行为分析、验证码挑战等手段做精准识别。这样既不会误杀正常用户的突发访问,又能有效抵御CC攻击。

五、生产环境中的关键优化点

算法选好了,落地还有很多坑要填。第一个是分布式一致性问题。如果你的服务部署在多台机器上,每个节点都维护自己的计数器,那统计结果一定不准。解决方案是把计数状态集中存储到Redis集群,所有节点共享同一个计数状态。Redis的Lua脚本可以保证"读取-判断-写入"的原子性,避免并发竞争。

第二个是粒度选择。统计粒度太粗(比如按IP统计)容易被代理IP池绕过,太细(比如按每个请求的User-Agent统计)又会误伤正常用户。实际做法是多维度联合统计,IP+User-Agent+访问路径组合起来作为唯一标识,同时设置合理的阈值。一般来说,单个IP每分钟超过60次请求就需要重点关注,超过200次基本可以判定为CC攻击。

第三个是动态阈值调整。固定阈值在流量高峰时段会误杀,在攻击时段又可能不够用。比较成熟的做法是基于历史流量做基线学习,动态调整阈值。比如用过去7天同时段的平均QPS作为基准,当前流量超过基准的3倍就触发限流。这种自适应机制能大幅降低误报率。

第四个是性能开销。限流逻辑本身会增加请求处理的延迟,如果实现不当,限流器本身就成了瓶颈。Redis操作通常在毫秒级,可以接受;但如果每次请求都要做多次Redis调用,累积起来就不可忽视了。优化手段包括:用本地内存缓存做一级快速判断,只有命中阈值附近才查Redis做精确统计;使用Pipeline批量执行Redis命令减少网络往返。

六、CC防护的整体架构建议

一个完整的CC防护体系不是靠单一算法就能搞定的,需要多层协同。最外层是CDN或WAF,在边缘节点就把明显的攻击流量清洗掉;中间层是滑动窗口+漏桶的限流引擎,做精细化的频率控制;内层是应用层的行为分析,通过请求模式、访问深度、会话行为等特征做二次甄别。每一层都有自己的职责,层层过滤,最终到达后端的流量既干净又可控。

另外要特别注意的是,CC防护不能只盯着频率,还要关注请求的"质量"。有些高级CC攻击会模拟正常用户的访问模式,频率不高但专门请求高消耗的接口(比如搜索、下单)。这种情况下单纯的频率限流效果有限,需要结合接口级别的限流、资源消耗监控、人机验证等手段综合应对。

总结一下,滑动窗口和漏桶算法是CC防护中访问频率统计和流量控制的两大基石。滑动窗口负责"看清楚",漏桶负责"管得住"。理解它们的原理、差异和适用场景,根据自己的业务特点合理选型和组合,才能构建出真正有效的CC防护体系。不要迷信单一算法,多层防御、动态调整、持续优化才是正道。