Skip to content

Commit c6d3d6f

Browse files
authored
*: simplify parca-load and make deterministic (#467)
* querier: use native histograms and include labels label in Range/Merge queries Signed-off-by: Alfonso Subiotto Marques <[email protected]> * querier: remove single query and use only arrow report type in merge Signed-off-by: Alfonso Subiotto Marques <[email protected]> * add labels flag to set constant labels to query by in range and merge This is helpful to keep the labels variable constant over time in order to correctly understand query performance over time. Signed-off-by: Alfonso Subiotto Marques <[email protected]> * add types flag to specify profile types to query This removes variability in query runtimes Signed-off-by: Alfonso Subiotto Marques <[email protected]> * add values-for-labels flag to query values only for these labels This also reduces query variability Signed-off-by: Alfonso Subiotto Marques <[email protected]> * querier: run queries in parallel Now that queries don't share state for discoverability, run them in parallel Signed-off-by: Alfonso Subiotto Marques <[email protected]> * update and simplify README Signed-off-by: Alfonso Subiotto Marques <[email protected]> --------- Signed-off-by: Alfonso Subiotto Marques <[email protected]>
1 parent 7f005ac commit c6d3d6f

File tree

5 files changed

+429
-350
lines changed

5 files changed

+429
-350
lines changed

README.md

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,66 @@
11
# parca-load
22

3-
This is a tool that continuously queries Parca instances for their data.
3+
A load testing tool that continuously queries Parca instances.
44

5-
It is based on the Parca gRPC APIs defined on https://buf.build/parca-dev/parca and uses the generated connect-go code via `go.buf.build/bufbuild/connect-go/parca-dev/parca`.
6-
7-
### Installation
5+
## Installation
86

97
```
108
go install github.com/parca-dev/parca-load@latest
119
```
1210

13-
or run the container images
11+
or via container:
1412

1513
```
16-
docker run -d ghcr.io/parca-dev/parca-load
14+
docker run ghcr.io/parca-dev/parca-load
1715
```
1816

19-
### How it works
17+
## How it works
18+
19+
The tool runs five query types in parallel at a configurable interval (default: 5s):
20+
21+
- **ProfileTypes** - discovers available profile types
22+
- **Labels** - queries label names for each profile type
23+
- **Values** - queries label values (if `-values-for-labels` is set)
24+
- **QueryRange** - fetches profile series data
25+
- **Query (merge)** - fetches merged flamegraph data
26+
27+
Each query type runs against all configured profile types, time ranges, and label selectors.
2028

21-
It runs a goroutine per API type.
29+
Metrics are exposed at `http://<addr>/metrics` (default: `127.0.0.1:7171`).
2230

23-
It starts a `Labels` goroutine that starts querying all labels on a Parca instance and then writes these into a shared map.
24-
The map is then read by the `Values` goroutine that selects a random label and queries all values for it.
31+
## Flags
2532

26-
This process it repeated every 5 seconds (configurable).
27-
The entries of the shared map eventually expire to not query too old data.
33+
| Flag | Default | Description |
34+
|------|---------|-------------|
35+
| `-url` | `http://localhost:7070` | Parca instance URL |
36+
| `-addr` | `127.0.0.1:7171` | HTTP server address for metrics |
37+
| `-query-interval` | `5s` | Interval between query rounds |
38+
| `-query-range` | `15m;12h;168h` | Time ranges for queries (semicolon-separated) |
39+
| `-types` | (auto-discover) | Profile types to query (semicolon-separated) |
40+
| `-labels` | `all` | Label selectors for filtering (semicolon-separated) |
41+
| `-values-for-labels` | (none) | Label names to query values for (semicolon-separated) |
42+
| `-token` | | Bearer token for authentication |
43+
| `-headers` | | Custom headers (`key=value,key2=value2`) |
44+
| `-client-timeout` | `10s` | HTTP client timeout |
2845

29-
Similarly, it starts querying `ProfileTypes` every 10 seconds (configurable) to know what profile types are available.
30-
The result is written to a shared map.
31-
Every 15 seconds (configurable) there are `QueryRange` requests (querying 15min and 7 day ranges) for all series.
46+
## Examples
3247

33-
Once the profile series are discovered above there are `Query` requests querying single profiles every 10 seconds.
34-
For these queries it picks a random timestamp of the available time range and queries a random report type (flame graph, top table, pprof download).
48+
```bash
49+
# Basic usage with auto-discovered profile types
50+
./parca-load -url=http://localhost:7070
3551

36-
Every 15 seconds (configurable) there are `Query` requests that actually request merged profiles for either 15min, or 7 days, if enough data is available for each in a series.
52+
# Specific profile types
53+
./parca-load -url=http://localhost:7070 \
54+
-types='parca_agent:samples:count:cpu:nanoseconds:delta'
55+
56+
# Custom time ranges and label filtering
57+
./parca-load -url=http://localhost:7070 \
58+
-query-range='1h;24h' \
59+
-labels='{job="api",env="dev"}'
60+
61+
# Query values for specific labels
62+
./parca-load -url=http://localhost:7070 \
63+
-values-for-labels='job;namespace'
64+
```
3765

38-
Metrics are collected and available on http://localhost:7171/metrics
66+
Profile type format: `name:sample_type:sample_unit:period_type:period_unit[:delta]`

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,15 @@ require (
1313
github.com/hashicorp/vault/api/auth/kubernetes v0.10.0
1414
github.com/oklog/run v1.2.0
1515
github.com/prometheus/client_golang v1.23.2
16-
github.com/prometheus/prometheus v0.301.0
17-
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
16+
golang.org/x/sync v0.19.0
1817
google.golang.org/protobuf v1.36.11
1918
)
2019

2120
require (
2221
github.com/beorn7/perks v1.0.1 // indirect
2322
github.com/cespare/xxhash/v2 v2.3.0 // indirect
23+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2424
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
25-
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
2625
github.com/hashicorp/errwrap v1.1.0 // indirect
2726
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
2827
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -36,6 +35,7 @@ require (
3635
github.com/mitchellh/go-homedir v1.1.0 // indirect
3736
github.com/mitchellh/mapstructure v1.5.0 // indirect
3837
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
38+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3939
github.com/prometheus/client_model v0.6.2 // indirect
4040
github.com/prometheus/common v0.66.1 // indirect
4141
github.com/prometheus/procfs v0.16.1 // indirect

go.sum

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
2222
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
2323
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2424
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
25-
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
26-
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
2725
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
2826
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
2927
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -79,8 +77,6 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
7977
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
8078
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
8179
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
82-
github.com/prometheus/prometheus v0.301.0 h1:0z8dgegmILivNomCd79RKvVkIols8vBGPKmcIBc7OyY=
83-
github.com/prometheus/prometheus v0.301.0/go.mod h1:BJLjWCKNfRfjp7Q48DrAjARnCi7GhfUVvUFEAWTssZM=
8480
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
8581
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
8682
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
@@ -93,10 +89,10 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
9389
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
9490
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
9591
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
96-
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
97-
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
9892
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
9993
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
94+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
95+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
10096
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
10197
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
10298
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
@@ -110,7 +106,5 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
110106
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
111107
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
112108
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
113-
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
114-
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
115109
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
116110
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import (
2222
"github.com/prometheus/client_golang/prometheus/promhttp"
2323
)
2424

25-
const grpcCodeOK = "ok"
25+
const (
26+
grpcCodeOK = "ok"
27+
flagSeparator = ";"
28+
)
2629

2730
func main() {
2831
url := flag.String("url", "http://localhost:7070", "The URL for the Parca instance to query")
@@ -35,7 +38,10 @@ func main() {
3538
customHeadersStr := flag.String("headers", "", "Comma-separated custom headers in the format 'key=value,key2=value2' to attach to requests")
3639

3740
queryInterval := flag.Duration("query-interval", 5*time.Second, "The time interval between queries to the Parca instance")
38-
queryRangeStr := flag.String("query-range", "15m,12h,168h", "Comma-separated time durations for query")
41+
queryRangeStr := flag.String("query-range", "15m;12h;168h", "Semicolon-separated time durations for query ranges")
42+
labelsStr := flag.String("labels", "all", "Semicolon-separated label selectors for queries (e.g., '{job=\"api\"};{level=\"info\"}'), or 'all' for no filtering")
43+
typesStr := flag.String("types", "", "Semicolon-separated profile types to query. If empty, types are auto-discovered from the backend.")
44+
valuesForLabelsStr := flag.String("values-for-labels", "", "Semicolon-separated label names to query values for (e.g., 'job;namespace'). If empty, values queries are skipped.")
3945

4046
flag.Parse()
4147

@@ -85,6 +91,12 @@ func main() {
8591
log.Fatalf("parse time range string error: %v", err)
8692
}
8793

94+
labelSelectors := parseLabels(*labelsStr)
95+
96+
profileTypes := parseProfileTypes(*typesStr)
97+
98+
valuesForLabels := parseValuesForLabels(*valuesForLabelsStr)
99+
88100
customHeaders, err := parseHeaders(*customHeadersStr)
89101
if err != nil {
90102
log.Fatalf("parse custom headers error: %v", err)
@@ -110,7 +122,7 @@ func main() {
110122
reg.MustRegister(collectors.NewGoCollector())
111123
reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
112124

113-
querier := NewQuerier(reg, client, queryRanges)
125+
querier := NewQuerier(reg, client, queryRanges, labelSelectors, profileTypes, valuesForLabels)
114126

115127
var gr run.Group
116128
gr.Add(run.SignalHandler(ctx, os.Interrupt, syscall.SIGTERM))
@@ -203,7 +215,7 @@ func (i *customHeadersInterceptor) WrapStreamingHandler(handler connect.Streamin
203215
}
204216

205217
func parseTimeRanges(input string) ([]time.Duration, error) {
206-
parts := strings.Split(input, ",")
218+
parts := strings.Split(input, flagSeparator)
207219
durations := make([]time.Duration, len(parts))
208220
var err error
209221

@@ -217,6 +229,54 @@ func parseTimeRanges(input string) ([]time.Duration, error) {
217229
return durations, nil
218230
}
219231

232+
func parseLabels(input string) []string {
233+
if input == "" || input == "all" {
234+
return []string{"all"}
235+
}
236+
parts := strings.Split(input, flagSeparator)
237+
selectors := make([]string, 0, len(parts))
238+
for _, p := range parts {
239+
p = strings.TrimSpace(p)
240+
if p != "" {
241+
selectors = append(selectors, p)
242+
}
243+
}
244+
if len(selectors) == 0 {
245+
return []string{"all"}
246+
}
247+
return selectors
248+
}
249+
250+
func parseProfileTypes(input string) []string {
251+
if input == "" {
252+
return nil
253+
}
254+
parts := strings.Split(input, flagSeparator)
255+
types := make([]string, 0, len(parts))
256+
for _, p := range parts {
257+
p = strings.TrimSpace(p)
258+
if p != "" {
259+
types = append(types, p)
260+
}
261+
}
262+
return types
263+
}
264+
265+
func parseValuesForLabels(input string) []string {
266+
if input == "" {
267+
return nil
268+
}
269+
parts := strings.Split(input, flagSeparator)
270+
labels := make([]string, 0, len(parts))
271+
for _, p := range parts {
272+
p = strings.TrimSpace(p)
273+
if p != "" {
274+
labels = append(labels, p)
275+
}
276+
}
277+
return labels
278+
}
279+
220280
func parseHeaders(input string) (map[string]string, error) {
221281
if input == "" {
222282
return nil, nil

0 commit comments

Comments
 (0)