文中的地址、域名和设备唯一标识均已匿名化,监控指标名称和原始量级保留。不能用这些数据推算任何具体网络。
1. 现象与结论#
办公终端先经过企业路由器,再从两条 WAN 选择出口。WAN1 后面是 pfSense,WAN2 后面是负责 PPPoE 和 NAT 的 FortiGate。两条线路分别拨号,各自建立上游映射。
flowchart LR
C[办公终端与 VLAN] --> R[企业双 WAN 路由器]
R -->|WAN1| P[pfSense]
R -->|WAN2| F[FortiGate]
P --> C1[运营商 CGNAT 出口 A]
F --> C2[运营商 CGNAT 出口 B]
C1 --> I[Internet]
C2 --> I
D[内部 DNS] --> P
K[内部应用] --> P
一台固定走 WAN1 的 Linux 终端连续请求同一站点,10 次里只有 5 次成功。DNS 始终在 1 至 2 毫秒内返回,失败都发生在 TCP connect 阶段。相同时间,pfSense 本机 10 次请求全部成功,PPPoE 网关丢包为 0%。
失败连接大多停在 SYN_SENT。我为这件事加了四个指标:
| 指标 | 含义 |
|---|---|
W4 | 当前经过 WAN NAT 的 IPv4 逻辑会话 |
A4 | 最近 120 秒内计数器有变化的会话 |
C4 | 当前采样周期新出现的会话 |
F4_ratio | 失败握手状态数除以 W4 |
现场用 F4_ratio 观察失败握手状态在当前 WAN IPv4 会话中的占比。分母是 W4,不是 SYN 尝试总数,不能把它读成 SYN 丢包率。早期几次高峰里,W4 接近一万条时 F4_ratio 会迅速上升,终端也同时出现连接超时;后续样本没有重复出一个固定门槛,第 6 节再展开。
| 指标 | 调整前 | 稳定后 |
|---|---|---|
| pfSense WAN IPv4 会话 | 约 12,000 | 约 6,000 |
F4_ratio | 17% 至 37% | 约 1.7% 至 2.5% |
| TP-Link 出口会话分布 | 约 90:10 | 约 50:50 |
| 长驻 WAN TCP DNS 会话 | 约 1,000 | 0 |
| 数据库 TIME_WAIT 峰值 | 约 3,500 | 0 |
| WAN2 网关 CPU idle | 一度 1% | 40% 至 66% |
PF 本地状态数不能当成运营商 CGNAT 映射数。两侧超时不同;PPPoE 重拨会使运营商侧旧映射失效,而 pfSense 本地状态可能继续保留。运营商没有给出真实配额,我没法直接证实 CGNAT 已耗尽,这个判断来自几轮高峰期同步采到的数据。
2. 排除的几个假线索#
PF 状态表没有满#
PF 状态一度从两万多涨到四万多,当时配置的 state limit 约为 161 万,峰值约占 2.5%。内存、mbuf、socket 和哈希表指标未见压力。
PF 状态数与运营商 CGNAT 映射数没有一一对应关系。提高 state limit 只增加本地容量,不能提高上游配额。
RRD 图表被多个采集器写坏#
目标地址图曾在一分钟内从几百跳到几千。我最初以为是 CDN 突发,检查进程后发现系统同时运行了八个 RRD 采集器。七个孤儿进程的 PPID=1,它们与正常进程共同写同一个临时文件,没有单例锁。
处理时没有重启服务。我逐个核对 PID、父进程和完整命令,只停止七个孤儿。保留的采集器继续每分钟更新 RRD;文件修改时间连续十五分钟稳定推进后,图表才重新用于容量判断。
以后遇到同类尖峰,我会同时核对采集器数量和原始状态;两组数据一致后才调整防火墙。
一轮 WAN2 CPU 尖峰来自残留抓包进程#
完成分流后,FortiGate CPU idle 一度只剩 1%,业务请求却很正常:约四千条会话,内存和吞吐都没到瓶颈。进程采样是这样的:
cmdbsvr 28% 至 76%
miglogd 18% 至 25%
snifferd 合计约 30% 至 40%
设备里残留了十个 snifferd。它们是验证 WAN 分流时启动的无限抓包;本地 SSH 超时后,远端抓包没有跟着退出。
我逐个核对 PID 和完整过滤条件,确认不是业务进程后才发送 SIGTERM。每个 snifferd 的配对 newcli 会跟着退出:
diagnose sys kill 15 <PID>
fnsysctl ps | grep snifferd
清理后会话数没有明显变化,CPU idle 回到 40% 至 66%。新建 PPPoE 会话仍显示 no_ofld_reason: non-npu-intf。降低日志量只能稍微减轻 CPU 占用,PPPoE 虚拟接口依旧无法使用 NP4Lite。
这次清理只能解释当时那一轮尖峰。后面没有 snifferd 时,cmdbsvr 仍出现过高占用;get system performance status 本身也会启动 CLI 查询,放大单核设备上的 CPU 读数。WAN2 采集后来改成每 5 分钟只执行一次 diagnose sys session stat,不再抓 CPU、内存和吞吐。
后面再做远端抓包,我会限制包数,结束后顺手查一次进程:
diagnose sniffer packet any 'host 192.0.2.10' 2 1000 l
fnsysctl ps | grep snifferd
3. 减少应用产生的连接#
DNS 转发#
Technitium DNS 集群最初强制使用 TCP、并发数为 10,同时查询四个上游。forwarderTimeout=10000ms、forwarderRetries=3、并发转发开启,DNSSEC 验证关闭。再叠加 pfSense 的 24 小时 tcp.established,WAN 侧慢慢攒出约 1,035 条 TCP DNS 状态。
防火墙 timeout 尝试失败#
我先给 pfSense 的 DNS 规则设置短 timeout,随后发现选错了规则:allow2dns 只匹配客户端访问内部 DNS,不匹配 DNS 服务器访问公网。只改 LAN 入方向也不会缩短 PPPoE 出方向状态。
补上的 WAN 浮动规则又漏了 Disable Reply-To,pfSense 自动生成:
reply-to (pppoe0 gateway)
新的 TCP DNS 查询开始卡在 SYN_SENT:CLOSED,只有一包 SYN。规则随即回滚,没有清状态,也没有继续调整全局 timeout。
协议切换,并发 10 降到 2#
Technitium 集群设置会同时作用于两个节点,不能做单节点 canary。变更前分别导出两个节点的配置备份,并完成 16 次无副作用查询:两个节点、四个上游,每个上游测试普通 A 记录和根 DNSKEY,全部返回 NoError。
这轮只改两项:
forwarderProtocol: Tcp → Udp
forwarderConcurrency: 10 → 2
这里的 2 只是过渡值。四分钟后,WAN TCP/53 状态从 1,035 降到 854,TIME_WAIT 从 231 降到 129,同时出现约 330 条 UDP/53 状态。TCP 状态后来降到 328,再自然归零;我没有执行 pfctl -k。
并发 2 降到 1#
改用 UDP 后,映射不再保留那么久。把并发从 2 降到 1,减少的是同一时刻发往上游的查询。DNS 公网状态从约 2,399 降到 1,390,70 秒后稳定在 1,321,减少 44.9%;普通 UDP 查询和 TCP fallback 都返回 NoError。
少一条并行上游路径后,缓存未命中的尾延迟可能上升。现场没看出延迟变化,我就把并发保留在 1。
上游从 4 个减到 2 个#
我又删掉两个低优先级上游,会话却从约 1,286 变成 1,337,完全落在正常波动里。这一步没省下什么。两个上游最后还是保留了,只为故障冗余。
| 参数 | 初始值 | 最终值 | 结果 |
|---|---|---|---|
forwarderProtocol | TCP | UDP | 长驻 TCP 映射降到 0 |
forwarderConcurrency | 10 | 1 | DNS 会话下降约 45% |
| 上游数量 | 4 | 2 | 对总会话影响很小 |
forwarderTimeout | 10000ms | 10000ms | 未调整 |
forwarderRetries | 3 | 3 | 未调整 |
全局 UDP timeout 没有调整,避免影响视频会议、VoIP、QUIC 和游戏;也没有强制放大 CDN 域名 TTL。UDP 仍占用 CGNAT 映射,但空闲状态不会像 TCP established 那样在 pfSense 里保留一天。
数据库连接池#
另一个大尖峰来自内部 MySQL。后来确认这些连接来自 Kubernetes 工作负载,连接池配置如下:
DB_MAX_OPEN=100
DB_MAX_IDLE=10
ConnMaxLifetime=5m
ConnMaxIdleTime=10m
连接池回落时会关闭超过 10 条的空闲连接;超过 5 分钟生命周期的连接在归还连接池后也会陆续关闭。现场一度出现约 3,500 条 TIME_WAIT。
修改后,DB_MAX_IDLE 为 25,ConnMaxLifetime 默认为 15 分钟,ConnMaxIdleTime 保持 10 分钟。稳定采样中,连接维持在 20 至 26 条 ESTABLISHED,TIME_WAIT 降到 0。
这段流量发生在内部 VLAN,不经过 PPPoE。它应该修,但不会占用运营商 CGNAT 配额。
4. pfSense 的两个 TCP timeout 调整#
| 参数 | 原值 | 新值 | 原因 |
|---|---|---|---|
tcp.established | 86,400 秒 | 43,200 秒 | 回收连续空闲超过 12 小时的状态,仍高于 RFC 5382 的 7,440 秒下限 |
tcp.closing | 900 秒 | 450 秒 | 更快回收 FIN_WAIT/CLOSING,仍高于 240 秒量级的兼容性边界 |
tcp.established 调整后,约三千多条连续空闲超过 12 小时的状态被回收。它减少的是 pfSense 本地历史状态,不会通知运营商删除 CGNAT 映射。
UDP、adaptive timeout、state limit 和静态端口 NAT 均保持不变。全程没有清空状态表。
5. 双 WAN 为什么没有按权重分流#
隐藏的 ISP 选路覆盖了负载均衡#
企业路由器已经启用双 WAN,界面权重为 4:6,但二十秒新 SYN 抓包显示,WAN1 拿走了接近九成连接。切换连接均衡并把权重改成 3:7 后,结果仍然接近 90:10。

