2026/10/20 23:15:52

令牌桶算法详解:从QoS限速原理到Linux tc与双桶三色标记实战

令牌桶算法详解:从QoS限速原理到Linux tc与双桶三色标记实战 1. 从一个真实场景说起为什么你的视频会议总是卡你有没有遇到过这种情况家里宽带明明是500M测速也跑得满但只要一开视频会议画面就开始糊声音断断续续与此同时后台的下载任务还在疯狂抢占带宽。你可能会想不是带宽够吗为什么还会卡这个问题我在实际做网络调优的时候被问过无数次。答案往往不在“总带宽够不够”而在于带宽怎么分配。网络设备默认的行为是“先到先得、尽力而为”谁的数据包先到队列谁就先被转发。下载任务会瞬间把队列塞满视频会议的包只能排在后面等下载的包发完了才轮到它。对下载来说晚几十毫秒无所谓对实时音视频来说晚几十毫秒就是一次卡顿。QoSQuality of Service服务质量就是用来解决这个问题的。而令牌桶Token Bucket是 QoS 体系里最核心、最常用的一种流量控制算法。它要解决的问题非常朴素如何让流量既不超过某个上限又允许一定程度的突发。这篇文章我会把令牌桶从原理到落地完整讲一遍。包括它到底怎么工作的、单桶和双桶有什么区别、参数怎么算、在 Linux 和常见设备上怎么配、以及我在实际调优中踩过的那些坑。适合做网络运维、后端开发、嵌入式网络协议栈或者单纯想把家里网络调明白的人看。不需要你有多深的数学基础我会尽量用生活化的类比把每个概念讲透。2. 令牌桶到底在解决什么问题2.1 先理解“限速”这件事的两种极端要给流量限速最直觉的做法有两种但都有问题。第一种是直接丢弃超出的部分。比如你规定每秒只能过1MB第1MB之后的所有包全部扔掉。这种做法叫“ policing监管”简单粗暴但问题是它会把突发流量直接打死。现实中很多流量本身就是突发的比如你打开一个网页浏览器会瞬间发起几十个请求这一瞬间的速率远超平均值但持续不到一秒。如果直接丢用户体验就是网页加载慢、图片裂开。第二种是缓存起来慢慢发。超出速率的包先放进缓冲区排队等有空闲了再发。这种做法叫“ shaping整形”它不丢包但会引入延迟。如果缓冲区设得太大延迟会高得离谱实时业务直接废掉设得太小缓冲区一满又开始丢包。令牌桶的巧妙之处在于它同时给出了速率上限和突发额度让你能精确控制“平均速率是多少、允许突发多大”。这就像高速公路的收费站平均每分钟放行多少辆车是固定的速率但如果你攒了一会儿没车来可以一次性放行一小批突发而不是死板地每秒钟只放一辆。2.2 令牌桶的核心机制一个不断加水的桶令牌桶的模型非常直观我用一个桶来类比。想象你面前有一个桶桶的容量是固定的比如能装 B 个令牌。系统会以一个恒定的速率 r 往桶里放令牌比如每秒放 1000 个。桶满了之后多余的令牌就溢出丢弃不会无限累积。当数据包要发送时它必须先从桶里拿走对应数量的令牌。一个包有多大就消耗多少个令牌通常按字节数算。如果桶里的令牌够包就被放行令牌被扣掉如果令牌不够包就有两种命运要么被丢弃policing要么被缓存等待令牌攒够shaping。这里有两个关键参数r令牌添加速率决定了长期的平均速率上限。桶里令牌加得多快你长期平均就只能发多快。B桶容量决定了允许的突发大小。桶越大能一次性攒下的令牌越多允许的突发就越大。我用一个具体数字帮你建立感觉。假设 r 1000 令牌/秒B 5000 令牌。如果网络空闲了5秒桶里攒满了5000个令牌。这时候突然来了一波流量它可以瞬间以很高的速率发出去直到这5000个令牌消耗完然后就被限制回1000/秒的平均速率。这就是“允许突发”的含义。注意桶容量 B 和速率 r 的比值决定了突发的持续时间。B/r 就是“满桶能撑多久的满速突发”。这个比值在调参时非常关键后面会细讲。2.3 令牌桶 vs 漏桶别搞混了很多人会把令牌桶和漏桶Leaky Bucket搞混这里必须澄清一下因为选错模型会导致完全不同的效果。漏桶的模型是桶底有个洞水以恒定速率漏出。不管上面倒进来多少水漏出的速率永远恒定。这对应的是严格的恒定速率输出完全没有突发能力。它适合那种必须绝对平滑的场景但对大多数业务来说太死板了。令牌桶则相反它允许在平均速率约束下突发。两者的区别可以用一句话概括漏桶限制的是“流出速率恒定”令牌桶限制的是“长期平均速率 突发上限”。对比维度令牌桶漏桶输出速率平均固定允许突发严格恒定突发支持支持由桶容量决定不支持适用场景大多数网络业务需要绝对平滑的场合参数速率 r、容量 B漏出速率、桶容量对突发流量的态度友好攒令牌换突发不友好直接削平实际工程里令牌桶用得远比漏桶多因为真实流量几乎都是突发的。漏桶更多出现在理论教学和少数特殊场景里。3. 单桶不够用双桶令牌桶的进阶玩法3.1 单桶的局限峰值速率不可控单桶令牌桶有一个隐藏问题它只约束了平均速率但没有约束瞬时峰值速率。还是用刚才的例子r 1000B 5000。如果桶是满的理论上你可以在一瞬间把这5000个令牌全部用掉。如果这些令牌对应的是5000字节而你的链路处理能力有限这一瞬间的爆发可能会把下游设备打爆或者导致队列瞬间堆积。换句话说单桶能保证“长期平均不超过 r”但短时间内的峰值速率取决于桶有多满、包发得多快可能远超 r。对于需要同时约束“平均速率”和“峰值速率”的场景单桶就不够了。3.2 双桶模型C桶和E桶的分工双桶令牌桶Dual Token Bucket就是为了解决这个问题。它有两个桶通常叫C桶Committed Bucket承诺桶和E桶Excess Bucket超额桶。工作逻辑是这样的C桶的速率是CIRCommitted Information Rate承诺信息速率容量是CBSCommitted Burst Size承诺突发尺寸。E桶的速率也是 CIR或者单独设容量是EBSExcess Burst Size超额突发尺寸。数据包优先消耗 C桶的令牌。如果 C桶够包标记为“绿色”正常转发。如果 C桶不够但 E桶够包标记为“黄色”可以转发但可能被降级处理比如降低优先级、标记为可丢弃。如果两个桶都不够包标记为“红色”直接丢弃或严格限速。这套机制对应的是 RFC 2697 定义的srTCM单速率三色标记。还有更复杂的trTCM双速率三色标记RFC 2698它给 C桶和E桶分别设不同的速率能同时约束峰值速率PIR和承诺速率CIR。3.3 三色标记的实际意义为什么要搞三种颜色因为网络里不是所有流量都同等重要。举个实际例子。你是一家公司的网络管理员出口带宽有限。老板的视频会议、财务系统的交易数据这些是“绿色”流量必须优先保证。员工的网页浏览、邮件这些可以算“黄色”带宽紧张时降级。而某些娱乐流量、大文件下载算“红色”拥塞时优先丢弃。三色标记让设备能区分对待而不是一刀切。这也是为什么运营商和企业网络里双桶三色标记几乎是标配。单桶只能回答“超没超”双桶能回答“超了多少、超的部分有多重要”。实操心得很多设备默认只开单桶如果你需要做精细化的分级限速一定要确认设备是否支持双桶三色标记。我见过不少人配了半天发现效果不对最后发现是设备只支持单桶白折腾。4. 参数怎么算从需求到配置的完整推导4.1 先搞清楚你的需求是什么配令牌桶之前必须先回答三个问题要限制的平均速率是多少这对应 r 或 CIR。允许多大的突发这对应 B 或 CBS。超出突发的部分怎么处理丢弃还是降级这三个问题看起来简单但实际做的时候很多人拍脑袋填数字结果要么限得太死业务跑不动要么限了等于没限。4.2 速率 r 的确定别只看带宽速率 r 的确定最直接的想法是“链路带宽是多少就设多少”。但这样往往不对。假设你有一条100M的链路你想给某个业务限速到20M。那 r 设20M就完了吗不一定。你要考虑这个业务的实际流量特征。如果它是持续稳定的流比如大文件传输20M就是20M。如果它是突发的比如API调用你可能需要把 r 设得比平均需求高一些让它有喘息空间。我的经验是r 应该设成业务“可持续的平均需求”而不是“峰值需求”。峰值需求靠桶容量 B 来吸收。如果你把 r 设成峰值那桶容量就没意义了因为平时根本用不到突发。4.3 桶容量 B 的计算一个可落地的公式桶容量 B 是最容易配错的参数。配小了突发被削平业务体验差配大了突发太猛等于没限速。一个常用的经验公式是B r × T其中 T 是你允许的“突发持续时间”。比如 r 20Mbps你希望允许持续 100ms 的满速突发那 B 20Mbps × 0.1s 2Mbit 250KB。这个 T 怎么定取决于你的业务对延迟和抖动的容忍度实时音视频T 要小一般 10~50ms。因为突发太大意味着队列堆积延迟会上去。交互式业务网页、APIT 可以中等50~200ms。大文件传输、备份T 可以大几百毫秒甚至1秒因为对延迟不敏感。我用一个具体案例帮你算一遍。某公司要给视频会议限速链路100M希望视频会议稳定占用10M允许50ms的突发。r 10MbpsT 50ms 0.05sB 10Mbps × 0.05s 0.5Mbit 62.5KB所以配置就是速率10Mbps桶容量62.5KB。这个容量大概能容纳40多个1500字节的包足够吸收正常的突发又不会引入太大延迟。注意桶容量的单位在不同设备上可能不一样有的是字节有的是比特有的是包数量。配置前一定要看清楚单位我踩过这个坑把字节当比特填结果限速差了8倍。4.4 双桶参数CIR、CBS、EBS 怎么配双桶的参数配置核心是确定三个值CIR、CBS、EBS。CIR承诺速率就是你必须保证的平均速率。CBS承诺突发绿色流量的突发上限。EBS超额突发黄色流量的额外突发额度。一个实用的配法是CBS 按上面的公式算EBS 设成 CBS 的 1~2 倍。这样黄色流量能比绿色多突出一截但总量仍然可控。举个数字CIR 10MbpsCBS 62.5KBEBS 125KB。这样绿色流量能突发62.5KB黄色流量能再额外突发125KB红色直接丢。三级梯度清晰既保证了核心业务又给了普通业务一定弹性。参数含义典型取值影响CIR承诺平均速率业务平均需求长期速率上限CBS承诺突发尺寸CIR × 50ms绿色突发额度EBS超额突发尺寸CBS × 1~2黄色额外额度PIR峰值速率trTCMCIR × 1.5~2瞬时峰值上限5. 落地实操Linux 和常见设备上的配置5.1 Linux tc 里的令牌桶Linux 的流量控制工具tc是令牌桶最经典的落地场景。它里面的tbfToken Bucket Filter就是标准的单桶令牌桶。先看一个最基本的配置把 eth0 的出方向限速到 1Mbit桶容量 10KBtc qdisc add dev eth0 root tbf rate 1mbit burst 10kb latency 70ms这里三个参数对应关系是rate 1mbit就是令牌添加速率 r。burst 10kb就是桶容量 B。latency 70ms这是 tbf 的一个额外参数表示“包在队列里最多等多久”。它其实会反过来影响实际桶容量tbf 会取burst和rate × latency中较大的那个作为实际桶大小。这个latency参数很多人会忽略但它很关键。如果你只设burst不设latency或者设得不合理实际限速效果可能和你想的不一样。我的建议是先按 B r × T 算出 burst然后 latency 设成 T 的 1.5 倍左右给系统一点余量。再看一个双桶的场景。Linux 的tc本身没有直接叫“双桶”的 qdisc但可以用htbHierarchical Token Bucket配合police来实现类似三色标记的效果。htb的ceil参数相当于允许借用的额外带宽rate相当于 CIR。tc qdisc add dev eth0 root handle 1: htb default 20 tc class add dev eth0 parent 1: classid 1:10 htb rate 10mbit ceil 20mbit burst 62kb tc class add dev eth0 parent 1: classid 1:20 htb rate 5mbit ceil 10mbit burst 31kb这段配置的意思是1:10 这个类保证10M最多能借到20M1:20 保证5M最多借到10M。ceil和rate的差值就相当于 EBS 提供的额外突发空间。5.2 交换机上的 QoS 配置思路企业级交换机上的令牌桶配置通常藏在 QoS 策略里。不同厂商的命令不一样但逻辑是相通的。一般流程是定义 class-map把流量分类比如按 ACL、按 DSCP 标记。定义 policy-map在策略里对每个 class 应用 police 或 shape。把 policy-map 应用到接口。以某类设备的配置逻辑为例命令做了抽象重点是思路class-map match-any VIDEO match dscp ef policy-map QOS-POLICY class VIDEO police cir 10000000 bc 62500 be 125000 conform-action transmit exceed-action set-dscp-transmit af31 violate-action drop interface GigabitEthernet0/1 service-policy output QOS-POLICY这里的police cir 10000000 bc 62500 be 125000就是双桶三色标记CIR 10MCBS 62500字节EBS 125000字节。conform是绿色动作exceed是黄色动作这里改成 af31 降级violate是红色动作丢弃。实操心得交换机上配 police 时一定要确认bc和be的单位。有的设备用字节有的用比特有的用毫秒。我见过把bc填成 62500 结果设备按比特算实际桶容量只有 7.8KB突发全被削平了。配置完一定要用流量测试验证。5.3 应用层自己实现令牌桶有时候你需要在应用层做限流比如保护后端 API 不被刷爆。这时候令牌桶也是首选算法因为它能平滑限流又允许突发。用 Python 实现一个简单的令牌桶核心逻辑就几十行import time import threading class TokenBucket: def __init__(self, rate, capacity): self.rate rate # 每秒添加的令牌数 self.capacity capacity # 桶容量 self.tokens capacity # 当前令牌数 self.last_time time.time() self.lock threading.Lock() def _refill(self): now time.time() elapsed now - self.last_time self.tokens min(self.capacity, self.tokens elapsed * self.rate) self.last_time now def consume(self, n1): with self.lock: self._refill() if self.tokens n: self.tokens - n return True return False这个实现里rate就是 rcapacity就是 B。每次请求前调用consume()返回 True 就放行False 就拒绝或排队。这里有个细节要注意令牌的补充是“惰性”的不是后台起个定时器每秒加而是在每次请求时根据流逝的时间算出来该加多少。这样做的好处是没有额外的定时器开销性能更好。但代价是精度依赖time.time()的精度高并发下可能有微小误差。如果对精度要求极高可以用time.monotonic()替代避免系统时间调整带来的问题。6. 常见问题与排查技巧实录6.1 限速不生效先查这几个地方令牌桶配了但没效果是最常见的问题。我整理了一个排查顺序按这个走基本能定位。排查项可能问题检查方法方向配了入方向但实际要限出方向确认 qdisc/policy 挂在 ingress 还是 egress单位字节/比特/毫秒搞混查设备文档用测试流量验证分类流量没匹配到 class-map看匹配计数是否增长层级qdisc 被上层覆盖用 tc -s qdisc show 看统计硬件网卡不支持硬件限速换软件限速或换网卡我遇到最多的是方向搞反。很多人想限制下载速度结果把 qdisc 配在了出方向。下载是入方向流量出方向的 qdisc 管不到。Linux 里入方向限速要用ingressqdisc 配合police或者用ifb做重定向比出方向麻烦不少。6.2 突发被削平桶容量太小现象是明明设了允许突发但实际流量一超就被限完全没有突发的空间。原因通常是桶容量 B 设得太小。回到公式 B r × T如果 T 设得太小比如只给了 1ms那 B 就非常小稍微一点突发就超了。排查方法算一下你的业务典型突发的持续时间和幅度。比如一个网页加载瞬间可能有 2MB 的流量在 200ms 内爆发那 B 至少要能容纳 2MB否则就会被削。这时候如果 r 是 10MbpsT 2MB / 10Mbps 1.6s意味着桶容量要设到 2MB 才能不削这个突发。注意桶容量设大不等于限速失效。因为 r 还在约束长期平均速率桶再大也只能撑一时。真正要小心的是桶太大导致延迟堆积这才是问题。6.3 延迟飙升桶太大或队列太长反过来如果桶容量设得太大或者 shaping 的队列太长会出现延迟飙升。现象是带宽利用率看着不高但 ping 值很高实时业务卡顿。这是因为 shaping 会把超出的包缓存起来缓存越多包在队列里等的时间越长。如果队列长度是 1000 个包每个包 1500 字节在 1Mbps 的链路上排空这个队列需要 1000 × 1500 × 8 / 1000000 12 秒。12 秒的延迟什么实时业务都废了。解决办法是限制队列长度。Linux 的 tbf 有latency参数就是干这个的。交换机上一般有 queue-limit 或类似参数。原则是队列能缓存的包排空时间不应超过业务能容忍的延迟。实时业务控制在 50ms 以内普通业务 200ms 以内。6.4 双桶配置后黄色流量没降级配了双桶三色标记但发现超出 CIR 的流量还是正常转发没有降级。这通常是动作配置问题。三色标记只是“标记”标记完还要有对应的“动作”。如果 exceed-action 配的是 transmit直接转发那黄色流量和绿色流量待遇一样等于没降级。要真正降级exceed-action 应该配成 set-dscp 改标记然后下游队列根据新标记做不同处理。另一个可能是下游没有配合。你在这台设备上标了黄色但下一台设备不认识这个标记照样按普通流量转发。三色标记要端到端生效需要整条路径上的设备都认可这套标记体系。6.5 高并发下令牌桶精度问题应用层实现令牌桶时高并发下可能出现“超发”。原因是多个线程同时读tokens都判断够然后都扣导致扣多了。上面的 Python 实现里我加了threading.Lock()就是为了解决这个问题。但加锁会带来性能开销QPS 很高时锁竞争会很激烈。优化思路有两个一是用原子操作替代锁比如用 CASCompare-And-Swap二是分片把一个大桶拆成多个小桶每个线程操作自己的桶减少竞争。分片的代价是精度略降但高并发下更实用。我在一个日请求量千万级的服务上用过令牌桶限流最初用单桶加锁QPS 到 5万就开始有性能问题。后来改成 16 个分片每个分片独立加锁性能直接上去了限流精度也够用。7. 几个容易被忽略的实战细节7.1 令牌桶和队列的关系令牌桶本身只负责“放行或拦截”它不负责排队。真正排队的是队列queue。令牌桶决定一个包能不能进入发送队列队列决定包什么时候真正发出去。所以完整的限速链路是分类 → 令牌桶检查 → 入队 → 调度发送。令牌桶和队列是配合关系不是替代关系。配令牌桶的时候一定要同时考虑队列怎么设否则可能出现“令牌桶放行了但队列堵死了”的情况。7.2 突发和延迟的权衡这是令牌桶调参的核心矛盾桶越大突发吸收能力越强但延迟风险越高桶越小延迟越低但突发容易被削。没有万能参数只有适合业务的参数。我的做法是先按业务类型定 T实时业务 T 小普通业务 T 大然后跑真实流量观察看突发有没有被削、延迟有没有超标最后微调。7.3 别忘了测试验证配完令牌桶一定要测。测试方法很简单用 iperf 或类似工具打流观察实际速率和预期是否一致同时用 ping 或专用工具测延迟看有没有异常。我习惯用两个指标验证实际平均速率应该接近 r实际突发幅度应该接近 B。如果差太多说明参数或配置有问题。提示测试时要用真实业务流量特征不要只用恒定速率打流。恒定流测不出突发问题一定要模拟突发场景。令牌桶这个东西原理不复杂但真正配好、调好需要对业务流量特征有清晰的认识。参数是死的流量是活的多观察、多测试比背公式有用得多。