Repository: VictoriaMetrics/VictoriaMetrics
Pull request #1076 (2021-02-17 21:06:24)

Add `Labels limit exceeded` panel to dashboard
Same as https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1072 but for cluster


Patch:
diff --git a/app/vmagent/README.md b/app/vmagent/README.md
index bf40e17ba..145491fcb 100644
--- a/app/vmagent/README.md
+++ b/app/vmagent/README.md
@@ -298,6 +298,16 @@ It may be useful for performing `vmagent` rolling update without scrape loss.
   the url may contain sensitive information such as auth tokens or passwords.
   Pass `-remoteWrite.showURL` command-line flag when starting `vmagent` in order to see all the valid urls.
 
+* If scrapes must be aligned in time (for instance, if they must be performed at the beginning of every hour), then set `scrape_align_interval` option
+  in the corresponding scrape config. For example, the following config aligns hourly scrapes to the nearest 10 minutes:
+
+  ```yml
+  scrape_configs:
+  - job_name: foo
+    scrape_interval: 1h
+    scrape_align_interval: 10m
+  ```
+
 * If you see `skipping duplicate scrape target with identical labels` errors when scraping Kubernetes pods, then it is likely these pods listen multiple ports
   or they use init container. These errors can be either fixed or suppressed with `-promscrape.suppressDuplicateScrapeTargetErrors` command-line flag.
   See available options below if you prefer fixing the root cause of the error:
diff --git a/app/vmagent/remotewrite/remotewrite.go b/app/vmagent/remotewrite/remotewrite.go
index 337772560..824ea9ad7 100644
--- a/app/vmagent/remotewrite/remotewrite.go
+++ b/app/vmagent/remotewrite/remotewrite.go
@@ -227,6 +227,7 @@ func (rwctx *remoteWriteCtx) MustStop() {
 	}
 	rwctx.idx = 0
 	rwctx.pss = nil
+	rwctx.fq.UnblockAllReaders()
 	rwctx.c.MustStop()
 	rwctx.c = nil
 	rwctx.fq.MustClose()
