cilium

2026年4月6日 · 6972 · 更新 2026年4月7日

BPF

BPF(Berkeley Packet Filter,伯克利报文过滤器)最早来源于1992年的一篇经典论文,最初实现于BSD内核中。由于其优秀的设计思想,随后被引入到Linux系统中,并成为网络抓包与过滤的重要基础设施。

BPF的核心能力在于:允许用户空间程序定义过滤规则,并将其下沉到内核中执行,从而避免不必要的数据拷贝,提高数据包处理效率。例如,在使用tcpdump分析网络问题时,可以通过-d参数查看过滤表达式被编译后的指令,这些指令正是BPF程序。 在实现上,用户态通过libpcap将过滤规则编译为BPF指令,并通过系统调用传递到内核。当网卡接收到数据包后,内核在数据包分发路径中执行对应的BPF程序(如packet_rcv流程中),根据过滤结果决定是否将该数据包复制到用户空间。若不满足条件,则数据包会被直接丢弃,从而避免不必要的内存拷贝和上下文切换开销。

从本质上看,BPF可以被视为一个基于寄存器的轻量级虚拟机,它支持有限的指令集(如加载、存储、算术运算和跳转),并在内核态安全执行用户定义的逻辑。这种“将计算前移到内核”的设计,大幅提升了数据包过滤性能,相比传统用户态过滤方式可获得数量级的性能提升。不过,经典的BPF(cBPF)能力相对有限,它主要用于网络数据包过滤,指令集简单,扩展性较弱,这也促使了后来eBPF的出现。

eBPF

eBPF是BPF的扩展和彻底重构,它于2014年被引入Linux内核,将BPF的能力从单一的网络过滤扩展到了整个内核。eBPF的核心目的是允许用户在不修改内核代码的情况下,安全地向Linux内核添加自定义、可编程的逻辑。相比传统BPF,eBPF在设计上引入了多项关键能力:

  • 通用虚拟机:eBPF是一个全新的、64位的通用虚拟机。它拥有更多的寄存器和更强大的指令集,使其能够执行更复杂的程序,支持算术运算、条件跳转、函数调用、内存访问等,远强于原始BPF
  • Hooks:eBPF程序可以被附加到Linux内核中的数百个钩子点上,包括网络接口 (网络转发)、系统调用 (安全审计)、内核函数、跟踪点 (性能分析) 等
  • eBPF映射:eBPF引入了Maps机制,允许eBPF程序在内核中存储共享数据,允许用户空间程序安全地访问这些数据
  • JIT编译:eBPF字节码在加载后会被内核即时编译为原生机器码(性能接近内核代码)
  • 校验器:eBPF程序在加载到内核之前,必须经过严格的静态分析和验证。校验器确保程序不会访问非法内存、不会包含无限循环,从而保证内核的稳定性

eBPF将Linux内核变成了可编程的。它允许像Cilium这样的应用程序安全、高效地运行自定义代码,而不需要对内核进行任何修改。eBPF技术在不断发展,越新的内核包含特性越丰富,使用高版本内核编写内核程序,如果使用新实现的eBPF特性,那么该程序在低版本内核可能就无法运行。eBPF的整个技术栈如下图所示:

eBPF事件

eBPF程序采用典型的事件驱动模型执行。它并不是一个持续运行的后台服务,而是一段被动等待、按需执行的内核程序。只有当特定事件发生时,eBPF程序才会被触发执行。从本质上看,eBPF程序本身是“惰性”的,必须附着在Linux内核的某个钩子点上,才能参与系统运行。因此,一个eBPF程序的完整生命周期可以理解为由事件驱动的执行流程

  • Load:用户态程序编写eBPF程序,编译为eBPF字节码,通过系统调用(如bpf())加载到内核。在在加载阶段还会经过Verifier校验
  • Attach:程序被附着到指定的钩子点上。例如,网络路径(XDP/TC)
  • Idle:eBPF程序不会主动执行。没有后台线程和轮训机制,处于被动等待状态
  • Trigger:当事件流经对应hook点时,例如收到网络数据包,内核会触发对应的eBPF程序执行
  • Execution:eBPF程序在内核中运行,读取上下文数据并执行逻辑判断,再返回处理结果(如允许/丢弃/重定向等)
  • Detach:当不再需要该功能时,程序会被安全地从钩子点卸载并释放资源 eBPF采用事件驱动模型带来了巨大的性能提升:程序只在事件发生时才执行,没有轮询或后台开销。由于直接在内核中执行,响应事件的速度是纳秒级的;eBPF程序只在处理特定事件时消耗CPU,处理完成后立即退出;eBPF机制要求程序必须是短暂的,且不能有无限循环,确保即使代码有错误,也不会拖垮整个内核

