流量整形不是限速:令牌桶、缓冲队列与「削峰填谷」
很多人把流量整形当成”限速”的同义词,配一条 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,给得太小会误伤正常突发。
六、几个容易忽略的点
- 缓冲区不是越大越好。队列太长会变成 bufferbloat——带宽没超,延迟先炸了。CBS 要按
带宽 × 可容忍延迟估,不能凭感觉填。 - 整形只平滑,不增加容量。出口带宽就那么多,整形只是让你用得更稳,不是让链路变快。
- 监控优先于调参。先看实际流量的峰值分布,再决定 CIR 和 CBS,比抄一组网上的参数靠谱。
- 别在拥塞点上整形。整形该配在瓶颈出口,配在带宽充裕的接口上只会白增延迟。
回到开头那句话:整形不是限速。限速的目标是”少发”,整形的目标是”发得更均匀”。搞清楚这个,shape 和 police 什么时候该出现在同一份配置里,就有答案了。
