Analyzing Trace Performance with Flame Graphs¶
This article helps you understand what distributed tracing is and how to use tools to analyze performance bottlenecks in a trace.
Basic Concepts and Tools¶
- Distributed Tracing (Trace)
- Analysis Tools
- Flame Graph
- Span List
- Service Map
- Duration / Execution Time
Distributed Tracing¶
Generally, a single Trace consists of multiple Spans and forms a tree or a directed acyclic graph (DAG). Each Span represents a named and timed continuous execution segment within the Trace, as shown in the figure below. Because the core of a Span is to record the start and end times of the corresponding program execution segment, and there are parent-child call relationships between execution segments, Spans logically form a tree structure.
Note: The parent-child relationship of spans can be associated by setting the child span's parent_id to the parent Span's span_id.
Flame Graph¶
A Flame Graph is a visualization invented by Linux performance optimization master Brendan Gregg for analyzing performance bottlenecks. It provides a global view of time distribution, listing all Spans that may cause performance bottlenecks from top to bottom.
Rendering Logic¶
- The vertical axis (Y-axis) represents the depth of the call Span hierarchy, indicating the call relationships between program execution segments: the upper Span is the parent of the lower Span (data-wise, this can also be associated by matching the child span's
parent_idwith the parent Span'sspan_id). - The horizontal axis (X-axis) represents the duration of Spans within a single Trace. The wider a block, the longer the duration of that Span from start to end, which may indicate a performance bottleneck.
Display Description¶
Flame Graph
- The color of each Span block in the flame graph corresponds to the color of its service.
From the flame graph, you can intuitively see which services are performing requests in the current Trace. (Service color generation logic: When you log in to a workspace and access the APM module, Guance automatically generates colors based on service names. This color assignment is inherited in analysis pages such as the Trace Explorer.)
- Span block default display: The current Span's resource name or operation, duration, and whether there is an error (status = error).
- Each Span tooltip displays the corresponding resource, duration, and the percentage of the total time.
Service List
The service list on the right side of the flame graph displays the service names, colors, and the ratio of each service's execution time to the total execution time for the current Trace.
Note: If a service name is displayed as None, it means the current trace has not found a top-level Span with parent_id = 0.
Interaction Description¶
- Full screen / Restore default size: Click the full-screen icon in the upper-right corner of the trace details to expand the flame graph horizontally. Click the restore default size icon to return to the normal view.
- Expand / Collapse minimap: Click the expand/collapse minimap icon on the left side of the trace details. Use the minimap to select a range, drag, or scroll to quickly navigate the flame graph.
- View entire Trace: Click the "View entire Trace" icon on the left side of the trace details to view the complete trace in the flame graph.
- Collapse bottom tab details: Click the collapse button to hide the bottom tab details area.
- Double-click a Span: Enlarges the display of that Span in the flame graph, allowing you to quickly locate and view its contextual Spans.
- Click a service name on the right: Highlights the corresponding Spans. Click the service name again to restore the default selection of all Spans. You can click a service name to quickly filter and view the Spans belonging to that service.
Special Notes¶
Due to multi-threading or asynchronous tasks, the relationships between spans in the flame graph may appear as follows:
- Sibling spans under the same parent may overlap.
Because span overlap can occur, the frontend performs some display adjustments when rendering the flame graph to show each Span and its child spans more intuitively. The positions are calculated based on time and spatial dimensions to ensure that spans and their child spans are displayed without overlapping.
Example 1:
In a normal Trace, spans at the same level do not overlap in time, but they may overlap with their child spans. Parent-child relationships are indicated by connecting lines. Child spans with connecting lines are also rendered using the same logic.
Example 2:
In an abnormal Trace, spans at the same level may still overlap in time. However, the actual data may show that the start time of the Trace's top-level Span (parent_id = 0) is later than the start time of its child spans.
Analysis Logic:
According to the parent-child execution relationship in the trace, the parent Span's start time must be earlier than the child Span's start time. Therefore, if the flame graph shows that the parent Span and child Span belong to different services, it is likely that the system times of the servers where the two services are running are inconsistent. You should first verify and synchronize the system times before analyzing the actual performance bottleneck.
Span List¶
Display Description¶
Collapsed List View
- Column 1: Displays the service type, service name, service color, and whether there are Spans with
status = errorunder the current service. - Column 2: Displays the number of Spans under the current service.
- Column 3: Displays the average duration of Spans under the current service.
- Column 4: Displays the total execution time of Spans under the current service.
- Column 5: Displays the ratio of the current service's execution time to the total execution time.
Expanded Service Row View
- Column 1: Displays the resource name, corresponding service color, and whether the current span has
status = error. - Column 2: Empty.
- Column 3: Displays the current Span's duration.
- Column 4: Displays the current Span's execution time.
- Column 5: Displays the ratio of the current Span's execution time to the total execution time.
Interaction Description¶
- Search: Supports fuzzy search by resource name.
- Supports selecting a Span and then switching to the flame graph to view the context of the corresponding Span.
Service Map¶
Display Description¶
Displays the topological relationship between services in the current trace.
- Supports fuzzy matching by resource name to locate the upstream and downstream service call relationships of a specific resource.
- Hovering over a service shows: the number of Spans under the service, the service execution time, and its percentage.
Duration¶
Duration refers to the start and end times of the program execution segment corresponding to a Span. This is typically marked by the duration field in the trace data.
Execution Time¶
As mentioned in the Special Notes, the end times of parent and child Spans may not match. Therefore, the execution time is calculated according to the following logic.
Span Execution Time¶
- Child span may end after the parent span ends
Child Span execution time = Duration of Children
Total execution time = Children's end time - Parent's start time
Parent Span execution time = Total execution time - Child Span execution time
- Child span may start after the parent span ends
Child Span execution time = Duration of Children
Total execution time = Children's end time - Parent's start time
Parent Span execution time = Total execution time - Child Span execution time
- Sibling spans under the same parent may overlap
Parent Span execution time = p(1) + p(2)
Children 1 Span execution time = c1(1) + c1(2)
Children 2 Span execution time = c2(1) + c2(2)
Note: Because Children 1 Span and Children 2 Span overlap in actual execution time, the overlapping portion is split evenly between the two spans.
Example
In a synchronous task, where Spans execute in the order "Span1 start -> Span1 end -> Span2 start -> Span2 end -> ...", the execution time of each Span and its parent Span is calculated as follows:
Example 1:
Parent Span = Couldcare SPAN1
Child Spans = MyDQL SPAN2, MyDQL SPAN3, MyDQL SPAN4, MyDQL SPAN5, MyDQL SPAN6, MyDQL SPAN7, MyDQL SPAN8, MyDQL SPAN9, MyDQL SPAN10, MyDQL SPAN11
Calculation Analysis:
Since none of the child Spans have further child Spans, the execution time of all child Spans in the figure below equals their Span duration. The parent Span has child Spans, so its actual execution time is obtained by subtracting the sum of all child Spans' execution times from the parent Span's duration.
Service Execution Time¶
Execution time of each service = Sum of the execution times of all Spans belonging to that service within the Trace.
Total Execution Time¶
Total execution time = The last end time of any Span in the Trace - The earliest start time of any Span in the Trace.
Trace Analysis Example¶
Collector Configuration (Host Installation)¶
Go to the conf.d/ddtrace directory under the DataKit installation directory, copy ddtrace.conf.sample and rename it to ddtrace.conf. Example:
ddtrace.conf Example
[[inputs.ddtrace]]
## DDTrace Agent endpoints register by version respectively.
## Endpoints can be skipped listen by remove them from the list.
## Default value set as below. DO NOT MODIFY THESE ENDPOINTS if not necessary.
endpoints = ["/v0.3/traces", "/v0.4/traces", "/v0.5/traces"]
## customer_tags is a list of keys contains keys set by client code like span.SetTag(key, value)
## that want to send to data center. Those keys set by client code will take precedence over
## keys in [inputs.ddtrace.tags]. DOT(.) IN KEY WILL BE REPLACED BY DASH(_) WHEN SENDING.
# customer_tags = ["key1", "key2", ...]
## Keep rare tracing resources list switch.
## If some resources are rare enough(not presend in 1 hour), those resource will always send
## to data center and do not consider samplers and filters.
# keep_rare_resource = false
## By default every error presents in span will be send to data center and omit any filters or
## sampler. If you want to get rid of some error status, you can set the error status list here.
# omit_err_status = ["404"]
## Ignore tracing resources map like service:[resources...].
## The service name is the full service name in current application.
## The resource list is regular expressions uses to block resource names.
## If you want to block some resources universally under all services, you can set the
## service name as "*". Note: double quotes "" cannot be omitted.
# [inputs.ddtrace.close_resource]
# service1 = ["resource1", "resource2", ...]
# service2 = ["resource1", "resource2", ...]
# "*" = ["close_resource_under_all_services"]
# ...
## Sampler config uses to set global sampling strategy.
## sampling_rate used to set global sampling rate.
# [inputs.ddtrace.sampler]
# sampling_rate = 1.0
# [inputs.ddtrace.tags]
# key1 = "value1"
# key2 = "value2"
# ...
## Threads config controls how many goroutines an agent cloud start.
## buffer is the size of jobs' buffering of worker channel.
## threads is the total number fo goroutines at running time.
# [inputs.ddtrace.threads]
# buffer = 100
# threads = 8
## Storage config a local storage space in hard dirver to cache trace data.
## path is the local file path used to cache data.
## capacity is total space size(MB) used to store data.
# [inputs.ddtrace.storage]
# path = "./ddtrace_storage"
# capacity = 5120
After configuration, restart DataKit.
HTTP Settings¶
If the trace data is sent from a different machine, you need to configure DataKit's HTTP settings.
If ddtrace data is sent to DataKit, you can see the following in DataKit's monitor:
DDtrace sends data to the /v0.4/traces endpoint.
SDK Integration (Go Example)¶
Install Dependencies¶
Install the ddtrace Golang library by running the following command in the development directory:
Configure DataKit¶
You need to install, start DataKit, and enable the ddtrace collector.
Code Example¶
The following code demonstrates trace data collection for a file open operation.
In the main() entry point, set the basic trace parameters and start the trace:
Example
package main
import (
"io/ioutil"
"os"
"time"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
func main() {
tracer.Start(
tracer.WithEnv("prod"),
tracer.WithService("test-file-read"),
tracer.WithServiceVersion("1.2.3"),
tracer.WithGlobalTag("project", "add-ddtrace-in-golang-project"),
)
// end of app exit, make sure tracer stopped
defer tracer.Stop()
tick := time.NewTicker(time.Second)
defer tick.Stop()
// your-app-main-entry...
for {
runApp()
runAppWithError()
select {
case <-tick.C:
}
}
}
func runApp() {
var err error
// Start a root span.
span := tracer.StartSpan("get.data")
defer span.Finish(tracer.WithError(err))
// Create a child of it, computing the time needed to read a file.
child := tracer.StartSpan("read.file", tracer.ChildOf(span.Context()))
child.SetTag(ext.ResourceName, os.Args[0])
// Perform an operation.
var bts []byte
bts, err = ioutil.ReadFile(os.Args[0])
span.SetTag("file_len", len(bts))
child.Finish(tracer.WithError(err))
}
Build and Run¶
Linux/Mac environment:
Windows environment:
go build main.go -o my-app.exe
$env:DD_AGENT_HOST="localhost"; $env:DD_TRACE_AGENT_PORT="9529"; .\my-app.exe
After running the program for a while, you can see trace data similar to the following in Guance:
Golang program trace data display
Supported Environment Variables¶
The following environment variables can be used to specify ddtrace configuration parameters when starting the program. The basic format is:
Important: These environment variables will be overridden by corresponding fields set via WithXXX() in the code. Therefore, configuration injected via code has higher priority. These ENV variables only take effect when the corresponding fields are not specified in the code.
| Key | Default | Description |
|---|---|---|
| DD_VERSION | - | Set the application version, e.g., 1.2.3, 2022.02.13 |
| DD_SERVICE | - | Set the application service name |
| DD_ENV | - | Set the current environment of the application, e.g., prod, pre-prod |
| DD_AGENT_HOST | localhost | Set the IP address of DataKit. Trace data generated by the application will be sent to this DataKit. |
| DD_TRACE_AGENT_PORT | - | Set the port for receiving DataKit trace data. You need to manually specify the HTTP port of DataKit (usually 9529) |
| DD_DOGSTATSD_PORT | - | If you want to receive statsd data generated by ddtrace, you need to manually enable the statsd collector on DataKit. |
| DD_TRACE_SAMPLING_RULES | - | Use a JSON array to configure sampling rules. The sampling rate is applied in the order of the array. sample_rate is the sampling rate, ranging from [0.0, 1.0]. Example 1: Set a global sampling rate of 20%: DD_TRACE_SAMPLE_RATE='[{"sample_rate": 0.2}]' ./my-app Example 2: Set a sampling rate of 10% for services matching app1.* with span name abc, and 20% for all others: DD_TRACE_SAMPLE_RATE='[{"service": "app1.*", "name": "b", "sample_rate": 0.1}, {"sample_rate": 0.2}]' ./my-app |
| DD_TRACE_SAMPLE_RATE | - | Enable the sampling rate switch above |
| DD_TRACE_RATE_LIMIT | - | Set the number of span samples per second per Golang process. If DD_TRACE_SAMPLE_RATE is enabled, the default is 100. |
| DD_TAGS | - | Inject a set of global tags. These tags will appear in every span and profile data. Multiple tags can be separated by spaces or commas, e.g., layer:api,team:intake or layer:api team:intake. |
| DD_TRACE_STARTUP_LOGS | true | Enable ddtrace configuration and diagnostic logs |
| DD_TRACE_DEBUG | false | Enable ddtrace debug logs |
| DD_TRACE_ENABLED | true | Enable the trace switch. If disabled, no trace data will be generated. |
| DD_SERVICE_MAPPING | - | Dynamically rename service names. Multiple service name mappings can be separated by spaces or commas, e.g., mysql:mysql-service-name,postgres:postgres-service-name or mysql:mysql-service-name postgres:postgres-service-name. |
Real Trace Data Analysis¶
- Log in to the Guance workspace and view the service list in the APM module. From the service page, you can already see that the P90 response time of the
browserservice is relatively high.
- Click the
browserservice name to view the service overview analysis page. It shows that the most critical resource affecting the response time of this service is thequery_dataAPI. Since this is a data query API of Guance, we need to investigate why this API takes a long time.
- Click the resource name to navigate to the Explorer. Sort by duration in descending order to find the maximum response time.
- Click a Span to view and analyze the execution performance of that Span in the entire trace, along with other relevant information.
- Click the [Full Screen] button in the upper-right corner to enlarge the flame graph. Combined with the overall trace view, you can see that the
browserservice accounts for up to 96.26% of the total execution time. This conclusion is also supported by the Span list. Based on the flame graph proportions and the corresponding trace details, you can infer that thequery_dataSpan of thebrowserservice took over 400 milliseconds forresource_ttfb(resource loading request response time) and 1.46 seconds forresource_first_byte(resource loading first byte time). Additionally, theprovincefield indicates a geographic location of Singapore, while the site is deployed in the Hangzhou node. This suggests that the geographic distance caused longer data transmission time, which in turn affected the overall latency.





