在单个CPU上,eBPF程序的执行是串行的;但在多核系统中,同一个eBPF程序可以在不同CPU上并行执行(例如处理不同的数据包或事件)。此外,一个eBPF程序也可以同时附着在多个hook点上,从而在多个执行路径中被触发

eBPF运行机制

当eBPF程序被编译成字节码后,使用加载程序Loader通过bpf()系统调用将字节码加载至内核,这些字节码是一种与平台无关的指令集,类似于精简的汇编语言,可以在eBPF虚拟机中执行。在程序真正运行之前,必须经过内核中的Verifier校验器进行严格的静态分析。这一步至关重要,它确保eBPF程序是安全的,不会破坏内核的稳定性或引入安全风险。为了实现安全检查,eBPF程序还有着如下限制:

  • eBPF程序不能调用任意内核函数,只限于内核模块中列出的BPF辅助函数,辅助函数支持列表一般随着内核的演进在不断增加
  • eBPF程序的栈空间固定为512字节
  • eBPF早期指令数量限制在4096条,截止到内核Linux 5.8版本,已将放宽至100万。但对于无特权的BPF程序,仍然受4096限制
  • eBPF程序不允许存在不可达指令,也不允许非法跳转,确保程序结构清晰、可分析
  • eBPF程序必须在有限时间内结束,不允许无限循环。从Linux 5.3开始,支持有界循环,Verifier会证明循环一定能终止

当eBPF程序通过校验后,内核的JIT编译器会将eBPF字节码实时地编译为本地机器码,这样可以极大加速BPF程序的执行。通常,指令可以1:1映射到底层架构的原生指令,对于CISC指令集(例如x86),JIT还做了很多特殊优化,目的是为给定的指令产生可能的最短操作码,以降低程序翻译过程所需的空间。

机器码可以直接在CPU上运行,省去了虚拟机模拟指令的开销,使得eBPF程序的执行速度接近于内核的编译代码

eBPF Maps

eBPF程序本身是无状态的,执行完毕后不会保留任何状态信息。为了弥补“无状态”的限制,eBPF引入了Maps机制,用于在内核中持久化数据,并实现用户态与内核态之间的通信。eBPF提供了多种Maps类型,以适应不同的数据结构和访问需求,如Hash Map、Array Map、LPM Trie、Ring Buffer、Per-CPU Map等。

XDP & TC

XDP和TC是eBPF在Linux网络数据路径中最核心的两个挂载点。它们决定了eBPF程序“在哪一层”拦截数据包,以及“能做什么程度的处理”。两者的核心区别在于:

  • XDP:发生在协议栈之前(极致性能)
  • TC:发生在协议栈内部(功能更强)

在理解XDP/TC之前,先看看Linux网络栈的几个关键开销:使用软中断处理网络数据;每个数据包都需要构造sk_buff;用户态与内核态频繁切换

sk_buff

