beyla 反对通过 ebpf,无侵入的、主动采集应用程序的 trace 信息。
以 golang 的 nethttp 为例,讲述 beyla 对 trace 的采集的实现原理。
一. 整体原理
trace 采集时,监听了 golang 应用程序的 net/http 中的函数:
- net/http.serverHandler.ServeHTTP;
- net/http.(*Transport).roundTrip;
监听 ServeHTTP 时:
- 若 requset 中没有 trace 信息,则生成 traceparent,存入 go_trace_map 构造 (key=goroutine 地址,value=trace 信息);
- 若 request 中有 trace 信息,则依据 trace 信息,从新生成 span,存入 go_trace_map 构造;
监听 roundTrip 的调用:
- 首先,依据 goroutine 地址,读 go_trace_map 构造,失去 trace 信息;
- 而后,将以后连贯的 trace 信息,存入 ongoing_http_client_requests 构造 (key=goroutine 地址,value=trace 信息);
监听 roundTrip 的调用返回:
- 首先,依据 goroutine 地址,读 ongoing_http_client_requests 构造,失去 trace 信息;
- 而后,将以后调用的 trace 信息,转换为 http_request_trace 构造,保留到 ringbuf 中;
最终,ebpf 用户程序,读取 ringbuf 中的 trace 信息,采集到 trace 信息。
二. 监听 uprobe/ServeHTTP
解决流程:
- 首先,提取 goroutine 和 request 指针;
- 而后,通过 server_trace_parent() 函数,解决 trace 信息,存入 go_trace_map 构造;
- 最初,将数据存入 onging_http_server_requests 构造;
// beyla/bpf/go_nethttp.c
SEC("uprobe/ServeHTTP")
int uprobe_ServeHTTP(struct pt_regs *ctx) {void *goroutine_addr = GOROUTINE_PTR(ctx);
void *req = GO_PARAM4(ctx);
http_func_invocation_t invocation = {.start_monotime_ns = bpf_ktime_get_ns(),
.req_ptr = (u64)req,
.tp = {0}
};
if (req) {
// 解决 trace 信息,存入 go_trace_map
server_trace_parent(goroutine_addr, &invocation.tp, (void*)(req + req_header_ptr_pos));
}
// write event
if (bpf_map_update_elem(&ongoing_http_server_requests, &goroutine_addr, &invocation, BPF_ANY)) {bpf_dbg_printk("can't update map element");
}
return 0;
}
重点看一下 server_trace_parent() 函数:
-
首先,从 req_header 读取 traceparent:
- 若读到了,则 copy traceId,将 parentId= 下层的 spanId;
- 否则,则生成 trace_id,将 parentId=0;
- 而后,应用 urand,生成随机的 spanId;
- 最初,将 trace 信息存入 go_trace_map 构造,key=goroutine 地址,value=trace 信息;
// bpf/go_common.h
static __always_inline void server_trace_parent(void *goroutine_addr, tp_info_t *tp, void *req_header) {
// May get overriden when decoding existing traceparent, but otherwise we set sample ON
tp->flags = 1;
// Get traceparent from the Request.Header
void *traceparent_ptr = extract_traceparent_from_req_headers(req_header);
if (traceparent_ptr != NULL) { // 读到了 traceparent
....
} else { // 未读到 traceparent
bpf_dbg_printk("No traceparent in headers, generating");
urand_bytes(tp->trace_id, TRACE_ID_SIZE_BYTES); // 生成随机的 trace_id;*((u64 *)tp->parent_id) = 0;
}
urand_bytes(tp->span_id, SPAN_ID_SIZE_BYTES);
bpf_map_update_elem(&go_trace_map, &goroutine_addr, tp, BPF_ANY);
}
go_trace_map 对象的定义:
struct {__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, void *); // key: pointer to the goroutine
__type(value, tp_info_t); // value: traceparent info
__uint(max_entries, MAX_CONCURRENT_SHARED_REQUESTS);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} go_trace_map SEC(".maps");
typedef struct tp_info {unsigned char trace_id[TRACE_ID_SIZE_BYTES];
unsigned char span_id[SPAN_ID_SIZE_BYTES];
unsigned char parent_id[SPAN_ID_SIZE_BYTES];
u64 ts;
u8 flags;
} tp_info_t;
三. 监听 uprobe/roundTrip
roundTrip 函数,在应用 http client 发动申请时,被调用。
解决流程:
- 首先,提取 goroutine 地址和 request 地址;
- 而后,依据 goroutine_addr 和 request,查找 trace 信息;
- 最初,将 trace 信息写入 ongoing_http_client_requests 对象;
// beyla/bpf/go_nethttp.c
SEC("uprobe/roundTrip")
int uprobe_roundTrip(struct pt_regs *ctx) {roundTripStartHelper(ctx);
return 0;
}
static __always_inline void roundTripStartHelper(struct pt_regs *ctx) {void *goroutine_addr = GOROUTINE_PTR(ctx);
void *req = GO_PARAM2(ctx);
http_func_invocation_t invocation = {.start_monotime_ns = bpf_ktime_get_ns(),
.req_ptr = (u64)req,
.tp = {0}
};
// 依据 request 和 goroutine_addr,查找 trace 信息
__attribute__((__unused__)) u8 existing_tp = client_trace_parent(goroutine_addr, &invocation.tp, (void*)(req + req_header_ptr_pos));
// 将 trace 信息写入 ongoing_http_client_requests
if (bpf_map_update_elem(&ongoing_http_client_requests, &goroutine_addr, &invocation, BPF_ANY)) {bpf_dbg_printk("can't update http client map element");
}
}
重点看一下查找 trace 信息的 client_trace_parent() 函数:
-
首先,尝试从 request 的 header 中提取 traceparent:
- 若找到了,则 copy traceId,设置以后 span.parentId= 上游 span 的 spanId;
-
而后,再应用 goroutine 及其 parent_goroutine,去 go_trace_map 中找:
- 若找到了,则 copy traceId,设置以后 span.parentId= 上游 span 的 spanId;
// beyla/go_common.h
static __always_inline u8 client_trace_parent(void *goroutine_addr, tp_info_t *tp_i, void *req_header) {
u8 found_trace_id = 0;
u8 trace_id_exists = 0;
// May get overriden when decoding existing traceparent or finding a server span, but otherwise we set sample ON
tp_i->flags = 1;
// 首先尝试从 request 的 header 中提取 traceparent
if (req_header) {...}
// 而后再应用 goroutine 去 go_trace_map 中找
if (!found_trace_id) {
tp_info_t *tp = 0;
u64 parent_id = find_parent_goroutine(goroutine_addr);
if (parent_id) {// we found a parent request
tp = (tp_info_t *)bpf_map_lookup_elem(&go_trace_map, &parent_id);
}
if (tp) { // 找到了,copy traceId,以后 span.parentId= 上流 span.spanId
*((u64 *)tp_i->trace_id) = *((u64 *)tp->trace_id);
*((u64 *)(tp_i->trace_id + 8)) = *((u64 *)(tp->trace_id + 8));
*((u64 *)tp_i->parent_id) = *((u64 *)tp->span_id);
tp_i->flags = tp->flags;
}
...
// 生成以后 span.spanId
urand_bytes(tp_i->span_id, SPAN_ID_SIZE_BYTES);
}
return trace_id_exists;
}
这里有个隐形的假如条件:
- 一个 goroutine 及其 child goroutine 仅解决一个 http 申请;
- nethttp 的框架在设计时,就由一个 goroutine 去解决一个 http 申请,是合乎这个假如的;
四. 监听 uprobe/roundTrip_return
解决流程:
- 首先,应用 goroutine_addr,从 ongoing_http_client_requests 中找 trace 信息;
-
而后,初始化 http_request_trace:
- 从 request 中找 method/host/url/content_length,赋值给 http_request_trace;
- 将 trace 信息赋值到 http_request_trace;
- 从 response 中找 status,赋值给 http_request_trace;
- 最初,将 http_request_trace 提交到 ringbuf;
// beyla/bpf/go_nethttp.c
SEC("uprobe/roundTrip_return")
int uprobe_roundTripReturn(struct pt_regs *ctx) {void *goroutine_addr = GOROUTINE_PTR(ctx);
// 应用 goroutine_addr 找 ongoing_http_client_requests
http_func_invocation_t *invocation =
bpf_map_lookup_elem(&ongoing_http_client_requests, &goroutine_addr);
bpf_map_delete_elem(&ongoing_http_client_requests, &goroutine_addr);
http_request_trace *trace = bpf_ringbuf_reserve(&events, sizeof(http_request_trace), 0);
// 初始化 http_request_trace
task_pid(&trace->pid);
trace->type = EVENT_HTTP_CLIENT;
trace->start_monotime_ns = invocation->start_monotime_ns;
trace->go_start_monotime_ns = invocation->start_monotime_ns;
trace->end_monotime_ns = bpf_ktime_get_ns();
void *req_ptr = (void *)invocation->req_ptr;
void *resp_ptr = (void *)GO_PARAM1(ctx);
// 从 request 中找 method,赋值给 trace->method
if (!read_go_str("method", req_ptr, method_ptr_pos, &trace->method, sizeof(trace->method))) {...}
// 从 request 中找 host,赋值给 trace->host
if (!read_go_str("host", req_ptr, host_ptr_pos, &trace->host, sizeof(trace->host))) {...}
// 从 request 中找 url,赋值给 trace->path
void *url_ptr = 0;
bpf_probe_read(&url_ptr, sizeof(url_ptr), (void *)(req_ptr + url_ptr_pos));
if (!url_ptr || !read_go_str("path", url_ptr, path_ptr_pos, &trace->path, sizeof(trace->path))) {...}
// 赋值 trace 信息
trace->tp = invocation->tp;
// 从 request 中找 content_length,赋值给 trace->content_length
bpf_probe_read(&trace->content_length, sizeof(trace->content_length), (void *)(req_ptr + content_length_ptr_pos));
// 从 resp 中找 status,赋值给 trace->status
bpf_probe_read(&trace->status, sizeof(trace->status), (void *)(resp_ptr + status_code_ptr_pos));
// 提交 trace 到 ringbuf
bpf_ringbuf_submit(trace, get_flags());
return 0;
}
参考:
1.https://github.com/grafana/beyla/issues/521
2.https://github.com/grafana/beyla/blob/main/docs/sources/distributed-traces.md