GRE over IPSec跑不满带宽?先把你隧道里的MTU账算清楚
先说结论
如果你在用 GRE over IPSec 隧道,发现大文件传输速度异常低(标称千兆只能跑到 200-300Mbps),先别急着加带宽、换设备、查 QoS,把隧道两端的 MTU 和 MSS 钳制配置捋一遍,大概率就是这儿出问题。
我踩过的坑告诉我:这个问题的根本原因是隧道嵌套后的协议头开销没有纳入 MTU 计算,数据包在穿越隧道时被二次分片,内核的重组和重传把性能拖垮了。下面是完整的排查和修复过程。
业务场景
我们有个跨地域站点互联的项目,Site A(上海数据中心)和 Site B(广州办公室)之间需要加密互通。拓扑简化如下:
Site A (10.10.1.0/24) Site B (10.10.2.0/24)
| |
eth0: 192.168.100.1 eth0: 192.168.101.1
| |
GRE Tunnel (10.10.10.1) ----> GRE Tunnel (10.10.10.2)
| |
IPSec Overlay (WAN) IPSec Overlay (WAN)
物理链路是两家运营商的专线,WAN 侧 MTU 1500,没什么特别的。业务方要求在隧道里跑文件共享和数据库同步,对吞吐量有明确要求(不低于链路标称带宽的 80%)。
环境约束:
- 操作系统:CentOS 7.9,内核 3.10
- 网络设备:Cisco ISR 4331 作为 WAN 边界
- 隧道类型:GRE over IPSec(传输模式)
- 隧道端点:两端都是公网 IP,NAT 环境
现象与影响面
隧道建立后,基础连通性没问题。
# 小包 ping,完全正常
$ ping -c 100 10.10.10.2 -s 100
PING 10.10.10.2 (10.10.10.2) 100(128) bytes of data.
108 bytes from 10.10.10.2: icmp_seq=1 ttl=64 time=12.3 ms
# iperf3 小包测试,接近标称带宽
$ iperf3 -c 10.10.10.2 -t 30 -b 1G -l 1K
[ ID] Interval Transfer Bandwidth
[ 4] 0.00-30.00 sec 3.52 GBytes 1007 Mbits/sec
但用大包测,或者直接跑文件拷贝,速度直接腰斩:
# iperf3 大包测试,崩了
$ iperf3 -c 10.10.10.2 -t 30 -b 1G -l 64K
[ ID] Interval Transfer Bandwidth
[ 4] 0.00-30.00 sec 1.09 GBytes 312 Mbits/sec
业务方反馈:文件共享下载一个 500MB 的镜像,要跑将近 15 秒,正常应该 4-5 秒。用户投诉说”网络慢”,但我们看链路利用率只有 30%,明显不对。
影响面:
- 大文件传输速度只有标称带宽的 30%
- TCP 重传率明显上升
- 隧道两端设备的 CPU 和内存没有明显瓶颈
排查路径
第一步:排除设备性能瓶颈
先确认不是硬件问题。
# Site A 查看 CPU 和网络中断分布
$ top -bn1 | grep -E "Cpu(s)|softirq"
%Cpu(s): 8.3 us, 2.1 sy, 0.0 ni, 89.2 id
$ cat /proc/interrupts | grep -E "eth0|ens" | awk '{print $1, $NF}'
39: eth0-TxRx-0
40: eth0-TxRx-1
41: eth0-TxRx-2
42: eth0-TxRx-3
CPU 使用率很低,没有单核瓶颈。网络中断分布均匀,说明负载均衡正常。
第二步:tcpdump 抓包看异常
用大包 ping 试试,看有没有丢包或分片。
# 大包 ping,-M do 表示不分片
$ ping -c 10 10.10.10.2 -s 1400 -M do
PING 10.10.10.2 (10.10.10.2) 1400(1428) bytes of data.
From 192.168.100.1: icmp_seq=1 Fragmentation needed.
ping: local error: message too long, fmt: Jumbogram has bad length
注意这个错误:Fragmentation needed,说明 1400 字节的数据包需要分片,但 DF(Don’t Fragment)位被设置了,设备直接拒绝转发。
抓包验证:
# 在 GRE 接口上抓包
$ tcpdump -i gre0 -nn -v -c 20
# 同时在物理口抓 ESP 包
$ tcpdump -i eth0 -nn -v host <Site B 公网IP> and esp
抓包结果显示:
- 从应用层发来的 TCP 包,Length: 1460(MTU – IP头 – TCP头)
- 到达 GRE 接口后,被加上 GRE 头(4 字节封装头 + 4 字节 key + 20 字节内层 IP)
- 加上 IPSec ESP 加密头(约 50-60 字节,含 SPI、序列号、IV、认证标签)
- 再加上外层 IP 头(20 字节)
总长度 = 1460 + 24(GRE) + 56(ESP) + 20(外层IP) = 1560 字节
超过 WAN 侧的 MTU 1500,所以数据包被分片。但分片发生在加密之后,而 IPSec ESP 不支持分片(因为数据被加密了,设备无法识别上层协议),于是数据包直接被丢弃。
第三步:计算协议栈开销
让我把这个账算清楚:
标准以太网 MTU: 1500 字节
应用层数据 (TCP MSS): 1460 字节
TCP 头: 20 字节
内层 IP 头: 20 字节
GRE 封装头: 4 字节 (Protocol Type: 0x0800)
GRE 头 (Key 可选): 4 字节
内层 IP 头 (隧道场景): 20 字节
IPSec ESP 头: ~8 字节 (SPI + 序列号)
IPSec ESP IV: 16 字节
IPSec ESP Auth Tag: 12 字节
IPSec ESP Padding: 1-16 字节
IPSec ESP Tail: 2 字节
外层 IP 头: 20 字节
总计协议开销: 24 + 8 + 16 + 12 + 20 = 80 字节 (保守估算)
最大可承载的应用数据: 1500 - 80 - 40 = 1380 字节 (如果内层用 IPv6,就是 1360)
但通常我们说的 MSS = 1460 = 1500 - 20(IP) - 20(TCP)
所以需要 MSS 钳制到: 1500 - 80 = 1420 (保守值取 1400)
这就是问题所在:我们的 TCP 连接在三次握手时协商的 MSS 还是 1460(基于内层网络的 MTU),但实际数据包穿越 GRE over IPSec 隧道后,体积膨胀到 1560+,设备根本处理不了。
最终方案
方案取舍
方案 A:增大物理接口 MTU(如设为 9000 Jumbo Frame)
理论上可行,但需要协调运营商,而且两端的网络设备都要改,风险较高。PASS。
方案 B:手动设置 GRE 隧道的 MTU
# 在 GRE 接口上设置 MTU
ip link set dev gre0 mtu 1400
这能让应用层数据在进入 GRE 隧道前被分片,但问题是:
- 分片发生在应用层,降低效率
- 不解决 TCP MSS 协商问题
方案 C:配置 TCP MSS 钳制(推荐)
在两端防火墙上配置 iptables,针对穿越隧道的数据包修改 TCP MSS 值:
# Site A (上海)
iptables -t mangle -A FORWARD -p tcp --syn -s 10.10.1.0/24 -d 10.10.2.0/24 -j TCPMSS --set-mss 1380
iptables -t mangle -A FORWARD -p tcp --syn -d 10.10.1.0/24 -s 10.10.2.0/24 -j TCPMSS --set-mss 1380
# 或者更通用的写法(匹配 GRE 隧道口)
iptables -t mangle -A FORWARD -o gre0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1380
这样三次握手时,双方会基于 MSS 1380 协商窗口大小,后续数据包就不会超过隧道承载能力。
为什么选方案 C:
- 不改物理网络配置,改动范围小
- TCP MSS 钳制在三层处理,比分片效率高
- 可通过 iptables 规则精确控制
完整配置步骤
1. 调整 GRE 隧道 MTU
# Site A
ip link set dev gre0 mtu 1400
ip addr show gre0 | grep -E "mtu|inet"
# inet 10.10.10.1/30 scope global gre0
# mtu 1400
# Site B 同样配置
ip link set dev gre0 mtu 1400
2. 配置 iptables TCP MSS 钳制
# Site A
cat >> /etc/sysconfig/iptables.mangle << 'EOF'
*mangle
# 限制从内网到对端内网的 TCP MSS
-A FORWARD -p tcp --tcp-flags SYN,RST SYN -s 10.10.1.0/24 -d 10.10.2.0/24 -j TCPMSS --set-mss 1380
# 限制从对端内网到本端内网的 TCP MSS
-A FORWARD -p tcp --tcp-flags SYN,RST SYN -d 10.10.1.0/24 -s 10.10.2.0/24 -j TCPMSS --set-mss 1380
COMMIT
EOF
# 加载规则
iptables-restore < /etc/sysconfig/iptables.mangle
# Site B 同样配置,只是网段互换
3. 如果使用 Cisco ISR 4331
! 在隧道接口上配置
interface Tunnel0
ip mtu 1380
ip tcp adjust-mss 1360
!
! 在物理出口配置 ACL,配合 MPF
ip access-list extended MSS_FIX
permit gre host <Site A WAN IP> host <Site B WAN IP>
permit esp host <Site A WAN IP> host <Site B WAN IP>
!
class-map match-all MSS_FIX_CLASS
match access-group name MSS_FIX
!
policy-map MSS_FIX_POLICY
class MSS_FIX_CLASS
set tcp adjust-mss 1360
!
interface GigabitEthernet0/0/1
service-policy output MSS_FIX_POLICY
4. 确认 IPSec 没有截断分片
IPSec ESP 在传输模式下,理论上不应该分片,但如果底层链路的 MTU 设置不对,还是会出问题。确认物理口的 MTU 是 1500(标准值):
# 确认 MTU
ip link show eth0 | grep mtu
# mtu 1500
# 测试路径 MTU
$ tracepath -m 5 10.10.10.2
1?: [LOCALHOST] pmtu 1500
1: no reply
2: <Site B 公网IP> 1502
Resume: pmtu 1500
验证与评估
验证方法
1. ping 大包测试
# 之前 1400 报 Fragmentation needed,现在 1400 应该正常
$ ping -c 10 10.10.10.2 -s 1400 -M do
PING 10.10.10.2 (10.10.10.2) 1400(1428) bytes of data.
1408 bytes from 10.10.10.2: icmp_seq=1 ttl=64 time=13.1 ms
# 继续往上探,1472 应该刚好触发问题(1472 + 20 + 8 = 1500,接近边界)
$ ping -c 5 10.10.10.2 -s 1472 -M do
# 这个应该还是能过
# 1500+ 的包会怎样
$ ping -c 5 10.10.10.2 -s 1473 -M do
# 这个应该失败
2. tcpdump 确认 MSS 钳制生效
# 抓取 SYN 包,确认 MSS 值
$ tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0' -c 10
# 正常应该看到 Mss 1380 或 1360,而不是 1460
# 18:32:15.123456 IP 10.10.1.10.45678 > 10.10.2.20.80: Flags [S], seq 12345, win 65535, options [mss 1380, ...]
3. iperf3 带宽对比
# Server 端(Site B)
$ iperf3 -s -i 1
# Client 端(Site A)
$ iperf3 -c 10.10.10.2 -t 60 -P 4 -b 1G -l 64K
优化前后对比:
| 测试场景 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| iperf3 -l 64K (4并发) | 312 Mbps | 942 Mbps | 202% |
| iperf3 -l 16K (单并发) | 287 Mbps | 918 Mbps | 220% |
| 大文件 SCP (500MB) | ~4.2 MB/s | ~11.8 MB/s | 181% |
| TCP 重传率 | 3.2% | 0.15% | 降低了 95% |
tcpdump 观察到的变化:
优化前:
- 大量 TCP Retransmission
- IP Fragments(分片包)
- ACK 延迟明显
优化后:
- TCP 三次握手的 MSS 变成 1380
- 无分片包
- 重传率降到 0.1% 级别
常见坑
坑 1:只改一边
TCP MSS 钳制必须双向配置,或者至少让发起方的规则生效。如果你只在一端配置了 MSS 1380,但另一端回复时还是用 MSS 1460 协商,大包还是会出问题。
验证方法:
# 在 Site A 抓 SYN 包
$ tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0' | grep -o "mss [0-9]*"
# 确认两个方向都是 1380
坑 2:MTU 和 MSS 傻傻分不清
- MTU:Maximum Transmission Unit,链路层最大帧长
- MSS:Maximum Segment Size,TCP 最大报文段长(不包含 IP 头和 TCP 头)
MSS 钳制只管三次握手时的协商值,不改变实际数据包的 MTU。如果你的应用直接发 UDP 大包,还是会触发分片。
坑 3:忽略 GRE Keepalive 的影响
GRE 隧道默认会发 Keepalive 包,如果隧道 MTU 设置不当,Keepalive 也会被分片或丢弃,导致隧道不稳定。
# 查看 GRE Keepalive 配置
$ ip -d link show gre0
# 如果有 keepalive 配置,确认包大小不超过 MTU
坑 4:路径 MTU 发现(PMTUD)被防火墙阻断
有些防火墙会丢弃 ICMP “Fragmentation Needed” 消息,导致路径 MTU 发现失效。确认两端设备的 ICMP 不要被 ACL 拦掉:
# Cisco 设备
show access-lists | include ICMP
# 确认有类似 "permit icmp any any time-exceeded" 的规则
坑 5:IPSec 的 padding 导致额外开销
ESP 加密有块大小对齐要求(AES 需要 16 字节对齐),padding 可能额外增加 1-16 字节。如果你的数据刚好踩在边界上,可能因为 padding 导致整体超过 MTU。
我的经验值:
| 隧道类型 | 推荐 MSS 钳制值 |
|---|---|
| 纯 GRE | 1460 |
| GRE over IPSec (ESP-AES-128) | 1380 |
| GRE over IPSec (ESP-AES-256 + SHA512) | 1360 |
| IPSec 隧道模式 (带新 IP 头) | 1340 |
保守起见,我一般取 1360,留 20 字节余量。
上线观察指标
配置上线后,建议持续观察以下指标:
# 1. 网络丢包和重传
$ sar -n EDEV 1 | grep -E "TCP|tcp"
# 关注 retrans/s 和 failed/s
# 2. GRE 隧道状态
$ ip -s tunnel show
# 确认没有 errors 和 drops
# 3. iptables mangle 计数器
$ iptables -t mangle -L FORWARD -v -n
# 确认 TCPMSS 规则有命中计数
# 4. 带宽利用率
$ iftop -i gre0 -B
如果有条件,可以用 SmokePing 或 Prometheus + Grafana 做长期监控,观察大文件传输场景的延迟和吞吐量是否稳定。
总结
GRE over IPSec 隧道性能差的根本原因,90% 是 MTU 配置问题。核心逻辑就一句话:隧道嵌套后的协议头会吃掉 MTU 空间,必须通过 MSS 钳制让 TCP 主动降速。
排查思路:
- 先用
ping -M do -s 1400确认是不是 MTU 问题 tcpdump -i gre0 -nn -v看有没有分片和重传- 计算协议栈开销,确认 MSS 应该设多少
- 两端配置 TCP MSS 钳制
- iperf3 验证效果
别忘了:MSS 钳制只管 TCP,三次握手之后的 UDP 包和其他协议还是要靠正确的 MTU 设置兜底。