1. 从一个真实场景说起:为什么你的视频会议总是卡你有没有遇到过这种情况:家里宽带明明是500M,测速也跑得满,但只要一开视频会议,画面就开始糊,声音断断续续,与此同时后台的下载任务还在疯狂抢占带宽。你可能会想,不是带宽够吗,为什么还会卡?这个问题我在实际做网络调优的时候被问过无数次。答案往往不在“总带宽够不够”,而在于带宽怎么分配。网络设备默认的行为是“先到先得、尽力而为”,谁的数据包先到队列,谁就先被转发。下载任务会瞬间把队列塞满,视频会议的包只能排在后面,等下载的包发完了才轮到它。对下载来说,晚几十毫秒无所谓;对实时音视频来说,晚几十毫秒就是一次卡顿。QoS(Quality 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 = 1000,B = 5000。如果桶是满的,理论上你可以在一瞬间把这5000个令牌全部用掉。如果这些令牌对应的是5000字节,而你的链路处理能力有限,这一瞬间的爆发可能会把下游设备打爆,或者导致队列瞬间堆积。换句话说,单桶能保证“长期平均不超过 r”,但短时间内的峰值速率取决于桶有多满、包发得多快,可能远超 r。对于需要同时约束“平均速率”和“峰值速率”的场景,单桶就不够了。3.2 双桶模型:C桶和E桶的分工双桶令牌桶(Dual Token Bucket)就是为了解决这个问题。它有两个桶,通常叫C桶(Committed Bucket,承诺桶)和E桶(Excess Bucket,超额桶)。工作逻辑是这样的:C桶的速率是CIR(Committed Information Rate,承诺信息速率),容量是CBS(Committed Burst Size,承诺突发尺寸)。E桶的速率也是 CIR(或者单独设),容量是EBS(Excess 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。因为突发太大意味着队列堆积,延迟会上去。交互式业务(网页、API):T 可以中等,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 = 10Mbps,CBS = 62.5KB,EBS = 125KB。这样绿色流量能突发62.5KB,黄色流量能再额外突发125KB,红色直接丢。三级梯度清晰,既保证了核心业务,又给了普通业务一定弹性。参数含义典型取值影响CIR承诺平均速率业务平均需求长期速率上限CBS承诺突发尺寸CIR × 50ms绿色突发额度EBS超额突发尺寸CBS × 1~2黄色额外额度PIR峰值速率(trTCM)CIR × 1.5~2瞬时峰值上限5. 落地实操:Linux 和常见设备上的配置5.1 Linux tc 里的令牌桶Linux 的流量控制工具tc是令牌桶最经典的落地场景。它里面的tbf(Token Bucket Filter)就是标准的单桶令牌桶。先看一个最基本的配置,把 eth0 的出方向限速到 1Mbit,桶容量 10KB:tc 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,但可以用htb(Hierarchical 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,最多能借到20M;1: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 10M,CBS 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, n=1): with self.lock: self._refill() if self.tokens >= n: self.tokens -= n return True return False这个实现里,rate就是 r,capacity就是 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 是 10Mbps,T = 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 很高时锁竞争会很激烈。优化思路有两个:一是用原子操作替代锁,比如用 CAS(Compare-And-Swap);二是分片,把一个大桶拆成多个小桶,每个线程操作自己的桶,减少竞争。分片的代价是精度略降,但高并发下更实用。我在一个日请求量千万级的服务上用过令牌桶限流,最初用单桶加锁,QPS 到 5万就开始有性能问题。后来改成 16 个分片,每个分片独立加锁,性能直接上去了,限流精度也够用。7. 几个容易被忽略的实战细节7.1 令牌桶和队列的关系令牌桶本身只负责“放行或拦截”,它不负责排队。真正排队的是队列(queue)。令牌桶决定一个包能不能进入发送队列,队列决定包什么时候真正发出去。所以完整的限速链路是:分类 → 令牌桶检查 → 入队 → 调度发送。令牌桶和队列是配合关系,不是替代关系。配令牌桶的时候,一定要同时考虑队列怎么设,否则可能出现“令牌桶放行了,但队列堵死了”的情况。7.2 突发和延迟的权衡这是令牌桶调参的核心矛盾:桶越大,突发吸收能力越强,但延迟风险越高;桶越小,延迟越低,但突发容易被削。没有万能参数,只有适合业务的参数。我的做法是:先按业务类型定 T,实时业务 T 小,普通业务 T 大;然后跑真实流量观察,看突发有没有被削、延迟有没有超标;最后微调。7.3 别忘了测试验证配完令牌桶,一定要测。测试方法很简单:用 iperf 或类似工具打流,观察实际速率和预期是否一致;同时用 ping 或专用工具测延迟,看有没有异常。我习惯用两个指标验证:实际平均速率应该接近 r,实际突发幅度应该接近 B。如果差太多,说明参数或配置有问题。提示:测试时要用真实业务流量特征,不要只用恒定速率打流。恒定流测不出突发问题,一定要模拟突发场景。令牌桶这个东西,原理不复杂,但真正配好、调好,需要对业务流量特征有清晰的认识。参数是死的,流量是活的,多观察、多测试,比背公式有用得多。
1. 从一个真实场景说起:为什么你的视频会议总是卡你有没有遇到过这种情况:家里宽带明明是500M,测速也跑得满,但只要一开视频会议,画面就开始糊,声音断断续续,与此同时后台的下载任务还在疯狂抢占带宽。你可能会想,不是带宽够吗,为什么还会卡?这个问题我在实际做网络调优的时候被问过无数次。答案往往不在“总带宽够不够”,而在于带宽怎么分配。网络设备默认的行为是“先到先得、尽力而为”,谁的数据包先到队列,谁就先被转发。下载任务会瞬间把队列塞满,视频会议的包只能排在后面,等下载的包发完了才轮到它。对下载来说,晚几十毫秒无所谓;对实时音视频来说,晚几十毫秒就是一次卡顿。QoS(Quality 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 = 1000,B = 5000。如果桶是满的,理论上你可以在一瞬间把这5000个令牌全部用掉。如果这些令牌对应的是5000字节,而你的链路处理能力有限,这一瞬间的爆发可能会把下游设备打爆,或者导致队列瞬间堆积。换句话说,单桶能保证“长期平均不超过 r”,但短时间内的峰值速率取决于桶有多满、包发得多快,可能远超 r。对于需要同时约束“平均速率”和“峰值速率”的场景,单桶就不够了。3.2 双桶模型:C桶和E桶的分工双桶令牌桶(Dual Token Bucket)就是为了解决这个问题。它有两个桶,通常叫C桶(Committed Bucket,承诺桶)和E桶(Excess Bucket,超额桶)。工作逻辑是这样的:C桶的速率是CIR(Committed Information Rate,承诺信息速率),容量是CBS(Committed Burst Size,承诺突发尺寸)。E桶的速率也是 CIR(或者单独设),容量是EBS(Excess 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。因为突发太大意味着队列堆积,延迟会上去。交互式业务(网页、API):T 可以中等,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 = 10Mbps,CBS = 62.5KB,EBS = 125KB。这样绿色流量能突发62.5KB,黄色流量能再额外突发125KB,红色直接丢。三级梯度清晰,既保证了核心业务,又给了普通业务一定弹性。参数含义典型取值影响CIR承诺平均速率业务平均需求长期速率上限CBS承诺突发尺寸CIR × 50ms绿色突发额度EBS超额突发尺寸CBS × 1~2黄色额外额度PIR峰值速率(trTCM)CIR × 1.5~2瞬时峰值上限5. 落地实操:Linux 和常见设备上的配置5.1 Linux tc 里的令牌桶Linux 的流量控制工具tc是令牌桶最经典的落地场景。它里面的tbf(Token Bucket Filter)就是标准的单桶令牌桶。先看一个最基本的配置,把 eth0 的出方向限速到 1Mbit,桶容量 10KB:tc 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,但可以用htb(Hierarchical 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,最多能借到20M;1: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 10M,CBS 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, n=1): with self.lock: self._refill() if self.tokens >= n: self.tokens -= n return True return False这个实现里,rate就是 r,capacity就是 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 是 10Mbps,T = 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 很高时锁竞争会很激烈。优化思路有两个:一是用原子操作替代锁,比如用 CAS(Compare-And-Swap);二是分片,把一个大桶拆成多个小桶,每个线程操作自己的桶,减少竞争。分片的代价是精度略降,但高并发下更实用。我在一个日请求量千万级的服务上用过令牌桶限流,最初用单桶加锁,QPS 到 5万就开始有性能问题。后来改成 16 个分片,每个分片独立加锁,性能直接上去了,限流精度也够用。7. 几个容易被忽略的实战细节7.1 令牌桶和队列的关系令牌桶本身只负责“放行或拦截”,它不负责排队。真正排队的是队列(queue)。令牌桶决定一个包能不能进入发送队列,队列决定包什么时候真正发出去。所以完整的限速链路是:分类 → 令牌桶检查 → 入队 → 调度发送。令牌桶和队列是配合关系,不是替代关系。配令牌桶的时候,一定要同时考虑队列怎么设,否则可能出现“令牌桶放行了,但队列堵死了”的情况。7.2 突发和延迟的权衡这是令牌桶调参的核心矛盾:桶越大,突发吸收能力越强,但延迟风险越高;桶越小,延迟越低,但突发容易被削。没有万能参数,只有适合业务的参数。我的做法是:先按业务类型定 T,实时业务 T 小,普通业务 T 大;然后跑真实流量观察,看突发有没有被削、延迟有没有超标;最后微调。7.3 别忘了测试验证配完令牌桶,一定要测。测试方法很简单:用 iperf 或类似工具打流,观察实际速率和预期是否一致;同时用 ping 或专用工具测延迟,看有没有异常。我习惯用两个指标验证:实际平均速率应该接近 r,实际突发幅度应该接近 B。如果差太多,说明参数或配置有问题。提示:测试时要用真实业务流量特征,不要只用恒定速率打流。恒定流测不出突发问题,一定要模拟突发场景。令牌桶这个东西,原理不复杂,但真正配好、调好,需要对业务流量特征有清晰的认识。参数是死的,流量是活的,多观察、多测试,比背公式有用得多。