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

如何在运行 Ubuntu 的香港服务器上,用 ethtool + 多队列网卡把跨境玩家匹配做“顺”

发布人:Minchunlin 发布时间:2025-09-14 10:17 阅读量:847


“晚上 8 点 30,旺角机柜通道里风声像空调在喘。我们香港集群的匹配延迟 p99 又在尖叫——从 35ms 飙到 85ms。监控墙上一片红。我把手伸进机柜,摸着那块发烫的 X710 网卡散热片,心里想:问题一定不在应用代码,而在我们网络收包路径。于是,我坐在冷风口边拿出小板凳,开始了那场把 ethtool、RSS、多队列、IRQ 亲和、RPS/XPS 全部盘一遍的夜战。”

适用场景与目标

业务场景:跨境(内地 ⇄ 香港 ⇄ 全球)玩家匹配/房间分配/短连接 RPC(HTTP/gRPC/TCP/UDP 混合)。

核心目标:

  • 降低匹配 API 的 p95/p99 延迟与抖动(Jitter)。
  • 提升突发时段的 收包并发承载能力,避免单核/单队列热点。
  • 在不中断业务的前提下,可回滚、可观测、可持续运营。

现场环境与硬件规格

型号/参数
机房 香港葵涌(双上联运营商 + 直出 CN2/GIA)
OS Ubuntu Server 22.04.4 LTS(5.15 内核)
CPU 2× Intel Xeon Silver 4310(12C/24T ×2,NUMA=2)
内存 128GB DDR4(每 NUMA 节点 64GB)
网卡 Intel X710-DA4(i40e 驱动,4×10GbE SFP+,单口对外)
磁盘 2× Intel P5510 NVMe(系统+日志)
业务 匹配服务(Go/epoll,SO_REUSEPORT,多进程),UDP 信令旁路

实际部署仅启用 ens2f0 一口对外,其他口热备/业务内网。

为什么是“多队列 + RSS + 正确的 CPU 亲和”?

单队列时代:所有包/中断落在一个 CPU 上,尖峰期轻松打满单核 -> 排队、抖动。

多队列(MQ):网卡按 五元组 Hash(RSS) 把不同连接/流均匀撒到多个 Rx 队列;每个队列对应 MSI-X 中断 → 并行收包。

关键不只是开多队列,而是:

  • 队列数与活跃核心匹配;
  • RSS 重定向表均匀;
  • 中断亲和固定到合适的核(考虑 NUMA、应用核位、softirq);
  • 适度使用 RPS/XPS 补齐硬件 RSS 的不足;
  • Coalescing、Ring、Offload 取舍,以 低延迟为优先。

Step 0:基线与症状确认(先量,再治)

# 网络与队列观察
ethtool -S ens2f0 | egrep "rx_queue_|tx_queue_|miss|dropped" | head -n 40
sar -n DEV 1
nstat -az | egrep -i 'InErrs|InDiscards|TcpRetransSegs|UdpInErrors'

# 应用延迟
# NGINX/Envoy/grafana-loki 都行,关键是要能落盘 p95/p99
# 这里以自研/Prometheus 指标为例
curl http://127.0.0.1:9090/api/v1/query?query=match_p99_ms

# 路由与抖动
mtr -rw -c 100 some-cn-ip

基线(真实一晚上的数):

指标 调优前
匹配接口 p95 41 ms
匹配接口 p99 86 ms
网卡队列分布(最热/最冷) 68% / 1%
CPU 单核软中断峰值 92%(ksoftirqd/0)
UDP 丢包(内核计数) 0.15%

Step 1:确认驱动与能力

ethtool -i ens2f0
ethtool -k ens2f0
ethtool -l ens2f0          # channels 能开多少队列
ethtool -g ens2f0          # ring 缓冲能力
ethtool -x ens2f0          # 查看 RSS indirection table
ethtool -n ens2f0 rx-flow-hash tcp4
lspci -vv -s $(ethtool -i ens2f0 | awk '/bus-info/{print $2}')
numactl -H                 # 看看这口网卡属于哪个 NUMA

要点:X710(i40e 驱动)一般支持 64 队列,但不要盲目开满,先与 CPU/NUMA/业务核数匹配。

Step 2:规划队列 & Ring 缓冲(以低延迟优先)

服务器 2 颗 CPU,每颗 12C。我们给收包中断规划在 NUMA 0 的 8 个物理核(16 逻辑)。

队列数先定为 8(便于调度和观测)。

# 将组合队列压到 8
sudo ethtool -L ens2f0 combined 8

# Ring 大小:低延迟场景不要太大,避免队列沉积
sudo ethtool -G ens2f0 rx 512 tx 512

经验:跨境小包/短连接业务,rx/tx ring 在 256~1024 内找平衡。512 往往够且不“发粘”。

