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

如何在香港服务器的 Debian 环境里,用内核 TC 做流量整形,稳住 MOBA 战斗数据的低延迟

发布人:Minchunlin 发布时间:2025-09-08 09:48 阅读量:744
那天是凌晨 2:40,在香港葵涌机房,我们刚把新一批游戏服切到香港节点,战斗服的延迟图还在墙上的大屏上跳动:平均 RTT 其实不差,但抖动(jitter)像心电图——玩家团战一开,语音和战斗包互相抢路,瞬时排队导致小包排在大包后面。
 
我盯着那台 1U 的主机,想起白天在公司会议室里我们讨论过的所有“优化”:从应用层重传、到 UDP ring buffer 的参数,再到换网卡。最后我决定回到网络的“地基”——Linux 内核的 TC(Traffic Control)。只有当我们在发送与接收路径上控制排队,把战斗数据从系统队列里“快车道”送出去、从入口“快速疏导”进来,才能在拥塞时稳住抖动。
 
下面,是那一夜我在 Debian 环境一步步把 TC 落地、上线、回看指标、复盘坑点的全部过程。写给需要和我一样,既要给新同事看得懂、又能让老工程师挑不出硬伤的人。

现场环境与目标

硬件与网络(实配)

机型:Dell R650(1U)
CPU:Intel Xeon Silver 4314(16C/32T),BIOS 开启 Turbo,C-State 限到 C1
内存:128GB DDR4-3200(NUMA 单路)
系统盘:2× NVMe(RAID1)
网卡:Intel X710-DA2(10GbE,SFP+,驱动 i40e)
上联:机房 TOR 交换机 2×10G LACP 到 Core(我们按单口 10G 估算整形上限)

系统与内核(实配)

OS:Debian 12 (bookworm) 最小化安装
内核:6.1.x(Debian LTS 内核,已带 sch_fq_codel、ifb、act_mirred 等模块)

业务端口与流特征(约定)

战斗服 UDP 端口段:20000-20100/udp(进程动态分配,但固定区间)
战斗包:小包多、速率稳定、对延迟与抖动极敏感
其他流量:日志收集(TCP)、监控(TCP/UDP 混合)、对象存储同步(大流 TCP)

目标

出口(egress):拥塞时让战斗流量优先发,避免被大包“顶住”;
入口(ingress):将入口流量转到 ifb 做整形,解决对端拥塞或上联突发导致的队列抖动;
排队算法:HTB 做层级带宽与优先级,叶子挂 fq_codel 降低延迟与尾部排队;
自动化:系统启动即生效,可一键回滚。

1. 设计思路与拓扑

我们在两侧排队:
 
      [应用进程]
           |
        Socket
           |
        内核协议栈
           |
   TX Path (egress) ——> [eth0] ——> 上联
           ^
           |
 HTB root + classes(战斗高优先) + fq_codel
 
   RX Path (ingress) <—— [eth0]
           |
       ingress qdisc (ffff:)
           |  重定向
           v
         [ifb0] ——> HTB + fq_codel(入口侧分类与整形)
 
egress:HTB 负责“份额+上限”,叶子挂 fq_codel 抑制 bufferbloat;
ingress:eth0 ingress 把包 镜像重定向 到 ifb0,在 ifb0 上用同样的 HTB/fq_codel 逻辑做“入口整形”;
分类策略:在 nftables 里对战斗端口打 skb mark(或 DSCP),TC 用 fw filter(按 mark)或 flower/u32 识别并送入对应 class。
 
为什么不只用 CAKE?CAKE 很好用,但分层资源配给(战斗/后台/默认)与**双向(egress+ingress)**可控更贴近我们的运营需求,且 HTB + fq_codel 在 10G 接口下的可解释性、透明度更高,团队也更熟。

2. 基线调优(上线 TC 之前必须做)

2.1 网卡与中断

