Skip to content

Commit 77b0fe7

Browse files
authored
perf(resolver): avoid per-query label map allocations in metrics resolver (#2233)
1 parent c46ed64 commit 77b0fe7

4 files changed

Lines changed: 93 additions & 17 deletions

File tree

docs/prometheus_grafana.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Following metrics will be exported:
1717
| blocky_query_total | Counter of total queries, partitioned by client and DNS request type (A, AAAA, PTR, etc) |
1818
| blocky_request_duration_seconds | Histogram of request duration, partitioned by response type (Blocked, cached, etc) |
1919
| blocky_response_total | Counter of responses, partitioned by response type (Blocked, cached, etc), DNS response code, and reason |
20-
| blocky_client_response_total | Counter of responses, partitioned by client and response type (Blocked, cached, etc) |
20+
| blocky_client_response_total | Counter of query outcomes, partitioned by client and response type (Blocked, cached, etc); failed requests are counted as `response_type="err"` |
2121
| blocky_blocking_enabled | Boolean 1 if blocking is enabled, 0 otherwise |
2222
| blocky_cache_entries | Gauge of entries in cache |
2323
| blocky_cache_hits_total | Counter of the number of cache hits |
@@ -40,7 +40,7 @@ Following metrics will be exported:
4040

4141
To keep the `reason` label of `blocky_response_total` bounded, blocked responses use the matched
4242
group names only (e.g. `BLOCKED (ads)`), **not** the matched rule. The full reason including the
43-
matched rule (e.g. `BLOCKED (ads: *.docler.com)`) is still available in the [query log](configuration.md#query-log).
43+
matched rule (e.g. `BLOCKED (ads: *.docler.com)`) is still available in the [query log](configuration.md#query-logging).
4444
This avoids unbounded metric cardinality when large deny lists are used.
4545

4646
!!! note "`client` label cardinality"
@@ -52,6 +52,38 @@ Following metrics will be exported:
5252
the set of `client` label values can grow effectively unbounded over time. Consider this before
5353
scraping/retaining these metrics on such networks.
5454

55+
Blocky has no option to drop the label yet, so the mitigation is on the Prometheus side: drop the
56+
affected metrics at scrape time when you do not need the per-client breakdown.
57+
58+
```yaml
59+
metric_relabel_configs:
60+
- source_labels: [__name__]
61+
regex: "blocky_(query|client_response)_total"
62+
action: drop
63+
```
64+
65+
Dropping only the `client` label (`labeldrop`) does **not** work: the remaining series of the
66+
different clients collapse into one, and Prometheus rejects the scrape with a duplicate-sample
67+
error.
68+
69+
!!! note "`response_type` values of `blocky_client_response_total`"
70+
71+
The counter is incremented once per query that reaches the metrics resolver, so it sums to
72+
`blocky_query_total` rather than to `blocky_response_total` — the latter counts only successful
73+
responses. Requests that produced no response at all are recorded as `response_type="err"`, which
74+
is not one of the regular response types.
75+
76+
`FILTERED` and `NOTFQDN` never appear: the `filtering` and `fqdnOnly` resolvers answer those
77+
queries above the metrics resolver in the chain, so they are missing from `blocky_query_total`,
78+
`blocky_response_total` and `blocky_request_duration_seconds` as well. The query log sits below
79+
them in the chain too, so those queries are only visible in the [statistics](configuration.md#statistics).
80+
81+
Example — per-client rate of queries that were actually resolved rather than blocked:
82+
83+
```promql
84+
sum by (client) (rate(blocky_client_response_total{response_type!~"BLOCKED|REBIND|err"}[5m]))
85+
```
86+
5587
### Grafana dashboard
5688

5789
Example [Grafana](https://grafana.com/) dashboard

e2e/metrics_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ var _ = Describe("Metrics functional tests", func() {
245245
g.Expect(metrics).Should(SatisfyAll(
246246
ContainElement(MatchRegexp(`blocky_query_total\{[^}]*type="A"[^}]*\} \d+`)),
247247
ContainElement(MatchRegexp(`blocky_response_total\{[^}]*\} \d+`)),
248+
// blocked.com is on the ads denylist, so the per-client counter must show it
249+
ContainElement(MatchRegexp(`blocky_client_response_total\{client="[^"]+",response_type="BLOCKED"\} \d+`)),
250+
ContainElement(MatchRegexp(`blocky_client_response_total\{client="[^"]+",response_type="RESOLVED"\} \d+`)),
248251
ContainElement(MatchRegexp(`blocky_request_duration_seconds_bucket\{[^}]*\}`)),
249252
ContainElement(MatchRegexp(`blocky_request_duration_seconds_sum\{[^}]*\} [\d.]+`)),
250253
ContainElement(MatchRegexp(`blocky_request_duration_seconds_count\{[^}]*\} \d+`)),

resolver/metrics_resolver.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const (
2323
labelReason = "reason"
2424
labelResponseCode = "response_code"
2525
labelResponseType = "response_type"
26+
27+
// responseTypeErr is the synthetic response_type used when the chain returned no
28+
// response at all. It is not part of the model.ResponseType enum, so metrics using
29+
// the response_type label can carry it in addition to the enum values.
30+
responseTypeErr = "err"
2631
)
2732

2833
// MetricsResolver resolver that records metrics about requests/response
@@ -48,24 +53,21 @@ func (r *MetricsResolver) Resolve(ctx context.Context, request *model.Request) (
4853

4954
clientLabel := strings.Join(request.ClientNames, ",")
5055

51-
r.totalQueries.With(prometheus.Labels{
52-
labelClient: clientLabel,
53-
labelType: dns.TypeToString[request.Req.Question[0].Qtype],
54-
}).Inc()
56+
// WithLabelValues is used instead of With(prometheus.Labels{...}) throughout: the map
57+
// literal costs an allocation per query on the hot path. The value order must match the
58+
// label order of the corresponding metric constructor below.
59+
r.totalQueries.WithLabelValues(clientLabel, dns.TypeToString[request.Req.Question[0].Qtype]).Inc()
5560

5661
reqDuration := time.Since(request.RequestTS)
57-
responseType := "err"
62+
responseType := responseTypeErr
5863

5964
if response != nil {
6065
responseType = response.RType.String()
6166
}
6267

6368
r.durationHistogram.WithLabelValues(responseType).Observe(reqDuration.Seconds())
6469

65-
r.totalClientResponse.With(prometheus.Labels{
66-
labelClient: clientLabel,
67-
labelResponseType: responseType,
68-
}).Inc()
70+
r.totalClientResponse.WithLabelValues(clientLabel, responseType).Inc()
6971

7072
if err != nil {
7173
r.totalErrors.Inc()
@@ -79,11 +81,11 @@ func (r *MetricsResolver) Resolve(ctx context.Context, request *model.Request) (
7981
reasonLabel = response.Reason
8082
}
8183

82-
r.totalResponse.With(prometheus.Labels{
83-
labelReason: reasonLabel,
84-
labelResponseCode: dns.RcodeToString[response.Res.Rcode],
85-
labelResponseType: response.RType.String(),
86-
}).Inc()
84+
r.totalResponse.WithLabelValues(
85+
reasonLabel,
86+
dns.RcodeToString[response.Res.Rcode],
87+
response.RType.String(),
88+
).Inc()
8789
}
8890

8991
return response, err
@@ -158,7 +160,8 @@ func totalClientResponseMetric() *prometheus.CounterVec {
158160
return prometheus.NewCounterVec(
159161
prometheus.CounterOpts{
160162
Name: "blocky_client_response_total",
161-
Help: "Number of total responses per client and response type",
163+
Help: "Number of total responses per client and response type, " +
164+
"including failed requests as response_type=\"err\"",
162165
}, []string{labelClient, labelResponseType},
163166
)
164167
}

resolver/metrics_resolver_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ var _ = Describe("MetricResolver", func() {
108108
Expect(err).Should(Succeed())
109109
Expect(testutil.ToFloat64(cnt)).Should(BeNumerically("==", 1))
110110
})
111+
It("records the blocked response per client, without the unbounded reason", func() {
112+
_, err := sut.Resolve(ctx, newRequestWithClient("example.com.", A, "", "client"))
113+
Expect(err).Should(Succeed())
114+
115+
clientCnt, err := sut.totalClientResponse.GetMetricWith(prometheus.Labels{
116+
labelClient: "client",
117+
labelResponseType: ResponseTypeBLOCKED.String(),
118+
})
119+
Expect(err).Should(Succeed())
120+
Expect(testutil.ToFloat64(clientCnt)).Should(BeNumerically("==", 1))
121+
})
111122
})
112123
When("Response has no ReasonLabel", func() {
113124
BeforeEach(func() {
@@ -132,6 +143,19 @@ var _ = Describe("MetricResolver", func() {
132143
Expect(testutil.ToFloat64(cnt)).Should(BeNumerically("==", 1))
133144
})
134145
})
146+
When("A client resolves to several names", func() {
147+
It("joins them into a single client label, as blocky_query_total does", func() {
148+
_, err := sut.Resolve(ctx, newRequestWithClient("example.com.", A, "", "name1", "name2"))
149+
Expect(err).Should(Succeed())
150+
151+
clientCnt, err := sut.totalClientResponse.GetMetricWith(prometheus.Labels{
152+
labelClient: "name1,name2",
153+
labelResponseType: ResponseTypeRESOLVED.String(),
154+
})
155+
Expect(err).Should(Succeed())
156+
Expect(testutil.ToFloat64(clientCnt)).Should(BeNumerically("==", 1))
157+
})
158+
})
135159
When("Error occurs while request processing", func() {
136160
BeforeEach(func() {
137161
m = &mockResolver{}
@@ -153,6 +177,20 @@ var _ = Describe("MetricResolver", func() {
153177
Expect(testutil.ToFloat64(clientCnt)).Should(BeNumerically("==", 1))
154178
})
155179
})
180+
When("Metrics are disabled", func() {
181+
BeforeEach(func() {
182+
sut = NewMetricsResolver(config.Metrics{Enable: false})
183+
sut.Next(m)
184+
})
185+
It("records nothing", func() {
186+
_, err := sut.Resolve(ctx, newRequestWithClient("example.com.", A, "", "client"))
187+
Expect(err).Should(Succeed())
188+
189+
Expect(testutil.CollectAndCount(sut.totalClientResponse)).Should(BeZero())
190+
Expect(testutil.CollectAndCount(sut.totalQueries)).Should(BeZero())
191+
Expect(testutil.CollectAndCount(sut.totalResponse)).Should(BeZero())
192+
})
193+
})
156194
})
157195
})
158196
})

0 commit comments

Comments
 (0)