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

[dbnode] Query stats tracker for metrics and limits #2405

Merged
merged 26 commits into from
Jun 19, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bdc8559
query stats limits and tests 1
rallen090 Jun 11, 2020
a003a09
query stats limits and tests 2
rallen090 Jun 11, 2020
4e72fed
query stats limits and tests 3
rallen090 Jun 11, 2020
b402eb5
feedback on max docs field type
rallen090 Jun 11, 2020
f39608f
feedback on max docs field type 2
rallen090 Jun 11, 2020
42a4224
feedback on max docs field type 3
rallen090 Jun 11, 2020
1b1fc43
config test update
rallen090 Jun 11, 2020
88bed19
Merge remote-tracking branch 'origin/master' into ra/query-stats-limiter
rallen090 Jun 12, 2020
98e2fc2
Merge branch 'master' into ra/query-stats-limiter
rallen090 Jun 12, 2020
3822b7e
Merge branch 'master' into ra/query-stats-limiter
rallen090 Jun 15, 2020
284e072
Merge branch 'master' into ra/query-stats-limiter
rallen090 Jun 16, 2020
bff736a
suffix of Configuration on config structs
rallen090 Jun 16, 2020
b1b06be
fixing nits
rallen090 Jun 17, 2020
be9fd08
rename blocks to docs
rallen090 Jun 17, 2020
630bffb
rename blocks to docs 2
rallen090 Jun 17, 2020
fbde7e4
Address feedback
robskillington Jun 19, 2020
d83f73c
Merge branch 'master' into ra/query-stats-limiter
robskillington Jun 19, 2020
bda36cd
Add documentation
robskillington Jun 19, 2020
d324a8e
Fix config test
robskillington Jun 19, 2020
158add6
Log actual failing group and error
robskillington Jun 19, 2020
7e939fa
Fix links
robskillington Jun 19, 2020
13e9f8b
Fix links and regen open API docs
robskillington Jun 19, 2020
a91a4a2
Fix link to etcd doc
robskillington Jun 19, 2020
5effc10
Fix link to direct link to etcd external cluster section
robskillington Jun 19, 2020
196a604
Fix blocks link
robskillington Jun 19, 2020
c49cf41
Sleep between link checks to avoid documentation test failures
robskillington Jun 19, 2020
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
13 changes: 13 additions & 0 deletions src/cmd/services/m3dbnode/config/limits.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

package config

import "time"

