HOOOS

Cilium eBPF 碰上 Istio Envoy:NodePort 流量的劫持与交接艺术

0 15 云网架构师 KubernetesCiliumIstio
Apple

在当今的 Kubernetes 生产实践中,Cilium(eBPF CNI)Istio(Envoy Service Mesh) 的强强联合已成为高性能云原生架构的标配。然而,这种双重数据面架构也引入了极高的复杂度。

当一个外部客户端通过 NodePort 访问集群内的服务时,流量在内核态与用户态之间频繁穿梭。本文将深度剖析:NodePort 流量是如何在主机的 eBPF 层面完成初次拦截与路由,又是如何安全、高效地递交给 Pod 内的 Envoy Sidecar 的?


流量鸟瞰图:从物理网卡到应用进程

在展开细节之前,我们先梳理一条清晰的流量链路(假设 Pod 部署在当前接收到 NodePort 流量的节点上):

  1. 主机物理网卡接收 $\rightarrow$ Cilium tc/XDP eBPF 程序拦截。
  2. eBPF 查表 DNAT $\rightarrow$ 转换 NodeIP:NodePortPodIP:TargetPort
  3. 内核跨命名空间路由 $\rightarrow$ 通过 veth pairipvlan 将报文送入 Pod 网络命名空间。
  4. Pod 命名空间劫持 $\rightarrow$ 传统模式(iptables)或加速模式(Cilium sockops)将流量重定向至 Envoy 的 15006 监听端口。
  5. Envoy 转发 $\rightarrow$ Envoy 经过 L7 规则处理后,将流量投递给本地的应用容器。

第一阶段:主机网卡处的 eBPF 拦截与 DNAT

传统的 Kubernetes 节点使用 kube-proxyiptablesIPVS 规则来处理 NodePort。但在 Cilium 接管的集群中,kube-proxy 通常会被完全替换(Kube-Proxy Free 模式)。

[ 外部流量 ] -> [ 主机网卡 eth0 ] 
                     │
              (tc ingress BPF) ──> 查找 cilium_lb4_services 
                     │
              [ eBPF 核心态 DNAT ] (修改 DstIP=PodIP, DstPort=AppPort)
                     │
              (bpf_redirect_peer) 
                     │
                     ▼
             [ Pod veth (lxcxx) ]

1. eBPF 挂载点(Hook Point)

当 NodePort 报文到达物理网卡(如 eth0)时,挂载在 tc(Traffic Control)ingress 阶段(或 XDP,如果开启了 XDP 加速)的 Cilium eBPF 程序(通常是 from-netdev)会率先捕获该报文。

2. BPF Map 检索与 DNAT

eBPF 程序会提取报文的五元组信息,并检索 Cilium 维护的 BPF 映射表:

  • cilium_lb4_services:存储 Service 的前端配置(例如 NodeIP:NodePort)。
  • cilium_lb4_backends:存储后端的 Endpoint 列表(即真实的 PodIP:TargetPort)。

一旦匹配成功,eBPF 直接在内核态修改 IP 首部和 TCP/UDP 首部,进行 DNAT 转换:

  • 目标 IP 变更为:PodIP
  • 目标端口 变更为:ContainerPort(即容器声明的监听端口,并非 Envoy 的 15006)
  • 同时计算并更新 IP 和 TCP 校验和。

3. 直接重定向(Direct Routing)

传统网络栈需要经过繁琐的路由表查询。Cilium 此时会利用 bpf_redirectbpf_redirect_peer 辅助函数,避开主机的协议栈,将报文直接推送到连接该 Pod 的虚拟网卡对(lxcxxxx)的主机端。


第二阶段:进入 Pod 命名空间

