流量整形不是限速:令牌桶、缓冲队列与「削峰填谷」

· 7 分钟阅读

很多人把流量整形当成”限速”的同义词,配一条 shape average 就以为带宽被卡死了。其实整形做的事正好相反——它不砍流量,它把超速的部分存起来晚点发。理解这个差别,才知道什么时候该用整形、什么时候该用监管。

一、整形到底在做什么

流量整形(Traffic Shaping)是 QoS 里的一种流量调节机制,目的是让输出速率变得平稳可预测。它的手段是:超过约定速率的报文不丢弃,而是放进缓冲区排队,等令牌攒够了再发出去。

这就是所谓的”削峰填谷”——把突发的尖峰摊平,让出口流量长时间维持在一个稳定的水位。

它带来四个直接收益:

  • 避免拥塞:突发被摊平,下游不会瞬间被打爆
  • 带宽合理分配:不同业务按预设策略占用带宽
  • 遵守 SLA:出口速率符合与运营商签的带宽合同,超了要罚钱的那部分
  • 降低抖动:对语音、视频这类业务,平稳比峰值更重要

二、令牌桶:两个参数决定一切

整形通常基于令牌桶(Token Bucket)实现。原理很简单:

  • 系统以固定速率往桶里放令牌,这个速率就是 CIR(承诺信息速率)
  • 桶有容量上限,这个容量就是 CBS(承诺突发尺寸)
  • 桶满了,新生成的令牌就溢出丢弃,不会累积
  • 报文到达时按长度取令牌,够就走,不够就等(整形)或被丢(监管)

所以 CIR 决定的是长期平均速率,CBS 决定的是能容忍多大的瞬时突发。两者一起,才构成完整的”整形”语义。只配 CIR 不配 CBS,突发稍微大一点就被削掉,业务会莫名其妙地卡。

用一个简化模型说明判断逻辑:

import time

class TokenBucket:
    """教学用的简化模型,真实设备实现还涉及队列调度与多级桶"""

    def __init__(self, cir_bps, cbs_bytes):
        self.cir = cir_bps / 8          # 令牌按字节计
        self.cbs = cbs_bytes
        self.tokens = cbs_bytes
        self.last = time.time()

    def consume(self, pkt_bytes):
        now = time.time()
        self.tokens = min(self.cbs, self.tokens + (now - self.last) * self.cir)
        self.last = now

        if pkt_bytes <= self.tokens:
            self.tokens -= pkt_bytes
            return True      # 放行
        return False         # 整形:入队等待;监管:丢弃或降级

注意最后一行注释:同一个桶,两种处置方式,这就是整形和监管的分岔口。

graph TD
    A[报文到达] --> B{令牌是否足够}
    B -- 足够 --> C[立即发送]
    B -- 不足 --> D["流量整形:入队等待"]
    B -- 不足 --> E["流量监管:丢弃或降级"]
    D --> F[令牌攒够后发送]
    C --> G[出口]
    F --> G
    E --> G

三、整形 vs 监管

维度流量整形(Shaping)流量监管(Policing)
超速处置缓存,延后发送丢弃或重标记
方向通常只做出方向入方向、出方向都能做
带宽利用用满链路,不浪费严格封顶,可能空着
延迟会引入排队延迟几乎不引入
抖动平滑可能引入突发丢失
典型场景出口对齐运营商带宽接入侧限制用户速率

选择逻辑很直接:

  • 不能丢、能忍延迟 → 整形
  • 必须卡死速率、对延迟敏感、可以丢 → 监管
  • 既要保障关键业务、又要压住出口突发 → 两者叠加,这也是实际组网里最常见的做法

四、配置怎么写

Cisco:父策略整形,子策略排队

这是最容易被配错的地方。整形和队列调度是两层不同的动作,硬塞进同一个 policy-map 里,语义会打架。标准写法是分层(HQoS):

! 子策略:管"谁先走"
policy-map CHILD-QUEUE
 class VOIP-TRAFFIC
  priority percent 20
 class BUSINESS-TRAFFIC
  bandwidth percent 50
 class class-default
  fair-queue

! 父策略:管"总共能跑多快"
policy-map WAN-SHAPE
 class class-default
  shape average 95000000
  service-policy CHILD-QUEUE

interface GigabitEthernet0/1
 service-policy output WAN-SHAPE

100M 接口整形到 95M,是给二层开销和突发留余量;子策略在这 95M 内部再做优先级分配。priority 保证语音的延迟上限,bandwidth 给业务系统留出保障带宽,剩下的走默认队列。

顺手提一个细节:Cisco 的命令行注释 ! 必须独占一行。网上大量示例把它写在命令后面(shape average 10000000 ! 整形为10Mbps),直接粘到设备上是会报错的。

Linux:HTB 队列

Linux 侧用 tc 的 HTB(Hierarchical Token Bucket),思路一致——父类限总量,子类分份额:

# 根队列,默认流量走 1:30
tc qdisc add dev eth0 root handle 1: htb default 30

# 根类:整条链路封顶 10Mbit
tc class add dev eth0 parent 1: classid 1:1 htb rate 10mbit

# 子类:分别保障 8M 与 2M,允许借用峰值
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 8mbit ceil 10mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 2mbit ceil 5mbit

# 用 u32 过滤器把 80/443 的流量导进 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 80 0xffff flowid 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 443 0xffff flowid 1:10

rate 是保障速率,ceil 是允许借用的上限——这是 HTB 和 Cisco shape 在语义上的区别,别混着理解。

五、两个实际场景

场景一:企业互联网出口

问题:租了 100Mbps 出口,内部 P2P 下载经常把带宽占满,关键业务跟着遭殃。

思路:先整形压住突发(避免超合同被运营商限速),再在整形后的带宽里做队列优先级。上面那段 HQoS 配置就是干这个的。

如果不想搞分层,只做监管也能挡住 P2P,但用户体验会是”明明有带宽却用不上”——因为监管的处置是丢弃,不是排队。

场景二:云上多租户带宽

问题:每个租户买的规格不同,不能互相干扰。

思路:按租户虚拟接口分别建 HTB,把购买的带宽写进 rate:

# 租户 A:50Mbps
tc qdisc add dev veth-tenant-a root handle 1: htb default 10
tc class add dev veth-tenant-a parent 1: classid 1:1 htb rate 50mbit burst 100k

# 租户 B:100Mbps
tc qdisc add dev veth-tenant-b root handle 1: htb default 10
tc class add dev veth-tenant-b parent 1: classid 1:1 htb rate 100mbit burst 200k

burst 在这里就是 HTB 的 CBS,给得太小会误伤正常突发。

六、几个容易忽略的点

  1. 缓冲区不是越大越好。队列太长会变成 bufferbloat——带宽没超,延迟先炸了。CBS 要按 带宽 × 可容忍延迟 估,不能凭感觉填。
  2. 整形只平滑,不增加容量。出口带宽就那么多,整形只是让你用得更稳,不是让链路变快。
  3. 监控优先于调参。先看实际流量的峰值分布,再决定 CIR 和 CBS,比抄一组网上的参数靠谱。
  4. 别在拥塞点上整形。整形该配在瓶颈出口,配在带宽充裕的接口上只会白增延迟。

回到开头那句话:整形不是限速。限速的目标是”少发”,整形的目标是”发得更均匀”。搞清楚这个,shape 和 police 什么时候该出现在同一份配置里,就有答案了。