4. 探针与采集器部署:Agent架构、无侵入采集、eBPF技术初探
聊到数据采集,很多团队第一反应就是“装个Agent”。但装什么、怎么装、装了之后对业务有没有影响,这里面的门道其实不少。我见过不少案例,采集器一上线,业务延迟直接飙红,最后排查下来,是Agent把CPU吃满了。
这一章,我们就来拆解探针和采集器的部署逻辑。说白了,就是解决三个问题:数据怎么拿、拿了之后怎么传、怎么保证不影响业务。
4.1 Agent架构的三种主流模式
我个人习惯把Agent架构分成三类。你想想看,不同的场景,选型差别很大。
| 架构模式 | 核心特点 | 典型场景 | 我踩过的坑 |
|---|---|---|---|
| 嵌入式Agent | 集成在应用进程内,作为库或插件运行 | Java APM(如SkyWalking)、Python探针 | 内存泄漏风险高,GC压力大 |
| 独立进程Agent | 以Sidecar或DaemonSet形式部署 | K8s环境、日志采集(Filebeat)、指标采集(Node Exporter) | 资源隔离好,但管理成本上升 |
| 无代理模式 | 通过远程协议直接拉取数据 | SNMP、JMX、Prometheus Exporter | 网络开销大,不适合高频采集 |
选型建议:如果你的应用是Java系,嵌入式Agent配合字节码增强,采集精度最高。但如果是Go或Rust写的网络中间件,我建议优先考虑独立进程模式,避免侵入业务逻辑。
4.2 无侵入采集:不动代码,拿到数据
“无侵入”这三个字,听起来很美好。但实际做起来,你会发现它是有代价的。
什么叫无侵入?就是不改一行业务代码,不重启进程,不引入新的依赖。那数据从哪来?
- 系统调用层面:通过ptrace或eBPF拦截syscall,拿到网络IO、文件IO、进程调度信息。
- 网络流量层面:通过libpcap或AF_Packet抓取网卡数据包,解析协议栈。
- 容器运行时层面:通过cgroup和namespace的接口,读取资源使用量。
我记得有一次,一个核心交易系统需要做全链路监控,但业务方死活不同意改代码。最后我们用了eBPF的方案,在Kernel层面把TCP连接、RTT、重传率全捞了出来。业务方看了数据,说了一句:“你们是不是偷偷埋了探针?”——其实我们什么都没装。
一个小技巧:无侵入采集的“副作用”往往被忽略。比如抓包会导致CPU软中断升高,eBPF程序如果写得不好,可能触发内核panic。建议先在预发环境压测三天,再上生产。
4.3 eBPF技术初探:内核里的“瑞士军刀”
eBPF这几年火得不行。说白了,它允许你在Linux内核里安全地运行一段沙箱程序。不需要改内核代码,不需要加载内核模块,就能动态挂载到各种事件点上。
为什么会这么强?因为传统的内核观测手段,要么太粗(比如/proc/stat),要么太重(比如SystemTap需要编译内核模块)。eBPF在中间找到了一个平衡点。
4.3.1 eBPF的核心工作流
我画了一张图,帮你理解eBPF程序从编写到生效的全过程。
你看,整个流程其实很清晰。用户态程序写好eBPF C代码,通过bpf()系统调用送入内核。验证器会做严格的检查——有没有死循环、有没有越界访问、有没有不安全的指针操作。一旦通过,JIT编译器把它转成机器码,挂载到指定的事件点。
警告:eBPF验证器非常严格。我曾经写过一个遍历链表的程序,因为循环次数无法在编译期确定,直接被拒绝加载。解决办法是使用BPF_MAP_TYPE_PERF_EVENT_ARRAY来传递数据,把遍历逻辑放到用户态做。
4.3.2 实战:用eBPF采集TCP重传事件
光说不练假把式。我们来看一个具体的例子——监控TCP重传。重传率高,说明网络质量有问题,或者对端处理不过来。
下面这段eBPF程序,挂载在tcp_retransmit_skb这个内核函数上。每次发生重传,就把源IP、目的IP、端口号记录下来。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
struct event_t {
__u32 saddr;
__u32 daddr;
__u16 sport;
__u16 dport;
};
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
__uint(max_entries, 128);
} perf_map SEC(".maps");
SEC("kprobe/tcp_retransmit_skb")
int trace_retransmit(struct pt_regs *ctx) {
struct sock *sk = (struct sock *) PT_REGS_PARM1(ctx);
struct event_t event = {};
event.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
event.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
event.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
event.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU,
&event, sizeof(event));
return 0;
}
char LICENSE[] SEC("license") = "GPL";
这段代码的核心逻辑就三件事:读结构体字段、组装事件、输出到环形缓冲区。用户态程序只要从perf_map里读数据,就能实时拿到每一次重传的详细信息。
避坑指南:我曾经在生产环境直接用了BPF_CORE_READ的宏,结果发现内核版本升级后,skc_daddr的偏移量变了。解决方案是启用CO-RE(Compile Once - Run Everywhere),配合BTF信息,让eBPF程序在不同内核版本间自动适配。
4.4 采集器的部署策略
探针写好了,怎么部署到成百上千台机器上?这里有几个原则,是我从多次故障中总结出来的。
- 先灰度,再全量:哪怕eBPF程序再安全,也要先放一台机器跑24小时。我见过eBPF程序导致内存碎片化的案例,灰度期就发现了。
- 资源限制:给Agent设置CPU和内存上限。在K8s里用ResourceQuota,在物理机里用cgroup。我曾经遇到过Agent把宿主机内存吃光,触发OOM Killer的情况。
- 降级机制:如果采集器挂了,不能影响业务。我习惯在Agent里加一个“熔断开关”,当自身资源使用超过阈值时,主动停止采集,只保留心跳。
- 数据缓冲:网络抖动时,采集的数据不能丢。本地磁盘缓冲(比如用mmap写环形文件)是最稳妥的方案。我一般预留500MB的磁盘空间做缓冲。
4.5 三种采集模式的对比
最后,我把嵌入式Agent、独立进程Agent和eBPF无侵入采集放在一起做个对比。方便你根据实际情况选型。
| 维度 | 嵌入式Agent | 独立进程Agent | eBPF无侵入采集 |
|---|---|---|---|
| 对业务的影响 | 高(共享进程资源) | 低(独立进程隔离) | 极低(内核态执行) |
| 数据精度 | 最高(能拿到应用上下文) | 中(只能看到系统视角) | 高(能关联内核与应用) |
| 部署复杂度 | 低(随应用一起发布) | 中(需要单独管理) | 高(依赖内核版本+BTF) |
| 安全风险 | 内存泄漏、GC压力 | 端口冲突、资源抢占 | 内核panic(概率极低) |
| 典型工具 | SkyWalking Agent、Pinpoint | Filebeat、Node Exporter | Cilium、Pixie、BCC |
嗯,看到这里你应该明白了。没有银弹。嵌入式Agent适合精细化追踪,独立进程Agent适合通用采集,eBPF适合那些“不能碰业务代码”的硬骨头场景。我个人更倾向于混合使用——核心链路用eBPF做无侵入兜底,非核心服务用轻量级独立进程采集。这样既保证了覆盖度,又控制了风险。