我又让两台终端访问一组从未使用过的测试地址,免得旧连接干扰判断。一台始终走 WAN1,另一台始终走 WAN2。“特殊应用程序选路”确实在按主机保持出口,改权重不会迁走已经固定的主机。
接着查 传输控制 → 路由设置 → ISP 选路,才看到一条历史规则 dianxing。它把某运营商地址组强制指向 WAN1,而两条宽带恰好来自同一运营商。关掉这条规则后,新连接先从 90:10 改善到约 75:25。
这条 dianxing 是现场以前加的,不是 TP-Link 默认配置。其他设备没有这条规则就直接跳过,别照着文章新建。
| TP-Link 设置 | 最终状态 | 原因 |
|---|---|---|
| 流量均衡 | 开启 | 按连接数参与双 WAN 分流 |
| 智能均衡 | 关闭 | 现场瓶颈是会话数,不是带宽占用 |
| 特殊应用程序选路 | 开启 | 保持视频会议、SIP、FTP 等相关连接使用同一出口 |
dianxing ISP 选路 | 关闭 | 两条线路来自同一 ISP,规则会把大部分目标压到 WAN1 |
截图记录的是测试阶段,“特殊应用程序选路”当时曾临时关闭;生产恢复后重新开启。
先建内部目的地址组#
这台 TP-Link 的 PBR 比普通静态路由更早参与选路,生效接口又只有 WAN1 和 WAN2。我不敢直接写“源 VLAN → ANY → WAN2”,那样可能连内部服务器流量也一起送走。
我先从 系统路由 和 静态路由 页面抄出所有必须经 WAN1 到达的网络:
WAN1_TRANSIT 10.0.0.0/24
SERVER_NETS_A 10.20.0.0/16
SERVER_NETS_B 10.30.0.0/16
REMOTE_NETS 10.40.0.0/16
OVERLAY_NETS 172.20.0.0/16
菜单路径是 对象管理 → 地址管理 → 地址。每段网络单独建对象,再到 对象管理 → 地址管理 → 地址组,组合成 INTERNAL_NETS。

