令牌桶算法能允许突发流量且保持平均速率,而单纯sleep只能实现固定间隔限流,无法应对真实场景中的流量波动需求。

为什么直接用 rate 或 sleep 不够用
单纯用 sleep 0.1 控制每秒 10 次调用,会把请求压成固定间隔(比如严格 0.1s/次),但实际中你可能希望:允许短时间突发(比如 5 个请求瞬间进来)、之后自动降速、整体仍不超平均速率。这就是令牌桶要解决的问题——它比固定 sleep 更贴近真实限速需求。
Shell 本身没内置令牌桶,但可以用 date +%s.%N + 简单状态变量模拟核心逻辑,关键在于「按时间匀速产令牌」+「消耗前检查余额」。
用 Bash 实现带桶容量的限速函数 throttle
下面这个 throttle 函数支持设置最大速率(如每秒 2 个)和桶大小(如最多攒 5 个令牌),所有状态只靠几个变量维持,无需外部依赖或临时文件:
throttle() {
local rate=${1:-1} # 每秒允许请求数(浮点也行,如 0.5)
local burst=${2:-1} # 最大令牌数(桶容量)
local now=$(date +%s.%N)
<pre class='brush:php;toolbar:false;'># 初始化或更新令牌
if [[ -z "$_throttle_last" ]]; then
_throttle_tokens=$burst
_throttle_last=$now
else
local elapsed=$(echo "$now - $_throttle_last" | bc -l)
local new_tokens=$(echo "$elapsed * $rate" | bc -l)
_throttle_tokens=$(echo "scale=0; $_throttle_tokens + $new_tokens" | bc -l)
_throttle_tokens=$(echo "if ($_throttle_tokens > $burst) $burst else $_throttle_tokens" | bc -l)
_throttle_last=$now
fi
# 消耗一个令牌
if (( $(echo "$_throttle_tokens >= 1" | bc -l) )); then
_throttle_tokens=$(echo "$_throttle_tokens - 1" | bc -l)
return 0
else
# 等待直到能拿到令牌(计算还需等多久)
local wait=$(echo "(1 - $_throttle_tokens) / $rate" | bc -l)
sleep $(echo "if ($wait < 0) 0 else $wait" | bc -l)
_throttle_tokens=0
_throttle_last=$(date +%s.%N)
return 0
fi}
使用示例:for i in {1..10}; do echo "req $i"; throttle 3 4; done 表示每秒最多 3 次、最多突发 4 次。
-
rate为 0.2 时,平均 5 秒一次;burst设为 3 就允许开头连发 3 次 - 必须用
bc,因为 Shell 原生不支持浮点运算;若系统没装bc,得先apt install bc或yum install bc - 所有状态变量(
_throttle_tokens等)是全局的,不能并发调用;如需多进程安全,得改用文件锁或redis
遇到 command not found: bc 或精度不准怎么办
bc 缺失是最常见报错,但还有更隐蔽的问题:某些嵌入式 BusyBox 环境里 date +%N 不可用,或 bc 版本太老不支持 scale。这时得降级处理:
- 用
date +%s替代%N(秒级精度),适合对突发不敏感的场景,比如限速日志轮转 - 避免
bc:用整数微秒计算,例如把速率换算成「每毫秒多少令牌」,再用$(( ))整数运算,但会损失小数精度 - 简单替代方案:用
tc(traffic control)在网卡层限速,比如tc qdisc add dev lo root tbf rate 1mbit burst 32kbit latency 400ms,但这作用于网络流,不是 shell 进程级限速
和 ratelimit 工具或 systemd-run --scope 的区别
有人会想到用现成工具,比如 ratelimit(C 写的小程序)或 systemd-run --scope --scope-property=CPUQuota=10%。它们确实更稳,但有硬约束:
-
ratelimit只能包装命令(ratelimit -r 10 -w 1 curl ...),无法嵌入脚本逻辑做条件限速 -
systemd-run依赖 systemd,且 CPUQuota 是粗粒度资源限制,不是请求计数器,对 I/O 密集型任务(如频繁curl)几乎无效 - 自己写的
throttle函数可以放在任意位置:循环里、管道后、甚至find -exec中,控制粒度最细
真正难的是时间精度和跨进程状态同步——如果脚本被多个终端同时运行,每个实例都有自己的 _throttle_tokens,那就不是“全局限速”而是“单实例限速”。这时候就得上 Redis 或文件锁,纯 Shell 就到头了。


















