Nettrace工具学习记录之流程梳理

开源工具学习记录之流程梳理

近期对腾讯的的开源项目: nettrace(网络故障分析工具) ,进行源码学习。
开源仓库:Nettrace开源仓库
开源工具实现注释:nettrace学习记录

Nettrace工具学习记录之流程梳理

nettrace工具在编译成功后,会生成nettrace可执行文件,当我们通过传参使用该工具时,会经历以下4个阶段,分别是准备阶段、数据采集阶段、数据处理阶段、数据分析阶段。

初步介绍一下nettrace整体流程:

  • 当用户通过参数控制启用nettrace某一功能后(开启某模式)后,首先会进入准备阶段,该阶段会构建trace tree、通过解析参数设置跟踪模式、根据确定的跟踪模式启用对应的跟踪点(这里涉及到了BPF程序自动挂载技术);
  • 当确定了启用哪些跟踪点后,将会进入到内核数据采集阶段,在该阶段,会根据启用跟踪点自动生成的BPF程序来对内核数据进行过滤与采集(这里涉及到BPF程序自动生成技术),并将采集到的数据传输到Perfbuffer中,等待用户态数据处理模块对其进行处理;
  • 在数据采集阶段采集到内核数据并传输至Perfbuffer中后,将会根据对应的跟踪模式触发对应的数据处理模块,将收到的数据进行数据处理与输出;
  • 对于大部分的跟踪点,都定义了所对应的数据分析器,如果需要,会进入数据分析器进行数据分析;

在这里插入图片描述

1.数据采集流程梳理

1.1 参数控制->准备阶段

用户在通过字符串界面输入传参后, 会执行以下步骤:

在这里插入图片描述

  • 1.首先会进入准备阶段,该阶段会根据构建跟踪点组也即图中的trace树;

在这里插入图片描述

  • 2.在将nettrace项目所涉及到的所有跟踪点构建成树后,会将用户的传参进行解析,来确定跟踪模式(该项目提供了八种跟踪模式,不同的跟踪模式所使用到的跟踪点以及数据处理方式均不相同);

  • 3.在确定了跟踪模式后会根据跟踪模式在trace树中启用相关的跟踪点;

    在这里插入图片描述

1.2 准备阶段->数据采集

经过准备阶段将trace树、跟踪模式、要启用的跟踪点设置完毕后;将进行数据采集阶段,在数据采集部分,会根据跟踪模式及挂载的跟踪点,进行数据收集和数据过滤,并将数据传输至perfbuffer处;