当报文跨越 veth pair 边界,从主机的 lxcxxxx 进入到 Pod 内部的 eth0(Pod 内部网卡)时,报文的特征为:

  • 源 IP / 端口:外部客户端 IP / 客户端随机端口(若未开启 SNAT)
  • 目的 IP / 端口PodIP : TargetPort(例如 10.244.1.45:8080

此时,流量正式移交给 Pod 的网络命名空间。根据集群的配置不同,接下来的“劫持”动作分两种路径进行。


第三阶段:Pod 内的流量劫持与 Envoy 接管

这是 Cilium 与 Istio 交接的核心地带。

路径 A:标准 Istio 模式(基于 iptables 劫持)

即使主机侧使用了 eBPF 替代了 kube-proxy,在 Pod 内部,Istio 默认依然使用 istio-init 容器注入的 iptables(或 nftables)规则。

[ Pod eth0 ] 
     │
     ▼
[ TCP/IP 协议栈 ]
     │
(PREROUTING 链) ──> 命中 ISTIO_INBOUND 规则
     │
     ▼
[ REDIRECT / TPROXY ] ──> 强制将 DstPort 改为 15006
     │
     ▼
[ Envoy (15006) ] ──> (L7 策略处理) ──> 发送给本地 App 进程 (8080)
  1. 协议栈上送:内核协议栈开始在 Pod 命名空间内解析此 IP 报文。
  2. 命中 Netfilter 规则
    • 报文触发 PREROUTING 链。
    • Istio 预设的 ISTIO_INBOUND 规则链拦截目的端口为 8080(TargetPort)的 TCP SYN 包。
    • 通过 REDIRECTTPROXY 动作,将该报文的目标端口强行重定向到本地的 15006 端口(Envoy 的 Inbound 监听器)。
  3. Envoy 接收连接
    • Envoy 的 virtualInbound 监听器在 15006 端口接收到连接。
    • 通过 Linux getsockopt(SOL_IP, SO_ORIGINAL_DST, ...) 系统调用,Envoy 能够读取到被 iptables 修改前的原始目标地址——即 PodIP:8080
    • Envoy 匹配对应的 Filter Chain,执行 mTLS 认证、L7 路由、限流等策略。

路径 B:Cilium Sockops 加速模式(免 iptables 劫持)

如果集群开启了 Cilium Socket Layer Enforcement(即 sockops 优化),那么整个劫持过程将发生质的飞跃:丢弃 iptables,在 Socket 层直接握手。

[ Pod eth0 ] 
     │
     ▼
[ TCP 握手 (SYN) ] ──> 触发 eBPF sockops (cgroup)
                               │
            ┌──────────────────┴──────────────────┐
            ▼                                     ▼
   识别目标为 PodIP:8080                 直接修改 Socket 关联关系
            │                                     │
            └─────────────────────────────────────┘
                               │
                               ▼
                    [ Envoy (15006) Socket ]
  1. Cgroup 级 eBPF 挂载
    Cilium 将 eBPF 程序(如 bpf_sockmap)挂载到 Pod 所在的 cgroup。这意味着该 Pod 内所有进程的 Socket 创建、连接建立等系统调用都会被 BPF 拦截。
  2. Socket 映射表(sockmap
    当外部流量到达,内核准备为新连接建立 struct sock 时,eBPF 程序触发。
  3. 四层直接重定向
    • eBPF 判断此入站连接的目标是本 Pod 的服务端口(8080)。
    • eBPF 直接修改内核 Socket 的对端关联,将原本指向 App:8080 的连接,在套接字层直接重定向(Redirect)到正在监听 15006 的 Envoy Socket。
    • 优势:此过程完全绕过了 TCP/IP 协议栈中复杂的路由、Netfilter 过滤和多次内存拷贝。数据包在内核中通过 sk_msg 直接从网卡接收缓冲区复制到 Envoy 的接收缓冲区。

第四阶段:Envoy 到真实应用(Local Loopback)

当 Envoy 处理完 L7 逻辑后,需要将流量发送给真正的应用进程(如容器内的 Go/Java 进程,监听 127.0.0.1:8080)。

[ Envoy (Client Sock) ] ──(bpf_msg_redirect)──> [ App (Server Sock) ]
                                                     ▲
                                                     │
                                            (绕过本地环回 TCP/IP 栈)
  1. 传统路径: Envoy 发起一个到 127.0.0.1:8080 的新 TCP 连接,数据包通过本地环回网卡(lo)进行一次完整的 TCP/IP 封装和解封装。
  2. Cilium Sockops 优化路径
    • Cilium 的 sockops 已经将 Envoy 的客户端套接字与应用程序的监听套接字记录在了同一张 BPF Map 中。
    • 当 Envoy 向 App 发送数据(调用 sendmsg)时,eBPF 程序 bpf_msg_redirect 拦截此调用,直接将数据放入 App 套接字的接收队列中。
    • 彻底免除了 lo 接口的 CPU 消耗和网络延迟。

深度比对:两种交接模式的性能与可观测性损耗

维度 路径 A:标准 Istio(iptables) 路径 B:Cilium eBPF Sockops 加速
内核态/用户态上下文切换 高(多次经历 TCP/IP 协议栈及 Netfilter) 极低(Socket 层直接映射,短路协议栈)
CPU 消耗 较高(在大流量下,iptables 规则链匹配尤为明显) 极低
排障工具表现 tcpdump -i any 能清晰看到重定向过程 tcpdump 可能看不到部分被 eBPF bypass 的本地流量
诊断手段 iptables-save, netstat cilium monitor, bpftool map dump
配置复杂度 默认支持,开箱即用 依赖特定内核版本(建议 $\ge 5.4$),需调整 Cilium 配置

避坑指南与排障建议

在混合部署 Cilium 与 Istio 的环境中,定位 NodePort 链路问题时,请遵循以下路径:

1. 确认主机层 DNAT 是否成功

在 Node 节点上执行,查看 Cilium 的负载均衡 BPF Map:

cilium bpf lb list

检查是否存在你所访问的 NodeIP:NodePort 映射到 PodIP:TargetPort 的条目。如果这里缺失,说明 Cilium 未正确感知 Service 或 Endpoint。

2. 区分流量是被 Cilium 丢弃还是被 Envoy 拒绝

使用 cilium monitor 工具捕获丢包:

cilium monitor --type drop

如果未发现 Drop 记录,但外部访问返回 503 或连接重置,通常说明流量已安全到达 Pod,问题出在 Envoy 的路由配置(VirtualService)或 mTLS 证书校验失败上。

3. 注意 MTU 引起的“诡异”丢包

Cilium 结合 Istio 时,如果开启了 VxLAN 隧道模式,额外的头部开销会导致有效 MTU 减小。若 NodePort 传输大包时发生连接卡死,尝试调整:

# Cilium ConfigMap
mtu: "1450" # 针对 VxLAN 环境,默认 1500 可能会导致 IP 分片或丢包

点评评价

captcha
健康