Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add query results cache support for cardinality analysis API #5212

Merged
merged 8 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* [CHANGE] Store-gateway: skip verifying index header integrity upon loading. To enable verification set `blocks_storage.bucket_store.index_header.verify_on_load: true`.
* [CHANGE] Querier: change the default value of the experimental `-querier.streaming-chunks-per-ingester-buffer-size` flag to 256. #5203
* [FEATURE] Query-frontend: added experimental support to cache cardinality query responses. The cache will be used when `-query-frontend.cache-results` is enabled and `-query-frontend.results-cache-ttl-for-cardinality-query` set to a value greater than 0. #5212
* [ENHANCEMENT] Cardinality API: When zone aware replication is enabled, the label values cardinality API can now tolerate single zone failure #5178
* [ENHANCEMENT] Distributor: optimize sending requests to ingesters when incoming requests don't need to be modified. For now this feature can be disabled by setting `-timeseries-unmarshal-caching-optimization-enabled=false`. #5137
* [ENHANCEMENT] Add advanced CLI flags to control gRPC client behaviour: #5161
Expand Down
11 changes: 11 additions & 0 deletions cmd/mimir/config-descriptor.json
Original file line number Diff line number Diff line change
Expand Up @@ -3159,6 +3159,17 @@
"fieldType": "duration",
"fieldCategory": "experimental"
},
{
"kind": "field",
"name": "results_cache_ttl_for_cardinality_query",
"required": false,
"desc": "Time to live duration for cached cardinality query results. The value 0 disables the cache.",
"fieldValue": null,
"fieldDefaultValue": 0,
"fieldFlag": "query-frontend.results-cache-ttl-for-cardinality-query",
"fieldType": "duration",
"fieldCategory": "experimental"
},
{
"kind": "field",
"name": "max_query_expression_size_bytes",
Expand Down
2 changes: 2 additions & 0 deletions cmd/mimir/help-all.txt.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,8 @@ Usage of ./cmd/mimir/mimir:
False to disable query statistics tracking. When enabled, a message with some statistics is logged for every query. (default true)
-query-frontend.results-cache-ttl duration
[experimental] Time to live duration for cached query results. If query falls into out-of-order time window, -query-frontend.results-cache-ttl-for-out-of-order-time-window is used instead. (default 1w)
-query-frontend.results-cache-ttl-for-cardinality-query duration
[experimental] Time to live duration for cached cardinality query results. The value 0 disables the cache.
-query-frontend.results-cache-ttl-for-out-of-order-time-window duration
[experimental] Time to live duration for cached query results if query falls into out-of-order time window. This is lower than -query-frontend.results-cache-ttl so that incoming out-of-order samples are returned in the query results sooner. (default 10m)
-query-frontend.results-cache.backend string
Expand Down
1 change: 1 addition & 0 deletions docs/sources/mimir/configure/about-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ The following features are currently experimental:
- Cardinality-based query sharding (`-query-frontend.query-sharding-target-series-per-shard`)
- Use of Redis cache backend (`-query-frontend.results-cache.backend=redis`)
- Query expression size limit (`-query-frontend.max-query-expression-size-bytes`)
- Cardinality query result caching (`-query-frontend.results-cache-ttl-for-cardinality-query`)
- Query-scheduler
- `-query-scheduler.querier-forget-delay`
- Store-gateway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2856,6 +2856,11 @@ The `limits` block configures default and per-tenant limits imposed by component
# CLI flag: -query-frontend.results-cache-ttl-for-out-of-order-time-window
[results_cache_ttl_for_out_of_order_time_window: <duration> | default = 10m]

# (experimental) Time to live duration for cached cardinality query results. The
# value 0 disables the cache.
# CLI flag: -query-frontend.results-cache-ttl-for-cardinality-query
[results_cache_ttl_for_cardinality_query: <duration> | default = 0s]

# (experimental) Max size of the raw query, in bytes. 0 to not apply a limit to
# the size of the query.
# CLI flag: -query-frontend.max-query-expression-size-bytes
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/golang/snappy v0.0.4
github.com/google/gopacket v1.1.19
github.com/gorilla/mux v1.8.0
github.com/grafana/dskit v0.0.0-20230608060957-487cf8d81b00
github.com/grafana/dskit v0.0.0-20230609090147-e93a97f8661a
github.com/grafana/e2e v0.1.1-0.20230221201045-21ebba73580b
github.com/hashicorp/golang-lru v0.6.0
github.com/json-iterator/go v1.1.12
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -863,8 +863,8 @@ github.com/gosimple/slug v1.1.1 h1:fRu/digW+NMwBIP+RmviTK97Ho/bEj/C9swrCspN3D4=
github.com/gosimple/slug v1.1.1/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
github.com/grafana-tools/sdk v0.0.0-20211220201350-966b3088eec9 h1:LQAhgcUPnzdjU/OjCJaLlPQI7NmQCRlfjMPSA1VegvA=
github.com/grafana-tools/sdk v0.0.0-20211220201350-966b3088eec9/go.mod h1:AHHlOEv1+GGQ3ktHMlhuTUwo3zljV3QJbC0+8o2kn+4=
github.com/grafana/dskit v0.0.0-20230608060957-487cf8d81b00 h1:7GKZNjkqVAnQmHp8PgNe0ywOOI2NKUaQx/8FhWAIBQA=
github.com/grafana/dskit v0.0.0-20230608060957-487cf8d81b00/go.mod h1:M03k2fzuQ2n9TVE1xfVKTESibxsXdw0wYfWT3+9Owp4=
github.com/grafana/dskit v0.0.0-20230609090147-e93a97f8661a h1:cJWfTVpWHlMWTMFpwe/Bu249yZBwVHSMP9QnUDhLYrA=
github.com/grafana/dskit v0.0.0-20230609090147-e93a97f8661a/go.mod h1:M03k2fzuQ2n9TVE1xfVKTESibxsXdw0wYfWT3+9Owp4=
github.com/grafana/e2e v0.1.1-0.20230221201045-21ebba73580b h1:dzle+89/D0hOxscjZlkb6ovYA52t9hl6h/S+hI8ek1Q=
github.com/grafana/e2e v0.1.1-0.20230221201045-21ebba73580b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY=
github.com/grafana/gomemcache v0.0.0-20230316202710-a081dae0aba9 h1:WB3bGH2f1UN6jkd6uAEWfHB8OD7dKJ0v2Oo6SNfhpfQ=
Expand Down
2 changes: 1 addition & 1 deletion operations/helm/charts/mimir-distributed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Helm chart for deploying [Grafana Mimir](https://grafana.com/docs/mimir/latest/)

For the full documentation, visit [Grafana mimir-distributed Helm chart documentation](https://grafana.com/docs/helm-charts/mimir-distributed/latest/).

> **Note:** The documentation version is derived from the Helm chart version which is 4.5.0-weekly.240.
> **Note:** The documentation version is derived from the Helm chart version which is 4.5.0-weekly.241.

When upgrading from Helm chart version 3.x, please see [Migrate from single zone to zone-aware replication with Helm](https://grafana.com/docs/helm-charts/mimir-distributed/latest/migration-guides/migrate-from-single-zone-with-helm/).
When upgrading from Helm chart version 2.1, please see [Upgrade the Grafana Mimir Helm chart from version 2.1 to 3.0](https://grafana.com/docs/helm-charts/mimir-distributed/latest/migration-guides/migrate-helm-chart-2.1-to-3.0/) as well.
Expand Down
226 changes: 226 additions & 0 deletions pkg/cardinality/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// SPDX-License-Identifier: AGPL-3.0-only

package cardinality

import (
"fmt"
"net/http"
"strconv"
"strings"

"github.com/pkg/errors"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
"golang.org/x/exp/slices"
)

const (
RequestTypeLabelNames = RequestType(iota)
RequestTypeLabelValues = RequestType(iota)

minLimit = 0
maxLimit = 500
defaultLimit = 20

stringParamSeparator = rune(0)
stringValueSeparator = rune(1)
)

type RequestType int

type LabelNamesRequest struct {
Matchers []*labels.Matcher
Limit int
}

// Strings returns a full representation of the request. The returned string can be
// used to uniquely identify the request.
func (r *LabelNamesRequest) String() string {
b := strings.Builder{}

// Add matchers.
for idx, matcher := range r.Matchers {
if idx > 0 {
b.WriteRune(stringValueSeparator)
}
b.WriteString(matcher.String())
}

// Add limit.
b.WriteRune(stringParamSeparator)
b.WriteString(strconv.Itoa(r.Limit))

return b.String()
}

func (r *LabelNamesRequest) RequestType() RequestType {
return RequestTypeLabelNames
}

// DecodeLabelNamesRequest decodes the input http.Request into a LabelNamesRequest.
// The input http.Request can either be a GET or POST with URL-encoded parameters.
func DecodeLabelNamesRequest(r *http.Request) (*LabelNamesRequest, error) {
var (
parsed = &LabelNamesRequest{}
err error
)

err = r.ParseForm()
if err != nil {
return nil, err
}

parsed.Matchers, err = extractSelector(r)
if err != nil {
return nil, err
}

parsed.Limit, err = extractLimit(r)
if err != nil {
return nil, err
}

return parsed, nil
}

type LabelValuesRequest struct {
LabelNames []model.LabelName
Matchers []*labels.Matcher
Limit int
}

// Strings returns a full representation of the request. The returned string can be
// used to uniquely identify the request.
func (r *LabelValuesRequest) String() string {
b := strings.Builder{}

// Add label names.
for idx, name := range r.LabelNames {
if idx > 0 {
b.WriteRune(stringValueSeparator)
}
b.WriteString(string(name))
}

// Add matchers.
b.WriteRune(stringParamSeparator)
for idx, matcher := range r.Matchers {
if idx > 0 {
b.WriteRune(stringValueSeparator)
}
b.WriteString(matcher.String())
}

// Add limit.
b.WriteRune(stringParamSeparator)
b.WriteString(strconv.Itoa(r.Limit))

return b.String()
}

func (r *LabelValuesRequest) RequestType() RequestType {
return RequestTypeLabelValues
}

// DecodeLabelValuesRequest decodes the input http.Request into a LabelValuesRequest.
// The input http.Request can either be a GET or POST with URL-encoded parameters.
func DecodeLabelValuesRequest(r *http.Request) (*LabelValuesRequest, error) {
var (
parsed = &LabelValuesRequest{}
err error
)

if err = r.ParseForm(); err != nil {
return nil, err
}

parsed.LabelNames, err = extractLabelNames(r)
if err != nil {
return nil, err
}

parsed.Matchers, err = extractSelector(r)
if err != nil {
return nil, err
}

parsed.Limit, err = extractLimit(r)
if err != nil {
return nil, err
}

return parsed, nil
}

// extractSelector parses and gets selector query parameter containing a single matcher
func extractSelector(r *http.Request) (matchers []*labels.Matcher, err error) {
selectorParams := r.Form["selector"]
if len(selectorParams) == 0 {
return nil, nil
}
if len(selectorParams) > 1 {
return nil, fmt.Errorf("multiple 'selector' params are not allowed")
}
matchers, err = parser.ParseMetricSelector(selectorParams[0])
if err != nil {
return nil, errors.Wrap(err, "failed to parse selector")
}

// Ensure stable sorting (improves query results cache hit ratio).
slices.SortFunc(matchers, func(a, b *labels.Matcher) bool {
if a.Name != b.Name {
return a.Name < b.Name
}
if a.Type != b.Type {
return a.Type < b.Type
}
return a.Value < b.Value
})

return matchers, nil
}

// extractLimit parses and validates request param `limit` if it's defined, otherwise returns default value.
func extractLimit(r *http.Request) (limit int, err error) {
limitParams := r.Form["limit"]
if len(limitParams) == 0 {
return defaultLimit, nil
}
if len(limitParams) > 1 {
return 0, fmt.Errorf("multiple 'limit' params are not allowed")
}
limit, err = strconv.Atoi(limitParams[0])
if err != nil {
return 0, err
}
if limit < minLimit {
return 0, fmt.Errorf("'limit' param cannot be less than '%v'", minLimit)
}
if limit > maxLimit {
return 0, fmt.Errorf("'limit' param cannot be greater than '%v'", maxLimit)
}
return limit, nil
}

// extractLabelNames parses and gets label_names query parameter containing an array of label values
func extractLabelNames(r *http.Request) ([]model.LabelName, error) {
labelNamesParams := r.Form["label_names[]"]
if len(labelNamesParams) == 0 {
return nil, fmt.Errorf("'label_names[]' param is required")
}

labelNames := make([]model.LabelName, 0, len(labelNamesParams))
for _, labelNameParam := range labelNamesParams {
labelName := model.LabelName(labelNameParam)
if !labelName.IsValid() {
return nil, fmt.Errorf("invalid 'label_names' param '%v'", labelNameParam)
}
labelNames = append(labelNames, labelName)
}

// Ensure stable sorting (improves query results cache hit ratio).
slices.Sort(labelNames)

return labelNames, nil
}
Loading