学习目标
学完本章,你应该能够:
- 讲清楚 BCC 是什么、为什么它能让 eBPF 程序开发「简单」,并说出 BCC 运行一个 eBPF 程序时「用户态 / 内核态 / 数据交互」三个阶段各自发生了什么。
- 读懂并运行 BCC 自带的
hello_world.py/hello.py示例,能在不改业务代码的前提下,用 BCC + kprobe 监听指定进程(按 PID)的 TCP 收发并抓出 HTTP 请求延迟。 - 解释 Beyla 为什么能做到「零侵入」地为 HTTP/gRPC 服务采集 Metrics / Tracing,并写出最小 Beyla 配置(
services.open_ports+otel_metrics_export/otel_traces_export)。 - 讲清 Cilium 取代 kube-proxy / iptables 后数据路径为什么更短更快;掌握
cilium install、cilium monitor、启用 Hubble、hubble observe/hubble ui的用法,理解 service 依赖拓扑如何被零侵入画出。 - 说明 Falco 的「事件采集 → 规则引擎 → 输出」架构,掌握其安装、规则(rules)概念与常见高危安全规则,并能说出如何与 Argo Events/Workflows 联动做自动处置。
前置知识:
- 能打开 K3s / k8s 集群并执行
helm install、kubectl、kubectl port-forward;理解 DaemonSet、Pod、Service、命名空间。 - 掌握上节课 kprobe / uProbe / tracepoint 事件源与 eBPF 执行路径(加载→验证→运行)的基本概念。
- 会一点 Python(能读懂
from bcc import BPF的脚本)和基本的syscall概念(如sys_clone、TCP 收发的内核路径)。 - 了解 OpenTelemetry(OTLP)、Prometheus、Grafana、RED 指标这些可观测性名词。
本章你会动手做的事:
- 用 Terraform 开一台腾讯云 2 核 4G 虚拟机,
apt-get安装 BCC,克隆 BCC 仓库,跑通hello_world.py与hello.py,理解运行过程。 - 写一个 Go HTTP server,再手写一个
delay.py的 BCC 脚本,按 PID 用tcp_sendmsg/tcp_cleanup_rbuf两个 kprobe 零侵入地抓出每个请求的延迟。 - 用 Beyla 的一组 YAML 配置把同一个服务「零侵入」接入指标与追踪,把数据以 OTLP 导出到 Tempo / Prometheus。
- 在 K3s 上
cilium install(配合关闭 flannel/network-policy 的 K3s 参数),部署星战示例应用,cilium enable hubble并打开hubble ui,看服务依赖拓扑。 - 用 Helm 安装 Falco(ebpf 驱动 + falcosidekick),在 Alpine 容器里执行命令制造安全事件,在 Falco UI 里看到告警,并了解自动删除风险 Pod 的处置流程。
先建立整体直觉:一条主线贯穿四个工具
进入本模块前先校准心态:这一章讲的四个东西——BCC、Beyla、Cilium+Hubble、Falco——本质上共享同一个底层技术 eBPF,只是从不同角度使用它:
- BCC 是「开发框架 / 工具箱」:让你亲手写 eBPF 程序、观测内核行为,是理解后面所有工程的启蒙砖。
- Beyla、Cilium+Hubble、Falco 是「开箱即用的产品」:它们把 eBPF 的能力封装好,让你零侵入地获得可观测性或安全能力。
贯穿全章的关键词是**「零侵入」**——不用改一行业务代码,只挂在内核钩子上就把数据取出来。听懂这一条,后面四节就顺了:
flowchart LR
APP[业务应用
不改一行代码] -->|eBPF 挂钩子取数| K[内核 kprobe/uprobe/tracepoint]
K --> BCC[BCC
自写 BPF 观察内核]
K --> BEYLA[Beyla
指标+追踪]
K --> CIL[Cilium+Hubble
网络拓扑+流量]
K --> FALCO[Falco
安全威胁检测]术语提醒(重要!):这是 ASR 转写最考验技术判断的一章。请看下面的「一表纠错」,确保你在阅读任何教程 / 资料时头脑里自动纠正这些错名,后面不再重复。
| 你会看到的错名 | 真实名字 | 是什么 |
|---|---|---|
| Flaco / inspire / f | Falco | Sysdig 出品的云原生运行时安全工具 |
| Flaco-kit / s-kit | falcosidekick | Falco 的告警中转组件(能转发到外部) |
| Metricses / matris | Metrics | 指标 |
| Bella / 贝拉 / 贝 +grap | Beyla | Grafana 的零侵入 auto-instrumentation 工具 |
| V-BF / EPF / APPF | eBPF | 扩展伯克利包过滤器 |
| BC / V-C / BCCD | BCC | BPF Compiler Collection,eBPF 开发框架 |
| CM / CDM / Cium | Cilium | 基于 eBPF 的 CNI 网络插件 |
| Hober / 湖本 | Hubble | Cilium 的可观测性层 |
| Red / rad | RED 指标 | 四大黄金指标之一(Rate/Errors/Duration) |
| 阿over / goflow | Argo Events / Argo Workflow | 事件驱动 + 工作流,用于自动处置 |
大概分法:Metrics 对应「指标」、Tracing 对应「追踪」、Hubble 对应「网络可观测」、Falco 对应「安全」。下面我们一节一节来。
第一讲:BCC 实战——亲手写 eBPF 程序
什么是 BCC,为什么它让 eBPF 变简单
BCC(BPF Compiler Collection)是一个用来开发、编译、加载、运行 eBPF 程序的开源框架,由 Facebook 开源。它的核心目标就一句话:让开发者更轻松地编写、加载、运行 eBPF 程序。
没有 BCC 之前,裸写 eBPF 需要你处理大量底层细节;BCC 把用户空间与内核空间交互、字节码的加载与验证等复杂逻辑封装成好用的 API,让你能用 Python / C++ 等高级语言快速开发 eBPF 程序。转写里的话很实在:BCC 里大部分是 Python 代码,只有内核段会用到一小块 C 代码,所以「会用 Python 就能上手 BCC」。
使用 BCC 的前提:Linux 内核版本大于 4.1(这是 eBPF 的基础门槛,不同功能还各自有更高的版本要求)。
BCC 的核心功能可以归纳为四点:
| 能力 | 说明 |
|---|---|
| 简化 eBPF 开发 | 提供用户空间库和工具,屏蔽了 eBPF 开发中复杂的细节(用户/内核空间数据交互、字节码加载与验证等) |
| 多语言支持 | 提供 Python、C++ 等高级语言 API,可以快速开发 eBPF 程序 |
| 内置工具集 | 内置大量现成工具,覆盖性能监测、网络监控、内核调试、性能分析、安全监控 |
| 丰富内核观测能力 | 提供 Linux 内核数据结构访问能力,可挂载到 kprobe、tracepoint 以及网络数据路径上,高效监控内核行为 |
pandas 之于 Python。原生 API 内核很强大但上手难;框架帮你封装好常用能力,让你用更少的代码做出「能跑」的东西。BCC 内置的 tools/ 目录就像一箱「开箱即用的好工具」。准备环境:Terraform 开云虚拟机 + SSH Remote
因为大家的本地开发环境千差万别,课程统一用一台腾讯云虚拟机做实验。开通方式和前面课程一样,用已经提供好的 Terraform 代码(模板叫 tfrender / module),开一台 2 核 4G 的虚拟机,模板里会顺带装好 VPC 和 K3s(虽然本讲用不到 K3s)。
# 关键拓扑(示意):复用之前的 Terraform 模板开云机
module "vpc" { ... }
module "k3s" { ... } # 本讲其实只用这台机器跑 BCC,K3s 顺带装着
开通后为方便操作,用 VS Code 的 SSH Remote 插件连上去:
- 选择一个远程目录(如
home),SSH 输入公网 IP 和默认密码(课程里默认是password123)。 - VSCode 会在远端下载一个 code-server(VSCode 的前端和后端是分开的,SSH Remote 的原理就是让前端在本地、后端在远端跑)。
- 之后你在 VSCode 里新建的终端,全都是远端 Ubuntu 环境,读写都在虚拟机里,体验就像平时写代码一样。
为什么这么折腾:eBPF 对内核版本和权限有硬性要求,本地(尤其是 Mac / Windows)很难跑通。统一用云 VM 能保证「能跑」。真实想在自己环境跑 BCC 的,请确保 Linux ≥ 4.1 且有 root 权限。
进入远端终端后安装 BCC 并克隆它的官方仓库:
# 1. 用 apt 包管理器安装 BCC 工具集(Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y bpfcc-tools # 安装 BCC 命令行工具
# (部分发行版包名是 python3-bcc,请按系统调整)
# 2. 克隆 BCC 官方 git 仓库(用于看它自带的示例代码)
git clone https://github.com/iovisor/bcc.git
cd bcc
# 3. 我们后面要研究示例脚本,先看仓库里自带的 Python 例子
ls examples/
第一个示例:hello_world.py
进入 BCC 仓库的 examples/ 目录,你会看到 hello_world.py。别看它短,它把「eBPF 程序开发的骨架」全讲透了。完整带注释版本如下:
# hello_world.py —— 事件驱动:只要有进程调用 sys_clone 就打印 Hello World
from bcc import BPF
# BPF(text=...) 里装的是一段 C 语言编写的「内核探针」源码
prog = r"""
// prog 表示这是一个内核探针,并挂载到内核函数上,
// 当指定内核事件发生时执行这段业务逻辑。
// 这里挂在 sys_clone(内核中类似 fork 的进程创建系统调用函数)上:
// 任何进程调用 sys_clone 后,我们的 eBPF 程序就会被触发。
int hello(void *ctx) {
// bpf_trace_printk 是 eBPF 提供的函数,
// 用于向用户空间输出调试信息(本处打印 Hello World)
bpf_trace_printk("Hello World\n");
return 0;
}
"""
# 1. 用 BPF 对象编译并加载上面的 C 程序(源码以 text 形式传入)
BPF(text=prog)
# 2. trace_print() 实时读取并打印内核里 eBPF 程序输出的调试信息(阻塞式)
BPF(text=prog).trace_print()
运行它:
cd /root/bcc/examples/
sudo python3 hello_world.py
你会看到终端不断输出 Hello World。为什么呢?因为 Ubuntu 系统里不断有进程在调用 sys_clone(创建子进程/线程),synchronously 触发我们的 eBPF 程序被调用。你能看到进程名,比如 bash、k3s 等。按 Ctrl+C 终止。
几个关键点消化一下:
- 事件驱动:eBPF 不是轮询,而是「事件发生时被内核调用」。
ctx(hello 的参数void *ctx,对它不用的程序就写上占个位)是提供事件相关信息的上下文参数(Raw Tracepoint 里没有强类型)。bpf_trace_printk输出调试信息,但并不会直接打印到终端——它写进内核 trace 管道,需要用户态trace_print()把它读出来真正打印到控制台。trace_print()是阻塞的,持续监听并打印,直到Ctrl+C终止。
运行一个 eBPF 程序时,到底发生了什么?
这是理解 BCC 最关键的一环。把 hello_world 的运行过程拆成用户态(User Space)、内核态(Kernel Space)、数据交互三层:
1) 用户态(用户空间)
- BCC 调用 clang / LLVM 把
BPF(text=...)里的 C 程序编译成 eBPF 字节码(bytecode)。 - 编译时会做基础处理:语法分析、语义检查,生成 eBPF 的中间表示(IR),最终产出符合 eBPF 虚拟机运行的字节码。
libbcc(BCC 的底层库)调用bpf_program_load()把这个字节码真正加载到内核里。
2) 内核态(Kernel Space)
- 验证器(Verifier) 验证 eBPF 程序:检查是否有非法内存访问、执行流程是否有限(会不会死循环)、是否符合 eBPF 指令集限制等。
- 之后进入 JIT 编译:把字节码编译成本机机器码以提升性能(
t-cee是否启用是可选的;不启用就以字节码方式在 eBPF 虚拟机里跑)。 - 最终运行的 BPF 函数被挂载到内核的 hook 点上,基于事件触发——比如 hello_world 挂在
sys_clone函数入口,拦截sys_clone的调用,在执行时调用我们的 eBPF 程序。
3) 数据交互(User ↔ Kernel)
内核里取到的数据用户空间无法直接访问,靠 eBPF Map 做桥梁:
- 用
bpf()系统调用创建 map; - 也可以把用户数据传递给内核中的 eBPF 程序;
- 用户态调用相关接口(如
perf_buffer_poll)读取 / 操作这些 map 数据。
flowchart LR
subgraph U [用户态]
SRC[C 程序 text] -->|clang/LLVM 编译| IR[中间表示 IR] --> BC[eBPF 字节码]
end
BC -->|libbcc + bpf_program_load| LOAD[加载进内核]
subgraph K [内核态]
LOAD --> VER[验证器 Verifier
检查内存访问/死循环/指令集] --> JIT[编译成机器码] --> HOOK[挂载到内核 hook]
HOOK -->|事件触发| RUN[运行 BPF 程序]
end
RUN -->|写| MAP[(eBPF Map)]
MAP -->|perf_buffer_poll 读取| LST[用户态实时读取展示]这份「用户态 → 内核态 → 数据交互」的心智模型,是后面理解 Beyla、Hubble、Falco 所有产线的基础。它们无非是「封装了更专业的探针 + 更漂亮的输出」。
第二个示例:hello.py——更灵活的绑定与多字段输出
BCC 仓库的 examples 下还有 hello.py,它是 hello_world 的「变种」,解决了两件事:① 程序与 hook 解耦;② 从管道里取多个字段并解析。
# hello.py —— 程序不写死绑定 hook,加载后用 attach_kprobe 动态挂载
from bcc import BPF
# 1. 把 eBPF 程序定义成独立变量 prog
# 好处:定义时不需要强制绑定到某个 hook,load 之后想挂哪挂哪,更灵活
bpf_text = r"""
int hello(void *ctx) {
bpf_trace_printk("Hello World\n");
return 0;
}
"""
# 2. 编译加载这段 BPF 程序(此时还没有绑定到任何 hook)
prog = BPF(text=bpf_text)
# 3. load 后在用户态把 "hello" 函数 attach 到 sys_clone 这个 kprobe 上
# event=挂载点 / fn_name=我们前面定义的 BPF 函数名
prog.attach_kprobe(event="sys_clone", fn_name="hello")
# 4. 阻塞式读取跟踪管道,并把取到的行按字段解析后打印
# (trace_readline 会返回形如 "time 字段 comm … pid … message" 的文本)
while True:
try:
msg = prog.trace_readline()
# 此处可对 msg 做字段切分/解析,例如取出时间、进程名、PID、消息
print(msg)
except KeyboardInterrupt:
break
运行它:
sudo python3 hello.py
输出会更好看:能看到时间、耗时、命令(command)、用户ID/uname、PID、消息(message)等字段。注意与 hello_world.py 的差别——hello.py 没有把一个 BPF 函数一路写死在 BPF(text=...) 里,而是在 Python 层用 attach_kprobe 动态绑定,并且打印前多了一步「从跟踪管道里取行、按字段拆解」的过程,这与我们下面要学的真实项目写法更一致。
实战:用 BCC 零侵入抓取 HTTP 请求延迟
现在动真格。目标场景:服务器上跑了一个 Go 写的 HTTP 服务,我们希望不侵入业务代码、不需要网关,就能拿到它的每个请求延迟。这比业务里自己记耗时更精确(后面解释为什么)。
Step 1:写一个最简单的 Go HTTP Server
在虚拟机里建 demo 目录,写一个极简 HTTP 服务(监听所有路径,打印日志并返回):
// main.go —— 一个极简 Go HTTP 服务
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 打印请求方法和路径到日志
fmt.Printf("recv %s %s\n", r.Method, r.URL.Path)
// 向浏览器返回一段文本
fmt.Fprintf(w, "hello: %s\n", r.URL.Path)
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("start server on :8080")
// 监听 8080 端口
http.ListenAndServe(":8080", nil)
}
在 Ubuntu 上装 Go 并跑起来:
# 安装 Go(这里以 go1.22 linux/amd64 为例)
wget https://go.dev/dl/go1.22.x.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.x.*.tar.gz
export PATH=$PATH:/usr/local/go/bin
go mod init demo
go run main.go # 实现:start server on :8080
Step 2:写 BCC 脚本 delay.py,按 PID 抓延迟
核心思路:一个请求的耗时 = 内核里「发送完成」与「处理完成」两个时刻之差。我们用两个 kernel 探针:
- 开始点
tcp_sendmsg:TCP 发送消息的时刻(代表请求被发出的时间点); - 结束点
tcp_cleanup_rbuf(TCP 读缓冲清理):接收完成、数据读走的时刻。
两个时间戳相减即得该进程处理一个请求的延迟。完整带注释的 delay.py 如下:
# delay.py —— 用 BCC + kprobe 按 PID 抓取某进程的 HTTP 请求延迟
import argparse # 解析命令行参数
from bcc import BPF
# ---- 1. 构造命令行参数解析器:接收用户想监控的目标 PID ----
parser = argparse.ArgumentParser(description="Track HTTP request latency by PID")
parser.add_argument("-p", "--pid", type=int, required=True, help="PID of process to trace")
args = parser.parse_args()
pid = args.pid # 拿到目标的 PID >> {{ pid }} 占位
# ---- 2. eBPF 内核 C 程序(用多行字符串)----
bpf_text = r'''
#include <uapi/linux/ptrace.h> // eBPF 用户态跟踪所需头文件
#include <linux/sched.h> // 进程调度相关结构体定义
// 结构体:把跟踪数据从内核态发送到用户态
struct data_t {
u32 pid; // 进程 ID
u64 ts; // 延迟时间
char comm[16]; // 进程名
};
// 哈希表:存储每个 PID 的开始时间戳,用于后面计算延迟
BPF_HASH(start, u32); // key = PID
// 环形缓冲区:把事件从内核态传递到用户态
BPF_PERF_OUTPUT(output);
// ---- 开始点:tcp_sendmsg 表示“发送 TCP 消息”的时刻 ----
int trace_entry(struct pt_regs *ctx, struct sock *sk) {
u32 tgid = bpf_get_current_pid_tgid() >> 32; // 取当前进程 PID
if (pid != 0 && tgid != pid) { // 只跟踪用户指定的 PID
return 0;
}
u64 ts = bpf_ktime_get_ns(); // 获取内核时间戳(单位 ns)
start.update(&tgid, &ts); // 把时间戳存入哈希表(key=PID)
return 0;
}
// ---- 结束点 : tcp_cleanup_rbuf 表示“TCP 完成接收、缓冲已读出”的时刻 ----
int trace_return(struct pt_regs *ctx, struct sock *sk) {
u32 tgid = bpf_get_current_pid_tgid() >> 32; // 同样取 PID 并按需过滤
if (pid != 0 && tgid != pid) {
return 0;
}
u64 *tsp = start.lookup(&tgid); // 查哈希表,取之前记的开始时间
if (tsp == 0) { // 没找到开始点,说明漏了事件
return 0;
}
u64 delta = bpf_ktime_get_ns() - *tsp; // 延迟 = 当前时间 - 开始时间
start.delete(&tgid); // 用完删除,便于下次计算
struct data_t data = {};
data.pid = tgid; // 记录进程 ID
data.ts = delta; // 记录延迟
bpf_get_current_comm(&data.comm, sizeof(data.comm)); // 获取进程名
output.perf_submit(ctx, &data, sizeof(data)); // 发送给用户态
return 0;
}
'''
# ---- 3. 替换 eBPF 程序中的 PID 占位符,并加载 ----
bpf_text = bpf_text.replace("{{ pid }}", str(pid))
BPF(text=bpf_text) # 编译加载
# 注意:上面用 start.update/lookup 需要把 bpf program 拿出来挂载。
# 更标准的写法是用 BPF(text=...).load() 后分别 attach_kprobe,如下:
# b = BPF(text=bpf_text)
# b.attach_kprobe(event="tcp_sendmsg", fn_name="trace_entry")
# b.attach_kprobe(event="tcp_cleanup_rbuf", fn_name="trace_return")
# ---- 4. 定义用户态回调:处理 eBPF 发送来的事件 ----
def print_event(cpu, data, size):
event = BPF_OFFSET(data, struct_data) # 解析出 data
# 打印时间、PID、进程名、延迟(ns 换算成 ms)
print("%s %-6d %-16s %8.2f ms" % (time.strftime("%H:%M:%S"),
event.pid, event.comm, event.ts / 1e6))
# ---- 5. 把事件缓冲区绑定到回调,并进入阻塞监听循环 ----
b["output"].open_perf_buffer(print_event) # 绑定
print("tracing HTTP request for PID", pid)
while True:
try:
b.perf_buffer_poll() # 阻塞式读取事件,触发 print_event
except KeyboardInterrupt:
break
运行它需要小技巧:先找到 Go server 的 PID,再传进去。
# 找到正在运行的 go server 的 PID
ps aux | grep main | grep -v grep # 记下第二列的 PID,例如 22333
# 用 BCC 脚本跟踪该 PID 的 HTTP 请求延迟
sudo python3 delay.py -p 22333
# 另开一个终端,向 8080 发几次请求制造流量
curl http://127.0.0.1:8080/test
curl http://127.0.0.1:8080/hello
朝着该进程发了 curl 之后,delay.py 终端就会不断打印出类似:
09:33:12 22333 main/my-server 0.83 ms
09:33:14 22333 main/my-server 1.12 ms
Step 3:为什么这种延迟比业务自计耗时更精确?
转写里特别强调了这一点。业务里用「收到请求 → 处理 → 返回」算出来的耗时,其实包含了用户态代码调度、socket 层排队、用户/内核态来回切换等一系列开销,而且位置滞后。
而用 tcp_sendmsg / tcp_cleanup_rbuf 在内核 TCP 收发路径上取的两个时间戳,直接在数据包进出内核 socket 的瞬间打点,更贴近「网络视角 / socket 处理视角」的真实耗时,且不带业务代码的额外开销和抖动。这就是零侵入观测更客观、更干净的原因。
第二讲:eBPF + Beyla 实现零侵入 Metrics 和 Tracing
Beyla 是什么:把 eBPF 能力「嵌进应用观测」
上一讲我们是自己写 BCC 脚本 + 手动传 PID。到了 K8s 集群,服务成千上万,手动传 PID 显然不可能。Beyla(Grafana 开源的国产 BCC 同级品) 就是为了解决「怎么把 eBPF 拿到的指标和 Pod/Service 元数据自动关联」而生。
Beyla 是一款基于 eBPF 的应用程序自动检测工具:它把 eBPF 能力内嵌进自身应用,自动检测应用及操作系统的网络层,并以零侵入方式捕获 Web 事物以及 Linux 里 HTTP / HTTPS / gRPC 服务(本质都在 TCP 层)的指标与跟踪,比如速率(Rate)、错误(Errors)、持续时间(Duration) 等 RED 指标。
| Beyla 能力 | 说明 |
|---|---|
| 多语言零侵入集成 | 支持 Go、C++、Rust、Python 等多种语言 |
| 厂商无关 | 以 OTel 格式和原生 Prometheus 指标生态导出数据 |
| 分布式跟踪 | 对 gRPC server 可做分布式追踪,且是零侵入的,无需任何集成 |
| 跨环境运行 | 可在任何 Linux 环境运行 |
| K8s 元数据关联 | 用 Pod 和 Service 的元数据(label) 修饰指标与跟踪(relabel),方便在 Grafana 里把 service/pod 对应到 eBPF 指标上 |
它补上了 BCC 实战里缺的那块拼图:在 K8s 里监听到那么多服务的 eBPF 指标,怎么跟 Pod/Service 元数据关联起来?——Beyla 就像 Prometheus 一样帮你做 relabel,把 eBPF 指标和对应的 K8s 资源关联起来。
使用 Beyla 的要求:Linux 内核版本 ≥ 5.8,并满足 eBPF 最基本要求(开启 BTF 等)。
零侵入的原理:detect by open_ports,导出走 OTLP
Beyla 怎么知道该观测哪个服务?配置 services 后它自动去探测——最常用的匹配方式就是按「开放的端口」自动识别监听端口上的 HTTP/gRPC 服务,然后通过 eBPF uprobes/tracepoints 自动埋点采集。
一个最小可用的 Beyla 配置(beyla.yml)长这样:
# beyla.yml —— 零侵入自动埋点的最小配置
# 告诉 Beyla 观测哪些服务:按开放端口匹配,无需改任何业务代码
services:
- open_ports: ["8080", "9001"]
# 也可以按 executable / kubernetes metadata 匹配,这题最常用 open_ports
# 用 K8s 元数据去修饰(relabel)指标和追踪
# 让 Grafana 里能直接看到 service / pod / namespace 标签
attributes:
kubernetes:
enable: true
# 指标导出:走 OpenTelemetry OTLP(Prometheus 后端 / Grafana Mimir 也能接)
otel_metrics_export:
endpoint: http://otel-collector:4317
# interval: 15s # 指标上报间隔(可选)
# 追踪导出:走 OTLP,链路可以打到 Tempo 这类后端
otel_traces_export:
endpoint: http://otel-collector:4317
# insecure: true # 非 TLS 场景开关
把它放进 K8s,Beyla 作为 DaemonSet 或 sidecar 运行后:
# 以 DaemonSet 方式部署 Beyla(示意)
kubectl apply -f beyla-daemonset.yaml
kubectl logs -f <beyla-pod> # 看到日志显示已自动发现 8080 的服务
由于导出的数据是标准 OTel / Prometheus 格式,你可以把它们接到统一的 OTLP Collector,再分发到:
- Tempo(追踪):在 Grafana 里看到零侵入产生的分布式调用链;
- Prometheus / Mimir(指标):在 Grafana 里看到它自动采集的 RED 指标。
flowchart LR
APP[业务服务
8080/gRPC 端口] -->|open_ports 自动发现| BEYLA[Beyla
eBPF uprobe 自动埋点]
BEYLA -->|OTLP| COL[OTel Collector]
COL --> TEMPO[(Tempo 追踪)]
COL --> PROM[(Prometheus/Mimir 指标)]
TEMPO --> GF[Grafana]
PROM --> GF对比第一讲的收获:第一讲我们是手动写 BPF + 手动传 PID;用 Beyla 我们配置文件声明端口即可自动埋点、自动关联 K8s 元数据、以标准格式导出——可观测性从此真正「零侵入、可规模化」。这也是 Grafana 生态里 INFOQ 等团队很喜欢它的原因。
第三讲:eBPF + Cilium + Hubble 实现零侵入可观测性
Cilium:以 eBPF 为核心的高性能网络插件
Cilium 是一个用于云原生(特指 K8s)环境、以 eBPF 为核心的开源项目,主打高性能、安全、网络与服务互联,专注容器与微服务之间的网络连接。为什么 Cilium 主要做网络?因为 eBPF 在网络层面能发挥的作用最大。
Cilium 的三大核心能力:
| 能力 | 说明 |
|---|---|
| Networking(网络连接) | 提供高性能容器网络连接:动态负载均衡、服务发现、网络多租户等;支持 CNI(容器标准网络接口),甚至可以替代 iptables |
| Network Security(网络安全) | 用 eBPF 流量过滤器实现细粒度安全策略,可提供 L3/L4 和 L7 层网络安全策略,支持隔离应用通信 |
| Observability(可观测性) | 在服务层面实时观测流量,通过 Hubble 提供强大流量跟踪能力 |
为什么 Cilium 能取代 iptables:数据路径对比
转写里强调得最多的对比,就是「有 iptables 和没有 iptables 的容器网络路径差异」。
原来的 iptables 时代(强依赖 iptables):网络包和路由在处理时经过 iptables 的多层处理链(PREROUTING、POSTROUTING、FORWARD 等),最终才到达容器网络层。
- 每一层 iptables 的匹配和链路转发都增加额外开销;
- 需要频繁在用户态 / 内核态之间切换上下文;
- 整体路径复杂,网络包需多次流经内核数据处理模块才能到达目的地。
问题很明显:在大规模 K8s 集群里,iptables 性能差,且随着规模/流量增长性能显著下降;路径长也显著增加服务间通信延迟。
有了 Cilium(eBPF 时代):网络链路变得非常简单——原先左边一大块 iptables 链全部消失,数据包直接经 eBPF 转发到虚拟网络接口;我们可以在内核里直接运行自定义程序实现高效数据包处理,完全替代 iptables。
| 维度 | iptables 时代 | Cilium(eBPF) |
|---|---|---|
| 数据路径 | 多层链 + 频繁状态切换 | 直接 eBPF 在内核内转发,路径极短 |
| 开销 | 每层匹配 + conntrack 额外开销 | 减少 iptables / conntrack 开销 |
| 灵活性 | 静态规则 | eBPF 程序可动态加载,适应复杂网络 |
| 延迟 / 性能 | 规模增大性能显著下降 | 路径短、低延迟、高性能 |
flowchart LR
subgraph OLD [原来:iptables 数据路径]
PKT0[网络包] --> ROUTE0[路由] --> PRE0[PREROUTING] --> POST0[POSTROUTING] --> FWD0[FORWARD] --> VETH0[容器虚拟网卡]
end
subgraph NEW [Cilium:eBPF 数据路径]
PKT1[网络包] -->|"内核内直接运行 eBPF 程序转发"| VETH1[容器虚拟网卡]
endCilium 的架构:Operator(控制平面)+ Agent(数据平面)+ CNI
Cilium 架构里有几个核心角色:
- Cilium Operator(管理平面):以 Deployment 方式运行,与 K8s API Server 交互,同步集群状态、感知集群变化;还会与 Cilium Agent 交互,在网络拓扑变更时动态加载特定 eBPF 程序来实现流量转发设置。
- Cilium Agent(数据平面):以 DaemonSet 方式运行在每个 Node 上。
- Cilium CNI:负责配置节点的 CNI,以便使用 Cilium 的网络插件。
- Hubble-Relay:通过 gRPC 与 Agent 通信,提供集群级可观测性,给 Hubble UI / CLI 提供数据源。
Hubble:Cilium 的可观测性层
Hubble 与 Cilium 一般安装时一起装,是不可或缺的组件。它是一个分布式的网络与安全的可观测性平台,建立在 Cilium(之上也是 eBPF)之上,能以透明 / 零侵入方式分析服务流量与网络基础设施的通信指标。
Hubble 的核心功能:
| 功能 | 说明 |
|---|---|
| 服务依赖 / 通信图 | 零侵入直观展现服务之间的依赖关系与通信图,帮助你理解集群里服务如何互相调用(如 HTTP、gRPC) |
| 网络监控与告警 | 检测网络通信失败(L4 / L7 层中断),如 DNS 解析失败等问题 |
| 应用程序监控 | 采集特定服务或集群的 5xx / 4xx 响应码发生率,以及 P99 / P95 服务间通信延迟(RED 指标,同样借助 eBPF 拿到) |
Hubble 架构:它作为 DaemonSet 运行在每个节点,与 Cilium Agent 协同工作,从 eBPF 中提取网络监控数据;对外暴露 gRPC 服务接口供其他组件订阅(如给 Hubble UI 提供数据源),还暴露 /metrics 接口给 Prometheus 拉取做监控与性能分析。
实战:在 K3s 上装 Cilium + Hubble
第一步:K3s 特殊参数。 用 K3s 装 Cilium 需要给 K3s 两个特殊参数(改写法里的 K3s module):
# 与默认的 flannel / network-policy 并存会冲突,安装 Cilium 时要关掉它们
# 在 K3s 的启动参数里加:
curl -sfL https://get.k3s.io | \
INSTALL_K3S_EXEC="--flannel-backend=none --disable-network-policy" sh -
第二步:安装 Cilium CLI 与 Cilium。 本机装 Cilium CLI(到 Cilium GitHub Release 下载对应平台版本解压到 /usr/local/bin):
# 1. 下载并解压 cilium CLI,放到 PATH 里(Linux/Mac/Windows 各自下载对应版本)
# 2. 配置 kubeconfig 指向你的集群
export KUBECONFIG=$(pwd)/k3s.yaml
# 3. 用 cilium install 一键安装(可指定版本)
cilium install --version 1.16.4
# 4. 查看安装状态
cilium status
因为禁用了两个配置(flannel / network-policy),状态里会有一些警告,等 Pod 全部 running 即安装完成。
第三步:部署示例应用并启用 Hubble。 部署 Cilium 提供的星战示例应用(tiefly / x-wing → deathstar 三个服务):
# 创建示例应用
kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/.../demo-app.yaml
# 启用 Hubble(即安装 Hubble 组件)
cilium enable hubble
# 查看 Hubble 组件状态
cilium status
# 安装 Hubble CLI(同样到 Hubble Release 下载对应平台版本)
第四步:生成流量并观测:
# 1. 把 Hubble relay 的端口转发出来(4245 端口提供数据源)
kubectl port-forward -n kube-system svc/hubble-relay 4245:80 &
# 2. 用 Hubble CLI 直接拉取流量数据(会看到大量 DNS / HTTP / code 数据)
hubble observe --port 4245
# 3. 启用 Hubble UI
cilium hubble enable --ui
cilium hubble ui # 该命令会自动做端口转发并打开浏览器
# 4. 生成测试流量
kubectl exec -it tiefly -- curl http://deathstar.default.svc.cluster.local
kubectl exec -it x-wing -- curl http://deathstar.default.svc.cluster.local/request-landing
在 Hubble UI 里你会看到 tiefly 与 x-wing 两个服务都请求了 deathstar 的拓扑结构,并且能点开具体服务查看延迟、请求耗时、端口号、label 等细节。
和 Beyla 的相似点:Hubble 和 Cilium 也实现了「从 eBPF 取数据 → 与 K8s 服务元数据关联(relabel)→ 用 UI 可视化」的整套链路,让你直观地看服务间请求链路和错误归属——只是它专注的是「网络流量与连接」这一层。
flowchart LR
EPF[eBPF 探针采集网络流量] --> AGENT[Cilium Agent
DaemonSet 每节点]
AGENT -->|gRPC| RELAY[Hubble Relay
集群级聚合]
RELAY --> UI[Hubble UI 拓扑图]
RELAY --> CLI[hubble observe 命令行]
AGENT -->|/metrics| PROM[Prometheus 抓取]第四讲:eBPF + Falco 实时监测 K8s 安全威胁
为什么用 eBPF 做安全威胁检测
前三节讲的全是可观测性(指标、追踪)。其实 eBPF 还有一个重要场景——安全威胁检测。
eBPF 可以挂在很多 kprobe / tracepoint 上,其中有一些「你很感兴趣的安全威胁探针」。最典型的一种威胁:有人进入你的容器里执行了命令(shell 命令)——一旦某人进入了容器并执行了命令,就等于你的 shell 被拿到了,他可以在容器里运行任何东西,甚至威胁到节点。所以我们可以用 eBPF 实时监测集群里的安全威胁。
Falco 的架构:事件采集 → 规则引擎 → 输出
Falco(转写里被识别成 Flaco / f,这里以官方名称为准)的流程可以从「获取事件 → rules 引擎匹配 → 输出」三部分理解:
flowchart LR
LINUX[Linux / 容器运行时] -->|syscall / ring buffer / eBPF probes| CAP[falco 事件采集]
CAP --> LIB[libsinsp
完善信息 + 过滤]
LIB --> RULES[规则引擎 Rule Engine
与 rules 匹配]
RULES --> OUT[输出]
OUT --> STDOUT[标准输出 / 文件]
OUT --> KS[falcosidekick
推到外部系统]
KS --> EXT[邮件 / IM / webhook / 队列...]Falco 事件采集的原理分两部分:
- 默认方式:基于
libsinsp(一个 C++ 库)构建的内核模块,从 Linux 的 ring buffer(环形缓冲区) 获取系统事件(syscall)。 - eBPF 方式:通过 eBPF probes 获取事件,使用 eBPF 取事件的要求是 内核 ≥ 4.16。
获取到事件后,事件被发送到 libsinsp 组件,它负责完善信息(eBPF 里的信息往往很不好读,libsinsp 把它加工补全)并使用内部规则引擎过滤事件,最后交给 rules 引擎做策略处理。
实战:安装 Falco 并制造安全事件
第一步:安装 Falco(用 Helm):
# 1. 添加 falcosecurity 官方 repo
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
# 2. 创建命名空间并安装
kubectl create ns falco
helm install falco falcosecurity/falco \
--namespace falco \
--set driver.kind=ebpf \ # 用 eBPF 驱动(内核 >= 4.16)
--set falcosidekick.enabled=true # 开启 falcosidekick(UI/告警中转组件)
# 3. 等所有 Pod ready(falco 进程要加载 eBPF 程序,启动会慢一些)
kubectl get po -n falco -w
关键参数说明:
driver.kind=ebpf指定用 eBPF 探针而非默认内核模块;falcosidekick.enabled=true开启 falcosidekick——它是 Falco 的告警中转组件,既能提供 Web UI 展示安全事件,也能把事件转发到外部系统(邮件、IM、Slack、Webhook、队列等)。
第二步:制造一个"安全威胁"。 跑一个一直存活的 Alpine 容器,然后 exec 进去执行一条 shell 命令,模拟「Pod 权限泄露、有人进入容器执行命令」:
# 1. 创建一个长期存活的 Alpine 容器
kubectl run alpine --image=alpine --restart=Never -- sleep 1d
# 2. 等它在 Running 后,进去执行一条命令(模拟攻击者在容器里执行命令)
kubectl exec -it alpine -- sh -c "uptime"
# 3. 从 Falco 的日志里看安全事件
kubectl logs -n falco <falco-pod> | grep -i "shell" | head
Falco 日志会输出类似一段安全事件:
09:40:01.123 Notice A shell was spawned in a container with an attached terminal
(user=root user_loginuid=-1 ... container=27f9... container_name=alpine image=alpine
node=gke-... command=sh -c uptime)
它明确告诉你:哪个容器里运行了 shell、容器名、节点名、执行了什么命令(sh -c uptime)——这正是 eBPF + 规则匹配检测出的真实安全威胁。
第三步:用 falcosidekick UI 查看。
# falcosidekick 默认暴露一个名为 falcosidekick 的 service,端口 2802
kubectl port-forward -n falco svc/falcosidekick 2802:2802 &
# 浏览器访问 http://localhost:2802
进去后能看到一个 Notice 事件——就是我们刚才看到的「Terminal shell in container」,在 Events 里能看到更详细信息(container id、container name、执行的命令内容等)。
转写原话点睛:Falco 本身只会输出日志,不能直接告警,日志读起来也不方便。所以我们的
skeleton/falcosidekick组件作为安全事件控制台,还支持把安全事件发送到外部系统。
Falco 建议监控的高危安全事件
转写里归纳了生产环境特别有用的几类高危安全事件,建议重点监控:
| 类别 | 说明 / 示例 |
|---|---|
| Shell 命令操作 | 容器内执行 bash / sh、如 Terminal shell in container |
| SSH 登录 | 检测到容器 / SSH 登录行为 |
| 修改用户名密码 / 提权 | 改密码、创建用户、特权提升 |
| 导出数据库 | 生产测试环境的数据库跑了导出(pg_dump、mysqldump 等),有数据丢失风险 |
| VPN 操作 | 打开 OpenVPN 等异常网络通道 |
| 定时命令 | 有人动了 cron / 定时任务配置 |
| 配置文件改动 | 有人碰了 chme type 之类的敏感配置 |
| 查找/删除日志 | 攻击者在攻击后删除日志、抹除痕迹 |
Falco Rules:规则引擎怎么写
Falco 规则的原理是事件(conditions)匹配——每个规则由条件(condition)和输出(output)组成,条件里引用了事件来源与字段:
# rules:检测「容器内执行了终端 shell」的规则(精简示意)
- rule: Terminal shell in container
desc: A shell was used as entrypoint/exec in the container with an attached terminal
condition: >
spawn_rename and container and
shell_procs and proc.tty != 0 and container_entrypoint
output: >
A shell was spawned in a container with an attached terminal
(user=%user.name user_loginuid=%user.loginuid ... container=%container.id
container_name=%container.name image=%container.image.repository node=%k8s.node.name
command=%proc.cmdline)
priority: NOTICE
tags: [mitre_execution, container]
转写提醒:手写 rules 其实很复杂,关键是
condition里的字段(事件来源 + 字段)——它们都是 eBPF 抓到、再由 Falco/libsinsp 加工出来的。好消息是,生产里常见的高危事件,Falco 默认 rules 已内置。
默认 rules 从哪来?在 falcosecurity/rules 官方仓库里,规则文件按成熟度分为三档演进:
| 档位 | 说明 |
|---|---|
| sandbox | 孵化中的规则 |
| incubating | 晋级为稳定中的规则 |
| production | 生产级(默认启用) |
默认 rules 内容非常丰富,比如有 Terminal shell in container、数据库导出相关、useradd/用户密码操作、OpenVPN、pg_dump、mysqldump 等——这些就是刚才列举的高危事件的来源。你可以在默认规则之上做自定义补充。
进阶:与 Argo 联动实现全自动处置
最后分享一个非常实用的最佳实践:告警 ≠ 处置,把安全事件自动化处理掉。
流程是:Falco 检测到风险 Pod → falcosidekick 通知 Argo Events → Argo Events 触发 Argo Workflow → workflow 创建一个「删除 Pod」的 workflow → 自动删除有风险的 Pod。
sequenceDiagram
participant F as Falco
participant KS as falcosidekick
participant AE as Argo Events
participant AW as Argo Workflow
F->>KS: 检测到高危安全事件
KS->>AE: webhook 推送事件
AE->>AW: 触发工作流
AW->>AW: 创建删除 Pod 的 workflow
AW->>K8s: 自动删除有风险的 Pod设想有人进入 Pod 执行了一条导数据的命令,两三秒后他发现Pod 没了——因为 Falco 检测到后自动删了它,攻击者无法达到目的。你也无需自己看日志、登录 Pod 判断风险、手动删除——整套流程全自动化跑起来。Argo Events / Argo Workflow 是可选的加分项,感兴趣可以自行实践。
总结
这一章你从「手写 eBPF」一路走到了「开箱即用的 eBPF 产品」,主线始终是零侵入:
- BCC 实战:理解 eBPF 程序「用户态编译加载 → 内核验证执行 → map 交互」的完整流程;跑通
hello_world.py/hello.py;亲手写了delay.py,用tcp_sendmsg/tcp_cleanup_rbuf两个 kprobe 按 PID 零侵入抓出 HTTP 延迟,明白它比业务自计耗时更干净。 - Beyla 零侵入 Metrics/Tracing:用
services.open_ports自动探测服务,自动埋点 HTTP/gRPC,以 OTLP 导出到 Tempo / Prometheus,并用 K8s Pod/Service 元数据做 relabel,解决了「BCC 手动传 PID 没法规模化」的问题。 - Cilium + Hubble 网络可观测:Cilium 用 eBPF 取代 iptables,把冗长多跳的数据路径缩短到「内核内直接转发」,低延迟高性能;掌握
cilium install、cilium enable hubble、hubble observe / hubble ui,零侵入地画出服务依赖拓扑。 - Falco 安全威胁检测:用 eBPF 在容器内执行 shell、SSH 登录、改密、导库等高危事件做实时监测;理解「事件采集 → libsinsp 完善 → 规则引擎匹配 → 输出」架构;掌握 rules 概念与 falcosidekick 中转,并了解与 Argo 联动自动删除风险 Pod 的处置最佳实践。
至此,你已把 eBPF 从「观测网络 / 指标」延伸到「观测服务拓扑」乃至「守护集群安全」,并且全程不碰业务代码。这些零侵入采集到的指标、追踪和安全事件,接下来就是喂给 AIOps 做异常检测、根因定位与自动修复的优质数据源。
自测题与动手练习
自测题(合上书能答出来,才算懂):
bpf_trace_printk 打印的内容为什么不会直接出现在终端?要如何显示?bpf_trace_printk 输出写进内核的 trace 管道,并不会直接打到终端。需要用户态调用 trace_print() 这个阻塞方法实时读取并打印,直到 Ctrl+C 终止。tcp_sendmsg(发送 TCP 消息的时刻),结束点是 tcp_cleanup_rbuf(TCP 完成接收、缓冲被读走的时刻)。用开始与结束两个时间戳做差得到进程处理请求的延迟;为避免影响其他进程,用 bpf_get_current_pid_tgid 判定当前 PID 并只跟踪用户指定的 PID。services.open_ports 自动探测端口上的 HTTP/gRPC 服务并自动埋点,再用 K8s Pod/Service 元数据 relabel 关联,导出 OTLP,从而无需逐进程传 PID。Falco 通常监控哪些高危安全事件?
动手练习(建议真做一遍):
- 完整跑一遍本章的
delay.py最小闭环:起一个 Go HTTP server,ps aux | grep main拿到 PID,sudo python3 delay.py -p <PID>开始跟踪,另开终端用curl http://127.0.0.1:8080/xxx造几次流量,观察打印出的 ms 延迟;再把delay.py里的 PID 过滤去掉跑一次,看它会不会把系统里所有走 TCP 的进程都"看进眼里",体会"零侵入 + 精确过滤"两个特性要同时具备才有意义。 - 在 K3s 上部署 Cilium 并开启 Hubble:先
hubble observe --port 4245看一次真实的 service 间请求(tiefly → deathstar),再在cilium hubble ui里点开 deathstar 服务看 P99/P95 通信延迟与 4xx 数,对照本章的 Cilium 数据路径图,用一句话说说为什么它比 iptables 时代快在哪。 - 用 Helm 装一套 Falco(
driver.kind=ebpf+falcosidekick.enabled=true),在 alpine 容器里kubectl exec -it alpine -- sh -c "uptime",到 falcosidekick UI 里打开这条 Notice 事件、看全字段;再在默认 rules 基础上加一条自定义规则(比如给"检测到 mysqldump 导出"专门弹一条告警),体会condition里字段匹配的实际手感。
本章小结
- 这一章的四个主角分享同一条 eBPF 底层:编译成字节码 → 内核验证 → JIT → 挂到 hook → 事件触发 → 数据经 Map/管道回流用户态。
BCC是"自己写得动"的开发框架,Beyla / Cilium+Hubble / Falco是"开箱即用"的封装产品,本质都在把这条流水线用好。 - 延迟观测要抓住"开始点/结束点"这两个内核 socket 时刻(
tcp_sendmsg/tcp_cleanup_rbuf),比业务自计耗时更干净;规模化靠 Beyla 的open_ports自动发现 + OTLP 导出 + K8s 元数据 relabel,可观测性从此可铺到整个集群而不再依赖手动传 PID。 - 网络与安全是 eBPF 最能发力的两块:Cilium 用 eBPF 删掉 iptables 的多层链、在内核内直接转发换回低延迟;Falco 用 eBPF 探针把容器内执行 shell、SSH 登录、导库、开 VPN 等高危行为实时报给 falcosidekick,再经 Argo 自动删除风险 Pod——「告警」终于能闭环成「处置」。
- 全程零侵入——不碰一行业务代码,就拿到了指标、追踪、服务拓扑与安全事件。这些恰恰是下一步喂给 AIOps 做异常检测、根因定位与自动修复的优质数据源。下一篇我们将把这些零侵入观测数据接回 Agent / Operator 体系,走通"观测 → 决策 → 自动处置"的完整闭环。