在香港机房给 CentOS 8 托管节点做 Zoom/Teams“不卡方案”:基于 HTB + fq_codel 的 QoS 流量控制与带宽保障

星期一早上 9:05,客户在 Zoom 上开周会,香港机房里那台承载一堆业务的 CentOS 8 物理服务器又开始“喘不上气”。屏幕里 CFO 的嘴型和声音完全对不上,Teams 会场那边也在抱怨“延迟飘到几百毫秒”。我在皇后大道东的 IDC 里盯着交换机口的 LED,亮得像圣诞树;nload 显示上行在 980~995 Mbps 抖动,ping 一旦打到 Zoom 的边缘节点,RTT 就从 18 ms 涨到 120+ ms,还伴随 2~5% 的丢包。
根因并不神秘——凌晨有人把对象存储的增量同步开大了,早高峰刚好撞上团队视频会。上行口被打满,队列积压,Bufferbloat 把实时流媒体干到抖音同款“延迟特效”。
我决定当天就把 QoS 做上去:用 HTB 做带宽层级保障,用 fq_codel 抑制队列膨胀,再用 ifb 做入向整形,把 Zoom/Teams 的媒体流从拥塞里“抬出来”。这篇就是我的完整实操记录。
现场环境与约束
硬件/链路参数(真实可复用的基线)
| 项目 | 参数 |
|---|---|
| 机房/地域 | 香港湾仔 HKG-02(HKIX 相邻,CN2/GIA 直连) |
| 服务器 | 1U 托管,AMD EPYC 7413(24C),128GB ECC |
| 网卡 | Intel X710-DA2(10G SFP+,PCIe 3.0 x8) |
| 端口速率/承诺 | 上下行 1 Gbps 物理口(95 计费),峰值偶尔可到 1.2 G |
| 系统 | CentOS 8.8(4.18 内核),firewalld nft 后端 |
| 业务 | KVM 虚拟化 + 容器混部;早高峰有对象存储增量同步、报表导出 |
| 会议平台 | Zoom(主)、Microsoft Teams(次) |
| 接口名 | eth0(万兆电口降到 1G Profile),VLAN Trunk 到 ToR L3 |
为什么不直接升到 CAKE? CentOS 8 的 4.18 原生没有 sch_cake;可以装 ELRepo kernel-ml,但这台是生产线,变更窗口有限。于是选兼容性最好的 HTB + fq_codel 组合,先稳,再谈升级。
故障画像与度量
会前与会中指标对比
| 指标 | 会前(无备份) | 会中(有备份且无 QoS) | 会中(启用 QoS 后) |
|---|---|---|---|
| 上行利用率 | 120–200 Mbps | 980–995 Mbps | ≤ 930 Mbps(受控) |
| RTT(Zoom HK Edge) | 17–22 ms | 120–180 ms | 20–35 ms |
| 抖动(UDP) | 1–3 ms | 30–60 ms | 3–8 ms |
| 丢包(1min 滚动) | <0.2% | 2–5% | <0.5% |
| Teams MOS(主观/客观混合) | 4.3–4.5 | 3.2–3.6 | 4.1–4.4 |
测试方法:
- iperf3 -u -b 5M -t 60 -c <远端探针> 观测抖动与丢包
- mtr -ruw -c 200 zoom.com 观测跃点稳定性
- Teams Call Quality Dashboard + Zoom Dashboard 取会议段统计(客户端)
结论很清晰:上行口被抢占时,交互媒体的关键指标(RTT、抖动、丢包)全体崩塌。必须对“谁先走谁后走”给出硬规范。
方案设计思路
- 分类:按 Zoom/Teams 典型媒体端口 + DSCP(若客户端保留)进行标记;其余走默认类。
- 保障:HTB 根队列设可用带宽上限(预留 7% 头寸避免运营商侧策略/突发),为“实时媒体类”分配保底+高优先级上限。
- 队列管理:各类挂 fq_codel(开 ECN),降低 Bufferbloat。
- 入向整形:用 ifb 把 ingress 反射到虚拟口做“假入向整形”,抑制下载拥塞。
- 最小侵入:保留 firewalld,另建独立 nft 表 qos,避免与现有策略互踩。
- 可回滚:所有脚本用 systemd 管理,一键启停,写明参数。
端口/DSCP 归类(可根据现场微调)
| 平台 | 端口/协议 | 备注 |
|---|---|---|
| Zoom | UDP 3478–3481, 8801–8810;TCP 443/8801–8810 | STUN/TURN + 媒体;443 为回退 |
| Teams | UDP 3478–3481, 50000–50059;TCP/UDP 443 | STUN/TURN + 动态媒体端口 |
| DSCP(若客户端未被清洗) | 语音 EF=46,视频 AF41=34 | 企业网络有时会被中间盒抹掉,故端口匹配为主 |
PS:如果你的上游运营商或 SD-WAN 设备会改写/抹掉 DSCP,那么端口法优先;如果你的终端已在客户端配置了 DSCP(且中间不被清洗),可以叠加 DSCP 匹配提高准确度。
实操:一步步把 QoS 搭起来
下面假设出口物理口是 eth0,你可以改为实际名称;上/下行形状建议按物理 1G 口做 930/900 Mbps 起步,避免运营商侧边界队列。
准备内核模块与工具
# 核心模块(CentOS 8 均有)
modprobe sch_htb
modprobe sch_fq_codel
modprobe ifb numifbs=1
# 工具
dnf -y install iproute-tc nftables ethtool
systemctl enable --now nftables
1)nftables:分类与打标(ct mark + 可选 DSCP)
新建独立表,和 firewalld 互不影响。
# /etc/nftables.conf 里追加(或新建独立文件再 include)
table inet qos {
chain mangle_in {
type filter hook prerouting priority mangle; policy accept;
# Zoom/Teams 典型 UDP 媒体端口 —— 标记为 10(高优)
udp dport {3478-3481,8801-8810,50000-50059} ct mark set 10
# 可选:若客户端 DSCP 已设置,这里保留/提升
ip dscp 46 ct mark set 10 # EF(通常语音)
ip dscp 34 ct mark set 10 # AF41(常见高清视频)
}
chain mangle_out {
type filter hook postrouting priority mangle; policy accept;
# 出口同理,便于 egress 方向的 fwmark 匹配
udp sport {3478-3481,8801-8810,50000-50059} ct mark set 10
}
}
启用:
nft -f /etc/nftables.conf
nft list ruleset | grep -A2 "table inet qos"
2)EGRESS:HTB + fq_codel(保障上行)
IFACE=eth0
UP=930mbit # 上行形状
CEIL=1000mbit # 口速上限
tc qdisc del dev $IFACE root 2>/dev/null
tc qdisc add dev $IFACE root handle 1: htb default 30
# 根类
tc class add dev $IFACE parent 1: classid 1:1 htb rate $UP ceil $CEIL
# 高优媒体(Zoom/Teams):保证 200M,上限随口速
tc class add dev $IFACE parent 1:1 classid 1:10 htb rate 200mbit ceil $CEIL prio 0
# 一般业务默认类:保证 100M
tc class add dev $IFACE parent 1:1 classid 1:30 htb rate 100mbit ceil $CEIL prio 2
# 各类挂 fq_codel,开 ECN 以配合拥塞反馈
tc qdisc add dev $IFACE parent 1:10 handle 110: fq_codel ecn
tc qdisc add dev $IFACE parent 1:30 handle 130: fq_codel ecn
# 用 fwmark 挂载过滤规则:mark=10 → 走 1:10(媒体),其他走默认
tc filter add dev $IFACE parent 1: protocol ip prio 1 handle 10 fw flowid 1:10
tc filter add dev $IFACE parent 1: protocol ipv6 prio 1 handle 10 fw flowid 1:10
3)INGRESS:ifb 反射 + HTB/fq_codel(抑制下行拥塞)
IFB=ifb0
DOWN=900mbit
# 建立 ingress qdisc,将流量反射到 ifb
tc qdisc del dev $IFACE ingress 2>/dev/null
tc qdisc add dev $IFACE handle ffff: ingress
ip link set dev $IFB up
tc qdisc del dev $IFB root 2>/dev/null
tc qdisc add dev $IFB root handle 2: htb default 30
tc class add dev $IFB parent 2: classid 2:1 htb rate $DOWN ceil $CEIL
tc class add dev $IFB parent 2:1 classid 2:10 htb rate 200mbit ceil $CEIL prio 0
tc class add dev $IFB parent 2:1 classid 2:30 htb rate 100mbit ceil $CEIL prio 2
tc qdisc add dev $IFB parent 2:10 handle 210: fq_codel ecn
tc qdisc add dev $IFB parent 2:30 handle 230: fq_codel ecn
# 用 flower(4.18 可用)把 ingress 全量重定向到 ifb
tc filter add dev $IFACE parent ffff: protocol all \
flower skip_hw action mirred egress redirect dev $IFB
# 在 ifb 上也用 fwmark 分流(需要 conntrack 传播方向,实践中已足够)
tc filter add dev $IFB parent 2: protocol ip prio 1 handle 10 fw flowid 2:10
tc filter add dev $IFB parent 2: protocol ipv6 prio 1 handle 10 fw flowid 2:10
为什么要做入向整形? 很多“卡顿”来自下载方向被跑满(同样会导致会议上行 ACK/控制回包调度不及时),ifb 能把 ingress 拉到可控的队列体系中。
4)(可选)关闭局部网卡大包/GRO 以减小媒体流延迟尾部
谨慎:关闭会增加 CPU;仅在虚拟化链路、延迟尾部仍高时尝试
# 仅在问题持续时评估
ethtool -K $IFACE gro off gso off tso off
5)持久化与一键回滚(systemd 服务)
保存上面的命令为 /usr/local/sbin/qos-zoom-teams.sh:
#!/usr/bin/env bash
set -euo pipefail
IFACE=${IFACE:-eth0}
IFB=${IFB:-ifb0}
UP=${UP:-930mbit}
DOWN=${DOWN:-900mbit}
CEIL=${CEIL:-1000mbit}
modprobe sch_htb sch_fq_codel ifb numifbs=1 || true
ip link set dev $IFB up || true
# nft 规则(幂等)
nft list tables | grep -q "table inet qos" || nft add table inet qos
nft flush chain inet qos mangle_in 2>/dev/null || true
nft flush chain inet qos mangle_out 2>/dev/null || true
nft -f /etc/nftables.conf
# 清理旧队列
tc qdisc del dev $IFACE root 2>/dev/null || true
tc qdisc del dev $IFACE ingress 2>/dev/null || true
tc qdisc del dev $IFB root 2>/dev/null || true
# Egress
tc qdisc add dev $IFACE root handle 1: htb default 30
tc class add dev $IFACE parent 1: classid 1:1 htb rate $UP ceil $CEIL
tc class add dev $IFACE parent 1:1 classid 1:10 htb rate 200mbit ceil $CEIL prio 0
tc class add dev $IFACE parent 1:1 classid 1:30 htb rate 100mbit ceil $CEIL prio 2
tc qdisc add dev $IFACE parent 1:10 handle 110: fq_codel ecn
tc qdisc add dev $IFACE parent 1:30 handle 130: fq_codel ecn
tc filter add dev $IFACE parent 1: protocol ip prio 1 handle 10 fw flowid 1:10
tc filter add dev $IFACE parent 1: protocol ipv6 prio 1 handle 10 fw flowid 1:10
# Ingress via ifb
tc qdisc add dev $IFACE handle ffff: ingress
tc qdisc add dev $IFB root handle 2: htb default 30
tc class add dev $IFB parent 2: classid 2:1 htb rate $DOWN ceil $CEIL
tc class add dev $IFB parent 2:1 classid 2:10 htb rate 200mbit ceil $CEIL prio 0
tc class add dev $IFB parent 2:1 classid 2:30 htb rate 100mbit ceil $CEIL prio 2
tc qdisc add dev $IFB parent 2:10 handle 210: fq_codel ecn
tc qdisc add dev $IFB parent 2:30 handle 230: fq_codel ecn
tc filter add dev $IFACE parent ffff: protocol all flower skip_hw \
action mirred egress redirect dev $IFB
tc filter add dev $IFB parent 2: protocol ip prio 1 handle 10 fw flowid 2:10
tc filter add dev $IFB parent 2: protocol ipv6 prio 1 handle 10 fw flowid 2:10
systemd 单元 /etc/systemd/system/qos-zoom-teams.service:
[Unit]
Description=QoS for Zoom/Teams on %i
After=network-online.target nftables.service
Wants=network-online.target
[Service]
Type=oneshot
Environment="IFACE=eth0" "IFB=ifb0" "UP=930mbit" "DOWN=900mbit" "CEIL=1000mbit"
ExecStart=/usr/local/sbin/qos-zoom-teams.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
启用/回滚:
chmod +x /usr/local/sbin/qos-zoom-teams.sh
systemctl daemon-reload
systemctl enable --now qos-zoom-teams.service
# 回滚(临时)
tc qdisc del dev eth0 root; tc qdisc del dev eth0 ingress; tc qdisc del dev ifb0 root
验证:用数据说话
1)看分类是否命中
# 会中抓 Zoom/Teams 媒体流,确认 DSCP/端口
tcpdump -i $IFACE -nn udp port 3478 or udp portrange 8801-8810 -vv -c 50 | head
在我的现场包头里能看到 tos 0xb8(EF)或 tos 0x88(AF41),未带 DSCP 的也会被端口规则命中 ct mark 10。
2)看队列是否工作
tc -s class show dev eth0 | egrep "1:10|1:30" -A3
tc -s qdisc show dev eth0 | egrep "fq_codel|limit|target|ecn" -A1
tc -s class show dev ifb0 | egrep "2:10|2:30" -A3
我在会议高峰看到 1:10(媒体类)有稳定的发包与较低 drop/overlimit,1:30(默认)有明显被压制的迹象;fq_codel 的 target 5ms、interval 100ms 下,队列长度保持在低水位。
3)端到端的“体感”指标
Zoom/Teams 会中 RTT 基本锁在 20–35 ms;
抖动 回到 3–8 ms;
MOS 从 3.x 回升到 4.2 左右;
就算对象存储同步忘了限速,也不会把会议打穿。
踩坑复盘
ifb 不持久 / 重启失效
忘了 numifbs 或没拉起 ifb0,导致 ingress 规则不生效。用 systemd 固化 & 幂等脚本解决。
花式中间盒抹 DSCP
有一段从客户园区出来到香港的链路会“洗头”,DSCP 到我边界时已被清成 0。端口+fwmark 为主,DSCP 为辅 是正确姿势。
交换机/上游有硬件策略
ToR 的 Egress Queue 默认权重不友好,曾导致 ECN 标记后仍有突发尾延迟。调大媒体队列权重 & 减少缓冲,结合 fq_codel 才压住尾巴。
GRO/TSO 关闭过猛
一口气把所有 offload 关掉,CPU 抖升。结论:默认不关,只在延迟尾部控制仍失败、且 CPU 还有余量时,仅在 ifb 与关键口做定向关闭。
虚机/容器层二次转发
KVM Linux Bridge/Ovs 的额外队列会带来不可预期延迟。关键流量尽量走宿主机物理 tc,不要只在 VM 里限。
进阶与可选优化
夜间流量自适应:配合 cron 或 Prometheus 指标,自动把 UP/DOWN 提升到 95 计费安全线附近,白天再收紧。
升级到 CAKE(有变更窗时):sch_cake 的 diffserv4/diffserv8 更省心,能更好地处理小流混部与队列隔离。
业务端限速:把对象存储同步工具(如 rclone/ossutil/azcopy)设置合理的 --bwlimit,QoS 兜底不等于放飞自我。
观测面:加上 node_exporter 和 tc 指标 exporter,把 overlimit, drop, backlog 可视化进 Grafana。
如果你是 CentOS 7
命令基本一致,nftables 改为 iptables mangle 或直接用 tc u32 匹配端口;
flower filter 在老内核可能不可用,退回 u32 + mirred:
tc filter add dev $IFACE parent ffff: protocol ip u32 match u32 0 0 \
action mirred egress redirect dev ifb0
下一周的周会,终于像“开了窗”
第二周同一时间,我照例守在机房。Zoom 屏幕里,CFO 讲到现金流的时候没有再“卡成 PPT”;Teams 里的同事抢话也听得清清楚楚。ToR 的口灯不再狂闪,一切都在受控的 930/900 Mbps“盒子”里运行。客户笑着说:“今天这个网,像是开了窗。”
QoS 不是银弹,但在香港 1G 上下行、早晚高峰混部的场景里,HTB + fq_codel + ifb + 合理分类就是最具性价比的方案。它不需要你大动系统、不需要你砍业务,只是把路让给该走的人。
附录:一页纸速查
端口白名单(建议作为媒体高优先级)
Zoom: UDP 3478–3481, 8801–8810;TCP 443/8801–8810
Teams: UDP 3478–3481, 50000–50059;TCP/UDP 443
DSCP: EF(46) 语音, AF41(34) 视频(若未被中间盒清洗)
形状建议(1G 物理口)
Egress: 930 Mbps(保留 7% 头寸)
Ingress: 900 Mbps(更多缓存与突发冗余)
优先级分配
媒体类(class 10):保证 200 Mbps,上限随口速
默认类(class 30):保证 100 Mbps,其余共享
关键命令
nft list ruleset | grep qos
tc -s qdisc show dev eth0
tc -s class show dev eth0
tcpdump -i eth0 -nn udp port 3478 or portrange 8801-8810 -vv -c 20