上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-25 10:28 阅读量:775


星期一早上 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

目录结构
全文