观测云核心技术解密:eBPF Tracing 实现原理
前言
eBPF 是一种强大的内核技术,允许在内核中安全地执行自定义代码。通过 eBPF,开发者可以在不修改内核源码的情况下,对内核功能进行扩展和监控。eBPF Tracing 利用这一技术,对系统调用、内核函数等进行跟踪,从而实现对应用行为的深入洞察。
与传统的监控方式相比,eBPF Tracing 具有以下优势:
- 无侵入性:无需修改应用代码即可进行监控。
- 高性能:在内核层面执行,减少了对应用性能的影响。
- 细粒度:可以精确到单个系统调用或内核函数的监控。
eBPF(网络)链路实现
实现由三部分构成:
- eBPF 探针程序
- 解析来自 BPF Map 的网络请求数据、线程(协程)跟踪与 eBPF Span 生成
- 链接来自所有节点的汇总 eBPF Span (以下简称 eSpan) 生成链路
eBPF 探针程序
eBPF 探针用于获取程序读写 socket fd 的网络数据,并通过 BPF Map 发送给用户空间的程序进行协议解析。
Linux syscall 函数触发时 eBPF 探针将读取 buf 参数的内容写入 BPF Map,参考 syscall 如下:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
...
写入 BPF Map 的数据将记录触发的 syscall 函数及时间、TCP 连接信息、线程/协程信息以及 Linux syscall 的 buf 参数的部分 payload,以下为结构体:
struct network_data
{
struct
{
__u64 ts; // 函数调用开始时间
__u64 ts_tail; // 函数调用结束时间
__u64 tid_utid; // 内核线程、用户线程 id
__u8 comm[KERNEL_TASK_COMM_LEN]; // task comm
struct
{
__u64 sk; // sock addr
__u32 ktime; // kernel time
__u32 prandom; // random number
} uni_id; // 该网络连接的唯一标识 id
struct
{
__be32 saddr[4]; // src(proc self) ip address; Use the last element to store the IPv4 address
__be32 daddr[4]; // dst ip address
__u16 sport; // src(proc self) port
__u16 dport; // dst port
__u32 pid;
__u32 netns; // network namespace inode. (`stat -L /proc/<pid>/ns/net` or `lsns -t net`)
__u32 meta; // first byte: 0x0000|IPv4 or 0x0001|IPv6; second byte 0x0000|TCP or 0x0100|UDP; ...
} conn; // 连接信息
__u32 tcp_seq; // first byte tcp seq
__u16 _pad0; // 内存对齐填充
__u16 func_id; // 函数编号
__s32 fd; // socket fd
__s32 buf_len; // 读写的网络数据字节数
__s32 act_size; // 实际采集的 payload 的字节数
__u32 index; // 自增,用于标记进程对 socket fd 读写函数调用顺序
} meta; // 网络数据 meta
__u8 payload[L7_BUFFER_SIZE]; // 网络数据 payload
};
由第二部分的 Agent 实现 eBPF 探针程序的加载和卸载,工作示意图:
网络请求解析、线程跟踪与 eSpan 生成
DataKit-eBPF 程序从 BPF Map 获取网络数据,解析网络协议,根据进程和线程信息构建线程跟踪模型。
其构成主要包含:
- 采集:
收集来自 BPF Map 的网络数据等;加载 eBPF 探针程序,attach 到相应的 syscall 函数 - 解析:
- 建立进程的线程-网络请求时序模型,通过为应用内关联网络请求(IN/OUT)提供线程跟踪 ID 实现进程内跟踪;
- 检测网络数据里的网络协议,识别到 HTTP、Redis、MySQL 等协议后进入解析模式,并进行网络协议/进程过滤等;
- 解析网络协议,采集网络请求,生成请求数据,附加根据 TCP 序列号生成跨进程网络跟踪 ID、附加 Otel/DDTrace 等的链路传播信息(如果有);
- 生成:
向生成请求数据中注入线程跟踪 ID,进一步生成 eBPF Span 和生成网络请求聚合数据。 - 发送:
由于当前 eBPF Span 中没有建立跨进程调用的父子关系,需要将所有节点的 eSpan 发送至同一个用于 eSpan 链接的 DataKit 或 DataKit-ELinker 服务, 如图:
eSpan 链接
该部分接收来自所有节点的 eSpan 数据,用于建立这些来自多个节点的 eSpan 的跨进程应用调用关系,实现生成完整的链路。
其构成主要包含:
- 接收 eSpan 并缓存 N 个链接周期(该周期可根据单次应用调用链路的总耗时调参)的 eSpan 数据
- 按链接周期取数据,加工 eSpan,根据 eSpan 中的进程内跟踪 ID 和跨进程网络跟踪 ID 完成 eSpan 间的链接
- 采样,该采样为尾部采样;同时,可根据 Otel/DDtrace 等通过网络传播的链路信息中的采样信息进行采样
- 上传 eBPF 链路数据到观测云