引言
在多条 PPPoE 接入线路的环境下,PCC(Per Connection Classifier) 是一种常用的负载均衡方案。它能够根据 IP 地址、端口等信息将连接均匀分布到多条线路上,实现带宽叠加和高可用。
然而,当某条 PPPoE 线路意外掉线时,静态 PCC 规则仍然会将新连接分配给已离线的接口,导致连接超时、网页打不开等问题。虽然 RouterOS 支持通过脚本监控接口状态,但手动调整 PCC 规则既繁琐又容易出错。
本文介绍一个 动态 PCC 调整脚本,它能自动检测所有 PPPoE 接口的在线状态,根据在线接口数量重新计算 PCC 的“分子/分母”,并启用或禁用对应的防火墙标记规则。通过定时运行此脚本,您的负载均衡配置将始终保持与当前在线线路数一致,有效提升网络的可靠性和用户体验。
脚本功能概览
- 统计指定范围内的 PPPoE 接口(如
pppoe-out1至pppoe-out20)的运行状态。 - 如果没有任何接口在线,禁用所有相关的 PCC 规则,防止流量走无效线路。
- 如果部分接口在线,则按接口编号顺序为每个在线接口分配一个递增的分子值(
0 ~ N-1),分母为在线接口总数N,并启用对应的防火墙标记规则。 - 离线的接口,其对应的所有规则会被禁用,避免残留规则干扰流量。
使用环境要求
- MikroTik RouterOS(版本需支持脚本和 PCC)。
- 已配置多个 PPPoE 客户端接口(例如
pppoe-out1、pppoe-out2……)。 - 已根据静态 PCC 思路创建了基本的防火墙标记(mangle)规则(下文会给出模板)。
- 有基本的 RouterOS 命令行或 WinBox 操作经验。
脚本内容
# 动态调整 PCC 负载均衡(基于 PPPoE 接口状态)
# 检测所有 pppoe-out 接口,根据在线数量重新分配 PCC 分子/分母
# 通过 scheduler 定期执行(例如每分钟一次)
:local N 0
# 第一次循环统计在线接口数
:for i from=1 to=20 do={
:local intName "pppoe-out$i"
:local status [/interface pppoe-client get [find name=$intName] running]
:if ($status = true) do={ :set N ($N + 1) }
}
:if ($N = 0) do={
# 所有接口均掉线,禁用全部 PCC 规则
:for i from=1 to=20 do={
:local intName "pppoe-out$i"
# 禁用 prerouting mark-connection
:local connRule [/ip firewall mangle find where chain=prerouting and action=mark-connection and new-connection-mark="C$i"]
:if ([:len $connRule] > 0) do={
:local currDisabled [/ip firewall mangle get $connRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $connRule disabled=yes
}
}
# 禁用 prerouting mark-routing
:local routeRule [/ip firewall mangle find where chain=prerouting and action=mark-routing and new-routing-mark="R$i"]
:if ([:len $routeRule] > 0) do={
:local currDisabled [/ip firewall mangle get $routeRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $routeRule disabled=yes
}
}
# 禁用 input mark-connection
:local inRule [/ip firewall mangle find where chain=intput and action=mark-connection and in-interface=$intName]
:if ([:len $inRule] > 0) do={
:local currDisabled [/ip firewall mangle get $inRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $inRule disabled=yes
}
}
# 禁用 output mark-routing
:local outRule [/ip firewall mangle find where chain=output and action=mark-routing and connection-mark="C$i"]
:if ([:len $outRule] > 0) do={
:local currDisabled [/ip firewall mangle get $outRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $outRule disabled=yes
}
}
}
:log info "所有 PPPoE 接口离线,PCC 规则已禁用"
} else={
:local order 0
# 第二次循环按接口编号顺序分配分子(0 ~ N-1)
:for i from=1 to=20 do={
:local intName "pppoe-out$i"
:local status [/interface pppoe-client get [find name=$intName] running]
:if ($status = true) do={
:local classifier "both-addresses-and-ports:$N/$order"
# 处理 prerouting 链的 mark-connection 规则(设置分类器和启用)
:local connRule [/ip firewall mangle find where chain=prerouting and action=mark-connection and new-connection-mark="C$i"]
:if ([:len $connRule] > 0) do={
:local currDisabled [/ip firewall mangle get $connRule disabled]
:local currClassifier [/ip firewall mangle get $connRule per-connection-classifier]
:if ($currDisabled != no || $currClassifier != $classifier) do={
/ip firewall mangle set $connRule disabled=no per-connection-classifier=$classifier
}
}
# 处理 prerouting 链的 mark-routing 规则(只需启用)
:local routeRule [/ip firewall mangle find where chain=prerouting and action=mark-routing and new-routing-mark="R$i"]
:if ([:len $routeRule] > 0) do={
:local currDisabled [/ip firewall mangle get $routeRule disabled]
:if ($currDisabled != no) do={
/ip firewall mangle set $routeRule disabled=no
}
}
# 处理 input 链的 mark-connection 规则(只需启用)
:local inRule [/ip firewall mangle find where chain=intput and action=mark-connection and in-interface=$intName]
:if ([:len $inRule] > 0) do={
:local currDisabled [/ip firewall mangle get $inRule disabled]
:if ($currDisabled != no) do={
/ip firewall mangle set $inRule disabled=no
}
}
# 处理 output 链的 mark-routing 规则(只需启用)
:local outRule [/ip firewall mangle find where chain=output and action=mark-routing and connection-mark="C$i"]
:if ([:len $outRule] > 0) do={
:local currDisabled [/ip firewall mangle get $outRule disabled]
:if ($currDisabled != no) do={
/ip firewall mangle set $outRule disabled=no
}
}
:set order ($order + 1)
} else={
# 接口离线,禁用所有相关规则(同样加入状态判断)
:local intName "pppoe-out$i"
:local connRule [/ip firewall mangle find where chain=prerouting and action=mark-connection and new-connection-mark="C$i"]
:if ([:len $connRule] > 0) do={
:local currDisabled [/ip firewall mangle get $connRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $connRule disabled=yes
}
}
:local routeRule [/ip firewall mangle find where chain=prerouting and action=mark-routing and new-routing-mark="R$i"]
:if ([:len $routeRule] > 0) do={
:local currDisabled [/ip firewall mangle get $routeRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $routeRule disabled=yes
}
}
:local inRule [/ip firewall mangle find where chain=intput and action=mark-connection and in-interface=$intName]
:if ([:len $inRule] > 0) do={
:local currDisabled [/ip firewall mangle get $inRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $inRule disabled=yes
}
}
:local outRule [/ip firewall mangle find where chain=output and action=mark-routing and connection-mark="C$i"]
:if ([:len $outRule] > 0) do={
:local currDisabled [/ip firewall mangle get $outRule disabled]
:if ($currDisabled != yes) do={
/ip firewall mangle set $outRule disabled=yes
}
}
}
}
:log info "PCC 已调整:$N 个接口在线,分子 0~$($N-1)"
}脚本详解
- 统计在线接口数
脚本第一个循环遍历pppoe-out1到pppoe-out20,通过running属性判断接口是否在线,得到在线总数N。 - 全离线处理
若N=0,说明所有线路断开,则将所有与 PCC 相关的规则禁用,并记录日志。这样可以避免流量因无可用出口而失败,后续由默认路由或其他机制处理。 部分在线处理
- 重置
order计数器为 0。 再次遍历接口,对每个在线接口:
- 构建 PCC 分类器参数
both-addresses-and-ports:$N/$order。 - 查找并更新 prerouting 链的 mark-connection 规则,设置
per-connection-classifier并启用。 - 启用 prerouting 链的 mark-routing 规则(无需分类器,依赖连接标记)。
- 启用 input 链的 mark-connection 规则(注意原脚本链名为
intput,常见为input,使用时请确认)。 - 启用 output 链的 mark-routing 规则。
- 计数器
order加 1。
- 构建 PCC 分类器参数
- 对离线接口,禁用其对应的所有四条规则。
- 重置
- 日志输出
每次调整后都会在系统日志中记录当前在线接口数和分配范围,便于排障。
如何修改脚本以适应自己的环境
1. 接口命名和数量范围
脚本默认处理 pppoe-out1 到 pppoe-out20,如果你的接口名称不同或数量不同,请修改两处:
- 循环的起始和结束数字:
for i from=1 to=20改为你的最大编号。 - 接口名称模板:
"pppoe-out$i"改为你的实际前缀,例如"pppoe-WAN$i"、"adsl$i"。
2. 防火墙标记的名称
脚本中使用的标记名称遵循 C$i(连接标记)、R$i(路由标记),并且与接口编号绑定。如果你已有规则使用不同的命名风格(例如 conn-mark-pppoe1、route-mark-1),你需要修改脚本中对应的查找条件:
- 查找
new-connection-mark="C$i"的地方改为你的连接标记名,如"conn-mark-pppoe$i"。 - 查找
new-routing-mark="R$i"的地方改为你的路由标记名,如"route-mark-pppoe$i"。 - 查找
connection-mark="C$i"的地方(output 链)也要同步修改。
3. 防火墙链的名称
脚本中出现了 chain=intput,这很可能是笔误,标准的 RouterOS 链名为 input、output、prerouting。请根据你实际规则所在的链名进行调整。如果你在 input 链有规则,就将脚本中的 intput 改为 input。
4. PCC 分类器类型
脚本使用了 both-addresses-and-ports,即根据源/目的地址和端口进行哈希。你可以根据需求改为:
src-address:仅源地址dst-address:仅目的地址src-port:仅源端口dst-port:仅目的端口both-addresses:仅源和目的地址
修改 :local classifier "both-addresses-and-ports:$N/$order" 中的分类器名称即可。
5. 规则存在性判断
脚本在修改规则前会查找规则是否存在,若找不到则跳过。你可以根据实际需要,预先创建好所有可能用到的规则(即使接口编号不存在),脚本只负责启用/禁用和修改分类器。这样可以避免规则缺失导致脚本报错。
预置 PCC 规则模板
为了让脚本正确管理,你需要提前创建好以下四种规则(每条线一组)。以接口 pppoe-out1 为例:
# 1. 入口连接标记(prerouting)
/ip firewall mangle add chain=prerouting action=mark-connection new-connection-mark=C1 \
passthrough=yes per-connection-classifier=both-addresses-and-ports:1/0 disabled=yes
# 2. 入口路由标记(prerouting)
/ip firewall mangle add chain=prerouting action=mark-routing new-routing-mark=R1 \
connection-mark=C1 passthrough=no disabled=yes
# 3. 本地上行连接标记(input)
/ip firewall mangle add chain=input action=mark-connection new-connection-mark=C1 \
in-interface=pppoe-out1 passthrough=yes disabled=yes
# 4. 本地上行路由标记(output)
/ip firewall mangle add chain=output action=mark-routing new-routing-mark=R1 \
connection-mark=C1 passthrough=no disabled=yes- 初始状态全部设为
disabled=yes,脚本会根据接口状态自动启用。 - 对于第 1 条规则,
per-connection-classifier的分母/分子会被脚本动态修改,初始值可以随便填(如1/0)。 - 第 2、4 条规则依赖连接标记,因此无需分类器。
- 第 3 条规则用于处理路由器自身发出的连接(如 DNS 查询、更新等),确保它们也能正确走对应线路。
请为每个接口(pppoe-out1 ~ pppoe-out20)重复上述四条规则,并将标记名中的数字改为接口编号。
设置定时调度
将脚本添加到 System → Scheduler,设置间隔(例如每 60 秒执行一次):
/system scheduler add name="dynamic-pcc" interval=1m on-event="/system script run dynamic-pcc-script" start-time=startup也可以将脚本直接写在 scheduler 的 on-event 中,但为了便于维护,建议先将脚本保存为脚本(System → Scripts),然后在 scheduler 中调用。
测试与验证
- 手动断开某条 PPPoE 线路,观察脚本是否将该线路的规则禁用,其他线路的分类器是否重新调整。
- 检查日志(
/log print)查看脚本输出信息。 - 使用
ip firewall mangle print查看相关规则的disabled状态和per-connection-classifier值是否与预期一致。 - 进行实际的上网测试,确认负载均衡按在线线路数正常工作。
注意事项
- 脚本依赖于规则存在,如果某个编号的规则不存在,
find可能返回空值,set命令不会执行。建议确保所有可能用到的编号规则都已提前创建。 - 如果接口数量很多(例如 50 条),脚本循环会稍慢,但每分钟执行一次对路由器性能影响极小。
- 脚本只管理了标记规则,路由表或 NAT 规则需要单独配置(通常只需为每个接口添加一条默认网关,并设置路由标记即可)。
结语
通过上述动态 PCC 脚本,你的 RouterOS 负载均衡系统将具备自适应能力,在线路波动时自动调整,无需人工干预。只需根据实际环境调整接口范围、标记名称和链名,并创建好基础规则,就能大幅提升多线接入的稳定性。
如果你在部署过程中遇到问题,欢迎在评论区交流讨论。