多个实例共用一个令牌桶怎么保证原子?速度能到多少?时间戳该用谁的?Redis 卡住的时候是放行、拒绝还是退回本地?这篇用本机的 Redis 8.10.2 和 Lua 脚本量出来。
实现 | 3 轮各放行多少(应为 1000) | 3 轮结束时桶里剩余令牌(应为 0) | 3 轮 WATCH 冲突重试次数 | 尝试次数/秒(中位数) -----------------------+--------------------+--------------------+--------------------+------------ (a) 读-改-写(不原子) | 3200 / 3200 / 3200 | 315 / 336 / 338 | 0 / 0 / 0 | 8585 (b) WATCH + MULTI/EXEC | 1000 / 1000 / 1000 | 0 / 0 / 0 | 3899 / 3891 / 3893 | 3066 (c) Lua 脚本 | 1000 / 1000 / 1000 | 0 / 0 / 0 | 0 / 0 / 0 | 16021
读-改-写会让所有尝试都过;WATCH 靠冲突重试保证,开销大;Lua 脚本在 Redis 里整体原子。
场景 | 吞吐 | 延迟中位数 | p99 --------------------+-----------+---------+-------- 热点:16 个线程共用 1 个 key | 16291/秒 | 0.85 ms | 2.87 ms 分散:每个线程一个 key | 16364/秒 | 0.84 ms | 2.86 ms 对照:进程内的令牌桶(一把锁) | 4157005/秒 | - | -
热点和分散差别不大;跨进程往返比进程内慢两个数量级。
“现在”的来源 | 5 秒内放行总数 | 三个客户端各放行 -----------------------------------------------+----------+---------------- (a) Redis 的 TIME | 509 | [170, 165, 174] (b) 客户端时间戳(偏差 0 / +300 / −300 ms),ts 直接记成传入的时间 | 1500 | [500, 500, 500] (c) 客户端时间戳(同样的偏差),ts 只增不减(math.max) | 508 | [72, 382, 54]
客户端时钟有偏差时,令牌会被反复补满;用 Redis 的 TIME 最稳。
策略 | | 第 0 秒 | 1 | 2 | 3 | 4 | 5 | 6 | 7
------------+---------+-------+-----+-----+-----+-----+-----+-----+----
fail-open | 放行/秒 | 105 | 100 | 199 | 200 | 200 | 105 | 100 | 100
| 平均耗时 ms | 1 | 1 | 37 | 2 | 2 | 1 | 1 | 1
fail-closed | 放行/秒 | 105 | 100 | 4 | 0 | 0 | 96 | 101 | 100
| 平均耗时 ms | 1 | 1 | 41 | 2 | 2 | 1 | 1 | 1
退回本地令牌桶 | 放行/秒 | 105 | 100 | 188 | 96 | 104 | 96 | 100 | 100
| 平均耗时 ms | 1 | 1 | 37 | 2 | 2 | 1 | 1 | 0
fail-open 这一轮里客户端遇到的错误(第 2 秒起;键 = (是否已过第 3 秒, 错误类型与信息前 40 个字符)): {(False, 'TimeoutError:Timeout reading from socket'): 136, (False, 'ConnectionError:Error 54 connecting to localhost:9151. C'): 27, (False, 'ConnectionError:Error 32 while writing to socket. Broken'): 18, (False, 'ConnectionError:Error while reading from localhost:9151 '): 15, (True, 'ConnectionError:Error 54 connecting to localhost:9151. C'): 214, (True, 'ConnectionError:Error 32 while writing to socket. Broken'): 90, (True, 'ConnectionError:Error while reading from localhost:9151 '): 102, (True, 'ConnectionError:Error 54 while writing to socket. Connec'): 2}
Redis 暂停的第 2 到第 5 秒:fail-open 把请求几乎全放行(接近 200 个/秒,限额形同虚设),fail-closed 几乎全部拒绝,退回本地令牌桶则按各线程的本地额度放行(8 个线程的额度加起来约 100;切换那一秒因为本地桶是满的,会多放行一些);只有切换那一秒每次判断平均慢几十毫秒(等 socket 超时),之后很快就报连接错误fail-open 没有限额,fail-closed 全部拒绝,退回本地桶最接近限额。