# 绑定中断到本 NUMA 节点的 CPU(示例:绑到 0-7)
apt install irqbalance -y
systemctl stop irqbalance
for i in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
  echo 0xFF > /proc/irq/$i/smp_affinity   # 0-7 的位图,可按需细化
done

# 适度降低中断合并,减少延迟(量化需现场压测)
ethtool -C eth0 adaptive-rx off adaptive-tx off rx-usecs 25 tx-usecs 25

2.2 关闭部分 offload(利于小包时延)

ethtool -K eth0 tso off gso off gro off rxvlan on txvlan on
说明:GRO/TSO 对大包吞吐有利,但容易让小包“等车”。关闭后用 fq_codel 抑制排队;如你的业务还跑大流,需要折中测试。

2.3 系统参数(/etc/sysctl.d/99-game.conf)

net.core.netdev_max_backlog = 4096
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.ipv4.udp_rmem_min = 16384
net.ipv4.udp_wmem_min = 16384
net.ipv4.tcp_congestion_control = bbr
net.ipv4.ip_local_port_range = 10000 65000

sysctl --system

3. 出口(egress)整形:HTB + fq_codel

为便于迁移与回滚,我把所有命令做成脚本 /usr/local/sbin/tc_moba.sh,并用变量控制接口/速率。

3.1 脚本(出口部分)

#!/usr/bin/env bash
set -euo pipefail

IFACE="${IFACE:-eth0}"
# 我们按单口 10G,但留余量给交换机突发与内核调度
# 也可按对端瓶颈设更保守值(例如 8gbit)
EGRESS_CEIL="${EGRESS_CEIL:-10gbit}"
ROOT_RATE="${ROOT_RATE:-8gbit}"

# 清理旧规则
tc qdisc del dev "$IFACE" root 2>/dev/null || true

# Root HTB
tc qdisc add dev "$IFACE" root handle 1: htb default 30
tc class add dev "$IFACE" parent 1: classid 1:1 htb rate "$ROOT_RATE" ceil "$EGRESS_CEIL"

# 战斗高优先:预留+高上限
tc class add dev "$IFACE" parent 1:1 classid 1:10 htb rate 2gbit ceil "$EGRESS_CEIL" prio 0 quantum 1514 burst 64k cburst 64k
tc qdisc add dev "$IFACE" parent 1:10 handle 10: fq_codel limit 1000 target 5ms interval 50ms ecn

# 后台(日志/同步)
tc class add dev "$IFACE" parent 1:1 classid 1:20 htb rate 1gbit ceil 6gbit prio 1 quantum 1514 burst 64k cburst 64k
tc qdisc add dev "$IFACE" parent 1:20 handle 20: fq_codel limit 2000 target 10ms interval 100ms ecn

# 默认兜底
tc class add dev "$IFACE" parent 1:1 classid 1:30 htb rate 500mbit ceil 5gbit prio 2 quantum 1514 burst 64k cburst 64k
tc qdisc add dev "$IFACE" parent 1:30 handle 30: fq_codel limit 4000 target 15ms interval 100ms ecn

# 分类:优先用 fwmark(由 nftables 打标)
# fw filter:handle=0x10 -> 战斗流
tc filter add dev "$IFACE" parent 1: protocol ip    prio 1 handle 0x10 fw flowid 1:10
tc filter add dev "$IFACE" parent 1: protocol ipv6  prio 1 handle 0x10 fw flowid 1:10

# 兜底:其余走默认 1:30(无需额外 filter)
echo "[EGRESS] HTB + fq_codel applied on $IFACE"

3.2 nftables 打标(对战斗端口段)

Debian 12 默认优先 nft。我们建一张表,对发出的战斗 UDP 包打 mark=0x10(你也可以顺带把 DSCP 置为 EF=46,给上游队列一个提示):
 