sk_buff是Linux内核中用于表示网络数据包的核心数据结构,它用来在内核中存储、传递和管理网络包。无论数据包是从网卡接收进来的,还是从应用程序发出去的,它都会在内核中以sk_buff的形式存在和流转。sk_buff中包含两部分信息:

  • 数据包本身:包含指向网络数据包实际内容(如Ethernet头部、IP头部、TCP/UDP头部及载荷)的指针。随着数据包在协议栈中上升或下降,这些指针会不断移动,指向当前正在处理的协议层
  • 元数据:是sk_buff最复杂和最有价值的地方,包含了协议栈处理数据包所需的一切信息。如
    • 状态信息:标识数据包类型的protocol,对数据报作特殊处理标记的vlan_tag等
    • I/O接口:记录数据包来自哪个网卡,以及应该通过哪个网卡发送
    • 路由信息:dst目标路由缓存,记录内核路由查找的结果,指导数据包的下一跳
    • 连接追踪:nfct,记录数据包属于哪个连接,这是NAT和防火墙状态检查的基础

一个数据包在接收路径(RX)中的流动过程大致是这样的:物理网卡(NIC) → 网卡驱动 → 分配并填充sk_buff → 内核协议栈(L2 → L3 → L4) → Socket → 用户态应用。

在这个过程中,数据包始终以sk_buff结构体作为载体在内核中流转。各协议层会逐步解析协议头,并通过调整skb->data指针(而非真正拷贝数据)来“剥离”对应的协议头部。最终,当数据通过socket传递到用户空间后,相关的sk_buff会被内核释放并回收。由于不同协议层通过操作元数据,实现对数据包的逐层处理。这种设计使得Linux网络栈在保持灵活性的同时,也带来了较高的处理开销,尤其是在高包速率场景下。在这类场景下,现代高性能网络思路主要有两种:

  1. DPDK:绕过内核,在用户态处理数据包。虽然性能极高,但与内核生态割裂,编程复杂
  2. XDP:在保留内核网络路径的前提下,将数据包处理逻辑前移到网卡驱动层,在sk_buff构建之前完成决策,从而避免协议栈和内存分配带来的开销

sk_buff详情可参考文档:https://docs.kernel.org/networking/skbuff.html

XDP

XDP运行在数据包从网卡进入系统内存后、但在构建sk_buff之前。其工作流程大致如下:

  • 事件触发:位于网卡驱动层的XDP在数据包还未变成sk_buff之前拦截了它。此时数据包只是一个原始的内存页(xdp_md结构体,比sk_buff轻量地多)
  • 数据访问:XDP程序通过xdp_md上下文访问数据
  • 高速决策:XDP程序根据简单的L2/L3信息来处理数据包,并返回一个动作码,驱动根据该动作决定如何处理数据包

在eBPF网络体系中,RX是XDP的主战场,用于处理报文,对应的动作码有:

  • XDP_DROP:直接丢弃数据包
  • XDP_PASS:放行数据包进入协议栈(构建sk_buff),后续交由TC/内核继续处理

而在TX链路上,XDP也可以实现快进快出的高性能转发(接近零拷贝),对应动作码有:

  • XDP_TX:将数据包从当前网卡直接发回(用于快速转发)
  • XDP_REDIRECT:将数据包重定向到其它网卡或其它CPU队列(常用于高性能转发和负载均衡)

XDP有三种运行模式:Native、Generic、Offloaded

TC

TC是Linux内核中负责流量控制、服务质量以及数据包调度的核心框架。当eBPF程序附着在TC钩子点上时,它能够获取完整的协议栈上下文,对数据包进行精细化处理。TC在数据路径中提供两个关键钩子:

  • TC Ingress:位于RX路径,数据包经过XDP后进入协议栈时触发。可用于ACL控制、Qos分类和流量过滤
  • TC Egress:位于TX路径,在数据包完成路由、NAT等处理之后。用于出站安全控制、流量整形和隧道封装(如VXLAN、Geneve)

TC处理的是完整的sk_buff,其中包含丰富的元数据,这使得它可以实现更复杂的网络逻辑。而且由于TC位于协议栈内部,程序可以调用那些需要访问内核复杂服务的Helper函数,从而可以安全的修改sk_buff的关键元数据。TC常见的动作码有:

  • TC_ACT_OK:继续交由协议栈处理(正常路径)
  • TC_ACT_SHOT:丢弃数据包
  • TC_ACT_REDIRECT:重定向到其他网络接口
  • TC_ACT_STOLEN:完全接管数据包(不再交给内核后续处理)