Step 3:RSS 重定向表与 Hash 字段(均匀撒流)

# 看看当前散列表
sudo ethtool -x ens2f0

# 重新均匀映射到 8 个队列(举例:轮询分配)
# 这里给出一段小脚本生成均匀映射(i40e 支持)
map=()
for i in $(seq 0 127); do map[$i]=$(( i % 8 )); done
sudo ethtool -X ens2f0 equal 8   # 先粗暴均衡
# 如需指定 key:sudo ethtool -X ens2f0 hkey <32*4字节HEX>  # 可选

Hash 字段(示意,按需开启):

在部分驱动上可用 ethtool -N 配置 TCP/UDP 的散列字段(源/目的 IP、端口等),确保五元组足够分散。示例(支持时):

# 让 TCP/UDP 都考虑 src/dst IP + src/dst port
sudo ethtool -N ens2f0 rx-flow-hash tcp4 sdfn
sudo ethtool -N ens2f0 rx-flow-hash udp4 sdfn

如果驱动不支持修改,至少保证 GRO/LRO 设置不会把不同流粘一起(见 Step 5)。

Step 4:中断亲和(把队列“钉”到对的核)

停掉 irqbalance(否则会把你手动绑好的亲和给“搬家”):

sudo systemctl stop irqbalance
sudo systemctl disable irqbalance

找到每个队列的 IRQ 号:

grep -i ens2f0 /proc/interrupts

把 ens2f0-TxRx-0..7 分别绑到 NUMA 0 的 8 个物理核(示例:核 2,4,6,8,10,12,14,16):

# 生成 CPU mask 的函数(单核)
cpu_mask() { printf "%x" $((1<<$1)); }

# 假设 IRQ 号分别是 120..127,对应绑核如上
for i in 0 1 2 3 4 5 6 7; do
  irq=$((120+i))
  cpu=$((2 + i*2))                  # 示例选偶数核,避开 0/1 给系统
  mask=$(cpu_mask $cpu)
  echo $mask | sudo tee /proc/irq/$irq/smp_affinity
done

配套:把业务进程也尽量 pin 在 同一 NUMA 的邻近核,减少跨节点内存访问抖动。

Go 服务可用 GOMAXPROCS + taskset;容器可用 cpuset/cgroup 约束。

Step 5:Coalescing & Offload(收敛与硬件卸载的取舍)

低延迟优先建议初值:

# 降低 coalescing,减少 NIC 为“凑包”而延迟发中断
sudo ethtool -C ens2f0 rx-usecs 4 rx-frames 1 tx-usecs 8

# 关闭 LRO(服务端容易导致跨流合并过度),保留 GRO(适度)
sudo ethtool -K ens2f0 lro off gro on gso on tso on tx-nocache-copy on

为什么:

  • LRO 过度合并可能增大单次处理批量→尾延迟抬头。
  • GRO/GSO/TSO 保留能减轻 CPU,为同一流做批处理,通常对短连接影响可控。
  • Coalescing 过小可能提高 CPU 占用;我们从 4/1/8 usecs 起步,根据 p99 观察微调。

Step 6:RPS/RFS/XPS(软件层再分流 & 发包亲和)

硬件 RSS 足够时可以先不启用 RPS;当观察到队列仍偏斜或内核路径出现瓶颈,再启。

# 为每个 RX 队列配置 RPS(把 softirq 分摊到一组 CPU)
# 示例:把 8 个队列对应到 NUMA0 的 16 逻辑核(2..17)
mask=$(printf "%x" $((0x0003FFFC)))  # 2-17 位掩码示意,根据机器调整

for q in /sys/class/net/ens2f0/queues/rx-*; do
  echo $mask | sudo tee $q/rps_cpus >/dev/null
done

# 可选:RFS 根据 socket 迁移,使同一流尽量落在处理该 socket 的核
echo 32768 | sudo tee /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/ens2f0/queues/rx-*; do
  echo 4096 | sudo tee $q/rps_flow_cnt >/dev/null
done

# XPS:发送路径亲和,避免跨核 cache miss
for q in /sys/class/net/ens2f0/queues/tx-*; do
  echo $mask | sudo tee $q/xps_cpus >/dev/null
done

RPS/RFS 会增加一次软中断重分发,先做 A/B。若 p99 变差,回退只保留 XPS。

Step 7:Socket/内核网络栈参数(TCP/UDP 各就各位)

/etc/sysctl.d/99-match-tune.conf:

# 队列与 backlog
net.core.somaxconn = 8192
net.core.netdev_max_backlog = 4096

# Buffer 上限(按需)
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.udp_mem = 4096 87380 134217728
net.ipv4.udp_rmem_min = 16384
net.ipv4.udp_wmem_min = 16384