cat >/etc/nftables.d/moba.nft <<'EOF'
table inet moba {
  chain out {
    type filter hook output priority 0;
    # 战斗 UDP
    udp dport 20000-20100 meta mark set 0x10
    # (可选)同时设置 DSCP=EF(46)
    udp dport 20000-20100 ip dscp set 46
  }
  chain preroute {
    type filter hook prerouting priority 0;
    # 入方向也打标,方便 ifb 分类
    udp sport 20000-20100 meta mark set 0x10
    udp sport 20000-20100 ip dscp set 46
  }
}
EOF

# 引入到主配置
grep -q 'include "/etc/nftables.d/*.nft"' /etc/nftables.conf || \
  sed -i '1a include "/etc/nftables.d/*.nft"' /etc/nftables.conf

nft -f /etc/nftables.conf
nft list ruleset | grep -A2 'table inet moba'

4. 入口(ingress)整形:ifb 重定向 + HTB/fq_codel

为什么要整 ingress?
很多时候排队发生在对端或上联,内核收包路径也会出现突发拥塞(尤其是 CDN/对象存储回流同时到来时)。我们把 eth0 的入口流量 重定向到 ifb0,在 ifb0 上做与出口类似的层级整形,按照我们自己的“优先级”来“消化”入口队列。

4.1 脚本(入口部分)

在同一个 /usr/local/sbin/tc_moba.sh 里追加:
# ---------- Ingress via ifb ----------
modprobe ifb || true
ip link add ifb0 type ifb 2>/dev/null || true
ip link set dev ifb0 up

# 清理
tc qdisc del dev "$IFACE" ingress 2>/dev/null || true
tc qdisc del dev ifb0 root 2>/dev/null || true

# 把 eth0 的入口挂 ingress,并重定向到 ifb0
tc qdisc add dev "$IFACE" handle ffff: ingress
tc filter add dev "$IFACE" parent ffff: protocol all u32 match u32 0 0 action mirred egress redirect dev ifb0

# 在 ifb0 上做 HTB/fq_codel(入口“自定义节流/排队”)
tc qdisc add dev ifb0 root handle 2: htb default 230
tc class add dev ifb0 parent 2: classid 2:1 htb rate 8gbit ceil 10gbit

tc class add dev ifb0 parent 2:1 classid 2:10 htb rate 2gbit ceil 10gbit prio 0
tc qdisc add dev ifb0 parent 2:10 handle 210: fq_codel limit 1000 target 5ms interval 50ms ecn

tc class add dev ifb0 parent 2:1 classid 2:20 htb rate 1gbit ceil 6gbit prio 1
tc qdisc add dev ifb0 parent 2:20 handle 220: fq_codel limit 2000 target 10ms interval 100ms ecn

tc class add dev ifb0 parent 2:1 classid 2:30 htb rate 500mbit ceil 5gbit prio 2
tc qdisc add dev ifb0 parent 2:30 handle 230: fq_codel limit 4000 target 15ms interval 100ms ecn

# 分类(沿用 nft mark=0x10)
tc filter add dev ifb0 parent 2: protocol ip    prio 1 handle 0x10 fw flowid 2:10
tc filter add dev ifb0 parent 2: protocol ipv6  prio 1 handle 0x10 fw flowid 2:10

echo "[INGRESS] ifb0 HTB + fq_codel applied"

5. 一键启动与回滚(systemd)

5.1 脚本保存与执行权限

chmod +x /usr/local/sbin/tc_moba.sh

5.2 Systemd 服务

/etc/systemd/system/tc-moba.service:
[Unit]
Description=TC shaping for MOBA traffic (egress+ingress)
After=network-online.target nftables.service
Wants=network-online.target

[Service]
Type=oneshot
Environment="IFACE=eth0" "EGRESS_CEIL=10gbit" "ROOT_RATE=8gbit"
ExecStart=/usr/local/sbin/tc_moba.sh
ExecStop=/usr/local/sbin/tc_moba_rollback.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

5.3 回滚脚本

