分类: 运维实战 / 故障排查

  • 深入 IPVLAN L3 模式丢包排查:非对称路由引发的 rp_filter 拦截与网络黑洞实战

    结论先行:在将容器底层网络从 Macvlan 规模化迁移至 IPVLAN L3 模式时,跨网段 RPC 调用偶尔会出现 3s 超时重传。根本原因是 IPVLAN L3 绕过了宿主机的二层协议栈直接在网络层路由,导致出入站流量路径不一致(非对称路由),触发了内核(以 Linux 5.15 为例)严格模式下的 rp_filter (反向路径过滤) 拦截丢包。修复方案:将宿主机物理网卡及 all 级别的 rp_filter1(严格校验)降级为 2(松散校验)或 0

    故障现场与指标异动

    排查过程中,某业务线反馈部分 Pod 在跨可用区调用时存在偶发性的连接超时,99线从 15ms 剧增至 3s(触发 TCP 默认初始 RTO)。 登录宿主机排查基础指标:Load Average 正常,物理网卡 eth0 的 RX/TX 并没有达到硬件瓶颈,ksoftirqd 软中断也没有打满。

    在宿主机和 Pod 内分别双向抓包,发现了一个经典的“网络黑洞”现象:

    1. Pod 内发出的 SYN 包正常离境。

    2. 远端的 SYN+ACK 响应包已经到达了宿主机的物理网卡 eth0

    3. 但这个 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 丢弃。

    底层原理:内核源码级丢包追踪

    为了拿到最硬核的证据,我们可以用 eBPFperf 捕获内核网络栈的丢包点(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 子接口。