diff --git a/app/vmalert/multiarch/Dockerfile b/app/vmalert/multiarch/Dockerfile
new file mode 100644
index 000000000..14ad8f0e8
--- /dev/null
+++ b/app/vmalert/multiarch/Dockerfile
@@ -0,0 +1,12 @@
+# See https://medium.com/on-docker/use-multi-stage-builds-to-inject-ca-certs-ad1e8f01de1b
+ARG certs_image
+ARG root_image
+FROM $certs_image as certs
+RUN apk --update --no-cache add ca-certificates
+
+FROM $root_image
+COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
+EXPOSE 8880
+ENTRYPOINT ["/vmalert-prod"]
+ARG TARGETARCH
+COPY vmalert-${TARGETARCH}-prod ./vmalert-prod
diff --git a/app/vmselect/promql/binary_op.go b/app/vmselect/promql/binary_op.go
index 7d827607a..d258d6330 100644
--- a/app/vmselect/promql/binary_op.go
+++ b/app/vmselect/promql/binary_op.go
@@ -252,16 +252,21 @@ func mergeNonOverlappingTimeseries(dst, src *timeseries) bool {
 	// Verify whether the time series can be merged.
 	srcValues := src.Values
 	dstValues := dst.Values
+	overlaps := 0
 	_ = dstValues[len(srcValues)-1]
 	for i, v := range srcValues {
 		if math.IsNaN(v) {
 			continue
 		}
 		if !math.IsNaN(dstValues[i]) {
-			return false
+			overlaps++
 		}
 	}
-
+	// Allow up to two overlapping datapoints, which can appear due to staleness algorithm,
+	// which can add a few datapoints in the end of time series.
+	if overlaps > 2 {
+		return false
+	}
 	// Time series can be merged. Merge them.
 	for i, v := range srcValues {
 		if math.IsNaN(v) {
diff --git a/dashboards/victoriametrics.json b/dashboards/victoriametrics.json
index f714a10cf..f4e87cd62 100644
--- a/dashboards/victoriametrics.json
+++ b/dashboards/victoriametrics.json
@@ -50,7 +50,7 @@
   "gnetId": null,
   "graphTooltip": 0,
   "id": null,
-  "iteration": 1606684714463,
+  "iteration": 1613595700130,
   "links": [
     {
       "icon": "doc",
@@ -2264,7 +2264,7 @@
           "fillGradient": 0,
           "gridPos": {
             "h": 8,
-            "w": 24,
+            "w": 12,
             "x": 0,
             "y": 11
           },
@@ -2339,6 +2339,105 @@
             "align": false,
             "alignLevel": null
           }
+        },
+        {
+          "aliasColors": {},
+          "bars": false,
+          "dashLength": 10,
+          "dashes": false,
+          "datasource": "$ds",
+          "description": "VictoriaMetrics limits the number of labels per each metric with `-maxLabelsPerTimeseries` command-line flag.\n\nThis prevents from ingesting metrics with too many labels. The value of `maxLabelsPerTimeseries` must be adjusted for your workload.\n\nWhen limit is exceeded (graph is > 0) - extra labels are dropped, which could result in unexpected identical time series.",
+          "fieldConfig": {
+            "defaults": {
+              "custom": {},
+              "links": []
+            },
+            "overrides": []
+          },
+          "fill": 1,
+          "fillGradient": 0,
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 12,
+            "y": 11
+          },
+          "hiddenSeries": false,
+          "id": 116,
+          "legend": {
+            "avg": false,
+            "current": false,
+            "max": false,
+            "min": false,
+            "show": false,
+            "total": false,
+            "values": false
+          },
+          "lines": true,
+          "linewidth": 1,
+          "links": [],
+          "nullPointMode": "null",
+          "percentage": false,
+          "pluginVersion": "7.1.1",
+          "pointradius": 2,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [],
+          "spaceLength": 10,
+          "stack": false,
+          "steppedLine": false,
+          "targets": [
+            {
+              "expr": "sum(increase(vm_metrics_with_dropped_labels_total{job=\"$job_insert\", instance=~\"$instance\"}[5m]))",
+              "format": "time_series",
+              "hide": false,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "metrics with dropped labels",
+              "refId": "A"
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeRegions": [],
+          "timeShift": null,
+          "title": "Labels limit exceeded ($instance)",
+          "tooltip": {
+            "shared": true,
+            "sort": 0,
+            "value_type": "individual"
+          },
+          "type": "graph",
+          "xaxis": {
+            "buckets": null,
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "decimals": 2,
+              "format": "percentunit",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": "0",
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": "0",
+              "show": true
+            }
+          ],
+          "yaxis": {
+            "align": false,
+            "alignLevel": null
+          }
         }
       ],
       "title": "Troubleshooting",
diff --git a/docs/Articles.md b/docs/Articles.md
index 3a42d62fe..4a4248610 100644
--- a/docs/Articles.md
+++ b/docs/Articles.md
@@ -24,6 +24,8 @@
 * [Monitoring Kubernetes with VictoriaMetrics+Prometheus](https://speakerdeck.com/bo0km4n/victoriametrics-plus-prometheusdegou-zhu-surufu-shu-kubernetesfalsejian-shi-ji-pan)
 * [High-performance Graphite storage solution on top of VictoriaMetrics](https://golangexample.com/a-high-performance-graphite-storage-solution/)
 * [Cloud Native Model Driven Telemetry Stack on OpenShift](https://cer6erus.medium.com/cloud-native-model-driven-telemetry-stack-on-openshift-80712621f5bc)
+* [Observability, Availability & DORA’s Research Program](https://medium.com/alteos-tech-blog/observability-availability-and-doras-research-program-85deb6680e78)
+* [Tame Kubernetes Costs with Percona Monitoring and Management and Prometheus Operator](https://www.percona.com/blog/2021/02/12/tame-kubernetes-costs-with-percona-monitoring-and-management-and-prometheus-operator/)
 
 
 ## Our articles
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 44e43252f..94403e514 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -2,6 +2,19 @@
 
 # tip
 
+* FEATURE: vmagent: add `scrape_align_interval` config option, which can be used for aligning scrapes to the beginning of the configured interval. See [these docs](https://victoriametrics.github.io/vmagent.html#troubleshooting) for details.
+
+* BUGFIX: vmagent: properly perform graceful shutdown on `SIGINT` and `SIGTERM` signals. The graceful shutdown has been broken in `v1.54.0`. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1065
+* BUGFIX: reduce the probability of `duplicate time series` errors when querying Kubernetes metrics.
+
+
+# [v1.54.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.1)
+
+* BUGFIX: properly handle queries containing a filter on metric name plus any number of negative filters and zero non-negative filters. For example, `node_cpu_seconds_total{mode!="idle"}`. The bug was introduced in [v1.54.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.0).
+
+
+# [v1.54.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.0)
+
 * FEATURE: optimize searching for matching metrics for `metric{<label_filters>}` queries if `<label_filters>` contains at least a single filter. For example, the query `up{job="foobar"}` should find the matching time series much faster than previously.
 * FEATURE: reduce execution times for `q1 <binary_op> q2` queries by executing `q1` and `q2` in parallel.
 * FEATURE: switch from Go1.15 to [Go1.16](https://golang.org/doc/go1.16) for building prod binaries.
diff --git a/docs/vmagent.md b/docs/vmagent.md
index bf40e17ba..145491fcb 100644
--- a/docs/vmagent.md
+++ b/docs/vmagent.md
@@ -298,6 +298,16 @@ It may be useful for performing `vmagent` rolling update without scrape loss.
   the url may contain sensitive information such as auth tokens or passwords.
   Pass `-remoteWrite.showURL` command-line flag when starting `vmagent` in order to see all the valid urls.
 
+* If scrapes must be aligned in time (for instance, if they must be performed at the beginning of every hour), then set `scrape_align_interval` option
+  in the corresponding scrape config. For example, the following config aligns hourly scrapes to the nearest 10 minutes:
+
+  ```yml
+  scrape_configs:
+  - job_name: foo
+    scrape_interval: 1h
+    scrape_align_interval: 10m
+  ```
+
 * If you see `skipping duplicate scrape target with identical labels` errors when scraping Kubernetes pods, then it is likely these pods listen multiple ports
   or they use init container. These errors can be either fixed or suppressed with `-promscrape.suppressDuplicateScrapeTargetErrors` command-line flag.
   See available options below if you prefer fixing the root cause of the error:
diff --git a/lib/persistentqueue/fastqueue.go b/lib/persistentqueue/fastqueue.go
index 5a3dd33e2..5c25933f6 100644
--- a/lib/persistentqueue/fastqueue.go
+++ b/lib/persistentqueue/fastqueue.go
@@ -52,16 +52,24 @@ func MustOpenFastQueue(path, name string, maxInmemoryBlocks, maxPendingBytes int
 	return fq
 }
 
-// MustClose unblocks all the readers.
-//
-// It is expected no new writers during and after the call.
-func (fq *FastQueue) MustClose() {
+// UnblockAllReaders unblocks all the readers.
+func (fq *FastQueue) UnblockAllReaders() {
 	fq.mu.Lock()
 	defer fq.mu.Unlock()
 
 	// Unblock blocked readers
 	fq.mustStop = true
 	fq.cond.Broadcast()
+}
+
+// MustClose unblocks all the readers.
+//
+// It is expected no new writers during and after the call.
+func (fq *FastQueue) MustClose() {
+	fq.UnblockAllReaders()
+
+	fq.mu.Lock()
+	defer fq.mu.Unlock()
 
 	// flush blocks from fq.ch to fq.pq, so they can be persisted
 	fq.flushInmemoryBlocksToFileLocked()
diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go
index 31e1ce858..aade32faa 100644
--- a/lib/promscrape/config.go
+++ b/lib/promscrape/config.go
@@ -89,9 +89,10 @@ type ScrapeConfig struct {
 	SampleLimit          int                         `yaml:"sample_limit,omitempty"`
 
 	// These options are supported only by lib/promscrape.
-	DisableCompression bool `yaml:"disable_compression,omitempty"`
-	DisableKeepAlive   bool `yaml:"disable_keepalive,omitempty"`
-	StreamParse        bool `yaml:"stream_parse,omitempty"`
+	DisableCompression  bool          `yaml:"disable_compression,omitempty"`
+	DisableKeepAlive    bool          `yaml:"disable_keepalive,omitempty"`
+	StreamParse         bool          `yaml:"stream_parse,omitempty"`
+	ScrapeAlignInterval time.Duration `yaml:"scrape_align_interval,omitempty"`
 
 	// This is set in loadConfig
 	swc *scrapeWorkConfig
@@ -508,6 +509,7 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf
 		disableCompression:   sc.DisableCompression,
 		disableKeepAlive:     sc.DisableKeepAlive,
 		streamParse:          sc.StreamParse,
+		scrapeAlignInterval:  sc.ScrapeAlignInterval,
 	}
 	return swc, nil
 }
@@ -530,6 +532,7 @@ type scrapeWorkConfig struct {
 	disableCompression   bool
 	disableKeepAlive     bool
 	streamParse          bool
+	scrapeAlignInterval  time.Duration
 }
 
 func appendKubernetesScrapeWork(dst []*ScrapeWork, sdc *kubernetes.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) {
@@ -761,6 +764,7 @@ func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, e
 		DisableCompression:   swc.disableCompression,
 		DisableKeepAlive:     swc.disableKeepAlive,
 		StreamParse:          swc.streamParse,
+		ScrapeAlignInterval:  swc.scrapeAlignInterval,
 
 		jobNameOriginal: swc.jobName,
 	})
diff --git a/lib/promscrape/scrapework.go b/lib/promscrape/scrapework.go
index 585529e01..adff00c2c 100644
--- a/lib/promscrape/scrapework.go
+++ b/lib/promscrape/scrapework.go
@@ -90,6 +90,9 @@ type ScrapeWork struct {
 	// Whether to parse target responses in a streaming manner.
 	StreamParse bool
 
+	// The interval for aligning the first scrape.
+	ScrapeAlignInterval time.Duration
+
 	// The original 'job_name'
 	jobNameOriginal string
 }
@@ -100,9 +103,9 @@ type ScrapeWork struct {
 func (sw *ScrapeWork) key() string {
 	// Do not take into account OriginalLabels.
 	key := fmt.Sprintf("ScrapeURL=%s, ScrapeInterval=%s, ScrapeTimeout=%s, HonorLabels=%v, HonorTimestamps=%v, Labels=%s, "+
-		"AuthConfig=%s, MetricRelabelConfigs=%s, SampleLimit=%d, DisableCompression=%v, DisableKeepAlive=%v, StreamParse=%v",
+		"AuthConfig=%s, MetricRelabelConfigs=%s, SampleLimit=%d, DisableCompression=%v, DisableKeepAlive=%v, StreamParse=%v, ScrapeAlignInterval=%s",
 		sw.ScrapeURL, sw.ScrapeInterval, sw.ScrapeTimeout, sw.HonorLabels, sw.HonorTimestamps, sw.LabelsString(),
-		sw.AuthConfig.String(), sw.metricRelabelConfigsString(), sw.SampleLimit, sw.DisableCompression, sw.DisableKeepAlive, sw.StreamParse)
+		sw.AuthConfig.String(), sw.metricRelabelConfigsString(), sw.SampleLimit, sw.DisableCompression, sw.DisableKeepAlive, sw.StreamParse, sw.ScrapeAlignInterval)
 	return key
 }
 
@@ -180,20 +183,27 @@ type scrapeWork struct {
 }
 
 func (sw *scrapeWork) run(stopCh <-chan struct{}) {
-	// Calculate start time for the first scrape from ScrapeURL and labels.
-	// This should spread load when scraping many targets with different
-	// scrape urls and labels.
-	// This also makes consistent scrape times across restarts
-	// for a target with the same ScrapeURL and labels.
 	scrapeInterval := sw.Config.ScrapeInterval
-	key := fmt.Sprintf("ScrapeURL=%s, Labels=%s", sw.Config.ScrapeURL, sw.Config.LabelsString())
-	h := uint32(xxhash.Sum64([]byte(key)))
-	randSleep := uint64(float64(scrapeInterval) * (float64(h) / (1 << 32)))
-	sleepOffset := uint64(time.Now().UnixNano()) % uint64(scrapeInterval)
-	if randSleep < sleepOffset {
-		randSleep += uint64(scrapeInterval)
+	var randSleep uint64
+	if sw.Config.ScrapeAlignInterval <= 0 {
+		// Calculate start time for the first scrape from ScrapeURL and labels.
+		// This should spread load when scraping many targets with different
+		// scrape urls and labels.
+		// This also makes consistent scrape times across restarts
+		// for a target with the same ScrapeURL and labels.
+		key := fmt.Sprintf("ScrapeURL=%s, Labels=%s", sw.Config.ScrapeURL, sw.Config.LabelsString())
+		h := uint32(xxhash.Sum64([]byte(key)))
+		randSleep = uint64(float64(scrapeInterval) * (float64(h) / (1 << 32)))
+		sleepOffset := uint64(time.Now().UnixNano()) % uint64(scrapeInterval)
+		if randSleep < sleepOffset {
+			randSleep += uint64(scrapeInterval)
+		}
+		randSleep -= sleepOffset
+	} else {
+		d := uint64(sw.Config.ScrapeAlignInterval)
+		randSleep = d - uint64(time.Now().UnixNano())%d
+		randSleep %= uint64(scrapeInterval)
 	}
-	randSleep -= sleepOffset
 	timer := timerpool.Get(time.Duration(randSleep))
 	var timestamp int64
 	var ticker *time.Ticker
diff --git a/lib/storage/index_db.go b/lib/storage/index_db.go
index 2e4fdc8d8..792a8e116 100644
--- a/lib/storage/index_db.go
+++ b/lib/storage/index_db.go
@@ -6,7 +6,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"math"
 	"path/filepath"
 	"sort"
 	"sync"
@@ -2072,7 +2071,7 @@ func (is *indexSearch) getTagFilterWithMinMetricIDsCount(tfs *TagFilters, maxMet
 			continue
 		}
 
-		metricIDs, err := is.getMetricIDsForTagFilter(tf, nil, maxMetrics)
+		metricIDs, _, err := is.getMetricIDsForTagFilter(tf, nil, maxMetrics)
 		if err != nil {
 			if err == errFallbackToMetricNameMatch {
 				// Skip tag filters requiring to scan for too many metrics.
@@ -2394,34 +2393,36 @@ const (
 
 var uselessTagFilterCacheValue = []byte("1")
 
-func (is *indexSearch) getMetricIDsForTagFilter(tf *tagFilter, filter *uint64set.Set, maxMetrics int) (*uint64set.Set, error) {
+func (is *indexSearch) getMetricIDsForTagFilter(tf *tagFilter, filter *uint64set.Set, maxMetrics int) (*uint64set.Set, uint64, error) {
 	if tf.isNegative {
 		logger.Panicf("BUG: isNegative must be false")
 	}
 	metricIDs := &uint64set.Set{}
 	if len(tf.orSuffixes) > 0 {
 		// Fast path for orSuffixes - seek for rows for each value from orSuffixes.
-		if err := is.updateMetricIDsForOrSuffixesNoFilter(tf, maxMetrics, metricIDs); err != nil {
+		loopsCount, err := is.updateMetricIDsForOrSuffixesNoFilter(tf, maxMetrics, metricIDs)
+		if err != nil {
 			if err == errFallbackToMetricNameMatch {
-				return nil, err
+				return nil, loopsCount, err
 			}
-			return nil, fmt.Errorf("error when searching for metricIDs for tagFilter in fast path: %w; tagFilter=%s", err, tf)
+			return nil, loopsCount, fmt.Errorf("error when searching for metricIDs for tagFilter in fast path: %w; tagFilter=%s", err, tf)
 		}
-		return metricIDs, nil
+		return metricIDs, loopsCount, nil
 	}
 
 	// Slow path - scan for all the rows with the given prefix.
-	maxLoops := maxMetrics * maxIndexScanSlowLoopsPerMetric
-	if err := is.getMetricIDsForTagFilterSlow(tf, filter, maxLoops, metricIDs.Add); err != nil {
+	maxLoopsCount := uint64(maxMetrics) * maxIndexScanSlowLoopsPerMetric
+	loopsCount, err := is.getMetricIDsForTagFilterSlow(tf, filter, maxLoopsCount, metricIDs.Add)
+	if err != nil {
 		if err == errFallbackToMetricNameMatch {
-			return nil, err
+			return nil, loopsCount, err
 		}
-		return nil, fmt.Errorf("error when searching for metricIDs for tagFilter in slow path: %w; tagFilter=%s", err, tf)
+		return nil, loopsCount, fmt.Errorf("error when searching for metricIDs for tagFilter in slow path: %w; tagFilter=%s", err, tf)
 	}
-	return metricIDs, nil
+	return metricIDs, loopsCount, nil
 }
 
-func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, filter *uint64set.Set, maxLoops int, f func(metricID uint64)) error {
+func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, filter *uint64set.Set, maxLoopsCount uint64, f func(metricID uint64)) (uint64, error) {
 	if len(tf.orSuffixes) > 0 {
 		logger.Panicf("BUG: the getMetricIDsForTagFilterSlow must be called only for empty tf.orSuffixes; got %s", tf.orSuffixes)
 	}
@@ -2433,40 +2434,40 @@ func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, filter *uint6
 	mp.Reset()
 	var prevMatchingSuffix []byte
 	var prevMatch bool
-	loops := 0
+	var loopsCount uint64
 	loopsPaceLimiter := 0
 	prefix := tf.prefix
 	ts.Seek(prefix)
 	for ts.NextItem() {
 		if loopsPaceLimiter&paceLimiterMediumIterationsMask == 0 {
 			if err := checkSearchDeadlineAndPace(is.deadline); err != nil {
-				return err
+				return loopsCount, err
 			}
 		}
 		loopsPaceLimiter++
 		item := ts.Item
 		if !bytes.HasPrefix(item, prefix) {
-			return nil
+			return loopsCount, nil
 		}
 		tail := item[len(prefix):]
 		n := bytes.IndexByte(tail, tagSeparatorChar)
 		if n < 0 {
-			return fmt.Errorf("invalid tag->metricIDs line %q: cannot find tagSeparatorChar=%d", item, tagSeparatorChar)
+			return loopsCount, fmt.Errorf("invalid tag->metricIDs line %q: cannot find tagSeparatorChar=%d", item, tagSeparatorChar)
 		}
 		suffix := tail[:n+1]
 		tail = tail[n+1:]
 		if err := mp.InitOnlyTail(item, tail); err != nil {
-			return err
+			return loopsCount, err
 		}
 		mp.ParseMetricIDs()
+		loopsCount += uint64(mp.MetricIDsLen())
+		if loopsCount > maxLoopsCount {
+			return loopsCount, errFallbackToMetricNameMatch
+		}
 		if prevMatch && string(suffix) == string(prevMatchingSuffix) {
 			// Fast path: the same tag value found.
 			// There is no need in checking it again with potentially
 			// slow tf.matchSuffix, which may call regexp.
-			loops += mp.MetricIDsLen()
-			if loops > maxLoops {
-				return errFallbackToMetricNameMatch
-			}
 			for _, metricID := range mp.MetricIDs {
 				if filter != nil && !filter.Has(metricID) {
 					continue
@@ -2480,11 +2481,11 @@ func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, filter *uint6
 			// since the current row has no matching metricIDs.
 			continue
 		}
-
 		// Slow path: need tf.matchSuffix call.
 		ok, err := tf.matchSuffix(suffix)
+		loopsCount += reMatchCost
 		if err != nil {
-			return fmt.Errorf("error when matching %s against suffix %q: %w", tf, suffix, err)
+			return loopsCount, fmt.Errorf("error when matching %s against suffix %q: %w", tf, suffix, err)
 		}
 		if !ok {
 			prevMatch = false
@@ -2499,18 +2500,16 @@ func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, filter *uint6
 			// The last char in kb.B must be tagSeparatorChar. Just increment it
 			// in order to jump to the next tag value.
 			if len(kb.B) == 0 || kb.B[len(kb.B)-1] != tagSeparatorChar || tagSeparatorChar >= 0xff {
-				return fmt.Errorf("data corruption: the last char in k=%X must be %X", kb.B, tagSeparatorChar)
+				return loopsCount, fmt.Errorf("data corruption: the last char in k=%X must be %X", kb.B, tagSeparatorChar)
 			}
 			kb.B[len(kb.B)-1]++
 			ts.Seek(kb.B)
+			// Assume that a seek cost is equivalent to 100 ordinary loops.
+			loopsCount += 100
 			continue
 		}
 		prevMatch = true
 		prevMatchingSuffix = append(prevMatchingSuffix[:0], suffix...)
-		loops += mp.MetricIDsLen()
-		if loops > maxLoops {
-			return errFallbackToMetricNameMatch
-		}
 		for _, metricID := range mp.MetricIDs {
 			if filter != nil && !filter.Has(metricID) {
 				continue
@@ -2519,29 +2518,32 @@ func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, filter *uint6
 		}
 	}
 	if err := ts.Error(); err != nil {
-		return fmt.Errorf("error when searching for tag filter prefix %q: %w", prefix, err)
+		return loopsCount, fmt.Errorf("error when searching for tag filter prefix %q: %w", prefix, err)
 	}
-	return nil
+	return loopsCount, nil
 }
 
-func (is *indexSearch) updateMetricIDsForOrSuffixesNoFilter(tf *tagFilter, maxMetrics int, metricIDs *uint64set.Set) error {
+func (is *indexSearch) updateMetricIDsForOrSuffixesNoFilter(tf *tagFilter, maxMetrics int, metricIDs *uint64set.Set) (uint64, error) {
 	if tf.isNegative {
 		logger.Panicf("BUG: isNegative must be false")
 	}
 	kb := kbPool.Get()
 	defer kbPool.Put(kb)
+	var loopsCount uint64
 	for _, orSuffix := range tf.orSuffixes {
 		kb.B = append(kb.B[:0], tf.prefix...)
 		kb.B = append(kb.B, orSuffix...)
 		kb.B = append(kb.B, tagSeparatorChar)
-		if err := is.updateMetricIDsForOrSuffixNoFilter(kb.B, maxMetrics, metricIDs); err != nil {
-			return err
+		lc, err := is.updateMetricIDsForOrSuffixNoFilter(kb.B, maxMetrics, metricIDs)
+		if err != nil {
+			return loopsCount, err
 		}
+		loopsCount += lc
 		if metricIDs.Len() >= maxMetrics {
-			return nil
+			return loopsCount, nil
 		}
 	}
-	return nil
+	return loopsCount, nil
 }
 
 func (is *indexSearch) updateMetricIDsForOrSuffixesWithFilter(tf *tagFilter, metricIDs, filter *uint64set.Set) error {
@@ -2559,39 +2561,39 @@ func (is *indexSearch) updateMetricIDsForOrSuffixesWithFilter(tf *tagFilter, met
 	return nil
 }
 
-func (is *indexSearch) updateMetricIDsForOrSuffixNoFilter(prefix []byte, maxMetrics int, metricIDs *uint64set.Set) error {
+func (is *indexSearch) updateMetricIDsForOrSuffixNoFilter(prefix []byte, maxMetrics int, metricIDs *uint64set.Set) (uint64, error) {
 	ts := &is.ts
 	mp := &is.mp
 	mp.Reset()
-	maxLoops := maxMetrics * maxIndexScanLoopsPerMetric
-	loops := 0
+	maxLoopsCount := uint64(maxMetrics) * maxIndexScanLoopsPerMetric
+	var loopsCount uint64
 	loopsPaceLimiter := 0
 	ts.Seek(prefix)
 	for metricIDs.Len() < maxMetrics && ts.NextItem() {
 		if loopsPaceLimiter&paceLimiterFastIterationsMask == 0 {
 			if err := checkSearchDeadlineAndPace(is.deadline); err != nil {
-				return err
+				return loopsCount, err
 			}
 		}
 		loopsPaceLimiter++
 		item := ts.Item
 		if !bytes.HasPrefix(item, prefix) {
-			return nil
+			return loopsCount, nil
 		}
 		if err := mp.InitOnlyTail(item, item[len(prefix):]); err != nil {
-			return err
+			return loopsCount, err
 		}
-		loops += mp.MetricIDsLen()
-		if loops > maxLoops {
-			return errFallbackToMetricNameMatch
+		loopsCount += uint64(mp.MetricIDsLen())
+		if loopsCount > maxLoopsCount {
+			return loopsCount, errFallbackToMetricNameMatch
 		}
 		mp.ParseMetricIDs()
 		metricIDs.AddMulti(mp.MetricIDs)
 	}
 	if err := ts.Error(); err != nil {
-		return fmt.Errorf("error when searching for tag filter prefix %q: %w", prefix, err)
+		return loopsCount, fmt.Errorf("error when searching for tag filter prefix %q: %w", prefix, err)
 	}
-	return nil
+	return loopsCount, nil
 }
 
 func (is *indexSearch) updateMetricIDsForOrSuffixWithFilter(prefix []byte, metricIDs *uint64set.Set, sortedFilter []uint64, isNegative bool) error {
@@ -2603,8 +2605,8 @@ func (is *indexSearch) updateMetricIDsForOrSuffixWithFilter(prefix []byte, metri
 	ts := &is.ts
 	mp := &is.mp
 	mp.Reset()
-	maxLoops := len(sortedFilter) * maxIndexScanLoopsPerMetric
-	loops := 0
+	maxLoopsCount := uint64(len(sortedFilter)) * maxIndexScanLoopsPerMetric
+	var loopsCount uint64
 	loopsPaceLimiter := 0
 	ts.Seek(prefix)
 	var sf []uint64
@@ -2635,8 +2637,8 @@ func (is *indexSearch) updateMetricIDsForOrSuffixWithFilter(prefix []byte, metri
 			return nil
 		}
 		sf = sortedFilter
-		loops += mp.MetricIDsLen()
-		if loops > maxLoops {
+		loopsCount += uint64(mp.MetricIDsLen())
+		if loopsCount > maxLoopsCount {
 			return errFallbackToMetricNameMatch
 		}
 		mp.ParseMetricIDs()
@@ -2806,31 +2808,43 @@ func (is *indexSearch) tryUpdatingMetricIDsForDateRange(metricIDs *uint64set.Set
 }
 
 func (is *indexSearch) getMetricIDsForDateAndFilters(date uint64, tfs *TagFilters, maxMetrics int) (*uint64set.Set, error) {
-	// Sort tfs by the duration from previous queries.
+	// Sort tfs by loopsCount needed for performing each filter.
+	// This stats is usually collected from the previous queries.
 	// This way we limit the amount of work below by applying fast filters at first.
 	type tagFilterWithWeight struct {
 		tf                 *tagFilter
-		durationSeconds    float64
+		loopsCount         uint64
 		lastQueryTimestamp uint64
 	}
 	tfws := make([]tagFilterWithWeight, len(tfs.tfs))
+	currentTime := fasttime.UnixTimestamp()
 	for i := range tfs.tfs {
 		tf := &tfs.tfs[i]
-		durationSeconds, lastQueryTimestamp := is.getDurationAndTimestampForDateFilter(date, tf)
-		if durationSeconds == 0 {
-			// Assume that unknown tag filters can take quite big amounts of time.
-			durationSeconds = 1.0
+		loopsCount, lastQueryTimestamp := is.getLoopsCountAndTimestampForDateFilter(date, tf)
+		origLoopsCount := loopsCount
+		if currentTime > lastQueryTimestamp+60*60 {
+			// Reset loopsCount to 0 every hour for collecting updated stats for the tf.
+			loopsCount = 0
+		}
+		if loopsCount == 0 {
+			// Prevent from possible thundering herd issue when heavy tf is executed from multiple concurrent queries
+			// by temporary persisting its position in the tag filters list.
+			if origLoopsCount == 0 {
+				origLoopsCount = 10e6
+			}
+			lastQueryTimestamp = 0
+			is.storeLoopsCountForDateFilter(date, tf, origLoopsCount, lastQueryTimestamp)
 		}
 		tfws[i] = tagFilterWithWeight{
 			tf:                 tf,
-			durationSeconds:    durationSeconds,
+			loopsCount:         loopsCount,
 			lastQueryTimestamp: lastQueryTimestamp,
 		}
 	}
 	sort.Slice(tfws, func(i, j int) bool {
 		a, b := &tfws[i], &tfws[j]
-		if a.durationSeconds != b.durationSeconds {
-			return a.durationSeconds < b.durationSeconds
+		if a.loopsCount != b.loopsCount {
+			return a.loopsCount < b.loopsCount
 		}
 		return a.tf.Less(b.tf)
 	})
@@ -2847,7 +2861,8 @@ func (is *indexSearch) getMetricIDsForDateAndFilters(date uint64, tfs *TagFilter
 			tfwsRemaining = append(tfwsRemaining, tfw)
 			continue
 		}
-		m, err := is.getMetricIDsForDateTagFilter(tf, tfw.lastQueryTimestamp, date, nil, tfs.commonPrefix, maxDateMetrics)
+		m, loopsCount, err := is.getMetricIDsForDateTagFilter(tf, date, nil, tfs.commonPrefix, maxDateMetrics)
+		is.storeLoopsCountForDateFilter(date, tf, loopsCount, tfw.lastQueryTimestamp)
 		if err != nil {
 			return nil, err
 		}
@@ -2897,13 +2912,16 @@ func (is *indexSearch) getMetricIDsForDateAndFilters(date uint64, tfs *TagFilter
 			// Short circuit - there is no need in applying the remaining filters to an empty set.
 			break
 		}
-		if float64(metricIDsLen)/metricNameMatchesPerSecond < tfw.durationSeconds {
+		if uint64(metricIDsLen)*maxIndexScanLoopsPerMetric < tfw.loopsCount {
 			// It should be faster performing metricName match on the remaining filters
 			// instead of scanning big number of entries in the inverted index for these filters.
 			tfsPostponed = append(tfsPostponed, tf)
+			// Store stats for non-executed tf, since it could be updated during protection from thundered herd.
+			is.storeLoopsCountForDateFilter(date, tf, tfw.loopsCount, tfw.lastQueryTimestamp)
 			continue
 		}
-		m, err := is.getMetricIDsForDateTagFilter(tf, tfw.lastQueryTimestamp, date, metricIDs, tfs.commonPrefix, maxDateMetrics)
+		m, loopsCount, err := is.getMetricIDsForDateTagFilter(tf, date, metricIDs, tfs.commonPrefix, maxDateMetrics)
+		is.storeLoopsCountForDateFilter(date, tf, loopsCount, tfw.lastQueryTimestamp)
 		if err != nil {
 			return nil, err
 		}
@@ -2933,12 +2951,6 @@ func (is *indexSearch) getMetricIDsForDateAndFilters(date uint64, tfs *TagFilter
 	return metricIDs, nil
 }
 
-// The estimated number of per-second loops inside updateMetricIDsByMetricNameMatch
-//
-// This value is used for determining when matching by metric name must be perfromed instead of matching
-// by the remaining tag filters.
-const metricNameMatchesPerSecond = 50000
-
 func (is *indexSearch) storeDateMetricID(date, metricID uint64) error {
 	ii := getIndexItems()
 	defer putIndexItems(ii)
@@ -3085,8 +3097,7 @@ func (is *indexSearch) hasDateMetricID(date, metricID uint64) (bool, error) {
 	return true, nil
 }
 
-func (is *indexSearch) getMetricIDsForDateTagFilter(tf *tagFilter, lastQueryTimestamp, date uint64,
-	filter *uint64set.Set, commonPrefix []byte, maxMetrics int) (*uint64set.Set, error) {
+func (is *indexSearch) getMetricIDsForDateTagFilter(tf *tagFilter, date uint64, filter *uint64set.Set, commonPrefix []byte, maxMetrics int) (*uint64set.Set, uint64, error) {
 	// Augument tag filter prefix for per-date search instead of global search.
 	if !bytes.HasPrefix(tf.prefix, commonPrefix) {
 		logger.Panicf("BUG: unexpected tf.prefix %q; must start with commonPrefix %q", tf.prefix, commonPrefix)
@@ -3098,32 +3109,21 @@ func (is *indexSearch) getMetricIDsForDateTagFilter(tf *tagFilter, lastQueryTime
 	tfNew := *tf
 	tfNew.isNegative = false // isNegative for the original tf is handled by the caller.
 	tfNew.prefix = kb.B
-	startTime := time.Now()
-	metricIDs, err := is.getMetricIDsForTagFilter(&tfNew, filter, maxMetrics)
+	metricIDs, loopsCount, err := is.getMetricIDsForTagFilter(&tfNew, filter, maxMetrics)
 	kbPool.Put(kb)
-	currentTimestamp := fasttime.UnixTimestamp()
-	if currentTimestamp > lastQueryTimestamp+5 {
-		// The cache already contains quite fresh entry for the current (date, tf).
-		// Do not update it too frequently.
-		return metricIDs, err
-	}
-	// Store the duration for tag filter execution in the cache in order to sort tag filters
-	// in ascending durations on the next search.
-	durationSeconds := time.Since(startTime).Seconds()
-	if err != nil && durationSeconds < 10 {
-		// Set high duration for failing filter, so it is moved to the end of filter list.
-		durationSeconds = 10
+	if err != nil {
+		// Set high loopsCount for failing filter, so it is moved to the end of filter list.
+		loopsCount = 1e9
 	}
 	if metricIDs.Len() >= maxMetrics {
-		// Increase the duration for tag filter matching too many metrics,
-		// So next time it will be applied after filters matching lower number of metrics.
-		durationSeconds *= 2
+		// Increase loopsCount for tag filter matching too many metrics,
+		// So next time it is moved to the end of filter list.
+		loopsCount *= 2
 	}
-	is.storeDurationAndTimestampForDateFilter(date, tf, durationSeconds, currentTimestamp)
-	return metricIDs, err
+	return metricIDs, loopsCount, err
 }
 
-func (is *indexSearch) getDurationAndTimestampForDateFilter(date uint64, tf *tagFilter) (float64, uint64) {
+func (is *indexSearch) getLoopsCountAndTimestampForDateFilter(date uint64, tf *tagFilter) (uint64, uint64) {
 	is.kb.B = appendDateTagFilterCacheKey(is.kb.B[:0], date, tf, is.accountID, is.projectID)
 	kb := kbPool.Get()
 	defer kbPool.Put(kb)
@@ -3131,18 +3131,22 @@ func (is *indexSearch) getDurationAndTimestampForDateFilter(date uint64, tf *tag
 	if len(kb.B) != 16 {
 		return 0, 0
 	}
-	n := encoding.UnmarshalUint64(kb.B)
-	durationSeconds := math.Float64frombits(n)
+	loopsCount := encoding.UnmarshalUint64(kb.B)
 	timestamp := encoding.UnmarshalUint64(kb.B[8:])
-	return durationSeconds, timestamp
+	return loopsCount, timestamp
 }
 
-func (is *indexSearch) storeDurationAndTimestampForDateFilter(date uint64, tf *tagFilter, durationSeconds float64, timestamp uint64) {
+func (is *indexSearch) storeLoopsCountForDateFilter(date uint64, tf *tagFilter, loopsCount, prevTimestamp uint64) {
+	currentTimestamp := fasttime.UnixTimestamp()
+	if currentTimestamp < prevTimestamp+5 {
+		// The cache already contains quite fresh entry for the current (date, tf).
+		// Do not update it too frequently.
+		return
+	}
 	is.kb.B = appendDateTagFilterCacheKey(is.kb.B[:0], date, tf, is.accountID, is.projectID)
-	n := math.Float64bits(durationSeconds)
 	kb := kbPool.Get()
-	kb.B = encoding.MarshalUint64(kb.B[:0], n)
-	kb.B = encoding.MarshalUint64(kb.B, timestamp)
+	kb.B = encoding.MarshalUint64(kb.B[:0], loopsCount)
+	kb.B = encoding.MarshalUint64(kb.B, currentTimestamp)
 	is.db.durationsPerDateTagFilterCache.Set(is.kb.B, kb.B)
 	kbPool.Put(kb)
 }
@@ -3268,8 +3272,8 @@ func (is *indexSearch) intersectMetricIDsWithTagFilterNocache(tf *tagFilter, fil
 	}
 
 	// Slow path - scan for all the rows with the given prefix.
-	maxLoops := filter.Len() * maxIndexScanSlowLoopsPerMetric
-	err := is.getMetricIDsForTagFilterSlow(tf, filter, maxLoops, func(metricID uint64) {
+	maxLoopsCount := uint64(filter.Len()) * maxIndexScanSlowLoopsPerMetric
+	_, err := is.getMetricIDsForTagFilterSlow(tf, filter, maxLoopsCount, func(metricID uint64) {
 		if tf.isNegative {
 			// filter must be equal to metricIDs
 			metricIDs.Del(metricID)
diff --git a/lib/storage/table.go b/lib/storage/table.go
index 646d33074..5049a18e0 100644
--- a/lib/storage/table.go
+++ b/lib/storage/table.go
@@ -203,7 +203,7 @@ func (tb *table) MustClose() {
 		if n := atomic.LoadUint64(&ptw.refCount); n != 1 {
 			logger.Panicf("BUG: unexpected refCount=%d when closing the partition; probably there are pending searches", n)
 		}
-		ptw.pt.MustClose()
+		ptw.decRef()
 	}
 
 	// Release exclusive lock on the table.
diff --git a/lib/storage/tag_filters.go b/lib/storage/tag_filters.go
index 5f1079a07..90ee88a52 100644
--- a/lib/storage/tag_filters.go
+++ b/lib/storage/tag_filters.go
@@ -29,14 +29,15 @@ func convertToCompositeTagFilterss(tfss []*TagFilters) []*TagFilters {
 func convertToCompositeTagFilters(tfs *TagFilters) *TagFilters {
 	// Search for metric name filter, which must be used for creating composite filters.
 	var name []byte
+	hasPositiveFilter := false
 	for _, tf := range tfs.tfs {
 		if len(tf.key) == 0 && !tf.isNegative && !tf.isRegexp {
 			name = tf.value
-			break
+		} else if !tf.isNegative {
+			hasPositiveFilter = true
 		}
 	}
 	if len(name) == 0 {
-		// There is no metric name filter, so composite filters cannot be created.
 		atomic.AddUint64(&compositeFilterMissingConversions, 1)
 		return tfs
 	}
@@ -45,7 +46,7 @@ func convertToCompositeTagFilters(tfs *TagFilters) *TagFilters {
 	compositeFilters := 0
 	for _, tf := range tfs.tfs {
 		if len(tf.key) == 0 {
-			if tf.isNegative || tf.isRegexp || string(tf.value) != string(name) {
+			if !hasPositiveFilter || tf.isNegative || tf.isRegexp || string(tf.value) != string(name) {
 				tfsNew = append(tfsNew, tf)
 			}
 			continue
@@ -233,7 +234,9 @@ type tagFilter struct {
 	value      []byte
 	isNegative bool
 	isRegexp   bool
-	matchCost  uint64
+
+	// matchCost is a cost for matching a filter against a single string.
+	matchCost uint64
 
 	// Prefix always contains {nsPrefixTagToMetricIDs, AccountID, ProjectID, key}.
 	// Additionally it contains:


Test patch:
diff --git a/app/victoria-metrics/testdata/graphite/graphite-selector.json b/app/victoria-metrics/testdata/graphite/graphite-selector.json
deleted file mode 100644
index 5ab1c3ce9..000000000
--- a/app/victoria-metrics/testdata/graphite/graphite-selector.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "name": "graphite-selector",
-  "issue": "",
-  "data": [
-    "graphite-selector.bar.baz 1 {TIME_S-1m}",
-    "graphite-selector.xxx.yy  2 {TIME_S-1m}",
-    "graphite-selector.bb.cc 3 {TIME_S-1m}",
-    "graphite-selector.a.baz 4 {TIME_S-1m}"],
-  "query": ["/api/v1/query?query=sort({__graphite__='graphite-selector.*.baz'})&time={TIME_S-1m}"],
-  "result_query": {
-    "status":"success",
-    "data":{"resultType":"vector","result":[
-	    {"metric":{"__name__":"graphite-selector.bar.baz"},"value":["{TIME_S-1m}","1"]},
-	    {"metric":{"__name__":"graphite-selector.a.baz"},"value":["{TIME_S-1m}","4"]}
-    ]}
-  }
-}
diff --git a/app/victoria-metrics/testdata/prometheus/with_request_extra_filter.json b/app/victoria-metrics/testdata/prometheus/with_request_extra_filter.json
deleted file mode 100644
index 5bfbece19..000000000
--- a/app/victoria-metrics/testdata/prometheus/with_request_extra_filter.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "name": "basic_select_with_extra_labels",
-  "data": ["[{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.tenant.limits\"},{\"name\":\"baz\",\"value\":\"qux\"},{\"name\":\"tenant\",\"value\":\"dev\"}],\"samples\":[{\"value\":100000,\"timestamp\":\"{TIME_MS}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.up\"},{\"name\":\"baz\",\"value\":\"qux\"}],\"samples\":[{\"value\":100000,\"timestamp\":\"{TIME_MS}\"}]}]"],
-  "query": ["/api/v1/export?match={__name__!=''}&extra_label=tenant=dev"],
-  "result_metrics": [
-    {"metric":{"__name__":"prometheus.tenant.limits","baz":"qux","tenant": "dev"},"values":[100000], "timestamps": ["{TIME_MS}"]}
-  ]
-}
diff --git a/lib/promscrape/config_test.go b/lib/promscrape/config_test.go
index 9982efb1a..a1dbc5f57 100644
--- a/lib/promscrape/config_test.go
+++ b/lib/promscrape/config_test.go
@@ -1275,6 +1275,7 @@ scrape_configs:
     disable_keepalive: true
     disable_compression: true
     stream_parse: true
+    scrape_align_interval: 1s
     static_configs:
       - targets:
         - 192.168.1.2  # SNMP device.
@@ -1323,12 +1324,13 @@ scrape_configs:
 					Value: "snmp",
 				},
 			},
-			AuthConfig:         &promauth.Config{},
-			SampleLimit:        100,
-			DisableKeepAlive:   true,
-			DisableCompression: true,
-			StreamParse:        true,
-			jobNameOriginal:    "snmp",
+			AuthConfig:          &promauth.Config{},
+			SampleLimit:         100,
+			DisableKeepAlive:    true,
+			DisableCompression:  true,
+			StreamParse:         true,
+			ScrapeAlignInterval: time.Second,
+			jobNameOriginal:     "snmp",
 		},
 	})
 	f(`
diff --git a/lib/storage/tag_filters_test.go b/lib/storage/tag_filters_test.go
index 80b3f4eee..7f3f0ba9b 100644
--- a/lib/storage/tag_filters_test.go
+++ b/lib/storage/tag_filters_test.go
@@ -156,6 +156,70 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		},
 	})
 
+	// A name filter with a single negative filter
+	f([]TagFilter{
+		{
+			Key:        nil,
+			Value:      []byte("bar"),
+			IsNegative: false,
+			IsRegexp:   false,
+		},
+		{
+			Key:        []byte("foo"),
+			Value:      []byte("abc"),
+			IsNegative: true,
+			IsRegexp:   false,
+		},
+	}, []TagFilter{
+		{
+			Key:        nil,
+			Value:      []byte("bar"),
+			IsNegative: false,
+			IsRegexp:   false,
+		},
+		{
+			Key:        []byte("\xfe\x03barfoo"),
+			Value:      []byte("abc"),
+			IsNegative: true,
+			IsRegexp:   false,
+		},
+	})
+
+	// A name filter with a negative and a positive filter
+	f([]TagFilter{
+		{
+			Key:        nil,
+			Value:      []byte("bar"),
+			IsNegative: false,
+			IsRegexp:   false,
+		},
+		{
+			Key:        []byte("foo"),
+			Value:      []byte("abc"),
+			IsNegative: true,
+			IsRegexp:   false,
+		},
+		{
+			Key:        []byte("a"),
+			Value:      []byte("b.+"),
+			IsNegative: false,
+			IsRegexp:   true,
+		},
+	}, []TagFilter{
+		{
+			Key:        []byte("\xfe\x03barfoo"),
+			Value:      []byte("abc"),
+			IsNegative: true,
+			IsRegexp:   false,
+		},
+		{
+			Key:        []byte("\xfe\x03bara"),
+			Value:      []byte("b.+"),
+			IsNegative: false,
+			IsRegexp:   true,
+		},
+	})
+
 	// Two name filters with non-name filter.
 	f([]TagFilter{
 		{
@@ -179,19 +243,19 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 	}, []TagFilter{
 		{
 			Key:        nil,
-			Value:      []byte("baz"),
+			Value:      []byte("bar"),
 			IsNegative: false,
 			IsRegexp:   false,
 		},
 		{
-			Key:        []byte("\xfe\x03barfoo"),
+			Key:        []byte("\xfe\x03bazfoo"),
 			Value:      []byte("abc"),
 			IsNegative: false,
 			IsRegexp:   false,
 		},
 	})
 
-	// A name filter with negative regexp non-name filter, which can be converted to non-regexp.
+	// A name filter with regexp non-name filter, which can be converted to non-regexp.
 	f([]TagFilter{
 		{
 			Key:        nil,
@@ -202,19 +266,19 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		{
 			Key:        []byte("foo"),
 			Value:      []byte("abc"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   true,
 		},
 	}, []TagFilter{
 		{
 			Key:        []byte("\xfe\x03barfoo"),
 			Value:      []byte("abc"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   false,
 		},
 	})
 
-	// A name filter with negative regexp non-name filter.
+	// A name filter with regexp non-name filter.
 	f([]TagFilter{
 		{
 			Key:        nil,
@@ -225,14 +289,14 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		{
 			Key:        []byte("foo"),
 			Value:      []byte("abc.+"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   true,
 		},
 	}, []TagFilter{
 		{
 			Key:        []byte("\xfe\x03barfoo"),
 			Value:      []byte("abc.+"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   true,
 		},
 	})
@@ -277,7 +341,7 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		{
 			Key:        []byte("foo"),
 			Value:      []byte("abc"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   true,
 		},
 		{
@@ -290,7 +354,7 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		{
 			Key:        []byte("\xfe\x03barfoo"),
 			Value:      []byte("abc"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   false,
 		},
 		{
@@ -312,14 +376,14 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		{
 			Key:        []byte("foo"),
 			Value:      []byte("abc"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   false,
 		},
 	}, []TagFilter{
 		{
 			Key:        []byte("\xfe\x03barfoo"),
 			Value:      []byte("abc"),
-			IsNegative: true,
+			IsNegative: false,
 			IsRegexp:   false,
 		},
 	})