位于Linux上的用户态tc工具,可以直接操作内核中的TC,有兴趣的可以研究下

cilium介绍

cilium是eBPF在云原生领域最重要的落地实践之一,它将网络转发、安全策略和负载均衡能力下沉到内核,通过可编程的数据路径彻底重构了Kubernetes网络模型。

cilium采用典型的控制平面+数据平面架构。控制平面负责策略计算、状态管理;数据平面则基于eBPF执行高性能数据处理。核心组件有以下几个:

  • cilium agent:以DaemonSet控制器形式运行,每个节点都会运行一个agent。用于管理eBPF程序的加载与更新、维护本地的eBPF Maps、处理kubernetes事件、下发网络策略
  • cilium operator:以Deployment控制器形式运行,是控制平面的核心。用于管理集群范围资源、分配IP(IPAM)、管理CRD、处理节点间协调
  • eBPF程序:真正干活的程序,运行在XDP、TC、Socket等内核hooks上。用于数据包转发、负载均衡、安全策略执行、流量监管
  • eBPF Maps:数据平面的“数据库”,存储security id身份标识、路由信息、连接状态。具体实现是cilium_ipcache
  • hubble:是可观测性组件。用于实时流量监控、网络可视化、安全审计
  • cni插件:是一个轻量级的二进制文件,遵循CNI标准。不负责复杂的逻辑,只是简单地向cilium Agent发起请求,触发网卡创建、IP分配和eBPF程序加载
  • cilium cli工具链:用于部署和管理、调试eBPF、查看流量

cilium网络模式

在cilium中,pod的网络连接是通过veth pair虚拟网卡对(本质是ip link创建)建立的,一端放入pod的网络命名空间,另一端保留在宿主机的命名空间,以此实现pod间通信。而且cilium会在这些veth设备上,将eBPF程序挂载到TC Ingress钩子上,从而实现高效而精准的流量策略控制。

需要注意的是,宿主机侧的veth接口通常不会配置IP地址。这意味着数据转发并不依赖传统的Linux路由机制,大部分网络逻辑都由eBPF程序完成

cilium数据平面网络的网络模式主要分为两大类:

  • 隧道模式(vxlan、geneve、WireGuard):该模式下,cilium会创建VXLAN或Geneve隧道接口,将Pod网络流量封装后在节点之间传输。这种模式对底层网络没有特殊要求,节点之间只需具备IP可达性即可完成通信,因此非常适合云环境或网络不可控的场景
  • 原生路由模式:该模式下,cilium不负责建立跨节点的网络可达性,而是依赖底层网络提供Pod网段之间的路由能力。也就是说,要么集群各节点都在同一L2下;要么节点间可以通过三层路由(L3)实现互通(如静态路由或BGP)

补充:隧道封装会增加数据包长度,因此必须通过合理设置MTU来避免分片,从而保证网络性能和稳定性

vxlan

VXLAN是一种隧道协议,用于在三层网络之上构建二层虚拟网络。它通过引入24位的VNI网络标识,将网络隔离规模从传统VLAN的4096个扩展到约1600万个,从而解决了VLAN可扩展性不足的问题。而且相比传统的GRE隧道,VXLAN具备更好的扩展性和兼容性(基于UDP,易于穿越NAT和负载均衡设备),因此更适合现代数据中心和云原生网络场景。

什么叫“在三层网络之上构建二层虚拟网络”呢?我们先看看VXLAN的报文结构:

  • 承载协议:这部分用于在底层物理网络中传输VXLAN数据包
    • 外层MAC头:包含本地VTEP设备的MAC地址、物理网络中下一跳路由设备的MAC地址
    • 外层IP头:包含本地VTEP设备的IP地址、目标VTEP的IP地址
  • 隧道协议:包含UDP头和VXLAN头,用于封装内层数据
    • 外层UDP头:在承载协议之后
      • 源端口:通常是哈希计算得到的随机端口(用于ECMP负载均衡)
      • 目标端口:用于标识内部封装的是一个VXLAN数据包,cilium中默认为8472
    • VXLAN头:紧跟在UDP头之后
      • Flags:标志位,I位必须为1,表示VNI有效
      • VNI:提供1600万个唯一的网路标识,本质是虚拟二层网络的ID
  • 乘客协议:这部分可以看作是被封装的原始数据包
    • 内层MAC头:包含源MAC地址(源Pod的MAC地址)、目标MAC地址(如目标Pod的MAC地址)
    • 内层IP头:包含源IP地址(如源Pod的IP地址)、目标IP地址(如目标Pod的IP地址)、发出的实际数据(TCP/UDP/HTTP等)