# 端口与短连接
net.ipv4.ip_local_port_range = 10000 65535

# 拥塞控制与队列算法(短连接/低延迟)
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = cubic

# Busy poll(按需,小心打开)
net.core.busy_read = 50
net.core.busy_poll = 50
net.ipv4.tcp_fastopen = 3

UDP 旁路可选(仅当明确知道影响):

对指定 UDP 端口关闭 conntrack,可降低 CPU 抖动,但要评估防火墙/NAT 影响。

# UFW/iptables raw 表 NOTRACK(示例端口 30000-30100)
sudo iptables -t raw -A PREROUTING -p udp --dport 30000:30100 -j NOTRACK
sudo iptables -t raw -A OUTPUT     -p udp --sport 30000:30100 -j NOTRACK

Step 8:应用层配合(SO_REUSEPORT + 进程绑核 + NUMA 亲和)

多进程 + SO_REUSEPORT(Go/NGINX/Envoy 都支持),让内核在同一端口对多个监听进行负载均衡(与 RSS/RPS 协同)。

为每个进程 taskset 到与对应队列邻近的核。

NUMA 绑定:numactl --cpunodebind=0 --membind=0。

示例 systemd(/etc/systemd/system/match@.service):

[Unit]
Description=Match Worker %i
After=network-online.target

[Service]
Environment="GOMAXPROCS=2"
ExecStartPre=/usr/local/sbin/net-queue-pin.sh %i
ExecStart=/usr/local/bin/match-server --listen :8443 --reuseport
CPUAffinity=2 4 6 8 10 12 14 16
NUMAPolicy=preferred
LimitNOFILE=1048576
Restart=always

/usr/local/sbin/net-queue-pin.sh(按实例序号把进程与队列对应):

#!/usr/bin/env bash
# 用 %i 实例号映射到固定核位
inst=$1
cpu=$(( 2 + (inst-1)*2 ))
taskset -pc $cpu $$ >/dev/null

生产里我最终跑了 8 个进程,一一对应 8 个队列;观测最稳。

Step 9:把一切持久化(重启不丢)

ethtool 持久化(systemd oneshot) /etc/systemd/system/ethtool-ens2f0.service:

[Unit]
Description=Ethtool tuning for ens2f0
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ethtool-ens2f0-apply.sh

[Install]
WantedBy=multi-user.target

/usr/local/sbin/ethtool-ens2f0-apply.sh:

#!/usr/bin/env bash
set -e
ethtool -L ens2f0 combined 8
ethtool -G ens2f0 rx 512 tx 512
ethtool -C ens2f0 rx-usecs 4 rx-frames 1 tx-usecs 8
ethtool -K ens2f0 lro off gro on gso on tso on tx-nocache-copy on
ethtool -X ens2f0 equal 8
# 如驱动支持:ethtool -N ens2f0 rx-flow-hash tcp4 sdfn
# 中断亲和
IRQS=$(grep -i "ens2f0-TxRx" /proc/interrupts | awk '{print $1}' | tr -d :)
idx=0
for irq in $IRQS; do
  cpu=$((2 + idx*2))
  mask=$(printf "%x" $((1<<cpu)))
  echo $mask > /proc/irq/$irq/smp_affinity
  idx=$((idx+1))
done
# XPS/RPS(按需)
mask=$(printf "%x" $((0x0003FFFC)))
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries || true
for q in /sys/class/net/ens2f0/queues/rx-*; do echo $mask > $q/rps_cpus; echo 4096 > $q/rps_flow_cnt; done
for q in /sys/class/net/ens2f0/queues/tx-*; do echo $mask > $q/xps_cpus; done

sudo chmod +x /usr/local/sbin/ethtool-ens2f0-apply.sh
sudo systemctl enable --now ethtool-ens2f0.service

sysctl 持久化

已写入 /etc/sysctl.d/99-match-tune.conf,执行 sudo sysctl --system。

Step 10:验证与对比(不是“感觉快”,要数据快)

压测组合

  • 内地⇄香港:两条线路(CN2/GIA 和普通 163),不同 RTT。
  • 工具:wrk(HTTP)、hping3(UDP burst)、tc netem(模拟 20~50ms RTT)。
  • 观测:Prometheus 指标 + ethtool -S 队列均衡度 + /proc/softirqs 软中断占比。

实测结果(2 小时高峰):

指标 调优前 调优后
匹配接口 p95 41 ms 28 ms
匹配接口 p99 86 ms 42 ms
Jitter(p99-p50) 57 ms 19 ms
队列分布(最热/最冷) 68% / 1% 15% / 9%
CPU 单核软中断峰值 92% 48%
UDP 丢包(内核计数) 0.15% <0.02%
业务错误率 0.3% 0.08%

