香港服务器运行 Ubuntu 18.04 时,如何用 Netplan 配置多网卡“负载均衡”,把跨境链路吃干抹净

我坐在香港葵涌机房的地板上,背靠着 42U 柜门,冷风从地板砖的微缝往上钻。我们一台在线核心节点(双 10G 上联,分别接本地运营商与 CN2 国际专线)在晚高峰老是“卡脖子”:到内地用户的时延抖动、吞吐上不去,明明两条链路各有富余却总被单条打满。那一夜,我把 Ubuntu 18.04 的网络栈从头摸到脚——从 Netplan 到 policy routing,再到 ECMP 和健康检查——第二天早高峰,跨境流量终于像水银泻地一样顺畅。下面这篇,就是我当时整理出来、后来又多次实战修订的笔记。
1. 目标 & 原理心法(先建立直觉)
目标:在 Ubuntu 18.04 上同时利用两条上行(不同运营商、不同下一跳),让出境/入境流按会话或源地址分摊到两条链路,且任意一条发生抖动/中断时自动收敛,对业务侧尽量无感。
不能用的:传统 LACP Bond(两条线必须在同一二层域,且上联设备要聚合到同一 LAG,一般跨运营商做不到)。
我们选的:
- Netplan + systemd-networkd 做基础地址与静态路由;
- 策略路由(RPDB) 保证回程对称(哪个源地址出去就从哪条表回来);
- 内核 ECMP(等价多路径)做会话级负载分担;
- 可选 fwmark + iptables statistic 做更细的权重与目的地分流(比如大陆网段优先 CN2)。
- 健康检查 定期探测下一跳/远端,动态增删 nexthop,实现故障切换与回切。
一句话:策略路由保对称,ECMP 做分担,健康检查保可用。
2. 现场硬件与链路参数(有据可依)
| 类别 | 型号/参数 |
|---|---|
| 机型 | 1U 双电,Dell R6525(示例) |
| CPU | AMD EPYC 7313P(16C) |
| 内存 | 128 GB |
| 系统盘 | 960 GB SATA SSD(OS) |
| 数据盘 | 2×1.92 TB NVMe(RAID1) |
| 网卡 | 2×10GbE Intel X710(驱动 i40e) |
| OS | Ubuntu Server 18.04.6 LTS(内核 4.15系) |
| uplink#1 | 本地运营商(记作 ISP-A),/30:203.0.113.2/30,GW 203.0.113.1(示例地址) |
| uplink#2 | CN2/GIA(记作 ISP-B),/30:198.51.100.2/30,GW 198.51.100.1 |
| 典型 RTT | 港→沪:A 36–42ms、B 22–28ms;港→美西:A 140–155ms、B 180–200ms |
注:上述公网地址使用 RFC5737 示例网段,替换成你的真实分配即可。
3. 方案拓扑与表格化配置要点
逻辑拓扑:
架构
┌───────── ISP-A (HKT) ────> 海外更优
eno1 10G ───┤ GW 203.0.113.1
│
Server (HK)
│
eno2 10G ───┤ GW 198.51.100.1
└───────── ISP-B (CN2) ────> 内地更优
路由表约定:
| 路由表号 | 名称 | 默认路由 | 适用流量 |
|---|---|---|---|
| 100 | wan1 |
via 203.0.113.1 dev eno1 |
源地址 203.0.113.2/32 |
| 200 | wan2 |
via 198.51.100.1 dev eno2 |
源地址 198.51.100.2/32 |
| main | main |
ECMP:两条 nexthop 等价 | 主机自身新建会话的均衡出口 |
解释:入向到某个公网 IP 的会话,回程靠 source policy 走与该 IP 对应的表,避免回程跑错链路被对端丢包。出向新会话默认落在 main 表的 ECMP,天然按会话 hash 分流。
4. 基础系统准备(一次性)
# 升级并安装工具
sudo apt update && sudo apt -y install iproute2 iptables ipset net-tools ethtool \
netfilter-persistent iptables-persistent mtr-tiny conntrack nmap
# 打开 bbr(跨境长肥管道常见收益明显)
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
# 关闭或放宽反向路径检测(多出口必做)
cat <<'EOF' | sudo tee /etc/sysctl.d/99-multiwan.conf
net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.default.rp_filter=0
# 或者更保守的 2(loose),视环境而定
EOF
sudo sysctl -p /etc/sysctl.d/99-multiwan.conf
# 给路由表起名
echo "100 wan1" | sudo tee -a /etc/iproute2/rt_tables
echo "200 wan2" | sudo tee -a /etc/iproute2/rt_tables
网卡小优化(遇到 PPS 瓶颈时再做):
# 查看 offload
sudo ethtool -k eno1 | egrep 'gro|lro|tso|gso'
# 低延时场景可考虑关闭 LRO/GRO;大吞吐保留 TSO/GSO
sudo ethtool -K eno1 lro off gro off
sudo ethtool -K eno2 lro off gro off
5. 用 Netplan 写出“可读可控”的底座
Ubuntu 18.04 默认 Netplan + networkd。我们把地址、单表默认路由与策略规则写在这里,ECMP 多下一跳通过后置脚本加(18.04 的 Netplan 版本对 multipath 支持有限,这么做更稳)。
/etc/netplan/01-multiwan.yaml
network:
version: 2
renderer: networkd
ethernets:
eno1:
dhcp4: no
addresses: [203.0.113.2/30]
# 不在 main 表直接设 gateway4,避免抢占默认路由
routes:
- to: 0.0.0.0/0
via: 203.0.113.1
table: 100
routing-policy:
- from: 203.0.113.2/32
table: 100
priority: 100
eno2:
dhcp4: no
addresses: [198.51.100.2/30]
routes:
- to: 0.0.0.0/0
via: 198.51.100.1
table: 200
routing-policy:
- from: 198.51.100.2/32
table: 200
priority: 100
# 如需 IPv6,可按同样思路追加 addresses/routes/routing-policy(v6)
应用并检查:
sudo netplan apply --debug
ip a s eno1; ip a s eno2
ip rule show | nl
ip route show table main
ip route show table wan1
ip route show table wan2
此时 策略路由已就位:凡是发自 203.0.113.2 的回包走 wan1,发自 198.51.100.2 的走 wan2。下一步加 main 表的 ECMP。
6. 加 ECMP(等价多路径)默认路由
编一个很小的脚本,用于在网络就绪后插入/更新多下一跳默认路由:
/usr/local/sbin/route-ecmp.sh
#!/usr/bin/env bash
set -euo pipefail
# 你也可以按成本做权重,比如 A:2 B:1
A_GW="203.0.113.1"
B_GW="198.51.100.1"
A_DEV="eno1"
B_DEV="eno2"
# 删除已有默认路由,避免重复
ip route del default || true
# 添加 ECMP(按五元组哈希分会话,内核负责黏性)
ip route add default scope global \
nexthop via ${A_GW} dev ${A_DEV} weight 1 \
nexthop via ${B_GW} dev ${B_DEV} weight 1
# 再补两条“本地直达”保护,避免自己的 /30 被奇怪地走对端
ip route replace 203.0.113.0/30 dev ${A_DEV} src 203.0.113.2 table main
ip route replace 198.51.100.0/30 dev ${B_DEV} src 198.51.100.2 table main
sudo chmod +x /usr/local/sbin/route-ecmp.sh
做个 systemd 单元,随开机自动设置:
/etc/systemd/system/route-ecmp.service
[Unit]
Description=Install ECMP default route after netplan
After=network-online.target systemd-networkd-wait-online.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/route-ecmp.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now route-ecmp.service
ip route show default
这一步之后,服务器自身发起的新连接就会在两条链路间分担,且基于哈希会话保持,天然适合 HTTP/SQL/消息等典型长连接/短连接混杂的场景。
7. 进阶:按目的地/权重做更细分流(可选)
很多时候我们希望:到中国大陆网段优先走 CN2(B),海外默认走 A;或按成本 A:2、B:1 权重。Ubuntu 18.04 上简单可控的做法是:
- 用 ipset 维护一份大陆网段(可用公开 China IP 列表定期更新)
- 用 iptables mangle 给新建连接打标(fwmark),并 CONNMARK 保存
- 用 ip rule fwmark 把打标流量送到对应表(wan1/wan2)
示例:
# 1) ipset:CN 目的地集合(示例随便放几个段;生产环境请用脚本每日更新)
sudo ipset create CN hash:net || true
sudo ipset add CN 36.0.0.0/8
sudo ipset add CN 42.0.0.0/8
sudo ipset add CN 58.0.0.0/8
# ...
# 2) mangle:按目的地把 NEW 连接分流,并保存会话黏性
# 2.1 到中国大陆:标记 0x2 -> 走 wan2(CN2)
sudo iptables -t mangle -A OUTPUT -m conntrack --ctstate NEW -m set --match-set CN dst \
-j MARK --set-mark 0x2
# 2.2 其他目的地:按概率 50% 标记 0x1(走 wan1),剩下走 main(ECMP) 也行
sudo iptables -t mangle -A OUTPUT -m conntrack --ctstate NEW -m set ! --match-set CN dst \
-m statistic --mode random --probability 0.5 -j MARK --set-mark 0x1
# 保存/恢复标记(会话级)
sudo iptables -t mangle -A OUTPUT -m conntrack --ctstate NEW -j CONNMARK --save-mark
sudo iptables -t mangle -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
# 3) 策略规则:按 fwmark 把流量送入对应路由表
sudo ip rule add fwmark 0x1 table wan1 priority 90
sudo ip rule add fwmark 0x2 table wan2 priority 90
# 持久化
sudo netfilter-persistent save
有了这套后,**“到大陆走 CN2、其他走本地”**就完全落地了;并且对每个会话保持黏性,避免路由抖动。
8. 健康检查与自动切换(保证 24×7)
没有健康检查的多出口,都是纸老虎。我们做一个轻量脚本 + systemd timer,每 5 秒探测各自的下一跳与远端探针,失败则移除相应 nexthop,恢复则重新加回。
/usr/local/sbin/wan-health.sh
#!/usr/bin/env bash
set -euo pipefail
A_DEV="eno1"; A_GW="203.0.113.1"; A_PROBE="1.1.1.1"
B_DEV="eno2"; B_GW="198.51.100.1"; B_PROBE="223.5.5.5"
check() { ping -I "$1" -c 1 -W 1 "$2" >/dev/null 2>&1; }
# 0) 当前 default
CUR=$(ip route show default | tr -s ' ')
# 1) 探测
A_OK=0; B_OK=0
check "$A_DEV" "$A_GW" && check "$A_DEV" "$A_PROBE" && A_OK=1
check "$B_DEV" "$B_GW" && check "$B_DEV" "$B_PROBE" && B_OK=1
# 2) 根据探测结果重装 ECMP
if [[ $A_OK -eq 1 && $B_OK -eq 1 ]]; then
NEW="default scope global nexthop via $A_GW dev $A_DEV weight 1 nexthop via $B_GW dev $B_DEV weight 1"
elif [[ $A_OK -eq 1 && $B_OK -eq 0 ]]; then
NEW="default via $A_GW dev $A_DEV"
elif [[ $A_OK -eq 0 && $B_OK -eq 1 ]]; then
NEW="default via $B_GW dev $B_DEV"
else
exit 0 # 都不通,别乱动,交给上层告警
fi
if [[ "default $CUR" != "$NEW" ]]; then
ip route del default || true
ip route add $NEW
fi
systemd 定时器:
/etc/systemd/system/wan-health.service
[Unit]
Description=Probe uplinks and maintain ECMP/failover
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/wan-health.sh
/etc/systemd/system/wan-health.timer
[Unit]
Description=Run wan-health every 5 seconds
[Timer]
OnBootSec=10s
OnUnitActiveSec=5s
AccuracySec=1s
Unit=wan-health.service
[Install]
WantedBy=timers.target
sudo chmod +x /usr/local/sbin/wan-health.sh
sudo systemctl daemon-reload
sudo systemctl enable --now wan-health.timer
systemctl list-timers | grep wan-health
9. (可选)作为网关时的 NAT 与容器/虚机转发
如果这台服务器还要给后端容器/虚拟机做出网 NAT:
# 打开转发
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/10-forward.conf
sudo sysctl -p /etc/sysctl.d/10-forward.conf
# 按出接口做 SNAT,保证回程对称
sudo iptables -t nat -A POSTROUTING -o eno1 -j SNAT --to-source 203.0.113.2
sudo iptables -t nat -A POSTROUTING -o eno2 -j SNAT --to-source 198.51.100.2
sudo netfilter-persistent save
容器网络(如 CNI/bridge)出口分流,可复用 fwmark + ip rule 思路:对来自不同业务网段/namespace 的流量打不同 mark,再送入 wan1/wan2 表。
10. 验证清单(逐项过)
# 路由与策略
ip rule show | nl
ip route show table main
ip route show table wan1
ip route show table wan2
# 连通性
ping -I eno1 8.8.8.8 -c 3
ping -I eno2 114.114.114.114 -c 3
# 会话分流(重复多次观察出接口)
curl -s https://ifconfig.me # 多试几次,观察返回公网 IP 是否在两条之间切换
# 或
for i in {1..10}; do ip route get 1.1.1.1; done
# 指标与诊断
ss -s
nstat -az
mtr -rwzbc100 www.jd.com
压测结果(示例,iperf3 取 3 轮中位):
| 场景 | 峰值带宽 | 平均 RTT(沪) | 95% 抖动 |
|---|---|---|---|
| 单链路 ISP-A | 6.8 Gbps | 40 ms | 6.3 ms |
| 单链路 ISP-B | 5.9 Gbps | 24 ms | 5.1 ms |
| ECMP 双链 | 11.6 Gbps | 27 ms | 4.9 ms |
| ECMP + 目的地分流(CN→B) | 11.4 Gbps | 22 ms | 3.8 ms |
注意:带宽加和 ≠ 100% 线性叠加,瓶颈可能在对端、协议拥塞控制、应用并发数等。
11. 常见坑 & 我在现场是这么解决的
rp_filter 掉包
一条进、一条出,严格 RPF 会直接丢;把 rp_filter 调成 0 或 2(loose)。
症状:特定目标丢包、但单接口 ping 正常。
解决:见前文 /etc/sysctl.d/99-multiwan.conf。
Netplan 版本不支持某些字段
某些老版本对 routing-policy.priority 或 table 的语法挑剔。
兜底:把 ip rule add ... 和 ip route add ... 放进 oneshot 服务,在 netplan apply 后执行,逻辑等价。
ECMP 会话跑偏导致入向回包错路
入向到 A 的连接,回包被 ECMP 分到 B。
要点:必须有 source-based policy(from 203.0.113.2 table wan1),优先级比 main 表高(如 100 vs main 的 32766)。
运营商侧反向不通/ACL 限制
某些国际线路对“来源非本链路”的包比较敏感。
化解:NAT 场景严格按出接口 SNAT;公网直连场景确保回程源地址与入口一致(靠 policy)。
MSS/MTU 引发的碎片或握手失败
跨境链路有时 MTU 较小或中间隧道化。
对策:在 mangle/forward 阶段对 TCP SYN 做 MSS clamp(--clamp-mss-to-pmtu),或明确设置 mtu 1480 等。
网卡 offload 与延迟
GRO/LRO 对小包低延时业务有副作用。
经验:批量传输保留 GSO/TSO;追求延迟与抖动,适度关 GRO/LRO。
监控与回切风暴
健康检查阈值太激进会反复增删路由。
办法:探针双重(下一跳 + 远端),最短 5s 间隔,建议做3 次失败才切换、30s 稳定才回切的抖动抑制(脚本里加状态机即可)。
12. 附录:一键回滚与应急
临时只走 ISP-A:
sudo systemctl stop wan-health.timer
sudo ip route replace default via 203.0.113.1 dev eno1
恢复 ECMP:
sudo systemctl start wan-health.timer
sudo /usr/local/sbin/route-ecmp.sh
13. 为什么这套对“跨境链路”特别友好?
路径选择更贴合体验:大陆去 CN2,海外走本地;把好钢用在刀刃上。
吞吐更稳定:ECMP 按会话分流,避免单链路排队过长。
回程对称:policy routing 解决“入 A 出 B”的隐患,ACL/RPF 都不怕。
可观测可控:完全 OS 级实现,任何一个环节都能 ip rule/route 看得清清楚楚。
14. 收尾:那一夜之后
凌晨 3:10,我把最后一条健康检查日志看了一遍,ECMP 的两条 nexthop 一明一暗像心跳。第二天 9 点流量波峰开始,Grafana 上那条红色拥塞曲线不见了——取而代之的是两条上联的均匀抬升。值班同事在群里说:“今天很稳。”
后来我们把这套在几家香港机柜复制,连同自动化脚本纳入 Ansible。每次站在机房风口,我都会想起那一夜:不是一定要换更粗的管子,很多时候只是要把水分得更聪明。
TL;DR(给赶时间的你)
- Netplan 配地址 + 两张自定义路由表(wan1/wan2),routing-policy 按源地址保回程对称。
- main 表用 ECMP 多下一跳分担新会话;可选用 fwmark + ipset 做目的地/权重分流。
- 加 健康检查动态增删 nexthop,形成故障切换/回切闭环。
- 结合 BBR、rp_filter、offload 等细节优化,跨境体验显著改善。
如果你也在香港机房为跨境链路头疼,照着本文做一遍,八成能复现我那晚的“安静早高峰”。剩下两成,欢迎你把“坑”再抛给我,我们一起填