TL-R5005PE-AC 双 WAN 负载均衡踩坑

笔者最近给一台 TP-LINK TL-R5005PE-AC 企业路由器接入了两台蜂窝 CPE。上游分别是电信和联通的流量卡,每张卡每月均有 2TB 流量。两台 CPE 各用一根网线接入主路由的两个 WAN 口,链路测速基本一致。原本以为开启双 WAN 负载均衡后,两张卡就能一起分担流量,结果却是电信卡已接近耗尽,联通卡才用了约 20%。

为了解决这个问题,笔者先研究了官方固件中的选路逻辑,又在局域网机器上持续采集主路由的流量计数。期间一度得出了一个非常离谱的结论:即使取消了运营商绑定,甚至交换两条线路,统计出来的流量仍然严重偏向 WAN1,最高甚至接近 98:2。看起来无论怎么调整,路由器都更偏好字面意义上的「WAN1」。

但在进一步分析内核和硬件转发计数后,笔者发现这个结论的依据本身就存在问题:两个 WAN 在 Linux 系统中的接口统计根本不是同一种口径,WAN2 的大量硬件加速(HNAT)流量压根没有被统计进去。改用底层物理端口计数,并同时记录硬件与软件两条转发路径后,使用连接均衡设置,两条线路的累计流量终于接近各占一半。

这次排查遇到了两个叠加的坑:一是手动指定运营商导致 ISP 选路规则提前截流了一部分连接,二是硬件加速机制导致上层监控拿到了错误的流量比例。本文将记录完整排查过程,以及固件中几个容易被 UI 界面掩盖的实现细节。

两张 2TB 流量卡,为什么只用完一张?

本次的网络拓扑非常简单:

flowchart LR
    A["电信流量卡 + CPE"] --> C["TL-R5005PE-AC<br/>双WAN负载均衡"]
    B["联通流量卡 + CPE"] --> C
    C --> D["局域网设备"]

两个上游均为蜂窝网络,网速也基本一致。刚开始,笔者在路由器界面上启用了负载均衡,并按照实际线路给两个 WAN 口分别指定了运营商(WAN1 指定电信,WAN2 指定联通)。然而,流量卡后台显示的实际套餐消耗却大相径庭:一张 2TB 套餐已然耗尽,另一张才消耗了约五分之一。这个真实的套餐余量差异促使笔者展开排查。

从固件实现看「负载均衡」的真实逻辑

本文分析的设备为 TL-R5005PE-AC V1.0,固件版本为 1.0.1 Build 241108 Rel.60855n。解包根文件系统(rootfs)后,可以找到几套职责分明的程序:

文件或模块主要用途
/usr/sbin/isp_route按目标 IP 匹配运营商地址库并选择出口
/usr/sbin/load_balance根据接口实时负载选择较空闲线路(即带宽均衡)
/usr/sbin/default_balance、ipt_balance.ko对新选择请求进行轮询分配(即连接均衡)
special_route 相关模块保存和查询地址对绑定,复用已有的出口选择
/etc/init.d/hnat 及 Realtek FC 代码负责软件 / 硬件加速及其计量统计

其中不少控制逻辑直接写在 Shell 脚本里,但连接选择器和地址绑定的关键核心位于内核模块。为了核实细节,笔者提取了固件内核,随后对相关函数进行了分析。

分析表明,界面上的「负载均衡」并非一个统一处理所有连接的总开关。一个典型的 IPv4 报文进入路由后,会依次经过以下层级:

1
2
3
4
5
6
7
策略选路
↓
特殊应用选路及已有地址对绑定
↓
ISP运营商选路
↓
带宽均衡 / 连接均衡

连接跟踪(conntrack)中的 connmark 负责保存已经选择好的出口。后端的均衡器在恢复标记时,一旦发现标记非零,就会直接返回已有出口,不会再将该连接重新分配。因此,在分析「负载均衡为什么不均」之前,首先需要确认是否有更早的规则提前把出口强行选走了。

第一个坑:填写运营商,意外触发了前置选路规则

最早的时候,WAN1 被指定为电信,WAN2 被指定为联通,导致 ISP 选路(ISP Route)处于开启状态:

1
2
WAN1: isp_name=dianxin,  priority=user
WAN2: isp_name=liantong, priority=user