在这里插入图片描述

  • 1.BPF程序自动生成:对于传统的ebpf程序,会对每个跟踪点定义其数据收集数据过滤逻辑;而nettrace则是通过定义了三个宏,分别是KPROBE_DEFAULTTP_DEFAULTFNC,分别对应默认的使用kprobe挂载的跟踪逻辑、默认使用tracepoint挂载的跟踪逻辑、以及自定义的跟踪逻辑,大部分跟踪点均使用默认的跟踪逻辑(也就是前两个宏定义的逻辑),极少数使用了FNC来自定义数据收集逻辑;我们将KPROBE_DEFAULT、TP_DEFAULT宏分别展开来探究两个默认数据采集的逻辑:

    • 默认tracepoint跟踪点将会执行以下数据采集逻辑,(仅以napi_gro_receive_entry跟踪点为例)
    static __always_inline int fake__napi_gro_receive_entry(context_info_t *info);
    
    SEC("tp_btf/napi_gro_receive_entry")
    int trace_napi_gro_receive_entry(void **ctx) {
        context_info_t info = {
            .func = INDEX_napi_gro_receive_entry,
            .ctx = ctx,
            .args = (void *)CONFIG(),
            .skb = ctx_get_arg(ctx, 3)
        };
        if (pre_handle_entry(&info, INDEX_napi_gro_receive_entry))
            return 0;
        handle_entry_finish(&info, fake__napi_gro_receive_entry(&info));
        return 0;
    }
    
    static __always_inline int fake__napi_gro_receive_entry(context_info_t *info) {
        return default_handle_entry(info);
    }
    
    • 默认kprobe相关跟踪点将会执行以下数据采集逻辑(仅以dev_gro_receive跟踪点为例)
    static __always_inline int fake__dev_gro_receive(context_info_t *info);
    
    SEC("kretprobe/dev_gro_receive")
    int trace_ret_dev_gro_receive(struct pt_regs *ctx)
    {
        return handle_exit(ctx, INDEX_dev_gro_receive);
    }
    
    SEC("kprobe/dev_gro_receive")
    int trace_dev_gro_receive(struct pt_regs *ctx)
    {
        context_info_t info = {
            .func = INDEX_dev_gro_receive,
            .ctx = ctx,
            .args = (void *)CONFIG(),
            .skb = nt_ternary_take(1, ctx_get_arg(ctx, 1), NULL),
            .sk = nt_ternary_take(, ctx_get_arg(ctx, ), NULL)
        };
        if (pre_handle_entry(&info, INDEX_dev_gro_receive))
            return 0;
        handle_entry_finish(&info, fake__dev_gro_receive(&info));
        return 0;
    }
    
    static __always_inline int fake__dev_gro_receive(context_info_t *info) {
        return default_handle_entry(info);
    }
    
  • 2.BPF程序自动挂载:我们在准备阶段已经根据不同的跟踪模式,启用了所需的跟踪点,其中打部分跟踪点均可自动挂载,而部分跟踪点则需要手动完成挂载工作;nettrace使用到ebpf提供的trace_attachkprobe_attach进行自动挂载,使用自定义的probe_trace_attach_manual进行手动挂载操作;

    [Nettrace 自动挂载方式探究](D:\研\项目\华为\nettrace-master\Nettrace 自动挂载方式探究.md)

  • 3.数据采集:在将BPF程序挂载到指定跟踪点后,便开始进行数据采集工作了;当触发到我们挂载的跟踪点时,会执行相应跟踪点预设的数据过滤和数据采集逻辑(也即BPF程序自动生成中,我们通过三个宏定义的数据采集逻辑),通过pre_handle_entry进行数据预处理、在default_handle_entry->handle_entry中进行数据过滤与数据采集逻辑、并最终在default_handle_entry中将从内核中采集到的数据传输至Perfbuffer中,以供用户态数据处理逻辑进行对应的数据处理工作;用户态对数据的处理逻辑如下:

    在这里插入图片描述

2. 数据处理流程梳理

数据处理阶段会根据不同跟踪模式定义不同的数据处理函数,接下来就是分析这些数据处理函数时如何进行数据处理和分析的;

在这里插入图片描述

我们以basic_poll_handler为例, 梳理该函数是如何处理TRACE_MODE_BASIC、TRACE_MODE_DROP、TRACE_MODE_MONITOR模式下采集到的的数据。

/*TRACE_MODE_BASIC
 *TRACE_MODE_DROP
 *TRACE_MODE_MONITOR
 */
void basic_poll_handler(void *ctx, int cpu, void *data, u32 size)
{
	analy_entry_t entry = {
		.event = data,
		.cpu = cpu 
	};
	/*对采集到的单条数据 analy_entry_t 执行分析处理*/
	entry_basic_poll(&entry);
}

可以看出该函数先是初始化了analy_entry_t结构体,然后通过entry_basic_poll(&entry)进行数据分析处理.所以entry_basic_poll(&entry)是处理数据的核心函数,analy_entry_t是分析数据的核心结构体他存储着要分析的数据块的全部信息;

在这里插入图片描述

/*对采集到的单条数据 analy_entry_t 执行分析处理*/
static inline void entry_basic_poll(analy_entry_t *entry)
{
	trace_t *trace;
	/*1.从 analy_entry_t 中获取 trace_t 信息*/
	trace = get_trace_from_analy_entry(entry);
	/*2.尝试运行与 trace 绑定的 entry 分析器*/
	try_run_entry(trace, trace->analyzer, entry);
	/*3.如果需要分析返回值,调用 exit 分析器*/
	if (trace_analyse_ret(trace)) {
		analy_exit_t analy_exit = {
			.event = {
				.val = entry->event->retval,
			},
			.entry = entry,
		};
		try_run_exit(trace, trace->analyzer, &analy_exit);
	}
	/*4.输出 entry 的分析结果*/
	analy_entry_output(entry, NULL);
}

