Compare commits

...

141 Commits

Author SHA1 Message Date
Aliaksandr Valialkin
844ce4731e app/vmselect/promql: suppress error when template func is used inside modifier list. Just leave it as is
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/78
2019-06-25 20:43:22 +03:00
Aliaksandr Valialkin
683bf2a11f lib/storage: make sure non-nil args are passed to openIndexDB 2019-06-25 20:10:04 +03:00
Aliaksandr Valialkin
eb2283a029 lib/storage: reduce too big maxMetrics in getTagFilterWithMinMetricIDsCountAdaptive
This should improve performance on inverted index search for big amount of unique time series
when big -search.maxUniqueTimeseries is set.
2019-06-25 19:55:27 +03:00
Aliaksandr Valialkin
e8377011ab lib/storage: free up memory from caches owned by indexDB when it is deleted 2019-06-25 14:42:44 +03:00
Aliaksandr Valialkin
33ea2120c3 lib/storage: use unversioned keys for tag cache in extDB
Data in ExtDB cannot be changed, so it is OK to use unversioned keys for tag cache.
This should improve performance for index lookups over big amount of time series.
2019-06-25 13:08:58 +03:00
Aliaksandr Valialkin
cf63669303 lib/storage: skip searching in extDB if it doesn't contain items for the given time range
This should improve inverted index search performance for big amount
of unique time series when the search is performed only on recent data.
2019-06-25 13:00:37 +03:00
Aliaksandr Valialkin
feacfffe89 app/vmselect/promql: increase default value for -search.maxPointsPerTimeSeries from 10k to 30k
This may be required for subqueries with small steps. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/77
2019-06-24 22:53:18 +03:00
Aliaksandr Valialkin
4bb738ddd9 app/vmselect/promql: adjust value returned by linearRegression to the end of time range like Prometheus does
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/71
2019-06-24 22:45:58 +03:00
Aliaksandr Valialkin
90e72c2a42 app/vmselect/promql: add sum2 and sum2_over_time, geomean and geomean_over_time funcs.
These functions may be useful for statistic calculations.
2019-06-24 16:44:44 +03:00
Aliaksandr Valialkin
ccd8b7a003 README.md: mention how to recover from broken parts due to disk errors
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/76
2019-06-24 14:17:58 +03:00
Aliaksandr Valialkin
d32845781e README.md: remove unused TOC items 2019-06-24 14:12:07 +03:00
Aliaksandr Valialkin
af2ceaaa0b lib/storage: mention source parts on merge error
This should improve determining broken source part.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/76
2019-06-24 14:08:43 +03:00
Aliaksandr Valialkin
61926bae01 app/vmselect/promql: adjust the provided window only for range functions with dt in denominator
This should fix range function calculations such as `changes(m[d])` where `d` is smaller
than the scrape interval.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/72
2019-06-23 19:27:31 +03:00
Aliaksandr Valialkin
ee13256f74 app/vmselect/promql: use deriv_fast instead of deriv in ttf, since deriv calculations have been changed recently 2019-06-23 15:54:18 +03:00
Aliaksandr Valialkin
3b3b2f1e6e app/vmselect/promql: adjust ttf calculation, so deriv(freev) for freev=m[d] could be properly calculated 2019-06-23 14:31:19 +03:00
Aliaksandr Valialkin
c9cbf5351c vendor: update github.com/valyala/gozstd to v1.5.1 2019-06-22 00:14:19 +03:00
Aliaksandr Valialkin
146c6e1f72 app/vmselect/promql: typo fixes in comments 2019-06-21 23:22:59 +03:00
Aliaksandr Valialkin
d261fa2885 app/vmselect/promql: add deriv_fast function for calculating fast derivative
`deriv_fast` calculates derivative based on the first and the last point on the interval
instead of calculating linear regression based on all the data points on the interval.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/73
2019-06-21 23:05:39 +03:00
Aliaksandr Valialkin
5b47c00910 app/vmselect/promql: use linear regression in deriv func like Prometheus does
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/73
2019-06-21 22:59:46 +03:00
Aliaksandr Valialkin
9e1119dab8 app/vmselect/promql: ajdust data model to the model used in Prometheus
Do not take into account data points on the range `[timestamp .. timestamp+step)`
when calculating value on the given `timestamp`.
Use only data points from the past when performing these calculations like Prometheus does.

This should reduce discrepancies between results returned by VictoriaMetrics
and results returned by Prometheus.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/72
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/71
2019-06-21 21:54:48 +03:00
Aliaksandr Valialkin
47a3228108 app/vmselect/promql: do not strip __name__ form time series after binary comparison operation
Example:

  foo > 10

