已发布·持续改进
限流 指南 · 2/6
本章目前仅提供英文版。
This chapter traces small examples to show, step by step, how each of the five algorithms accepts and rejects requests, which makes it easier to judge which one fits a given situation.
A fixed window with a limit of "5 per minute" keeps one counter per window number, which is the timestamp divided by 60. When the window changes, the counter starts again from zero. The trouble is the boundary.
| Time | Window | Counter | Result |
|---|---|---|---|
| 0:59.0 to 0:59.9 (5 requests) | 0 | 1 to 5 | all allowed |
| 1:00.0 to 1:00.9 (5 requests) | 1 | 1 to 5 | all allowed |
| 1:01.0 | 1 | 6 | rejected |
Ten requests got through in about two seconds: twice the limit, if the intent was "never more than 5 in any 60 seconds". Sliding windows fix this.
Keep the timestamps of accepted requests in a deque. On each request, drop timestamps that have fallen out of the window from the front, then count what remains. The limit here is "3 per 10 seconds".
| Time | Dropped | Log after | Result |
|---|---|---|---|
| 1 | none | [1] | allowed |
| 2 | none | [1, 2] | allowed |
| 3 | none | [1, 2, 3] | allowed |
| 5 | none | [1, 2, 3] | rejected (already 3) |
| 11 | 1 | [2, 3, 11] | allowed |
| 12 | 2 | [3, 11, 12] | allowed |
The window is (now - 10, now], so at time 11 the entry at 1 expires. No 10-second span ever contains more than 3 accepted requests, so the limit is exact.
from collections import deque
def sliding_log(times: list[float], limit: int, window: float) -> list[bool]:
log: deque[float] = deque()
result = []
for now in times:
while log and log[0] <= now - window:
log.popleft() # forget entries outside the window
if len(log) < limit:
log.append(now) # only accepted requests are logged
result.append(True)
else:
result.append(False)
return result
print(sliding_log([1, 2, 3, 5, 11, 12], limit=3, window=10))
# [True, True, True, False, True, True]Instead of a log, keep just two counters: the previous window and the current one. Assume the previous window's requests were spread evenly, and count only the fraction of it that still overlaps the last 60 seconds.
estimate = previous count × (1 - time into current window / window length) + current count
With a limit of "10 per 60 seconds", 8 requests in the previous window (0 to 60 s) and 3 so far in the current one (60 to 120 s):
| Time | Previous weight | Estimate | Result | Current count |
|---|---|---|---|---|
| 75 | 1 - 15/60 = 0.75 | 8 × 0.75 + 3 = 9.0 | allowed | 4 |
| 76 | 1 - 16/60 ≈ 0.733 | 5.87 + 4 = 9.87 | allowed | 5 |
| 80 | 1 - 20/60 ≈ 0.667 | 5.33 + 5 = 10.33 | rejected | 5 |
| 105 | 1 - 45/60 = 0.25 | 2.0 + 5 = 7.0 | allowed | 6 |
As time passes the previous window fades out. If its traffic was bunched at one end the estimate is slightly off, but each key needs only two numbers.
Tokens flow into the bucket at rate per second, up to capacity. Each request takes one token and is rejected when fewer than one remains. No timer is needed: when a request arrives, add "time since last check × rate" in one go (lazy refill).
Let us trace rate = 1 per second and capacity = 3.
| Time | Tokens after refill | Result | Tokens left |
|---|---|---|---|
| 0.0 | 3.0 | allowed | 2.0 |
| 0.2 | 2.2 | allowed | 1.2 |
| 0.4 | 1.4 | allowed | 0.4 |
| 0.6 | 0.6 | rejected | 0.6 |
| 1.5 | 1.5 | allowed | 0.5 |
| 3.5 | 2.5 | allowed | 1.5 |
| 3.6 | 1.6 | allowed | 0.6 |
| 3.7 | 0.7 | rejected | 0.7 |
The bucket starts full, so a burst of three passes within 0.4 seconds, after which the client is held to one request per second. After a quiet period the bucket refills and absorbs the next burst.
def token_bucket(times: list[float], rate: float, capacity: float) -> list[bool]:
tokens, last = capacity, times[0] if times else 0.0
result = []
for now in times:
tokens = min(capacity, tokens + (now - last) * rate) # lazy refill
last = now
if tokens >= 1:
tokens -= 1
result.append(True)
else:
result.append(False)
return result
print(token_bucket([0, 0.2, 0.4, 0.6, 1.5, 3.5, 3.6, 3.7], rate=1, capacity=3))
# [True, True, True, False, True, True, True, False]Picture a bucket with a hole in the bottom. Incoming requests are placed in a fixed-size queue, and a worker takes them out at a constant rate. When the queue is full, new requests are dropped. With a queue of 3 and one request processed per second, five requests arriving together at time 0 result in three queued and two rejected; the queued ones are handled at 1, 2 and 3 seconds.
from collections import deque
def leaky_bucket(arrivals: list[int], queue_size: int, leak_per_tick: int) -> None:
queue: deque[int] = deque()
for tick, count in enumerate(arrivals):
for _ in range(leak_per_tick): # drain at a constant rate first
if queue:
print(f"t={tick}: processed request {queue.popleft()}")
for i in range(count): # accept new work only if there is room
if len(queue) < queue_size:
queue.append(tick * 100 + i)
else:
print(f"t={tick}: rejected request {tick * 100 + i}")
leaky_bucket([5, 0, 0, 0], queue_size=3, leak_per_tick=1)Unlike a token bucket, output never exceeds the rate, which makes it a good fit for shielding a downstream system that needs a steady pace. The cost is latency while requests wait in the queue. Note that the "meter" form of the leaky bucket, which measures without queueing, makes exactly the same decisions as a token bucket.
0 条评论
登录 · 登录后即可发表评论。
来发表第一条评论吧。