策略路由里选不到 LAN/VLAN 接口,本地直连 VLAN 会不会被影响,配置页给不了答案。我只敢先放一台 canary,等真实流量验证通过再扩。
用 /32 canary 验证两条规则#
我先在 对象管理 → 地址管理 建一个低连接终端:
CANARY_HOST = 192.168.50.171/32
然后打开 传输控制 → 路由设置 → 策略路由,按下面的顺序建两条规则:
| 字段 | 内部流量规则 | 公网流量规则 |
|---|---|---|
| 规则名称 | CANARY_INTERNAL_WAN1 | CANARY_PUBLIC_WAN2 |
| 服务类型 | ALL | ALL |
| 源地址 | CANARY_HOST | CANARY_HOST |
| 目的地址 | INTERNAL_NETS | IPGROUP_ANY |
| 生效接口 | WAN1 | WAN2 |
| 生效时间 | Any | Any |
| 强制 | 是 | 否 |
| 顺序 | 1 | 2 |
两条规则的顺序不能换。公网规则不要勾“强制”。设备还要启用 WAN2 在线检测并配置备用出口;WAN2 被判定离线后,新连接才会回退 WAN1。
保存后我没有清状态,旧连接继续走原出口。canary 检查四件事:
- 公网 IP 与已知 WAN2 测试机一致;
- 内部 DNS 和内部服务端口正常;
- 从其他 VLAN 新建连接到 canary 能正常返回;
- WAN2 断开后,新公网连接能够回退 WAN1。
第三项是我最担心的。它用真实流量确认 IPGROUP_ANY 没有破坏本地直连 VLAN;这项没过,我不会把整段 VLAN 放进来。
从单机扩到地址组#
canary 通过后,我没有给每个网段再复制一套规则,而是建了 WAN2_PBR_SOURCES。两条生产规则共用这个源地址组:
SRC_INTERNAL_WAN1: WAN2_PBR_SOURCES → INTERNAL_NETS → WAN1,强制=是
SRC_PUBLIC_WAN2: WAN2_PBR_SOURCES → IPGROUP_ANY → WAN2,强制=否