/usr/local/sbin/tc_moba_rollback.sh:
#!/usr/bin/env bash
set -euo pipefail
IFACE="${IFACE:-eth0}"


tc qdisc del dev "$IFACE" root 2>/dev/null || true
tc qdisc del dev "$IFACE" ingress 2>/dev/null || true
tc qdisc del dev ifb0 root 2>/dev/null || true
ip link set ifb0 down 2>/dev/null || true
ip link del ifb0 2>/dev/null || true
echo "[ROLLBACK] All TC/IFB removed."

5.4 启用

systemctl daemon-reload
systemctl enable --now tc-moba.service

6. 观测与验收

6.1 tc 统计与队列状态

tc -s qdisc show dev eth0
tc -s class show dev eth0
tc -s qdisc show dev ifb0
tc -s class show dev ifb0
关注点:backlog、drops、ecn_mark、各 class 的字节/包计数是否符合预期(战斗流应主要打到 1:10 / 2:10)。

6.2 压测方法(实操)

战斗模拟:内部工具或 tcpreplay 回放 pcap;
后台压力:iperf3 -c <collector> -P 4 -t 120 并行大流;
观测:mtr -u(UDP)、业务侧 RTT 指标、服务器 tc -s 统计。

6.3 上线前后对比(真实一次窗口)

场景 指标 上线前 上线后
合服团战峰值(混有日志/同步) 战斗 RTT 均值 26.8 ms 24.9 ms
  抖动(p95-p50) 11.3 ms 4.1 ms
  战斗包丢弃(服务器侧) 0.12% 0.02%
后台同步并发 4 路 同步吞吐 3.6 Gbps 3.2 Gbps(略降,预期)
NIC 队列 backlog 峰值 backlog 18~22ms 等效 < 6ms
解释:我们故意在默认/后台 class 上收紧 ceil 与 fq_codel 参数,把拥塞期的队列占用让位给战斗小包。后台吞吐略降属预期(可在离峰加大上限或定时放宽)。

7. 参数表与调优要点

7.1 关键参数对照

组件 参数 我们的值 说明
HTB root rate/ceil 8G / 10G 预留抖动空间,避免内核/队列爆顶
战斗 class rate/ceil/prio 2G / 10G / 0 小包优先,不做过度限速
fq_codel target/interval 5ms / 50ms 更激进地控制延迟
ifb ingress 同 egress 同步配置 入口同样拥塞时可控
ethtool coalesce rx/tx-usecs 25/25 降低延迟的温和值,需结合 CPU
offload tso/gso/gro off 小包业务更稳,吞吐略损
sysctl netdev_max_backlog 4096 减少过度排队

7.2 进阶分类(可选)

DSCP 分类:在 TC 里按 ip tos/dscp 匹配(上游若支持 QoS,可进一步打通);
flower 过滤器:更易读的五元组匹配(内核新、驱动新时建议);
cgroup net_cls:按进程/服务维度打 classid,避免端口漂移带来的错分。

8. 常见坑与现场解决

fw filter 不生效
排查:nft list ruleset 看 mark 是否成功;tcpdump -vv 观察 DSCP/mark;
一次真实故障是我们把规则写在 postrouting,实测对本机进程发出的包不生效,换到 output 解决。
 
ifb 无法创建或不计数
核心模块缺失或被禁:modprobe ifb;确保没有被旧的 tc 残留影响,先 tc qdisc del ... 再加。
记得 ip link set ifb0 up,否则统计空空如也。
 
X710 硬件 offload 抢活
某些队列/分流 offload 会绕过软件 qdisc。我们统一关闭 L3/L4 硬分流特性(视驱动版本),或用 skip_hw 的 flower 策略,保持可预期。
 
fq_codel 太激进导致后台吞吐掉得更多
放宽 interval 到 100ms、增大 limit,或在离峰把后台 ceil 提高。
 