此时 isp_route 模块生成的匹配条件基于目标 IP 地址:

1
2
-m set --match-set ISP_CHINA_TELECOM dst
-m set --match-set ISP_UNICOM_CNC dst

这意味着,当一条连接尚未被更早的策略或绑定决定出口时,只要访问的目标 IP 落在电信地址库内,就会优先强制走电信线路;目标 IP 落在联通地址库内,则强制走联通线路。匹配成功后生成的 connmark 会被锁定,导致后续的普通负载均衡模块根本无法介入。

这一设计的初衷是为了减少跨网访问、降低延迟,但也会导致双 WAN 的流量不平衡。结合固件逻辑,如果终端实际访问的目标服务器大多落在电信地址库中,或者承载大流量的业务(如视频下载等)刚好匹配了电信 IP,这部分流量就会被提前锁定到电信线路,连接均衡器完全没有介入的机会。流量严重偏向电信也有了合理解释。

取消手动运营商指定后,再次检查发现两个 WAN 的状态均变更为 auto_check / non_set,isp_route 链清空且不再被入口规则引用。对于纯流量卡叠加的使用场景,彻底禁用 ISP 运营商选路是实现流量均衡的前提。

带宽均衡并不关心「本月用了多少 GB」

排除 ISP 选路的干预后,笔者进一步研究了固件中「带宽均衡」的算法。load_balance 的核心计算逻辑可以抽象为如下伪代码:

1
2
3
4
5
6
7
8
# 约 10 秒采样窗口内的字节增量;正数情况下 // 表示向下取整
up_rate = delta_tx_bytes * 8 // 1024 // 10
down_rate = delta_rx_bytes * 8 // 1024 // 10

up_level = up_rate * 200 // configured_up + 1
down_level = down_rate * 200 // configured_down + 1

score = max(up_level, down_level)

它对每条在线线路计算出一个整数「负载档位」,并选择当前档位最低的线路分配给后续到达的新连接。每次 10 秒采样后约有 4 秒等待,更新周期约为 14 秒。

这里有几个极其关键的实现细节:

  • 仅比较短时利用率:算法只评估过去几秒的瞬时速率,完全不了解两张卡本月分别消耗了多少 GB,也没有月度套餐余量的反馈机制。
  • 已建连接不会重新迁移:负载更新只影响后续新建连接的选择,已绑定标记的活跃连接会继续沿用原有出口。
  • 评分采用取整计算:在配置带宽较大、实际使用速率较小时,两条线路极易落入完全相同的档位。
  • 评分相同时存在固定的首位偏好:代码逻辑仅在新评分严格低于当前选择时才进行替换。若两边评分并列,程序会默认维持当前列表靠前的接口(即 WAN1)。

此外,固件还保留了普通的 IPv4 源 / 目的地址对绑定机制(IP-Pair Binding)。同一台设备持续访问同一个目标 IP 时,不同端口的新建连接可能会自动复用最初的出口。因此,即便在 UI 界面上关掉了「特殊应用选路」,也并不等于完全关闭了底层地址对绑定的干扰。

第二个坑:硬件加速导致流量统计失真

为了实时观察流量分配,笔者在局域网的 Linux 服务器上运行了自动化采集脚本,定期登录主路由读取两个 WAN 口的累计传输字节数。

在路由器系统中,两个 WAN 接口分别呈现为:

1
2
WAN1 → eth0.8
WAN2 → eth0.9.14

脚本最初读取的是 /proc/net/dev,后来改用 /sys/class/net/<接口>/statistics/ 下的 rx_bytes 与 tx_bytes。通过计算每分钟的字节增量来评估流量分布。

然而,采集到的数据却显示 WAN1 始终占据绝对主导,部分时间窗口内的流量比例甚至接近惊人的 98:2。更诡异的是,即便是将两台 CPE 的物理网线对调,「偏向 WAN1」的现象依然纹丝不动。这让笔者一度怀疑固件内部是否存在硬编码的接口偏好。

为了进一步确认,笔者尝试查看 conntrack 和 iptables 的规则计数,却发现大量高流量连接的包计数器卡在极小的值(如 16 个包)就不再增长了。分析 hnat 硬件加速启动脚本后发现,其设置的参数为 mode kernel 与 count 16。这意味着,报文在经过软件层处理 16 个包后,后续流量就会直接交由硬件 ASIC/Fast Forwarding 芯片进行加速,不再经过常规的 Linux 网络协议栈软件计数器。

