结论先行:在将容器底层网络从 Macvlan 规模化迁移至 IPVLAN L3 模式时,跨网段 RPC 调用偶尔会出现 3s 超时重传。根本原因是 IPVLAN L3 绕过了宿主机的二层协议栈直接在网络层路由,导致出入站流量路径不一致(非对称路由),触发了内核(以 Linux 5.15 为例)严格模式下的 rp_filter (反向路径过滤) 拦截丢包。修复方案:将宿主机物理网卡及 all 级别的 rp_filter 从 1(严格校验)降级为 2(松散校验)或 0。
故障现场与指标异动
排查过程中,某业务线反馈部分 Pod 在跨可用区调用时存在偶发性的连接超时,99线从 15ms 剧增至 3s(触发 TCP 默认初始 RTO)。
登录宿主机排查基础指标:Load Average 正常,物理网卡 eth0 的 RX/TX 并没有达到硬件瓶颈,ksoftirqd 软中断也没有打满。
在宿主机和 Pod 内分别双向抓包,发现了一个经典的“网络黑洞”现象:
-
Pod 内发出的 SYN 包正常离境。
-
远端的 SYN+ACK 响应包已经到达了宿主机的物理网卡
eth0。 -
但这个 SYN+ACK 并没有被投递到 Pod 内的虚拟网卡
eth0,直接在宿主机内核网络栈中“消失”了。
既然包到了宿主机却没进 Pod,且没有被 iptables/Netfilter 的 DROP 规则拦截(通过 iptables -t filter -nvL 确认),第一反应是内核底层的静默丢包。直接通过 nstat 抓取网络层异常统计:
# 持续观察 IPReversePathFilter 计数器
nstat -az | grep -i filter
IpExtIPReversePathFilter 124505 0.0
每当业务出现超时,IpExtIPReversePathFilter 指标就会暴增。这直接锁定了凶手:Linux 内核的反向路由过滤机制(Reverse Path Filter)。
为什么 IPVLAN L3 会触发非对称路由拦截?
要理解这个报错,必须先搞懂 IPVLAN 的工作机制以及 L3 模式的特殊性。
不同于 veth pair 会创建成对的虚拟网卡,也不同于 Macvlan 会为每个容器生成独立的 MAC 地址。IPVLAN 的核心特征是“MAC 地址复用”:所有的 IPVLAN 子接口都与宿主机的物理网卡共享同一个 MAC 地址。
IPVLAN 支持三种模式:L2、L3、L3S。
-
L2 模式:行为类似交换机,处理 ARP 广播,同网段二层互通。
-
L3 模式:行为类似路由器,完全丢弃所有广播/多播包(包括 ARP)。容器向外发包时,依赖三层路由表,出站流量直接借用宿主机物理网卡的 MAC 地址发出去;入站流量到达宿主机物理网卡后,内核根据目的 IP 在宿主机的路由表中查找,将其转发给对应的 IPVLAN 子接口。
触发 rp_filter 丢包的条件,恰恰就在 L3 模式的“非对称路由”特性上。
在复杂的 K8S 生产环境中,通常会配置多块网卡或复杂的策略路由(Policy Routing)。当远端响应包从宿主机的 eth0 进入时,内核的 Netfilter 系统会执行源地址有效性校验(fib_validate_source)。
如果 rp_filter=1(严格模式),内核会假设:“如果我现在要给这个发件人(源 IP)回包,根据我当前的路由表,最优出站网卡是哪一个?”
如果最优出站网卡不是当前收到包的网卡(例如收到包是 eth0,但默认路由指向了 eth1,或者由于 CNI 注入了策略路由导致反向查找不匹配主路由表),内核就会判定这是一个源地址欺骗(Spoofing)的非法数据包,直接调用 kfree_skb 丢弃。
底层原理:内核源码级丢包追踪
为了拿到最硬核的证据,我们可以用 eBPF 或 perf 捕获内核网络栈的丢包点(Drop Point)。
使用 perf 挂载 kfree_skb 跟踪点:
perf record -g -a -e skb:kfree_skb --filter 'skbaddr != 0' sleep 10
perf script
捕获到的调用栈如下,清晰地指向了 fib_validate_source:
ksoftirqd/0 [000] 12345.678901: skb:kfree_skb: skbaddr=0xffff888123456700 protocol=2048 location=ip_rcv_core+0x2f0
ffffffff817a1234 kfree_skb+0x74 ([kernel.kallsyms])
ffffffff817c5678 ip_rcv_core+0x2f0 ([kernel.kallsyms]) # 丢包点
ffffffff817c6789 ip_rcv+0x59 ([kernel.kallsyms])
ffffffff817e890a fib_validate_source+0x12a ([kernel.kallsyms]) # 反向路由校验失败
...
在 Linux 内核源码 net/ipv4/fib_frontend.c 中,fib_validate_source 的逻辑极其严苛。更坑的是,Linux 的 sysctl 配置中,rp_filter 的最终生效值是由 all 和具体网卡(如 eth0)的值取 最大值 (MAX) 决定的。
// 内核宏定义:取 MAX(conf/all/rp_filter, conf/dev/rp_filter)
#define IN_DEV_RPFILTER(in_dev) \
(max(IN_DEV_CONF_GET((in_dev), RP_FILTER), \
IN_DEV_CONF_GET((in_dev)->cnf.all, RP_FILTER)))
这意味着,哪怕你把 net.ipv4.conf.eth0.rp_filter 设成了 0,只要 net.ipv4.conf.all.rp_filter 是 1,系统依然会执行严格校验。很多排查人员在这里踩了坑,改了单网卡配置却发现不生效。
破局与防御性配置实践
解决此类问题,最稳妥的方式是将物理网卡和全局的 rp_filter 设置为 2(松散模式:Loose Mode)。在松散模式下,只要内核路由表中有任何一条路由可以到达该源 IP(无论通过哪个网卡),校验就会放行。
1. 动态修复(立即生效)
# 必须同时修改 all 和对应的物理网卡
sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.default.rp_filter=2
sysctl -w net.ipv4.conf.eth0.rp_filter=2
# 验证当前生效值
cat /proc/sys/net/ipv4/conf/all/rp_filter
2. 自动化配置(防御性编程)
在 DevOps 体系中,不能指望人工救火。应该在主机初始化 Ansible 剧本或者 K8S CNI 的 initContainers 中强制声明此状态。
编辑 /etc/sysctl.d/99-kubernetes.conf:
# IPVLAN L3 Mode prerequisite
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
net.ipv4.conf.eth0.rp_filter=2
执行 sysctl --system 加载,确保机器重启后不会复发。
常见问题 (FAQ)
Q1:高并发场景下,veth pair 和 IPVLAN 该怎么选?
如果你的业务特点是超高 PPS(每秒包数),坚决选 IPVLAN(或 Macvlan)。veth pair 的通信路径需要穿越宿主机的虚拟网卡,对端的 rx_handler 会触发大量的软中断(softirq),在单核高 PPS 下极易打满 CPU 导致限流丢包。IPVLAN 直接挂载在物理网卡的 rx_handler 上,跳过了中间的虚拟网络层开销,网络吞吐和延迟性能无限逼近物理机。
Q2:既然 Macvlan 也能提升性能,为什么还要花精力迁移到 IPVLAN? Macvlan 的致命缺陷是“MAC 地址泛滥”。它会为每个 Pod 申请一个 MAC 地址,当单台宿主机上运行上百个 Pod,集群规模达到数千台时,底层物理交换机的 CAM 表(MAC 地址表)极易被打爆,导致整个机房的网络瘫痪。IPVLAN 共享宿主机物理网卡的 MAC 地址,彻底根绝了 CAM 表溢出问题。
Q3:IPVLAN 容器内可以和宿主机直接通信吗? 默认不行。由于流量被直接截获投递给虚拟子接口,宿主机的协议栈和 IPVLAN 子接口处于隔离状态。如果需要互通,经典解法是在宿主机上再创建一个 IPVLAN 接口并配置 IP,将宿主机的通信也接入到这个虚拟网络层中。
Q4:IPVLAN L3 模式下,外部请求是怎么找到容器的? 因为 L3 模式没有 ARP,外部请求找不到容器的 MAC。生产实践中,必须结合 BGP 路由协议(如 Calico BGP)或者在上一级路由器/网关配置静态路由,将容器子网的下一跳(Next Hop)直接指向宿主机物理网卡的 IP。包到达宿主机后,宿主机的路由表再负责将包“导”入 IPVLAN 子接口。