entry_basic_poll()函数会先通过get_trace_from_analy_entry()找到数据块的源头trace_t的信息,即哪一个跟踪点产生了这个数据块;接着尝试使用trace对应的分析器的entry和exit函数去进行数据处理与分析(try_run_entry(),try_run_exit()),最后调用analy_entry_output()输出分析结果;

所以说实现数据分析的具体函数在分析器中定义着,需要进一步去查看分析器相关实现代码;

3. 数据分析器

在第二部分中,entry_basic_poll()函数是通过trace_t结构体找到分析器的entry和exit处理函数,先看一下trace_t结构体:

typedef struct trace {
    // 内核函数或跟踪点的名称,最长 64 字符
    char name[64];
    // 对跟踪点的简要描述,例如用途或功能
    char *desc;
    // 关联的消息,用于日志记录或调试输出
    char *msg;
    // 绑定到该跟踪点的 eBPF 程序的名称
    char *prog;
    // 跟踪点的类型,例如函数跟踪(TRACE_FUNCTION)或 tracepoint(TRACE_TP)
    enum trace_type type;
    // 可选的条件,用于判断是否启用跟踪点
    char *cond;
    // 可选的正则表达式,用于匹配内核函数名称
    char *regex;
    // 如果是 tracepoint 类型,记录 tracepoint 的名称
    char *tp;
    // 函数参数列表中 skb(socket buffer)的索引,从 1 开始,0 表示没有 skb
    u8 skb;
    // 在 ftrace 事件中 skb 的偏移量
    u8 skboffset;
    // 函数参数列表中 socket 的索引,与 skb 类似
    u8 sk;
    // 在 ftrace 事件中 socket 的偏移量
    u8 skoffset;
    // 全局链表,包含所有的 trace_t 实例
    struct list_head all;
    // 属于同一组的链表,管理 trace 分组
    struct list_head list;
    // 与该跟踪点关联的规则链表,例如匹配条件或阈值
    struct list_head rules;
    // 指向备份 trace 的指针,用于冗余或备用
    struct trace *backup;
    // 标记该 trace 是否为备份点
    bool is_backup;
    // 标志是否为探针模式(通常用于调试)
    bool probe;
    // 标志是否默认启用此跟踪点
    bool def;
    // 标志 eBPF 程序是否为自定义的
    bool custom;
    // 监控模式的标志,定义该 trace 的监控模式
    int monitor;
    // 唯一索引,用于标识此 trace
    int index;
    // 被跟踪函数的参数数量
    int arg_count;
    // 跟踪点的状态,例如加载、启用或无效等标志位
    u32 status;
    // 指向父组的指针,表示此 trace 的分组归属
    trace_group_t *parent;
    // 指向分析器模块的指针,用于关联分析逻辑
    struct analyzer *analyzer;
} trace_t;

这个结构体中的最后一个字段是analyzer也就是当前跟踪点对应的分析器,entry_basic_poll()就是通过这个分析器中的函数进行数据分析的;

3.1 trace_t与分析器的关系

不同的跟踪点根据trace.yaml配置文件定义了该跟踪点是否需要分析器以及用哪个分析器;trace.yaml文件会通过gen_trace.py文件生成trace_group.c以及kprobe_trace.h文件。在所生成的trace_group.c文件中,定义了每个跟踪点的trace_t结构体,我们拿trace_tcp_ack_update_rtt挂载点为例进行介绍:

trace_t trace_tcp_ack_update_rtt = {
	.desc = "",
	.type = TRACE_FUNCTION,
    /*分析器*/
	.analyzer = &ANALYZER(rtt),
	.is_backup = false,
	.probe = false,
	.name = "tcp_ack_update_rtt",
	.sk = 1,
	.custom = true,
	.def = true,
	.index = INDEX_tcp_ack_update_rtt,
	.prog = "__trace_tcp_ack_update_rtt",
	.parent = &group_tcp_state,
	.rules = LIST_HEAD_INIT(trace_tcp_ack_update_rtt.rules),
};