从上述报文结构可以看出,原始的二层以太网帧(乘客协议)被封装在基于IP的三层网络(承载协议)中进行传输。也正因为如此,VXLAN能够在三层网络之上“承载”二层网络,实现L2 over L3的虚拟网络模型,从而突破传统二层网络的物理拓扑限制。

对VXLAN的组播泛洪感兴趣的可以去了解下

VTEP映射

VTEP是VXLAN网络中的关键组件,负责对数据包进行封装和解封装。在k8s场景中,VTEP可以看作是k8s各节点的IP地址。

当一个Pod要访问另一个Pod时,这个目标Pod应该发往哪个VTEP呢?cilium维护了一个类似下面的VTEP映射:

Pod IP → Node IP(VTEP)

该映射会存储在eBPF Maps中,并通过cilium Agent在各节点进行同步,在需要的时候,去eBPF Maps中查询即可

MAC地址如何获取?

从上节内容可知,数据包的封装需要两个目标MAC地址(内层和外层),那它们是怎么获取的呢?

  • 外层目标MAC地址:这个MAC地址是目标VTEP的MAC地址,也就是目标主机的MAC地址,如果跨网段的话则是网关的MAC地址。一般是通过标准的ARP协议查找到的,也可以通过cilium agent发现和同步
  • 内层目标MAC地址:这个MAC地址是目标Pod的MAC地址。如果是同节点Pod通信,可以通过ARP或内核邻居表发现;如果是跨节点Pod通信,则是由cilium维护,并保存在eBPF Maps中。这样可以不依赖ARP广播,性能更高

Security ID

在cilium中,Security ID是用于标识工作负载(如Pod)的一种抽象标识。cilium agent会根据Pod的标签为其分配一个唯一的 Security ID。同一组标签的Pod会共享同一个Security ID,从而实现基于“身份”的统一管理。

Security ID是cilium实现网络策略核心机制,通过制定策略可以决定Security ID不同的Pod是否可以互相通信。

Security ID也会存储在eBPF Maps中,并通过cilium agent在各节点同步,保证策略判断的一致性。在VXLAN网络模式下,报文中是不能携带这个Security ID的,故接收端每次解包后,都需要通过源Pod的IP到eBPF Maps中找到其对应的Security ID(反查),再根据制定的策略来决定是否放行。

VXLAN没有拓展字段,应该是携带不了这个Security ID的,有时间后面验证后再看。官方原话是:"Encapsulation protocols allow for the carrying of metadata along with the network packet. Cilium makes use of this ability to transfer metadata such as the source security identity. The identity transfer is an optimization designed to avoid one identity lookup on the remote node."

geneve

如果说VXLAN是固定格式的隧道协议,那Geneve就是一个可拓展的通用隧道协议。Geneve的报文结构如下图所示 可以看到,报文结构中有一个可变长度选项的字段,通过这个字段,使得数据包能够携带丰富的元数据信息,例如Security ID。这就意味着数据平面在进行策略决策时无需再通过IP进行反查,从而减少了对控制平面映射表的依赖并降低了查表开销。然而,这种灵活性也带来了额外的封装开销:由于报文头变长,会压缩有效载荷空间,通常需要调大MTU以避免分片,否则可能引入分片开销并影响转发性能。