// Limits contains configuration for configurable limits that can be applied to M3DB.
type Limits struct {
// MaxOutstandingWriteRequests controls the maximum number of outstanding write requests
Expand All @@ -39,4 +41,15 @@ type Limits struct {
// process would pause until some of the repaired bytes had been persisted to disk (and subsequently
// evicted from memory) at which point it would resume.
MaxOutstandingRepairedBytes int64 `yaml:"maxOutstandingRepairedBytes" validate:"min=0"`

// MaxRecentlyQueriedBlocks controls the max blocks being queried within a given
rallen090 marked this conversation as resolved.
Show resolved Hide resolved
// lookback period. Queries which are issued while this max is surpassed are abandonded.
MaxRecentlyQueriedBlocks *MaxRecentlyQueriedBlocks `yaml:"maxRecentlyQueriedBlocks"`
}

// MaxRecentlyQueriedBlocks controls the max blocks being queried within a given
// lookback period. Queries which are issued while this max is surpassed are abandonded.
type MaxRecentlyQueriedBlocks struct {
rallen090 marked this conversation as resolved.
Show resolved Hide resolved
Max int64 `yaml:"max" validate:"min=0"`
rallen090 marked this conversation as resolved.
Show resolved Hide resolved
Lookback time.Duration `yaml:"lookback"`
rallen090 marked this conversation as resolved.
Show resolved Hide resolved
}
19 changes: 16 additions & 3 deletions src/dbnode/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ type RunOptions struct {
InterruptCh <-chan error

// QueryStatsTrackerFn returns a tracker for tracking query stats.
QueryStatsTrackerFn func(instrument.Options) stats.QueryStatsTracker
QueryStatsTrackerFn func(opts instrument.Options, config stats.QueryStatsConfig) stats.QueryStatsTracker

// CustomOptions are custom options to apply to the session.
CustomOptions []client.CustomAdminOption
Expand Down Expand Up @@ -409,11 +409,24 @@ func Run(runOpts RunOptions) {
defer stopReporting()

// Setup query stats tracking.
var statsConfig stats.QueryStatsConfig
if runOpts.Config.Limits.MaxRecentlyQueriedBlocks == nil {
statsConfig = stats.DefaultQueryStatsConfigForMetrics()
} else {
maxQueriedBlocks := runOpts.Config.Limits.MaxRecentlyQueriedBlocks
statsConfig = stats.DefaultQueryStatsConfigForMetricsAndLimits(
maxQueriedBlocks.Max,
maxQueriedBlocks.Lookback,
)
}
var tracker stats.QueryStatsTracker
if runOpts.QueryStatsTrackerFn == nil {
tracker = stats.DefaultQueryStatsTrackerForMetrics(iopts)
tracker, err = stats.DefaultQueryStatsTracker(iopts, statsConfig)
if err != nil {
logger.Fatal("could not construct query stats tracker from config", zap.Error(err))
}
} else {
tracker = runOpts.QueryStatsTrackerFn(iopts)
tracker = runOpts.QueryStatsTrackerFn(iopts, statsConfig)
}
queryStats := stats.NewQueryStats(tracker)
queryStats.Start()
Expand Down
9 changes: 9 additions & 0 deletions src/dbnode/storage/stats/query_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ type QueryStats interface {
Stop()
}

// QueryStatsConfig holds configuration for how a tracker should handle query stats.
type QueryStatsConfig struct {
// MaxDocs limits how many recently queried max
// documents are allowed before queries are abandoned.
MaxDocs *int64
rallen090 marked this conversation as resolved.
Show resolved Hide resolved
// Lookback specifies the lookback period over which stats are aggregated.
Lookback time.Duration
rallen090 marked this conversation as resolved.
Show resolved Hide resolved
}

// QueryStatsValues stores values of query stats.
type QueryStatsValues struct {
RecentDocs int64
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package stats

import (
"fmt"
"time"

"github.com/m3db/m3/src/x/instrument"
Expand All @@ -31,31 +32,71 @@ import (
const defaultLookback = time.Second * 5

// Tracker implementation that emits query stats as metrics.
type queryStatsMetricsTracker struct {
type queryStatsTracker struct {
recentDocs tally.Gauge
totalDocs tally.Counter

config QueryStatsConfig
}

var _ QueryStatsTracker = (*queryStatsTracker)(nil)

// DefaultQueryStatsConfigForMetrics returns a default
// config for tracking query stats as metrics.
func DefaultQueryStatsConfigForMetrics() QueryStatsConfig {
return QueryStatsConfig{
MaxDocs: nil,
Lookback: defaultLookback,
}
}

var _ QueryStatsTracker = (*queryStatsMetricsTracker)(nil)
// DefaultQueryStatsConfigForMetricsAndLimits returns a
// default config for tracking query stats as metrics.
func DefaultQueryStatsConfigForMetricsAndLimits(
rallen090 marked this conversation as resolved.
Show resolved Hide resolved
maxDocs int64,
lookback time.Duration,
) QueryStatsConfig {
return QueryStatsConfig{
MaxDocs: &maxDocs,
Lookback: lookback,
}
}

// DefaultQueryStatsTrackerForMetrics provides a tracker
// implementation that emits query stats as metrics.
func DefaultQueryStatsTrackerForMetrics(opts instrument.Options) QueryStatsTracker {
// DefaultQueryStatsTracker provides a tracker
// implementation that emits query stats as metrics
// and enforces limits.
func DefaultQueryStatsTracker(
opts instrument.Options,
config QueryStatsConfig,
) (QueryStatsTracker, error) {
if config.MaxDocs != nil && *config.MaxDocs <= 0 {
return nil, fmt.Errorf("query stats tracker requires max docs > 0 if not nil (%d)", *config.MaxDocs)
}
if config.Lookback <= 0 {
return nil, fmt.Errorf("query stats tracker requires lookback > 0 (%d)", config.Lookback)
}
scope := opts.
MetricsScope().
SubScope("query-stats")
return &queryStatsMetricsTracker{
return &queryStatsTracker{
config: config,
recentDocs: scope.Gauge("recent-docs-per-block"),
totalDocs: scope.Counter("total-docs-per-block"),
}
}, nil
}

func (t *queryStatsMetricsTracker) TrackStats(values QueryStatsValues) error {
func (t *queryStatsTracker) TrackStats(values QueryStatsValues) error {
// Track stats as metrics.
t.recentDocs.Update(float64(values.RecentDocs))
t.totalDocs.Inc(values.NewDocs)

// Enforce max queried docs (if specified).
if t.config.MaxDocs != nil && values.RecentDocs > *t.config.MaxDocs {
return fmt.Errorf("query was aborted due to too many recent docs queried (%d)", values.RecentDocs)
robskillington marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}

func (t *queryStatsMetricsTracker) Lookback() time.Duration {
return defaultLookback
func (t *queryStatsTracker) Lookback() time.Duration {
return t.config.Lookback
}
155 changes: 155 additions & 0 deletions src/dbnode/storage/stats/query_stats_default_tracker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package stats

import (
"testing"
"time"

"github.com/m3db/m3/src/x/instrument"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber-go/tally"
)

func TestValidateTrackerInputs(t *testing.T) {
scope := tally.NewTestScope("", nil)
opts := instrument.NewOptions().SetMetricsScope(scope)

validMax := int64(1)
negativeMax := int64(-1)
zeroMax := int64(0)

for _, test := range []struct {
name string
maxDocs *int64
lookback time.Duration
expectedError string
}{
{"valid lookback without limit", nil, time.Millisecond, ""},
{"valid lookback with valid limit", &validMax, time.Millisecond, ""},
{"negative lookback", nil, -time.Millisecond, "query stats tracker requires lookback > 0 (-1000000)"},
{"zero lookback", nil, time.Duration(0), "query stats tracker requires lookback > 0 (0)"},
{"negative max", &negativeMax, time.Millisecond, "query stats tracker requires max docs > 0 if not nil (-1)"},
{"zero max", &zeroMax, time.Millisecond, "query stats tracker requires max docs > 0 if not nil (0)"},
} {
t.Run(test.name, func(t *testing.T) {
_, err := DefaultQueryStatsTracker(opts, QueryStatsConfig{
MaxDocs: test.maxDocs,
Lookback: test.lookback,
})
if test.expectedError != "" {
require.Error(t, err)
require.Equal(t, test.expectedError, err.Error())
} else {
require.NoError(t, err)
}
})
}
}

func TestEmitQueryStatsBasedMetrics(t *testing.T) {
for _, test := range []struct {
name string
config QueryStatsConfig
}{
{"metrics only", DefaultQueryStatsConfigForMetrics()},
{"metrics and limits", DefaultQueryStatsConfigForMetricsAndLimits(1000, time.Second)},
} {
t.Run(test.name, func(t *testing.T) {
scope := tally.NewTestScope("", nil)
opts := instrument.NewOptions().SetMetricsScope(scope)

tracker, err := DefaultQueryStatsTracker(opts, test.config)
require.NoError(t, err)

err = tracker.TrackStats(QueryStatsValues{RecentDocs: 100, NewDocs: 5})
require.NoError(t, err)
verifyMetrics(t, scope, 100, 5)

err = tracker.TrackStats(QueryStatsValues{RecentDocs: 140, NewDocs: 10})
require.NoError(t, err)
verifyMetrics(t, scope, 140, 15)
})
}
}

func TestLimitMaxDocs(t *testing.T) {
scope := tally.NewTestScope("", nil)
opts := instrument.NewOptions().SetMetricsScope(scope)

maxDocs := int64(100)

for _, test := range []struct {
name string
config QueryStatsConfig
expectLimitError bool
}{
{"metrics only", DefaultQueryStatsConfigForMetrics(), false},
{"metrics and limits", DefaultQueryStatsConfigForMetricsAndLimits(maxDocs, time.Second), true},
} {
t.Run(test.name, func(t *testing.T) {
tracker, err := DefaultQueryStatsTracker(opts, test.config)
require.NoError(t, err)

err = tracker.TrackStats(QueryStatsValues{RecentDocs: maxDocs + 1})
if test.expectLimitError {
require.Error(t, err)
require.Equal(t, "query was aborted due to too many recent docs queried (101)", err.Error())
} else {
require.NoError(t, err)
}

err = tracker.TrackStats(QueryStatsValues{RecentDocs: maxDocs - 1})
require.NoError(t, err)

err = tracker.TrackStats(QueryStatsValues{RecentDocs: 0})
require.NoError(t, err)

err = tracker.TrackStats(QueryStatsValues{RecentDocs: maxDocs + 1})
if test.expectLimitError {
require.Error(t, err)
require.Equal(t, "query was aborted due to too many recent docs queried (101)", err.Error())
} else {
require.NoError(t, err)
}

err = tracker.TrackStats(QueryStatsValues{RecentDocs: maxDocs - 1})
require.NoError(t, err)

err = tracker.TrackStats(QueryStatsValues{RecentDocs: 0})
require.NoError(t, err)
})
}
}

func verifyMetrics(t *testing.T, scope tally.TestScope, expectedRecent float64, expectedTotal int64) {
snapshot := scope.Snapshot()

recent, exists := snapshot.Gauges()["query-stats.recent-docs-per-block+"]
assert.True(t, exists)
assert.Equal(t, expectedRecent, recent.Value())

total, exists := snapshot.Counters()["query-stats.total-docs-per-block+"]
assert.True(t, exists)
assert.Equal(t, expectedTotal, total.Value())
}
58 changes: 0 additions & 58 deletions src/dbnode/storage/stats/query_stats_metrics_test.go

This file was deleted.