可以看到该跟踪点的分析器指向了某个地址&ANALYZER(rtt),该地址做进一步的探索:

在analysis.h文件中有对ANALYZER的宏定义:

#define ANALYZER(name) analyzer_##name

将宏ANALYZER(rtt)展开后便是:.analyzer = &analyzer_rtt,analyzer_rtt是什么?它对应的地址有什么?后面我们会涉及;

3.2 分析器的定义

analysis.c文件中定义了八个不同的分析器,我们以rtt为例进行介绍;

DEFINE_ANALYZER_ENTRY(rtt, TRACE_MODE_ALL_MASK)
{
	/*1.通过define_pure_event提取rtt事件数据*/
	define_pure_event(rtt_event_t, event, e->event);
	char *msg = malloc(1024);

	msg[0] = '\0';
	/*2.将first_rtt和last_rtt格式化成字符串,用于输出和日志记录*/
	sprintf(msg, PFMT_EMPH_STR(" *rtt:%ums, rtt_min:%ums*"),
		event->first_rtt, event->last_rtt);
	/*3.entry_set_msg将聚合后的消息绑定到当前分析条目analy_entry_t*/
	entry_set_msg(e, msg);

	return RESULT_CONT;
}
DEFINE_ANALYZER_EXIT_FUNC_DEFAULT(rtt)

上面这段代码是对rtt分析器的定义,我们进一步了解一下DEFINE_ANALYZER_ENTRY(rtt, TRACE_MODE_ALL_MASK)DEFINE_ANALYZER_EXIT_FUNC_DEFAULT(rtt)是什么,展开后是什么样的;

analysis.h文件中包含着与分析器相关的宏定义:

#define ANALYZER(name) analyzer_##name

#define DEFINE_ANALYZER_PART(name, type, mode_mask)			\
	analyzer_result_t analyzer_##name##_exit(trace_t *trace,	\
		analy_exit_t *e) __attribute__((weak));			\
	analyzer_result_t analyzer_##name##_entry(trace_t *trace,	\
		analy_entry_t *e) __attribute__((weak));		\
	analyzer_t ANALYZER(name) = {					\
		.analy_entry = analyzer_##name##_entry,			\
		.analy_exit = analyzer_##name##_exit,			\
		.mode = mode_mask,					\
	};								\
	analyzer_result_t analyzer_##name##_##type(trace_t *trace,	\
		analy_##type##_t *e)

#define DEFINE_ANALYZER_ENTRY(name, mode)				\
	DEFINE_ANALYZER_PART(name, entry, mode)

#define DEFINE_ANALYZER_EXIT(name, mode)				\
	DEFINE_ANALYZER_PART(name, exit, mode)

#define DEFINE_ANALYZER_EXIT_FUNC(name)					\
	analyzer_result_t analyzer_##name##_exit(trace_t *trace,	\
		analy_exit_t *e)

#define DEFINE_ANALYZER_EXIT_FUNC_DEFAULT(name)				\
DEFINE_ANALYZER_EXIT_FUNC(name)						\
{									\
	rule_run_ret(e->entry, trace, e->event.val);			\
	return RESULT_CONT;						\
}

#define DECLARE_ANALYZER(name) extern analyzer_t ANALYZER(name)
#define IS_ANALYZER(target, name) (target == &(ANALYZER(name)))

我们以rtt为例,将分析器的定义进行宏展开:

analyzer_result_t analyzer_rtt_exit(trace_t *trace, analy_exit_t *e) __attribute__((weak));
analyzer_result_t analyzer_rtt_entry(trace_t *trace, analy_entry_t *e) __attribute__((weak));
/*analyzer_rtt结构体,trace_t中的.analyzer指向的就是这个结构体*/
analyzer_t analyzer_rtt = {
    .analy_entry = analyzer_rtt_entry,
    .analy_exit = analyzer_rtt_exit,
    .mode = TRACE_MODE_ALL_MASK,
};