Would leave `foo` name for all the matching time series on the left.
2019-06-21 13:09:38 +03:00
Aliaksandr Valialkin
e88a03323a all: initial stubs for Windows support; see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/70 2019-06-20 20:07:10 +03:00
Aliaksandr Valialkin
b75630fcf4 Makefile: enable golangci-lint in make check_all 2019-06-20 14:52:58 +03:00
Aliaksandr Valialkin
80db24386e lib/storage: typo fixes found by golangci-lint; updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/69 2019-06-20 14:37:55 +03:00
Aliaksandr Valialkin
296c14317f lib/netutil: remove unused TCPListener.name; updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/69 2019-06-20 14:36:15 +03:00
Aliaksandr Valialkin
973e4b5b76 app/vmselect/promql: remove unused func keepLastValue; updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/69 2019-06-20 14:35:11 +03:00
Aliaksandr Valialkin
7aadec8e3c app/vmselect/promql: typo fix; updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/69 2019-06-20 14:33:47 +03:00
Aliaksandr Valialkin
45fc8cb72f Makefile: add make golangci-lint rule for running golangci-lint run; updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/69 2019-06-20 14:30:55 +03:00
Aliaksandr Valialkin
4b2523fb40 app/vminsert/opentsdb: remove unused const maxReadPacketSize; update https://github.com/VictoriaMetrics/VictoriaMetrics/issues/69 2019-06-20 14:30:06 +03:00
Aliaksandr Valialkin
70ba36fa37 app/vmselect/prometheus: return better error messages on missing args to /api/v1/* 2019-06-20 14:07:55 +03:00
Aliaksandr Valialkin
a78b3dba7f app/vmstorage: add vm_cache_entries{type="storage/hour_metric_ids"} metric for tracking active time series count 2019-06-19 18:36:47 +03:00
Aliaksandr Valialkin
a9cfca6a72 README.md: add max_shards: 100 to the recommended Prometheus config
Prometheus establishes a connection per shard in remote_write config.
By default it establishes up to 1000 connections to remote storage (max_shards: 1000).
This is quite big, so set `max_shards: 100` in the recommmended Prometheus config.
2019-06-19 17:48:09 +03:00
Aliaksandr Valialkin
710d6c33ea lib/prompb: remove superflouos bytes copying in ReadSnappy 2019-06-18 20:37:51 +03:00
Aliaksandr Valialkin
a8d4224828 app/vminsert/graphite: allow skipping timestamps in Graphite plaintext protocol
In this case VictoriaMetrics uses the ingestion time as a timestamp.
2019-06-18 19:04:04 +03:00
Aliaksandr Valialkin
341bed4822 README.md: mention that arbitrary number of lines may be sent in a single request via supported ingestion protocols 2019-06-18 18:59:12 +03:00
Aliaksandr Valialkin
5982e94c94 vendor: update golang.org/x/sys 2019-06-18 16:19:26 +03:00
Aliaksandr Valialkin
6d6c9eb1f8 lib/flagutil: remove unused package 2019-06-18 10:43:55 +03:00
Aliaksandr Valialkin
86d3d907a5 app/vminsert/influx: add -influxSkipSingleField flag for using {measurement} instead of {measurement}{separator}{field_name} for Influx lines with a single field
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/66
2019-06-17 19:05:57 +03:00
Aliaksandr Valialkin
269285848f app/vminsert/influx: add -influxMeasurementFieldSeparator flag for the ability to change separator for {measurement}{separator}{field_name} metric name 2019-06-14 10:00:12 +03:00
Aliaksandr Valialkin
47e1e5eb4b deployment/docker: switch builder from go1.12.5 to go1.12.6 2019-06-14 09:32:06 +03:00
Aliaksandr Valialkin
d2c801029b lib/storage: persist metric ids for the current and the previous hour on graceful shutdown
This should improve performance after restart when the db contains a lot of time series
with high time series churn (i.e. metrics from Kubernetes with many pods and frequent deployments)
2019-06-14 07:55:14 +03:00
Aliaksandr Valialkin
beb479b8f1 app/vmselect/promql: use dynamic limit on memory for concurrent queries 2019-06-12 23:18:44 +03:00
Aliaksandr Valialkin
611c4401f8 README.md: mention about multi-tenancy 2019-06-12 21:30:36 +03:00
Aliaksandr Valialkin
a8db528930 app/vmselect/promql: merge non-overlapping duplicate time series in group_left and group_right joins 2019-06-12 20:32:32 +03:00
Aliaksandr Valialkin
15613e5338 app/vmselect/promql: swap binary operation with modifier in the error message for improved readability 2019-06-12 17:14:39 +03:00
Aliaksandr Valialkin
3237d0309c app/vmselect/promql: list a sample of duplicate time series in the error message for group_left or group_right
This should improve troubleshooting for complex queries involving `group_left` and `group_right` modifiers.
2019-06-12 16:57:37 +03:00
Aliaksandr Valialkin
26f8d7ea1b lib/fs: sync parent dir in MustRemoveAll only if it exists
The parent directory may be non-existing when the deleted directory
didn't exist before the MustRemoveAll call
2019-06-12 02:14:44 +03:00
Aliaksandr Valialkin
419197ba08 lib/fs: consolidate *RemoveAll* funcs into a single MustRemoveAll func
The func syncs parent dir in order to persist directory removal
in the event of power loss
2019-06-12 01:53:46 +03:00
Aliaksandr Valialkin
a4b4db9bf6 README.md: add a chapter about downsampling 2019-06-12 01:32:26 +03:00
Aliaksandr Valialkin
c1276edab5 lib/fs: panic with fatal error when directories cannot be removed
Unremoved directories may lead to inconsistent data directory,
so VictoriaMetrics will fail to start next time.

So panic on the first error when trying to remove directory in order
to simplify recover process.
2019-06-12 01:20:54 +03:00
Aliaksandr Valialkin
2322c9a45a lib/fs: attempt #2 to work around NFS issue with directory removal
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/61
2019-06-12 01:07:05 +03:00
Aliaksandr Valialkin
89b928ff24 vendor: update github.com/VictoriaMetrics/fastcache to v1.5.1 2019-06-11 23:56:08 +03:00
Aliaksandr Valialkin
935bfd7a18 lib/fs: consistency renaming SyncPath -> MustSyncPath, since it doesnt return error 2019-06-11 23:13:49 +03:00
Aliaksandr Valialkin
3dd36b8088 lib/fs: make sure the created directory remains visible in the fs in the event of power loss
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/63
2019-06-11 23:08:09 +03:00
Aliaksandr Valialkin
afb964670a lib/fs: use filepath.Dir instead of filepath.Split, since the filename is unused 2019-06-11 22:54:26 +03:00
Aliaksandr Valialkin
20fc0e0e54 lib/{storage,mergeset}: sync filenames inside part when finalizing the part
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/63
2019-06-11 21:51:13 +03:00
Aliaksandr Valialkin
4d9f088526 README.md: add examples on how to write data with Graphite and OpenTSDB protocols 2019-06-11 21:24:32 +03:00
Aliaksandr Valialkin
82d1707861 README.md: add missing port to example urls 2019-06-11 21:05:24 +03:00
Aliaksandr Valialkin
70d20ce8de README.md: use proper urls for single-node version in examples 2019-06-11 20:33:52 +03:00
Aliaksandr Valialkin
723bf1af7f README.md: add example on how to write data with Influx line protocol to VictoriaMetrics 2019-06-11 20:31:25 +03:00
Aliaksandr Valialkin
ac7b186f13 all: try hard removing directory with contents
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/61
2019-06-11 01:57:59 +03:00
Roman Khavronenko
cd1bc32158 convert dashboard for provisioning (#62) 2019-06-11 01:07:09 +03:00
Aliaksandr Valialkin
1c33b5937e app/vmselect/promql: prevent from count_values explosion of timeseries, which could result in OOM 2019-06-11 01:03:13 +03:00
Aliaksandr Valialkin
8bb6bc986d app/vmselect/promql: skip superflouos timestamps copying in count_values 2019-06-11 00:44:01 +03:00
Aliaksandr Valialkin
d2be567482 app/vmselect/promql: remove superflouos timeseries copy in histogram_quantile func 2019-06-11 00:39:41 +03:00
Aliaksandr Valialkin
7e7d4d5275 app/vmselect/promql: remove superflouos timeseries copy in union func 2019-06-11 00:35:20 +03:00
Aliaksandr Valialkin
bf9782eaf6 app/vmselect/promql: skip NaN values in count_values func 2019-06-10 22:42:32 +03:00
Aliaksandr Valialkin
cbe692f0e2 app/vmselect: add /api/v1/labels/count handler for quick detection of labels with the maximum number of distinct values 2019-06-10 19:55:38 +03:00
Aliaksandr Valialkin
7b6623558f lib/storage: skip adaptive searching for tag filter matching the minimum number of metrics if the identical previous search didn't found such filter
This should improve speed for searching metrics among high number of time series
with high churn rate like in big Kubernetes clusters with frequent deployments.
2019-06-10 14:07:39 +03:00
Aliaksandr Valialkin
a1351bbaee lib/storage: factor out getTagFilterWithMinMetricIDsCountAdaptive from updateMetricIDsForTagFilters 2019-06-10 13:26:44 +03:00
Aliaksandr Valialkin
b4d707d9bb lib/storage: give clearer names to more functions 2019-06-10 13:01:23 +03:00
Aliaksandr Valialkin
bee7298f81 lib/storage: give more clear names to functions 2019-06-10 12:50:44 +03:00
Aliaksandr Valialkin
dbd217b8f0 lib/storage: test GetSeriesCount 2019-06-10 12:43:34 +03:00
Aliaksandr Valialkin
4d936b1524 lib/storage: make getSeriesCount func indexSearch method 2019-06-10 12:29:11 +03:00
Aliaksandr Valialkin
7354090aad app/vmstorage: add missing _total suffixes to newly added metrics 2019-06-09 22:11:36 +03:00
Aliaksandr Valialkin
d37924900b lib/storage: optimize time series lookup for recent hours when the db contains many millions of time series with high churn rate (aka frequent deployments in Kubernetes) 2019-06-09 19:13:56 +03:00
Aliaksandr Valialkin
c0baa977cf app/vminsert/concurrencylimiter: typo fix in the error message 2019-06-08 22:43:33 +03:00
Aliaksandr Valialkin
f4252f87e6 app/vminsert: really fix #60
ReadLinesBlock may accept dstBuf with non-zero length. In this case the last line without trailing newline isn't read.
Fix this by comparing len(dstBuf) to 0 instead of its original length.
2019-06-07 23:37:03 +03:00
Aliaksandr Valialkin
0b78d228d2 app/vminsert: properly read trailing line without newline in the end
This fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/60
2019-06-07 23:17:59 +03:00
Aliaksandr Valialkin
0371c216a7 deployment/docker: move victoriametrics single-node docker image from valyala/victoria-metrics to victoriametrics/victoria-metrics docker hub path 2019-06-07 11:52:53 +03:00
Aliaksandr Valialkin
c1f18ee48d app/vmselect/promql: properly handle {__name__ op "string"} queries
This has been broken in 7294ef333ad26f4f6578b783e97649e58b1f8945 .
2019-06-07 02:02:04 +03:00
Roman Khavronenko
fbd7044b2b Dashboard update (#57)
* split "pending datapoints" by storage and index pending entities

* update provisioned dVM dashboard
2019-06-07 01:31:45 +03:00
Roman Khavronenko
2afe511d80 Setup Grafana provisioning for docker-compose setup (#50)
* setup Grafana provisioning for docker-compose setup

* review fixes
2019-06-06 23:37:44 +03:00
Seua Polyakov
f4e63cd070 Add SIGINT as stopsignal to docker file (#54)
Add sigint as stopsignal to docker file. You can find more here: https://docs.docker.com/engine/reference/builder/#usage
With this change, the main process inside the container will receive SIGINT, and after a grace period, SIGKILL.
2019-06-06 22:36:21 +03:00
Aliaksandr Valialkin
667115a5c7 app/vmselect/prometheus: report about incorrect time or duration instead of silently using the default value
This should prevent from incorrect usage of the querying API.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/52
2019-06-06 22:18:18 +03:00
Aliaksandr Valialkin
1458450dba app/vmselect/promql: return the correct time series from quantile
Previously arbitrary time series could be returned from `quantile`
depending on sort order for the last data point in the selected range.

Fix this by returning the calculated time series.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/55
2019-06-06 17:07:31 +03:00
Aliaksandr Valialkin
5a5ba749f2 README.md: add an example on how Influx line protocol is converted into Prometheus data points 2019-06-06 16:08:29 +03:00
Aliaksandr Valialkin
a3e26de45e lib/procutil: typo fix in comment to WaitForSigterm 2019-06-04 17:31:47 +03:00
Aliaksandr Valialkin
53ea90865d app/vmselect/promql: add -search.disableCache flag for disabling response caching
This may be useful for data back-filling, when the response caching
could interfere badly with newly added data points with timestamps
in the past.
2019-06-04 17:30:45 +03:00
Aliaksandr Valialkin
17f0a53068 app/vminsert: explain that /query request emulation is required for TSBS benchmark 2019-06-03 18:40:27 +03:00
Anton Patsev
b03bdb32ff Prettify Table of contents (#47) 2019-06-03 17:31:15 +03:00
Aliaksandr Valialkin
15f59c6df9 deployment/docker: remove trailing whitespace 2019-06-03 14:53:08 +03:00
Artem Navoiev
da45a20491 docker compose for VM 2019-06-03 09:57:33 +02:00
Roman Khavronenko
5859bb9556 Add grafana dashboard for VM (#46) 2019-06-03 00:25:07 +03:00
Aliaksandr Valialkin
28f6c36ab4 lib/storage: tune updating a map with today`s metric ids
- Increase update iterval from 1s to 10s. This should reduce CPU usage
  for large amounts of metric ids with constant churn.
- Reduce pendingTodayMetricIDsLock lock duration during the update.
2019-06-02 21:58:16 +03:00
Aliaksandr Valialkin
4794f894a4 lib/storage: speed up checking metricID existence in the list for the current date 2019-06-02 18:34:08 +03:00
Aliaksandr Valialkin
c7280ba61a vendor: update deps with make vendor-update 2019-06-01 23:39:58 +03:00
Aliaksandr Valialkin
fbd8b03f15 README.md: fixed the link to yum repository source codes 2019-06-01 13:55:44 +03:00
Aliaksandr Valialkin
d17a47e3e0 README.md: add setting up service chapter 2019-05-31 23:34:09 +03:00
Aliaksandr Valialkin
d6862a2d97 README.md: mention that VictoriaMetrics works with time series data from Kubernetes 2019-05-31 22:53:35 +03:00
Aliaksandr Valialkin
f2cf5d8e36 app/vmselect/promql: allow escaping identifiers with \ and \xXX
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/42
2019-05-31 17:35:17 +03:00
Aliaksandr Valialkin
27f0d098bd app/victoria-metrics: add make victoria-metrics-arm64 rule for building GOARCH=arm64 binary 2019-05-29 23:07:14 +03:00
Aliaksandr Valialkin
a51ff2c6cb README.md: add LICENSE shield 2019-05-29 14:09:36 +03:00
Aliaksandr Valialkin
56b952c456 app/vminsert: add -maxConcurrentInserts command-line flag for limiting the number of concurrent inserts 2019-05-29 12:41:23 +03:00
Aliaksandr Valialkin
61bad1e07e Makefile: run go vet with -mod=vendor in order to disable downloading vendored deps 2019-05-29 01:38:13 +03:00
Artem Navoiev
be97f764f5 [ci-ci] enable CI (#39) 2019-05-29 01:32:49 +03:00
Artem Navoiev
a576d1f5d3 README.md: add links to slack and telegrams (#40) 2019-05-29 01:30:37 +03:00
Aliaksandr Valialkin
968d094524 app/vminsert: reduce memory usage for Influx, Graphite and OpenTSDB protocols
Do not buffer per-connection data and just store it as it arrives
2019-05-28 18:47:23 +03:00
Aliaksandr Valialkin
e307a4d92c lib/timerpool: use timer pool in concurrency limiters
This should reduce the number of memory allocations in highly loaded system
2019-05-28 17:20:10 +03:00
Aliaksandr Valialkin
0eae39daa7 app/vminsert: properly reset InsertCtx.mrs - they must be empty after Reset call 2019-05-28 16:08:01 +03:00
Aliaksandr Valialkin
437e0b2300 README.md: typo fix 2019-05-27 21:37:48 +03:00
Aliaksandr Valialkin
4b3af728ea README.md: add steps for restoring from a snapshot 2019-05-27 20:36:51 +03:00
Aliaksandr Valialkin
4a12c4c982 README.md: add Third-party contributions section 2019-05-27 20:23:39 +03:00
Anton Patsev
2e75efb64e README.md: add unofficial yum repository (#37) 2019-05-27 20:19:54 +03:00
Aliaksandr Valialkin
25900162f6 Makefile: add -mod=vendor to go test, so tests use external deps from vendor folder 2019-05-27 00:35:46 +03:00
Aliaksandr Valialkin
16afcd6aff vendor: update dependencies with make vendor-update 2019-05-26 23:25:12 +03:00
Aliaksandr Valialkin
c2a5eef5e3 Makefile: pass GO111MODULE=on to all the go invocations 2019-05-26 23:23:43 +03:00
Aliaksandr Valialkin
4859ca0cda app/vmselect: update comment according to the updated code 2019-05-26 22:38:58 +03:00
Aliaksandr Valialkin
feb6b203a4 app/vminsert/influx: try converting string values to numeric values, since Influx agents may send numeric values as strings
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/34
2019-05-26 22:11:19 +03:00
Aliaksandr Valialkin
51ee990902 README.md: typo fix 2019-05-26 17:59:04 +03:00
Aliaksandr Valialkin
5262aae5da app/vmselect/promql: misspeling fix 2019-05-25 21:53:11 +03:00
Aliaksandr Valialkin
54fb8b21f9 all: fix misspellings 2019-05-25 21:51:11 +03:00
Aliaksandr Valialkin
d6523ffe90 Makefile: add -s flag to go fmt in make fmt command 2019-05-25 21:43:35 +03:00
Aliaksandr Valialkin
024560b161 README.md: add goreportcard.com badge 2019-05-25 21:38:57 +03:00
Aliaksandr Valialkin
96ac664b27 Add make victoria-metrics Makefile rule for building dev binary 2019-05-25 18:24:51 +03:00
Aliaksandr Valialkin
2ffcf7a4a5 README.md: mention that VictoriaMetrics is scalable 2019-05-25 17:09:43 +03:00
Aliaksandr Valialkin
5cbd4cfca9 app/vmselect: log slow queries if their execution time exceeds -search.logSlowQueryDuration 2019-05-24 16:12:31 +03:00
Aliaksandr Valialkin
718ce33714 app/vmselect: consume resultsCh data in exportHandler if writeResponseFunc failed to consume it 2019-05-24 14:54:31 +03:00
Aliaksandr Valialkin
f332c0d54e README.md: add contacts chapter 2019-05-24 13:58:26 +03:00
0xflotus
eca566ed22 fixed small errors (#31) 2019-05-24 13:27:42 +03:00
Aliaksandr Valialkin
5bbfdff9fe Makefile: add make publish and make package shortcuts for building and publishing docker images 2019-05-24 13:19:24 +03:00
Aliaksandr Valialkin
6b0ae332f8 lib/encoding: add vm_zstd_block_{compress|decompress}_calls_total for determining the number CompressZSTD / DecompressZSTD calls 2019-05-24 13:01:02 +03:00
Aliaksandr Valialkin
2eb3602d61 app/victoria-metrics: remove -p XXXX:XXXX from docker run options, since it is unnesessary if --net=host is set 2019-05-24 12:54:53 +03:00
Aliaksandr Valialkin
6fb9dd09f5 lib/encoding: add vm_zstd_block_{original|compressed}_bytes_total metrics for rough estimation of block compression ratio 2019-05-24 12:34:32 +03:00
Aliaksandr Valialkin
19b6643e5c lib/encoding: substitute CompressZSTD with CompressZSTDLevel 2019-05-24 12:32:55 +03:00
Aliaksandr Valialkin
08b889ef09 lib/httpserver: add -http.disableResponseCompression flag, which may help saving CPU resources at the cost of higher network bandwidth usage 2019-05-24 12:18:40 +03:00
Aliaksandr Valialkin
d15d0127fe app/vmselect/promql: add alias(q, name) function that sets the given name to all the time series in q 2019-05-24 02:41:45 +03:00
Aliaksandr Valialkin
674888fdc9 lib/decimal: add a comment explaining weird code in maxUpExponent. Fixes #29 2019-05-23 17:18:35 +03:00
Aliaksandr Valialkin
fb140eda33 app/vmselect/promql: add label_transform(q, label, regexp, replacement) function for replacing all the occurences of regexp with replacement in the given label for q 2019-05-23 16:26:19 +03:00
Aliaksandr Valialkin
398ec4383e README.md: typo fix 2019-05-23 02:09:51 +03:00
Aliaksandr Valialkin
eff0debe14 README.md: mention that VictoriaMetrics is high-perf cost-effective TSDB 2019-05-23 00:36:45 +03:00
197 changed files with 11483 additions and 3146 deletions

22
.travis.yml Normal file
View File

@@ -0,0 +1,22 @@
language: go
go:
- 1.12.x
install: make
env:
- GO111MODULE=on
before_install:
- GO111MODULE=off go get -v golang.org/x/lint/golint
- GO111MODULE=off go get -u github.com/kisielk/errcheck
script:
- make check_all
- git diff --exit-code
- make test_full
- make victoria-metrics
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@@ -19,16 +19,20 @@ include deployment/*/Makefile
clean:
rm -rf bin/*
publish: publish-victoria-metrics
package: package-victoria-metrics
release: victoria-metrics-prod
cd bin && tar czf victoria-metrics-$(PKG_TAG).tar.gz victoria-metrics-prod
fmt:
go fmt $(PKG_PREFIX)/lib/...
go fmt $(PKG_PREFIX)/app/...
GO111MODULE=on gofmt -l -w -s ./lib
GO111MODULE=on gofmt -l -w -s ./app
vet:
go vet $(PKG_PREFIX)/lib/...
go vet $(PKG_PREFIX)/app/...
GO111MODULE=on go vet -mod=vendor ./lib/...
GO111MODULE=on go vet -mod=vendor ./app/...
lint: install-golint
golint lib/...
@@ -46,19 +50,34 @@ errcheck: install-errcheck
install-errcheck:
which errcheck || GO111MODULE=off go get -u github.com/kisielk/errcheck
check_all: fmt vet lint errcheck golangci-lint
test:
go test $(PKG_PREFIX)/lib/...
GO111MODULE=on go test -mod=vendor ./lib/...
GO111MODULE=on go test -mod=vendor ./app/...
test_full:
GO111MODULE=on go test -mod=vendor -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
benchmark:
go test -bench=. $(PKG_PREFIX)/lib/...
GO111MODULE=on go test -mod=vendor -bench=. ./lib/...
GO111MODULE=on go test -mod=vendor -bench=. ./app/...
vendor-update:
go get -u
go mod tidy
go mod vendor
GO111MODULE=on go get -u ./lib/...
GO111MODULE=on go get -u ./app/...
GO111MODULE=on go mod tidy
GO111MODULE=on go mod vendor
quicktemplate-gen: install-qtc
qtc
install-qtc:
which qtc || GO111MODULE=off go get -u github.com/valyala/quicktemplate/qtc
golangci-lint: install-golangci-lint
golangci-lint run --exclude '(SA4003|SA1019):' -D errcheck
install-golangci-lint:
which golangci-lint || GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint

228
README.md
View File

@@ -1,12 +1,16 @@
<img text-align="center" alt="Victoria Metrics" src="logo.png">
[![Latest Release](https://img.shields.io/github/release/VictoriaMetrics/VictoriaMetrics.svg?style=flat-square)](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
[![GitHub license](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics.svg)](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE)
[![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics)](https://goreportcard.com/report/github.com/VictoriaMetrics/VictoriaMetrics)
[![Build Status](https://travis-ci.org/VictoriaMetrics/VictoriaMetrics.svg?branch=master)](https://travis-ci.org/VictoriaMetrics/VictoriaMetrics)
[![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics)
<img text-align="center" alt="Victoria Metrics" src="logo.png">
## Single-node VictoriaMetrics
[![Latest Release](https://img.shields.io/github/release/VictoriaMetrics/VictoriaMetrics.svg?style=flat-square)](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
VictoriaMetrics is a long-term remote storage for Prometheus.
VictoriaMetrics is fast, cost-effective and scalable time series database. It can be used as a long-term remote storage for Prometheus.
It is available in [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases),
[docker images](https://hub.docker.com/r/valyala/victoria-metrics/) and
[docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and
in [source code](https://github.com/VictoriaMetrics/VictoriaMetrics).
Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
@@ -37,7 +41,7 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
* [Graphite plaintext protocol](https://graphite.readthedocs.io/en/latest/feeding-carbon.html) with [tags](https://graphite.readthedocs.io/en/latest/tags.html#carbon)
if `-graphiteListenAddr` is set.
* [OpenTSDB put message](http://opentsdb.net/docs/build/html/api_telnet/put.html) if `-opentsdbListenAddr` is set.
* Ideally works with big amounts of time series data from IoT sensors, connected car sensors and industrial sensors.
* Ideally works with big amounts of time series data from Kubernetes, IoT sensors, connected cars and industrial telemetry.
* Has open source [cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
@@ -46,34 +50,47 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
### Table of contents
* [How to build from sources](#how-to-build-from-sources)
* [How to start VictoriaMetrics](#how-to-start-victoriametrics)
* [Prometheus setup](#prometheus-setup)
* [Grafana setup](#grafana-setup)
* [How to send data from InfluxDB-compatible agents such as Telegraf](#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf)
* [How to send data from Graphite-compatible agents such as StatsD](#how-to-send-data-from-graphite-compatible-agents-such-as-statsd)
* [How to send data from OpenTSDB-compatible agents](#how-to-send-data-from-opentsdb-compatible-agents)
* [How to apply new config / ugrade VictoriaMetrics](#how-to-apply-new-config--upgrade-victoriametrics)
* [How to work with snapshots](#how-to-work-with-snapshots)
* [How to delete time series](#how-to-delete-time-series)
* [How to export time series](#how-to-export-time-series)
* [Federation](#federation)
* [Capacity planning](#capacity-planning)
* [High Availability](#high-availability)
* [Multiple retentions](#multiple-retentions)
* [Scalability and cluster version](#scalability-and-cluster-version)
* [Security](#security)
* [Tuning](#tuning)
* [Monitoring](#monitoring)
* [Troubleshooting](#troubleshooting)
* [Community and contributions](#community-and-contributions)
* [Reporting bugs](#reporting-bugs)
- [How to build from sources](#how-to-build-from-sources)
- [Development build](#development-build)
- [Production build](#production-build)
- [Building docker images](#building-docker-images)
- [How to start VictoriaMetrics](#how-to-start-victoriametrics)
- [Setting up service](#setting-up-service)
- [Third-party contributions](#third-party-contributions)
- [Prometheus setup](#prometheus-setup)
- [Grafana setup](#grafana-setup)
- [How to send data from InfluxDB-compatible agents such as Telegraf?](#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf)
- [How to send data from Graphite-compatible agents such as StatsD?](#how-to-send-data-from-graphite-compatible-agents-such-as-statsd)
- [How to send data from OpenTSDB-compatible agents?](#how-to-send-data-from-opentsdb-compatible-agents)
- [How to apply new config / upgrade VictoriaMetrics?](#how-to-apply-new-config--upgrade-victoriametrics)
- [How to work with snapshots?](#how-to-work-with-snapshots)
- [How to delete time series?](#how-to-delete-time-series)
- [How to export time series?](#how-to-export-time-series)
- [Federation](#federation)
- [Capacity planning](#capacity-planning)
- [High availability](#high-availability)
- [Multiple retentions](#multiple-retentions)
- [Downsampling](#downsampling)
- [Multi-tenancy](#multi-tenancy)
- [Scalability and cluster version](#scalability-and-cluster-version)
- [Security](#security)
- [Tuning](#tuning)
- [Monitoring](#monitoring)
- [Troubleshooting](#troubleshooting)
- [Contacts](#contacts)
- [Community and contributions](#community-and-contributions)
- [Reporting bugs](#reporting-bugs)
- [Victoria Metrics Logo](#victoria-metrics-logo)
- [Logo Usage Guidelines](#logo-usage-guidelines)
- [Font used:](#font-used)
- [Color Palette:](#color-palette)
- [We kindly ask:](#we-kindly-ask)
### How to build from sources
We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) or
[docker images](https://hub.docker.com/r/valyala/victoria-metrics/) instead of building VictoriaMetrics
[docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/) instead of building VictoriaMetrics
from sources. Building from sources is reasonable when developing an additional features specific
to your needs.
@@ -81,21 +98,20 @@ to your needs.
#### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.12.
2. Run `go build ./app/victoria-metrics` from the root folder of the repository.
It will build `victoria-metrics` binary in the root folder of the repository.
2. Run `make victoria-metrics` from the root folder of the repository.
It will build `victoria-metrics` binary and put it into the `bin` folder.
#### Production build
1. [Install docker](https://docs.docker.com/install/).
2. Run `make victoria-metrics-prod` from the root folder of the respository.
2. Run `make victoria-metrics-prod` from the root folder of the repository.
It will build `victoria-metrics-prod` binary and put it into the `bin` folder.
#### Building docker images
Run `make package-victoria-metrics`. It will build `valyala/victoria-metrics:<PKG_TAG>` docker image locally.
Run `make package-victoria-metrics`. It will build `victoriametrics/victoria-metrics:<PKG_TAG>` docker image locally.
`<PKG_TAG>` is auto-generated image tag, which depends on source code in the repository.
The `<PKG_TAG>` may be manually set via `PKG_TAG=foobar make package`.
The `<PKG_TAG>` may be manually set via `PKG_TAG=foobar make package-victoria-metrics`.
### How to start VictoriaMetrics
@@ -113,6 +129,16 @@ The following command line flags are used the most:
Pass `-help` to see all the available flags with description and default values.
### Setting up service
Read [these instructions](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/43) on how to set up VictoriaMetrics as a service in your OS.
### Third-party contributions
* [Unofficial yum repository](https://copr.fedorainfracloud.org/coprs/antonpatsev/VictoriaMetrics/) ([source code](https://github.com/patsevanton/victoriametrics-rpm))
### Prometheus setup
Add the following lines to Prometheus config file (it is usually located at `/etc/prometheus/prometheus.yml`):
@@ -122,6 +148,7 @@ remote_write:
- url: http://<victoriametrics-addr>:8428/api/v1/write
queue_config:
max_samples_per_send: 10000
max_shards: 100
```
Substitute `<victoriametrics-addr>` with the hostname or IP address of VictoriaMetrics.
@@ -176,10 +203,43 @@ For instance, put the following lines into `Telegraf` config, so it sends data t
Do not forget substituting `<victoriametrics-addr>` with the real address where VictoriaMetrics runs.
VictoriaMetrics maps Influx data using the following rules:
* [`db` query arg](https://docs.influxdata.com/influxdb/v1.7/tools/api/#write-http-endpoint) is mapped into `db` label value
* Field names are mapped to time series names prefixed by `{measurement}.` value
* Field values are mapped to time series values
* Tags are mapped to Prometheus labels as-is
* [`db` query arg](https://docs.influxdata.com/influxdb/v1.7/tools/api/#write-http-endpoint) is mapped into `db` label value.
* Field names are mapped to time series names prefixed with `{measurement}{separator}` value. `{separator}` equals to `.` by default, but can be changed with `-influxMeasurementFieldSeparator` command-line flag.
* Field values are mapped to time series values.
* Tags are mapped to Prometheus labels as-is.
For example, the following Influx line:
```
foo,tag1=value1,tag2=value2 field1=12,field2=40
```
is converted into the following Prometheus data points:
```
foo.field1{tag1="value1", tag2="value2"} 12
foo.field2{tag1="value1", tag2="value2"} 40
```
Example for writing data with Influx line protocol to local VictoriaMetrics using `curl`:
```
curl -d 'measurement,tag1=value1,tag2=value2 field1=123,field2=1.23' -X POST 'http://localhost:8428/write'
```
Arbitrary number of lines delimited by '\n' may be sent in a single request.
After that the data may be read via [/api/v1/export](#how-to-export-time-series) endpoint:
```
curl -G 'http://localhost:8428/api/v1/export' --data-urlencode 'match={__name__!=""}'
```
The `/api/v1/export` endpoint should return the following response:
```
{"metric":{"__name__":"measurement.field1","tag1":"value1","tag2":"value2"},"values":[123],"timestamps":[1560272508147]}
{"metric":{"__name__":"measurement.field2","tag1":"value1","tag2":"value2"},"values":[1.23],"timestamps":[1560272508147]}
```
### How to send data from Graphite-compatible agents such as [StatsD](https://github.com/etsy/statsd)?
@@ -188,25 +248,66 @@ VictoriaMetrics maps Influx data using the following rules:
the following command will enable Graphite receiver in VictoriaMetrics on TCP and UDP port `2003`:
```
/path/to/victoria-metrics-prod ... -graphiteListenAddr=:2003
/path/to/victoria-metrics-prod -graphiteListenAddr=:2003
```
2) Use the configured address in Graphite-compatible agents. For instance, set `graphiteHost`
to the VictoriaMetrics host in `StatsD` configs.
Example for writing data with Graphite plaintext protocol to local VictoriaMetrics using `nc`:
```
echo "foo.bar.baz;tag1=value1;tag2=value2 123 `date +%s`" | nc -N localhost 2003
```
VictoriaMetrics sets the current time if timestamp is omitted.
Arbitrary number of lines delimited by `\n` may be sent in one go.
After that the data may be read via [/api/v1/export](#how-to-export-time-series) endpoint:
```
curl -G 'http://localhost:8428/api/v1/export' --data-urlencode 'match={__name__!=""}'
```
The `/api/v1/export` endpoint should return the following response:
```
{"metric":{"__name__":"foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123],"timestamps":[1560277406000]}
```
### How to send data from OpenTSDB-compatible agents?
1) Enable OpenTSDB receiver in VictoriaMetrics by setting `-opentsdbListenAddr` command line flag. For instance,
the following command will enable OpenTSDB receiver in VictoriaMetrics on TCP and UDP port `4242`:
```
/path/to/victoria-metrics-prod ... -opentsdbListenAddr=:4242
/path/to/victoria-metrics-prod -opentsdbListenAddr=:4242
```
2) Send data to the given address from OpenTSDB-compatible agents.
Example for writing data with OpenTSDB protocol to local VictoriaMetrics using `nc`:
```
echo "put foo.bar.baz `date +%s` 123 tag1=value1 tag2=value2" | nc -N localhost 4242
```
Arbitrary number of lines delimited by `\n` may be sent in one go.
After that the data may be read via [/api/v1/export](#how-to-export-time-series) endpoint:
```
curl -G 'http://localhost:8428/api/v1/export' --data-urlencode 'match={__name__!=""}'
```
The `/api/v1/export` endpoint should return the following response:
```
{"metric":{"__name__":"foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123],"timestamps":[1560277292000]}
```
### How to apply new config / upgrade VictoriaMetrics?
VictoriaMetrics must be restarted in order to upgrade or apply new config:
@@ -218,6 +319,8 @@ VictoriaMetrics must be restarted in order to upgrade or apply new config:
### How to work with snapshots?
VictoriaMetrics is able to create [instant snapshots](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282)
for all the data stored under `-storageDataPath` directory.
Navigate to `http://<victoriametrics-addr>:8428/snapshot/create` in order to create an instant snapshot.
The page will return the following JSON response:
@@ -226,7 +329,7 @@ The page will return the following JSON response:
```
Snapshots are created under `<-storageDataPath>/snapshots` directory, where `<-storageDataPath>`
is the command-line flag value. Snapshots can be archived to backup storage via `rsync -L`, `scp -r`
is the command-line flag value. Snapshots can be archived to backup storage via `cp -L`, `rsync -L`, `scp -r`
or any similar tool that follows symlinks during copying.
The `http://<victoriametrics-addr>:8428/snapshot/list` page contains the list of available snapshots.
@@ -236,6 +339,12 @@ to delete `<snapshot-name>` snapshot.
Navigate to `http://<victoriametrics-addr>:8428/snapshot/delete_all` in order to delete all the snapshots.
Steps for restoring from a snapshot:
1. Stop VictoriaMetrics with `kill -INT`.
2. Remove the entire contents of the directory pointed by `-storageDataPath` command-line flag.
3. Copy snapshot contents to the directory pointed by `-storageDataPath`.
4. Start VictoriaMetrics.
### How to delete time series?
@@ -268,7 +377,7 @@ at `http://<victoriametrics-addr>:8428/federate?match[]=<timeseries_selector_for
Optional `start` and `end` args may be added to the request in order to scrape the last point for each selected time series on the `[start ... end]` interval.
`start` and `end` may contain either unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values. By default the last point
on the interval `[now - max_lookback ... now]` is scraped for each time series. Default value for `max_lookback` is `5m` (5 minutes), but can be overriden.
on the interval `[now - max_lookback ... now]` is scraped for each time series. Default value for `max_lookback` is `5m` (5 minutes), but can be overridden.
For instance, `/federate?match[]=up&max_lookback=1h` would return last points on the `[now - 1h ... now]` interval. This may be useful for time series federation
with scrape intervals exceeding `5m`.
@@ -325,6 +434,23 @@ Just start multiple VictoriaMetrics instances with distinct values for the follo
* `-httpListenAddr`, so clients may reach VictoriaMetrics instance with proper retention
### Downsampling
There is no downsampling support at the moment, but:
- VictoriaMetrics is optimized for querying big amounts of raw data. See benchmark results for heavy queries
in [this article](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
- VictoriaMetrics has good compression for on-disk data. See [this article](https://medium.com/@valyala/victoriametrics-achieving-better-compression-for-time-series-data-than-gorilla-317bc1f95932)
for details.
These properties reduce the need in downsampling. We plan implementing downsampling in the future.
See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/36) for details.
### Multi-tenancy
Single-node VictoriaMetrics doesn't support multi-tenancy. Use [cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) instead.
### Scalability and cluster version
Though single-node VictoriaMetrics cannot scale to multiple nodes, it is optimized for resource usage - storage size / bandwidth / IOPS, RAM, CPU.
@@ -376,10 +502,24 @@ There is [an official Grafana dashboard for single-node VictoriaMetrics](https:/
Another option is to increase `-memory.allowedPercent` command-line flag value. Be careful with this
option, since too big value for `-memory.allowedPercent` may result in high I/O usage.
* If VictoriaMetrics doesn't work because of certain parts are corrupted due to disk errors,
then just remove directoreis with broken parts. This will recover VictoriaMetrics at the cost
of data loss stored in the broken parts. In the future `vmrecover` tool will be created
for automatic recovering from such errors.
## Contacts
Contact us with any questions regarding VictoriaMetrics at [info@victoriametrics.com](mailto:info@victoriametrics.com).
## Community and contributions
Feel free asking any questions regarding VictoriaMetrics [here](https://groups.google.com/forum/#!forum/victorametrics-users).
Feel free asking any questions regarding VictoriaMetrics:
- [telergam-en](https://t.me/VictoriaMetrics_en)
- [telergam-ru](https://t.me/VictoriaMetrics_ru1)
- [google groups](https://groups.google.com/forum/#!forum/victorametrics-users)
We are open to third-party pull requests provided they follow [KISS design principle](https://en.wikipedia.org/wiki/KISS_principle):

View File

@@ -1,5 +1,8 @@
# All these commands must run from repository root.
victoria-metrics:
GO111MODULE=on go build -mod=vendor -ldflags "$(GO_BUILDINFO)" -o bin/victoria-metrics ./app/victoria-metrics
victoria-metrics-prod:
APP_NAME=victoria-metrics $(MAKE) app-via-docker
@@ -12,10 +15,13 @@ publish-victoria-metrics:
run-victoria-metrics:
mkdir -p victoria-metrics-data
DOCKER_OPTS='-v $(shell pwd)/victoria-metrics-data:/victoria-metrics-data -p 8428:8428 -p 2003:2003 -p 2003:2003/udp' \
DOCKER_OPTS='-v $(shell pwd)/victoria-metrics-data:/victoria-metrics-data' \
APP_NAME=victoria-metrics \
ARGS='-graphiteListenAddr=:2003 -opentsdbListenAddr=:4242 -retentionPeriod=12 -search.maxUniqueTimeseries=1000000 -search.maxQueryDuration=10m' \
$(MAKE) run-via-docker
victoria-metrics-arm:
CC=arm-linux-gnueabi-gcc CGO_ENABLED=1 GOARCH=arm GO111MODULE=on go build -mod=vendor -ldflags "$(GO_BUILDINFO)" -o bin/victoria-metrics-arm ./app/victoria-metrics
victoria-metrics-arm64:
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOARCH=arm64 GO111MODULE=on go build -mod=vendor -ldflags "$(GO_BUILDINFO)" -o bin/victoria-metrics-arm64 ./app/victoria-metrics

View File

@@ -30,11 +30,10 @@ func (ctx *InsertCtx) Reset(rowsLen int) {
mr.MetricNameRaw = nil
}
ctx.mrs = ctx.mrs[:0]
if n := rowsLen - cap(ctx.mrs); n > 0 {
ctx.mrs = append(ctx.mrs[:cap(ctx.mrs)], make([]storage.MetricRow, n)...)
}
ctx.mrs = ctx.mrs[:rowsLen]
ctx.mrs = ctx.mrs[:0]
ctx.metricNamesBuf = ctx.metricNamesBuf[:0]
}

View File

@@ -0,0 +1,67 @@
package common
import (
"bytes"
"fmt"
"io"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
)
// The maximum size of a single line returned by ReadLinesBlock.
const maxLineSize = 256 * 1024
// Default size in bytes of a single block returned by ReadLinesBlock.
const defaultBlockSize = 64 * 1024
// ReadLinesBlock reads a block of lines delimited by '\n' from tailBuf and r into dstBuf.
//
// Trailing chars after the last newline are put into tailBuf.
//
// Returns (dstBuf, tailBuf).
func ReadLinesBlock(r io.Reader, dstBuf, tailBuf []byte) ([]byte, []byte, error) {
if cap(dstBuf) < defaultBlockSize {
dstBuf = bytesutil.Resize(dstBuf, defaultBlockSize)
}
dstBuf = append(dstBuf[:0], tailBuf...)
again:
n, err := r.Read(dstBuf[len(dstBuf):cap(dstBuf)])
// Check for error only if zero bytes read from r, i.e. no forward progress made.
// Otherwise process the read data.
if n == 0 {
if err == nil {
return dstBuf, tailBuf, fmt.Errorf("no forward progress made")
}
if err == io.EOF && len(dstBuf) > 0 {
// Missing newline in the end of stream. This is OK,
/// so suppress io.EOF for now. It will be returned during the next
// call to ReadLinesBlock.
// This fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/60 .
return dstBuf, tailBuf, nil
}
return dstBuf, tailBuf, err
}
dstBuf = dstBuf[:len(dstBuf)+n]
// Search for the last newline in dstBuf and put the rest into tailBuf.
nn := bytes.LastIndexByte(dstBuf[len(dstBuf)-n:], '\n')
if nn < 0 {
// Didn't found at least a single line.
if len(dstBuf) > maxLineSize {
return dstBuf, tailBuf, fmt.Errorf("too long line: more than %d bytes", maxLineSize)
}
if cap(dstBuf) < 2*len(dstBuf) {
// Increase dsbBuf capacity, so more data could be read into it.
dstBufLen := len(dstBuf)
dstBuf = bytesutil.Resize(dstBuf, 2*cap(dstBuf))
dstBuf = dstBuf[:dstBufLen]
}
goto again
}
// Found at least a single line. Return it.
nn += len(dstBuf) - n
tailBuf = append(tailBuf[:0], dstBuf[nn+1:]...)
dstBuf = dstBuf[:nn]
return dstBuf, tailBuf, nil
}

View File

@@ -0,0 +1,146 @@
package common
import (
"bytes"
"fmt"
"io"
"testing"
)
func TestReadLinesBlockFailure(t *testing.T) {
f := func(s string) {
t.Helper()
r := bytes.NewBufferString(s)
if _, _, err := ReadLinesBlock(r, nil, nil); err == nil {
t.Fatalf("expecting non-nil error")
}
sbr := &singleByteReader{
b: []byte(s),
}
if _, _, err := ReadLinesBlock(sbr, nil, nil); err == nil {
t.Fatalf("expecting non-nil error")
}
fr := &failureReader{}
if _, _, err := ReadLinesBlock(fr, nil, nil); err == nil {
t.Fatalf("expecting non-nil error")
}
}
// empty string
f("")
// too long string
b := make([]byte, maxLineSize+1)
f(string(b))
}
type failureReader struct{}
func (fr *failureReader) Read(p []byte) (int, error) {
return 0, fmt.Errorf("some error")
}
func TestReadLineBlockSuccessSingleByteReader(t *testing.T) {
f := func(s, dstBufExpected, tailBufExpected string) {
t.Helper()
r := &singleByteReader{
b: []byte(s),
}
dstBuf, tailBuf, err := ReadLinesBlock(r, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if string(dstBuf) != dstBufExpected {
t.Fatalf("unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
}
if string(tailBuf) != tailBufExpected {
t.Fatalf("unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
}
// Verify the same with non-empty dstBuf and tailBuf
r = &singleByteReader{
b: []byte(s),
}
dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf[:0])
if err != nil {
t.Fatalf("non-empty bufs: unexpected error: %s", err)
}
if string(dstBuf) != dstBufExpected {
t.Fatalf("non-empty bufs: unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
}
if string(tailBuf) != tailBufExpected {
t.Fatalf("non-empty bufs: unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
}
}
f("\n", "", "")
f("foo\n", "foo", "")
f("\nfoo", "", "")
f("foo\nbar", "foo", "")
f("foo\nbar\nbaz", "foo", "")
f("foo", "foo", "")
// The maximum line size
b := make([]byte, maxLineSize+10)
b[maxLineSize] = '\n'
f(string(b), string(b[:maxLineSize]), "")
}
func TestReadLineBlockSuccessBytesBuffer(t *testing.T) {
f := func(s, dstBufExpected, tailBufExpected string) {
t.Helper()
r := bytes.NewBufferString(s)
dstBuf, tailBuf, err := ReadLinesBlock(r, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if string(dstBuf) != dstBufExpected {
t.Fatalf("unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
}
if string(tailBuf) != tailBufExpected {
t.Fatalf("unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
}
// Verify the same with non-empty dstBuf and tailBuf
r = bytes.NewBufferString(s)
dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf[:0])
if err != nil {
t.Fatalf("non-empty bufs: unexpected error: %s", err)
}
if string(dstBuf) != dstBufExpected {
t.Fatalf("non-empty bufs: unexpected dstBuf; got %q; want %q; tailBuf=%q", dstBuf, dstBufExpected, tailBuf)
}
if string(tailBuf) != tailBufExpected {
t.Fatalf("non-empty bufs: unexpected tailBuf; got %q; want %q; dstBuf=%q", tailBuf, tailBufExpected, dstBuf)
}
}
f("\n", "", "")
f("foo\n", "foo", "")
f("\nfoo", "", "foo")
f("foo\nbar", "foo", "bar")
f("foo\nbar\nbaz", "foo\nbar", "baz")
// The maximum line size
b := make([]byte, maxLineSize+10)
b[maxLineSize] = '\n'
f(string(b), string(b[:maxLineSize]), string(b[maxLineSize+1:]))
}
type singleByteReader struct {
b []byte
}
func (sbr *singleByteReader) Read(p []byte) (int, error) {
if len(sbr.b) == 0 {
return 0, io.EOF
}
n := copy(p, sbr.b[:1])
sbr.b = sbr.b[n:]
if len(sbr.b) == 0 {
return n, io.EOF
}
return n, nil
}

View File

@@ -1,34 +1,49 @@
package concurrencylimiter
import (
"flag"
"fmt"
"runtime"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
"github.com/VictoriaMetrics/metrics"
)
var maxConcurrentInserts = flag.Int("maxConcurrentInserts", runtime.GOMAXPROCS(-1)*4, "The maximum number of concurrent inserts")
var (
// ch is the channel for limiting concurrent inserts.
// Put an item into it before performing an insert and remove
// the item after the insert is complete.
ch = make(chan struct{}, runtime.GOMAXPROCS(-1)*2)
// ch is the channel for limiting concurrent calls to Do.
ch chan struct{}
// waitDuration is the amount of time to wait until at least a single
// concurrent insert out of cap(Ch) inserts is complete.
// concurrent Do call out of cap(ch) inserts is complete.
waitDuration = time.Second * 30
)
// Init initializes concurrencylimiter.
//
// Init must be called after flag.Parse call.
func Init() {
ch = make(chan struct{}, *maxConcurrentInserts)
}
// Do calls f with the limited concurrency.
func Do(f func() error) error {
// Limit the number of conurrent inserts in order to prevent from excess
// Limit the number of conurrent f calls in order to prevent from excess
// memory usage and CPU trashing.
t := time.NewTimer(waitDuration)
t := timerpool.Get(waitDuration)
select {
case ch <- struct{}{}:
t.Stop()
timerpool.Put(t)
err := f()
<-ch
return err
case <-t.C:
return fmt.Errorf("the server is overloaded with %d concurrent inserts; either increase the number of CPUs or reduce the load", cap(ch))
timerpool.Put(t)
concurrencyLimitErrors.Inc()
return fmt.Errorf("the server is overloaded with %d concurrent inserts; either increase -maxConcurrentInserts or reduce the load", cap(ch))
}
}
var concurrencyLimitErrors = metrics.NewCounter(`vm_concurrency_limit_errors_total`)

View File

@@ -86,7 +86,9 @@ func (r *Row) unmarshal(s string, tagsPool []Tag) ([]Tag, error) {
n = strings.IndexByte(tail, ' ')
if n < 0 {
return tagsPool, fmt.Errorf("cannot find whitespace between value and timestamp in %q", s)
// There is no timestamp. Use default timestamp instead.
r.Value = fastfloat.ParseBestEffort(tail)
return tagsPool, nil
}
r.Value = fastfloat.ParseBestEffort(tail[:n])
r.Timestamp = fastfloat.ParseInt64BestEffort(tail[n+1:])

View File

@@ -22,9 +22,6 @@ func TestRowsUnmarshalFailure(t *testing.T) {
// Missing value
f("aaa")
// Missing timestamp
f("aaa 1123")
// Invalid multiline
f("aaa\nbbb 123 34")
@@ -81,6 +78,14 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
}},
})
// Missing timestamp
f("aaa 1123", &Rows{
Rows: []Row{{
Metric: "aaa",
Value: 1123,
}},
})
// Tags
f("foo;bar=baz 1 2", &Rows{
Rows: []Row{{
@@ -116,13 +121,17 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
})
// Multi lines
f("foo 0.3 2\nbar.baz 0.34 43\n", &Rows{
f("foo 0.3 2\naaa 3\nbar.baz 0.34 43\n", &Rows{
Rows: []Row{
{
Metric: "foo",
Value: 0.3,
Timestamp: 2,
},
{
Metric: "aaa",
Value: 3,
},
{
Metric: "bar.baz",
Value: 0.34,

View File

@@ -1,7 +1,6 @@
package graphite
import (
"bytes"
"fmt"
"io"
"net"
@@ -55,8 +54,6 @@ func (ctx *pushCtx) InsertRows() error {
return ic.FlushBufs()
}
const maxReadPacketSize = 4 * 1024 * 1024
const flushTimeout = 3 * time.Second
func (ctx *pushCtx) Read(r io.Reader) bool {
@@ -71,40 +68,40 @@ func (ctx *pushCtx) Read(r io.Reader) bool {
return false
}
}
lr := io.LimitReader(r, maxReadPacketSize)
ctx.reqBuf.Reset()
ctx.reqBuf.B = append(ctx.reqBuf.B[:0], ctx.tailBuf...)
n, err := io.CopyBuffer(&ctx.reqBuf, lr, ctx.copyBuf[:])
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
ctx.reqBuf, ctx.tailBuf, ctx.err = common.ReadLinesBlock(r, ctx.reqBuf, ctx.tailBuf)
if ctx.err != nil {
if ne, ok := ctx.err.(net.Error); ok && ne.Timeout() {
// Flush the read data on timeout and try reading again.
ctx.err = nil
} else {
graphiteReadErrors.Inc()
ctx.err = fmt.Errorf("cannot read graphite plaintext protocol data: %s", err)
if ctx.err != io.EOF {
graphiteReadErrors.Inc()
ctx.err = fmt.Errorf("cannot read graphite plaintext protocol data: %s", ctx.err)
}
return false
}
} else if n < maxReadPacketSize {
// Mark the end of stream.
ctx.err = io.EOF
}
// Parse all the rows until the last newline in ctx.reqBuf.B
nn := bytes.LastIndexByte(ctx.reqBuf.B, '\n')
ctx.tailBuf = ctx.tailBuf[:0]
if nn >= 0 {
ctx.tailBuf = append(ctx.tailBuf[:0], ctx.reqBuf.B[nn+1:]...)
ctx.reqBuf.B = ctx.reqBuf.B[:nn]
}
if err = ctx.Rows.Unmarshal(bytesutil.ToUnsafeString(ctx.reqBuf.B)); err != nil {
if err := ctx.Rows.Unmarshal(bytesutil.ToUnsafeString(ctx.reqBuf)); err != nil {
graphiteUnmarshalErrors.Inc()
ctx.err = fmt.Errorf("cannot unmarshal graphite plaintext protocol data with size %d: %s", len(ctx.reqBuf.B), err)
ctx.err = fmt.Errorf("cannot unmarshal graphite plaintext protocol data with size %d: %s", len(ctx.reqBuf), err)
return false
}
// Convert timestamps from seconds to milliseconds
for i := range ctx.Rows.Rows {
ctx.Rows.Rows[i].Timestamp *= 1e3
// Fill missing timestamps with the current timestamp rounded to seconds.
currentTimestamp := time.Now().Unix()
rows := ctx.Rows.Rows
for i := range rows {
r := &rows[i]
if r.Timestamp == 0 {
r.Timestamp = currentTimestamp
}
}
// Convert timestamps from seconds to milliseconds.
for i := range rows {
rows[i].Timestamp *= 1e3
}
return true
}
@@ -112,9 +109,8 @@ type pushCtx struct {
Rows Rows
Common common.InsertCtx
reqBuf bytesutil.ByteBuffer
reqBuf []byte
tailBuf []byte
copyBuf [16 * 1024]byte
err error
}
@@ -129,7 +125,7 @@ func (ctx *pushCtx) Error() error {
func (ctx *pushCtx) reset() {
ctx.Rows.Reset()
ctx.Common.Reset(0)
ctx.reqBuf.Reset()
ctx.reqBuf = ctx.reqBuf[:0]
ctx.tailBuf = ctx.tailBuf[:0]
ctx.err = nil

View File

@@ -293,8 +293,10 @@ func parseFieldValue(s string, hasQuotedFields bool) (float64, error) {
if len(s) < 2 || s[len(s)-1] != '"' {
return 0, fmt.Errorf("missing closing quote for quoted field value %s", s)
}
// Quoted string is translated to empty value.
return 0, nil
// Try converting quoted string to number, since sometimes Influx agents
// send numbers as strings.
s = s[1 : len(s)-1]
return fastfloat.ParseBestEffort(s), nil
}
ch := s[len(s)-1]
if ch == 'i' {

View File

@@ -241,17 +241,27 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
})
// Line with multiple tags, multiple fields and timestamp
f(`system,host=ip-172-16-10-144 uptime_format="3 days, 21:01" 1557761040000000000`, &Rows{
f(`system,host=ip-172-16-10-144 uptime_format="3 days, 21:01",quoted_float="-1.23",quoted_int="123" 1557761040000000000`, &Rows{
Rows: []Row{{
Measurement: "system",
Tags: []Tag{{
Key: "host",
Value: "ip-172-16-10-144",
}},
Fields: []Field{{
Key: "uptime_format",
Value: 0,
}},
Fields: []Field{
{
Key: "uptime_format",
Value: 0,
},
{
Key: "quoted_float",
Value: -1.23,
},
{
Key: "quoted_int",
Value: 123,
},
},
Timestamp: 1557761040000000000,
}},
})

View File

@@ -1,8 +1,8 @@
package influx
import (
"bytes"
"compress/gzip"
"flag"
"fmt"
"io"
"net/http"
@@ -17,6 +17,11 @@ import (
"github.com/VictoriaMetrics/metrics"
)
var (
measurementFieldSeparator = flag.String("influxMeasurementFieldSeparator", ".", "Separator for `{measurement}{separator}{field_name}` metric name when inserted via Influx line protocol")
skipSingleField = flag.Bool("influxSkipSingleField", false, "Uses `{measurement}` instead of `{measurement}{separator}{field_name}` for metic name if Influx line contains only a single field")
)
var rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="influx"}`)
// InsertHandler processes remote write for influx line protocol.
@@ -89,11 +94,16 @@ func (ctx *pushCtx) InsertRows(db string) error {
}
ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels)
ctx.metricGroupBuf = append(ctx.metricGroupBuf[:0], r.Measurement...)
ctx.metricGroupBuf = append(ctx.metricGroupBuf, '.')
skipFieldKey := len(r.Fields) == 1 && *skipSingleField
if !skipFieldKey {
ctx.metricGroupBuf = append(ctx.metricGroupBuf, *measurementFieldSeparator...)
}
metricGroupPrefixLen := len(ctx.metricGroupBuf)
for j := range r.Fields {
f := &r.Fields[j]
ctx.metricGroupBuf = append(ctx.metricGroupBuf[:metricGroupPrefixLen], f.Key...)
if !skipFieldKey {
ctx.metricGroupBuf = append(ctx.metricGroupBuf[:metricGroupPrefixLen], f.Key...)
}
metricGroup := bytesutil.ToUnsafeString(ctx.metricGroupBuf)
ic.Labels = ic.Labels[:0]
ic.AddLabel("", metricGroup)
@@ -123,36 +133,21 @@ func putGzipReader(zr *gzip.Reader) {
var gzipReaderPool sync.Pool
const maxReadPacketSize = 4 * 1024 * 1024
func (ctx *pushCtx) Read(r io.Reader, tsMultiplier int64) bool {
if ctx.err != nil {
return false
}
lr := io.LimitReader(r, maxReadPacketSize)
ctx.reqBuf.Reset()
ctx.reqBuf.B = append(ctx.reqBuf.B[:0], ctx.tailBuf...)
n, err := io.CopyBuffer(&ctx.reqBuf, lr, ctx.copyBuf[:])
if err != nil {
influxReadErrors.Inc()
ctx.err = fmt.Errorf("cannot read influx line protocol data: %s", err)
ctx.reqBuf, ctx.tailBuf, ctx.err = common.ReadLinesBlock(r, ctx.reqBuf, ctx.tailBuf)
if ctx.err != nil {
if ctx.err != io.EOF {
influxReadErrors.Inc()
ctx.err = fmt.Errorf("cannot read influx line protocol data: %s", ctx.err)
}
return false
}
if n < maxReadPacketSize {
// Mark the end of stream.
ctx.err = io.EOF
}
// Parse all the rows until the last newline in ctx.reqBuf.B
nn := bytes.LastIndexByte(ctx.reqBuf.B, '\n')
ctx.tailBuf = ctx.tailBuf[:0]
if nn >= 0 {
ctx.tailBuf = append(ctx.tailBuf[:0], ctx.reqBuf.B[nn+1:]...)
ctx.reqBuf.B = ctx.reqBuf.B[:nn]
}
if err = ctx.Rows.Unmarshal(bytesutil.ToUnsafeString(ctx.reqBuf.B)); err != nil {
if err := ctx.Rows.Unmarshal(bytesutil.ToUnsafeString(ctx.reqBuf)); err != nil {
influxUnmarshalErrors.Inc()
ctx.err = fmt.Errorf("cannot unmarshal influx line protocol data with size %d: %s", len(ctx.reqBuf.B), err)
ctx.err = fmt.Errorf("cannot unmarshal influx line protocol data with size %d: %s", len(ctx.reqBuf), err)
return false
}
@@ -191,9 +186,8 @@ type pushCtx struct {
Rows Rows
Common common.InsertCtx
reqBuf bytesutil.ByteBuffer
reqBuf []byte
tailBuf []byte
copyBuf [16 * 1024]byte
metricNameBuf []byte
metricGroupBuf []byte
@@ -211,7 +205,7 @@ func (ctx *pushCtx) reset() {
ctx.Rows.Reset()
ctx.Common.Reset(0)
ctx.reqBuf.Reset()
ctx.reqBuf = ctx.reqBuf[:0]
ctx.tailBuf = ctx.tailBuf[:0]
ctx.metricNameBuf = ctx.metricNameBuf[:0]
ctx.metricGroupBuf = ctx.metricGroupBuf[:0]

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/concurrencylimiter"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/graphite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/influx"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/opentsdb"
@@ -22,6 +23,7 @@ var (
// Init initializes vminsert.
func Init() {
concurrencylimiter.Init()
if len(*graphiteListenAddr) > 0 {
go graphite.Serve(*graphiteListenAddr)
}
@@ -63,7 +65,8 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
w.WriteHeader(http.StatusNoContent)
return true
case "/query":
// Emulate fake response for influx query
// Emulate fake response for influx query.
// This is required for TSBS benchmark.
influxQueryRequests.Inc()
fmt.Fprintf(w, `{"results":[{"series":[{"values":[]}]}]}`)
return true

View File

@@ -1,7 +1,6 @@
package opentsdb
import (
"bytes"
"fmt"
"io"
"net"
@@ -55,8 +54,6 @@ func (ctx *pushCtx) InsertRows() error {
return ic.FlushBufs()
}
const maxReadPacketSize = 4 * 1024 * 1024
const flushTimeout = 3 * time.Second
func (ctx *pushCtx) Read(r io.Reader) bool {
@@ -71,33 +68,22 @@ func (ctx *pushCtx) Read(r io.Reader) bool {
return false
}
}
lr := io.LimitReader(r, maxReadPacketSize)
ctx.reqBuf.Reset()
ctx.reqBuf.B = append(ctx.reqBuf.B[:0], ctx.tailBuf...)
n, err := io.CopyBuffer(&ctx.reqBuf, lr, ctx.copyBuf[:])
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
ctx.reqBuf, ctx.tailBuf, ctx.err = common.ReadLinesBlock(r, ctx.reqBuf, ctx.tailBuf)
if ctx.err != nil {
if ne, ok := ctx.err.(net.Error); ok && ne.Timeout() {
// Flush the read data on timeout and try reading again.
ctx.err = nil
} else {
opentsdbReadErrors.Inc()
ctx.err = fmt.Errorf("cannot read OpenTSDB put protocol data: %s", err)
if ctx.err != io.EOF {
opentsdbReadErrors.Inc()
ctx.err = fmt.Errorf("cannot read OpenTSDB put protocol data: %s", ctx.err)
}
return false
}
} else if n < maxReadPacketSize {
// Mark the end of stream.
ctx.err = io.EOF
}
// Parse all the rows until the last newline in ctx.reqBuf.B
nn := bytes.LastIndexByte(ctx.reqBuf.B, '\n')
ctx.tailBuf = ctx.tailBuf[:0]
if nn >= 0 {
ctx.tailBuf = append(ctx.tailBuf[:0], ctx.reqBuf.B[nn+1:]...)
ctx.reqBuf.B = ctx.reqBuf.B[:nn]
}
if err = ctx.Rows.Unmarshal(bytesutil.ToUnsafeString(ctx.reqBuf.B)); err != nil {
if err := ctx.Rows.Unmarshal(bytesutil.ToUnsafeString(ctx.reqBuf)); err != nil {
opentsdbUnmarshalErrors.Inc()
ctx.err = fmt.Errorf("cannot unmarshal OpenTSDB put protocol data with size %d: %s", len(ctx.reqBuf.B), err)
ctx.err = fmt.Errorf("cannot unmarshal OpenTSDB put protocol data with size %d: %s", len(ctx.reqBuf), err)
return false
}
@@ -112,9 +98,8 @@ type pushCtx struct {
Rows Rows
Common common.InsertCtx
reqBuf bytesutil.ByteBuffer
reqBuf []byte
tailBuf []byte
copyBuf [16 * 1024]byte
err error
}
@@ -129,7 +114,7 @@ func (ctx *pushCtx) Error() error {
func (ctx *pushCtx) reset() {
ctx.Rows.Reset()
ctx.Common.Reset(0)
ctx.reqBuf.Reset()
ctx.reqBuf = ctx.reqBuf[:0]
ctx.tailBuf = ctx.tailBuf[:0]
ctx.err = nil

View File

@@ -14,6 +14,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
"github.com/VictoriaMetrics/metrics"
)
@@ -42,13 +43,14 @@ func Stop() {
// RequestHandler handles remote read API requests for Prometheus
func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
// Limit the number of concurrent queries.
// Sleep for a second until giving up. This should resolve short bursts in requests.
t := time.NewTimer(*maxQueueDuration)
// Sleep for a while until giving up. This should resolve short bursts in requests.
t := timerpool.Get(*maxQueueDuration)
select {
case concurrencyCh <- struct{}{}:
t.Stop()
timerpool.Put(t)
defer func() { <-concurrencyCh }()
case <-t.C:
timerpool.Put(t)
httpserver.Errorf(w, "cannot handle more than %d concurrent requests", cap(concurrencyCh))
return true
}
@@ -115,6 +117,15 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
}
return true
case "/api/v1/labels/count":
labelsCountRequests.Inc()
httpserver.EnableCORS(w, r)
if err := prometheus.LabelsCountHandler(w, r); err != nil {
labelsCountErrors.Inc()
sendPrometheusError(w, r, err)
return true
}
return true
case "/api/v1/export":
exportRequests.Inc()
if err := prometheus.ExportHandler(w, r); err != nil {
@@ -178,6 +189,9 @@ var (
labelsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/labels"}`)
labelsErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/api/v1/labels"}`)
labelsCountRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/labels/count"}`)
labelsCountErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/api/v1/labels/count"}`)
deleteRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/admin/tsdb/delete_series"}`)
deleteErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/api/v1/admin/tsdb/delete_series"}`)

View File

@@ -400,6 +400,36 @@ func GetLabelValues(labelName string, deadline Deadline) ([]string, error) {
return labelValues, nil
}
// GetLabelEntries returns all the label entries until the given deadline.
func GetLabelEntries(deadline Deadline) ([]storage.TagEntry, error) {
labelEntries, err := vmstorage.SearchTagEntries(*maxTagKeysPerSearch, *maxTagValuesPerSearch)
if err != nil {
return nil, fmt.Errorf("error during label entries request: %s", err)
}
// Substitute "" with "__name__"
for i := range labelEntries {
e := &labelEntries[i]
if e.Key == "" {
e.Key = "__name__"
}
}
// Sort labelEntries by the number of label values in each entry.
sort.Slice(labelEntries, func(i, j int) bool {
a, b := labelEntries[i].Values, labelEntries[j].Values
if len(a) < len(b) {
return true
}
if len(a) > len(b) {
return false
}
return labelEntries[i].Key < labelEntries[j].Key
})
return labelEntries, nil
}
// GetSeriesCount returns the number of unique series.
func GetSeriesCount(deadline Deadline) (uint64, error) {
n, err := vmstorage.GetSeriesCount()

View File

@@ -22,9 +22,7 @@ func InitTmpBlocksDir(tmpDirPath string) {
tmpDirPath = os.TempDir()
}
tmpBlocksDir = tmpDirPath + "/searchResults"
if err := os.RemoveAll(tmpBlocksDir); err != nil {
logger.Panicf("FATAL: cannot remove %q: %s", tmpBlocksDir, err)
}
fs.MustRemoveAll(tmpBlocksDir)
if err := fs.MkdirAllIfNotExist(tmpBlocksDir); err != nil {
logger.Panicf("FATAL: cannot create %q: %s", tmpBlocksDir, err)
}

View File

@@ -0,0 +1,17 @@
{% import "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage" %}
{% stripspace %}
LabelsCountResponse generates response for /api/v1/labels/count .
{% func LabelsCountResponse(labelEntries []storage.TagEntry) %}
{
"status":"success",
"data":{
{% for i, e := range labelEntries %}
{%q= e.Key %}:{%d= len(e.Values) %}
{% if i+1 < len(labelEntries) %},{% endif %}
{% endfor %}
}
}
{% endfunc %}
{% endstripspace %}

View File

@@ -0,0 +1,74 @@
// Code generated by qtc from "labels_count_response.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line app/vmselect/prometheus/labels_count_response.qtpl:1
package prometheus
//line app/vmselect/prometheus/labels_count_response.qtpl:1
import "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
// LabelsCountResponse generates response for /api/v1/labels/count .
//line app/vmselect/prometheus/labels_count_response.qtpl:5
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line app/vmselect/prometheus/labels_count_response.qtpl:5
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line app/vmselect/prometheus/labels_count_response.qtpl:5
func StreamLabelsCountResponse(qw422016 *qt422016.Writer, labelEntries []storage.TagEntry) {
//line app/vmselect/prometheus/labels_count_response.qtpl:5
qw422016.N().S(`{"status":"success","data":{`)
//line app/vmselect/prometheus/labels_count_response.qtpl:9
for i, e := range labelEntries {
//line app/vmselect/prometheus/labels_count_response.qtpl:10
qw422016.N().Q(e.Key)
//line app/vmselect/prometheus/labels_count_response.qtpl:10
qw422016.N().S(`:`)
//line app/vmselect/prometheus/labels_count_response.qtpl:10
qw422016.N().D(len(e.Values))
//line app/vmselect/prometheus/labels_count_response.qtpl:11
if i+1 < len(labelEntries) {
//line app/vmselect/prometheus/labels_count_response.qtpl:11
qw422016.N().S(`,`)
//line app/vmselect/prometheus/labels_count_response.qtpl:11
}
//line app/vmselect/prometheus/labels_count_response.qtpl:12
}
//line app/vmselect/prometheus/labels_count_response.qtpl:12
qw422016.N().S(`}}`)
//line app/vmselect/prometheus/labels_count_response.qtpl:15
}
//line app/vmselect/prometheus/labels_count_response.qtpl:15
func WriteLabelsCountResponse(qq422016 qtio422016.Writer, labelEntries []storage.TagEntry) {
//line app/vmselect/prometheus/labels_count_response.qtpl:15
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/labels_count_response.qtpl:15
StreamLabelsCountResponse(qw422016, labelEntries)
//line app/vmselect/prometheus/labels_count_response.qtpl:15
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/labels_count_response.qtpl:15
}
//line app/vmselect/prometheus/labels_count_response.qtpl:15
func LabelsCountResponse(labelEntries []storage.TagEntry) string {
//line app/vmselect/prometheus/labels_count_response.qtpl:15
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/labels_count_response.qtpl:15
WriteLabelsCountResponse(qb422016, labelEntries)
//line app/vmselect/prometheus/labels_count_response.qtpl:15
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/labels_count_response.qtpl:15
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/labels_count_response.qtpl:15
return qs422016
//line app/vmselect/prometheus/labels_count_response.qtpl:15
}

View File

@@ -37,9 +37,21 @@ func FederateHandler(w http.ResponseWriter, r *http.Request) error {
return fmt.Errorf("cannot parse request form values: %s", err)
}
matches := r.Form["match[]"]
maxLookback := getDuration(r, "max_lookback", defaultStep)
start := getTime(r, "start", ct-maxLookback)
end := getTime(r, "end", ct)
if len(matches) == 0 {
return fmt.Errorf("missing `match[]` arg")
}
maxLookback, err := getDuration(r, "max_lookback", defaultStep)
if err != nil {
return err
}
start, err := getTime(r, "start", ct-maxLookback)
if err != nil {
return err
}
end, err := getTime(r, "end", ct)
if err != nil {
return err
}
deadline := getDeadline(r)
if start >= end {
start = end - defaultStep
@@ -97,10 +109,19 @@ func ExportHandler(w http.ResponseWriter, r *http.Request) error {
if len(matches) == 0 {
// Maintain backwards compatibility
match := r.FormValue("match")
if len(match) == 0 {
return fmt.Errorf("missing `match[]` arg")
}
matches = []string{match}
}
start := getTime(r, "start", 0)
end := getTime(r, "end", ct)
start, err := getTime(r, "start", 0)
if err != nil {
return err
}
end, err := getTime(r, "end", ct)
if err != nil {
return err
}
format := r.FormValue("format")
deadline := getDeadline(r)
if start >= end {
@@ -156,6 +177,11 @@ func exportHandler(w http.ResponseWriter, matches []string, start, end int64, fo
w.Header().Set("Content-Type", contentType)
writeResponseFunc(w, resultsCh)
// Consume all the data from resultsCh in the event writeResponseFunc
// fails to consume all the data.
for bb := range resultsCh {
quicktemplate.ReleaseByteBuffer(bb)
}
err = <-doneCh
if err != nil {
return fmt.Errorf("error during data fetching: %s", err)
@@ -175,6 +201,9 @@ func DeleteHandler(r *http.Request) error {
return fmt.Errorf("start and end aren't supported. Remove these args from the query in order to delete all the matching metrics")
}
matches := r.Form["match[]"]
if len(matches) == 0 {
return fmt.Errorf("missing `match[]` arg")
}
tagFilterss, err := getTagFilterssFromMatches(matches)
if err != nil {
return err
@@ -214,6 +243,23 @@ func LabelValuesHandler(labelName string, w http.ResponseWriter, r *http.Request
var labelValuesDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/label/{}/values"}`)
// LabelsCountHandler processes /api/v1/labels/count request.
func LabelsCountHandler(w http.ResponseWriter, r *http.Request) error {
startTime := time.Now()
deadline := getDeadline(r)
labelEntries, err := netstorage.GetLabelEntries(deadline)
if err != nil {
return fmt.Errorf(`cannot obtain label entries: %s`, err)
}
w.Header().Set("Content-Type", "application/json")
WriteLabelsCountResponse(w, labelEntries)
labelsCountDuration.UpdateDuration(startTime)
return nil
}
var labelsCountDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/labels/count"}`)
// LabelsHandler processes /api/v1/labels request.
//
// See https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names
@@ -260,8 +306,17 @@ func SeriesHandler(w http.ResponseWriter, r *http.Request) error {
return fmt.Errorf("cannot parse form values: %s", err)
}
matches := r.Form["match[]"]
start := getTime(r, "start", ct-defaultStep)
end := getTime(r, "end", ct)
if len(matches) == 0 {
return fmt.Errorf("missing `match[]` arg")
}
start, err := getTime(r, "start", ct-defaultStep)
if err != nil {
return err
}
end, err := getTime(r, "end", ct)
if err != nil {
return err
}
deadline := getDeadline(r)
tagFilterss, err := getTagFilterssFromMatches(matches)
@@ -297,11 +352,10 @@ func SeriesHandler(w http.ResponseWriter, r *http.Request) error {
WriteSeriesResponse(w, resultsCh)
// Consume all the data from resultsCh in the event WriteSeriesResponse
// fail to consume all the data.
// fails to consume all the data.
for bb := range resultsCh {
quicktemplate.ReleaseByteBuffer(bb)
}
err = <-doneCh
if err != nil {
return fmt.Errorf("error during data fetching: %s", err)
@@ -320,8 +374,17 @@ func QueryHandler(w http.ResponseWriter, r *http.Request) error {
ct := currentTime()
query := r.FormValue("query")
start := getTime(r, "time", ct)
step := getDuration(r, "step", latencyOffset)
if len(query) == 0 {
return fmt.Errorf("missing `query` arg")
}
start, err := getTime(r, "time", ct)
if err != nil {
return err
}
step, err := getDuration(r, "step", latencyOffset)
if err != nil {
return err
}
deadline := getDeadline(r)
if len(query) > *maxQueryLen {
@@ -384,9 +447,21 @@ func QueryRangeHandler(w http.ResponseWriter, r *http.Request) error {
ct := currentTime()
query := r.FormValue("query")
start := getTime(r, "start", ct-defaultStep)
end := getTime(r, "end", ct)
step := getDuration(r, "step", defaultStep)
if len(query) == 0 {
return fmt.Errorf("missing `query` arg")
}
start, err := getTime(r, "start", ct-defaultStep)
if err != nil {
return err
}
end, err := getTime(r, "end", ct)
if err != nil {
return err
}
step, err := getDuration(r, "step", defaultStep)
if err != nil {
return err
}
deadline := getDeadline(r)
mayCache := !getBool(r, "nocache")
@@ -463,25 +538,25 @@ func adjustLastPoints(tss []netstorage.Result) {
}
}
func getTime(r *http.Request, argKey string, defaultValue int64) int64 {
func getTime(r *http.Request, argKey string, defaultValue int64) (int64, error) {
argValue := r.FormValue(argKey)
if len(argValue) == 0 {
return defaultValue
return defaultValue, nil
}
secs, err := strconv.ParseFloat(argValue, 64)
if err != nil {
// Try parsing string format
t, err := time.Parse(time.RFC3339, argValue)
if err != nil {
return defaultValue
return 0, fmt.Errorf("cannot parse %q=%q: %s", argKey, argValue, err)
}
secs = float64(t.UnixNano()) / 1e9
}
msecs := int64(secs * 1e3)
if msecs < minTimeMsecs || msecs > maxTimeMsecs {
return defaultValue
return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, minTimeMsecs, maxTimeMsecs)
}
return msecs
return msecs, nil
}
const (
@@ -490,31 +565,34 @@ const (
maxTimeMsecs = int64(1<<63-1) / 1e6
)
func getDuration(r *http.Request, argKey string, defaultValue int64) int64 {
func getDuration(r *http.Request, argKey string, defaultValue int64) (int64, error) {
argValue := r.FormValue(argKey)
if len(argValue) == 0 {
return defaultValue
return defaultValue, nil
}
secs, err := strconv.ParseFloat(argValue, 64)
if err != nil {
// Try parsing string format
d, err := time.ParseDuration(argValue)
if err != nil {
return defaultValue
return 0, fmt.Errorf("cannot parse %q=%q: %s", argKey, argValue, err)
}
secs = d.Seconds()
}
msecs := int64(secs * 1e3)
if msecs <= 0 || msecs > maxDurationMsecs {
return defaultValue
return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, 0, maxDurationMsecs)
}
return msecs
return msecs, nil
}
const maxDurationMsecs = 100 * 365 * 24 * 3600 * 1000
func getDeadline(r *http.Request) netstorage.Deadline {
d := getDuration(r, "timeout", 0)
d, err := getDuration(r, "timeout", 0)
if err != nil {
d = 0
}
dMax := int64(maxQueryDuration.Seconds() * 1e3)
if d <= 0 || d > dMax {
d = dMax

View File

@@ -26,6 +26,8 @@ var aggrFuncs = map[string]aggrFunc{
"median": aggrFuncMedian,
"limitk": aggrFuncLimitK,
"distinct": newAggrFunc(aggrFuncDistinct),
"sum2": newAggrFunc(aggrFuncSum2),
"geomean": newAggrFunc(aggrFuncGeomean),
}
type aggrFunc func(afa *aggrFuncArg) ([]*timeseries, error)
@@ -100,10 +102,18 @@ func aggrFuncExt(afe func(tss []*timeseries) []*timeseries, argOrig []*timeserie
}
bbPool.Put(bb)
srcTssCount := 0
dstTssCount := 0
rvs := make([]*timeseries, 0, len(m))
for _, tss := range m {
rv := afe(tss)
rvs = append(rvs, rv...)
srcTssCount += len(tss)
dstTssCount += len(rv)
if dstTssCount > 2000 && dstTssCount > 16*srcTssCount {
// This looks like count_values explosion.
return nil, fmt.Errorf(`too many timeseries after aggragation; got %d; want less than %d`, dstTssCount, 16*srcTssCount)
}
}
return rvs, nil
}
@@ -132,6 +142,52 @@ func aggrFuncSum(tss []*timeseries) []*timeseries {
return tss[:1]
}
func aggrFuncSum2(tss []*timeseries) []*timeseries {
dst := tss[0]
for i := range dst.Values {
sum2 := float64(0)
count := 0
for _, ts := range tss {
v := ts.Values[i]
if math.IsNaN(v) {
continue
}
sum2 += v * v
count++
}
if count == 0 {
sum2 = nan
}
dst.Values[i] = sum2
}
return tss[:1]
}
func aggrFuncGeomean(tss []*timeseries) []*timeseries {
if len(tss) == 1 {
// Fast path - nothing to geomean.
return tss
}
dst := tss[0]
for i := range dst.Values {
p := 1.0
count := 0
for _, ts := range tss {
v := ts.Values[i]
if math.IsNaN(v) {
continue
}
p *= v
count++
}
if count == 0 {
p = nan
}
dst.Values[i] = math.Pow(p, 1/float64(count))
}
return tss[:1]
}
func aggrFuncMin(tss []*timeseries) []*timeseries {
if len(tss) == 1 {
// Fast path - nothing to min.
@@ -301,6 +357,9 @@ func aggrFuncCountValues(afa *aggrFuncArg) ([]*timeseries, error) {
m := make(map[float64]bool)
for _, ts := range tss {
for _, v := range ts.Values {
if math.IsNaN(v) {
continue
}
m[v] = true
}
}
@@ -313,7 +372,7 @@ func aggrFuncCountValues(afa *aggrFuncArg) ([]*timeseries, error) {
var rvs []*timeseries
for _, v := range values {
var dst timeseries
dst.CopyFrom(tss[0])
dst.CopyFromShallowTimestamps(tss[0])
dst.MetricName.RemoveTag(dstLabel)
dst.MetricName.AddTag(dstLabel, strconv.FormatFloat(v, 'g', -1, 64))
for i := range dst.Values {
@@ -457,6 +516,7 @@ func newAggrQuantileFunc(phis []float64) func(tss []*timeseries) []*timeseries {
idx := int(math.Round(float64(len(tss)-1) * phi))
dst.Values[n] = tss[idx].Values[n]
}
tss[0] = dst
return tss[:1]
}
}

View File

@@ -0,0 +1,3 @@
package promql
const maxByteSliceLen = 1 << 40

View File

@@ -271,7 +271,7 @@ func adjustBinaryOpTags(be *binaryOpExpr, left, right []*timeseries) ([]*timeser
rvsLeft := make([]*timeseries, len(right))
tsLeft := left[0]
for i, tsRight := range right {
tsRight.MetricName.ResetMetricGroup()
resetMetricGroupIfRequired(be, tsRight)
rvsLeft[i] = tsLeft
}
return rvsLeft, right, right, nil
@@ -281,7 +281,7 @@ func adjustBinaryOpTags(be *binaryOpExpr, left, right []*timeseries) ([]*timeser
rvsRight := make([]*timeseries, len(left))
tsRight := right[0]
for i, tsLeft := range left {
tsLeft.MetricName.ResetMetricGroup()
resetMetricGroupIfRequired(be, tsLeft)
rvsRight[i] = tsRight
}
return left, rvsRight, left, nil
@@ -296,8 +296,13 @@ func adjustBinaryOpTags(be *binaryOpExpr, left, right []*timeseries) ([]*timeser
if len(tss) == 1 {
return nil
}
return fmt.Errorf(`duplicate timeseries on the %s side of %q: %s %s`, side, be.Op, stringMetricTags(&tss[0].MetricName), be.GroupModifier.AppendString(nil))
if mergeNonOverlappingTimeseries(tss) {
return nil
}
return fmt.Errorf(`duplicate timeseries on the %s side of %s %s: %s and %s`, side, be.Op, be.GroupModifier.AppendString(nil),
stringMetricTags(&tss[0].MetricName), stringMetricTags(&tss[1].MetricName))
}
var rvsLeft, rvsRight []*timeseries
mLeft, mRight := createTimeseriesMapByTagSet(be, left, right)
joinOp := strings.ToLower(be.JoinModifier.Op)
@@ -335,7 +340,7 @@ func adjustBinaryOpTags(be *binaryOpExpr, left, right []*timeseries) ([]*timeser
if err := ensureOneX("right", tssRight); err != nil {
return nil, nil, nil, err
}
tssLeft[0].MetricName.ResetMetricGroup()
resetMetricGroupIfRequired(be, tssLeft[0])
rvsLeft = append(rvsLeft, tssLeft[0])
rvsRight = append(rvsRight, tssRight[0])
default:
@@ -349,6 +354,19 @@ func adjustBinaryOpTags(be *binaryOpExpr, left, right []*timeseries) ([]*timeser
return rvsLeft, rvsRight, dst, nil
}
func resetMetricGroupIfRequired(be *binaryOpExpr, ts *timeseries) {
if isBinaryOpCmp(be.Op) && !be.Bool {
// Do not reset MetricGroup for non-boolean `compare` binary ops like Prometheus does.
return
}
switch be.Op {
case "default", "if", "ifnot":
// Do not reset MetricGroup for these ops.
return
}
ts.MetricName.ResetMetricGroup()
}
func binaryOpPlus(left, right float64) float64 {
return left + right
}
@@ -497,3 +515,26 @@ func isScalar(arg []*timeseries) bool {
}
return len(mn.Tags) == 0
}
func mergeNonOverlappingTimeseries(tss []*timeseries) bool {
if len(tss) < 2 {
logger.Panicf("BUG: expecting at least two timeseries. Got %d", len(tss))
}
// Check whether time series in tss overlap.
var dst timeseries
dst.CopyFromShallowTimestamps(tss[0])
dstValues := dst.Values
for _, ts := range tss[1:] {
for i, value := range ts.Values {
if math.IsNaN(dstValues[i]) {
dstValues[i] = value
} else if !math.IsNaN(value) {
// Time series overlap.
return false
}
}
}
tss[0].CopyFromShallowTimestamps(&dst)
return true
}

View File

@@ -16,10 +16,10 @@ import (
)
var (
maxPointsPerTimeseries = flag.Int("search.maxPointsPerTimeseries", 10e3, "The maximum points per a single timeseries returned from the search")
maxPointsPerTimeseries = flag.Int("search.maxPointsPerTimeseries", 30e3, "The maximum points per a single timeseries returned from the search")
)
// The minumum number of points per timeseries for enabling time rounding.
// The minimum number of points per timeseries for enabling time rounding.
// This improves cache hit ratio for frequently requested queries over
// big time ranges.
const minTimeseriesPointsForTimeRounding = 50
@@ -31,7 +31,7 @@ const minTimeseriesPointsForTimeRounding = 50
func ValidateMaxPointsPerTimeseries(start, end, step int64) error {
points := (end-start)/step + 1
if uint64(points) > uint64(*maxPointsPerTimeseries) {
return fmt.Errorf(`too many points for the given step=%d, start=%d and end=%d: %d; cannot exceed %d points`,
return fmt.Errorf(`too many points for the given step=%d, start=%d and end=%d: %d; cannot exceed -search.maxPointsPerTimeseries=%d`,
step, start, end, uint64(points), *maxPointsPerTimeseries)
}
return nil
@@ -379,8 +379,7 @@ func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, re *
}
ecSQ := newEvalConfig(ec)
ecSQ.Start -= window + maxSilenceInterval
ecSQ.End += step
ecSQ.Start -= window + maxSilenceInterval + step
ecSQ.Step = step
if err := ValidateMaxPointsPerTimeseries(ecSQ.Start, ecSQ.End, ecSQ.Step); err != nil {
return nil, err
@@ -472,22 +471,6 @@ func removeNanValues(dstValues []float64, dstTimestamps []int64, values []float6
return dstValues, dstTimestamps
}
func getMaxPointsPerRollup() int {
maxPointsPerRollupOnce.Do(func() {
n := memory.Allowed() / 16 / 8
if n <= 16 {
n = 16
}
maxPointsPerRollup = n
})
return maxPointsPerRollup
}
var (
maxPointsPerRollup int
maxPointsPerRollupOnce sync.Once
)
var (
rollupResultCacheFullHits = metrics.NewCounter(`vm_rollup_result_cache_full_hits_total`)
rollupResultCachePartialHits = metrics.NewCounter(`vm_rollup_result_cache_partial_hits_total`)
@@ -533,12 +516,17 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me
// Verify timeseries fit available memory after the rollup.
// Take into account points from tssCached.
pointsPerTimeseries := 1 + (ec.End-ec.Start)/ec.Step
if uint64(pointsPerTimeseries) > uint64(getMaxPointsPerRollup()/rssLen/len(rcs)) {
rollupPoints := mulNoOverflow(pointsPerTimeseries, int64(rssLen*len(rcs)))
rollupMemorySize := mulNoOverflow(rollupPoints, 16)
rml := getRollupMemoryLimiter()
if !rml.Get(uint64(rollupMemorySize)) {
rss.Cancel()
return nil, fmt.Errorf("cannot process more than %d data points for %d time series with %d points in each time series; "+
"possible solutions are: reducing the number of matching time series; switching to node with more RAM; increasing `step` query arg (%gs)",
getMaxPointsPerRollup(), rssLen*len(rcs), pointsPerTimeseries, float64(ec.Step)/1e3)
return nil, fmt.Errorf("not enough memory for processing %d data points across %d time series with %d points in each time series; "+
"possible solutions are: reducing the number of matching time series; switching to node with more RAM; "+
"increasing -memory.allowedPercent; increasing `step` query arg (%gs)",
rollupPoints, rssLen*len(rcs), pointsPerTimeseries, float64(ec.Step)/1e3)
}
defer rml.Put(uint64(rollupMemorySize))
// Evaluate rollup
tss := make([]*timeseries, 0, rssLen*len(rcs))
@@ -575,6 +563,18 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me
return tss, nil
}
var (
rollupMemoryLimiter memoryLimiter
rollupMemoryLimiterOnce sync.Once
)
func getRollupMemoryLimiter() *memoryLimiter {
rollupMemoryLimiterOnce.Do(func() {
rollupMemoryLimiter.MaxSize = uint64(memory.Allowed()) / 4
})
return &rollupMemoryLimiter
}
func getRollupConfigs(name string, rf rollupFunc, start, end, step, window int64, sharedTimestamps []int64) (func(values []float64, timestamps []int64), []*rollupConfig) {
preFunc := func(values []float64, timestamps []int64) {}
if rollupFuncsRemoveCounterResets[name] {
@@ -584,13 +584,14 @@ func getRollupConfigs(name string, rf rollupFunc, start, end, step, window int64
}
newRollupConfig := func(rf rollupFunc, tagValue string) *rollupConfig {
return &rollupConfig{
TagValue: tagValue,
Func: rf,
Start: start,
End: end,
Step: step,
Window: window,
Timestamps: sharedTimestamps,
TagValue: tagValue,
Func: rf,
Start: start,
End: end,
Step: step,
Window: window,
MayAdjustWindow: rollupFuncsMayAdjustWindow[name],
Timestamps: sharedTimestamps,
}
}
appendRollupConfigs := func(dst []*rollupConfig) []*rollupConfig {
@@ -653,3 +654,11 @@ func evalTime(ec *EvalConfig) []*timeseries {
}
return rv
}
func mulNoOverflow(a, b int64) int64 {
if math.MaxInt64/b < a {
// Overflow
return math.MaxInt64
}
return a * b
}

View File

@@ -1,16 +1,21 @@
package promql
import (
"flag"
"fmt"
"math"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/metrics"
)
var logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging")
// ExpandWithExprs expands WITH expressions inside q and returns the resulting
// PromQL without WITH expressions.
func ExpandWithExprs(q string) (string, error) {
@@ -24,6 +29,16 @@ func ExpandWithExprs(q string) (string, error) {
// Exec executes q for the given ec until the deadline.
func Exec(ec *EvalConfig, q string) ([]netstorage.Result, error) {
if *logSlowQueryDuration > 0 {
startTime := time.Now()
defer func() {
d := time.Since(startTime)
if d >= *logSlowQueryDuration {
logger.Infof("slow query: duration=%s, start=%d, end=%d, step=%d, query=%q", d, ec.Start/1000, ec.End/1000, ec.Step/1000, q)
}
}()
}
ec.validate()
e, err := parsePromQLWithCache(q)

View File

@@ -286,7 +286,7 @@ func TestExecSuccess(t *testing.T) {
q := `time()[300s:100s] offset 100s`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{800, 1000, 1200, 1400, 1600, 1800},
Values: []float64{900, 1100, 1300, 1500, 1700, 1900},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -297,7 +297,7 @@ func TestExecSuccess(t *testing.T) {
q := `time()[1.5i:0.5i] offset 0.5i`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{800, 1000, 1200, 1400, 1600, 1800},
Values: []float64{900, 1100, 1300, 1500, 1700, 1900},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -308,7 +308,7 @@ func TestExecSuccess(t *testing.T) {
q := `time()[300s] offset 100s`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{700, 900, 1100, 1300, 1500, 1700},
Values: []float64{900, 1100, 1300, 1500, 1700, 1900},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -319,7 +319,7 @@ func TestExecSuccess(t *testing.T) {
q := `time()[300s]`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{800, 1000, 1200, 1400, 1600, 1800},
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -789,6 +789,18 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`alias()`, func(t *testing.T) {
t.Parallel()
q := `alias(time(), "foobar")`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.MetricGroup = []byte("foobar")
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`label_set(tag)`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "tagname", "tagvalue")`
@@ -1266,6 +1278,34 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`label_transform(mismatch)`, func(t *testing.T) {
t.Parallel()
q := `label_transform(time(), "__name__", "foobar", "xx")`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`label_transform(match)`, func(t *testing.T) {
t.Parallel()
q := `label_transform(
label_set(time(), "foo", "a.bar.baz"),
"foo", "\\.", "-")`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("a-bar-baz"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`label_replace(mismatch)`, func(t *testing.T) {
t.Parallel()
q := `label_replace(time(), "__name__", "x${1}y", "foo", ".+")`
@@ -1410,6 +1450,62 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected)
})
t.Run(`a cmp scalar (leave MetricGroup)`, func(t *testing.T) {
t.Parallel()
q := `sort_desc((
label_set(time(), "__name__", "foo", "a", "x"),
label_set(time()+200, "__name__", "bar", "a", "x"),
) > 1300)`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{nan, 1400, 1600, 1800, 2000, 2200},
Timestamps: timestampsExpected,
}
r1.MetricName.MetricGroup = []byte("bar")
r1.MetricName.Tags = []storage.Tag{{
Key: []byte("a"),
Value: []byte("x"),
}}
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{nan, nan, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r2.MetricName.MetricGroup = []byte("foo")
r2.MetricName.Tags = []storage.Tag{{
Key: []byte("a"),
Value: []byte("x"),
}}
resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected)
})
t.Run(`a cmp bool scalar (drop MetricGroup)`, func(t *testing.T) {
t.Parallel()
q := `sort_desc((
label_set(time(), "__name__", "foo", "a", "x"),
label_set(time()+200, "__name__", "bar", "a", "y"),
) >= bool 1200)`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1, 1, 1, 1, 1, 1},
Timestamps: timestampsExpected,
}
r1.MetricName.Tags = []storage.Tag{{
Key: []byte("a"),
Value: []byte("y"),
}}
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0, 1, 1, 1, 1, 1},
Timestamps: timestampsExpected,
}
r2.MetricName.Tags = []storage.Tag{{
Key: []byte("a"),
Value: []byte("x"),
}}
resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected)
})
t.Run(`1 > 2`, func(t *testing.T) {
t.Parallel()
q := `1 > 2`
@@ -1512,13 +1608,14 @@ func TestExecSuccess(t *testing.T) {
t.Run(`vector default scalar`, func(t *testing.T) {
t.Parallel()
q := `sort_desc(union(
label_set(time() > 1400, "foo", "bar"),
label_set(time() < 1700, "foo", "baz")) default 123)`
label_set(time() > 1400, "__name__", "x", "foo", "bar"),
label_set(time() < 1700, "__name__", "y", "foo", "baz")) default 123)`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{123, 123, 123, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r1.MetricName.MetricGroup = []byte("x")
r1.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
@@ -1528,6 +1625,7 @@ func TestExecSuccess(t *testing.T) {
Values: []float64{1000, 1200, 1400, 1600, 123, 123},
Timestamps: timestampsExpected,
}
r2.MetricName.MetricGroup = []byte("y")
r2.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("baz"),
@@ -1699,6 +1797,24 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`vector * on(foo) group_left() duplicate_timeseries`, func(t *testing.T) {
t.Parallel()
q := `label_set(time()/10, "foo", "bar") + on(foo) group_left() (
label_set(time() < 1400, "foo", "bar", "op", "le"),
label_set(time() >= 1400, "foo", "bar", "op", "ge"),
)`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1100, 1320, 1540, 1760, 1980, 2200},
Timestamps: timestampsExpected,
}
r1.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
resultExpected := []netstorage.Result{r1}
f(q, resultExpected)
})
t.Run(`vector * on() group_left scalar`, func(t *testing.T) {
t.Parallel()
q := `sort_desc((label_set(time(), "foo", "bar") or label_set(10, "foo", "qwert")) * on() group_left 2)`
@@ -2079,6 +2195,51 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`geomean(time)`, func(t *testing.T) {
t.Parallel()
q := `geomean(time()/100)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{10, 12, 14, 16, 18, 20},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`geomean_over_time(time)`, func(t *testing.T) {
t.Parallel()
q := `round(geomean_over_time(alias(time()/100, "foobar")[3i]), 0.1)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{6.8, 8.8, 10.9, 12.9, 14.9, 16.9},
Timestamps: timestampsExpected,
}
r.MetricName.MetricGroup = []byte("foobar")
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`sum2(time)`, func(t *testing.T) {
t.Parallel()
q := `sum2(time()/100)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{100, 144, 196, 256, 324, 400},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`sum2_over_time(time)`, func(t *testing.T) {
t.Parallel()
q := `sum2_over_time(alias(time()/100, "foobar")[3i])`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{155, 251, 371, 515, 683, 875},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`sum(multi-vector)`, func(t *testing.T) {
t.Parallel()
q := `sum(label_set(10, "foo", "bar") or label_set(time()/100, "baz", "sss"))`
@@ -2090,6 +2251,39 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`geomean(multi-vector)`, func(t *testing.T) {
t.Parallel()
q := `round(geomean(label_set(10, "foo", "bar") or label_set(time()/100, "baz", "sss")), 0.1)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{10, 11, 11.8, 12.6, 13.4, 14.1},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`sum2(multi-vector)`, func(t *testing.T) {
t.Parallel()
q := `sum2(label_set(10, "foo", "bar") or label_set(time()/100, "baz", "sss"))`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{200, 244, 296, 356, 424, 500},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`sqrt(sum2(multi-vector))`, func(t *testing.T) {
t.Parallel()
q := `round(sqrt(sum2(label_set(10, "foo", "bar") or label_set(time()/100, "baz", "sss"))))`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{14, 16, 17, 19, 21, 22},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`avg(multi-vector)`, func(t *testing.T) {
t.Parallel()
q := `avg(label_set(10, "foo", "bar") or label_set(time()/100, "baz", "sss"))`
@@ -2378,7 +2572,7 @@ func TestExecSuccess(t *testing.T) {
q := `distinct_over_time((time() < 1700)[500s])`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{3, 3, 3, 2, 1, nan},
Values: []float64{3, 3, 3, 3, 2, 1},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r1}
@@ -2389,7 +2583,7 @@ func TestExecSuccess(t *testing.T) {
q := `distinct_over_time((time() < 1700)[2.5i])`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{3, 3, 3, 2, 1, nan},
Values: []float64{3, 3, 3, 3, 2, 1},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r1}
@@ -2645,7 +2839,7 @@ func TestExecSuccess(t *testing.T) {
q := `integrate(time()*1e-3)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{200, 240.00000000000003, 280, 320, 360, 400},
Values: []float64{160, 200, 240.00000000000003, 280, 320, 360},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2667,7 +2861,7 @@ func TestExecSuccess(t *testing.T) {
q := `rate(2000-time())`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{4.5, 3.5, 2.5, 1.5, 0.5, -0.5},
Values: []float64{5.5, 4.5, 3.5, 2.5, 1.5, 0.5},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2678,7 +2872,7 @@ func TestExecSuccess(t *testing.T) {
q := `rate((2000-time())[100s])`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{4.5, 3.5, 2.5, 1.5, 0.5, -0.5},
Values: []float64{5.5, 4.5, 3.5, 2.5, 1.5, 0.5},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2689,7 +2883,7 @@ func TestExecSuccess(t *testing.T) {
q := `rate((2000-time())[100s:])`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{4.5, 3.5, 2.5, 1.5, 0.5, -0.5},
Values: []float64{5.5, 4.5, 3.5, 2.5, 1.5, 0.5},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2700,7 +2894,7 @@ func TestExecSuccess(t *testing.T) {
q := `rate((2000-time())[100s:100s])`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{4, 6.5, 4.5, 2.5, 0.5, -1.5},
Values: []float64{5.5, 4.5, 6.5, 4.5, 2.5, 0.5},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2711,7 +2905,7 @@ func TestExecSuccess(t *testing.T) {
q := `rate((2000-time())[100s:100s] offset 100s)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{4.5, 3.5, 5.5, 3.5, 1.5, -0.5},
Values: []float64{6, 5, 7.5, 5.5, 3.5, 1.5},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2722,7 +2916,7 @@ func TestExecSuccess(t *testing.T) {
q := `rate((2000-time())[100s:100s] offset 100s)[:] offset 100s`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{6, 5, 7.5, 5.5, 3.5, 1.5},
Values: []float64{7, 6, 5, 7.5, 5.5, 3.5},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2744,7 +2938,7 @@ func TestExecSuccess(t *testing.T) {
q := `increase(2000-time())`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{900, 700, 500, 300, 100, -100},
Values: []float64{1100, 900, 700, 500, 300, 100},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -3047,7 +3241,7 @@ func TestExecSuccess(t *testing.T) {
q := `sort(rollup(time()[:50s]))`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1050, 1250, 1450, 1650, 1850, 2050},
Values: []float64{850, 1050, 1250, 1450, 1650, 1850},
Timestamps: timestampsExpected,
}
r1.MetricName.Tags = []storage.Tag{{
@@ -3056,21 +3250,21 @@ func TestExecSuccess(t *testing.T) {
}}
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1200, 1400, 1600, 1800, 2000, 2200},
Values: []float64{925, 1125, 1325, 1525, 1725, 1925},
Timestamps: timestampsExpected,
}
r2.MetricName.Tags = []storage.Tag{{
Key: []byte("rollup"),
Value: []byte("max"),
Value: []byte("avg"),
}}
r3 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1125, 1325, 1525, 1725, 1925, 2125},
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r3.MetricName.Tags = []storage.Tag{{
Key: []byte("rollup"),
Value: []byte("avg"),
Value: []byte("max"),
}}
resultExpected := []netstorage.Result{r1, r2, r3}
f(q, resultExpected)
@@ -3461,6 +3655,7 @@ func TestExecError(t *testing.T) {
f(`hour(1,2)`)
f(`label_join()`)
f(`label_replace(1)`)
f(`label_transform(1)`)
f(`label_set()`)
f(`label_set(1, "foo")`)
f(`label_del()`)
@@ -3506,6 +3701,9 @@ func TestExecError(t *testing.T) {
f(`keep_last_value()`)
f(`distinct_over_time()`)
f(`distinct()`)
f(`alias()`)
f(`alias(1)`)
f(`alias(1, "foo", "bar")`)
// Invalid argument type
f(`median_over_time({}, 2)`)
@@ -3535,6 +3733,11 @@ func TestExecError(t *testing.T) {
f(`label_replace(1, "foo", "bar", 4, 5)`)
f(`label_replace(1, "foo", "bar", "baz", 5)`)
f(`label_replace(1, "foo", "bar", "baz", "invalid(regexp")`)
f(`label_transform(1, 2, 3, 4)`)
f(`label_transform(1, "foo", 3, 4)`)
f(`label_transform(1, "foo", "bar", 4)`)
f(`label_transform(1, "foo", "invalid(regexp", "baz`)
f(`alias(1, 2)`)
// Duplicate timeseries
f(`(label_set(1, "foo", "bar") or label_set(2, "foo", "baz"))
@@ -3545,12 +3748,24 @@ func TestExecError(t *testing.T) {
f(`1 + group_left() (label_set(1, "foo", bar"), label_set(2, "foo", "baz"))`)
f(`1 + on() group_left() (label_set(1, "foo", bar"), label_set(2, "foo", "baz"))`)
f(`1 + on(a) group_left(b) (label_set(1, "foo", bar"), label_set(2, "foo", "baz"))`)
f(`label_set(1, "foo", "bar") + on(foo) group_left() (label_set(1, "foo", "bar", "a", "b"), label_set(1, "foo", "bar", "a", "c"))`)
f(`(label_set(1, "foo", bar"), label_set(2, "foo", "baz")) + group_right 1`)
f(`(label_set(1, "foo", bar"), label_set(2, "foo", "baz")) + on() group_right 1`)
f(`(label_set(1, "foo", bar"), label_set(2, "foo", "baz")) + on(a) group_right(b,c) 1`)
f(`(label_set(1, "foo", bar"), label_set(2, "foo", "baz")) + on() 1`)
f(`(label_set(1, "foo", "bar", "a", "b"), label_set(1, "foo", "bar", "a", "c")) + on(foo) group_right() label_set(1, "foo", "bar")`)
f(`1 + on() (label_set(1, "foo", bar"), label_set(2, "foo", "baz"))`)
// duplicate metrics after binary op
f(`(
label_set(time(), "__name__", "foo", "a", "x"),
label_set(time()+200, "__name__", "bar", "a", "x"),
) > bool 1300`)
f(`(
label_set(time(), "__name__", "foo", "a", "x"),
label_set(time()+200, "__name__", "bar", "a", "x"),
) + 10`)
// With expressions
f(`ttf()`)
f(`ttf(1, 2)`)

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"strconv"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
type lexer struct {
@@ -85,10 +87,7 @@ again:
goto tokenFoundLabel
}
if isIdentPrefix(s) {
token, err = scanIdent(s)
if err != nil {
return "", err
}
token = scanIdent(s)
goto tokenFoundLabel
}
if isStringPrefix(s) {
@@ -210,15 +209,103 @@ func scanPositiveNumber(s string) (string, error) {
return s[:j], nil
}
func scanIdent(s string) (string, error) {
if len(s) == 0 {
return "", fmt.Errorf("ident cannot be empty")
}
func scanIdent(s string) string {
i := 0
for i < len(s) && isIdentChar(s[i]) {
i++
for i < len(s) {
if isIdentChar(s[i]) {
i++
continue
}
if s[i] != '\\' {
break
}
// Do not verify the next char, since it is escaped.
i += 2
if i > len(s) {
i--
break
}
}
return s[:i], nil
if i == 0 {
logger.Panicf("BUG: scanIdent couldn't find a single ident char; make sure isIdentPrefix called before scanIdent")
}
return s[:i]
}
func unescapeIdent(s string) string {
n := strings.IndexByte(s, '\\')
if n < 0 {
return s
}
dst := make([]byte, 0, len(s))
for {
dst = append(dst, s[:n]...)
s = s[n+1:]
if len(s) == 0 {
return string(dst)
}
if s[0] == 'x' && len(s) >= 3 {
h1 := fromHex(s[1])
h2 := fromHex(s[2])
if h1 >= 0 && h2 >= 0 {
dst = append(dst, byte((h1<<4)|h2))
s = s[3:]
} else {
dst = append(dst, s[0])
s = s[1:]
}
} else {
dst = append(dst, s[0])
s = s[1:]
}
n = strings.IndexByte(s, '\\')
if n < 0 {
dst = append(dst, s...)
return string(dst)
}
}
}
func fromHex(ch byte) int {
if ch >= '0' && ch <= '9' {
return int(ch - '0')
}
if ch >= 'a' && ch <= 'f' {
return int((ch - 'a') + 10)
}
if ch >= 'A' && ch <= 'F' {
return int((ch - 'A') + 10)
}
return -1
}
func toHex(n byte) byte {
if n < 10 {
return '0' + n
}
return 'a' + (n - 10)
}
func appendEscapedIdent(dst, s []byte) []byte {
for i := 0; i < len(s); i++ {
ch := s[i]
if isIdentChar(ch) {
if i == 0 && !isFirstIdentChar(ch) {
// hex-encode the first char
dst = append(dst, '\\', 'x', toHex(ch>>4), toHex(ch&0xf))
} else {
dst = append(dst, ch)
}
} else if ch >= 0x20 && ch < 0x7f {
// Leave ASCII printable chars as is
dst = append(dst, '\\', ch)
} else {
// hex-encode non-printable chars
dst = append(dst, '\\', 'x', toHex(ch>>4), toHex(ch&0xf))
}
}
return dst
}
func (lex *lexer) Prev() {
@@ -353,6 +440,10 @@ func isIdentPrefix(s string) bool {
if len(s) == 0 {
return false
}
if s[0] == '\\' {
// Assume this is an escape char for the next char.
return true
}
return isFirstIdentChar(s[0])
}
@@ -367,7 +458,7 @@ func isIdentChar(ch byte) bool {
if isFirstIdentChar(ch) {
return true
}
return isDecimalChar(ch) || ch == ':' || ch == '.'
return isDecimalChar(ch) || ch == '.'
}
func isSpaceChar(ch byte) bool {

View File

@@ -5,6 +5,57 @@ import (
"testing"
)
func TestUnescapeIdent(t *testing.T) {
f := func(s, resultExpected string) {
t.Helper()
result := unescapeIdent(s)
if result != resultExpected {
t.Fatalf("unexpected result for unescapeIdent(%q); got %q; want %q", s, result, resultExpected)
}
}
f("", "")
f("a", "a")
f("\\", "")
f(`\\`, `\`)
f(`\foo\-bar`, `foo-bar`)
f(`a\\\\b\"c\d`, `a\\b"cd`)
f(`foo.bar:baz_123`, `foo.bar:baz_123`)
f(`foo\ bar`, `foo bar`)
f(`\x21`, `!`)
f(`\xeDfoo\x2Fbar\-\xqw\x`, "\xedfoo\x2fbar-xqwx")
}
func TestAppendEscapedIdent(t *testing.T) {
f := func(s, resultExpected string) {
t.Helper()
result := appendEscapedIdent(nil, []byte(s))
if string(result) != resultExpected {
t.Fatalf("unexpected result for appendEscapedIdent(%q); got %q; want %q", s, result, resultExpected)
}
}
f(`a`, `a`)
f(`a.b:c_23`, `a.b:c_23`)
f(`a b-cd+dd\`, `a\ b\-cd\+dd\\`)
f("a\x1E\x20\xee", `a\x1e\ \xee`)
f("\x2e\x2e", `\x2e.`)
}
func TestScanIdent(t *testing.T) {
f := func(s, resultExpected string) {
t.Helper()
result := scanIdent(s)
if result != resultExpected {
t.Fatalf("unexpected result for scanIdent(%q): got %q; want %q", s, result, resultExpected)
}
}
f("a", "a")
f("foo.bar:baz_123", "foo.bar:baz_123")
f("a+b", "a")
f("foo()", "foo")
f(`a\-b+c`, `a\-b`)
f(`a\ b\\\ c\`, `a\ b\\\ c\`)
}
func TestLexerNextPrev(t *testing.T) {
var lex lexer
lex.Init("foo bar baz")

View File

@@ -0,0 +1,33 @@
package promql
import (
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
type memoryLimiter struct {
MaxSize uint64
mu sync.Mutex
usage uint64
}
func (ml *memoryLimiter) Get(n uint64) bool {
ml.mu.Lock()
ok := n <= ml.MaxSize && ml.MaxSize-n >= ml.usage
if ok {
ml.usage += n
}
ml.mu.Unlock()
return ok
}
func (ml *memoryLimiter) Put(n uint64) {
ml.mu.Lock()
if n > ml.usage {
logger.Panicf("BUG: n=%d cannot exceed %d", n, ml.usage)
}
ml.usage -= n
ml.mu.Unlock()
}

View File

@@ -0,0 +1,56 @@
package promql
import (
"testing"
)
func TestMemoryLimiter(t *testing.T) {
var ml memoryLimiter
ml.MaxSize = 100
// Allocate memory
if !ml.Get(10) {
t.Fatalf("cannot get 10 out of %d bytes", ml.MaxSize)
}
if ml.usage != 10 {
t.Fatalf("unexpected usage; got %d; want %d", ml.usage, 10)
}
if !ml.Get(20) {
t.Fatalf("cannot get 20 out of 90 bytes")
}
if ml.usage != 30 {
t.Fatalf("unexpected usage; got %d; want %d", ml.usage, 30)
}
if ml.Get(1000) {
t.Fatalf("unexpected get for 1000 bytes")
}
if ml.usage != 30 {
t.Fatalf("unexpected usage; got %d; want %d", ml.usage, 30)
}
if ml.Get(71) {
t.Fatalf("unexpected get for 71 bytes")
}
if ml.usage != 30 {
t.Fatalf("unexpected usage; got %d; want %d", ml.usage, 30)
}
if !ml.Get(70) {
t.Fatalf("cannot get 70 bytes")
}
if ml.usage != 100 {
t.Fatalf("unexpected usage; got %d; want %d", ml.usage, 100)
}
// Return memory back
ml.Put(10)
ml.Put(70)
if ml.usage != 20 {
t.Fatalf("unexpected usage; got %d; want %d", ml.usage, 20)
}
if !ml.Get(30) {
t.Fatalf("cannot get 30 bytes")
}
ml.Put(50)
if ml.usage != 0 {
t.Fatalf("unexpected usage; got %d; want %d", ml.usage, 0)
}
}

View File

@@ -6,7 +6,6 @@ import (
"strings"
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
@@ -19,12 +18,13 @@ func getDefaultWithArgExprs() []*withArgExpr {
// ttf - time to fuckup
`ttf(freev) = smooth_exponential(
clamp_max(clamp_min(freev, 0) / clamp_min(deriv(-freev), 0), 365*24*3600),
clamp_max(clamp_max(-freev, 0) / clamp_max(deriv_fast(freev), 0), 365*24*3600),
clamp_max(step()/300, 1)
)`,
`median_over_time(m) = quantile_over_time(0.5, m)`,
`range_median(q) = range_quantile(0.5, q)`,
`alias(q, name) = label_set(q, "__name__", name)`,
})
})
return defaultWithArgExprs
@@ -744,7 +744,7 @@ func expandWithExpr(was []*withArgExpr, e expr) (expr, error) {
if !t.HasNonEmptyMetricGroup() {
return t, nil
}
k := bytesutil.ToUnsafeString(t.TagFilters[0].Value)
k := string(appendEscapedIdent(nil, t.TagFilters[0].Value))
wa := getWithArgExpr(was, k)
if wa == nil {
return t, nil
@@ -811,7 +811,9 @@ func expandModifierArgs(was []*withArgExpr, args []string) ([]string, error) {
continue
}
if len(wa.Args) > 0 {
return nil, fmt.Errorf("cannot use func %q instead of %q in %s", wa.Name, arg, args)
// Template funcs cannot be used inside modifier list. Leave the arg as is.
dstArgs = append(dstArgs, arg)
continue
}
me, ok := wa.Expr.(*metricExpr)
if ok {
@@ -851,6 +853,10 @@ func expandModifierArgs(was []*withArgExpr, args []string) ([]string, error) {
func expandWithExprExt(was []*withArgExpr, wa *withArgExpr, args []expr) (expr, error) {
if len(wa.Args) != len(args) {
if args == nil {
// Just return metricExpr with the wa.Name name.
return newMetricExpr(wa.Name), nil
}
return nil, fmt.Errorf("invalid number of args for %q; got %d; want %d", wa.Name, len(args), len(wa.Args))
}
wasNew := make([]*withArgExpr, 0, len(was)+len(args))
@@ -869,6 +875,14 @@ func expandWithExprExt(was []*withArgExpr, wa *withArgExpr, args []expr) (expr,
return expandWithExpr(wasNew, wa.Expr)
}
func newMetricExpr(name string) *metricExpr {
return &metricExpr{
TagFilters: []storage.TagFilter{{
Value: []byte(name),
}},
}
}
func extractStringValue(token string) (string, error) {
if !isStringPrefix(token) {
return "", fmt.Errorf(`stringExpr must contain only string literals; got %q`, token)
@@ -1074,9 +1088,6 @@ func (p *parser) parseTagFilterExpr() (*tagFilterExpr, error) {
}
var tfe tagFilterExpr
tfe.Key = p.lex.Token
if tfe.Key == "__name__" {
tfe.Key = ""
}
if err := p.lex.Next(); err != nil {
return nil, err
}
@@ -1125,8 +1136,16 @@ func (tfe *tagFilterExpr) toTagFilter() (*storage.TagFilter, error) {
}
var tf storage.TagFilter
tf.Key = []byte(tfe.Key)
tf.Value = []byte(tfe.Value.S)
tf.Key = []byte(unescapeIdent(tfe.Key))
if len(tfe.Key) == 0 {
tf.Value = []byte(unescapeIdent(tfe.Value.S))
} else {
tf.Value = []byte(tfe.Value.S)
}
if string(tf.Key) == "__name__" {
// This is required for storage.Search
tf.Key = nil
}
tf.IsRegexp = tfe.IsRegexp
tf.IsNegative = tfe.IsNegative
if !tf.IsRegexp {
@@ -1507,7 +1526,7 @@ func (wa *withArgExpr) AppendString(dst []byte) []byte {
}
type rollupExpr struct {
// The expression for the rollup. Usually it is metricExpr, but may be arbitary expr
// The expression for the rollup. Usually it is metricExpr, but may be arbitrary expr
// if subquery is used. https://prometheus.io/blog/2019/01/28/subquery-support/
Expr expr
@@ -1585,7 +1604,7 @@ func (me *metricExpr) AppendString(dst []byte) []byte {
if len(tfs) > 0 {
tf := &tfs[0]
if len(tf.Key) == 0 && !tf.IsNegative && !tf.IsRegexp {
dst = append(dst, tf.Value...)
dst = appendEscapedIdent(dst, tf.Value)
tfs = tfs[1:]
}
}
@@ -1627,7 +1646,7 @@ func appendStringTagFilter(dst []byte, tf *storage.TagFilter) []byte {
if len(tf.Key) == 0 {
dst = append(dst, "__name__"...)
} else {
dst = append(dst, tf.Key...)
dst = appendEscapedIdent(dst, tf.Key)
}
var op string
if tf.IsNegative {

View File

@@ -118,6 +118,17 @@ func TestParsePromQLSuccess(t *testing.T) {
same("with")
same("WITH")
same("With")
same("alias")
same(`alias{foo="bar"}`)
same(`aLIas{alias="aa"}`)
another(`al\ias`, `alias`)
// identifiers with with escape chars
same(`foo\ bar`)
same(`foo\-bar\{{baz\+bar="aa"}`)
another(`\x2E\x2ef\oo{b\xEF\ar="aa"}`, `\x2e.foo{b\xefar="aa"}`)
// Duplicate filters
same(`foo{__name__="bar"}`)
same(`foo{a="b", a="c", __name__="aaa", b="d"}`)
// Metric filters ending with comma
another(`m{foo="bar",}`, `m{foo="bar"}`)
// String concat in tag value
@@ -251,6 +262,8 @@ func TestParsePromQLSuccess(t *testing.T) {
same(`rate(rate(m[5m]))`)
same(`rate(rate(m[5m])[1h:])`)
same(`rate(rate(m[5m])[1h:3s])`)
// funcName with escape chars
same(`foo\(ba\-r()`)
// aggrFuncExpr
same(`sum(http_server_request) by ()`)
@@ -295,10 +308,14 @@ func TestParsePromQLSuccess(t *testing.T) {
another(`with (ct={job="test", i="bar"}) ct + {ct, x="d"} + foo{ct, ct} + ctx(1)`,
`(({job="test", i="bar"} + {job="test", i="bar", x="d"}) + foo{job="test", i="bar"}) + ctx(1)`)
another(`with (foo = bar) {__name__=~"foo"}`, `{__name__=~"foo"}`)
another(`with (foo = bar) {__name__="foo"}`, `bar`)
another(`with (foo = bar) foo{__name__="foo"}`, `bar`)
another(`with (foo = bar) {__name__="foo", x="y"}`, `bar{x="y"}`)
another(`with (foo(bar) = {__name__!="bar"}) foo(x)`, `{__name__!="bar"}`)
another(`with (foo(bar) = {__name__="bar"}) foo(x)`, `x`)
another(`with (foo(bar) = bar{__name__="bar"}) foo(x)`, `x`)
another(`with (foo\-bar(baz) = baz + baz) foo\-bar((x,y))`, `(x, y) + (x, y)`)
another(`with (foo\-bar(baz) = baz + baz) foo\-bar(x*y)`, `(x * y) + (x * y)`)
another(`with (foo\-bar(baz) = baz + baz) foo\-bar(x\*y)`, `x\*y + x\*y`)
another(`with (foo\-bar(b\ az) = b\ az + b\ az) foo\-bar(x\*y)`, `x\*y + x\*y`)
// override ttf to something new.
another(`with (ttf = a) ttf + b`, `a + b`)
// override ttf to ru
@@ -332,8 +349,11 @@ func TestParsePromQLSuccess(t *testing.T) {
another(`with (x="a", y=x) y+"bc"`, `"abc"`)
another(`with (x="a", y="b"+x) "we"+y+"z"+f()`, `"webaz" + f()`)
another(`with (f(x) = m{foo=x+"y", bar="y"+x, baz=x} + x) f("qwe")`, `m{foo="qwey", bar="yqwe", baz="qwe"} + "qwe"`)
another(`with (f(a)=a) f`, `f`)
another(`with (f\q(a)=a) f\q`, `fq`)
// Verify withExpr for aggr func modifiers
another(`with (f(x) = x, y = sum(m) by (f)) y`, `sum(m) by (f)`)
another(`with (f(x) = sum(m) by (x)) f(foo)`, `sum(m) by (foo)`)
another(`with (f(x) = sum(m) by (x)) f((foo, bar, foo))`, `sum(m) by (foo, bar)`)
another(`with (f(x) = sum(m) without (x,y)) f((a, b))`, `sum(m) without (a, b, y)`)
@@ -658,7 +678,7 @@ func TestParsePromQLError(t *testing.T) {
f(`with (x=m) f(b, a{x})`)
f(`with (x=m) sum(a{x})`)
f(`with (x=m) (a{x})`)
f(`with (f(a)=a) f`)
f(`with (f(a)=a) f(1, 2)`)
f(`with (f(x)=x{foo="bar"}) f(1)`)
f(`with (f(x)=x{foo="bar"}) f(m + n)`)
f(`with (f = with`)
@@ -668,8 +688,7 @@ func TestParsePromQLError(t *testing.T) {
f(`with (f(,)=x) x`)
f(`with (x(a) = {b="c"}) foo{x}`)
f(`with (f(x) = m{foo=xx}) f("qwe")`)
f(`a + with(f(x)=x) f`)
f(`with (f(x) = x, y = sum(m) by (f)) y`)
f(`a + with(f(x)=x) f(1,2)`)
f(`with (f(x) = sum(m) by (x)) f({foo="bar"})`)
f(`with (f(x) = sum(m) by (x)) f((xx(), {foo="bar"}))`)
f(`with (f(x) = m + on (x) n) f(xx())`)

View File

@@ -1,7 +1,6 @@
package promql
import (
"fmt"
"regexp"
"sync"
"sync/atomic"
@@ -10,12 +9,16 @@ import (
)
func compileRegexpAnchored(re string) (*regexp.Regexp, error) {
reAnchored := "^(?:" + re + ")$"
return compileRegexp(reAnchored)
}
func compileRegexp(re string) (*regexp.Regexp, error) {
rcv := regexpCacheV.Get(re)
if rcv != nil {
return rcv.r, rcv.err
}
regexAnchored := fmt.Sprintf("^(?:%s)$", re)
r, err := regexp.Compile(regexAnchored)
r, err := regexp.Compile(re)
rcv = &regexpCacheValue{
r: r,
err: err,
@@ -77,7 +80,7 @@ func (rc *regexpCache) Get(regexp string) *regexpCacheValue {
rcv := rc.m[regexp]
rc.mu.RUnlock()
if rc == nil {
if rcv == nil {
atomic.AddUint64(&rc.misses, 1)
}
return rcv

View File

@@ -19,13 +19,14 @@ var rollupFuncs = map[string]newRollupFunc{
// See funcs accepting range-vector on https://prometheus.io/docs/prometheus/latest/querying/functions/ .
"changes": newRollupFuncOneArg(rollupChanges),
"delta": newRollupFuncOneArg(rollupDelta),
"deriv": newRollupFuncOneArg(rollupDeriv),
"deriv": newRollupFuncOneArg(rollupDerivSlow),
"deriv_fast": newRollupFuncOneArg(rollupDerivFast),
"holt_winters": newRollupHoltWinters,
"idelta": newRollupFuncOneArg(rollupIdelta),
"increase": newRollupFuncOneArg(rollupDelta), // + rollupFuncsRemoveCounterResets
"irate": newRollupFuncOneArg(rollupIderiv), // + rollupFuncsRemoveCounterResets
"predict_linear": newRollupPredictLinear,
"rate": newRollupFuncOneArg(rollupDeriv), // + rollupFuncsRemoveCounterResets
"rate": newRollupFuncOneArg(rollupDerivFast), // + rollupFuncsRemoveCounterResets
"resets": newRollupFuncOneArg(rollupResets),
"avg_over_time": newRollupFuncOneArg(rollupAvg),
"min_over_time": newRollupFuncOneArg(rollupMin),
@@ -37,6 +38,8 @@ var rollupFuncs = map[string]newRollupFunc{
"stdvar_over_time": newRollupFuncOneArg(rollupStdvar),
// Additional rollup funcs.
"sum2_over_time": newRollupFuncOneArg(rollupSum2),
"geomean_over_time": newRollupFuncOneArg(rollupGeomean),
"first_over_time": newRollupFuncOneArg(rollupFirst),
"last_over_time": newRollupFuncOneArg(rollupLast),
"distinct_over_time": newRollupFuncOneArg(rollupDistinct),
@@ -49,6 +52,13 @@ var rollupFuncs = map[string]newRollupFunc{
"rollup_increase": newRollupFuncOneArg(rollupFake), // + rollupFuncsRemoveCounterResets
}
var rollupFuncsMayAdjustWindow = map[string]bool{
"deriv": true,
"deriv_fast": true,
"irate": true,
"rate": true,
}
var rollupFuncsRemoveCounterResets = map[string]bool{
"increase": true,
"irate": true,
@@ -64,6 +74,7 @@ var rollupFuncsKeepMetricGroup = map[string]bool{
"max_over_time": true,
"quantile_over_time": true,
"rollup": true,
"geomean_over_time": true,
}
func getRollupArgIdx(funcName string) int {
@@ -120,6 +131,13 @@ type rollupConfig struct {
Step int64
Window int64
// Whether window may be adjusted to 2 x interval between data points.
// This is needed for functions which have dt in the denominator
// such as rate, deriv, etc.
// Without the adjustement their value would jump in unexpected directions
// when using window smaller than 2 x scrape_interval.
MayAdjustWindow bool
Timestamps []int64
}
@@ -162,7 +180,7 @@ func (rc *rollupConfig) Do(dstValues []float64, values []float64, timestamps []i
if window <= 0 {
window = rc.Step
}
if window < maxPrevInterval {
if rc.MayAdjustWindow && window < maxPrevInterval {
window = maxPrevInterval
}
rfa := getRollupFuncArg()
@@ -171,8 +189,7 @@ func (rc *rollupConfig) Do(dstValues []float64, values []float64, timestamps []i
i := 0
j := 0
for _, ts := range rc.Timestamps {
tEnd := ts + rc.Step
for _, tEnd := range rc.Timestamps {
tStart := tEnd - window
n := sort.Search(len(timestamps)-i, func(n int) bool {
return timestamps[i+n] > tStart
@@ -296,11 +313,11 @@ func newRollupHoltWinters(args []interface{}) (rollupFunc, error) {
return nil, err
}
rf := func(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue
}
sf := sfs[rfa.idx]
if sf <= 0 || sf >= 1 {
@@ -342,41 +359,55 @@ func newRollupPredictLinear(args []interface{}) (rollupFunc, error) {
return nil, err
}
rf := func(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// before calling rollup funcs.
values := rfa.values
timestamps := rfa.timestamps
if len(values) == 0 {
v, k := linearRegression(rfa)
if math.IsNaN(v) {
return nan
}
// See https://en.wikipedia.org/wiki/Simple_linear_regression#Numerical_example
// TODO: determine whether this shit really works.
tFirst := rfa.prevTimestamp
vSum := rfa.prevValue
if math.IsNaN(rfa.prevValue) {
tFirst = timestamps[0]
vSum = 0
}
tSum := float64(0)
tvSum := float64(0)
ttSum := float64(0)
for i, v := range values {
dt := float64(timestamps[i]-tFirst) * 1e-3
vSum += v
tSum += dt
tvSum += dt * v
ttSum += dt * dt
}
n := float64(len(values))
k := (n*tvSum - tSum*vSum) / (n*ttSum - tSum*tSum)
v := (vSum - k*tSum) / n
sec := secs[rfa.idx]
return v + k*sec
}
return rf, nil
}
func linearRegression(rfa *rollupFuncArg) (float64, float64) {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
timestamps := rfa.timestamps
if len(values) == 0 {
return rfa.prevValue, 0
}
// See https://en.wikipedia.org/wiki/Simple_linear_regression#Numerical_example
tFirst := rfa.prevTimestamp
vSum := rfa.prevValue
tSum := float64(0)
tvSum := float64(0)
ttSum := float64(0)
n := 1.0
if math.IsNaN(rfa.prevValue) {
tFirst = timestamps[0]
vSum = 0
n = 0
}
for i, v := range values {
dt := float64(timestamps[i]-tFirst) * 1e-3
vSum += v
tSum += dt
tvSum += dt * v
ttSum += dt * dt
}
n += float64(len(values))
if n == 1 {
return vSum, 0
}
k := (n*tvSum - tSum*vSum) / (n*ttSum - tSum*tSum)
v := (vSum - k*tSum) / n
// Adjust v to the last timestamp on the given time range.
v += k * (float64(timestamps[len(timestamps)-1]-tFirst) * 1e-3)
return v, k
}
func newRollupQuantile(args []interface{}) (rollupFunc, error) {
if err := expectRollupArgsNum(args, 2); err != nil {
return nil, err
@@ -386,11 +417,15 @@ func newRollupQuantile(args []interface{}) (rollupFunc, error) {
return nil, err
}
rf := func(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue
}
if len(values) == 1 {
// Fast path - only a single value.
return values[0]
}
hf := histogram.GetFast()
for _, v := range values {
@@ -408,11 +443,11 @@ func rollupAvg(rfa *rollupFuncArg) float64 {
// Do not use `Rapid calculation methods` at https://en.wikipedia.org/wiki/Standard_deviation,
// since it is slower and has no significant benefits in precision.
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue
}
var sum float64
for _, v := range values {
@@ -422,11 +457,11 @@ func rollupAvg(rfa *rollupFuncArg) float64 {
}
func rollupMin(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue
}
minValue := values[0]
for _, v := range values {
@@ -438,11 +473,11 @@ func rollupMin(rfa *rollupFuncArg) float64 {
}
func rollupMax(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue
}
maxValue := values[0]
for _, v := range values {
@@ -454,11 +489,11 @@ func rollupMax(rfa *rollupFuncArg) float64 {
}
func rollupSum(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue
}
var sum float64
for _, v := range values {
@@ -467,12 +502,43 @@ func rollupSum(rfa *rollupFuncArg) float64 {
return sum
}
func rollupCount(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
func rollupSum2(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue * rfa.prevValue
}
var sum2 float64
for _, v := range values {
sum2 += v * v
}
return sum2
}
func rollupGeomean(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return rfa.prevValue
}
p := 1.0
for _, v := range values {
p *= v
}
return math.Pow(p, 1/float64(len(values)))
}
func rollupCount(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
if math.IsNaN(rfa.prevValue) {
return nan
}
return 0
}
return float64(len(values))
}
@@ -485,11 +551,18 @@ func rollupStddev(rfa *rollupFuncArg) float64 {
func rollupStdvar(rfa *rollupFuncArg) float64 {
// See `Rapid calculation methods` at https://en.wikipedia.org/wiki/Standard_deviation
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
if math.IsNaN(rfa.prevValue) {
return nan
}
return 0
}
if len(values) == 1 {
// Fast path.
return values[0]
}
var avg float64
var count float64
@@ -504,7 +577,7 @@ func rollupStdvar(rfa *rollupFuncArg) float64 {
}
func rollupDelta(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
prevValue := rfa.prevValue
@@ -516,32 +589,42 @@ func rollupDelta(rfa *rollupFuncArg) float64 {
values = values[1:]
}
if len(values) == 0 {
return nan
return 0
}
return values[len(values)-1] - prevValue
}
func rollupIdelta(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
if math.IsNaN(rfa.prevValue) {
return nan
}
return 0
}
lastValue := values[len(values)-1]
values = values[:len(values)-1]
if len(values) == 0 {
prevValue := rfa.prevValue
if math.IsNaN(prevValue) {
return nan
return 0
}
return lastValue - prevValue
}
return lastValue - values[len(values)-1]
}
func rollupDeriv(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
func rollupDerivSlow(rfa *rollupFuncArg) float64 {
// Use linear regression like Prometheus does.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/73
_, k := linearRegression(rfa)
return k
}
func rollupDerivFast(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
timestamps := rfa.timestamps
@@ -557,7 +640,7 @@ func rollupDeriv(rfa *rollupFuncArg) float64 {
timestamps = timestamps[1:]
}
if len(values) == 0 {
return nan
return 0
}
vEnd := values[len(values)-1]
tEnd := timestamps[len(timestamps)-1]
@@ -567,12 +650,15 @@ func rollupDeriv(rfa *rollupFuncArg) float64 {
}
func rollupIderiv(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
timestamps := rfa.timestamps
if len(values) == 0 {
return nan
if math.IsNaN(rfa.prevValue) {
return nan
}
return 0
}
vEnd := values[len(values)-1]
tEnd := timestamps[len(timestamps)-1]
@@ -582,7 +668,7 @@ func rollupIderiv(rfa *rollupFuncArg) float64 {
prevTimestamp := rfa.prevTimestamp
if len(values) == 0 {
if math.IsNaN(prevValue) {
return nan
return 0
}
} else {
prevValue = values[len(values)-1]
@@ -594,16 +680,18 @@ func rollupIderiv(rfa *rollupFuncArg) float64 {
}
func rollupChanges(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
}
n := 0
prevValue := rfa.prevValue
n := 0
if math.IsNaN(prevValue) {
if len(values) == 0 {
return nan
}
prevValue = values[0]
values = values[1:]
n++
}
for _, v := range values {
if v != prevValue {
@@ -615,11 +703,14 @@ func rollupChanges(rfa *rollupFuncArg) float64 {
}
func rollupResets(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
if math.IsNaN(rfa.prevValue) {
return nan
}
return 0
}
prevValue := rfa.prevValue
if math.IsNaN(prevValue) {
@@ -627,7 +718,7 @@ func rollupResets(rfa *rollupFuncArg) float64 {
values = values[1:]
}
if len(values) == 0 {
return nan
return 0
}
n := 0
for _, v := range values {
@@ -646,7 +737,7 @@ func rollupFirst(rfa *rollupFuncArg) float64 {
return v
}
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
@@ -655,24 +746,27 @@ func rollupFirst(rfa *rollupFuncArg) float64 {
return values[0]
}
var rollupDefault = rollupFirst
var rollupDefault = rollupLast
func rollupLast(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
return rfa.prevValue
}
return values[len(values)-1]
}
func rollupDistinct(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
if len(values) == 0 {
return nan
if math.IsNaN(rfa.prevValue) {
return nan
}
return 0
}
m := make(map[float64]struct{})
for _, v := range values {
@@ -684,12 +778,15 @@ func rollupDistinct(rfa *rollupFuncArg) float64 {
func rollupIntegrate(rfa *rollupFuncArg) float64 {
prevTimestamp := rfa.prevTimestamp
// There is no need in handling NaNs here, since they must be cleanup up
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
timestamps := rfa.timestamps
if len(values) == 0 {
return nan
if math.IsNaN(rfa.prevValue) {
return nan
}
return 0
}
prevValue := rfa.prevValue
if math.IsNaN(prevValue) {
@@ -699,7 +796,7 @@ func rollupIntegrate(rfa *rollupFuncArg) float64 {
timestamps = timestamps[1:]
}
if len(values) == 0 {
return nan
return 0
}
var sum float64

View File

@@ -2,6 +2,7 @@ package promql
import (
"crypto/rand"
"flag"
"fmt"
"runtime"
"sync"
@@ -15,6 +16,8 @@ import (
"github.com/VictoriaMetrics/metrics"
)
var disableCache = flag.Bool("search.disableCache", false, "Whether to disable response caching. This may be useful during data backfilling")
var rollupResultCacheV = &rollupResultCache{
fastcache.New(1024 * 1024), // This is a cache for testing.
}
@@ -47,6 +50,10 @@ func InitRollupResultCache(cachePath string) {
} else {
c = fastcache.New(getRollupResultCacheSize())
}
if *disableCache {
c.Reset()
}
stats := &fastcache.Stats{}
var statsLock sync.Mutex
var statsLastUpdate time.Time
@@ -119,7 +126,7 @@ func ResetRollupResultCache() {
}
func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExpr, window int64) (tss []*timeseries, newStart int64) {
if !ec.mayCache() {
if *disableCache || !ec.mayCache() {
return nil, ec.Start
}
@@ -190,7 +197,7 @@ func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExp
}
func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExpr, window int64, tss []*timeseries) {
if len(tss) == 0 || !ec.mayCache() {
if *disableCache || len(tss) == 0 || !ec.mayCache() {
return
}

View File

@@ -39,7 +39,7 @@ func TestRollupResultCache(t *testing.T) {
t.Run("start-overlap", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{800, 1000, 1200},
Values: []float64{0, 1, 2},
},
@@ -50,7 +50,7 @@ func TestRollupResultCache(t *testing.T) {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1400)
}
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{1, 2},
},
@@ -62,7 +62,7 @@ func TestRollupResultCache(t *testing.T) {
t.Run("end-overlap", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{1800, 2000, 2200, 2400},
Values: []float64{333, 0, 1, 2},
},
@@ -81,7 +81,7 @@ func TestRollupResultCache(t *testing.T) {
t.Run("full-cover", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{1200, 1400, 1600},
Values: []float64{0, 1, 2},
},
@@ -100,7 +100,7 @@ func TestRollupResultCache(t *testing.T) {
t.Run("before-start", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{200, 400, 600},
Values: []float64{0, 1, 2},
},
@@ -119,7 +119,7 @@ func TestRollupResultCache(t *testing.T) {
t.Run("after-end", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{2200, 2400, 2600},
Values: []float64{0, 1, 2},
},
@@ -138,7 +138,7 @@ func TestRollupResultCache(t *testing.T) {
t.Run("bigger-than-start-end", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{800, 1000, 1200, 1400, 1600, 1800, 2000, 2200},
Values: []float64{0, 1, 2, 3, 4, 5, 6, 7},
},
@@ -149,7 +149,7 @@ func TestRollupResultCache(t *testing.T) {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 2200)
}
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{1, 2, 3, 4, 5, 6},
},
@@ -161,7 +161,7 @@ func TestRollupResultCache(t *testing.T) {
t.Run("start-end-match", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{1, 2, 3, 4, 5, 6},
},
@@ -172,7 +172,7 @@ func TestRollupResultCache(t *testing.T) {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 2200)
}
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{1, 2, 3, 4, 5, 6},
},
@@ -203,19 +203,19 @@ func TestRollupResultCache(t *testing.T) {
t.Run("multi-timeseries", func(t *testing.T) {
ResetRollupResultCache()
tss1 := []*timeseries{
&timeseries{
{
Timestamps: []int64{800, 1000, 1200},
Values: []float64{0, 1, 2},
},
}
tss2 := []*timeseries{
&timeseries{
{
Timestamps: []int64{1800, 2000, 2200, 2400},
Values: []float64{333, 0, 1, 2},
},
}
tss3 := []*timeseries{
&timeseries{
{
Timestamps: []int64{1200, 1400, 1600},
Values: []float64{0, 1, 2},
},
@@ -228,7 +228,7 @@ func TestRollupResultCache(t *testing.T) {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1400)
}
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{1, 2},
},
@@ -249,14 +249,14 @@ func TestMergeTimeseries(t *testing.T) {
t.Run("bStart=ec.Start", func(t *testing.T) {
a := []*timeseries{}
b := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{1, 2, 3, 4, 5, 6},
},
}
tss := mergeTimeseries(a, b, 1000, ec)
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{1, 2, 3, 4, 5, 6},
},
@@ -266,14 +266,14 @@ func TestMergeTimeseries(t *testing.T) {
t.Run("a-empty", func(t *testing.T) {
a := []*timeseries{}
b := []*timeseries{
&timeseries{
{
Timestamps: []int64{1400, 1600, 1800, 2000},
Values: []float64{3, 4, 5, 6},
},
}
tss := mergeTimeseries(a, b, bStart, ec)
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{nan, nan, 3, 4, 5, 6},
},
@@ -282,7 +282,7 @@ func TestMergeTimeseries(t *testing.T) {
})
t.Run("b-empty", func(t *testing.T) {
a := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{2, 1},
},
@@ -290,7 +290,7 @@ func TestMergeTimeseries(t *testing.T) {
b := []*timeseries{}
tss := mergeTimeseries(a, b, bStart, ec)
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{2, 1, nan, nan, nan, nan},
},
@@ -299,20 +299,20 @@ func TestMergeTimeseries(t *testing.T) {
})
t.Run("non-empty", func(t *testing.T) {
a := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{2, 1},
},
}
b := []*timeseries{
&timeseries{
{
Timestamps: []int64{1400, 1600, 1800, 2000},
Values: []float64{3, 4, 5, 6},
},
}
tss := mergeTimeseries(a, b, bStart, ec)
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{2, 1, 3, 4, 5, 6},
},
@@ -321,14 +321,14 @@ func TestMergeTimeseries(t *testing.T) {
})
t.Run("non-empty-distinct-metric-names", func(t *testing.T) {
a := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{2, 1},
},
}
a[0].MetricName.MetricGroup = []byte("bar")
b := []*timeseries{
&timeseries{
{
Timestamps: []int64{1400, 1600, 1800, 2000},
Values: []float64{3, 4, 5, 6},
},
@@ -336,14 +336,14 @@ func TestMergeTimeseries(t *testing.T) {
b[0].MetricName.MetricGroup = []byte("foo")
tss := mergeTimeseries(a, b, bStart, ec)
tssExpected := []*timeseries{
&timeseries{
{
MetricName: storage.MetricName{
MetricGroup: []byte("foo"),
},
Timestamps: []int64{1000, 1200, 1400, 1600, 1800, 2000},
Values: []float64{nan, nan, 3, 4, 5, 6},
},
&timeseries{
{
MetricName: storage.MetricName{
MetricGroup: []byte("bar"),
},

View File

@@ -143,10 +143,10 @@ func TestRollupPredictLinear(t *testing.T) {
testRollupFunc(t, "predict_linear", args, &me, vExpected)
}
f(0e-3, 63.739757761102624)
f(50e-3, 50.39682764539959)
f(100e-3, 37.053897529696556)
f(200e-3, 10.368037298290488)
f(0e-3, 30.382432471845043)
f(50e-3, 17.03950235614201)
f(100e-3, 3.696572240438975)
f(200e-3, -22.989287990967092)
}
func TestRollupHoltWinters(t *testing.T) {
@@ -189,10 +189,11 @@ func TestRollupNewRollupFuncSuccess(t *testing.T) {
testRollupFunc(t, funcName, args, &me, vExpected)
}
f("default_rollup", 123)
f("changes", 10)
f("default_rollup", 34)
f("changes", 11)
f("delta", -89)
f("deriv", -712)
f("deriv", -266.85860231406065)
f("deriv_fast", -712)
f("idelta", 0)
f("increase", 275)
f("irate", 0)
@@ -202,12 +203,16 @@ func TestRollupNewRollupFuncSuccess(t *testing.T) {
f("min_over_time", 12)
f("max_over_time", 123)
f("sum_over_time", 565)
f("sum2_over_time", 37951)
f("geomean_over_time", 39.33466603189148)
f("count_over_time", 12)
f("stddev_over_time", 30.752935722554287)
f("stdvar_over_time", 945.7430555555555)
f("first_over_time", 123)
f("last_over_time", 34)
f("integrate", 61.0275)
f("distinct_over_time", 8)
f("ideriv", 0)
}
func TestRollupNewRollupFuncError(t *testing.T) {
@@ -259,7 +264,7 @@ func TestRollupNoWindowNoPoints(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{nan, nan, nan, nan, 123}
valuesExpected := []float64{nan, nan, nan, nan, nan}
timestampsExpected := []int64{0, 1, 2, 3, 4}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -267,14 +272,14 @@ func TestRollupNoWindowNoPoints(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 120,
End: 144,
End: 148,
Step: 4,
Window: 0,
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{2, 2, 0, 0, 0, nan, nan}
timestampsExpected := []int64{120, 124, 128, 132, 136, 140, 144}
valuesExpected := []float64{2, 0, 0, 0, 0, 0, 0, nan}
timestampsExpected := []int64{120, 124, 128, 132, 136, 140, 144, 148}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
}
@@ -290,7 +295,7 @@ func TestRollupWindowNoPoints(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{nan, nan, nan, nan, 123}
valuesExpected := []float64{nan, nan, nan, nan, nan}
timestampsExpected := []int64{0, 1, 2, 3, 4}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -315,14 +320,14 @@ func TestRollupNoWindowPartialPoints(t *testing.T) {
rc := rollupConfig{
Func: rollupFirst,
Start: 0,
End: 20,
End: 25,
Step: 5,
Window: 0,
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{123, 123, 123, 123, 123}
timestampsExpected := []int64{0, 5, 10, 15, 20}
valuesExpected := []float64{nan, 123, 123, 123, 34, 34}
timestampsExpected := []int64{0, 5, 10, 15, 20, 25}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
t.Run("afterEnd", func(t *testing.T) {
@@ -335,7 +340,7 @@ func TestRollupNoWindowPartialPoints(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{44, 34, 34, nan}
valuesExpected := []float64{12, 44, 34, 34}
timestampsExpected := []int64{100, 120, 140, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -349,7 +354,7 @@ func TestRollupNoWindowPartialPoints(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{nan, 123, 54, 44, nan}
valuesExpected := []float64{nan, nan, 123, 54, 44}
timestampsExpected := []int64{-50, 0, 50, 100, 150}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -366,7 +371,7 @@ func TestRollupWindowPartialPoints(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{123, 123, 34, 34, 44}
valuesExpected := []float64{nan, 123, 123, 34, 34}
timestampsExpected := []int64{0, 5, 10, 15, 20}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -380,7 +385,7 @@ func TestRollupWindowPartialPoints(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{34, 34, nan, nan}
valuesExpected := []float64{44, 34, 34, nan}
timestampsExpected := []int64{100, 120, 140, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -394,7 +399,7 @@ func TestRollupWindowPartialPoints(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{54, 44, nan, nan}
valuesExpected := []float64{nan, 54, 44, 34}
timestampsExpected := []int64{0, 50, 100, 150}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -411,7 +416,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{123, 21, 12, 34, nan}
valuesExpected := []float64{nan, 123, 21, 12, 34}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -425,7 +430,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{4, 4, 3, 1, nan}
valuesExpected := []float64{nan, 4, 4, 3, 1}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -439,7 +444,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{21, 12, 32, 34, nan}
valuesExpected := []float64{nan, 21, 12, 32, 34}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -453,7 +458,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{123, 99, 44, 34, nan}
valuesExpected := []float64{nan, 123, 99, 44, 34}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -467,7 +472,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{222, 199, 110, 34, nan}
valuesExpected := []float64{nan, 222, 199, 110, 34}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -481,7 +486,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{-102, -9, 22, 0, nan}
valuesExpected := []float64{nan, -102, -9, 22, 0}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -495,7 +500,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{33, -87, 0, nan}
valuesExpected := []float64{0, 33, -87, 0}
timestampsExpected := []int64{10, 50, 90, 130}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -509,10 +514,24 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{3, 4, 3, 0, nan}
valuesExpected := []float64{nan, 4, 4, 3, 0}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
t.Run("changes_small_window", func(t *testing.T) {
rc := rollupConfig{
Func: rollupChanges,
Start: 0,
End: 45,
Step: 9,
Window: 9,
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{nan, 1, 1, 1, 1, 0}
timestampsExpected := []int64{0, 9, 18, 27, 36, 45}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
t.Run("resets", func(t *testing.T) {
rc := rollupConfig{
Func: rollupResets,
@@ -523,7 +542,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{2, 2, 1, 0, nan}
valuesExpected := []float64{nan, 2, 2, 1, 0}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -537,13 +556,13 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{55.5, 49.75, 36.666666666666664, 34, nan}
valuesExpected := []float64{nan, 55.5, 49.75, 36.666666666666664, 34}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
t.Run("deriv", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDeriv,
Func: rollupDerivSlow,
Start: 0,
End: 160,
Step: 40,
@@ -551,7 +570,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{-3290.3225806451615, -204.54545454545456, 550, 0, nan}
valuesExpected := []float64{0, -2879.310344827587, 558.0608793686592, 422.84569138276544, 0}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -565,7 +584,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{-1916.6666666666665, -43500, 400, 0, nan}
valuesExpected := []float64{nan, -1916.6666666666665, -43500, 400, 0}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -579,7 +598,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{39.81519810323691, 32.080952292598795, 5.2493385826745405, 0, nan}
valuesExpected := []float64{nan, 39.81519810323691, 32.080952292598795, 5.2493385826745405, 5.830951894845301}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -593,7 +612,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{4.6035, 4.3934999999999995, 2.166, 0.34, nan}
valuesExpected := []float64{nan, 4.6035, 4.3934999999999995, 2.166, 0.34}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
@@ -607,7 +626,7 @@ func TestRollupFuncsNoWindow(t *testing.T) {
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{4, 4, 3, 1, nan}
valuesExpected := []float64{nan, 4, 4, 3, 1}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})

View File

@@ -38,11 +38,13 @@ func (ts *timeseries) String() string {
return fmt.Sprintf("MetricName=%s, Values=%g, Timestamps=%d", &ts.MetricName, ts.Values, ts.Timestamps)
}
func (ts *timeseries) CopyFrom(src *timeseries) {
func (ts *timeseries) CopyFromShallowTimestamps(src *timeseries) {
ts.Reset()
ts.MetricName.CopyFrom(&src.MetricName)
ts.Values = append(ts.Values[:0], src.Values...)
ts.Timestamps = append(ts.Timestamps[:0], src.Timestamps...)
ts.Timestamps = src.Timestamps
ts.denyReuse = true
}
func (ts *timeseries) CopyFromMetricNames(src *timeseries) {

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"math"
"math/rand"
"regexp"
"sort"
"strconv"
"strings"
@@ -61,6 +62,7 @@ var transformFuncs = map[string]transformFunc{
"label_keep": transformLabelKeep,
"label_copy": transformLabelCopy,
"label_move": transformLabelMove,
"label_transform": transformLabelTransform,
"union": transformUnion,
"": transformUnion, // empty func is a synonim to union
"keep_last_value": transformKeepLastValue,
@@ -294,14 +296,12 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
if err != nil {
continue
}
var dst timeseries
dst.CopyFrom(ts)
dst.MetricName.ResetMetricGroup()
dst.MetricName.RemoveTag("le")
bb.B = marshalMetricTagsSorted(bb.B[:0], &dst.MetricName)
ts.MetricName.ResetMetricGroup()
ts.MetricName.RemoveTag("le")
bb.B = marshalMetricTagsSorted(bb.B[:0], &ts.MetricName)
m[string(bb.B)] = append(m[string(bb.B)], x{
le: le,
ts: &dst,
ts: ts,
})
}
bbPool.Put(bb)
@@ -394,13 +394,6 @@ func runningAvg(a, b float64, idx int) float64 {
return a + (b-a)/float64(idx+1)
}
func keepLastValue(a, b float64, idx int) float64 {
if math.IsNaN(b) {
return a
}
return b
}
func skipLeadingNaNs(values []float64) []float64 {
i := 0
for i < len(values) && math.IsNaN(values[i]) {
@@ -641,9 +634,7 @@ func transformUnion(tfa *transformFuncArg) ([]*timeseries, error) {
continue
}
m[string(bb.B)] = true
var dst timeseries
dst.CopyFrom(ts)
rvs = append(rvs, &dst)
rvs = append(rvs, ts)
}
}
bbPool.Put(bb)
@@ -816,6 +807,31 @@ func transformLabelJoin(tfa *transformFuncArg) ([]*timeseries, error) {
return rvs, nil
}
func transformLabelTransform(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 4); err != nil {
return nil, err
}
label, err := getString(args[1], 1)
if err != nil {
return nil, err
}
regex, err := getString(args[2], 2)
if err != nil {
return nil, err
}
replacement, err := getString(args[3], 3)
if err != nil {
return nil, err
}
r, err := compileRegexp(regex)
if err != nil {
return nil, fmt.Errorf(`cannot compile regex %q: %s`, regex, err)
}
return labelReplace(args[0], label, r, label, replacement)
}
func transformLabelReplace(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 5); err != nil {
@@ -842,11 +858,12 @@ func transformLabelReplace(tfa *transformFuncArg) ([]*timeseries, error) {
if err != nil {
return nil, fmt.Errorf(`cannot compile regex %q: %s`, regex, err)
}
return labelReplace(args[0], srcLabel, r, dstLabel, replacement)
}
func labelReplace(tss []*timeseries, srcLabel string, r *regexp.Regexp, dstLabel, replacement string) ([]*timeseries, error) {
replacementBytes := []byte(replacement)
rvs := args[0]
for _, ts := range rvs {
for _, ts := range tss {
mn := &ts.MetricName
dstValue := getDstValue(mn, dstLabel)
srcValue := mn.GetTagValue(srcLabel)
@@ -856,7 +873,7 @@ func transformLabelReplace(tfa *transformFuncArg) ([]*timeseries, error) {
mn.RemoveTag(dstLabel)
}
}
return rvs, nil
return tss, nil
}
func transformLn(v float64) float64 {

View File

@@ -96,6 +96,14 @@ func SearchTagValues(tagKey []byte, maxTagValues int) ([]string, error) {
return values, err
}
// SearchTagEntries searches for tag entries.
func SearchTagEntries(maxTagKeys, maxTagValues int) ([]storage.TagEntry, error) {
WG.Add(1)
tagEntries, err := Storage.SearchTagEntries(maxTagKeys, maxTagValues)
WG.Done()
return tagEntries, err
}
// GetSeriesCount returns the number of time series in the storage.
func GetSeriesCount() (uint64, error) {
WG.Add(1)
@@ -285,6 +293,18 @@ func registerStorageMetrics(strg *storage.Storage) {
metrics.NewGauge(`vm_missing_tsids_for_metric_id_total`, func() float64 {
return float64(idbm().MissingTSIDsForMetricID)
})
metrics.NewGauge(`vm_recent_hour_metric_ids_search_calls_total`, func() float64 {
return float64(idbm().RecentHourMetricIDsSearchCalls)
})
metrics.NewGauge(`vm_recent_hour_metric_ids_search_hits_total`, func() float64 {
return float64(idbm().RecentHourMetricIDsSearchHits)
})
metrics.NewGauge(`vm_date_metric_ids_search_calls_total`, func() float64 {
return float64(idbm().DateMetricIDsSearchCalls)
})
metrics.NewGauge(`vm_date_metric_ids_search_hits_total`, func() float64 {
return float64(idbm().DateMetricIDsSearchHits)
})
metrics.NewGauge(`vm_assisted_merges_total{type="storage/small"}`, func() float64 {
return float64(tm().SmallAssistedMerges)
@@ -342,6 +362,9 @@ func registerStorageMetrics(strg *storage.Storage) {
metrics.NewGauge(`vm_cache_entries{type="storage/date_metricID"}`, func() float64 {
return float64(m().DateMetricIDCacheSize)
})
metrics.NewGauge(`vm_cache_entries{type="storage/hour_metric_ids"}`, func() float64 {
return float64(m().HourMetricIDCacheSize)
})
metrics.NewGauge(`vm_cache_entries{type="storage/bigIndexBlocks"}`, func() float64 {
return float64(tm().BigIndexBlocksCacheSize)
})
@@ -357,6 +380,9 @@ func registerStorageMetrics(strg *storage.Storage) {
metrics.NewGauge(`vm_cache_entries{type="indexdb/tagFilters"}`, func() float64 {
return float64(idbm().TagCacheSize)
})
metrics.NewGauge(`vm_cache_entries{type="indexdb/uselessTagFilters"}`, func() float64 {
return float64(idbm().UselessTagFiltersCacheSize)
})
metrics.NewGauge(`vm_cache_entries{type="storage/regexps"}`, func() float64 {
return float64(storage.RegexpCacheSize())
})
@@ -376,6 +402,9 @@ func registerStorageMetrics(strg *storage.Storage) {
metrics.NewGauge(`vm_cache_size_bytes{type="indexdb/tagFilters"}`, func() float64 {
return float64(idbm().TagCacheBytesSize)
})
metrics.NewGauge(`vm_cache_size_bytes{type="indexdb/uselessTagFilters"}`, func() float64 {
return float64(idbm().UselessTagFiltersCacheBytesSize)
})
metrics.NewGauge(`vm_cache_requests_total{type="storage/tsid"}`, func() float64 {
return float64(m().TSIDCacheRequests)
@@ -404,6 +433,9 @@ func registerStorageMetrics(strg *storage.Storage) {
metrics.NewGauge(`vm_cache_requests_total{type="indexdb/tagFilters"}`, func() float64 {
return float64(idbm().TagCacheRequests)
})
metrics.NewGauge(`vm_cache_requests_total{type="indexdb/uselessTagFilters"}`, func() float64 {
return float64(idbm().UselessTagFiltersCacheRequests)
})
metrics.NewGauge(`vm_cache_requests_total{type="storage/regexps"}`, func() float64 {
return float64(storage.RegexpCacheRequests())
})
@@ -435,6 +467,9 @@ func registerStorageMetrics(strg *storage.Storage) {
metrics.NewGauge(`vm_cache_misses_total{type="indexdb/tagFilters"}`, func() float64 {
return float64(idbm().TagCacheMisses)
})
metrics.NewGauge(`vm_cache_misses_total{type="indexdb/uselessTagFilters"}`, func() float64 {
return float64(idbm().UselessTagFiltersCacheMisses)
})
metrics.NewGauge(`vm_cache_misses_total{type="storage/regexps"}`, func() float64 {
return float64(storage.RegexpCacheMisses())
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
DOCKER_NAMESPACE := valyala
BUILDER_IMAGE := local/builder:go1.12.5
DOCKER_NAMESPACE := victoriametrics
BUILDER_IMAGE := local/builder:go1.12.6
CERTS_IMAGE := local/certs:1.0.2
package-certs:
@@ -18,6 +18,7 @@ app-via-docker: package-certs package-builder
-w /VictoriaMetrics \
--mount type=bind,src="$(shell pwd)/gocache-for-docker",dst=/gocache \
--env GOCACHE=/gocache \
--env GO111MODULE=on \
$(BUILDER_IMAGE) \
go build $(RACE) -mod=vendor -ldflags "-s -w -extldflags '-static' $(GO_BUILDINFO)" -tags 'netgo osusergo' -o bin/$(APP_NAME)-prod $(PKG_PREFIX)/app/$(APP_NAME)

View File

@@ -0,0 +1,33 @@
### Folder contains basic images and tools for building and running Victoria Metrics in docker
#### Docker compose
To spin-up setup of VictoriaMetrics, Prometheus and Grafana run following command:
`docker-compose up`
##### VictoriaMetrics
VictoriaMetrics opens following ports:
* `--graphiteListenAddr=:2003`
* `--opentsdbListenAddr=:4242`
* `--httpListenAddr=:8428`
##### Prometheus
To access service open following [link](http://localhost:9090).
Prometheus is already configured to use VictoriaMetrics as remote storage.
##### Grafana
To access service open following [link](http://localhost:3000).
Default creds:
* login - `admin`
* password - `admin`
Grafana is provisioned by default with following entities:
* VictoriaMetrics datasource
* Prometheus datasource
* VictoriaMetrics overview dashboard

View File

@@ -1 +1,2 @@
FROM golang:1.12.5
FROM golang:1.12.6
STOPSIGNAL SIGINT

View File

@@ -0,0 +1,61 @@
version: '3.5'
services:
prometheus:
container_name: prometheus
image: prom/prometheus:v2.10.0
depends_on:
- "victoriametrics"
ports:
- 9090:9090
volumes:
- promdata:/prometheus
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
networks:
- vm_net
restart: always
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics
ports:
- 8428:8428
- 2003:2003
- 4242:4242
volumes:
- vmdata:/storage
command:
- '--storageDataPath=/storage'
- '--graphiteListenAddr=:2003'
- '--opentsdbListenAddr=:4242'
- '--httpListenAddr=:8428'
networks:
- vm_net
restart: always
grafana:
container_name: grafana
image: grafana/grafana:6.2.1
entrypoint: >
/bin/sh -c "
cd /var/lib/grafana &&
mkdir -p dashboards &&
sed 's/$${DS_PROMETHEUS}/Prometheus/g' vm.json > dashboards/vm.json &&
/run.sh"
depends_on:
- "victoriametrics"
ports:
- 3000:3000
volumes:
- grafanadata:/var/lib/grafana
- ./provisioning/:/etc/grafana/provisioning/
- ./../../dashboards/victoriametrics.json:/var/lib/grafana/vm.json
networks:
- vm_net
restart: always
volumes:
promdata: {}
vmdata: {}
grafanadata: {}
networks:
vm_net:

View File

@@ -0,0 +1,16 @@
global:
scrape_interval: 10s
evaluation_interval: 10s
remote_write:
- url: "http://victoriametrics:8428/api/v1/write"
queue_config:
max_samples_per_send: 10000
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['prometheus:9090']
- job_name: 'victoriametrics'
static_configs:
- targets: ['victoriametrics:8428']

View File

@@ -0,0 +1,9 @@
apiVersion: 1
providers:
- name: Prometheus
orgId: 1
folder: ''
type: file
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,14 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: false
- name: VictoriaMetrics
type: prometheus
access: proxy
url: http://victoriametrics:8428
isDefault: true

8
go.mod
View File

@@ -1,16 +1,16 @@
module github.com/VictoriaMetrics/VictoriaMetrics
require (
github.com/VictoriaMetrics/fastcache v1.5.0
github.com/VictoriaMetrics/metrics v1.4.0
github.com/VictoriaMetrics/fastcache v1.5.1
github.com/VictoriaMetrics/metrics v1.5.0
github.com/cespare/xxhash/v2 v2.0.1-0.20190104013014-3767db7a7e18
github.com/golang/snappy v0.0.1
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/valyala/fastjson v1.4.1
github.com/valyala/gozstd v1.5.0
github.com/valyala/gozstd v1.5.1
github.com/valyala/histogram v1.0.1
github.com/valyala/quicktemplate v1.1.1
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f
)
go 1.12

16
go.sum
View File

@@ -1,10 +1,10 @@
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/VictoriaMetrics/fastcache v1.5.0 h1:z8t2QV/CDXWVJ9vy9yRtGGDoOvk9W2aXQBijbLk0KCc=
github.com/VictoriaMetrics/fastcache v1.5.0/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE=
github.com/VictoriaMetrics/metrics v1.4.0 h1:3+XdciC4E8sywx+0PStXhtIdWxXP2bdJ06Whw0mViQE=
github.com/VictoriaMetrics/metrics v1.4.0/go.mod h1:QZAL5yLaXvhSPeib0ahluGo9VK0HXDZHovKaKlpuWvs=
github.com/VictoriaMetrics/fastcache v1.5.1 h1:qHgHjyoNFV7jgucU8QZUuU4gcdhfs8QW1kw68OD2Lag=
github.com/VictoriaMetrics/fastcache v1.5.1/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE=
github.com/VictoriaMetrics/metrics v1.5.0 h1:WvQqPn+z9pR1U7J58CgaGiWrN8phNGSpr2xUSxJnfpE=
github.com/VictoriaMetrics/metrics v1.5.0/go.mod h1:QZAL5yLaXvhSPeib0ahluGo9VK0HXDZHovKaKlpuWvs=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -36,13 +36,13 @@ github.com/valyala/fastjson v1.4.1 h1:hrltpHpIpkaxll8QltMU8c3QZ5+qIiCL8yKqPFJI/y
github.com/valyala/fastjson v1.4.1/go.mod h1:nV6MsjxL2IMJQUoHDIrjEI7oLyeqK6aBD7EFWPsvP8o=
github.com/valyala/fastrand v1.0.0 h1:LUKT9aKer2dVQNUi3waewTbKV+7H17kvWFNKs2ObdkI=
github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/gozstd v1.5.0 h1:OI7Z2e+GkvmmdRFiJeyuByhT/WMTLuRLa43Z2Tjzenw=
github.com/valyala/gozstd v1.5.0/go.mod h1:oYOS+oJovjw9ewtrwEYb9+ybolEXd6pHyLMuAWN5zts=
github.com/valyala/gozstd v1.5.1 h1:ZLepItgu2g+B2CfVQy6KCV/as8lnJ7ef1KU6DPxQSS0=
github.com/valyala/gozstd v1.5.1/go.mod h1:oYOS+oJovjw9ewtrwEYb9+ybolEXd6pHyLMuAWN5zts=
github.com/valyala/histogram v1.0.1 h1:FzA7n2Tz/wKRMejgu3PV1vw3htAklTjjuoI6z3d4KDg=
github.com/valyala/histogram v1.0.1/go.mod h1:lQy0xA4wUz2+IUnf97SivorsJIp8FxsnRd6x25q7Mto=
github.com/valyala/quicktemplate v1.1.1 h1:C58y/wN0FMTi2PR0n3onltemfFabany53j7M6SDDB8k=
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -13,6 +13,7 @@ var (
// Verify ByteBuffer implements the given interfaces.
_ io.Writer = &ByteBuffer{}
_ fs.ReadAtCloser = &ByteBuffer{}
_ io.ReaderFrom = &ByteBuffer{}
// Verify reader implement filestream.ReadCloser interface.
_ filestream.ReadCloser = &reader{}
@@ -44,7 +45,31 @@ func (bb *ByteBuffer) ReadAt(p []byte, offset int64) {
logger.Panicf("BUG: too big offset=%d; cannot exceed len(bb.B)=%d", offset, len(bb.B))
}
if n := copy(p, bb.B[offset:]); n < len(p) {
logger.Panicf("BUG: EOF occured after reading %d bytes out of %d bytes at offset %d", n, len(p), offset)
logger.Panicf("BUG: EOF occurred after reading %d bytes out of %d bytes at offset %d", n, len(p), offset)
}
}
// ReadFrom reads all the data from r to bb until EOF.
func (bb *ByteBuffer) ReadFrom(r io.Reader) (int64, error) {
b := bb.B
bLen := len(b)
b = Resize(b, 4*1024)
b = b[:cap(b)]
offset := bLen
for {
if free := len(b) - offset; free < offset {
n := len(b)
b = append(b, make([]byte, n)...)
}
n, err := r.Read(b[offset:])
offset += n
if err != nil {
bb.B = b[:offset]
if err == io.EOF {
err = nil
}
return int64(offset - bLen), err
}
}
}

View File

@@ -1,6 +1,7 @@
package bytesutil
import (
"bytes"
"fmt"
"io"
"testing"
@@ -66,6 +67,92 @@ func TestByteBuffer(t *testing.T) {
}
}
func TestByteBufferReadFrom(t *testing.T) {
var bbPool ByteBufferPool
t.Run("zero_bytes", func(t *testing.T) {
t.Parallel()
bb := bbPool.Get()
defer bbPool.Put(bb)
src := bytes.NewBufferString("")
n, err := bb.ReadFrom(src)
if err != nil {
t.Fatalf("error when reading empty string: %s", err)
}
if n != 0 {
t.Fatalf("unexpected number of bytes read; got %d; want %d", n, 0)
}
if len(bb.B) != 0 {
t.Fatalf("unexpejcted len(bb.B); got %d; want %d", len(bb.B), 0)
}
})
t.Run("non_zero_bytes", func(t *testing.T) {
t.Parallel()
bb := bbPool.Get()
defer bbPool.Put(bb)
s := "foobarbaz"
src := bytes.NewBufferString(s)
n, err := bb.ReadFrom(src)
if err != nil {
t.Fatalf("error when reading non-empty string: %s", err)
}
if n != int64(len(s)) {
t.Fatalf("unexpected number of bytes read; got %d; want %d", n, len(s))
}
if string(bb.B) != s {
t.Fatalf("unexpected value read; got %q; want %q", bb.B, s)
}
})
t.Run("big_number_of_bytes", func(t *testing.T) {
t.Parallel()
bb := bbPool.Get()
defer bbPool.Put(bb)
b := make([]byte, 1024*1024+234)
for i := range b {
b[i] = byte(i)
}
src := bytes.NewBuffer(b)
n, err := bb.ReadFrom(src)
if err != nil {
t.Fatalf("cannot read big value: %s", err)
}
if n != int64(len(b)) {
t.Fatalf("unexpected number of bytes read; got %d; want %d", n, len(b))
}
if string(bb.B) != string(b) {
t.Fatalf("unexpected value read; got %q; want %q", bb.B, b)
}
})
t.Run("non_empty_bb", func(t *testing.T) {
t.Parallel()
bb := bbPool.Get()
defer bbPool.Put(bb)
prefix := []byte("prefix")
bb.B = append(bb.B[:0], prefix...)
s := "aosdfdsafdjsf"
src := bytes.NewBufferString(s)
n, err := bb.ReadFrom(src)
if err != nil {
t.Fatalf("cannot read to non-empty bb: %s", err)
}
if n != int64(len(s)) {
t.Fatalf("unexpected number of bytes read; got %d; want %d", n, len(s))
}
if len(bb.B) != len(prefix)+len(s) {
t.Fatalf("unexpected bb.B len; got %d; want %d", len(bb.B), len(prefix)+len(s))
}
if string(bb.B[:len(prefix)]) != string(prefix) {
t.Fatalf("unexpected prefix; got %q; want %q", bb.B[:len(prefix)], prefix)
}
if string(bb.B[len(prefix):]) != s {
t.Fatalf("unexpected data read; got %q; want %q", bb.B[len(prefix):], s)
}
})
}
func TestByteBufferRead(t *testing.T) {
var bb ByteBuffer

View File

@@ -186,6 +186,7 @@ func maxUpExponent(v int64) int16 {
v = -v
}
if v < 0 {
// Handle corner case for v=-1<<63
return 0
}

View File

@@ -1,27 +1,34 @@
package encoding
import (
"github.com/VictoriaMetrics/metrics"
"github.com/valyala/gozstd"
)
// CompressZSTD compresses src, appends the result to dst and returns
// the appended dst.
//
// src must be non-empty.
func CompressZSTD(dst, src []byte) []byte {
return gozstd.CompressLevel(dst, src, 5)
}
// CompressZSTDLevel appends compressed src to dst and returns
// the appended dst.
//
// The given compressLevel is used for the compression.
func CompressZSTDLevel(dst, src []byte, compressLevel int) []byte {
return gozstd.CompressLevel(dst, src, compressLevel)
compressCalls.Inc()
originalBytes.Add(len(src))
dstLen := len(dst)
dst = gozstd.CompressLevel(dst, src, compressLevel)
compressedBytes.Add(len(dst) - dstLen)
return dst
}
// DecompressZSTD decompresses src, appends the result to dst and returns
// the appended dst.
func DecompressZSTD(dst, src []byte) ([]byte, error) {
decompressCalls.Inc()
return gozstd.Decompress(dst, src)
}
var (
compressCalls = metrics.NewCounter(`vm_zstd_block_compress_calls_total`)
decompressCalls = metrics.NewCounter(`vm_zstd_block_decompress_calls_total`)
originalBytes = metrics.NewCounter(`vm_zstd_block_original_bytes_total`)
compressedBytes = metrics.NewCounter(`vm_zstd_block_compressed_bytes_total`)
)

View File

@@ -17,7 +17,7 @@ func TestCompressDecompressZSTD(t *testing.T) {
}
func testCompressDecompressZSTD(t *testing.T, b []byte) {
bc := CompressZSTD(nil, b)
bc := CompressZSTDLevel(nil, b, 5)
bNew, err := DecompressZSTD(nil, bc)
if err != nil {
t.Fatalf("unexpected error when decompressing b=%x from bc=%x: %s", b, bc, err)
@@ -27,7 +27,7 @@ func testCompressDecompressZSTD(t *testing.T, b []byte) {
}
prefix := []byte{1, 2, 33}
bcNew := CompressZSTD(prefix, b)
bcNew := CompressZSTDLevel(prefix, b, 5)
if string(bcNew[:len(prefix)]) != string(prefix) {
t.Fatalf("invalid prefix for b=%x; got\n%x; expecting\n%x", b, bcNew[:len(prefix)], prefix)
}

View File

@@ -117,7 +117,7 @@ func marshalInt64Array(dst []byte, a []int64, precisionBits uint8) (result []byt
bb := bbPool.Get()
if isGauge(a) {
// Guage values are better compressed with delta encoding.
// Gauge values are better compressed with delta encoding.
mt = MarshalTypeZSTDNearestDelta
pb := precisionBits
if pb < 6 {

View File

@@ -0,0 +1,9 @@
package filestream
func (st *streamTracker) adviseDontNeed(n int, fdatasync bool) error {
return nil
}
func (st *streamTracker) close() error {
return nil
}

View File

@@ -1,18 +0,0 @@
package flagutil
import "strings"
// Array holds an array of flag values
type Array []string
// String implements flag.Value interface
func (a *Array) String() string {
return strings.Join(*a, ",")
}
// Set implements flag.Value interface
func (a *Array) Set(value string) error {
values := strings.Split(value, ",")
*a = append(*a, values...)
return nil
}

View File

@@ -1,34 +0,0 @@
package flagutil
import (
"flag"
"os"
"testing"
)
var fooFlag Array
func init() {
os.Args = append(os.Args, "--fooFlag=foo", "--fooFlag=bar")
flag.Var(&fooFlag, "fooFlag", "test")
}
func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
}
func TestArray(t *testing.T) {
expected := map[string]struct{}{
"foo": {},
"bar": {},
}
if len(expected) != len(fooFlag) {
t.Errorf("len array flag (%d) is not equal to %d", len(fooFlag), len(expected))
}
for _, i := range fooFlag {
if _, ok := expected[i]; !ok {
t.Errorf("unexpected item in array %v", i)
}
}
}

View File

@@ -5,6 +5,8 @@ import (
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/filestream"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -70,8 +72,8 @@ var (
readersCount = metrics.NewCounter(`vm_fs_readers`)
)
// SyncPath syncs contents of the given path.
func SyncPath(path string) {
// MustSyncPath syncs contents of the given path.
func MustSyncPath(path string) {
d, err := os.Open(path)
if err != nil {
logger.Panicf("FATAL: cannot open %q: %s", path, err)
@@ -111,8 +113,8 @@ func WriteFile(path string, data []byte) error {
if err != nil {
return fmt.Errorf("cannot obtain absolute path to %q: %s", path, err)
}
parentDirPath, _ := filepath.Split(absPath)
SyncPath(parentDirPath)
parentDirPath := filepath.Dir(absPath)
MustSyncPath(parentDirPath)
return nil
}
@@ -122,7 +124,7 @@ func MkdirAllIfNotExist(path string) error {
if IsPathExist(path) {
return nil
}
return os.MkdirAll(path, 0755)
return mkdirSync(path)
}
// MkdirAllFailIfExist creates the given path dir if it isn't exist.
@@ -132,7 +134,18 @@ func MkdirAllFailIfExist(path string) error {
if IsPathExist(path) {
return fmt.Errorf("the %q already exists", path)
}
return os.MkdirAll(path, 0755)
return mkdirSync(path)
}
func mkdirSync(path string) error {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
// Sync the parent directory, so the created directory becomes visible
// in the fs after power loss.
parentDirPath := filepath.Dir(path)
MustSyncPath(parentDirPath)
return nil
}
// RemoveDirContents removes all the contents of the given dir it it exists.
@@ -159,11 +172,9 @@ func RemoveDirContents(dir string) {
continue
}
fullPath := dir + "/" + name
if err := os.RemoveAll(fullPath); err != nil {
logger.Panicf("FATAL: cannot remove %q: %s", fullPath, err)
}
MustRemoveAll(fullPath)
}
SyncPath(dir)
MustSyncPath(dir)
}
// MustClose must close the given file f.
@@ -185,18 +196,79 @@ func IsPathExist(path string) bool {
return true
}
// MustRemoveAllSynced removes path with all the contents
// and syncs the parent directory, so it no longer contains the path.
func MustRemoveAllSynced(path string) {
if err := os.RemoveAll(path); err != nil {
func mustSyncParentDirIfExists(path string) {
parentDirPath := filepath.Dir(path)
if !IsPathExist(parentDirPath) {
return
}
MustSyncPath(parentDirPath)
}
// MustRemoveAll removes path with all the contents.
//
// It properly handles NFS issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/61 .
func MustRemoveAll(path string) {
err := os.RemoveAll(path)
if err == nil {
// Make sure the parent directory doesn't contain references
// to the current directory.
mustSyncParentDirIfExists(path)
return
}
if !isTemporaryNFSError(err) {
logger.Panicf("FATAL: cannot remove %q: %s", path, err)
}
SyncPath(filepath.Dir(path))
// NFS prevents from removing directories with open files.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/61 .
// Schedule for later directory removal.
select {
case removeDirCh <- path:
default:
logger.Panicf("FATAL: cannot schedule %s for removal, since the removal queue is full (%d entries)", path, cap(removeDirCh))
}
}
var removeDirCh = make(chan string, 1024)
func dirRemover() {
for path := range removeDirCh {
attempts := 0
for {
err := os.RemoveAll(path)
if err == nil {
break
}
if !isTemporaryNFSError(err) {
logger.Panicf("FATAL: cannot remove %q: %s", path, err)
}
// NFS prevents from removing directories with open files.
// Sleep for a while and try again in the hope open files will be closed.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/61 .
attempts++
if attempts > 10 {
logger.Panicf("FATAL: cannot remove %q in %d attempts: %s", path, attempts, err)
}
time.Sleep(100 * time.Millisecond)
}
// Make sure the parent directory doesn't contain references
// to the current directory.
mustSyncParentDirIfExists(path)
}
}
func isTemporaryNFSError(err error) bool {
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/61 for details.
errStr := err.Error()
return strings.Contains(errStr, "directory not empty") || strings.Contains(errStr, "device or resource busy")
}
func init() {
go dirRemover()
}
// HardLinkFiles makes hard links for all the files from srcDir in dstDir.
func HardLinkFiles(srcDir, dstDir string) error {
if err := os.MkdirAll(dstDir, 0755); err != nil {
if err := mkdirSync(dstDir); err != nil {
return fmt.Errorf("cannot create dstDir=%q: %s", dstDir, err)
}
@@ -227,7 +299,7 @@ func HardLinkFiles(srcDir, dstDir string) error {
}
}
SyncPath(dstDir)
MustSyncPath(dstDir)
return nil
}

View File

@@ -31,6 +31,8 @@ var (
httpAuthPassword = flag.String("httpAuth.password", "", "Password for HTTP Basic Auth. The authentication is disabled -httpAuth.username is empty")
metricsAuthKey = flag.String("metricsAuthKey", "", "Auth key for /metrics. It overrides httpAuth settings")
pprofAuthKey = flag.String("pprofAuthKey", "", "Auth key for /debug/pprof. It overrides httpAuth settings")
disableResponseCompression = flag.Bool("http.disableResponseCompression", false, "Disable compression of HTTP responses for saving CPU resources. By default compression is enabled to save network bandwidth")
)
var (
@@ -51,6 +53,8 @@ type RequestHandler func(w http.ResponseWriter, r *http.Request) bool
// By default all the responses are transparently compressed, since Google
// charges a lot for the egress traffic. The compression may be disabled
// by calling DisableResponseCompression before writing the first byte to w.
//
// The compression is also disabled if -http.disableResponseCompression flag is set.
func Serve(addr string, rh RequestHandler) {
scheme := "http"
if *tlsEnable {
@@ -224,6 +228,9 @@ func checkBasicAuth(w http.ResponseWriter, r *http.Request) bool {
}
func maybeGzipResponseWriter(w http.ResponseWriter, r *http.Request) http.ResponseWriter {
if *disableResponseCompression {
return w
}
ae := r.Header.Get("Accept-Encoding")
if ae == "" {
return w

View File

@@ -0,0 +1,40 @@
package memory
import (
"syscall"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// This has been adapted from https://github.com/pbnjay/memory.
type memStatusEx struct {
dwLength uint32
dwMemoryLoad uint32
ullTotalPhys uint64
unused [6]uint64
}
func sysTotalMemory() int {
kernel32, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
logger.Panicf("FATAL: cannot load kernel32.dll: %s", err)
}
globalMemoryStatusEx, err := kernel32.FindProc("GlobalMemoryStatusEx")
if err != nil {
logger.Panicf("FATAL: cannot find GlobalMemoryStatusEx: %s", err)
}
msx := &memStatusEx{
dwLength: uint32(unsafe.Sizeof(memStatusEx{})),
}
r, _, err := globalMemoryStatusEx.Call(uintptr(unsafe.Pointer(msx)))
if r == 0 {
logger.Panicf("FATAL: error in GlobalMemoryStatusEx: %s", err)
}
n := int(msx.ullTotalPhys)
if uint64(n) != msx.ullTotalPhys {
logger.Panicf("FATAL: int overflow for msx.ullTotalPhys=%d", msx.ullTotalPhys)
}
return n
}

View File

@@ -91,6 +91,13 @@ func (bsr *blockStreamReader) reset() {
bsr.err = nil
}
func (bsr *blockStreamReader) String() string {
if len(bsr.path) > 0 {
return bsr.path
}
return bsr.ph.String()
}
// InitFromInmemoryPart initializes bsr from the given ip.
func (bsr *blockStreamReader) InitFromInmemoryPart(ip *inmemoryPart) {
bsr.reset()

View File

@@ -2,7 +2,6 @@ package mergeset
import (
"fmt"
"os"
"path/filepath"
"sync"
@@ -92,7 +91,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
metaindexPath := path + "/metaindex.bin"
metaindexFile, err := filestream.Create(metaindexPath, false)
if err != nil {
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create metaindex file: %s", err)
}
@@ -100,7 +99,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
indexFile, err := filestream.Create(indexPath, nocache)
if err != nil {
metaindexFile.MustClose()
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create index file: %s", err)
}
@@ -109,7 +108,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
if err != nil {
metaindexFile.MustClose()
indexFile.MustClose()
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create items file: %s", err)
}
@@ -119,7 +118,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
metaindexFile.MustClose()
indexFile.MustClose()
itemsFile.MustClose()
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create lens file: %s", err)
}
@@ -152,6 +151,12 @@ func (bsw *blockStreamWriter) MustClose() {
bsw.itemsWriter.MustClose()
bsw.lensWriter.MustClose()
// Sync bsw.path contents to make sure it doesn't disappear
// after system crash or power loss.
if bsw.path != "" {
fs.MustSyncPath(bsw.path)
}
bsw.reset()
}

View File

@@ -29,7 +29,7 @@ func mergeBlockStreams(ph *partHeader, bsw *blockStreamWriter, bsrs []*blockStre
if err == errForciblyStopped {
return err
}
return fmt.Errorf("cannot merge block streams: %s", err)
return fmt.Errorf("cannot merge %d block streams: %s: %s", len(bsrs), bsrs, err)
}
var bsmPool = &sync.Pool{

View File

@@ -208,7 +208,7 @@ func (tb *Table) MustClose() {
logger.Infof("%d inmemory parts have been flushed to files in %s on %q", len(pws), time.Since(startTime), tb.path)
// Remove references to parts from the tb, so they may be eventually closed
// after all the seraches are done.
// after all the searches are done.
tb.partsLock.Lock()
parts := tb.parts
tb.parts = nil
@@ -846,22 +846,18 @@ func openParts(path string) ([]*partWrapper, error) {
}
txnDir := path + "/txn"
if err := os.RemoveAll(txnDir); err != nil {
return nil, fmt.Errorf("cannot remove %q: %s", txnDir, err)
}
fs.MustRemoveAll(txnDir)
if err := fs.MkdirAllFailIfExist(txnDir); err != nil {
return nil, fmt.Errorf("cannot create %q: %s", txnDir, err)
}
tmpDir := path + "/tmp"
if err := os.RemoveAll(tmpDir); err != nil {
return nil, fmt.Errorf("cannot remove %q: %s", tmpDir, err)
}
fs.MustRemoveAll(tmpDir)
if err := fs.MkdirAllFailIfExist(tmpDir); err != nil {
return nil, fmt.Errorf("cannot create %q: %s", tmpDir, err)
}
fs.SyncPath(path)
fs.MustSyncPath(path)
// Open parts.
fis, err := d.Readdir(-1)
@@ -965,9 +961,9 @@ func (tb *Table) CreateSnapshotAt(dstDir string) error {
}
}
fs.SyncPath(dstDir)
fs.MustSyncPath(dstDir)
parentDir := filepath.Dir(dstDir)
fs.SyncPath(parentDir)
fs.MustSyncPath(parentDir)
logger.Infof("created Table snapshot of %q at %q in %s", srcDir, dstDir, time.Since(startTime))
return nil
@@ -1033,9 +1029,7 @@ func runTransaction(txnLock *sync.RWMutex, pathPrefix, txnPath string) error {
if err != nil {
return fmt.Errorf("invalid path to remove: %s", err)
}
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("cannot remove %q: %s", path, err)
}
fs.MustRemoveAll(path)
}
// Move the new part to new directory.
@@ -1061,7 +1055,7 @@ func runTransaction(txnLock *sync.RWMutex, pathPrefix, txnPath string) error {
}
// Flush pathPrefix directory metadata to the underying storage.
fs.SyncPath(pathPrefix)
fs.MustSyncPath(pathPrefix)
// Remove the transaction file.
if err := os.Remove(txnPath); err != nil {

View File

@@ -33,6 +33,9 @@ func benchmarkTableSearch(b *testing.B, itemsCount int) {
// Force finishing pending merges
tb.MustClose()
tb, err = OpenTable(path)
if err != nil {
b.Fatalf("unexpected error when re-opening table %q: %s", path, err)
}
defer tb.MustClose()
keys := make([][]byte, len(items))

View File

@@ -47,8 +47,6 @@ type TCPListener struct {
net.Listener
name string
accepts *metrics.Counter
acceptErrors *metrics.Counter

View File

@@ -6,7 +6,7 @@ import (
"syscall"
)
// WaitForSigterm waits fro either SIGTERM or SIGINT
// WaitForSigterm waits for either SIGTERM or SIGINT
//
// Returns the caught signal.
func WaitForSigterm() os.Signal {

View File

@@ -11,13 +11,9 @@ import (
// ReadSnappy reads r, unpacks it using snappy, appends it to dst
// and returns the result.
func ReadSnappy(dst []byte, r io.Reader, maxSize int64) ([]byte, error) {
bb := bodyBufferPool.Get()
bb.B = bb.B[:0]
cb := copyBufferPool.Get()
cb.B = bytesutil.Resize(cb.B, 16*1024)
lr := io.LimitReader(r, maxSize+1)
reqLen, err := io.CopyBuffer(bb, lr, cb.B)
copyBufferPool.Put(cb)
bb := bodyBufferPool.Get()
reqLen, err := bb.ReadFrom(lr)
if err != nil {
bodyBufferPool.Put(bb)
return dst, fmt.Errorf("cannot read compressed request: %s", err)
@@ -45,7 +41,6 @@ func ReadSnappy(dst []byte, r io.Reader, maxSize int64) ([]byte, error) {
}
var bodyBufferPool bytesutil.ByteBufferPool
var copyBufferPool bytesutil.ByteBufferPool
// Reset resets wr.
func (wr *WriteRequest) Reset() {

View File

@@ -2,7 +2,6 @@ package storage
import (
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
@@ -85,7 +84,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
timestampsPath := path + "/timestamps.bin"
timestampsFile, err := filestream.Create(timestampsPath, nocache)
if err != nil {
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create timestamps file: %s", err)
}
@@ -93,7 +92,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
valuesFile, err := filestream.Create(valuesPath, nocache)
if err != nil {
timestampsFile.MustClose()
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create values file: %s", err)
}
@@ -102,7 +101,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
if err != nil {
timestampsFile.MustClose()
valuesFile.MustClose()
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create index file: %s", err)
}
@@ -114,7 +113,7 @@ func (bsw *blockStreamWriter) InitFromFilePart(path string, nocache bool, compre
timestampsFile.MustClose()
valuesFile.MustClose()
indexFile.MustClose()
_ = os.RemoveAll(path)
fs.MustRemoveAll(path)
return fmt.Errorf("cannot create metaindex file: %s", err)
}
@@ -147,6 +146,12 @@ func (bsw *blockStreamWriter) MustClose() {
bsw.indexWriter.MustClose()
bsw.metaindexWriter.MustClose()
// Sync bsw.path contents to make sure it doesn't disappear
// after system crash or power loss.
if bsw.path != "" {
fs.MustSyncPath(bsw.path)
}
bsw.reset()
}

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"sync"
@@ -15,6 +14,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/mergeset"
@@ -60,6 +60,10 @@ type indexDB struct {
// Cache for fast MetricID -> MetricName lookup.
metricNameCache *fastcache.Cache
// Cache holding useless TagFilters entries, which have no tag filters
// matching low number of metrics.
uselessTagFiltersCache *fastcache.Cache
indexSearchPool sync.Pool
// An inmemory map[uint64]struct{} of deleted metricIDs.
@@ -71,15 +75,46 @@ type indexDB struct {
deletedMetricIDs atomic.Value
deletedMetricIDsUpdateLock sync.Mutex
// Global lists of metric ids for the current and the previous hours.
// They are used for fast lookups on small time ranges covering
// up to two last hours.
currHourMetricIDs *atomic.Value
prevHourMetricIDs *atomic.Value
// The number of missing MetricID -> TSID entries.
// High rate for this value means corrupted indexDB.
missingTSIDsForMetricID uint64
// The number of calls to search for metric ids for recent hours.
recentHourMetricIDsSearchCalls uint64
// The number of cache hits during search for metric ids in recent hours.
recentHourMetricIDsSearchHits uint64
// The number of searches for metric ids by days.
dateMetricIDsSearchCalls uint64
// The number of successful searches for metric ids by days.
dateMetricIDsSearchHits uint64
mustDrop uint64
}
// openIndexDB opens index db from the given path with the given caches.
func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache) (*indexDB, error) {
func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache, currHourMetricIDs, prevHourMetricIDs *atomic.Value) (*indexDB, error) {
if metricIDCache == nil {
logger.Panicf("BUG: metricIDCache must be non-nil")
}
if metricNameCache == nil {
logger.Panicf("BUG: metricNameCache must be non-nil")
}
if currHourMetricIDs == nil {
logger.Panicf("BUG: currHourMetricIDs must be non-nil")
}
if prevHourMetricIDs == nil {
logger.Panicf("BUG: prevHourMetricIDs must be non-nil")
}
tb, err := mergeset.OpenTable(path)
if err != nil {
return nil, fmt.Errorf("cannot open indexDB %q: %s", path, err)
@@ -89,16 +124,19 @@ func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache) (
// Do not persist tagCache in files, since it is very volatile.
mem := memory.Allowed()
tagCache := fastcache.New(mem / 32)
db := &indexDB{
refCount: 1,
tb: tb,
name: name,
tagCache: tagCache,
metricIDCache: metricIDCache,
metricNameCache: metricNameCache,
tagCache: fastcache.New(mem / 32),
metricIDCache: metricIDCache,
metricNameCache: metricNameCache,
uselessTagFiltersCache: fastcache.New(mem / 128),
currHourMetricIDs: currHourMetricIDs,
prevHourMetricIDs: prevHourMetricIDs,
}
is := db.getIndexSearch()
@@ -119,12 +157,22 @@ type IndexDBMetrics struct {
TagCacheRequests uint64
TagCacheMisses uint64
UselessTagFiltersCacheSize uint64
UselessTagFiltersCacheBytesSize uint64
UselessTagFiltersCacheRequests uint64
UselessTagFiltersCacheMisses uint64
DeletedMetricsCount uint64
IndexDBRefCount uint64
MissingTSIDsForMetricID uint64
RecentHourMetricIDsSearchCalls uint64
RecentHourMetricIDsSearchHits uint64
DateMetricIDsSearchCalls uint64
DateMetricIDsSearchHits uint64
mergeset.TableMetrics
}
@@ -135,16 +183,29 @@ func (db *indexDB) scheduleToDrop() {
// UpdateMetrics updates m with metrics from the db.
func (db *indexDB) UpdateMetrics(m *IndexDBMetrics) {
var cs fastcache.Stats
cs.Reset()
db.tagCache.UpdateStats(&cs)
m.TagCacheSize += cs.EntriesCount
m.TagCacheBytesSize += cs.BytesSize
m.TagCacheRequests += cs.GetBigCalls
m.TagCacheMisses += cs.Misses
cs.Reset()
db.uselessTagFiltersCache.UpdateStats(&cs)
m.UselessTagFiltersCacheSize += cs.EntriesCount
m.UselessTagFiltersCacheBytesSize += cs.BytesSize
m.UselessTagFiltersCacheRequests += cs.GetBigCalls
m.UselessTagFiltersCacheMisses += cs.Misses
m.DeletedMetricsCount += uint64(len(db.getDeletedMetricIDs()))
m.IndexDBRefCount += atomic.LoadUint64(&db.refCount)
m.MissingTSIDsForMetricID += atomic.LoadUint64(&db.missingTSIDsForMetricID)
m.RecentHourMetricIDsSearchCalls += atomic.LoadUint64(&db.recentHourMetricIDsSearchCalls)
m.RecentHourMetricIDsSearchHits += atomic.LoadUint64(&db.recentHourMetricIDsSearchHits)
m.DateMetricIDsSearchCalls += atomic.LoadUint64(&db.dateMetricIDsSearchCalls)
m.DateMetricIDsSearchHits += atomic.LoadUint64(&db.dateMetricIDsSearchHits)
db.tb.UpdateMetrics(&m.TableMetrics)
db.doExtDB(func(extDB *indexDB) {
@@ -200,7 +261,7 @@ func (db *indexDB) incRef() {
func (db *indexDB) decRef() {
n := atomic.AddUint64(&db.refCount, ^uint64(0))
if n < 0 {
if int64(n) < 0 {
logger.Panicf("BUG: negative refCount: %d", n)
}
if n > 0 {
@@ -211,14 +272,21 @@ func (db *indexDB) decRef() {
db.tb.MustClose()
db.SetExtDB(nil)
// Free space occupied by caches owned by db.
db.tagCache.Reset()
db.uselessTagFiltersCache.Reset()
db.tagCache = nil
db.metricIDCache = nil
db.metricNameCache = nil
db.uselessTagFiltersCache = nil
if atomic.LoadUint64(&db.mustDrop) == 0 {
return
}
logger.Infof("dropping indexDB %q", tbPath)
if err := os.RemoveAll(tbPath); err != nil {
logger.Panicf("FATAL: cannot remove %q: %s", tbPath, err)
}
fs.MustRemoveAll(tbPath)
logger.Infof("indexDB %q has been dropped", tbPath)
}
@@ -273,8 +341,11 @@ func (db *indexDB) putMetricNameToCache(metricID uint64, metricName []byte) {
db.metricNameCache.Set(key[:], metricName)
}
func marshalTagFiltersKey(dst []byte, tfss []*TagFilters) []byte {
prefix := atomic.LoadUint64(&tagFiltersKeyGen)
func marshalTagFiltersKey(dst []byte, tfss []*TagFilters, versioned bool) []byte {
prefix := ^uint64(0)
if versioned {
prefix = atomic.LoadUint64(&tagFiltersKeyGen)
}
dst = encoding.MarshalUint64(dst, prefix)
for _, tfs := range tfss {
dst = append(dst, 0) // separator between tfs groups.
@@ -718,7 +789,7 @@ func (is *indexSearch) searchTagValues(tvs map[string]struct{}, prefix []byte, m
// up to two times - in db and extDB.
func (db *indexDB) GetSeriesCount() (uint64, error) {
is := db.getIndexSearch()
n, err := getSeriesCount(&is.ts, &is.kb)
n, err := is.getSeriesCount()
db.putIndexSearch(is)
if err != nil {
return 0, err
@@ -727,7 +798,7 @@ func (db *indexDB) GetSeriesCount() (uint64, error) {
var nExt uint64
ok := db.doExtDB(func(extDB *indexDB) {
is := extDB.getIndexSearch()
nExt, err = getSeriesCount(&is.ts, &is.kb)
nExt, err = is.getSeriesCount()
extDB.putIndexSearch(is)
})
if ok && err != nil {
@@ -872,7 +943,7 @@ func (db *indexDB) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int)
tfKeyBuf := tagFiltersKeyBufPool.Get()
defer tagFiltersKeyBufPool.Put(tfKeyBuf)
tfKeyBuf.B = marshalTagFiltersKey(tfKeyBuf.B[:0], tfss)
tfKeyBuf.B = marshalTagFiltersKey(tfKeyBuf.B[:0], tfss, true)
tsids, ok := db.getFromTagCache(tfKeyBuf.B)
if ok {
// Fast path - tsids found in the cache.
@@ -889,7 +960,12 @@ func (db *indexDB) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int)
var extTSIDs []TSID
if db.doExtDB(func(extDB *indexDB) {
tsids, ok := extDB.getFromTagCache(tfKeyBuf.B)
tfKeyExtBuf := tagFiltersKeyBufPool.Get()
defer tagFiltersKeyBufPool.Put(tfKeyExtBuf)
// Data in extDB cannot be changed, so use unversioned keys for tag cache.
tfKeyExtBuf.B = marshalTagFiltersKey(tfKeyExtBuf.B[:0], tfss, false)
tsids, ok := extDB.getFromTagCache(tfKeyExtBuf.B)
if ok {
extTSIDs = tsids
return
@@ -898,8 +974,7 @@ func (db *indexDB) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int)
extTSIDs, err = is.searchTSIDs(tfss, tr, maxMetrics)
extDB.putIndexSearch(is)
// Do not store found tsids into extDB.tagCache,
// since they will be stored into outer cache instead.
db.putToTagCache(tsids, tfKeyExtBuf.B)
}) {
if err != nil {
return nil, err
@@ -945,7 +1020,7 @@ func (is *indexSearch) getTSIDByMetricName(dst *TSID, metricName []byte) error {
if len(dmis) > 0 {
// Verify whether the dst is marked as deleted.
if _, deleted := dmis[dst.MetricID]; deleted {
// The dst is deleted. Continue seraching.
// The dst is deleted. Continue searching.
continue
}
}
@@ -1009,6 +1084,15 @@ func mergeTSIDs(a, b []TSID) []TSID {
}
func (is *indexSearch) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int) ([]TSID, error) {
// Verify whether `is` contains data for the given tr.
ok, err := is.containsTimeRange(tr)
if err != nil {
return nil, fmt.Errorf("error in containsTimeRange(%s): %s", &tr, err)
}
if !ok {
// Fast path: nothing to search.
return nil, nil
}
metricIDs, err := is.searchMetricIDs(tfss, tr, maxMetrics)
if err != nil {
return nil, err
@@ -1078,7 +1162,9 @@ func (is *indexSearch) getTSIDByMetricID(dst *TSID, metricID uint64) error {
return nil
}
func getSeriesCount(ts *mergeset.TableSearch, kb *bytesutil.ByteBuffer) (uint64, error) {
func (is *indexSearch) getSeriesCount() (uint64, error) {
ts := &is.ts
kb := &is.kb
var n uint64
kb.B = append(kb.B[:0], nsPrefixMetricIDToTSID)
ts.Seek(kb.B)
@@ -1095,9 +1181,9 @@ func getSeriesCount(ts *mergeset.TableSearch, kb *bytesutil.ByteBuffer) (uint64,
return n, nil
}
// searchMetricIDsMapByMetricNameMatch matches metricName values for the given srcMetricIDs against tfs
// updateMetricIDsByMetricNameMatch matches metricName values for the given srcMetricIDs against tfs
// and adds matching metrics to metricIDs.
func (is *indexSearch) searchMetricIDsMapByMetricNameMatch(metricIDs, srcMetricIDs map[uint64]struct{}, tfs []*tagFilter) error {
func (is *indexSearch) updateMetricIDsByMetricNameMatch(metricIDs, srcMetricIDs map[uint64]struct{}, tfs []*tagFilter) error {
// sort srcMetricIDs in order to speed up Seek below.
sortedMetricIDs := make([]uint64, 0, len(srcMetricIDs))
for metricID := range srcMetricIDs {
@@ -1132,7 +1218,66 @@ func (is *indexSearch) searchMetricIDsMapByMetricNameMatch(metricIDs, srcMetricI
return nil
}
func (is *indexSearch) getTagFilterWithMinMetricIDsMap(tfs *TagFilters, maxMetrics int) (*tagFilter, map[uint64]struct{}, error) {
func (is *indexSearch) getTagFilterWithMinMetricIDsCountAdaptive(tfs *TagFilters, maxMetrics int) (*tagFilter, map[uint64]struct{}, error) {
maxMetrics = is.adjustMaxMetricsAdaptive(maxMetrics)
kb := &is.kb
kb.B = tfs.marshal(kb.B[:0])
kb.B = encoding.MarshalUint64(kb.B, uint64(maxMetrics))
if len(is.db.uselessTagFiltersCache.Get(nil, kb.B)) > 0 {
// Skip useless work below, since the tfs doesn't contain tag filters matching less than maxMetrics metrics.
return nil, nil, errTooManyMetrics
}
// Iteratively increase maxAllowedMetrics up to maxMetrics in order to limit
// the time required for founding the tag filter with minimum matching metrics.
maxAllowedMetrics := 16
if maxAllowedMetrics > maxMetrics {
maxAllowedMetrics = maxMetrics
}
for {
minTf, minMetricIDs, err := is.getTagFilterWithMinMetricIDsCount(tfs, maxAllowedMetrics)
if err != nil {
return nil, nil, err
}
if len(minMetricIDs) < maxAllowedMetrics {
// Found the tag filter with the minimum number of metrics.
return minTf, minMetricIDs, nil
}
// Too many metrics matched.
if maxAllowedMetrics >= maxMetrics {
// The tag filter with minimum matching metrics matches at least maxMetrics metrics.
kb.B = tfs.marshal(kb.B[:0])
kb.B = encoding.MarshalUint64(kb.B, uint64(maxMetrics))
is.db.uselessTagFiltersCache.Set(kb.B, []byte("1"))
return nil, nil, errTooManyMetrics
}
// Increase maxAllowedMetrics and try again.
maxAllowedMetrics *= 4
if maxAllowedMetrics > maxMetrics {
maxAllowedMetrics = maxMetrics
}
}
}
var errTooManyMetrics = errors.New("all the tag filters match too many metrics")
func (is *indexSearch) adjustMaxMetricsAdaptive(maxMetrics int) int {
hmPrev := is.db.prevHourMetricIDs.Load().(*hourMetricIDs)
if !hmPrev.isFull {
return maxMetrics
}
hourMetrics := len(hmPrev.m)
if hourMetrics >= 256 && maxMetrics > hourMetrics/4 {
// It is cheaper to filter on the hour or day metrics if the minimum
// number of matching metrics across tfs exceeds hourMetrics / 4.
return hourMetrics / 4
}
return maxMetrics
}
func (is *indexSearch) getTagFilterWithMinMetricIDsCount(tfs *TagFilters, maxMetrics int) (*tagFilter, map[uint64]struct{}, error) {
var minMetricIDs map[uint64]struct{}
var minTf *tagFilter
for i := range tfs.tfs {
@@ -1141,7 +1286,7 @@ func (is *indexSearch) getTagFilterWithMinMetricIDsMap(tfs *TagFilters, maxMetri
// Skip negative filters.
continue
}
metricIDs, err := is.getMetricIDsMapForTagFilter(tf, maxMetrics)
metricIDs, err := is.getMetricIDsForTagFilter(tf, maxMetrics)
if err != nil {
if err == errFallbackToMetricNameMatch {
// Skip tag filters requiring to scan for too many metrics.
@@ -1167,7 +1312,7 @@ func (is *indexSearch) getTagFilterWithMinMetricIDsMap(tfs *TagFilters, maxMetri
// There is no positive filter with small number of matching metrics.
// Create it, so it matches all the MetricIDs for tfs.commonPrefix.
metricIDs := make(map[uint64]struct{})
if err := is.getMetricIDsMapForCommonPrefix(metricIDs, tfs.commonPrefix, maxMetrics); err != nil {
if err := is.updateMetricIDsForCommonPrefix(metricIDs, tfs.commonPrefix, maxMetrics); err != nil {
return nil, nil, err
}
return nil, metricIDs, nil
@@ -1236,84 +1381,60 @@ func matchTagFilter(b []byte, tf *tagFilter) (bool, error) {
}
func (is *indexSearch) searchMetricIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int) ([]uint64, error) {
metricIDsMap := make(map[uint64]struct{})
metricIDs := make(map[uint64]struct{})
for _, tfs := range tfss {
if len(tfs.tfs) == 0 {
// Return all the metric ids
if err := is.getMetricIDsMapForCommonPrefix(metricIDsMap, tfs.commonPrefix, maxMetrics+1); err != nil {
if err := is.updateMetricIDsForCommonPrefix(metricIDs, tfs.commonPrefix, maxMetrics+1); err != nil {
return nil, err
}
if len(metricIDsMap) > maxMetrics {
if len(metricIDs) > maxMetrics {
return nil, fmt.Errorf("the number or unique timeseries exceeds %d; either narrow down the search or increase -search.maxUniqueTimeseries", maxMetrics)
}
// Stop the iteration, since we cannot find more metric ids with the remaining tfss.
break
}
if err := is.searchMetricIDsMap(metricIDsMap, tfs, tr, maxMetrics+1); err != nil {
if err := is.updateMetricIDsForTagFilters(metricIDs, tfs, tr, maxMetrics+1); err != nil {
return nil, err
}
if len(metricIDsMap) > maxMetrics {
if len(metricIDs) > maxMetrics {
return nil, fmt.Errorf("the number or matching unique timeseries exceeds %d; either narrow down the search or increase -search.maxUniqueTimeseries", maxMetrics)
}
}
if len(metricIDsMap) == 0 {
if len(metricIDs) == 0 {
// Nothing found
return nil, nil
}
metricIDs := getSortedMetricIDs(metricIDsMap)
sortedMetricIDs := getSortedMetricIDs(metricIDs)
// Filter out deleted metricIDs.
dmis := is.db.getDeletedMetricIDs()
if len(dmis) > 0 {
metricIDsFiltered := metricIDs[:0]
for _, metricID := range metricIDs {
metricIDsFiltered := sortedMetricIDs[:0]
for _, metricID := range sortedMetricIDs {
if _, deleted := dmis[metricID]; !deleted {
metricIDsFiltered = append(metricIDsFiltered, metricID)
}
}
metricIDs = metricIDsFiltered
sortedMetricIDs = metricIDsFiltered
}
return metricIDs, nil
return sortedMetricIDs, nil
}
func (is *indexSearch) searchMetricIDsMap(metricIDs map[uint64]struct{}, tfs *TagFilters, tr TimeRange, maxMetrics int) error {
func (is *indexSearch) updateMetricIDsForTagFilters(metricIDs map[uint64]struct{}, tfs *TagFilters, tr TimeRange, maxMetrics int) error {
// Sort tag filters for faster ts.Seek below.
sort.Slice(tfs.tfs, func(i, j int) bool { return bytes.Compare(tfs.tfs[i].prefix, tfs.tfs[j].prefix) < 0 })
// Find the filter with minimum matching metrics.
// Iteratively increase maxAllowedMetrics up to maxMetrics in order to limit
// the time required for founding the tag filter with minimum matching metrics.
var minTf *tagFilter
var minMetricIDs map[uint64]struct{}
maxAllowedMetrics := 16
if maxAllowedMetrics > maxMetrics {
maxAllowedMetrics = maxMetrics
}
for {
var err error
minTf, minMetricIDs, err = is.getTagFilterWithMinMetricIDsMap(tfs, maxAllowedMetrics)
if err != nil {
minTf, minMetricIDs, err := is.getTagFilterWithMinMetricIDsCountAdaptive(tfs, maxMetrics)
if err != nil {
if err != errTooManyMetrics {
return err
}
if len(minMetricIDs) < maxAllowedMetrics {
// Found the tag filter with the minimum number of metrics.
break
}
// Too many metrics matched.
if maxAllowedMetrics < maxMetrics {
// Increase maxAllowedMetrics and try again.
maxAllowedMetrics *= 4
if maxAllowedMetrics > maxMetrics {
maxAllowedMetrics = maxMetrics
}
continue
}
// All the tag filters match too many metrics.
// The tag filter with minimum matching metrics matches at least maxMetrics.
//
// Slow path: try filtering the matching metrics by time range.
// This should work well for cases when old metrics are constantly substituted
// by big number of new metrics. For example, prometheus-operator creates many new
@@ -1324,11 +1445,8 @@ func (is *indexSearch) searchMetricIDsMap(metricIDs map[uint64]struct{}, tfs *Ta
maxTimeRangeMetrics := 20 * maxMetrics
metricIDsForTimeRange, err := is.getMetricIDsForTimeRange(tr, maxTimeRangeMetrics+1)
if err == errMissingMetricIDsForDate {
// Give up.
for metricID := range minMetricIDs {
metricIDs[metricID] = struct{}{}
}
return nil
return fmt.Errorf("cannot find tag filter matching less than %d time series; either increase -search.maxUniqueTimeseries or use more specific tag filters",
maxMetrics)
}
if err != nil {
return err
@@ -1339,7 +1457,6 @@ func (is *indexSearch) searchMetricIDsMap(metricIDs map[uint64]struct{}, tfs *Ta
}
minMetricIDs = metricIDsForTimeRange
minTf = nil
break
}
// Find intersection of minTf with other tfs.
@@ -1349,7 +1466,7 @@ func (is *indexSearch) searchMetricIDsMap(metricIDs map[uint64]struct{}, tfs *Ta
if tf == minTf {
continue
}
mIDs, err := is.intersectMetricIDsMapForTagFilter(tf, minMetricIDs)
mIDs, err := is.intersectMetricIDsWithTagFilter(tf, minMetricIDs)
if err == errFallbackToMetricNameMatch {
// The tag filter requires too many index scans. Postpone it,
// so tag filters with lower number of index scans may be applied.
@@ -1362,9 +1479,9 @@ func (is *indexSearch) searchMetricIDsMap(metricIDs map[uint64]struct{}, tfs *Ta
minMetricIDs = mIDs
}
for i, tf := range tfsPostponed {
mIDs, err := is.intersectMetricIDsMapForTagFilter(tf, minMetricIDs)
mIDs, err := is.intersectMetricIDsWithTagFilter(tf, minMetricIDs)
if err == errFallbackToMetricNameMatch {
return is.searchMetricIDsMapByMetricNameMatch(metricIDs, minMetricIDs, tfsPostponed[i:])
return is.updateMetricIDsByMetricNameMatch(metricIDs, minMetricIDs, tfsPostponed[i:])
}
if err != nil {
return err
@@ -1377,14 +1494,14 @@ func (is *indexSearch) searchMetricIDsMap(metricIDs map[uint64]struct{}, tfs *Ta
return nil
}
func (is *indexSearch) getMetricIDsMapForTagFilter(tf *tagFilter, maxMetrics int) (map[uint64]struct{}, error) {
func (is *indexSearch) getMetricIDsForTagFilter(tf *tagFilter, maxMetrics int) (map[uint64]struct{}, error) {
if tf.isNegative {
logger.Panicf("BUG: isNegative must be false")
}
metricIDs := make(map[uint64]struct{}, maxMetrics)
if len(tf.orSuffixes) > 0 {
// Fast path for orSuffixes - seek for rows for each value from orSuffxies.
if err := is.updateMetricIDsMapForOrSuffixesNoFilter(tf, maxMetrics, metricIDs); err != nil {
if err := is.updateMetricIDsForOrSuffixesNoFilter(tf, maxMetrics, metricIDs); err != nil {
return nil, err
}
return metricIDs, nil
@@ -1430,7 +1547,7 @@ func (is *indexSearch) getMetricIDsMapForTagFilter(tf *tagFilter, maxMetrics int
return metricIDs, nil
}
func (is *indexSearch) updateMetricIDsMapForOrSuffixesNoFilter(tf *tagFilter, maxMetrics int, metricIDs map[uint64]struct{}) error {
func (is *indexSearch) updateMetricIDsForOrSuffixesNoFilter(tf *tagFilter, maxMetrics int, metricIDs map[uint64]struct{}) error {
if tf.isNegative {
logger.Panicf("BUG: isNegative must be false")
}
@@ -1440,7 +1557,7 @@ func (is *indexSearch) updateMetricIDsMapForOrSuffixesNoFilter(tf *tagFilter, ma
kb.B = append(kb.B[:0], tf.prefix...)
kb.B = append(kb.B, orSuffix...)
kb.B = append(kb.B, tagSeparatorChar)
if err := is.updateMetricIDsMapForOrSuffixNoFilter(kb.B, maxMetrics, metricIDs); err != nil {
if err := is.updateMetricIDsForOrSuffixNoFilter(kb.B, maxMetrics, metricIDs); err != nil {
return err
}
if len(metricIDs) >= maxMetrics {
@@ -1450,7 +1567,7 @@ func (is *indexSearch) updateMetricIDsMapForOrSuffixesNoFilter(tf *tagFilter, ma
return nil
}
func (is *indexSearch) updateMetricIDsMapForOrSuffixesWithFilter(tf *tagFilter, metricIDs, filter map[uint64]struct{}) error {
func (is *indexSearch) updateMetricIDsForOrSuffixesWithFilter(tf *tagFilter, metricIDs, filter map[uint64]struct{}) error {
sortedFilter := getSortedMetricIDs(filter)
kb := kbPool.Get()
defer kbPool.Put(kb)
@@ -1458,14 +1575,14 @@ func (is *indexSearch) updateMetricIDsMapForOrSuffixesWithFilter(tf *tagFilter,
kb.B = append(kb.B[:0], tf.prefix...)
kb.B = append(kb.B, orSuffix...)
kb.B = append(kb.B, tagSeparatorChar)
if err := is.updateMetricIDsMapForOrSuffixWithFilter(kb.B, metricIDs, sortedFilter, tf.isNegative); err != nil {
if err := is.updateMetricIDsForOrSuffixWithFilter(kb.B, metricIDs, sortedFilter, tf.isNegative); err != nil {
return err
}
}
return nil
}
func (is *indexSearch) updateMetricIDsMapForOrSuffixNoFilter(prefix []byte, maxMetrics int, metricIDs map[uint64]struct{}) error {
func (is *indexSearch) updateMetricIDsForOrSuffixNoFilter(prefix []byte, maxMetrics int, metricIDs map[uint64]struct{}) error {
ts := &is.ts
maxLoops := maxMetrics * maxIndexScanLoopsPerMetric
loops := 0
@@ -1492,7 +1609,7 @@ func (is *indexSearch) updateMetricIDsMapForOrSuffixNoFilter(prefix []byte, maxM
return nil
}
func (is *indexSearch) updateMetricIDsMapForOrSuffixWithFilter(prefix []byte, metricIDs map[uint64]struct{}, sortedFilter []uint64, isNegative bool) error {
func (is *indexSearch) updateMetricIDsForOrSuffixWithFilter(prefix []byte, metricIDs map[uint64]struct{}, sortedFilter []uint64, isNegative bool) error {
ts := &is.ts
kb := &is.kb
for {
@@ -1533,7 +1650,7 @@ func (is *indexSearch) updateMetricIDsMapForOrSuffixWithFilter(prefix []byte, me
return nil
}
var errFallbackToMetricNameMatch = errors.New("fall back to searchMetricIDsMapByMetricNameMatch because of too many index scan loops")
var errFallbackToMetricNameMatch = errors.New("fall back to updateMetricIDsByMetricNameMatch because of too many index scan loops")
var errMissingMetricIDsForDate = errors.New("missing metricIDs for date")
@@ -1541,22 +1658,81 @@ func (is *indexSearch) getMetricIDsForTimeRange(tr TimeRange, maxMetrics int) (m
if tr.isZero() {
return nil, errMissingMetricIDsForDate
}
minDate := tr.MinTimestamp / msecPerDay
maxDate := tr.MaxTimestamp / msecPerDay
atomic.AddUint64(&is.db.recentHourMetricIDsSearchCalls, 1)
if metricIDs, ok := is.getMetricIDsForRecentHours(tr, maxMetrics); ok {
// Fast path: tr covers the current and / or the previous hour.
// Return the full list of metric ids for this time range.
atomic.AddUint64(&is.db.recentHourMetricIDsSearchHits, 1)
return metricIDs, nil
}
// Slow path: collect the metric ids for all the days covering the given tr.
atomic.AddUint64(&is.db.dateMetricIDsSearchCalls, 1)
minDate := uint64(tr.MinTimestamp) / msecPerDay
maxDate := uint64(tr.MaxTimestamp) / msecPerDay
if maxDate-minDate > 40 {
// Too much dates must be covered. Give up.
return nil, errMissingMetricIDsForDate
}
metricIDs := make(map[uint64]struct{}, maxMetrics)
for minDate <= maxDate {
if err := is.getMetricIDsForDate(uint64(minDate), metricIDs, maxMetrics); err != nil {
if err := is.getMetricIDsForDate(minDate, metricIDs, maxMetrics); err != nil {
return nil, err
}
minDate++
}
atomic.AddUint64(&is.db.dateMetricIDsSearchHits, 1)
return metricIDs, nil
}
func (is *indexSearch) getMetricIDsForRecentHours(tr TimeRange, maxMetrics int) (map[uint64]struct{}, bool) {
minHour := uint64(tr.MinTimestamp) / msecPerHour
maxHour := uint64(tr.MaxTimestamp) / msecPerHour
hmCurr := is.db.currHourMetricIDs.Load().(*hourMetricIDs)
if maxHour == hmCurr.hour && minHour == maxHour && hmCurr.isFull {
// The tr fits the current hour.
// Return a copy of hmCurr.m, because the caller may modify
// the returned map.
if len(hmCurr.m) > maxMetrics {
return nil, false
}
return getMetricIDsCopy(hmCurr.m), true
}
hmPrev := is.db.prevHourMetricIDs.Load().(*hourMetricIDs)
if maxHour == hmPrev.hour && minHour == maxHour && hmPrev.isFull {
// The tr fits the previous hour.
// Return a copy of hmPrev.m, because the caller may modify
// the returned map.
if len(hmPrev.m) > maxMetrics {
return nil, false
}
return getMetricIDsCopy(hmPrev.m), true
}
if maxHour == hmCurr.hour && minHour == hmPrev.hour && hmCurr.isFull && hmPrev.isFull {
// The tr spans the previous and the current hours.
if len(hmCurr.m)+len(hmPrev.m) > maxMetrics {
return nil, false
}
metricIDs := make(map[uint64]struct{}, len(hmCurr.m)+len(hmPrev.m))
for metricID := range hmCurr.m {
metricIDs[metricID] = struct{}{}
}
for metricID := range hmPrev.m {
metricIDs[metricID] = struct{}{}
}
return metricIDs, true
}
return nil, false
}
func getMetricIDsCopy(src map[uint64]struct{}) map[uint64]struct{} {
dst := make(map[uint64]struct{}, len(src))
for metricID := range src {
dst[metricID] = struct{}{}
}
return dst
}
func (db *indexDB) storeDateMetricID(date, metricID uint64) error {
is := db.getIndexSearch()
ok, err := is.hasDateMetricID(date, metricID)
@@ -1629,7 +1805,29 @@ func (is *indexSearch) getMetricIDsForDate(date uint64, metricIDs map[uint64]str
return nil
}
func (is *indexSearch) getMetricIDsMapForCommonPrefix(metricIDs map[uint64]struct{}, commonPrefix []byte, maxMetrics int) error {
func (is *indexSearch) containsTimeRange(tr TimeRange) (bool, error) {
ts := &is.ts
kb := &is.kb
// Verify whether the maximum date in `ts` covers tr.MinTimestamp.
minDate := uint64(tr.MinTimestamp) / msecPerDay
kb.B = marshalCommonPrefix(kb.B[:0], nsPrefixDateToMetricID)
kb.B = encoding.MarshalUint64(kb.B, minDate)
ts.Seek(kb.B)
if !ts.NextItem() {
if err := ts.Error(); err != nil {
return false, fmt.Errorf("error when searching for minDate=%d, prefix %q: %s", minDate, kb.B, err)
}
return false, nil
}
if !bytes.HasPrefix(ts.Item, kb.B[:1]) {
// minDate exceeds max date from ts.
return false, nil
}
return true, nil
}
func (is *indexSearch) updateMetricIDsForCommonPrefix(metricIDs map[uint64]struct{}, commonPrefix []byte, maxMetrics int) error {
ts := &is.ts
ts.Seek(commonPrefix)
for len(metricIDs) < maxMetrics && ts.NextItem() {
@@ -1654,11 +1852,11 @@ func (is *indexSearch) getMetricIDsMapForCommonPrefix(metricIDs map[uint64]struc
}
// The maximum number of index scan loops per already found metric.
// Bigger number of loops is slower than searchMetricIDsMapByMetricNameMatch
// Bigger number of loops is slower than updateMetricIDsByMetricNameMatch
// over the found metrics.
const maxIndexScanLoopsPerMetric = 32
func (is *indexSearch) intersectMetricIDsMapForTagFilter(tf *tagFilter, filter map[uint64]struct{}) (map[uint64]struct{}, error) {
func (is *indexSearch) intersectMetricIDsWithTagFilter(tf *tagFilter, filter map[uint64]struct{}) (map[uint64]struct{}, error) {
if len(filter) == 0 {
return nil, nil
}
@@ -1668,7 +1866,7 @@ func (is *indexSearch) intersectMetricIDsMapForTagFilter(tf *tagFilter, filter m
}
if len(tf.orSuffixes) > 0 {
// Fast path for orSuffixes - seek for rows for each value from orSuffixes.
if err := is.updateMetricIDsMapForOrSuffixesWithFilter(tf, metricIDs, filter); err != nil {
if err := is.updateMetricIDsForOrSuffixesWithFilter(tf, metricIDs, filter); err != nil {
return nil, err
}
return metricIDs, nil

View File

@@ -7,6 +7,7 @@ import (
"math/rand"
"os"
"reflect"
"sync/atomic"
"testing"
"time"
@@ -60,8 +61,14 @@ func TestIndexDBOpenClose(t *testing.T) {
metricNameCache := fastcache.New(1234)
defer metricIDCache.Reset()
defer metricNameCache.Reset()
var hmCurr atomic.Value
hmCurr.Store(&hourMetricIDs{})
var hmPrev atomic.Value
hmPrev.Store(&hourMetricIDs{})
for i := 0; i < 5; i++ {
db, err := openIndexDB("test-index-db", metricIDCache, metricNameCache)
db, err := openIndexDB("test-index-db", metricIDCache, metricNameCache, &hmCurr, &hmPrev)
if err != nil {
t.Fatalf("cannot open indexDB: %s", err)
}
@@ -82,8 +89,14 @@ func TestIndexDB(t *testing.T) {
metricNameCache := fastcache.New(1234)
defer metricIDCache.Reset()
defer metricNameCache.Reset()
var hmCurr atomic.Value
hmCurr.Store(&hourMetricIDs{})
var hmPrev atomic.Value
hmPrev.Store(&hourMetricIDs{})
dbName := "test-index-db-serial"
db, err := openIndexDB(dbName, metricIDCache, metricNameCache)
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
if err != nil {
t.Fatalf("cannot open indexDB: %s", err)
}
@@ -113,7 +126,7 @@ func TestIndexDB(t *testing.T) {
// Re-open the db and verify it works as expected.
db.MustClose()
db, err = openIndexDB(dbName, metricIDCache, metricNameCache)
db, err = openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
if err != nil {
t.Fatalf("cannot open indexDB: %s", err)
}
@@ -133,8 +146,14 @@ func TestIndexDB(t *testing.T) {
metricNameCache := fastcache.New(1234)
defer metricIDCache.Reset()
defer metricNameCache.Reset()
var hmCurr atomic.Value
hmCurr.Store(&hourMetricIDs{})
var hmPrev atomic.Value
hmPrev.Store(&hourMetricIDs{})
dbName := "test-index-db-concurrent"
db, err := openIndexDB(dbName, metricIDCache, metricNameCache)
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
if err != nil {
t.Fatalf("cannot open indexDB: %s", err)
}
@@ -293,6 +312,16 @@ func testIndexDBGetOrCreateTSIDByName(db *indexDB, accountsCount, projectsCount,
}
func testIndexDBCheckTSIDByName(db *indexDB, mns []MetricName, tsids []TSID, isConcurrent bool) error {
// fill Date -> MetricID cache
date := uint64(timestampFromTime(time.Now())) / msecPerDay
for i := range tsids {
tsid := &tsids[i]
if err := db.storeDateMetricID(date, tsid.MetricID); err != nil {
return fmt.Errorf("error in storeDateMetricID(%d, %d): %s", date, tsid.MetricID, err)
}
}
db.tb.DebugFlush()
hasValue := func(tvs []string, v []byte) bool {
for _, tv := range tvs {
if string(v) == tv {
@@ -382,6 +411,19 @@ func testIndexDBCheckTSIDByName(db *indexDB, mns []MetricName, tsids []TSID, isC
}
}
// Check timerseriesCounters only for serial test.
// Concurrent test may create duplicate timeseries, so GetSeriesCount
// would return more timeseries than needed.
if !isConcurrent {
n, err := db.GetSeriesCount()
if err != nil {
return fmt.Errorf("unexpected error in GetSeriesCount(): %s", err)
}
if n != uint64(len(timeseriesCounters)) {
return fmt.Errorf("unexpected GetSeriesCount(); got %d; want %d", n, uint64(len(timeseriesCounters)))
}
}
// Try tag filters.
for i := range mns {
mn := &mns[i]

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strconv"
"sync/atomic"
"testing"
"github.com/VictoriaMetrics/fastcache"
@@ -16,8 +17,14 @@ func BenchmarkIndexDBAddTSIDs(b *testing.B) {
metricNameCache := fastcache.New(1234)
defer metricIDCache.Reset()
defer metricNameCache.Reset()
var hmCurr atomic.Value
hmCurr.Store(&hourMetricIDs{})
var hmPrev atomic.Value
hmPrev.Store(&hourMetricIDs{})
const dbName = "bench-index-db-add-tsids"
db, err := openIndexDB(dbName, metricIDCache, metricNameCache)
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
if err != nil {
b.Fatalf("cannot open indexDB: %s", err)
}
@@ -76,8 +83,14 @@ func BenchmarkIndexDBSearchTSIDs(b *testing.B) {
metricNameCache := fastcache.New(1234)
defer metricIDCache.Reset()
defer metricNameCache.Reset()
var hmCurr atomic.Value
hmCurr.Store(&hourMetricIDs{})
var hmPrev atomic.Value
hmPrev.Store(&hourMetricIDs{})
const dbName = "bench-index-db-search-tsids"
db, err := openIndexDB(dbName, metricIDCache, metricNameCache)
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
if err != nil {
b.Fatalf("cannot open indexDB: %s", err)
}
@@ -88,8 +101,6 @@ func BenchmarkIndexDBSearchTSIDs(b *testing.B) {
}
}()
const accountsCount = 111
const projectsCount = 33333
const recordsCount = 1e5
// Fill the db with recordsCount records.
@@ -147,8 +158,14 @@ func BenchmarkIndexDBGetTSIDs(b *testing.B) {
metricNameCache := fastcache.New(1234)
defer metricIDCache.Reset()
defer metricNameCache.Reset()
var hmCurr atomic.Value
hmCurr.Store(&hourMetricIDs{})
var hmPrev atomic.Value
hmPrev.Store(&hourMetricIDs{})
const dbName = "bench-index-db-get-tsids"
db, err := openIndexDB(dbName, metricIDCache, metricNameCache)
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
if err != nil {
b.Fatalf("cannot open indexDB: %s", err)
}
@@ -160,8 +177,6 @@ func BenchmarkIndexDBGetTSIDs(b *testing.B) {
}()
const recordsPerLoop = 1000
const accountsCount = 111
const projectsCount = 33333
const recordsCount = 1e5
// Fill the db with recordsCount records.

View File

@@ -29,7 +29,7 @@ func mergeBlockStreams(ph *partHeader, bsw *blockStreamWriter, bsrs []*blockStre
if err == errForciblyStopped {
return err
}
return fmt.Errorf("cannot merge %d streams: %s", len(bsrs), err)
return fmt.Errorf("cannot merge %d streams: %s: %s", len(bsrs), bsrs, err)
}
var bsmPool = &sync.Pool{

View File

@@ -586,13 +586,6 @@ func copyTags(dst, src []Tag) []Tag {
return dst
}
func (mn *MetricName) tagsLess(i, j int) bool {
a, b := mn.Tags[i].Key, mn.Tags[j].Key
a = normalizeTagKey(a)
b = normalizeTagKey(b)
return bytes.Compare(a, b) < 0
}
var commonTagKeys = func() map[string][]byte {
lcm := map[string][]byte{
// job-like tags must go first in MetricName.Tags.

View File

@@ -212,14 +212,8 @@ func createPartition(timestamp int64, smallPartitionsPath, bigPartitionsPath str
// The pt must be detached from table before calling pt.Drop.
func (pt *partition) Drop() {
logger.Infof("dropping partition %q at smallPartsPath=%q, bigPartsPath=%q", pt.name, pt.smallPartsPath, pt.bigPartsPath)
if err := os.RemoveAll(pt.smallPartsPath); err != nil {
logger.Panicf("FATAL: cannot remove small parts directory %q: %s", pt.smallPartsPath, err)
}
if err := os.RemoveAll(pt.bigPartsPath); err != nil {
logger.Panicf("FATAL: cannot remove big parts directory %q: %s", pt.bigPartsPath, err)
}
fs.MustRemoveAll(pt.smallPartsPath)
fs.MustRemoveAll(pt.bigPartsPath)
logger.Infof("partition %q has been dropped", pt.name)
}
@@ -599,7 +593,7 @@ func (pt *partition) MustClose() {
logger.Infof("%d inmemory parts have been flushed to files in %s on %q", len(pws), time.Since(startTime), pt.smallPartsPath)
// Remove references to smallParts from the pt, so they may be eventually closed
// after all the seraches are done.
// after all the searches are done.
pt.partsLock.Lock()
smallParts := pt.smallParts
pt.smallParts = nil
@@ -1223,13 +1217,9 @@ func openParts(pathPrefix1, pathPrefix2, path string) ([]*partWrapper, error) {
}
txnDir := path + "/txn"
if err := os.RemoveAll(txnDir); err != nil {
return nil, fmt.Errorf("cannot delete transaction directory %q: %s", txnDir, err)
}
fs.MustRemoveAll(txnDir)
tmpDir := path + "/tmp"
if err := os.RemoveAll(tmpDir); err != nil {
return nil, fmt.Errorf("cannot remove temporary directory %q: %s", tmpDir, err)
}
fs.MustRemoveAll(tmpDir)
if err := createPartitionDirs(path); err != nil {
return nil, fmt.Errorf("cannot create directories for partition %q: %s", path, err)
}
@@ -1342,8 +1332,8 @@ func (pt *partition) createSnapshot(srcDir, dstDir string) error {
}
}
fs.SyncPath(dstDir)
fs.SyncPath(filepath.Dir(dstDir))
fs.MustSyncPath(dstDir)
fs.MustSyncPath(filepath.Dir(dstDir))
return nil
}
@@ -1408,9 +1398,7 @@ func runTransaction(txnLock *sync.RWMutex, pathPrefix1, pathPrefix2, txnPath str
if err != nil {
return fmt.Errorf("invalid path to remove: %s", err)
}
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("cannot remove %q: %s", path, err)
}
fs.MustRemoveAll(path)
}
// Move the new part to new directory.
@@ -1438,14 +1426,12 @@ func runTransaction(txnLock *sync.RWMutex, pathPrefix1, pathPrefix2, txnPath str
}
} else {
// Just remove srcPath.
if err := os.RemoveAll(srcPath); err != nil {
return fmt.Errorf("cannot remove %q: %s", srcPath, err)
}
fs.MustRemoveAll(srcPath)
}
// Flush pathPrefix* directory metadata to the underying storage.
fs.SyncPath(pathPrefix1)
fs.SyncPath(pathPrefix2)
fs.MustSyncPath(pathPrefix1)
fs.MustSyncPath(pathPrefix2)
// Remove the transaction file.
if err := os.Remove(txnPath); err != nil {
@@ -1487,6 +1473,6 @@ func createPartitionDirs(path string) error {
if err := fs.MkdirAllFailIfExist(tmpPath); err != nil {
return fmt.Errorf("cannot create tmp directory %q: %s", tmpPath, err)
}
fs.SyncPath(path)
fs.MustSyncPath(path)
return nil
}

View File

@@ -112,15 +112,15 @@ func testAppendPartsToMerge(t *testing.T, maxPartsToMerge int, initialRowsCount,
// Verify appending to prefix
prefix := []*partWrapper{
&partWrapper{
{
p: &part{
ph: partHeader{
RowsCount: 1234,
},
},
},
&partWrapper{},
&partWrapper{},
{},
{},
}
pms = appendPartsToMerge(prefix, pws, maxPartsToMerge, 1e9)
if !reflect.DeepEqual(pms[:len(prefix)], prefix) {

View File

@@ -19,7 +19,7 @@ type rawRow struct {
// Value is time series value for the given timestamp.
Value float64
// PrecisionBits is the number of the siginificant bits in the Value
// PrecisionBits is the number of the significant bits in the Value
// to store. Possible values are [1..64].
// 1 means max. 50% error, 2 - 25%, 3 - 12.5%, 64 means no error, i.e.
// Value stored without information loss.

View File

@@ -44,7 +44,7 @@ func TestSearchQueryMarshalUnmarshal(t *testing.T) {
if sq1.MaxTimestamp != sq2.MaxTimestamp {
t.Fatalf("unexpected MaxTimestamp; got %d; want %d", sq2.MaxTimestamp, sq1.MaxTimestamp)
}
if len(sq1.TagFilterss) != len(sq1.TagFilterss) {
if len(sq1.TagFilterss) != len(sq2.TagFilterss) {
t.Fatalf("unexpected TagFilterss len; got %d; want %d", len(sq2.TagFilterss), len(sq1.TagFilterss))
}
for ii := range sq1.TagFilterss {

View File

@@ -2,6 +2,7 @@ package storage
import (
"fmt"
"io/ioutil"
"math"
"os"
"path/filepath"
@@ -18,6 +19,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
"github.com/VictoriaMetrics/fastcache"
"golang.org/x/sys/unix"
)
@@ -30,7 +32,7 @@ type Storage struct {
cachePath string
retentionMonths int
// lock file for excluse access to the storage on the given path.
// lock file for exclusive access to the storage on the given path.
flockF *os.File
idbCurr atomic.Value
@@ -49,8 +51,20 @@ type Storage struct {
// dateMetricIDCache is (Date, MetricID) cache.
dateMetricIDCache *fastcache.Cache
stop chan struct{}
retentionWatcherWG sync.WaitGroup
// Fast cache for MetricID values occured during the current hour.
currHourMetricIDs atomic.Value
// Fast cache for MetricID values occured during the previous hour.
prevHourMetricIDs atomic.Value
// Pending MetricID values to be added to currHourMetricIDs.
pendingHourMetricIDsLock sync.Mutex
pendingHourMetricIDs map[uint64]struct{}
stop chan struct{}
currHourMetricIDsUpdaterWG sync.WaitGroup
retentionWatcherWG sync.WaitGroup
}
// OpenStorage opens storage on the given path with the given number of retention months.
@@ -99,13 +113,20 @@ func OpenStorage(path string, retentionMonths int) (*Storage, error) {
s.metricNameCache = s.mustLoadCache("MetricID->MetricName", "metricID_metricName", mem/8)
s.dateMetricIDCache = s.mustLoadCache("Date->MetricID", "date_metricID", mem/32)
hour := uint64(timestampFromTime(time.Now())) / msecPerHour
hmCurr := s.mustLoadHourMetricIDs(hour, "curr_hour_metric_ids")
hmPrev := s.mustLoadHourMetricIDs(hour-1, "prev_hour_metric_ids")
s.currHourMetricIDs.Store(hmCurr)
s.prevHourMetricIDs.Store(hmPrev)
s.pendingHourMetricIDs = make(map[uint64]struct{})
// Load indexdb
idbPath := path + "/indexdb"
idbSnapshotsPath := idbPath + "/snapshots"
if err := fs.MkdirAllIfNotExist(idbSnapshotsPath); err != nil {
return nil, fmt.Errorf("cannot create %q: %s", idbSnapshotsPath, err)
}
idbCurr, idbPrev, err := openIndexDBTables(idbPath, s.metricIDCache, s.metricNameCache)
idbCurr, idbPrev, err := openIndexDBTables(idbPath, s.metricIDCache, s.metricNameCache, &s.currHourMetricIDs, &s.prevHourMetricIDs)
if err != nil {
return nil, fmt.Errorf("cannot open indexdb tables at %q: %s", idbPath, err)
}
@@ -121,6 +142,7 @@ func OpenStorage(path string, retentionMonths int) (*Storage, error) {
}
s.tb = tb
s.startCurrHourMetricIDsUpdater()
s.startRetentionWatcher()
return s, nil
@@ -164,7 +186,7 @@ func (s *Storage) CreateSnapshot() (string, error) {
if err := fs.SymlinkRelative(bigDir, dstBigDir); err != nil {
return "", fmt.Errorf("cannot create symlink from %q to %q: %s", bigDir, dstBigDir, err)
}
fs.SyncPath(dstDataDir)
fs.MustSyncPath(dstDataDir)
idbSnapshot := fmt.Sprintf("%s/indexdb/snapshots/%s", s.path, snapshotName)
idb := s.idb()
@@ -184,8 +206,8 @@ func (s *Storage) CreateSnapshot() (string, error) {
return "", fmt.Errorf("cannot create symlink from %q to %q: %s", idbSnapshot, dstIdbDir, err)
}
fs.SyncPath(dstDir)
fs.SyncPath(srcDir + "/snapshots")
fs.MustSyncPath(dstDir)
fs.MustSyncPath(srcDir + "/snapshots")
logger.Infof("created Storage snapshot for %q at %q in %s", srcDir, dstDir, time.Since(startTime))
return snapshotName, nil
@@ -229,8 +251,8 @@ func (s *Storage) DeleteSnapshot(snapshotName string) error {
s.tb.MustDeleteSnapshot(snapshotName)
idbPath := fmt.Sprintf("%s/indexdb/snapshots/%s", s.path, snapshotName)
fs.MustRemoveAllSynced(idbPath)
fs.MustRemoveAllSynced(snapshotPath)
fs.MustRemoveAll(idbPath)
fs.MustRemoveAll(snapshotPath)
logger.Infof("deleted snapshot %q in %s", snapshotPath, time.Since(startTime))
@@ -273,6 +295,8 @@ type Metrics struct {
DateMetricIDCacheMisses uint64
DateMetricIDCacheCollisions uint64
HourMetricIDCacheSize uint64
IndexDBMetrics IndexDBMetrics
TableMetrics TableMetrics
}
@@ -316,6 +340,14 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
m.DateMetricIDCacheMisses += cs.Misses
m.DateMetricIDCacheCollisions += cs.Collisions
hmCurr := s.currHourMetricIDs.Load().(*hourMetricIDs)
hmPrev := s.prevHourMetricIDs.Load().(*hourMetricIDs)
hourMetricIDsLen := len(hmPrev.m)
if len(hmCurr.m) > hourMetricIDsLen {
hourMetricIDsLen = len(hmCurr.m)
}
m.HourMetricIDCacheSize += uint64(hourMetricIDsLen)
s.idb().UpdateMetrics(&m.IndexDBMetrics)
s.tb.UpdateMetrics(&m.TableMetrics)
}
@@ -340,11 +372,34 @@ func (s *Storage) retentionWatcher() {
}
}
func (s *Storage) startCurrHourMetricIDsUpdater() {
s.currHourMetricIDsUpdaterWG.Add(1)
go func() {
s.currHourMetricIDsUpdater()
s.currHourMetricIDsUpdaterWG.Done()
}()
}
var currHourMetricIDsUpdateInterval = time.Second * 10
func (s *Storage) currHourMetricIDsUpdater() {
t := time.NewTimer(currHourMetricIDsUpdateInterval)
for {
select {
case <-s.stop:
return
case <-t.C:
s.updateCurrHourMetricIDs()
t.Reset(currHourMetricIDsUpdateInterval)
}
}
}
func (s *Storage) mustRotateIndexDB() {
// Create new indexdb table.
newTableName := nextIndexDBTableName()
idbNewPath := s.path + "/indexdb/" + newTableName
idbNew, err := openIndexDB(idbNewPath, s.metricIDCache, s.metricNameCache)
idbNew, err := openIndexDB(idbNewPath, s.metricIDCache, s.metricNameCache, &s.currHourMetricIDs, &s.prevHourMetricIDs)
if err != nil {
logger.Panicf("FATAL: cannot create new indexDB at %q: %s", idbNewPath, err)
}
@@ -361,7 +416,7 @@ func (s *Storage) mustRotateIndexDB() {
s.idbCurr.Store(idbNew)
// Persist changes on the file system.
fs.SyncPath(s.path)
fs.MustSyncPath(s.path)
// Flush tsidCache, so idbNew can be populated with fresh data.
s.tsidCache.Reset()
@@ -378,6 +433,7 @@ func (s *Storage) MustClose() {
close(s.stop)
s.retentionWatcherWG.Wait()
s.currHourMetricIDsUpdaterWG.Wait()
s.tb.MustClose()
s.idb().MustClose()
@@ -388,12 +444,83 @@ func (s *Storage) MustClose() {
s.mustSaveCache(s.metricNameCache, "MetricID->MetricName", "metricID_metricName")
s.mustSaveCache(s.dateMetricIDCache, "Date->MetricID", "date_metricID")
hmCurr := s.currHourMetricIDs.Load().(*hourMetricIDs)
s.mustSaveHourMetricIDs(hmCurr, "curr_hour_metric_ids")
hmPrev := s.prevHourMetricIDs.Load().(*hourMetricIDs)
s.mustSaveHourMetricIDs(hmPrev, "prev_hour_metric_ids")
// Release lock file.
if err := s.flockF.Close(); err != nil {
logger.Panicf("FATAL: cannot close lock file %q: %s", s.flockF.Name(), err)
}
}
func (s *Storage) mustLoadHourMetricIDs(hour uint64, name string) *hourMetricIDs {
path := s.cachePath + "/" + name
logger.Infof("loading %s from %q...", name, path)
startTime := time.Now()
if !fs.IsPathExist(path) {
logger.Infof("nothing to load from %q", path)
return &hourMetricIDs{}
}
src, err := ioutil.ReadFile(path)
if err != nil {
logger.Panicf("FATAL: cannot read %s: %s", path, err)
}
srcOrigLen := len(src)
if len(src) < 24 {
logger.Errorf("discarding %s, since it has broken header; got %d bytes; want %d bytes", path, len(src), 24)
return &hourMetricIDs{}
}
isFull := encoding.UnmarshalUint64(src)
src = src[8:]
hourLoaded := encoding.UnmarshalUint64(src)
src = src[8:]
if hourLoaded != hour {
logger.Infof("discarding %s, since it is outdated", name)
return &hourMetricIDs{}
}
hmLen := encoding.UnmarshalUint64(src)
src = src[8:]
if uint64(len(src)) != 8*hmLen {
logger.Errorf("discarding %s, since it has broken body; got %d bytes; want %d bytes", path, len(src), 8*hmLen)
return &hourMetricIDs{}
}
m := make(map[uint64]struct{}, hmLen)
for i := uint64(0); i < hmLen; i++ {
metricID := encoding.UnmarshalUint64(src)
src = src[8:]
m[metricID] = struct{}{}
}
logger.Infof("loaded %s from %q in %s; entriesCount: %d; bytesSize: %d", name, path, time.Since(startTime), hmLen, srcOrigLen)
return &hourMetricIDs{
m: m,
hour: hourLoaded,
isFull: isFull != 0,
}
}
func (s *Storage) mustSaveHourMetricIDs(hm *hourMetricIDs, name string) {
path := s.cachePath + "/" + name
logger.Infof("saving %s to %q...", name, path)
startTime := time.Now()
dst := make([]byte, 0, len(hm.m)*8+24)
isFull := uint64(0)
if hm.isFull {
isFull = 1
}
dst = encoding.MarshalUint64(dst, isFull)
dst = encoding.MarshalUint64(dst, hm.hour)
dst = encoding.MarshalUint64(dst, uint64(len(hm.m)))
for metricID := range hm.m {
dst = encoding.MarshalUint64(dst, metricID)
}
if err := ioutil.WriteFile(path, dst, 0644); err != nil {
logger.Panicf("FATAL: cannot write %d bytes to %q: %s", len(dst), path, err)
}
logger.Infof("saved %s to %q in %s; entriesCount: %d; bytesSize: %d", name, path, time.Since(startTime), len(hm.m), len(dst))
}
func (s *Storage) mustLoadCache(info, name string, bytesSize int) *fastcache.Cache {
path := s.cachePath + "/" + name
logger.Infof("loading %s cache from %q...", info, path)
@@ -475,6 +602,39 @@ func (s *Storage) SearchTagValues(tagKey []byte, maxTagValues int) ([]string, er
return s.idb().SearchTagValues(tagKey, maxTagValues)
}
// SearchTagEntries returns a list of (tagName -> tagValues) for (accountID, projectID).
func (s *Storage) SearchTagEntries(maxTagKeys, maxTagValues int) ([]TagEntry, error) {
idb := s.idb()
keys, err := idb.SearchTagKeys(maxTagKeys)
if err != nil {
return nil, fmt.Errorf("cannot search tag keys: %s", err)
}
// Sort keys for faster seeks below
sort.Strings(keys)
tes := make([]TagEntry, len(keys))
for i, key := range keys {
values, err := idb.SearchTagValues([]byte(key), maxTagValues)
if err != nil {
return nil, fmt.Errorf("cannot search values for tag %q: %s", key, err)
}
te := &tes[i]
te.Key = key
te.Values = values
}
return tes, nil
}
// TagEntry contains (tagName -> tagValues) mapping
type TagEntry struct {
// Key is tagName
Key string
// Values contains all the values for Key.
Values []string
}
// GetSeriesCount returns the approximate number of unique time series.
//
// It includes the deleted series too and may count the same series
@@ -553,12 +713,13 @@ func (s *Storage) AddRows(mrs []MetricRow, precisionBits uint8) error {
// Limit the number of concurrent goroutines that may add rows to the storage.
// This should prevent from out of memory errors and CPU trashing when too many
// goroutines call AddRows.
t := time.NewTimer(addRowsTimeout)
t := timerpool.Get(addRowsTimeout)
select {
case addRowsConcurrencyCh <- struct{}{}:
t.Stop()
timerpool.Put(t)
defer func() { <-addRowsConcurrencyCh }()
case <-t.C:
timerpool.Put(t)
return fmt.Errorf("Cannot add %d rows to storage in %s, since it is overloaded with %d concurrent writers. Add more CPUs or reduce load",
len(mrs), addRowsTimeout, cap(addRowsConcurrencyCh))
}
@@ -656,13 +817,14 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
errors = s.updateDateMetricIDCache(rows, errors)
if len(errors) > 0 {
// Return only the first error, since it has no sense in returning all errors.
return rows, fmt.Errorf("errors occured during rows addition: %s", errors[0])
return rows, fmt.Errorf("errors occurred during rows addition: %s", errors[0])
}
return rows, nil
}
func (s *Storage) updateDateMetricIDCache(rows []rawRow, errors []error) []error {
var date uint64
var hour uint64
var prevTimestamp int64
kb := kbPool.Get()
defer kbPool.Put(kb)
@@ -674,17 +836,30 @@ func (s *Storage) updateDateMetricIDCache(rows []rawRow, errors []error) []error
r := &rows[i]
if r.Timestamp != prevTimestamp {
date = uint64(r.Timestamp) / msecPerDay
hour = uint64(r.Timestamp) / msecPerHour
prevTimestamp = r.Timestamp
}
metricID := r.TSID.MetricID
hm := s.currHourMetricIDs.Load().(*hourMetricIDs)
if hour == hm.hour {
// The r belongs to the current hour. Check for the current hour cache.
if _, ok := hm.m[metricID]; ok {
// Fast path: the metricID is in the current hour cache.
continue
}
s.pendingHourMetricIDsLock.Lock()
s.pendingHourMetricIDs[metricID] = struct{}{}
s.pendingHourMetricIDsLock.Unlock()
}
// Slower path: check global cache for (date, metricID) entry.
a[0] = date
a[1] = metricID
if s.dateMetricIDCache.Has(keyBuf) {
// Fast path: the (date, metricID) entry is in the cache.
continue
}
// Slow path: store the entry in the cache and in the indexDB.
// Slow path: store the entry in the (date, metricID) cache and in the indexDB.
// It is OK if the (date, metricID) entry is added multiple times to db
// by concurrent goroutines.
s.dateMetricIDCache.Set(keyBuf, nil)
@@ -696,6 +871,54 @@ func (s *Storage) updateDateMetricIDCache(rows []rawRow, errors []error) []error
return errors
}
func (s *Storage) updateCurrHourMetricIDs() {
hm := s.currHourMetricIDs.Load().(*hourMetricIDs)
s.pendingHourMetricIDsLock.Lock()
newMetricIDsLen := len(s.pendingHourMetricIDs)
s.pendingHourMetricIDsLock.Unlock()
hour := uint64(timestampFromTime(time.Now())) / msecPerHour
if newMetricIDsLen == 0 && hm.hour == hour {
// Fast path: nothing to update.
return
}
// Slow path: hm.m must be updated with non-empty s.pendingHourMetricIDs.
var m map[uint64]struct{}
isFull := hm.isFull
if hm.hour == hour {
m = make(map[uint64]struct{}, len(hm.m)+newMetricIDsLen)
for metricID := range hm.m {
m[metricID] = struct{}{}
}
} else {
m = make(map[uint64]struct{}, newMetricIDsLen)
isFull = true
}
s.pendingHourMetricIDsLock.Lock()
newMetricIDs := s.pendingHourMetricIDs
s.pendingHourMetricIDs = make(map[uint64]struct{}, len(newMetricIDs))
s.pendingHourMetricIDsLock.Unlock()
for metricID := range newMetricIDs {
m[metricID] = struct{}{}
}
hmNew := &hourMetricIDs{
m: m,
hour: hour,
isFull: isFull,
}
s.currHourMetricIDs.Store(hmNew)
if hm.hour != hour {
s.prevHourMetricIDs.Store(hm)
}
}
type hourMetricIDs struct {
m map[uint64]struct{}
hour uint64
isFull bool
}
func (s *Storage) getTSIDFromCache(dst *TSID, metricName []byte) bool {
buf := (*[unsafe.Sizeof(*dst)]byte)(unsafe.Pointer(dst))[:]
buf = s.tsidCache.Get(buf[:0], metricName)
@@ -707,7 +930,7 @@ func (s *Storage) putTSIDToCache(tsid *TSID, metricName []byte) {
s.tsidCache.Set(metricName, buf)
}
func openIndexDBTables(path string, metricIDCache, metricNameCache *fastcache.Cache) (curr, prev *indexDB, err error) {
func openIndexDBTables(path string, metricIDCache, metricNameCache *fastcache.Cache, currHourMetricIDs, prevHourMetricIDs *atomic.Value) (curr, prev *indexDB, err error) {
if err := fs.MkdirAllIfNotExist(path); err != nil {
return nil, nil, fmt.Errorf("cannot create directory %q: %s", path, err)
}
@@ -756,24 +979,22 @@ func openIndexDBTables(path string, metricIDCache, metricNameCache *fastcache.Ca
for _, tn := range tableNames[:len(tableNames)-2] {
pathToRemove := path + "/" + tn
logger.Infof("removing obsolete indexdb dir %q...", pathToRemove)
if err := os.RemoveAll(pathToRemove); err != nil {
return nil, nil, fmt.Errorf("cannot remove obsolete indexdb dir %q: %s", pathToRemove, err)
}
fs.MustRemoveAll(pathToRemove)
logger.Infof("removed obsolete indexdb dir %q", pathToRemove)
}
// Persist changes on the file system.
fs.SyncPath(path)
fs.MustSyncPath(path)
// Open the last two tables.
currPath := path + "/" + tableNames[len(tableNames)-1]
curr, err = openIndexDB(currPath, metricIDCache, metricNameCache)
curr, err = openIndexDB(currPath, metricIDCache, metricNameCache, currHourMetricIDs, prevHourMetricIDs)
if err != nil {
return nil, nil, fmt.Errorf("cannot open curr indexdb table at %q: %s", currPath, err)
}
prevPath := path + "/" + tableNames[len(tableNames)-2]
prev, err = openIndexDB(prevPath, metricIDCache, metricNameCache)
prev, err = openIndexDB(prevPath, metricIDCache, metricNameCache, currHourMetricIDs, prevHourMetricIDs)
if err != nil {
curr.MustClose()
return nil, nil, fmt.Errorf("cannot open prev indexdb table at %q: %s", prevPath, err)

View File

@@ -11,6 +11,184 @@ import (
"time"
)
func TestUpdateCurrHourMetricIDs(t *testing.T) {
newStorage := func() *Storage {
var s Storage
s.currHourMetricIDs.Store(&hourMetricIDs{})
s.prevHourMetricIDs.Store(&hourMetricIDs{})
s.pendingHourMetricIDs = make(map[uint64]struct{})
return &s
}
t.Run("empty_pedning_metric_ids_stale_curr_hour", func(t *testing.T) {
s := newStorage()
hour := uint64(timestampFromTime(time.Now())) / msecPerHour
hmOrig := &hourMetricIDs{
m: map[uint64]struct{}{
12: {},
34: {},
},
hour: 123,
}
s.currHourMetricIDs.Store(hmOrig)
s.updateCurrHourMetricIDs()
hmCurr := s.currHourMetricIDs.Load().(*hourMetricIDs)
if hmCurr.hour != hour {
// It is possible new hour occured. Update the hour and verify it again.
hour = uint64(timestampFromTime(time.Now())) / msecPerHour
if hmCurr.hour != hour {
t.Fatalf("unexpected hmCurr.hour; got %d; want %d", hmCurr.hour, hour)
}
}
if len(hmCurr.m) != 0 {
t.Fatalf("unexpected length of hm.m; got %d; want %d", len(hmCurr.m), 0)
}
if !hmCurr.isFull {
t.Fatalf("unexpected hmCurr.isFull; got %v; want %v", hmCurr.isFull, true)
}
hmPrev := s.prevHourMetricIDs.Load().(*hourMetricIDs)
if !reflect.DeepEqual(hmPrev, hmOrig) {
t.Fatalf("unexpected hmPrev; got %v; want %v", hmPrev, hmOrig)
}
if len(s.pendingHourMetricIDs) != 0 {
t.Fatalf("unexpected len(s.pendingHourMetricIDs); got %d; want %d", len(s.pendingHourMetricIDs), 0)
}
})
t.Run("empty_pedning_metric_ids_valid_curr_hour", func(t *testing.T) {
s := newStorage()
hour := uint64(timestampFromTime(time.Now())) / msecPerHour
hmOrig := &hourMetricIDs{
m: map[uint64]struct{}{
12: {},
34: {},
},
hour: hour,
}
s.currHourMetricIDs.Store(hmOrig)
s.updateCurrHourMetricIDs()
hmCurr := s.currHourMetricIDs.Load().(*hourMetricIDs)
if hmCurr.hour != hour {
// It is possible new hour occured. Update the hour and verify it again.
hour = uint64(timestampFromTime(time.Now())) / msecPerHour
if hmCurr.hour != hour {
t.Fatalf("unexpected hmCurr.hour; got %d; want %d", hmCurr.hour, hour)
}
// Do not run other checks, since they may fail.
return
}
if !reflect.DeepEqual(hmCurr, hmOrig) {
t.Fatalf("unexpected hmCurr; got %v; want %v", hmCurr, hmOrig)
}
if hmCurr.isFull {
t.Fatalf("unexpected hmCurr.isFull; got %v; want %v", hmCurr.isFull, false)
}
hmPrev := s.prevHourMetricIDs.Load().(*hourMetricIDs)
hmEmpty := &hourMetricIDs{}
if !reflect.DeepEqual(hmPrev, hmEmpty) {
t.Fatalf("unexpected hmPrev; got %v; want %v", hmPrev, hmEmpty)
}
if len(s.pendingHourMetricIDs) != 0 {
t.Fatalf("unexpected len(s.pendingHourMetricIDs); got %d; want %d", len(s.pendingHourMetricIDs), 0)
}
})
t.Run("nonempty_pending_metric_ids_stale_curr_hour", func(t *testing.T) {
s := newStorage()
pendingHourMetricIDs := map[uint64]struct{}{
343: {},
32424: {},
8293432: {},
}
s.pendingHourMetricIDs = pendingHourMetricIDs
hour := uint64(timestampFromTime(time.Now())) / msecPerHour
hmOrig := &hourMetricIDs{
m: map[uint64]struct{}{
12: {},
34: {},
},
hour: 123,
}
s.currHourMetricIDs.Store(hmOrig)
s.updateCurrHourMetricIDs()
hmCurr := s.currHourMetricIDs.Load().(*hourMetricIDs)
if hmCurr.hour != hour {
// It is possible new hour occured. Update the hour and verify it again.
hour = uint64(timestampFromTime(time.Now())) / msecPerHour
if hmCurr.hour != hour {
t.Fatalf("unexpected hmCurr.hour; got %d; want %d", hmCurr.hour, hour)
}
}
if !reflect.DeepEqual(hmCurr.m, pendingHourMetricIDs) {
t.Fatalf("unexpected hm.m; got %v; want %v", hmCurr.m, pendingHourMetricIDs)
}
if !hmCurr.isFull {
t.Fatalf("unexpected hmCurr.isFull; got %v; want %v", hmCurr.isFull, true)
}
hmPrev := s.prevHourMetricIDs.Load().(*hourMetricIDs)
if !reflect.DeepEqual(hmPrev, hmOrig) {
t.Fatalf("unexpected hmPrev; got %v; want %v", hmPrev, hmOrig)
}
if len(s.pendingHourMetricIDs) != 0 {
t.Fatalf("unexpected len(s.pendingHourMetricIDs); got %d; want %d", len(s.pendingHourMetricIDs), 0)
}
})
t.Run("nonempty_pending_metric_ids_valid_curr_hour", func(t *testing.T) {
s := newStorage()
pendingHourMetricIDs := map[uint64]struct{}{
343: {},
32424: {},
8293432: {},
}
s.pendingHourMetricIDs = pendingHourMetricIDs
hour := uint64(timestampFromTime(time.Now())) / msecPerHour
hmOrig := &hourMetricIDs{
m: map[uint64]struct{}{
12: {},
34: {},
},
hour: hour,
}
s.currHourMetricIDs.Store(hmOrig)
s.updateCurrHourMetricIDs()
hmCurr := s.currHourMetricIDs.Load().(*hourMetricIDs)
if hmCurr.hour != hour {
// It is possible new hour occured. Update the hour and verify it again.
hour = uint64(timestampFromTime(time.Now())) / msecPerHour
if hmCurr.hour != hour {
t.Fatalf("unexpected hmCurr.hour; got %d; want %d", hmCurr.hour, hour)
}
// Do not run other checks, since they may fail.
return
}
m := getMetricIDsCopy(pendingHourMetricIDs)
for metricID := range hmOrig.m {
m[metricID] = struct{}{}
}
if !reflect.DeepEqual(hmCurr.m, m) {
t.Fatalf("unexpected hm.m; got %v; want %v", hmCurr.m, m)
}
if hmCurr.isFull {
t.Fatalf("unexpected hmCurr.isFull; got %v; want %v", hmCurr.isFull, false)
}
hmPrev := s.prevHourMetricIDs.Load().(*hourMetricIDs)
hmEmpty := &hourMetricIDs{}
if !reflect.DeepEqual(hmPrev, hmEmpty) {
t.Fatalf("unexpected hmPrev; got %v; want %v", hmPrev, hmEmpty)
}
if len(s.pendingHourMetricIDs) != 0 {
t.Fatalf("unexpected len(s.pendingHourMetricIDs); got %d; want %d", len(s.pendingHourMetricIDs), 0)
}
})
}
func TestMetricRowMarshalUnmarshal(t *testing.T) {
var buf []byte
typ := reflect.TypeOf(&MetricRow{})

View File

@@ -164,10 +164,10 @@ func (tb *table) CreateSnapshot(snapshotName string) (string, string, error) {
}
}
fs.SyncPath(dstSmallDir)
fs.SyncPath(dstBigDir)
fs.SyncPath(filepath.Dir(dstSmallDir))
fs.SyncPath(filepath.Dir(dstBigDir))
fs.MustSyncPath(dstSmallDir)
fs.MustSyncPath(dstBigDir)
fs.MustSyncPath(filepath.Dir(dstSmallDir))
fs.MustSyncPath(filepath.Dir(dstBigDir))
logger.Infof("created table snapshot for %q at (%q, %q) in %s", tb.path, dstSmallDir, dstBigDir, time.Since(startTime))
return dstSmallDir, dstBigDir, nil
@@ -176,9 +176,9 @@ func (tb *table) CreateSnapshot(snapshotName string) (string, string, error) {
// MustDeleteSnapshot deletes snapshot with the given snapshotName.
func (tb *table) MustDeleteSnapshot(snapshotName string) {
smallDir := fmt.Sprintf("%s/small/snapshots/%s", tb.path, snapshotName)
fs.MustRemoveAllSynced(smallDir)
fs.MustRemoveAll(smallDir)
bigDir := fmt.Sprintf("%s/big/snapshots/%s", tb.path, snapshotName)
fs.MustRemoveAllSynced(bigDir)
fs.MustRemoveAll(bigDir)
}
func (tb *table) addPartitionNolock(pt *partition) {

View File

@@ -92,7 +92,7 @@ func createBenchTable(b *testing.B, path string, startTimestamp int64, rowsPerIn
r.Value = value
}
if err := tb.AddRows(rows); err != nil {
b.Fatalf("cannot add %d rows: %s", rowsPerInsert, err)
panic(fmt.Errorf("cannot add %d rows: %s", rowsPerInsert, err))
}
}
wg.Done()

View File

@@ -81,6 +81,13 @@ func (tfs *TagFilters) Reset() {
tfs.commonPrefix = marshalCommonPrefix(tfs.commonPrefix[:0], nsPrefixTagToMetricID)
}
func (tfs *TagFilters) marshal(dst []byte) []byte {
for i := range tfs.tfs {
dst = tfs.tfs[i].Marshal(dst)
}
return dst
}
// tagFilter represents a filter used for filtering tags.
type tagFilter struct {
key []byte

View File

@@ -67,3 +67,5 @@ func (tr *TimeRange) fromPartitionTime(t time.Time) {
}
const msecPerDay = 24 * 3600 * 1000
const msecPerHour = 3600 * 1000

View File

@@ -0,0 +1,38 @@
package timerpool
import (
"sync"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// Get returns a timer for the given duration d from the pool.
//
// Return back the timer to the pool with Put.
func Get(d time.Duration) *time.Timer {
if v := timerPool.Get(); v != nil {
t := v.(*time.Timer)
if t.Reset(d) {
logger.Panicf("BUG: active timer trapped to the pool!")
}
return t
}
return time.NewTimer(d)
}
// Put returns t to the pool.
//
// t cannot be accessed after returning to the pool.
func Put(t *time.Timer) {
if !t.Stop() {
// Drain t.C if it wasn't obtained by the caller yet.
select {
case <-t.C:
default:
}
}
timerPool.Put(t)
}
var timerPool sync.Pool

View File

@@ -17,6 +17,10 @@ const chunkSize = 64 * 1024
const bucketSizeBits = 40
const genSizeBits = 64 - bucketSizeBits
const maxGen = 1<<genSizeBits - 1
const maxBucketSize uint64 = 1 << bucketSizeBits
// Stats represents cache stats.
@@ -254,13 +258,13 @@ func (b *bucket) Reset() {
func (b *bucket) Clean() {
b.mu.Lock()
bGen := b.gen
bGen := b.gen & ((1 << genSizeBits) - 1)
bIdx := b.idx
bm := b.m
for k, v := range bm {
gen := v >> bucketSizeBits
idx := v & ((1 << bucketSizeBits) - 1)
if gen == bGen && idx < bIdx || gen+1 == bGen && idx >= bIdx {
if gen == bGen && idx < bIdx || gen+1 == bGen && idx >= bIdx || gen == maxGen && bGen == 1 && idx >= bIdx {
continue
}
delete(bm, k)
@@ -317,8 +321,8 @@ func (b *bucket) Set(k, v []byte, h uint64) {
idxNew = kvLen
chunkIdx = 0
b.gen++
if b.gen == 0 {
b.gen = 1
if b.gen&((1<<genSizeBits)-1) == 0 {
b.gen++
}
} else {
idx = chunkIdxNew * chunkSize
@@ -346,10 +350,11 @@ func (b *bucket) Get(dst, k []byte, h uint64, returnDst bool) ([]byte, bool) {
found := false
b.mu.RLock()
v := b.m[h]
bGen := b.gen & ((1 << genSizeBits) - 1)
if v > 0 {
gen := v >> bucketSizeBits
idx := v & ((1 << bucketSizeBits) - 1)
if gen == b.gen && idx < b.idx || gen+1 == b.gen && idx >= b.idx {
if gen == bGen && idx < b.idx || gen+1 == bGen && idx >= b.idx || gen == maxGen && bGen == 1 && idx >= b.idx {
chunkIdx := idx / chunkSize
if chunkIdx >= uint64(len(b.chunks)) {
// Corrupted data during the load from file. Just skip it.

View File

@@ -17,9 +17,7 @@ import (
//
// The returned counter is safe to use from concurrent goroutines.
func NewCounter(name string) *Counter {
c := &Counter{}
registerMetric(name, c)
return c
return defaultSet.NewCounter(name)
}
// Counter is a counter.
@@ -75,30 +73,5 @@ func (c *Counter) marshalTo(prefix string, w io.Writer) {
//
// Performance tip: prefer NewCounter instead of GetOrCreateCounter.
func GetOrCreateCounter(name string) *Counter {
metricsMapLock.Lock()
nm := metricsMap[name]
metricsMapLock.Unlock()
if nm == nil {
// Slow path - create and register missing counter.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Counter{},
}
metricsMapLock.Lock()
nm = metricsMap[name]
if nm == nil {
nm = nmNew
metricsMap[name] = nm
metricsList = append(metricsList, nm)
}
metricsMapLock.Unlock()
}
c, ok := nm.metric.(*Counter)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Counter. It is %T", name, nm.metric))
}
return c
return defaultSet.GetOrCreateCounter(name)
}

View File

@@ -19,14 +19,7 @@ import (
//
// The returned gauge is safe to use from concurrent goroutines.
func NewGauge(name string, f func() float64) *Gauge {
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
g := &Gauge{
f: f,
}
registerMetric(name, g)
return g
return defaultSet.NewGauge(name, f)
}
// Gauge is a float64 gauge.
@@ -66,35 +59,5 @@ func (g *Gauge) marshalTo(prefix string, w io.Writer) {
//
// Performance tip: prefer NewGauge instead of GetOrCreateGauge.
func GetOrCreateGauge(name string, f func() float64) *Gauge {
metricsMapLock.Lock()
nm := metricsMap[name]
metricsMapLock.Unlock()
if nm == nil {
// Slow path - create and register missing gauge.
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Gauge{
f: f,
},
}
metricsMapLock.Lock()
nm = metricsMap[name]
if nm == nil {
nm = nmNew
metricsMap[name] = nm
metricsList = append(metricsList, nm)
}
metricsMapLock.Unlock()
}
g, ok := nm.metric.(*Gauge)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Gauge. It is %T", name, nm.metric))
}
return g
return defaultSet.GetOrCreateGauge(name, f)
}

View File

@@ -8,25 +8,18 @@
// 1. Register the required metrics via New* functions.
// 2. Expose them to `/metrics` page via WritePrometheus.
// 3. Update the registered metrics during application lifetime.
//
// The package has been extracted from https://victoriametrics.com/
package metrics
import (
"fmt"
"io"
"runtime"
"sort"
"sync"
"time"
"github.com/valyala/histogram"
)
var (
metricsMapLock sync.Mutex
metricsList []*namedMetric
metricsMap = make(map[string]*namedMetric)
)
type namedMetric struct {
name string
metric metric
@@ -36,6 +29,8 @@ type metric interface {
marshalTo(prefix string, w io.Writer)
}
var defaultSet = NewSet()
// WritePrometheus writes all the registered metrics in Prometheus format to w.
//
// If exposeProcessMetrics is true, then various `go_*` metrics are exposed
@@ -48,17 +43,7 @@ type metric interface {
// })
//
func WritePrometheus(w io.Writer, exposeProcessMetrics bool) {
lessFunc := func(i, j int) bool {
return metricsList[i].name < metricsList[j].name
}
metricsMapLock.Lock()
if !sort.SliceIsSorted(metricsList, lessFunc) {
sort.Slice(metricsList, lessFunc)
}
for _, nm := range metricsList {
nm.metric.marshalTo(nm.name, w)
}
metricsMapLock.Unlock()
defaultSet.WritePrometheus(w)
if exposeProcessMetrics {
writeProcessMetrics(w)
}
@@ -118,25 +103,3 @@ func writeProcessMetrics(w io.Writer) {
fmt.Fprintf(w, "go_info_ext{compiler=%q, GOARCH=%q, GOOS=%q, GOROOT=%q} 1\n",
runtime.Compiler, runtime.GOARCH, runtime.GOOS, runtime.GOROOT())
}
var startTime = time.Now()
func registerMetric(name string, m metric) {
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
metricsMapLock.Lock()
nm, ok := metricsMap[name]
if !ok {
nm = &namedMetric{
name: name,
metric: m,
}
metricsMap[name] = nm
metricsList = append(metricsList, nm)
}
metricsMapLock.Unlock()
if ok {
panic(fmt.Errorf("BUG: metric %q is already registered", name))
}
}

308
vendor/github.com/VictoriaMetrics/metrics/set.go generated vendored Normal file
View File

@@ -0,0 +1,308 @@
package metrics
import (
"fmt"
"io"
"sort"
"sync"
"time"
)
// Set is a set of metrics.
//
// Metrics belonging to a set are exported separately from global metrics.
//
// Set.WritePrometheus must be called for exporting metrics from the set.
type Set struct {
mu sync.Mutex
a []*namedMetric
m map[string]*namedMetric
}
// NewSet creates new set of metrics.
func NewSet() *Set {
return &Set{
m: make(map[string]*namedMetric),
}
}
// WritePrometheus writes all the metrics from s to w in Prometheus format.
func (s *Set) WritePrometheus(w io.Writer) {
lessFunc := func(i, j int) bool {
return s.a[i].name < s.a[j].name
}
s.mu.Lock()
if !sort.SliceIsSorted(s.a, lessFunc) {
sort.Slice(s.a, lessFunc)
}
for _, nm := range s.a {
nm.metric.marshalTo(nm.name, w)
}
s.mu.Unlock()
}
// NewCounter registers and returns new counter with the given name in the s.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned counter is safe to use from concurrent goroutines.
func (s *Set) NewCounter(name string) *Counter {
c := &Counter{}
s.registerMetric(name, c)
return c
}
// GetOrCreateCounter returns registered counter in s with the given name
// or creates new counter if s doesn't contain counter with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned counter is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewCounter instead of GetOrCreateCounter.
func (s *Set) GetOrCreateCounter(name string) *Counter {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing counter.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Counter{},
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
c, ok := nm.metric.(*Counter)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Counter. It is %T", name, nm.metric))
}
return c
}
// NewGauge registers and returns gauge with the given name in s, which calls f
// to obtain gauge value.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// f must be safe for concurrent calls.
//
// The returned gauge is safe to use from concurrent goroutines.
func (s *Set) NewGauge(name string, f func() float64) *Gauge {
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
g := &Gauge{
f: f,
}
s.registerMetric(name, g)
return g
}
// GetOrCreateGauge returns registered gauge with the given name in s
// or creates new gauge if s doesn't contain gauge with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned gauge is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewGauge instead of GetOrCreateGauge.
func (s *Set) GetOrCreateGauge(name string, f func() float64) *Gauge {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing gauge.
if f == nil {
panic(fmt.Errorf("BUG: f cannot be nil"))
}
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &Gauge{
f: f,
},
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
g, ok := nm.metric.(*Gauge)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Gauge. It is %T", name, nm.metric))
}
return g
}
// NewSummary creates and returns new summary with the given name in s.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
func (s *Set) NewSummary(name string) *Summary {
return s.NewSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles)
}
// NewSummaryExt creates and returns new summary in s with the given name,
// window and quantiles.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
func (s *Set) NewSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
sm := newSummary(window, quantiles)
s.registerMetric(name, sm)
registerSummary(sm)
s.registerSummaryQuantiles(name, sm)
return sm
}
// GetOrCreateSummary returns registered summary with the given name in s
// or creates new summary if s doesn't contain summary with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewSummary instead of GetOrCreateSummary.
func (s *Set) GetOrCreateSummary(name string) *Summary {
return s.GetOrCreateSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles)
}
// GetOrCreateSummaryExt returns registered summary with the given name,
// window and quantiles in s or creates new summary if s doesn't
// contain summary with the given name.
//
// name must be valid Prometheus-compatible metric with possible lables.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned summary is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewSummaryExt instead of GetOrCreateSummaryExt.
func (s *Set) GetOrCreateSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing summary.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
sm := newSummary(window, quantiles)
nmNew := &namedMetric{
name: name,
metric: sm,
}
mustRegisterQuantiles := false
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
registerSummary(sm)
mustRegisterQuantiles = true
}
s.mu.Unlock()
if mustRegisterQuantiles {
s.registerSummaryQuantiles(name, sm)
}
}
sm, ok := nm.metric.(*Summary)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Summary. It is %T", name, nm.metric))
}
if sm.window != window {
panic(fmt.Errorf("BUG: invalid window requested for the summary %q; requested %s; need %s", name, window, sm.window))
}
if !isEqualQuantiles(sm.quantiles, quantiles) {
panic(fmt.Errorf("BUG: invalid quantiles requested from the summary %q; requested %v; need %v", name, quantiles, sm.quantiles))
}
return sm
}
func (s *Set) registerSummaryQuantiles(name string, sm *Summary) {
for i, q := range sm.quantiles {
quantileValueName := addTag(name, fmt.Sprintf(`quantile="%g"`, q))
qv := &quantileValue{
sm: sm,
idx: i,
}
s.registerMetric(quantileValueName, qv)
}
}
func (s *Set) registerMetric(name string, m metric) {
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
s.mu.Lock()
nm, ok := s.m[name]
if !ok {
nm = &namedMetric{
name: name,
metric: m,
}
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
if ok {
panic(fmt.Errorf("BUG: metric %q is already registered", name))
}
}

View File

@@ -38,7 +38,7 @@ type Summary struct {
//
// The returned summary is safe to use from concurrent goroutines.
func NewSummary(name string) *Summary {
return NewSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles)
return defaultSet.NewSummary(name)
}
// NewSummaryExt creates and returns new summary with the given name,
@@ -53,36 +53,21 @@ func NewSummary(name string) *Summary {
//
// The returned summary is safe to use from concurrent goroutines.
func NewSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
s := newSummary(window, quantiles)
registerMetric(name, s)
registerSummary(s)
registerSummaryQuantiles(name, s)
return s
return defaultSet.NewSummaryExt(name, window, quantiles)
}
func newSummary(window time.Duration, quantiles []float64) *Summary {
// Make a copy of quantiles in order to prevent from their modification by the caller.
quantiles = append([]float64{}, quantiles...)
validateQuantiles(quantiles)
s := &Summary{
sm := &Summary{
curr: histogram.NewFast(),
next: histogram.NewFast(),
quantiles: quantiles,
quantileValues: make([]float64, len(quantiles)),
window: window,
}
return s
}
func registerSummaryQuantiles(name string, s *Summary) {
for i, q := range s.quantiles {
quantileValueName := addTag(name, fmt.Sprintf(`quantile="%g"`, q))
qv := &quantileValue{
s: s,
idx: i,
}
registerMetric(quantileValueName, qv)
}
return sm
}
func validateQuantiles(quantiles []float64) {
@@ -94,29 +79,29 @@ func validateQuantiles(quantiles []float64) {
}
// Update updates the summary.
func (s *Summary) Update(v float64) {
s.mu.Lock()
s.curr.Update(v)
s.next.Update(v)
s.mu.Unlock()
func (sm *Summary) Update(v float64) {
sm.mu.Lock()
sm.curr.Update(v)
sm.next.Update(v)
sm.mu.Unlock()
}
// UpdateDuration updates request duration based on the given startTime.
func (s *Summary) UpdateDuration(startTime time.Time) {
func (sm *Summary) UpdateDuration(startTime time.Time) {
d := time.Since(startTime).Seconds()
s.Update(d)
sm.Update(d)
}
func (s *Summary) marshalTo(prefix string, w io.Writer) {
// Just update s.quantileValues and don't write anything to w.
// s.quantileValues will be marshaled later via quantileValue.marshalTo.
s.updateQuantiles()
func (sm *Summary) marshalTo(prefix string, w io.Writer) {
// Just update sm.quantileValues and don't write anything to w.
// sm.quantileValues will be marshaled later via quantileValue.marshalTo.
sm.updateQuantiles()
}
func (s *Summary) updateQuantiles() {
s.mu.Lock()
s.quantileValues = s.curr.Quantiles(s.quantileValues[:0], s.quantiles)
s.mu.Unlock()
func (sm *Summary) updateQuantiles() {
sm.mu.Lock()
sm.quantileValues = sm.curr.Quantiles(sm.quantileValues[:0], sm.quantiles)
sm.mu.Unlock()
}
// GetOrCreateSummary returns registered summary with the given name
@@ -134,7 +119,7 @@ func (s *Summary) updateQuantiles() {
//
// Performance tip: prefer NewSummary instead of GetOrCreateSummary.
func GetOrCreateSummary(name string) *Summary {
return GetOrCreateSummaryExt(name, defaultSummaryWindow, defaultSummaryQuantiles)
return defaultSet.GetOrCreateSummary(name)
}
// GetOrCreateSummaryExt returns registered summary with the given name,
@@ -152,45 +137,7 @@ func GetOrCreateSummary(name string) *Summary {
//
// Performance tip: prefer NewSummaryExt instead of GetOrCreateSummaryExt.
func GetOrCreateSummaryExt(name string, window time.Duration, quantiles []float64) *Summary {
metricsMapLock.Lock()
nm := metricsMap[name]
metricsMapLock.Unlock()
if nm == nil {
// Slow path - create and register missing summary.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
s := newSummary(window, quantiles)
nmNew := &namedMetric{
name: name,
metric: s,
}
mustRegisterQuantiles := false
metricsMapLock.Lock()
nm = metricsMap[name]
if nm == nil {
nm = nmNew
metricsMap[name] = nm
metricsList = append(metricsList, nm)
registerSummary(s)
mustRegisterQuantiles = true
}
metricsMapLock.Unlock()
if mustRegisterQuantiles {
registerSummaryQuantiles(name, s)
}
}
s, ok := nm.metric.(*Summary)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Summary. It is %T", name, nm.metric))
}
if s.window != window {
panic(fmt.Errorf("BUG: invalid window requested for the summary %q; requested %s; need %s", name, window, s.window))
}
if !isEqualQuantiles(s.quantiles, quantiles) {
panic(fmt.Errorf("BUG: invalid quantiles requested from the summary %q; requested %v; need %v", name, quantiles, s.quantiles))
}
return s
return defaultSet.GetOrCreateSummaryExt(name, window, quantiles)
}
func isEqualQuantiles(a, b []float64) bool {
@@ -207,14 +154,14 @@ func isEqualQuantiles(a, b []float64) bool {
}
type quantileValue struct {
s *Summary
sm *Summary
idx int
}
func (qv *quantileValue) marshalTo(prefix string, w io.Writer) {
qv.s.mu.Lock()
v := qv.s.quantileValues[qv.idx]
qv.s.mu.Unlock()
qv.sm.mu.Lock()
v := qv.sm.quantileValues[qv.idx]
qv.sm.mu.Unlock()
if !math.IsNaN(v) {
fmt.Fprintf(w, "%s %g\n", prefix, v)
}
@@ -227,10 +174,10 @@ func addTag(name, tag string) string {
return fmt.Sprintf("%s,%s}", name[:len(name)-1], tag)
}
func registerSummary(s *Summary) {
window := s.window
func registerSummary(sm *Summary) {
window := sm.window
summariesLock.Lock()
summaries[window] = append(summaries[window], s)
summaries[window] = append(summaries[window], sm)
if len(summaries[window]) == 1 {
go summariesSwapCron(window)
}
@@ -241,13 +188,13 @@ func summariesSwapCron(window time.Duration) {
for {
time.Sleep(window / 2)
summariesLock.Lock()
for _, s := range summaries[window] {
s.mu.Lock()
tmp := s.curr
s.curr = s.next
s.next = tmp
s.next.Reset()
s.mu.Unlock()
for _, sm := range summaries[window] {
sm.mu.Lock()
tmp := sm.curr
sm.curr = sm.next
sm.next = tmp
sm.next.Reset()
sm.mu.Unlock()
}
summariesLock.Unlock()
}

View File

@@ -56,14 +56,18 @@ There is also [StreamDecompress](https://godoc.org/github.com/valyala/gozstd#Str
and [Reader](https://godoc.org/github.com/valyala/gozstd#Reader) for stream decompression.
### Who uses gozstd?
* [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
## FAQ
* Q: _Which go version is supported?_
A: `go1.10` and newer. Pull requests for older go versions support
are accepted.
A: `go1.10` and newer. Pull requests for older go versions are accepted.
* Q: _Which platforms/architectures are supported?_
A: `linux/amd64`, `linux/arm`, `linux/arm64`, `darwin/amd64`, `windows/amd64`. Pull requests for other platforms/architectures support
A: `linux/amd64`, `linux/arm`, `linux/arm64`, `darwin/amd64`, `windows/amd64`. Pull requests for other platforms/architectures
are accepted.
* Q: _I don't trust `libzstd*.a` binary files from the repo or these files dont't work on my OS/ARCH. How to rebuild them?_

4
vendor/github.com/valyala/gozstd/doc.go generated vendored Normal file
View File

@@ -0,0 +1,4 @@
// Package gozstd is Go wrapper for zstd.
//
// Gozstd is used in https://github.com/VictoriaMetrics/VictoriaMetrics .
package gozstd

Binary file not shown.

54
vendor/golang.org/x/sys/unix/asm_linux_riscv64.s generated vendored Normal file
View File

@@ -0,0 +1,54 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build riscv64,!gccgo
#include "textflag.h"
//
// System calls for linux/riscv64.
//
// Where available, just jump to package syscall's implementation of
// these functions.
TEXT ·Syscall(SB),NOSPLIT,$0-56
JMP syscall·Syscall(SB)
TEXT ·Syscall6(SB),NOSPLIT,$0-80
JMP syscall·Syscall6(SB)
TEXT ·SyscallNoError(SB),NOSPLIT,$0-48
CALL runtime·entersyscall(SB)
MOV a1+8(FP), A0
MOV a2+16(FP), A1
MOV a3+24(FP), A2
MOV $0, A3
MOV $0, A4
MOV $0, A5
MOV $0, A6
MOV trap+0(FP), A7 // syscall entry
ECALL
MOV A0, r1+32(FP) // r1
MOV A1, r2+40(FP) // r2
CALL runtime·exitsyscall(SB)
RET
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
JMP syscall·RawSyscall(SB)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
JMP syscall·RawSyscall6(SB)
TEXT ·RawSyscallNoError(SB),NOSPLIT,$0-48
MOV a1+8(FP), A0
MOV a2+16(FP), A1
MOV a3+24(FP), A2
MOV ZERO, A3
MOV ZERO, A4
MOV ZERO, A5
MOV trap+0(FP), A7 // syscall entry
ECALL
MOV A0, r1+32(FP)
MOV A1, r2+40(FP)
RET

View File

@@ -105,25 +105,25 @@ dragonfly_amd64)
freebsd_386)
mkerrors="$mkerrors -m32"
mksyscall="go run mksyscall.go -l32"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/10/sys/kern/syscalls.master'"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/11/sys/kern/syscalls.master'"
mktypes="GOARCH=$GOARCH go tool cgo -godefs"
;;
freebsd_amd64)
mkerrors="$mkerrors -m64"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/10/sys/kern/syscalls.master'"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/11/sys/kern/syscalls.master'"
mktypes="GOARCH=$GOARCH go tool cgo -godefs"
;;
freebsd_arm)
mkerrors="$mkerrors"
mksyscall="go run mksyscall.go -l32 -arm"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/10/sys/kern/syscalls.master'"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/11/sys/kern/syscalls.master'"
# Let the type of C char be signed for making the bare syscall
# API consistent across platforms.
mktypes="GOARCH=$GOARCH go tool cgo -godefs -- -fsigned-char"
;;
freebsd_arm64)
mkerrors="$mkerrors -m64"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/10/sys/kern/syscalls.master'"
mksysnum="go run mksysnum.go 'https://svn.freebsd.org/base/stable/11/sys/kern/syscalls.master'"
mktypes="GOARCH=$GOARCH go tool cgo -godefs"
;;
netbsd_386)

Some files were not shown because too many files have changed in this diff Show More