多队列(MQ)与 RPS/RFS
在多核场景,确保 RX 队列与 CPU 亲和(ethtool -l / /proc/interrupts),必要时开启适度 RPS,但别一上来铺满所有核,先做单 NUMA 验证。

9. 验证脚本(快速健康检查)

echo "[Check] nft marks:"
nft list ruleset | grep -E "moba|dport 20000"

echo "[Check] egress classes:"
tc -s class show dev eth0 | egrep '1:10|1:20|1:30' -A2

echo "[Check] ingress classes:"
tc -s class show dev ifb0 | egrep '2:10|2:20|2:30' -A2

echo "[Check] live flows (udp 20000-20100):"
ss -uH 'dport = 20000 or dport = 20100' || true

10. 运维落地与值守策略

变更窗口:选非高峰(我们用香港时间 02:00-03:30),准备回滚脚本;
灰度:先对一台战斗服启用 TC,对比同池指标 30 分钟;
告警:新增战斗 class 的 drops、backlog 告警,阈值根据你的基线设;
文档化:把 rate/ceil、interval/target 的变更与效果纳入变更记录(谁调过、为什么、结果如何)。

11. 尾声:凌晨 4:10 的那张图

TC 上线 20 分钟后,墙上的延迟图慢慢“贴地”——不是戏剧性直线下降,而是抖动被削平,团战大混战时也不再出现“掉帧式”的突刺。后面一周,我们把这套规则模板化,按机房带宽和业务权重去套。
我始终记得那个清晨:机房的风很冷,风扇像在合唱。我们没有换任何一行业务逻辑,只是把内核的队列理顺了——把小包放在小车道上,把大包让到后面。玩家不会知道“HTB”“fq_codel”这些词,但他们会感觉到:团战更顺了。
 

12. 附录:可复制的最小可用配置(MVP)

适合快速验证,确认有效后再替换为上文完整版本。
# 1) 基本 nft 打标
nft add table inet moba
nft 'add chain inet moba out { type filter hook output priority 0; }'
nft 'add chain inet moba preroute { type filter hook prerouting priority 0; }'
nft 'add rule inet moba out udp dport 20000-20100 meta mark set 0x10'
nft 'add rule inet moba preroute udp sport 20000-20100 meta mark set 0x10'

# 2) 出口整形(eth0)
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 8gbit ceil 10gbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 2gbit ceil 10gbit prio 0
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel target 5ms interval 50ms ecn
tc class add dev eth0 parent 1:1 classid 1:30 htb rate 500mbit ceil 5gbit prio 2
tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel
tc filter add dev eth0 parent 1: protocol ip handle 0x10 fw flowid 1:10

# 3) 入口整形(ifb0)
modprobe ifb
ip link add ifb0 type ifb
ip link set ifb0 up
tc qdisc add dev eth0 handle ffff: ingress
tc filter add dev eth0 parent ffff: protocol all u32 match u32 0 0 action mirred egress redirect dev ifb0
tc qdisc add dev ifb0 root handle 2: htb default 230
tc class add dev ifb0 parent 2: classid 2:1 htb rate 8gbit ceil 10gbit
tc class add dev ifb0 parent 2:1 classid 2:10 htb rate 2gbit ceil 10gbit prio 0
tc qdisc add dev ifb0 parent 2:10 handle 210: fq_codel target 5ms interval 50ms ecn
tc filter add dev ifb0 parent 2: protocol ip handle 0x10 fw flowid 2:10

最后一口气的经验之谈

先把“分类”做准,再去抠 target/interval;
双向都要控:只做 egress 常常不够;
别迷信某一种算法:HTB+fq_codel、CAKE、prio……选择你团队能看懂与值守的;
给后台一个体面的位置:它不是敌人,只是在团战时让一让路。
如果你也在香港的深夜机房里和抖动缠斗,试试这套做法。愿你也在清晨 4 点,看见那张线条终于平了下来。
目录结构
全文