这就引发了一个根本性问题:监控程序在底层拿到的统计数据到底代表什么?

为什么 WAN1 和 WAN2 的计数口径完全不同?

分析实机配置与 RTL8198D/RTL8372 交换芯片的初始化脚本后,真相终于水落石出——WAN1 与 WAN2 走的是完全不同的硬件架构路径:

接口硬件架构路径Linux 系统呈现方式
WAN1RTL8198D 内部 MAC 端口 6,直连独立外部 WAN 口 PHYeth0.8,走原生驱动,包含硬件 MIB 统计
WAN2挂载在 RTL8372 外接交换芯片 4 号口,通过 VLAN 14 划分为 WANeth0.9.14,属于纯虚拟 VLAN 接口

WAN2 采用 VLAN 划分为路由器内部接口隔离的标准做法,外部 CPE 依然使用的是普通以太网连接。但两者的系统统计函数实现存在严重不对称:

  • 驱动中的 re8670_get_stats 会调用 rtk_dev_update_stats,从硬件寄存器中直接读取物理端口的 MIB 计数(WAN1 受益于此);
  • 而 VLAN 驱动中的 vlan_dev_get_stats64 仅汇总经过 Linux 软件协议栈的流量,根本不会主动读取外接交换芯片的硬件 MIB 计数(WAN2 受限于此)。

在这台路由器的架构下,WAN1 的 Linux 接口字节数已经涵盖了硬件加速流量,而 WAN2 的 VLAN 接口字节数却漏掉了绝大部分硬件转发流量。因此,采集脚本得出的「偏向 WAN1」并不是算法分配不均,而是一直在拿一个「包含硬件流量」的完整计数去对比另一个「漏掉硬件流量」的残缺计数。

修正方案:改用物理端口 MIB 统计

弄清原委后,笔者清除了基于旧口径收集的数据,并在局域网采集脚本中引入了物理端口计数与硬件加速(FC)计数的交叉对账机制,每分钟同步采集三类指标:

  1. 物理端口 MIB(主统计口径):直接读取芯片底层物理 Interface 的 RX/TX 字节数,不受 CPU 软件转发或 HNAT 硬件加速影响。
  2. Realtek FC 硬件 / 软件计数(对账辅助):读取 Realtek Fast Forwarding 引擎的 HW/SW MIB,用于观察硬件加速路径的覆盖率。
  3. Linux 接口计数(诊断对照):继续记录 /proc/net/dev,用于直观评估软件层的漏计程度。

在 TL-R5005PE-AC 上,读取物理端口 MIB 的底层命令如下:

1
2
3
4
5
# WAN1(内部 MAC 端口 6,连接独立外部 WAN 口)
diag mib dump counter port 6

# WAN2(外接 RTL8372 交换芯片物理端口 4)
swconfig dev switch1 port 4 get mib

由于 WAN2 返回的数据将高位与低位 32 位字节数拆开了,在 Python 采集脚本中需要通过位移拼接为 64 位无符号整数:

1
2
rx_bytes = ifInOctets_H * (1 << 32) + ifInOctets_L
tx_bytes = ifOutOctets_H * (1 << 32) + ifOutOctets_L

注:上述命令仅做读取操作,不会重置硬件计数器。由于不同硬件平台的端口映射关系差异极大,切勿直接将端口号套用到其他型号的路由器上。

带宽均衡自身也使用了受硬件漏计影响的数据

这个问题不只影响外部监控,还会影响固件自己的带宽均衡决策。 前文评分公式中的 delta_rx_bytes 和 delta_tx_bytes,来自 /usr/sbin/load_balance 的 get_iface_rx() 与 get_iface_tx(),读取路径正是我们已经发现存在漏计的 sysfs 接口统计:

1
2
3
4
5
# get_iface_tx() 中的读取
local kernel_tx=$(cat /sys/class/net/$dev/statistics/tx_bytes)

# get_iface_rx() 中的读取
local kernel_rx=$(cat /sys/class/net/$dev/statistics/rx_bytes)