analyzer_result_t analyzer_rtt_entry(trace_t *trace, analy_entry_t *e)
{
	/*1.通过define_pure_event提取rtt事件数据*/
	define_pure_event(rtt_event_t, event, e->event);
	char *msg = malloc(1024);

	msg[0] = '\0';
	/*2.将first_rtt和last_rtt格式化成字符串,用于输出和日志记录*/
	sprintf(msg, PFMT_EMPH_STR(" *rtt:%ums, rtt_min:%ums*"),
		event->first_rtt, event->last_rtt);
	/*3.entry_set_msg将聚合后的消息绑定到当前分析条目analy_entry_t*/
	entry_set_msg(e, msg);

	return RESULT_CONT;
}

analyzer_result_t analyzer_rtt_exit(trace_t *trace, analy_exit_t *e)
{
    rule_run_ret(e->entry, trace, e->event.val);
    return RESULT_CONT;
}

2.3.1小节中所提的两个问题:analyzer_rtt是什么?它对应的地址有什么?便得到了解答:

  • analyzer_rtt是我们在定义分析器时,描述该分析器rtt的结构体地址,该结构体中包含着当前分析器的entry数据分析函数和exit数据分析函数;

在这里插入图片描述

3.3 数据处理->数据分析

在第二部分中,我们知道了数据在采集到之后是如何进行数据处理的,也知道在对应的数据处理函数中是如何找到器对应跟踪点所使用的分析器的。在本章中我们了解到分析器时如何定义与实现的,try_run_entry()函数通过调用提前定义好的analyzer_rtt_entryanalyzer_rtt_exit函数,进行真正的数据分析与处理;

在这里插入图片描述

4.七层的数据采集实现细节

前面我们以及梳理了nettrace中数据是如何采集的,下面将围绕各个跟踪点在内核中进行数据采集的逻辑进行梳理,其实最终就是探究KPROBE_DEFAULTTP_DEFAULTFNC三者的具体实现逻辑;

在前面我们已经介绍了KPROBE_DEFAULTTP_DEFAULT两个宏展开后具体的逻辑,这里再展示一下:

  • TP_DEFAULT:默认tracepoint跟踪点将会执行以下数据采集逻辑,(仅以napi_gro_receive_entry跟踪点为例)
static __always_inline int fake__napi_gro_receive_entry(context_info_t *info);

SEC("tp_btf/napi_gro_receive_entry")
int trace_napi_gro_receive_entry(void **ctx) {
    context_info_t info = {
        .func = INDEX_napi_gro_receive_entry,
        .ctx = ctx,
        .args = (void *)CONFIG(),
        .skb = ctx_get_arg(ctx, 3)
    };
    if (pre_handle_entry(&info, INDEX_napi_gro_receive_entry))
        return 0;
    handle_entry_finish(&info, fake__napi_gro_receive_entry(&info));
    return 0;
}

static __always_inline int fake__napi_gro_receive_entry(context_info_t *info) {
    return default_handle_entry(info);
}
  • KPROBE_DEFAULT: 默认kprobe相关跟踪点将会执行以下数据采集逻辑(仅以dev_gro_receive跟踪点为例)
static __always_inline int fake__dev_gro_receive(context_info_t *info);

SEC("kretprobe/dev_gro_receive")
int trace_ret_dev_gro_receive(struct pt_regs *ctx)
{
    return handle_exit(ctx, INDEX_dev_gro_receive);
}

SEC("kprobe/dev_gro_receive")
int trace_dev_gro_receive(struct pt_regs *ctx)
{
    context_info_t info = {
        .func = INDEX_dev_gro_receive,
        .ctx = ctx,
        .args = (void *)CONFIG(),
        .skb = nt_ternary_take(1, ctx_get_arg(ctx, 1), NULL),
        .sk = nt_ternary_take(, ctx_get_arg(ctx, ), NULL)
    };
    if (pre_handle_entry(&info, INDEX_dev_gro_receive))
        return 0;
    handle_entry_finish(&info, fake__dev_gro_receive(&info));
    return 0;
}

static __always_inline int fake__dev_gro_receive(context_info_t *info) {
    return default_handle_entry(info);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值