每次扩容只往 WAN2_PBR_SOURCES 加一个地址对象。我会先确认内部规则已经覆盖它,再放公网流量去 WAN2,避免内部豁免尚未生效。
flowchart TD
S[WAN2_PBR_SOURCES 新连接] --> R1{内部目的地址?}
R1 -->|是| W1[WAN1 / 强制]
R1 -->|否| R2[ANY / WAN2 / 不强制]
R2 --> H{在线检测判定 WAN2 正常?}
H -->|是| W2[从 WAN2 建立新会话]
H -->|否| W1
L[本地直连 VLAN] --> D[设备直连路由
canary 已验证]
当时 WAN1/WAN2 约为 4,726/2,482,要接近均衡需迁移约 1,122 条。选择一个约 1,083 条连接的地址范围后,实际稳定在 3,977/3,914。已有连接不会迁移,新连接才进入 WAN2。
每次加完先看十五分钟,最后至少覆盖一个完整高峰。我盯着 F4_ratio、WAN2 CPU 和会话数,并用真实终端测试内部访问与故障回退。
要回滚也不用删规则或清状态。从 WAN2_PBR_SOURCES 移除刚加入的对象即可;新连接恢复原来的均衡策略,旧连接自己结束。
6. 这套网络接下来怎么扩#
先把几个数字分开#
现场高峰有 210 台左右的终端,TP-Link 显示 5,506 条终端连接,最高的一台是 379/400。pfSense 上的 W4 则是经过 WAN NAT 的 IPv4 逻辑会话。三个数字看起来都在说“连接数”,统计对象并不相同,更不能直接拿来推算运营商的映射表。
故障后,单终端默认上限从 300 收紧到 200。它主要用来拦异常下载、P2P 和失控程序。真有终端反复触顶,就查这台机器的进程和目的端口。如果大家都在正常增长,继续往下压单机上限只会让正常用户更早碰壁。
后来采样多了,连“5000 条左右开始卡”也站不住了。Grafana 记录的本地 WAN 会话有时跑到 6000 多条还正常,有时 3000 多、4000 多条就开始连不上;这些数不是运营商 CGNAT 映射数。卡顿时 F4_ratio 往往会升高,但单看本地会话总数猜不出下一次什么时候出问题。