除此之外,Geneve还引入了一个protocol字段,用于标识内层payload的协议类型(如Ethernet、IPv4、IPv6等)。这意味着Geneve不像VXLAN那样固定只能封装二层以太网帧,而是可以根据该字段灵活承载不同类型的协议数据。在承载L3的场景下,Geneve可以直接封装IP数据包,并通过protocol指示其为IPv4或IPv6,从而跳过传统的二层封装与MAC转发逻辑,使数据平面能够基于三层信息完成路由决策。

在cilium中,这种能力结合eBPF得到进一步发挥:cilium agent会将Pod IP到节点VTEP IP的映射关系写入本地eBPF Map,数据包在发送时直接根据目标Pod IP查表得到目标节点IP,并进行Geneve封装,通过底层L3网络转发到对端节点;对端节点解封装后再将数据包交付给目标Pod。整个过程无需依赖MAC地址或ARP机制,本质上构建了一个基于L3的分布式路由网络模型。

VXLAN是协议定义网络模型;Geneve是协议允许你定义网络模型

为什么是UDP呢?

为什么VXLAN和GENEVE都使用UDP作为传输层呢?主要原因有以下几点:

  • 易于穿越网络设备:相较于GRE直接封装在IP之上,UDP不容易被屏蔽或丢弃
  • 支持ECMP负载均衡:通过对五元组 (srcIP, dstIP, srcPort, dstPort, protocol) 做哈希可以决定报文的转发路径,ECMP(等价多路径)可实现负载均衡
  • 避免TCP over TCP问题:在一个TCP连接内部再嵌套另一个TCP连接时,外层TCP会把丢包隐藏起来,然后自己做重传,这样内层TCP看不到丢包事件。内层TCP因为没有看到丢包,会继续发送流量,认为网络很好,于是造成更多拥塞。这导致了外层TCP的重传和拥塞控制会延迟并串联内层TCP的行为,造成误判延迟与性能退化,最终出现性能崩溃现象。而UDP不提供可靠性,不会干扰内层TCP,可靠性完全由内层协议(如TCP)负责
  • 低开销,无连接:UDP相比TCP,无连接,头部更小,且无状态。更适合更低延迟、更高吞吐的场景

在Kubernetes场景中,当Pod内运行基于TCP的应用(如HTTP/HTTP2服务)时,应用感知到的仍然是端到端的TCP连接。VXLAN或Geneve仅负责将这些数据包在节点之间传输,并不会干预内层协议的可靠性和拥塞控制机制。换句话说,隧道协议只是“承载层”,而真正的传输语义仍由内层协议决定。

原生路由模式

在原生路由模式下,cilium完全放弃了隧道封装,使Pod流量直接融入底层物理网络。其核心思想是消除Overlay网络,让Pod IP成为底层网络中的一等公民,从而避免封装/解封装带来的额外开销以及隧道带来的MTU、可观测性和性能问题。

在这种模式下,cilium依赖Kubernetes API作为控制平面,并结合BGP将各节点的Pod CIDR(IPAM)通告给集群内其他节点及底层网络设备,使整个网络具备Pod IP的全局可达性。这意味着跨节点通信时,数据包无需携带额外的隧道信息,而是直接使用原始Pod IP作为目标地址,由底层网络根据路由表进行标准的三层转发。

在数据平面上,cilium通过eBPF重构了内核网络路径:数据包在进入内核后即可通过eBPF程序完成路由查找、策略匹配以及负载均衡等操作,而无需经过传统的iptables或复杂的内核路径。与隧道模式不同,路由模式下数据包在XDP/TC层即可直接识别原始Pod IP,因此可以更早地执行服务选择与策略决策,而无需等待解封装过程。

因此,其本质并不仅仅是“用eBPF实现内核级路由转发”,而是通过eBPF构建了一套可编程的数据平面,使每个节点具备类似分布式路由器与策略引擎的能力。在该模型中,跨节点通信本质上是一次真正的L3路由转发(Pod IP → Pod IP),而非通过隧道模拟的二层网络,从而实现更高效、更直接的云原生网络通信。

参考文档

cilium如何替换kube-proxy、实现流量整形、配置DSR后面文章再统一介绍