令牌桶限制获准进入的工作量,同时允许积累的额度在短时间内集中使用。它不会自动把请求均匀隔开,不保证成功完成的吞吐,也不限制仍在执行的操作数量;这些分别属于调度和容量问题。桶的两个参数描述的是预算,不是服务一定能够履行的处理能力承诺。
先补充额度,再扣除工作成本
设桶容量 B 为最多能保存的令牌数,回填速率 r 为每秒新增令牌数。每次请求到来,先将额度更新为 min(B, 上次额度 + r × 经过时间)。只有工作成本不超过当前额度才准入,然后扣除成本。本文采用的严格规则下,被拒绝的工作不消耗令牌。
这种计算可以在请求到达时按需进行,不需要后台定时器不断实际投放令牌。长时间空闲也不能积累无限额度,因为容量会截断回填结果。初始额度是一项策略选择:本例从满桶开始,因此允许立即突发;从空桶开始则会得到不同的初始行为,不能漏写这个条件。
突发额度不是峰值速率
对于这个理想严格模型,在长度为 t 的时间区间内,获准工作的总成本不超过 B + r × t。时间越长,突发项的相对影响越小。这不意味着每个一秒窗口都最多允许 r 个请求,也没有额外给出一个独立的瞬时峰值速率上限。
例如容量为三、每秒回填二时,三个单位成本请求可以在同一时刻全部通过,第四个被拒绝;半秒之后又有一枚令牌可用。如果已获准的工作必须均匀离开入口,还需要实际调度器、有界等待队列或其他节奏控制,不能指望令牌桶自动将同时到达的请求排成匀速流。
准入速率也不等于执行并发量。慢请求尚未结束时,令牌仍会继续回填,允许更多工作进入。需要通过合适的并发限制和超时策略保护工作线程、数据库等稀缺资源。同样,已经准入的请求仍可能在下游失败,所以回填速率不能被写成保证成功完成的吞吐。
预算单位和作用范围要一致
一枚令牌不一定对应一个请求,也可以用加权成本代表字节或预计工作量,但容量与回填速率必须使用相同单位。一个成本超过 B 的请求,无论等待多久都装不进这个严格模型的桶。应明确处理这种情况,而不是给出一个实际上永远不能兑现的重试时间。
还要明确预算按谁区分、由谁持有。四个互相独立的满桶,每个保存三枚令牌,可以同时允许十二个请求;它们不是一个全局三令牌桶。共享限流器必须把回填、比较和扣减协调成一次原子决定,仅共享一个计数器而分开执行这些步骤,并不足以保证配额正确。
本地回填应依据经过时间的时钟,不能让墙上时钟校准凭空增加额度,或使记账时间倒退。Python 的单调时钟适合比较两次读取的差值,但其参考原点不是可以在无关机器之间交换的通用时间戳。分布式时钟和状态归属仍需要自己的明确契约。
坏掉的样子
即使长期速率看起来保守,满桶释放的一次突发仍可能压垮脆弱的下游。无界等待队列会把超额需求转化为内存增长与延迟;直接拒绝和延迟整形是两种不同结果,接口应说明调用者会得到哪一种,不能把所有行为都笼统叫作“平滑”。
应一起观察准入成本、拒绝成本、排队、并发量和下游延迟。重试需要遵守限流策略,而不是立即重新制造被拒绝的负载。按尝试工作计费还是按完成工作计费,也是需要明确的选择;失败后退还令牌会改变模型,并引入额外记账要求,不能悄悄当成同一套公式。
托管产品的限制还可能弱于理想算法。Amazon API Gateway 明确说明限流和配额是尽力而为的目标,可能被超过。因此,不能把这里的数学上界宣传成该服务已经验证的硬保证、费用上限,或者任何未经测试部署的严格承诺。
快速验证
保存为 bucket_check.py,用 Python 3.10 以上版本执行 python3 bucket_check.py。本例已在 Python 3.14.4 上运行。标准库 Fraction 与注入的非递减时间戳让边界计算保持精确,不依赖真实等待。这是单线程准入模型,不是网络限流服务或分布式存储实现。
from fractions import Fraction
class Bucket:
def __init__(self, capacity, rate):
assert capacity > 0 and rate > 0
self.capacity, self.rate = capacity, rate
self.tokens, self.updated = Fraction(capacity), Fraction(0)
def take(self, now, cost=1):
assert now >= self.updated and cost > 0
self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.rate)
self.updated = now
if self.tokens < cost:
return False
self.tokens -= cost
return True
bucket = Bucket(capacity=3, rate=2)
events = ([(Fraction(0), 1)] * 4 + [(Fraction(1, 4), 1), (Fraction(1, 2), 1),
(Fraction(3, 2), 2)] + [(Fraction(100), 1)] * 4)
results = [(now, cost, bucket.take(now, cost)) for now, cost in events]
assert [allowed for _, _, allowed in results] == [
True, True, True, False, False, True, True, True, True, True, False,
]
for now, cost, allowed in results:
print(f"t={str(now):>3} cost={cost} {'allow' if allowed else 'deny'}")
assert bucket.tokens == 0
marks = sorted({now for now, _ in events})
for start in marks:
for end in marks:
if end >= start:
consumed = sum(cost for now, cost, allowed in results if allowed and start <= now <= end)
assert consumed <= bucket.capacity + bucket.rate * (end - start)
print("every event-bounded interval obeys admitted cost <= B + r * elapsed")
assert not bucket.take(Fraction(200), cost=4)
assert bucket.tokens == 3
assert bucket.take(Fraction(200), cost=3)
print("cost above capacity never fits; rejection does not consume tokens")
workers = [Bucket(capacity=3, rate=2) for _ in range(4)]
local_admitted = sum(worker.take(Fraction(0)) for worker in workers for _ in range(4))
shared = Bucket(capacity=3, rate=2)
shared_admitted = sum(shared.take(Fraction(0)) for _ in range(16))
assert local_admitted == 12 and shared_admitted == 3
print("four independent full buckets admit", local_admitted, "at once; one shared bucket admits", shared_admitted)
轨迹先允许三个同时到达的请求,拒绝第四个和四分之一秒时的请求;半秒时允许一个,随后在一秒半时允许成本为二的工作。经过很长空闲之后,也只能再放行三枚令牌对应的突发。断言检查所有以事件时间为端点的区间,确认准入成本符合预算上界。
超出桶容量的请求被拒绝,但不会花掉已经积累的额度。最后,四个独立桶同时允许十二个请求,而单个共享的顺序模型只允许三个。这能说明作用范围如何改变配额,却不是对分布式并发扣减正确性的验证,不能把顺序执行的结果当成消除了竞争条件的证据。
本实验没有测试并发竞争、真实时钟调整、队列调度、云平台限流、网络分区或生产性能。它验证的是严格模型的额度更新、分数时间边界和作用范围差异;落地时仍需针对实际状态存储、客户端行为与运行环境重新验证。
延伸阅读
来源
- RFC 3290,附录 A 的令牌桶预算上界与严格符合性计算,已核验 2026-09-08。
- Amazon API Gateway 限流,请求预算、突发与尽力而为限制,已核验 2026-09-08。
- Python time,单调经过时间时钟及参考原点边界,已核验 2026-09-08。