截图这一刻,WAN1/WAN2 分别有 5,019 和 6,770 条本地会话,握手失败率为 3.7% 和 4.6%;当天峰值则到过 18.26% 和 14.79%。
这种漂移可能与 PPPoE 重拨后落到不同运营商节点有关,也可能只是当时共享地址池的负载不同。现场没有运营商侧数据,这两种解释都还不能确认。
装机师傅给的说法是:普通家庭宽带约 4096 条,有公网 IPv4 后约 6 万条。他没有说明这是 CGNAT 映射数、接入策略还是设备会话容量,所以我先把它当作口头经验记录。4096 与几次卡顿的量级接近,却解释不了 6000 多条仍然正常的情况。
按 250 台设备、每台平均 40 条活动连接算,平时就可能超过 10,000 条,系统更新时还会再冲一截。运营商到底给了多少配额,还是得从晚高峰复测里慢慢确认。
公网 IPv4 怎么验#
下一步准备向运营商申请动态公网 IPv4。换好后先看 WAN 地址,确认它不在 100.64.0.0/10 或 RFC 1918 私网范围;再从外部查询出口 IPv4,看结果是否与 WAN 地址一致。
这两项对得上,只能说明 CGNAT 已经绕开。能扛多少会话,我现在没有数据,还得等一个完整高峰,看新连接成功率和 F4_ratio。出口路由器、防火墙和运营商接入侧也都有自己的上限。
套餐从 500M 升到 1G 只会增加带宽。拨号账号的会话资源不变,卡顿照样可能回来。
现场已经更换过一次宽带接入设备,但具体更换部件没有记清,暂时不把这项变化算进结论。新配置要在重启后重新确认;公网 IPv4 还在申请中,地址下发前先做 CGNAT 线路的晚高峰基线,下来后再用相同时间段对照。
公网 IPv4 还没下来时#
DNS、数据库连接池和 TP-Link 分流都已经改过了,单终端上限也从 300 收紧到 200。公网 IPv4 下来之前,先让这些改动跑过几个高峰,再看还剩多少失败连接。
真要继续加容量,我会接另一条独立 PPPoE 或换一家运营商。新的拨号通常会让运营商侧重新分配映射。应用也得继续查,HTTP 客户端、数据库驱动和代理程序能复用连接的地方,少开一条就是少占一条。
提高 PF state limit 或 Linux 的 nf_conntrack_max 只会扩大本地表。清掉 pfSense 状态也不会通知运营商立即释放映射。我曾看到把全局 tcp.established 直接降到 600 秒的建议,差点照搬;RFC 5382 给出的下限是 7,440 秒,线上会议、WebSocket 和低频长连接都可能被误伤。现场最后保留 43,200 秒,只把 tcp.closing 从 900 秒降到 450 秒。
IPv6 先测再分流#
原生 IPv6 流量不占 IPv4 CGNAT 映射,双栈站点可以分走一部分连接。先别急着全开:实际路径、PMTU 和客户端的地址族选择都可能出问题。Happy Eyeballs 也没有固定的 300 毫秒等待值,不同实现会有差异。
mihomo 里如果已经有独立的 IPv4、IPv6 出口节点,可以让 url-test 定期访问同一个探测地址,按延迟选择。只有一个出口时,加策略组并不会改善路由。我也不会一次把所有站点切到 IPv6,先从普通网页开始,视频会议和业务域名单独验证。
什么时候停手#
WAN1、WAN2 和 TP-Link 的数据已经进了 Grafana,Prometheus 负责告警。我平时看会话数、建连速率、失败握手比例、出口资源和单终端连接数。

这张图只记录 WAN1 吞吐和两条线路的建连速率,不拿其中某一条曲线单独解释卡顿。判断仍以真实终端连接测试、F4_ratio 和本地 WAN 会话变化为主。
高峰期 F4_ratio 能持续低于 5%,终端请求成功率超过 95%,出口设备也没有资源压力,就先停在这里。没必要为了图表上的 50:50 继续调权重。如果仍然反复越线,再申请公网 IPv4 或增加独立拨号,不再从全局 timeout 里挤空间。
7. 参考资料#
- RFC 5382: NAT Behavioral Requirements for TCP
- RFC 4787: NAT Behavioral Requirements for UDP
- RFC 7766: DNS Transport over TCP
- RFC 7857: Updates to NAT Behavioral Requirements
- RFC 8305: Happy Eyeballs Version 2
- RFC 8201: Path MTU Discovery for IP version 6
- pfSense Documentation: Viewing Firewall States
- Technitium DNS Server Help