观察到 p99 腰斩,同时队列偏斜明显改善。高峰期玩家“匹配等待”小红点,从平均 0.9s 降到 0.4s(含业务逻辑时间)。

线上踩坑与应急手术

irqbalance“复活”
运维伙伴在内核升级后忘记禁用,亲和被打乱 → p99 突然回升。
处置:加了 systemctl mask irqbalance,并把亲和设置纳入开机 oneshot。

GRO/LRO 混用导致队列热(某次把 gro off)
关闭 GRO 后单包增多,RX 中断风暴,p50 提升但 p99 变差。
处置:恢复 gro on,降低 coalescing,保留低延迟 + 适当批处理平衡。

容器网络叠加转发
某节点从 macvlan 换成 bridge,conntrack 抖动增加。
处置:匹配服务容器固定 hostNetwork(k8s),或为关键端口 raw 表 NOTRACK。

NUMA 远程内存
一版上线把进程跑在 NUMA1,网卡在 NUMA0 → 跨节点抖动严重。
处置:numactl 固定 + PromQL 报警“RX 队列核 != 进程核”。

过度 RFS
开太大 rps_flow_cnt 反而引入额外迁移成本。
处置:减半/关闭 RFS,仅保留 XPS,效果更稳。

观测面板建议(把问题“钉”在墙上)

  • 队列均衡度:ethtool -S ens2f0 | rx_queue_*_packets 计算变异系数(CV),CV < 0.25 为佳。
  • 软中断热核:node_softnet_* / /proc/softirqs 画 per-cpu 堆叠。
  • p95/p99 延迟 & Jitter:Prometheus Summary + 线路分组(CN2/163)。
  • 丢包:内核 InDiscards/OutDiscards + 应用侧 timeout 比例。
  • 应用核亲和审计:进程 CPU set 与队列 IRQ 核位的对应关系告警。

回滚策略(生产心态)

  • 一键回滚脚本:把 ethtool、亲和、RPS/XPS、sysctl 全部写入 undo。
  • 分批放量:先一台 canary,观测 30~60 分钟再灰度。
  • 护城河:保留 ethtool -C 的两套 profile(低延迟 / 高吞吐),必要时快速切换。

示例:快速回滚到“保守模式”

ethtool -C ens2f0 rx-usecs 16 rx-frames 4 tx-usecs 32
ethtool -G ens2f0 rx 1024 tx 1024
for q in /sys/class/net/ens2f0/queues/rx-*; do echo 0 > $q/rps_cpus; echo 0 > $q/rps_flow_cnt; done
for q in /sys/class/net/ens2f0/queues/tx-*; do echo 0 > $q/xps_cpus; done
systemctl start irqbalance

FAQ:一些你可能会问我的取舍

要不要上 BBR?
匹配短连接 + 小包为主,cubic + fq 已够稳;BBR 在高带宽长肥管道才显著。

队列开越多越好吗?
不。队列过多会稀释中断、增加调度成本。8~16 对 10GbE 常见足够。

DPDK/XDP 是否更香?
极限场景是的,但复杂度、可维护性与生态改造成本高。先把 ethtool+RSS+亲和吃满再考虑。

凌晨 1 点,冷风口的咖啡被吹得有点凉。我把最后一个 IRQ 写回亲和,盯着大屏的 p99 曲线一点一点往下落。那一刻,你能感觉到硬件队列、内核软中断、应用线程像齿轮一样咬合上了。
第二天晚高峰,我们坐在机房外的小茶餐厅里,手机上看着匹配延迟稳定在 40ms 以内,隔壁台还在讨论‘是不是代码优化了?’ 我笑了笑:‘不,是包走对了路。’

摘要速记(给后来者)

  • 先量后治:确认是单核/单队列热点。
  • 配队列:ethtool -L ens2f0 combined 8;-G rx/tx 512。
  • 均匀 RSS:ethtool -X ens2f0 equal 8;(可选 -N tcp4/udp4 sdfn)。
  • 绑 IRQ:关 irqbalance,/proc/irq/*/smp_affinity 绑到 NUMA 就近核。
  • Coalescing/Offload:-C rx-usecs 4 rx-frames 1 tx-usecs 8;-K lro off gro on。
  • RPS/XPS:先 XPS,再按需小剂量 RPS/RFS。
  • sysctl:somaxconn/backlog/buffer/fq/cubic;UDP 旁路慎用。
  • 应用:SO_REUSEPORT + 进程绑核 + NUMA。
  • 持久化:systemd oneshot + sysctl;回滚脚本常备。
  • 看板:队列 CV、软中断热核、p95/p99、Jitter、丢包。

——如果你也在香港机房里和 p99 拉锯,希望这份“汗味儿”的手记,能让你的曲线更优雅地落下去。

目录结构
全文