在 felixge 的这篇文章中,介绍了 datadog 的 timeline 功能:
复现
从 github 上 clone content-addressable-store 并运行:
https://github.com/DataDog/content-addressable-store
go build
DD_PROFILING_EXECUTION_TRACE_PERIOD=1m ./content-addressable-store
随意发送请求。
打开 datadog 页面,从 timeline 功能上可以发现。
goroutine 在不同阶段的耗时以及对应的堆栈:
- syscall
- blocking
- waiting
- stop thw world (gc)
在右上角点击下载数据。
使用 go tool trace 打开:
go tool trace /Users/jie.yang05/Downloads/20240715-030903_content-addressable-store_AZC0XnD1AADKLJyEuUszPQAA/go.trace
选择 user define task 区域
选择这条 5ms 的数据,点击 task:
刚才 datadog 那个请求对应的 trace:
原理。
go 原生的 runtime trace 功能,会在 goroutine 状态变化的时候记录调用堆栈。
datadog 在 start trace 的时候,将 span id 塞进 runtime trace 中,以进行数据关联。
}
span.goExecTraced = true
// Task name is the resource (operationName) of the span, e.g.
// "POST /foo/bar" (http) or "/foo/pkg.Method" (grpc).
taskName := span.Resource
// If the resource could contain PII (e.g. SQL query that's not using bind
// arguments), play it safe and just use the span type as the taskName,
// e.g. "sql".
if !spanResourcePIISafe(span) {
taskName = span.Type
}
end := noopTaskEnd
if !globalinternal.IsExecutionTraced(ctx) {
var task *rt.Task
ctx, task = rt.NewTask(ctx, taskName)
end = task.End
} else {
// We only want to skip task creation for this particular span,
// not necessarily for child spans which can come from different
// integrations. So update this context to be "not" execution
// traced so that derived contexts used by child spans don't get
// skipped.
ctx = globalinternal.WithExecutionNotTraced(ctx)
}
var b [8]byte
binary.LittleEndian.PutUint64(b[:], span.SpanID)
// TODO: can we make string(b[:]) not allocate? e.g. with unsafe
// shenanigans? rt.Log won't retain the message string, though perhaps
// we can't assume that will always be the case.
rt.Log(ctx, "datadog.uint64_span_id", string(b[:]))
return ctx, end
}
优化。
如何进行数据的折叠以及显示优化。
使用
golang.org/x/exp/trace 读取 trace 数据。
实际上会发现一个 trace id 表示的 goroutine 会有非常多的 goroutine 状态转换,这些数据在渲染的时候需要经过良好的过滤,才能最终对用户有价值。
这种 timeline 功能,实际很像,对每个 syscall 都进行一个 fprobe 获取调用堆栈。 无法获取运行时候的调用堆栈。
运行时候的开销,可以借助 go 的 cpu profiler, 将 trace id 塞进 goroutine 的 label 中实现,以进行关联。