脚本并非完全没有考虑硬件加速。它有 hw_nat_detect() 和额外硬件字节补偿,但平台检测只识别 /sys/hw_nat/dev_statics,随后在 case "$HW_NAT_TYPE" 的 mtk) 分支中读取 MTK 的硬件统计。这台设备使用的是 Realtek,所分析脚本中没有对应的 Realtek 额外补偿分支。

Realtek 原生驱动能够为某些接口提供硬件计数,然而,实机核对已经确认,WAN1 的 sysfs 字节包含硬件端口计数,而 WAN2 的 VLAN sysfs 字节漏掉了大量硬件流量。 sysfs 虽然也调用了 rtk_dev_update_stats,但更新函数受设备类型和标志限制。带宽均衡读取的是这组有偏差的输入,并没有改用我们后来采用的物理端口 MIB。

在两条线路配置带宽相同、其他条件相近时,漏计会压低 WAN2 的负载评分,使算法更容易认为 WAN2 空闲,把更多到达选择器、尚未标记的连接分配给 WAN2。与此同时,外部监控继续低估 WAN2,于是即使它的真实流量已经增多,监控仍可能显示「WAN1 占绝大多数」。路由器便在这样混乱的情况下做出了可能不完全合理的选路决策。

最终配置方案与连接均衡

考虑到固件自己的带宽均衡决策也存在问题,为了使选路逻辑简单可靠,笔者最终将路由器切换至连接均衡(Connection Balance)模式。实机校验表明,此时 IPv4 流量直接由 default_balance 内核模块接管。

最终保留的优化配置如下:

  1. 启用连接均衡:两条 WAN 线路共同参与 default_balance 轮询分配;
  2. 彻底禁用 ISP 选路:取消手动指定运营商,确保 isp_route 链不会截流;
  3. 关闭带宽均衡:避免 WAN2 硬件流量漏计导致的负载评分偏差;
  4. 保留 WAN 在线检测:确保某条线路异常断开时能自动踢出候选池;
  5. 保持 HNAT 硬件加速:提升转发性能,改用物理端口 MIB 建立监控体系。

default_balance 模块的核心分配逻辑非常精简:

1
2
mark = (counter % iface_num) + 0x401;
counter++;

算法通过对递增计数器取模,将未标记的新建连接轮流映射到可用的 WAN 口上。从概率论角度来看,只要长连接与大流量业务不是系统性地总是落在某一个奇 / 偶数序号上,随着新建连接基数的增大,两侧分摊的总流量在大数定律作用下会自然趋于平稳。

修正后的实际运行数据

修正为采用连接均衡后,共观察 76 小时 15 分钟,按物理端口计数统计如下(GB 为十进制):

指标WAN1WAN2
下载 RX15.70 GB10.23 GB
上传 TX12.85 GB12.97 GB
合计28.55 GB23.20 GB
份额55.17%44.83%
Linux 接口计数28.55 GB0.98 GB

累计的流量比例约为 55:45,上传接近均分,差额主要来自下载。最后 24 小时为 52.71%:47.29%,最后一小时为 50.73%:49.27%,已接近五五开。

WAN2 实际传输了 23.20 GB,Linux 接口却只记录了 0.98 GB,说明旧口径仍然严重漏计。

期间物理计数没有清零。FC 软件计数曾重置,分段对账后,两边的 FC 覆盖率约为物理计数的 96%。

总结与教训

针对双 WAN 负载均衡的排查,本次踩坑过程留下了两点非常值得借鉴的经验:

  1. 慎用运营商手动指定:对于纯流量卡叠加或双同质宽带场景,手动填写「电信 / 联通」往往会隐式激活前置的 ISP 目标地址选路(ISP Route),导致大流量连接在进入均衡器之前就被固定了出口。
  2. 警惕硬件加速对网络监控的干扰:HNAT 等硬件加速技术在大幅降低 CPU 占用率的同时,也会破坏 Linux 协议栈原有的统计链路。当不同 WAN 口在路由器内部处于不同的物理 / 逻辑总线(如原生 MAC 与外接 Switch VLAN)时,系统呈现的接口字节数可能存在巨大的口径偏差。

在怀疑负载均衡算法失效之前,务必优先确认你的监控指标是否真实反映了底层的物理流量。确保测量工具的准确性,往往比盲目修改算法更重要。