3、漏桶算法详解:漏桶原理、与令牌桶的区别、适用场景分析。

聊完令牌桶,咱们来看看它的“老对手”——漏桶算法。这两个算法经常被放在一起比较,很多人觉得它们差不多,其实不然。我个人习惯把漏桶看作一个“强制限流器”,它不关心你流量怎么来,只关心你怎么走。

3.1 漏桶的核心原理

漏桶这个名字很形象。你想象一个桶,底部有个小洞。不管你是往里面倒一杯水,还是泼一盆水,水从洞口流出的速度是恒定的。如果水来得太快,桶满了,多余的水就会溢出(被丢弃)。

在计算机网络里,这个“桶”就是一个缓冲区(队列),底部的“洞”就是固定的处理速率。流量进来先入队,然后以恒定的速率被处理。如果队列满了,新来的数据包直接丢掉。

核心思想:输出速率绝对平滑,但允许输入有突发。突发超过队列容量时,直接丢弃。

我给大家画个图,一看就明白:

漏桶算法工作原理 输入流量 突发 缓冲区(队列) 容量 = B 漏口速率 = R 恒定输出 溢出丢弃 关键参数 • 桶容量 B:最大突发容忍度 • 漏出速率 R:恒定处理速率 • 当前水位:队列中积压的数据量 特点: 1. 输出绝对平滑 2. 允许短时突发入队 3. 超量直接丢弃 4. 无法应对持续突发

3.2 漏桶 vs 令牌桶:到底差在哪?

很多新手容易把这两个搞混。我刚开始做流控的时候也犯过这个错。说白了,它们解决问题的角度完全不同。

令牌桶是“能不能放行?”——有令牌就放,没令牌就等或丢。它允许短时间内的突发流量,只要你有足够的令牌积累。

漏桶是“能不能进来?”——进来先排队,出去必须匀速。它强行把流量“捋直”了。

我整理了一个对比表,大家一看就清楚:

对比维度 令牌桶算法 漏桶算法
核心思想 有令牌才能通过 有空间才能进入,出去必须匀速
输出速率 允许突发,最大速率 = 桶大小 + 填充速率 绝对恒定,等于漏出速率
突发处理 允许短时突发(消耗积累令牌) 允许短时突发入队,但输出不变
队列机制 无队列,令牌桶本身是计数器 有队列,数据包排队等待
丢包策略 令牌不足时丢弃或延迟 队列满时丢弃
适用场景 允许突发、对延迟不敏感的业务 要求输出绝对平滑的场景

我的经验:如果你需要“削峰填谷”,让流量变得平滑,用漏桶。如果你需要“控制平均速率但允许短时爆发”,用令牌桶。我曾经在一个视频直播项目中,推流端用了令牌桶保证用户体验,转码端用了漏桶防止后端被冲垮——两个算法配合着用,效果很好。

3.3 适用场景分析

漏桶算法不是万能的,它有自己的“舒适区”。我给大家拆解一下:

3.3.1 最适合的场景

  • 流量整形(Traffic Shaping):比如运营商的带宽限制,你买了10Mbps,不管你怎么突发,出口速率就是10Mbps。漏桶天然适合。
  • 下游保护:你的后端服务处理能力有限(比如数据库每秒只能写1000条),用漏桶把前端请求“捋顺”了再发过去,防止被打死。
  • 音视频编码/转码:编码器需要恒定码率的输入流,漏桶可以保证数据到达的节奏稳定。

3.3.2 不太适合的场景

  • 交互式API限流:用户请求有突发性,漏桶会强制排队,导致延迟增加。令牌桶更合适。
  • 对延迟极度敏感的业务:比如高频交易、实时游戏。漏桶的排队机制会引入不确定的等待时间。

我曾经踩过的坑:有一次给一个API网关做限流,我用了漏桶。结果用户反馈接口响应时间忽高忽低。排查后发现,漏桶的队列把请求都堵住了,突发流量一来,后面的请求排队时间暴增。后来换成令牌桶,问题解决了。所以,漏桶不适合对延迟敏感的场景,这一点一定要记住。

3.4 漏桶算法的实现要点

实现漏桶其实不复杂,但有几个细节要注意。我习惯用“水位”的方式来模拟:

class LeakyBucket:
    def __init__(self, capacity, leak_rate):
        self.capacity = capacity      # 桶容量
        self.leak_rate = leak_rate    # 每秒漏出速率
        self.water = 0                # 当前水位
        self.last_time = time.time()  # 上次漏水时间

    def allow_request(self, packet_size=1):
        now = time.time()
        # 先漏水:根据时间差计算漏掉了多少
        elapsed = now - self.last_time
        leaked = elapsed * self.leak_rate
        self.water = max(0, self.water - leaked)
        self.last_time = now

        # 检查是否有空间
        if self.water + packet_size <= self.capacity:
            self.water += packet_size
            return True  # 放行
        else:
            return False  # 丢弃

嗯,这里要注意:漏水计算一定要基于时间差,不能每次请求都固定漏一个单位。否则在高并发下,漏水速率会不准。

3.5 漏桶的变种:分层漏桶

实际项目中,单层漏桶有时候不够用。比如你要同时限制总带宽和单连接带宽。我做过一个方案:

  • 第一层:全局漏桶,限制总出口带宽
  • 第二层:每个连接一个漏桶,限制单连接带宽
  • 第三层:优先级队列,高优先级流量可以“插队”

这种分层设计在运营商和企业网关里很常见。你想想看,如果不做分层,一个用户开多线程下载就能把整个带宽占满,其他用户就卡死了。

总结一下:漏桶算法的精髓在于“强制平滑”。它牺牲了突发能力,换来了输出的绝对稳定。选不选它,就看你的业务能不能接受排队和延迟。