Compare commits

...

188 Commits

Author SHA1 Message Date
Aliaksandr Valialkin
62a7353479 app/vmselect/prometheus: set start arg in /api/v1/series to the minimum allowed time by default as Prometheus does
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/91
2019-07-11 17:10:14 +03:00
Aliaksandr Valialkin
54bd21eb4a app/vmselect/prometheus: convert negative times to 0, since they arent supported by the storage 2019-07-11 17:07:20 +03:00
Aliaksandr Valialkin
2bd1a01d1a lib/storage: do not pollute inverted index with data for samples outside the retention period 2019-07-11 17:04:56 +03:00
Artem Navoiev
cd4833d3d0 integration tests 2019-07-11 15:48:08 +03:00
Aliaksandr Valialkin
101fa258e5 app/vmstorage: prepare for integration tests with multiple Init / Stop cycles 2019-07-11 15:34:50 +03:00
Aliaksandr Valialkin
d031e04023 lib/storage: use fast path for orSuffix when searching for metricIDs against plain tag value 2019-07-11 14:48:37 +03:00
Aliaksandr Valialkin
43ea4ce428 lib/storage: remember and skip individual tag filters matching too many metrics
This saves CPU time by skipping useless matching for individual tag filters.
2019-07-11 14:48:30 +03:00
Aliaksandr Valialkin
a336bb4e22 app/vmselect/promql: reduce RAM usage for aggregates over big number of time series
Calculate incremental aggregates for `aggr(metric_selector)` function instead of
keeping all the time series matching the given `metric_selector` in memory.
2019-07-10 13:04:39 +03:00
Aliaksandr Valialkin
1fe6d784d8 all: consistency renaming: bytesSize -> sizeBytes 2019-07-10 00:47:36 +03:00
Aliaksandr Valialkin
55fe36149c app/vmselect/promql: mention -search.logSlowQueryDuration flag value in the slow query log message 2019-07-10 00:41:24 +03:00
Aliaksandr Valialkin
9203170eb2 app/vmselect/promql: extract rmoeveGroupTags function for removing unneeded tags from MetricName according to the given modifierExpr 2019-07-09 23:20:48 +03:00
Aliaksandr Valialkin
2db685c19c app/vmselect/promql: properly preserve metric name after applying functions in any case from transformFuncsKeepMetricGroup 2019-07-09 23:10:35 +03:00
Aliaksandr Valialkin
6ddfb06b52 README.md: add alerting section 2019-07-08 22:45:34 +03:00
Aliaksandr Valialkin
40a6c0d672 app/vmselect/prometheus: typo fix 2019-07-07 23:34:23 +03:00
Aliaksandr Valialkin
1371024747 app/vmselect/prometheus: handle minTime and maxTime values that may be set by Promxy or Prometheus client
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/88
2019-07-07 21:53:48 +03:00
Roman Khavronenko
c27c6de297 add panels for Active time series, Disk space usage (datapoints) and Disk space usage (index) (#87) 2019-07-04 22:15:24 +03:00
Aliaksandr Valialkin
0c629429de README.md: clarify upgrading and applying new config sections 2019-07-04 20:07:00 +03:00
Aliaksandr Valialkin
4dbd642c86 app/vmselect/promql: remove empty timeseries left after topk call 2019-07-04 19:42:39 +03:00
Aliaksandr Valialkin
56c154f45b all: add vm_data_size_bytes metrics for easy monitoring of on-disk data size and on-disk inverted index size 2019-07-04 19:42:30 +03:00
Aliaksandr Valialkin
8d83dcf332 README.md: update community and contributions section 2019-07-04 09:36:36 +03:00
Aliaksandr Valialkin
9a4b2b8315 app/vmselect/prometheus: update adjustLastPoints function
- Do not overwrite last points by the previous NaNs, since this may result in empty time series.
- Overwrite the last 2 points instead of 3. This should be enough in most cases.
2019-07-04 09:14:18 +03:00
Aliaksandr Valialkin
e06866005d app/vmselect/promql: gracefully handle duplicate timestamps in irate and rollup_rate funcs
Previously such timestamps result in `+Inf` results. Now the previous timestamp is used
for the calculations.
2019-07-03 12:39:55 +03:00
Aliaksandr Valialkin
2c76a9c9ab README.md: enumerate the most interesting metrics exported at /metrics page 2019-07-01 23:41:08 +03:00
Aliaksandr Valialkin
b9166a60ff app/vmselect: do not return empty time series in /api/v1/query result 2019-07-01 17:16:34 +03:00
Aliaksandr Valialkin
c7034fc51b lib/memory: attempt #3 to determine memory limit for LXC container
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/84
2019-07-01 14:01:13 +03:00
Aliaksandr Valialkin
715c423f1a README.md: mention Thanos vs VictoriaMetrics article 2019-07-01 12:26:47 +03:00
Aliaksandr Valialkin
ca74e29458 README.md: explain how to configure HA setup for Prometheus HA pairs 2019-06-29 19:54:46 +03:00
Aliaksandr Valialkin
a41955863a lib/mergeset: make fmt 2019-06-29 14:25:26 +03:00
Aliaksandr Valialkin
2ecb117082 lib/storage: skip non-matching metricIDs in sortedFilter
This should improve performance for big sorteFilter lists.
2019-06-29 13:48:32 +03:00
Aliaksandr Valialkin
0c88afa386 lib/mergeset: speed up binarySearchKey by skipping the first item during binary search 2019-06-29 13:45:49 +03:00
Aliaksandr Valialkin
74c0fb04f3 app/vmselect/promql: consistency renaming: candlestick -> rollup_candlestick 2019-06-29 03:13:02 +03:00
Aliaksandr Valialkin
828078eb45 lib/memory: remove TestReadLXCMemoryLimit, since it doesnt work in Travis 2019-06-28 18:22:46 +03:00
Aliaksandr Valialkin
7b59466667 lib/memory: attempt #2 to determine memory limit inside LXC container
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/84
2019-06-28 18:08:15 +03:00
Aliaksandr Valialkin
79ac02ba74 README.md: clean up <img> attributes 2019-06-28 17:57:43 +03:00
Aliaksandr Valialkin
593bd35aaa lib/memory: an attempt to read proper memory limit inside LXC container
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/84
2019-06-28 15:34:30 +03:00
Aliaksandr Valialkin
7354f10336 vendor: update github.com/VictoriaMetrics/metrics to v1.6.2
This fixes Summary printing for *_count and *_sum values with metric names containing labels.
2019-06-28 14:17:17 +03:00
Aliaksandr Valialkin
e8998c69a7 vendor: update github.com/VictoriaMetrics/metrics to v1.6.1 2019-06-28 14:06:49 +03:00
Aliaksandr Valialkin
55bcf60ea6 app/vmselect: fix 32bit arm build
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/83
2019-06-27 19:36:17 +03:00
Aliaksandr Valialkin
796b010139 app/vmselect: add candlestick(m[d]) func for returning open, close, low and high rollups on the given time range d
This function is frequently used in financial apps. See https://en.wikipedia.org/wiki/Candlestick_chart
2019-06-27 18:46:13 +03:00
Aliaksandr Valialkin
0c8a09c8e1 README.md: mention about global query view 2019-06-27 17:38:37 +03:00
Aliaksandr Valialkin
c1be1e4342 lib/storage: optimize time series search by regexp filter
This should improve search speed on label filters like `{foo=~"bar.+baz"}`
2019-06-27 16:17:43 +03:00
Aliaksandr Valialkin
0c8d463307 README.md: mention that Prometheus 2.10.0+ works better with remote_write
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/80
2019-06-27 00:54:48 +03:00
Jiri Tyr
e0fccc6c60 Change the default influxMeasurementFieldSeparator 2019-06-26 13:22:03 +03:00
Aliaksandr Valialkin
1f7d9a213a app/vminsert: fix inifinite loop when reading two lines without newline in the end
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/82
2019-06-26 02:51:56 +03:00
Aliaksandr Valialkin
7ce1f73ada README.md: add more information to rough estimation of the required resources 2019-06-26 02:20:33 +03:00
Aliaksandr Valialkin
e605315f01 README.md: add link to slack chat 2019-06-26 02:05:38 +03:00
Aliaksandr Valialkin
fcef49184b README.md: clarify docs about Influx line protocol support 2019-06-26 00:05:09 +03:00
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
213 changed files with 13518 additions and 3494 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 -tags=integration -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

312
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 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).
@@ -16,6 +20,7 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
* Supports [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/), so it can be used as Prometheus drop-in replacement in Grafana.
Additionally, VictoriaMetrics extends PromQL with opt-in [useful features](https://github.com/VictoriaMetrics/VictoriaMetrics/wiki/ExtendedPromQL).
* Global query view. Multiple Prometheus instances may write data into VictoriaMetrics. Later this data may be used in a single query.
* High performance and good scalability for both [inserts](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b)
and [selects](https://medium.com/@valyala/when-size-matters-benchmarking-victoriametrics-vs-timescale-and-influxdb-6035811952d4).
[Outperforms InfluxDB and TimescaleDB by up to 20x](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
@@ -24,7 +29,8 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
may be crammed into a limited storage comparing to TimescaleDB.
* Optimized for storage with high-latency IO and low iops (HDD and network storage in AWS, Google Cloud, Microsoft Azure, etc). See [graphs from these benchmarks](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b).
* A single-node VictoriaMetrics may substitute moderately sized clusters built with competing solutions such as Thanos, Uber M3, Cortex, InfluxDB or TimescaleDB.
See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae)
and [comparing Thanos to VictoriaMetrics cluster](https://medium.com/@valyala/comparing-thanos-to-victoriametrics-cluster-b193bea1683).
* Easy operation:
* VictoriaMetrics consists of a single executable without external dependencies.
* All the configuration is done via explicit command-line flags with reasonable defaults.
@@ -37,7 +43,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 +52,49 @@ 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 upgrade VictoriaMetrics?](#how-to-upgrade-victoriametrics)
- [How to apply new config to VictoriaMetrics?](#how-to-apply-new-config-to-victoriametrics)
- [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 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)
- [Alerting](#alerting)
- [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 +102,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 +133,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 +152,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.
@@ -149,6 +180,10 @@ The label name may be arbitrary - `datacenter` is just an example. The label val
across Prometheus instances, so time series may be filtered and grouped by this label.
It is recommended upgrading Prometheus to [v2.10.0](https://github.com/prometheus/prometheus/releases) or newer,
since the previous versions may have issues with `remote_write`.
### Grafana setup
Create [Prometheus datasource](http://docs.grafana.org/features/datasources/prometheus/) in Grafana with the following Url:
@@ -163,6 +198,28 @@ Then build graphs with the created datasource using [Prometheus query language](
VictoriaMetrics supports native PromQL and [extends it with useful features](ExtendedPromQL).
### How to upgrade VictoriaMetrics?
It is safe upgrading VictoriaMetrics to new versions unless [release notes](https://github.com/VictoriaMetrics/VictoriaMetrics/releases)
say otherwise. It is recommended performing regular upgrades to the latest version,
since it may contain important bug fixes, performance optimizations or new features.
Follow the following steps during the upgrade:
1) Send `SIGINT` signal to VictoriaMetrics process in order to gracefully stop it.
2) Wait until the process stops. This can take a few seconds.
3) Start the upgraded VictoriaMetrics.
### How to apply new config to VictoriaMetrics?
VictoriaMetrics must be restarted for applying new config:
1) Send `SIGINT` signal to VictoriaMetrics process in order to gracefully stop it.
2) Wait until the process stops. This can take a few seconds.
3) Start VictoriaMetrics with new config.
### How to send data from InfluxDB-compatible agents such as [Telegraf](https://www.influxdata.com/time-series-platform/telegraf/)?
Just use `http://<victoriametric-addr>:8428` url instead of InfluxDB url in agents' configs.
@@ -176,10 +233,46 @@ 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,
where `{separator}` equals to `_` by default. It can be changed with `-influxMeasurementFieldSeparator` command-line flag.
See also `-influxSkipSingleField` 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](https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_tutorial/)
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,36 +281,70 @@ 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.
### How to apply new config / upgrade VictoriaMetrics?
Example for writing data with OpenTSDB protocol to local VictoriaMetrics using `nc`:
VictoriaMetrics must be restarted in order to upgrade or apply new config:
```
echo "put foo.bar.baz `date +%s` 123 tag1=value1 tag2=value2" | nc -N localhost 4242
```
1) Send `SIGINT` signal to VictoriaMetrics process in order to gracefully stop it.
2) Wait until the process stops. This can take a few seconds.
3) Start the upgraded VictoriaMetrics with new config.
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 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 +353,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 +363,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 +401,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`.
@@ -279,19 +412,26 @@ Rough estimation of the required resources:
* RAM size: less than 1KB per active time series. So, ~1GB of RAM is required for 1M active time series.
Time series is considered active if new data points have been added to it recently or if it has been recently queried.
VictoriaMetrics stores various caches in RAM. Memory size for these caches may be limited with `-memory.allowedPercent` flag.
The number of active time series may be obtained from `vm_cache_entries{type="storage/hour_metric_ids"}` metric
exproted on the `/metrics` page.
VictoriaMetrics stores various caches in RAM. Memory size for these caches may be limited by `-memory.allowedPercent` flag.
* CPU cores: a CPU core per 300K inserted data points per second. So, ~4 CPU cores are required for processing
the insert stream of 1M data points per second.
the insert stream of 1M data points per second. The ingestion rate may be lower for high cardinality data.
See [this article](https://medium.com/@valyala/insert-benchmarks-with-inch-influxdb-vs-victoriametrics-e31a41ae2893) for details.
If you see lower numbers per CPU core, then it is likely active time series info doesn't fit caches,
so you need more RAM for lowering CPU usage.
* Storage size: less than a byte per data point on average. So, ~260GB is required for storing a month-long insert stream
of 100K data points per second.
The actual storage size heavily depends on data randomness (entropy). Higher randomness means higher storage size requirements.
Read [this article](https://medium.com/faun/victoriametrics-achieving-better-compression-for-time-series-data-than-gorilla-317bc1f95932)
for details.
### High availability
1) Install multiple VictoriaMetrics instances in distinct datacenters.
1) Install multiple VictoriaMetrics instances in distinct datacenters (availability zones).
2) Add addresses of these instances to `remote_write` section in Prometheus config:
```yml
@@ -316,6 +456,10 @@ kill -HUP `pidof prometheus`
6) Set up Prometheus datasource in Grafana that points to Promxy.
If you have Prometheus HA pairs with replicas `r1` and `r2` in each pair, then configure each `r1`
to write data to `<victoriametrics-addr-1`, while each `r2` should write data to `victoriametrics-addr-2`.
### Multiple retentions
Just start multiple VictoriaMetrics instances with distinct values for the following flags:
@@ -325,17 +469,40 @@ 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.
This means that a single-node VictoriaMetrics may scale vertically and substitute moderately sized cluster built with competing solutions
such as Thanos, Uber M3, InfluxDB or TimescaleDB.
such as Thanos, Uber M3, InfluxDB or TimescaleDB. See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
So try single-node VictoriaMetrics at first and then [switch to cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) if you still need
horizontally scalable long-term remote storage for really large Prometheus deployments.
[Contact us](mailto:info@victoriametrics.com) for paid support.
### Alerting
VictoriaMetrics doesn't support rule evaluation and alerting yet, so these actions must be performed either
on [Prometheus side](https://prometheus.io/docs/alerting/overview/) or on [Grafana side](https://grafana.com/docs/alerting/rules/).
### Security
Do not forget protecting sensitive endpoints in VictoriaMetrics when exposing it to untrusted networks such as internet.
@@ -366,6 +533,21 @@ VictoriaMetrics exports internal metrics in Prometheus format on the `/metrics`
Add this page to Prometheus' scrape config in order to collect VictoriaMetrics metrics.
There is [an official Grafana dashboard for single-node VictoriaMetrics](https://grafana.com/dashboards/10229).
The most interesting metrics are:
* `vm_cache_entries{type="storage/hour_metric_ids"}` - the number of time series with new data points during the last hour
aka active time series.
* `vm_rows{type="indexdb"}` - the number of rows in inverted index. Each label in each unique time series adds a single
row into the inverted index. An approximate number of time series in the database may be calculated as
`vm_rows{type="indexdb"} / (avg_labels_per_series + 1)`, where `avg_labels_per_series` is the average number of labels
per each time series.
* Sum of `vm_rows{type="storage/big"}` and `vm_rows{type="storage/small"}` - total number of `(timestamp, value)` data points
in the database.
* Sum of all the `vm_cache_size_bytes` metrics - the total size of all the caches in the database.
* `vm_allowed_memory_bytes` - the maximum allowed size for caches in the database. It is calculated as `system_memory * <-memory.allowedPercent> / 100`,
where `system_memory` is the amount of system memory and `-memory.allowedPercent` is the corresponding flag value.
* `vm_rows_inserted_total` - the total number of inserted rows since VictoriaMetrics start.
### Troubleshooting
@@ -376,10 +558,32 @@ 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:
- [slack](http://slack.victoriametrics.com/)
- [telergam-en](https://t.me/VictoriaMetrics_en)
- [telergam-ru](https://t.me/VictoriaMetrics_ru1)
- [google groups](https://groups.google.com/forum/#!forum/victorametrics-users)
If you like VictoriaMetrics and want contributing, then we need the following:
- Filing issues and feature requests [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues).
- Spreading a word about VictoriaMetrics: conference talks, articles, comments, experience sharing with colleagues.
- Updating documentation.
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

@@ -0,0 +1,281 @@
// +build integration
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
const (
testFixturesDir = "testdata"
testStorageSuffix = "vm-test-storage"
testHTTPListenAddr = ":7654"
testStatsDListenAddr = ":2003"
testOpenTSDBListenAddr = ":4242"
testLogLevel = "INFO"
)
const (
testReadHTTPPath = "http://127.0.0.1" + testHTTPListenAddr
testWriteHTTPPath = "http://127.0.0.1" + testHTTPListenAddr + "/write"
testHealthHTTPPath = "http://127.0.0.1" + testHTTPListenAddr + "/health"
)
const (
testStorageInitTimeout = 10 * time.Second
)
var (
storagePath string
insertionTime = time.Now().UTC()
)
type test struct {
Name string `json:"name"`
Data string `json:"data"`
Query string `json:"query"`
Result []Row `json:"result"`
}
type Row struct {
Metric map[string]string `json:"metric"`
Values []float64 `json:"values"`
Timestamps []int64 `json:"timestamps"`
}
func TestMain(m *testing.M) {
setUp()
code := m.Run()
tearDown()
os.Exit(code)
}
func setUp() {
storagePath = filepath.Join(os.TempDir(), testStorageSuffix)
processFlags()
logger.Init()
vmstorage.InitWithoutMetrics()
vmselect.Init()
vminsert.Init()
go httpserver.Serve(*httpListenAddr, requestHandler)
readyStorageCheckFunc := func() bool {
resp, err := http.Get(testHealthHTTPPath)
if err != nil {
return false
}
resp.Body.Close()
return resp.StatusCode == 200
}
if err := waitFor(testStorageInitTimeout, readyStorageCheckFunc); err != nil {
log.Fatalf("http server can't start for %s seconds, err %s", testStorageInitTimeout, err)
}
}
func processFlags() {
flag.Parse()
for _, fs := range []struct {
flag string
value string
}{
{flag: "storageDataPath", value: storagePath},
{flag: "httpListenAddr", value: testHTTPListenAddr},
{flag: "graphiteListenAddr", value: testStatsDListenAddr},
{flag: "opentsdbListenAddr", value: testOpenTSDBListenAddr},
{flag: "loggerLevel", value: testLogLevel},
} {
// panics if flag doesn't exist
if err := flag.Lookup(fs.flag).Value.Set(fs.value); err != nil {
log.Fatalf("unable to set %q with value %q, err: %v", fs.flag, fs.value, err)
}
}
}
func waitFor(timeout time.Duration, f func() bool) error {
fraction := timeout / 10
for i := fraction; i < timeout; i += fraction {
if f() {
return nil
}
time.Sleep(fraction)
}
return fmt.Errorf("timeout")
}
func tearDown() {
vminsert.Stop()
vmstorage.Stop()
vmselect.Stop()
if err := httpserver.Stop(*httpListenAddr); err != nil {
log.Fatalf("cannot stop the webservice: %s", err)
}
os.RemoveAll(storagePath)
}
func TestWriteRead(t *testing.T) {
t.Run("write", testWrite)
time.Sleep(1 * time.Second)
vmstorage.Stop()
// open storage after stop in write
vmstorage.InitWithoutMetrics()
t.Run("read", testRead)
}
func testWrite(t *testing.T) {
t.Run("influxdb", func(t *testing.T) {
for _, test := range readIn("influxdb", t, fmt.Sprintf("%d", insertionTime.UnixNano())) {
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
httpWrite(t, testWriteHTTPPath, test.Data)
})
}
})
t.Run("graphite", func(t *testing.T) {
for _, test := range readIn("graphite", t, fmt.Sprintf("%d", insertionTime.Unix())) {
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
tcpWrite(t, "127.0.0.1"+testStatsDListenAddr, test.Data)
})
}
})
t.Run("opentsdb", func(t *testing.T) {
for _, test := range readIn("opentsdb", t, fmt.Sprintf("%d", insertionTime.Unix())) {
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
tcpWrite(t, "127.0.0.1"+testOpenTSDBListenAddr, test.Data)
})
}
})
}
func testRead(t *testing.T) {
for _, engine := range []string{"graphite", "opentsdb", "influxdb"} {
t.Run(engine, func(t *testing.T) {
for _, test := range readIn(engine, t, fmt.Sprintf("%d", insertionTime.UnixNano())) {
test := test
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
rowContains(t, httpRead(t, testReadHTTPPath, test.Query), test.Result)
})
}
})
}
}
func readIn(readFor string, t *testing.T, timeStr string) []test {
t.Helper()
s := newSuite(t)
var tt []test
s.noError(filepath.Walk(filepath.Join(testFixturesDir, readFor), func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) != ".json" {
return nil
}
b, err := ioutil.ReadFile(path)
s.noError(err)
item := test{}
s.noError(json.Unmarshal(b, &item))
item.Data = strings.Replace(item.Data, "{TIME}", timeStr, 1)
tt = append(tt, item)
return nil
}))
if len(tt) == 0 {
t.Fatalf("no test found in %s", filepath.Join(testFixturesDir, readFor))
}
return tt
}
func httpWrite(t *testing.T, address string, data string) {
t.Helper()
s := newSuite(t)
resp, err := http.Post(address, "", bytes.NewBufferString(data))
s.noError(err)
s.noError(resp.Body.Close())
s.equalInt(resp.StatusCode, 204)
}
func tcpWrite(t *testing.T, address string, data string) {
t.Helper()
s := newSuite(t)
conn, err := net.Dial("tcp", address)
s.noError(err)
defer conn.Close()
n, err := conn.Write([]byte(data))
s.noError(err)
s.equalInt(n, len(data))
}
func httpRead(t *testing.T, address, query string) []Row {
t.Helper()
s := newSuite(t)
resp, err := http.Get(address + query)
s.noError(err)
defer resp.Body.Close()
s.equalInt(resp.StatusCode, 200)
var rows []Row
for dec := json.NewDecoder(resp.Body); dec.More(); {
var row Row
s.noError(dec.Decode(&row))
rows = append(rows, row)
}
return rows
}
func rowContains(t *testing.T, rows, contains []Row) {
t.Helper()
for _, r := range rows {
contains = removeIfFound(r, contains)
}
if len(contains) > 0 {
t.Fatalf("result rows %+v not found in %+v", contains, rows)
}
}
func removeIfFound(r Row, contains []Row) []Row {
for i, item := range contains {
// todo check time
if reflect.DeepEqual(r.Metric, item.Metric) && reflect.DeepEqual(r.Values, item.Values) {
contains[i] = contains[len(contains)-1]
return contains[:len(contains)-1]
}
}
return contains
}
type suite struct{ t *testing.T }
func newSuite(t *testing.T) *suite { return &suite{t: t} }
func (s *suite) noError(err error) {
s.t.Helper()
if err != nil {
s.t.Errorf("unexpected error %v", err)
s.t.FailNow()
}
}
func (s *suite) equalInt(a, b int) {
s.t.Helper()
if a != b {
s.t.Errorf("%d not equal %d", a, b)
s.t.FailNow()
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "basic_insertion",
"data": "graphite.foo.bar.baz;tag1=value1;tag2=value2 123 {TIME}",
"query": "/api/v1/export?match={__name__!=\"\"}",
"result": [
{"metric":{"__name__":"graphite.foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123]}
]
}

View File

@@ -0,0 +1,9 @@
{
"name": "basic_insertion",
"data": "measurement,tag1=value1,tag2=value2 field1=1.23,field2=123",
"query": "/api/v1/export?match={__name__!=\"\"}",
"result": [
{"metric":{"__name__":"measurement_field2","tag1":"value1","tag2":"value2"},"values":[123]},
{"metric":{"__name__":"measurement_field1","tag1":"value1","tag2":"value2"},"values":[1.23]}
]
}

View File

@@ -0,0 +1,8 @@
{
"name": "basic_insertion",
"data": "put openstdb.foo.bar.baz {TIME} 123 tag1=value1 tag2=value2",
"query": "/api/v1/export?match={__name__!=\"\"}",
"result": [
{"metric":{"__name__":"openstdb.foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123]}
]
}

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,68 @@
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...)
tailBuf = tailBuf[:0]
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,213 @@
package common
import (
"bytes"
"fmt"
"io"
"reflect"
"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 TestReadLinesBlockMultiLinesSingleByteReader(t *testing.T) {
f := func(s string, linesExpected []string) {
t.Helper()
r := &singleByteReader{
b: []byte(s),
}
var err error
var dstBuf, tailBuf []byte
var lines []string
for {
dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf)
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("unexpected error in ReadLinesBlock(%q): %s", s, err)
}
lines = append(lines, string(dstBuf))
}
if !reflect.DeepEqual(lines, linesExpected) {
t.Fatalf("unexpected lines after reading %q: got %q; want %q", s, lines, linesExpected)
}
}
f("", nil)
f("foo", []string{"foo"})
f("foo\n", []string{"foo"})
f("foo\nbar", []string{"foo", "bar"})
f("\nfoo\nbar", []string{"", "foo", "bar"})
f("\nfoo\nbar\n", []string{"", "foo", "bar"})
f("\nfoo\nbar\n\n", []string{"", "foo", "bar", ""})
}
func TestReadLinesBlockMultiLinesBytesBuffer(t *testing.T) {
f := func(s string, linesExpected []string) {
t.Helper()
r := bytes.NewBufferString(s)
var err error
var dstBuf, tailBuf []byte
var lines []string
for {
dstBuf, tailBuf, err = ReadLinesBlock(r, dstBuf, tailBuf)
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("unexpected error in ReadLinesBlock(%q): %s", s, err)
}
lines = append(lines, string(dstBuf))
}
if !reflect.DeepEqual(lines, linesExpected) {
t.Fatalf("unexpected lines after reading %q: got %q; want %q", s, lines, linesExpected)
}
}
f("", nil)
f("foo", []string{"foo"})
f("foo\n", []string{"foo"})
f("foo\nbar", []string{"foo", "bar"})
f("\nfoo\nbar", []string{"\nfoo", "bar"})
f("\nfoo\nbar\n", []string{"\nfoo\nbar"})
f("\nfoo\nbar\n\n", []string{"\nfoo\nbar\n"})
}
func TestReadLinesBlockSuccessSingleByteReader(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 TestReadLinesBlockSuccessBytesBuffer(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,
}},
})
@@ -338,4 +348,30 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
},
},
})
// No newline after the second line.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/82
f("foo,tag=xyz field=1.23 48934\n"+
"bar x=-1i", &Rows{
Rows: []Row{
{
Measurement: "foo",
Tags: []Tag{{
Key: "tag",
Value: "xyz",
}},
Fields: []Field{{
Key: "field",
Value: 1.23,
}},
Timestamp: 48934,
},
{
Measurement: "bar",
Fields: []Field{{
Key: "x",
Value: -1,
}},
},
},
})
}

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,19 @@ 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")
}
// Set start to minTimeMsecs by default as Prometheus does.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/91
start, err := getTime(r, "start", minTimeMsecs)
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 +354,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 +376,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 {
@@ -363,7 +428,7 @@ func QueryHandler(w http.ResponseWriter, r *http.Request) error {
Step: step,
Deadline: deadline,
}
result, err := promql.Exec(&ec, query)
result, err := promql.Exec(&ec, query, true)
if err != nil {
return fmt.Errorf("cannot execute %q: %s", query, err)
}
@@ -384,9 +449,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")
@@ -409,12 +486,12 @@ func QueryRangeHandler(w http.ResponseWriter, r *http.Request) error {
Deadline: deadline,
MayCache: mayCache,
}
result, err := promql.Exec(&ec, query)
result, err := promql.Exec(&ec, query, false)
if err != nil {
return fmt.Errorf("cannot execute %q: %s", query, err)
}
if ct-end < latencyOffset {
adjustLastPoints(result)
result = adjustLastPoints(result)
}
w.Header().Set("Content-Type", "application/json")
@@ -427,17 +504,17 @@ var queryRangeDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/
// adjustLastPoints substitutes the last point values with the previous
// point values, since the last points may contain garbage.
func adjustLastPoints(tss []netstorage.Result) {
func adjustLastPoints(tss []netstorage.Result) []netstorage.Result {
if len(tss) == 0 {
return
return nil
}
// Search for the last non-NaN value across all the timeseries.
lastNonNaNIdx := -1
for i := range tss {
r := &tss[i]
j := len(r.Values) - 1
for j >= 0 && math.IsNaN(r.Values[j]) {
values := tss[i].Values
j := len(values) - 1
for j >= 0 && math.IsNaN(values[j]) {
j--
}
if j > lastNonNaNIdx {
@@ -446,75 +523,97 @@ func adjustLastPoints(tss []netstorage.Result) {
}
if lastNonNaNIdx == -1 {
// All timeseries contain only NaNs.
return
return nil
}
// Substitute last three values starting from lastNonNaNIdx
// Substitute the last two values starting from lastNonNaNIdx
// with the previous values for each timeseries.
for i := range tss {
r := &tss[i]
for j := 0; j < 3; j++ {
values := tss[i].Values
for j := 0; j < 2; j++ {
idx := lastNonNaNIdx + j
if idx <= 0 || idx >= len(r.Values) {
if idx <= 0 || idx >= len(values) || math.IsNaN(values[idx-1]) {
continue
}
r.Values[idx] = r.Values[idx-1]
values[idx] = values[idx-1]
}
}
return tss
}
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
// Handle Prometheus'-provided minTime and maxTime.
// See https://github.com/prometheus/client_golang/issues/614
switch argValue {
case prometheusMinTimeFormatted:
return minTimeMsecs, nil
case prometheusMaxTimeFormatted:
return maxTimeMsecs, nil
}
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
if msecs < minTimeMsecs {
msecs = 0
}
return msecs
if msecs > maxTimeMsecs {
msecs = maxTimeMsecs
}
return msecs, nil
}
var (
// These constants were obtained from https://github.com/prometheus/prometheus/blob/91d7175eaac18b00e370965f3a8186cc40bf9f55/web/api/v1/api.go#L442
// See https://github.com/prometheus/client_golang/issues/614 for details.
prometheusMinTimeFormatted = time.Unix(math.MinInt64/1000+62135596801, 0).UTC().Format(time.RFC3339Nano)
prometheusMaxTimeFormatted = time.Unix(math.MaxInt64/1000-62135596801, 999999999).UTC().Format(time.RFC3339Nano)
)
const (
// These values prevent from overflow when storing msec-precision time in int64.
minTimeMsecs = int64(-1<<63) / 1e6
minTimeMsecs = 0 // use 0 instead of `int64(-1<<63) / 1e6` because the storage engine doesn't actually support negative time
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, int64(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

@@ -0,0 +1,78 @@
package prometheus
import (
"fmt"
"net/http"
"net/url"
"testing"
)
func TestGetTimeSuccess(t *testing.T) {
f := func(s string, timestampExpected int64) {
t.Helper()
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
r, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
t.Fatalf("unexpected error in NewRequest: %s", err)
}
// Verify defaultValue
ts, err := getTime(r, "foo", 123)
if err != nil {
t.Fatalf("unexpected error when obtaining default time from getTime(%q): %s", s, err)
}
if ts != 123 {
t.Fatalf("unexpected default value for getTime(%q); got %d; want %d", s, ts, 123)
}
// Verify timestampExpected
ts, err = getTime(r, "s", 123)
if err != nil {
t.Fatalf("unexpected error in getTime(%q): %s", s, err)
}
if ts != timestampExpected {
t.Fatalf("unexpected timestamp for getTime(%q); got %d; want %d", s, ts, timestampExpected)
}
}
f("2019-07-07T20:01:02Z", 1562529662000)
f("2019-07-07T20:47:40+03:00", 1562521660000)
f("-292273086-05-16T16:47:06Z", minTimeMsecs)
f("292277025-08-18T07:12:54.999999999Z", maxTimeMsecs)
f("1562529662.324", 1562529662324)
f("-9223372036.854", minTimeMsecs)
f("-9223372036.855", minTimeMsecs)
f("9223372036.855", maxTimeMsecs)
}
func TestGetTimeError(t *testing.T) {
f := func(s string) {
t.Helper()
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
r, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
t.Fatalf("unexpected error in NewRequest: %s", err)
}
// Verify defaultValue
ts, err := getTime(r, "foo", 123)
if err != nil {
t.Fatalf("unexpected error when obtaining default time from getTime(%q): %s", s, err)
}
if ts != 123 {
t.Fatalf("unexpected default value for getTime(%q); got %d; want %d", s, ts, 123)
}
// Verify timestampExpected
_, err = getTime(r, "s", 123)
if err == nil {
t.Fatalf("expecting non-nil error in getTime(%q)", s)
}
}
f("foo")
f("2019-07-07T20:01:02Zisdf")
f("2019-07-07T20:47:40+03:00123")
f("-292273086-05-16T16:47:07Z")
f("292277025-08-18T07:12:54.999999998Z")
}

View File

@@ -6,6 +6,9 @@ import (
"sort"
"strconv"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
var aggrFuncs = map[string]aggrFunc{
@@ -26,6 +29,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)
@@ -65,33 +70,26 @@ func newAggrFunc(afe func(tss []*timeseries) []*timeseries) aggrFunc {
}
}
func removeGroupTags(metricName *storage.MetricName, modifier *modifierExpr) {
groupOp := strings.ToLower(modifier.Op)
switch groupOp {
case "", "by":
metricName.RemoveTagsOn(modifier.Args)
case "without":
metricName.RemoveTagsIgnoring(modifier.Args)
default:
logger.Panicf("BUG: unknown group modifier: %q", groupOp)
}
}
func aggrFuncExt(afe func(tss []*timeseries) []*timeseries, argOrig []*timeseries, modifier *modifierExpr, keepOriginal bool) ([]*timeseries, error) {
arg := copyTimeseriesMetricNames(argOrig)
// Filter out superflouos tags.
var groupTags []string
groupOp := "by"
if modifier.Op != "" {
groupTags = modifier.Args
groupOp = strings.ToLower(modifier.Op)
}
switch groupOp {
case "by":
for _, ts := range arg {
ts.MetricName.RemoveTagsOn(groupTags)
}
case "without":
for _, ts := range arg {
ts.MetricName.RemoveTagsIgnoring(groupTags)
}
default:
return nil, fmt.Errorf(`unknown modifier: %q`, groupOp)
}
// Perform grouping.
m := make(map[string][]*timeseries)
bb := bbPool.Get()
for i, ts := range arg {
removeGroupTags(&ts.MetricName, modifier)
bb.B = marshalMetricNameSorted(bb.B[:0], &ts.MetricName)
if keepOriginal {
ts = argOrig[i]
@@ -100,10 +98,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 +138,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 +353,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 +368,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 {
@@ -372,7 +427,7 @@ func newAggrFuncTopK(isReverse bool) aggrFunc {
ts.Values[n] = nan
}
}
return rvs
return removeNaNs(rvs)
}
return aggrFuncExt(afe, args[1], &afa.ae.Modifier, true)
}
@@ -457,6 +512,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,270 @@
package promql
import (
"math"
"strings"
"sync"
)
// callbacks for optimized incremental calculations for aggregate functions
// over rollups over metricExpr.
//
// These calculations save RAM for aggregates over big number of time series.
var incrementalAggrFuncCallbacksMap = map[string]*incrementalAggrFuncCallbacks{
"sum": {
updateAggrFunc: updateAggrSum,
finalizeAggrFunc: finalizeAggrCommon,
},
"min": {
updateAggrFunc: updateAggrMin,
finalizeAggrFunc: finalizeAggrCommon,
},
"max": {
updateAggrFunc: updateAggrMax,
finalizeAggrFunc: finalizeAggrCommon,
},
"avg": {
updateAggrFunc: updateAggrAvg,
finalizeAggrFunc: finalizeAggrAvg,
},
"count": {
updateAggrFunc: updateAggrCount,
finalizeAggrFunc: finalizeAggrCount,
},
"sum2": {
updateAggrFunc: updateAggrSum2,
finalizeAggrFunc: finalizeAggrCommon,
},
"geomean": {
updateAggrFunc: updateAggrGeomean,
finalizeAggrFunc: finalizeAggrGeomean,
},
}
type incrementalAggrFuncContext struct {
ae *aggrFuncExpr
mu sync.Mutex
m map[string]*incrementalAggrContext
callbacks *incrementalAggrFuncCallbacks
}
func newIncrementalAggrFuncContext(ae *aggrFuncExpr, callbacks *incrementalAggrFuncCallbacks) *incrementalAggrFuncContext {
return &incrementalAggrFuncContext{
ae: ae,
m: make(map[string]*incrementalAggrContext, 1),
callbacks: callbacks,
}
}
func (iafc *incrementalAggrFuncContext) updateTimeseries(ts *timeseries) {
removeGroupTags(&ts.MetricName, &iafc.ae.Modifier)
bb := bbPool.Get()
bb.B = marshalMetricNameSorted(bb.B[:0], &ts.MetricName)
iafc.mu.Lock()
iac := iafc.m[string(bb.B)]
if iac == nil {
tsAggr := &timeseries{
Values: make([]float64, len(ts.Values)),
Timestamps: ts.Timestamps,
denyReuse: true,
}
tsAggr.MetricName.CopyFrom(&ts.MetricName)
iac = &incrementalAggrContext{
ts: tsAggr,
values: make([]float64, len(ts.Values)),
}
iafc.m[string(bb.B)] = iac
}
iafc.callbacks.updateAggrFunc(iac, ts.Values)
iafc.mu.Unlock()
bbPool.Put(bb)
}
func (iafc *incrementalAggrFuncContext) finalizeTimeseries() []*timeseries {
// There is no need in iafc.mu.Lock here, since getTimeseries must be called
// without concurrent goroutines touching iafc.
tss := make([]*timeseries, 0, len(iafc.m))
finalizeAggrFunc := iafc.callbacks.finalizeAggrFunc
for _, iac := range iafc.m {
finalizeAggrFunc(iac)
tss = append(tss, iac.ts)
}
return tss
}
type incrementalAggrFuncCallbacks struct {
updateAggrFunc func(iac *incrementalAggrContext, values []float64)
finalizeAggrFunc func(iac *incrementalAggrContext)
}
func getIncrementalAggrFuncCallbacks(name string) *incrementalAggrFuncCallbacks {
name = strings.ToLower(name)
return incrementalAggrFuncCallbacksMap[name]
}
type incrementalAggrContext struct {
ts *timeseries
values []float64
}
func finalizeAggrCommon(iac *incrementalAggrContext) {
counts := iac.values
dstValues := iac.ts.Values
_ = dstValues[len(counts)-1]
for i, v := range counts {
if v == 0 {
dstValues[i] = nan
}
}
}
func updateAggrSum(iac *incrementalAggrContext, values []float64) {
dstValues := iac.ts.Values
dstCounts := iac.values
_ = dstValues[len(values)-1]
_ = dstCounts[len(values)-1]
for i, v := range values {
if math.IsNaN(v) {
continue
}
dstValues[i] += v
dstCounts[i] = 1
}
}
func updateAggrMin(iac *incrementalAggrContext, values []float64) {
dstValues := iac.ts.Values
dstCounts := iac.values
_ = dstValues[len(values)-1]
_ = dstCounts[len(values)-1]
for i, v := range values {
if math.IsNaN(v) {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = 1
continue
}
if v < dstValues[i] {
dstValues[i] = v
}
}
}
func updateAggrMax(iac *incrementalAggrContext, values []float64) {
dstValues := iac.ts.Values
dstCounts := iac.values
_ = dstValues[len(values)-1]
_ = dstCounts[len(values)-1]
for i, v := range values {
if math.IsNaN(v) {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = 1
continue
}
if v > dstValues[i] {
dstValues[i] = v
}
}
}
func updateAggrAvg(iac *incrementalAggrContext, values []float64) {
// Do not use `Rapid calculation methods` at https://en.wikipedia.org/wiki/Standard_deviation,
// since it is slower and has no obvious benefits in increased precision.
dstValues := iac.ts.Values
dstCounts := iac.values
_ = dstValues[len(values)-1]
_ = dstCounts[len(values)-1]
for i, v := range values {
if math.IsNaN(v) {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = 1
continue
}
dstValues[i] += v
dstCounts[i]++
}
}
func finalizeAggrAvg(iac *incrementalAggrContext) {
dstValues := iac.ts.Values
counts := iac.values
_ = dstValues[len(counts)-1]
for i, v := range counts {
if v == 0 {
dstValues[i] = nan
continue
}
dstValues[i] /= v
}
}
func updateAggrCount(iac *incrementalAggrContext, values []float64) {
dstValues := iac.ts.Values
_ = dstValues[len(values)-1]
for i, v := range values {
if math.IsNaN(v) {
continue
}
dstValues[i]++
}
}
func finalizeAggrCount(iac *incrementalAggrContext) {
// Nothing to do
}
func updateAggrSum2(iac *incrementalAggrContext, values []float64) {
dstValues := iac.ts.Values
dstCounts := iac.values
_ = dstValues[len(values)-1]
_ = dstCounts[len(values)-1]
for i, v := range values {
if math.IsNaN(v) {
continue
}
dstValues[i] += v * v
dstCounts[i] = 1
}
}
func updateAggrGeomean(iac *incrementalAggrContext, values []float64) {
dstValues := iac.ts.Values
dstCounts := iac.values
_ = dstValues[len(values)-1]
_ = dstCounts[len(values)-1]
for i, v := range values {
if math.IsNaN(v) {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = 1
continue
}
dstValues[i] *= v
dstCounts[i]++
}
}
func finalizeAggrGeomean(iac *incrementalAggrContext) {
dstValues := iac.ts.Values
counts := iac.values
_ = dstValues[len(counts)-1]
for i, v := range counts {
if v == 0 {
dstValues[i] = nan
continue
}
dstValues[i] = math.Pow(dstValues[i], 1/v)
}
}

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
@@ -145,14 +145,14 @@ func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
re := &rollupExpr{
Expr: me,
}
rv, err := evalRollupFunc(ec, "default_rollup", rollupDefault, re)
rv, err := evalRollupFunc(ec, "default_rollup", rollupDefault, re, nil)
if err != nil {
return nil, fmt.Errorf(`cannot evaluate %q: %s`, me.AppendString(nil), err)
}
return rv, nil
}
if re, ok := e.(*rollupExpr); ok {
rv, err := evalRollupFunc(ec, "default_rollup", rollupDefault, re)
rv, err := evalRollupFunc(ec, "default_rollup", rollupDefault, re, nil)
if err != nil {
return nil, fmt.Errorf(`cannot evaluate %q: %s`, re.AppendString(nil), err)
}
@@ -188,13 +188,30 @@ func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
if err != nil {
return nil, err
}
rv, err := evalRollupFunc(ec, fe.Name, rf, re)
rv, err := evalRollupFunc(ec, fe.Name, rf, re, nil)
if err != nil {
return nil, fmt.Errorf(`cannot evaluate %q: %s`, fe.AppendString(nil), err)
}
return rv, nil
}
if ae, ok := e.(*aggrFuncExpr); ok {
if callbacks := getIncrementalAggrFuncCallbacks(ae.Name); callbacks != nil {
fe, nrf := tryGetArgRollupFuncWithMetricExpr(ae)
if fe != nil {
// There is an optimized path for calculating aggrFuncExpr over rollupFunc over metricExpr.
// The optimized path saves RAM for aggregates over big number of time series.
args, re, err := evalRollupFuncArgs(ec, fe)
if err != nil {
return nil, err
}
rf, err := nrf(args)
if err != nil {
return nil, err
}
iafc := newIncrementalAggrFuncContext(ae, callbacks)
return evalRollupFunc(ec, fe.Name, rf, re, iafc)
}
}
args, err := evalExprs(ec, ae.Args)
if err != nil {
return nil, err
@@ -249,6 +266,69 @@ func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
return nil, fmt.Errorf("unexpected expression %q", e.AppendString(nil))
}
func tryGetArgRollupFuncWithMetricExpr(ae *aggrFuncExpr) (*funcExpr, newRollupFunc) {
if len(ae.Args) != 1 {
return nil, nil
}
e := ae.Args[0]
// Make sure e contains one of the following:
// - metricExpr
// - metricExpr[d]
// - rollupFunc(metricExpr)
// - rollupFunc(metricExpr[d])
if me, ok := e.(*metricExpr); ok {
// e = metricExpr
if me.IsEmpty() {
return nil, nil
}
fe := &funcExpr{
Name: "default_rollup",
Args: []expr{me},
}
nrf := getRollupFunc(fe.Name)
return fe, nrf
}
if re, ok := e.(*rollupExpr); ok {
if me, ok := re.Expr.(*metricExpr); !ok || me.IsEmpty() {
return nil, nil
}
// e = rollupExpr(metricExpr)
fe := &funcExpr{
Name: "default_rollup",
Args: []expr{re},
}
nrf := getRollupFunc(fe.Name)
return fe, nrf
}
fe, ok := e.(*funcExpr)
if !ok {
return nil, nil
}
nrf := getRollupFunc(fe.Name)
if nrf == nil {
return nil, nil
}
rollupArgIdx := getRollupArgIdx(fe.Name)
arg := fe.Args[rollupArgIdx]
if me, ok := arg.(*metricExpr); ok {
if me.IsEmpty() {
return nil, nil
}
return &funcExpr{
Name: fe.Name,
Args: []expr{me},
}, nrf
}
if re, ok := arg.(*rollupExpr); ok {
if me, ok := re.Expr.(*metricExpr); !ok || me.IsEmpty() {
return nil, nil
}
return fe, nrf
}
return nil, nil
}
func evalExprs(ec *EvalConfig, es []expr) ([][]*timeseries, error) {
var rvs [][]*timeseries
for _, e := range es {
@@ -308,7 +388,7 @@ func getRollupExprArg(arg expr) *rollupExpr {
return &reNew
}
func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, re *rollupExpr) ([]*timeseries, error) {
func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, re *rollupExpr, iafc *incrementalAggrFuncContext) ([]*timeseries, error) {
ecNew := ec
var offset int64
if len(re.Offset) > 0 {
@@ -325,19 +405,11 @@ func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, re *rollupExpr)
var rvs []*timeseries
var err error
if me, ok := re.Expr.(*metricExpr); ok {
if me.IsEmpty() {
rvs = evalNumber(ecNew, nan)
} else {
var window int64
if len(re.Window) > 0 {
window, err = DurationValue(re.Window, ec.Step)
if err != nil {
return nil, err
}
}
rvs, err = evalRollupFuncWithMetricExpr(ecNew, name, rf, me, window)
}
rvs, err = evalRollupFuncWithMetricExpr(ecNew, name, rf, me, iafc, re.Window)
} else {
if iafc != nil {
logger.Panicf("BUG: iafc must be nil for rollup %q over subquery %q", name, re.AppendString(nil))
}
rvs, err = evalRollupFuncWithSubquery(ecNew, name, rf, re)
}
if err != nil {
@@ -379,8 +451,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
@@ -407,6 +478,7 @@ func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, re *
ts.Values = rc.Do(ts.Values[:0], values, timestamps)
ts.Timestamps = sharedTimestamps
ts.denyReuse = true
tssLock.Lock()
tss = append(tss, &ts)
tssLock.Unlock()
@@ -472,31 +544,27 @@ 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`)
rollupResultCacheMiss = metrics.NewCounter(`vm_rollup_result_cache_miss_total`)
)
func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me *metricExpr, window int64) ([]*timeseries, error) {
func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me *metricExpr, iafc *incrementalAggrFuncContext, windowStr string) ([]*timeseries, error) {
if me.IsEmpty() {
return evalNumber(ec, nan), nil
}
var window int64
if len(windowStr) > 0 {
var err error
window, err = DurationValue(windowStr, ec.Step)
if err != nil {
return nil, err
}
}
// Search for partial results in cache.
tssCached, start := rollupResultCacheV.Get(name, ec, me, window)
tssCached, start := rollupResultCacheV.Get(name, ec, me, iafc, window)
if start > ec.End {
// The result is fully cached.
rollupResultCacheFullHits.Inc()
@@ -533,17 +601,83 @@ 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))
var tss []*timeseries
if iafc != nil {
tss, err = evalRollupWithIncrementalAggregate(iafc, rss, rcs, preFunc, sharedTimestamps)
} else {
tss, err = evalRollupNoIncrementalAggregate(rss, rcs, preFunc, sharedTimestamps)
}
if err != nil {
return nil, err
}
if !rollupFuncsKeepMetricGroup[name] {
tss = copyTimeseriesMetricNames(tss)
for _, ts := range tss {
ts.MetricName.ResetMetricGroup()
}
}
tss = mergeTimeseries(tssCached, tss, start, ec)
rollupResultCacheV.Put(name, ec, me, iafc, window, tss)
return tss, nil
}
var (
rollupMemoryLimiter memoryLimiter
rollupMemoryLimiterOnce sync.Once
)
func getRollupMemoryLimiter() *memoryLimiter {
rollupMemoryLimiterOnce.Do(func() {
rollupMemoryLimiter.MaxSize = uint64(memory.Allowed()) / 4
})
return &rollupMemoryLimiter
}
func evalRollupWithIncrementalAggregate(iafc *incrementalAggrFuncContext, rss *netstorage.Results, rcs []*rollupConfig,
preFunc func(values []float64, timestamps []int64), sharedTimestamps []int64) ([]*timeseries, error) {
err := rss.RunParallel(func(rs *netstorage.Result) {
preFunc(rs.Values, rs.Timestamps)
ts := getTimeseries()
defer putTimeseries(ts)
for _, rc := range rcs {
ts.Reset()
ts.MetricName.CopyFrom(&rs.MetricName)
if len(rc.TagValue) > 0 {
ts.MetricName.AddTag("rollup", rc.TagValue)
}
ts.Values = rc.Do(ts.Values[:0], rs.Values, rs.Timestamps)
ts.Timestamps = sharedTimestamps
iafc.updateTimeseries(ts)
ts.Timestamps = nil
}
})
if err != nil {
return nil, err
}
tss := iafc.finalizeTimeseries()
return tss, nil
}
func evalRollupNoIncrementalAggregate(rss *netstorage.Results, rcs []*rollupConfig,
preFunc func(values []float64, timestamps []int64), sharedTimestamps []int64) ([]*timeseries, error) {
tss := make([]*timeseries, 0, rss.Len()*len(rcs))
var tssLock sync.Mutex
err = rss.RunParallel(func(rs *netstorage.Result) {
err := rss.RunParallel(func(rs *netstorage.Result) {
preFunc(rs.Values, rs.Timestamps)
for _, rc := range rcs {
var ts timeseries
@@ -563,15 +697,6 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me
if err != nil {
return nil, err
}
if !rollupFuncsKeepMetricGroup[name] {
tss = copyTimeseriesMetricNames(tss)
for _, ts := range tss {
ts.MetricName.ResetMetricGroup()
}
}
tss = mergeTimeseries(tssCached, tss, start, ec)
rollupResultCacheV.Put(name, ec, me, window, tss)
return tss, nil
}
@@ -584,13 +709,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 {
@@ -617,6 +743,11 @@ func getRollupConfigs(name string, rf rollupFunc, start, end, step, window int64
deltaValues(values)
}
rcs = appendRollupConfigs(rcs)
case "rollup_candlestick":
rcs = append(rcs, newRollupConfig(rollupFirst, "open"))
rcs = append(rcs, newRollupConfig(rollupLast, "close"))
rcs = append(rcs, newRollupConfig(rollupMin, "low"))
rcs = append(rcs, newRollupConfig(rollupMax, "high"))
default:
rcs = append(rcs, newRollupConfig(rf, ""))
}
@@ -653,3 +784,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) {
@@ -22,8 +27,19 @@ func ExpandWithExprs(q string) (string, error) {
return string(buf), nil
}
// Exec executes q for the given ec until the deadline.
func Exec(ec *EvalConfig, q string) ([]netstorage.Result, error) {
// Exec executes q for the given ec.
func Exec(ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result, error) {
if *logSlowQueryDuration > 0 {
startTime := time.Now()
defer func() {
d := time.Since(startTime)
if d >= *logSlowQueryDuration {
logger.Infof("slow query according to -search.logSlowQueryDuration=%s: duration=%s, start=%d, end=%d, step=%d, query=%q",
*logSlowQueryDuration, d, ec.Start/1000, ec.End/1000, ec.Step/1000, q)
}
}()
}
ec.validate()
e, err := parsePromQLWithCache(q)
@@ -50,6 +66,14 @@ func Exec(ec *EvalConfig, q string) ([]netstorage.Result, error) {
}
ec.End -= ec.Step
if isFirstPointOnly {
// Remove all the points except the first one from every time series.
for _, ts := range rv {
ts.Values = ts.Values[:1]
ts.Timestamps = ts.Timestamps[:1]
}
}
maySort := maySortResults(e, rv)
result, err := timeseriesToResult(rv, maySort)
if err != nil {

View File

@@ -62,7 +62,7 @@ func TestExecSuccess(t *testing.T) {
Deadline: netstorage.NewDeadline(time.Minute),
}
for i := 0; i < 5; i++ {
result, err := Exec(ec, q)
result, err := Exec(ec, q, false)
if err != nil {
t.Fatalf(`unexpected error when executing %q: %s`, q, err)
}
@@ -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}
@@ -574,6 +574,30 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`clamp_max(alias(time(),"foobar"), 1400)`, func(t *testing.T) {
t.Parallel()
q := `clamp_max(alias(time(), "foobar"), 1400)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1400, 1400, 1400},
Timestamps: timestampsExpected,
}
r.MetricName.MetricGroup = []byte("foobar")
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`CLAmp_MAx(alias(time(),"foobar"), 1400)`, func(t *testing.T) {
t.Parallel()
q := `CLAmp_MAx(alias(time(), "foobar"), 1400)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1400, 1400, 1400},
Timestamps: timestampsExpected,
}
r.MetricName.MetricGroup = []byte("foobar")
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run("clamp_min(time(), -time()+3000)", func(t *testing.T) {
t.Parallel()
q := `clamp_min(time(), -time()+2500)`
@@ -789,6 +813,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 +1302,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 +1474,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 +1632,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 +1649,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 +1821,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)`
@@ -2068,6 +2208,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`avg(scalar) wiTHout (xx, yy)`, func(t *testing.T) {
t.Parallel()
q := `avg wiTHout (xx, yy) (123)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{123, 123, 123, 123, 123, 123},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`sum(time)`, func(t *testing.T) {
t.Parallel()
q := `sum(time()/100)`
@@ -2079,6 +2230,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 +2286,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"))`
@@ -2279,6 +2508,21 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected)
})
t.Run(`topk(1, nan_timeseries)`, func(t *testing.T) {
t.Parallel()
q := `topk(1, label_set(NaN, "foo", "bar") or label_set(time()/150, "baz", "sss")) default 0`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{6.666666666666667, 8, 9.333333333333334, 10.666666666666666, 12, 13.333333333333334},
Timestamps: timestampsExpected,
}
r1.MetricName.Tags = []storage.Tag{{
Key: []byte("baz"),
Value: []byte("sss"),
}}
resultExpected := []netstorage.Result{r1}
f(q, resultExpected)
})
t.Run(`topk(2)`, func(t *testing.T) {
t.Parallel()
q := `sort(topk(2, label_set(10, "foo", "bar") or label_set(time()/150, "baz", "sss")))`
@@ -2378,7 +2622,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 +2633,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 +2889,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 +2911,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 +2922,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 +2933,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 +2944,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 +2955,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 +2966,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 +2988,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}
@@ -3009,6 +3253,48 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`rollup_candlestick()`, func(t *testing.T) {
t.Parallel()
q := `sort(rollup_candlestick(round(rand(0),0.01)[:10s]))`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0.02, 0.02, 0.03, 0, 0.03, 0.02},
Timestamps: timestampsExpected,
}
r1.MetricName.Tags = []storage.Tag{{
Key: []byte("rollup"),
Value: []byte("low"),
}}
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0.32, 0.82, 0.13, 0.28, 0.86, 0.57},
Timestamps: timestampsExpected,
}
r2.MetricName.Tags = []storage.Tag{{
Key: []byte("rollup"),
Value: []byte("close"),
}}
r3 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0.9, 0.32, 0.82, 0.13, 0.28, 0.86},
Timestamps: timestampsExpected,
}
r3.MetricName.Tags = []storage.Tag{{
Key: []byte("rollup"),
Value: []byte("open"),
}}
r4 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0.85, 0.94, 0.97, 0.93, 0.98, 0.92},
Timestamps: timestampsExpected,
}
r4.MetricName.Tags = []storage.Tag{{
Key: []byte("rollup"),
Value: []byte("high"),
}}
resultExpected := []netstorage.Result{r1, r2, r3, r4}
f(q, resultExpected)
})
t.Run(`rollup_increase()`, func(t *testing.T) {
t.Parallel()
q := `sort(rollup_increase(time()))`
@@ -3047,7 +3333,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 +3342,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)
@@ -3429,7 +3715,14 @@ func TestExecError(t *testing.T) {
Deadline: netstorage.NewDeadline(time.Minute),
}
for i := 0; i < 4; i++ {
rv, err := Exec(ec, q)
rv, err := Exec(ec, q, false)
if err == nil {
t.Fatalf(`expecting non-nil error on %q`, q)
}
if rv != nil {
t.Fatalf(`expecting nil rv`)
}
rv, err = Exec(ec, q, true)
if err == nil {
t.Fatalf(`expecting non-nil error on %q`, q)
}
@@ -3461,6 +3754,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 +3800,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 +3832,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 +3847,31 @@ 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`)
// Invalid aggregates
f(`sum(1, 2)`)
f(`sum(1) foo (bar)`)
f(`sum foo () (bar)`)
f(`sum(foo) by (1)`)
f(`count(foo) without ("bar")`)
// 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),
@@ -47,6 +50,14 @@ var rollupFuncs = map[string]newRollupFunc{
"rollup_deriv": newRollupFuncOneArg(rollupFake),
"rollup_delta": newRollupFuncOneArg(rollupFake),
"rollup_increase": newRollupFuncOneArg(rollupFake), // + rollupFuncsRemoveCounterResets
"rollup_candlestick": newRollupFuncOneArg(rollupFake),
}
var rollupFuncsMayAdjustWindow = map[string]bool{
"deriv": true,
"deriv_fast": true,
"irate": true,
"rate": true,
}
var rollupFuncsRemoveCounterResets = map[string]bool{
@@ -64,6 +75,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 +132,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 +181,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 +190,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
@@ -246,12 +264,14 @@ func deltaValues(values []float64) {
if len(values) == 0 {
return
}
prevDelta := float64(0)
prevValue := values[0]
for i, v := range values[1:] {
values[i] = v - prevValue
prevDelta = v - prevValue
values[i] = prevDelta
prevValue = v
}
values[len(values)-1] = nan
values[len(values)-1] = prevDelta
}
func derivValues(values []float64, timestamps []int64) {
@@ -260,16 +280,23 @@ func derivValues(values []float64, timestamps []int64) {
if len(values) == 0 {
return
}
prevDeriv := float64(0)
prevValue := values[0]
prevTs := timestamps[0]
for i, v := range values[1:] {
ts := timestamps[i+1]
if ts == prevTs {
// Use the previous value for duplicate timestamps.
values[i] = prevDeriv
continue
}
dt := float64(ts-prevTs) * 1e-3
values[i] = (v - prevValue) / dt
prevDeriv = (v - prevValue) / dt
values[i] = prevDeriv
prevValue = v
prevTs = ts
}
values[len(values)-1] = nan
values[len(values)-1] = prevDeriv
}
type newRollupFunc func(args []interface{}) (rollupFunc, error)
@@ -296,11 +323,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 +369,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 +427,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 +453,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 +467,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 +483,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 +499,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 +512,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 +561,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 +587,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 +599,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 +650,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,43 +660,54 @@ 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]
values = values[:len(values)-1]
timestamps = timestamps[:len(timestamps)-1]
prevValue := rfa.prevValue
prevTimestamp := rfa.prevTimestamp
if len(values) == 0 {
if math.IsNaN(prevValue) {
return nan
}
} else {
prevValue = values[len(values)-1]
prevTimestamp = timestamps[len(timestamps)-1]
// Skip data points with duplicate timestamps.
for len(timestamps) > 0 && timestamps[len(timestamps)-1] >= tEnd {
timestamps = timestamps[:len(timestamps)-1]
}
dv := vEnd - prevValue
dt := tEnd - prevTimestamp
var tStart int64
var vStart float64
if len(timestamps) == 0 {
if math.IsNaN(rfa.prevValue) {
return 0
}
tStart = rfa.prevTimestamp
vStart = rfa.prevValue
} else {
tStart = timestamps[len(timestamps)-1]
vStart = values[len(timestamps)-1]
}
dv := vEnd - vStart
dt := tEnd - tStart
return dv / (float64(dt) / 1000)
}
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 +719,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 +734,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 +753,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 +762,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 +794,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 +812,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
@@ -64,7 +71,7 @@ func InitRollupResultCache(cachePath string) {
return stats
}
if len(rollupResultCachePath) > 0 {
logger.Infof("loaded rollupResult cache from %q in %s; entriesCount: %d, bytesSize: %d",
logger.Infof("loaded rollupResult cache from %q in %s; entriesCount: %d, sizeBytes: %d",
rollupResultCachePath, time.Since(startTime), fcs().EntriesCount, fcs().BytesSize)
}
@@ -101,7 +108,7 @@ func StopRollupResultCache() {
var fcs fastcache.Stats
rollupResultCacheV.c.UpdateStats(&fcs)
rollupResultCacheV.c.Reset()
logger.Infof("saved rollupResult cache to %q in %s; entriesCount: %d, bytesSize: %d",
logger.Infof("saved rollupResult cache to %q in %s; entriesCount: %d, sizeBytes: %d",
rollupResultCachePath, time.Since(startTime), fcs.EntriesCount, fcs.BytesSize)
}
}
@@ -118,8 +125,8 @@ func ResetRollupResultCache() {
rollupResultCacheV.c.Reset()
}
func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExpr, window int64) (tss []*timeseries, newStart int64) {
if !ec.mayCache() {
func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExpr, iafc *incrementalAggrFuncContext, window int64) (tss []*timeseries, newStart int64) {
if *disableCache || !ec.mayCache() {
return nil, ec.Start
}
@@ -127,7 +134,7 @@ func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExp
bb := bbPool.Get()
defer bbPool.Put(bb)
bb.B = marshalRollupResultCacheKey(bb.B[:0], funcName, me, window, ec.Step)
bb.B = marshalRollupResultCacheKey(bb.B[:0], funcName, me, iafc, window, ec.Step)
metainfoBuf := rrc.c.Get(nil, bb.B)
if len(metainfoBuf) == 0 {
return nil, ec.Start
@@ -145,7 +152,7 @@ func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExp
if len(resultBuf) == 0 {
mi.RemoveKey(key)
metainfoBuf = mi.Marshal(metainfoBuf[:0])
bb.B = marshalRollupResultCacheKey(bb.B[:0], funcName, me, window, ec.Step)
bb.B = marshalRollupResultCacheKey(bb.B[:0], funcName, me, iafc, window, ec.Step)
rrc.c.Set(bb.B, metainfoBuf)
return nil, ec.Start
}
@@ -189,8 +196,8 @@ func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExp
return tss, newStart
}
func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExpr, window int64, tss []*timeseries) {
if len(tss) == 0 || !ec.mayCache() {
func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExpr, iafc *incrementalAggrFuncContext, window int64, tss []*timeseries) {
if *disableCache || len(tss) == 0 || !ec.mayCache() {
return
}
@@ -235,7 +242,7 @@ func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExp
bb.B = key.Marshal(bb.B[:0])
rrc.c.SetBig(bb.B, tssMarshaled)
bb.B = marshalRollupResultCacheKey(bb.B[:0], funcName, me, window, ec.Step)
bb.B = marshalRollupResultCacheKey(bb.B[:0], funcName, me, iafc, window, ec.Step)
metainfoBuf := rrc.c.Get(nil, bb.B)
var mi rollupResultCacheMetainfo
if len(metainfoBuf) > 0 {
@@ -263,10 +270,16 @@ var (
var tooBigRollupResults = metrics.NewCounter("vm_too_big_rollup_results_total")
// Increment this value every time the format of the cache changes.
const rollupResultCacheVersion = 4
const rollupResultCacheVersion = 5
func marshalRollupResultCacheKey(dst []byte, funcName string, me *metricExpr, window, step int64) []byte {
func marshalRollupResultCacheKey(dst []byte, funcName string, me *metricExpr, iafc *incrementalAggrFuncContext, window, step int64) []byte {
dst = append(dst, rollupResultCacheVersion)
if iafc == nil {
dst = append(dst, 0)
} else {
dst = append(dst, 1)
dst = iafc.ae.AppendString(dst)
}
dst = encoding.MarshalUint64(dst, uint64(len(funcName)))
dst = append(dst, funcName...)
dst = encoding.MarshalInt64(dst, window)

View File

@@ -23,10 +23,15 @@ func TestRollupResultCache(t *testing.T) {
Value: []byte("xxx"),
}},
}
iafc := &incrementalAggrFuncContext{
ae: &aggrFuncExpr{
Name: "foobar",
},
}
// Try obtaining an empty value.
t.Run("empty", func(t *testing.T) {
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != ec.Start {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, ec.Start)
}
@@ -36,21 +41,42 @@ func TestRollupResultCache(t *testing.T) {
})
// Store timeseries overlapping with start
t.Run("start-overlap", func(t *testing.T) {
t.Run("start-overlap-no-iafc", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
&timeseries{
{
Timestamps: []int64{800, 1000, 1200},
Values: []float64{0, 1, 2},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 1400 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1400)
}
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{1, 2},
},
}
testTimeseriesEqual(t, tss, tssExpected)
})
t.Run("start-overlap-with-iafc", func(t *testing.T) {
ResetRollupResultCache()
tss := []*timeseries{
{
Timestamps: []int64{800, 1000, 1200},
Values: []float64{0, 1, 2},
},
}
rollupResultCacheV.Put(funcName, ec, me, iafc, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, iafc, window)
if newStart != 1400 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1400)
}
tssExpected := []*timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{1, 2},
},
@@ -62,13 +88,13 @@ 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},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 1000 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1000)
}
@@ -81,13 +107,13 @@ 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},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 1000 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1000)
}
@@ -100,13 +126,13 @@ 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},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 1000 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1000)
}
@@ -119,13 +145,13 @@ 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},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 1000 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1000)
}
@@ -138,18 +164,18 @@ 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},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 2200 {
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,18 +187,18 @@ 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},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 2200 {
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},
},
@@ -191,8 +217,8 @@ func TestRollupResultCache(t *testing.T) {
}
tss = append(tss, ts)
}
rollupResultCacheV.Put(funcName, ec, me, window, tss)
tssResult, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss)
tssResult, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 2200 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 2200)
}
@@ -203,32 +229,32 @@ 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},
},
}
rollupResultCacheV.Put(funcName, ec, me, window, tss1)
rollupResultCacheV.Put(funcName, ec, me, window, tss2)
rollupResultCacheV.Put(funcName, ec, me, window, tss3)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, window)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss1)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss2)
rollupResultCacheV.Put(funcName, ec, me, nil, window, tss3)
tss, newStart := rollupResultCacheV.Get(funcName, ec, me, nil, window)
if newStart != 1400 {
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1400)
}
tssExpected := []*timeseries{
&timeseries{
{
Timestamps: []int64{1000, 1200},
Values: []float64{1, 2},
},
@@ -249,14 +275,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 +292,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 +308,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 +316,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 +325,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 +347,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 +362,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

@@ -10,6 +10,68 @@ var (
testTimestamps = []int64{5, 15, 24, 36, 49, 60, 78, 80, 97, 115, 120, 130}
)
func TestRollupIderivDuplicateTimestamps(t *testing.T) {
rfa := &rollupFuncArg{
values: []float64{1, 2, 3, 4, 5},
timestamps: []int64{100, 100, 200, 300, 300},
}
n := rollupIderiv(rfa)
if n != 20 {
t.Fatalf("unexpected value; got %v; want %v", n, 20)
}
rfa = &rollupFuncArg{
values: []float64{1, 2, 3, 4, 5},
timestamps: []int64{100, 100, 300, 300, 300},
}
n = rollupIderiv(rfa)
if n != 15 {
t.Fatalf("unexpected value; got %v; want %v", n, 15)
}
rfa = &rollupFuncArg{
prevValue: nan,
values: []float64{},
timestamps: []int64{},
}
n = rollupIderiv(rfa)
if !math.IsNaN(n) {
t.Fatalf("unexpected value; got %v; want %v", n, nan)
}
rfa = &rollupFuncArg{
prevValue: nan,
values: []float64{15},
timestamps: []int64{100},
}
n = rollupIderiv(rfa)
if n != 0 {
t.Fatalf("unexpected value; got %v; want %v", n, 0)
}
rfa = &rollupFuncArg{
prevTimestamp: 100,
prevValue: 10,
values: []float64{15},
timestamps: []int64{100},
}
n = rollupIderiv(rfa)
if n != inf {
t.Fatalf("unexpected value; got %v; want %v", n, inf)
}
rfa = &rollupFuncArg{
prevTimestamp: 100,
prevValue: 10,
values: []float64{15, 20},
timestamps: []int64{100, 100},
}
n = rollupIderiv(rfa)
if n != inf {
t.Fatalf("unexpected value; got %v; want %v", n, inf)
}
}
func TestRemoveCounterResets(t *testing.T) {
removeCounterResets(nil)
@@ -38,19 +100,19 @@ func TestDeltaValues(t *testing.T) {
values := []float64{123}
deltaValues(values)
valuesExpected := []float64{nan}
valuesExpected := []float64{0}
testRowsEqual(t, values, testTimestamps[:1], valuesExpected, testTimestamps[:1])
values = append([]float64{}, testValues...)
deltaValues(values)
valuesExpected = []float64{-89, 10, -23, 33, -20, 65, -87, 32, -12, 2, 0, nan}
valuesExpected = []float64{-89, 10, -23, 33, -20, 65, -87, 32, -12, 2, 0, 0}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
// remove counter resets
values = append([]float64{}, testValues...)
removeCounterResets(values)
deltaValues(values)
valuesExpected = []float64{34, 10, 21, 33, 34, 65, 12, 32, 32, 2, 0, nan}
valuesExpected = []float64{34, 10, 21, 33, 34, 65, 12, 32, 32, 2, 0, 0}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
}
@@ -59,13 +121,13 @@ func TestDerivValues(t *testing.T) {
values := []float64{123}
derivValues(values, testTimestamps[:1])
valuesExpected := []float64{nan}
valuesExpected := []float64{0}
testRowsEqual(t, values, testTimestamps[:1], valuesExpected, testTimestamps[:1])
values = append([]float64{}, testValues...)
derivValues(values, testTimestamps)
valuesExpected = []float64{-8900, 1111.111111111111, -1916.6666666666665, 2538.461538461538, -1818.1818181818182, 3611.111111111111,
-43500, 1882.3529411764705, -666.6666666666666, 400, 0, nan}
-43500, 1882.3529411764705, -666.6666666666666, 400, 0, 0}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
// remove counter resets
@@ -73,8 +135,15 @@ func TestDerivValues(t *testing.T) {
removeCounterResets(values)
derivValues(values, testTimestamps)
valuesExpected = []float64{3400, 1111.111111111111, 1750, 2538.461538461538, 3090.909090909091, 3611.111111111111,
6000, 1882.3529411764705, 1777.7777777777776, 400, 0, nan}
6000, 1882.3529411764705, 1777.7777777777776, 400, 0, 0}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
// duplicate timestamps
values = []float64{1, 2, 3, 4, 5, 6, 7}
timestamps := []int64{100, 100, 200, 200, 300, 400, 400}
derivValues(values, timestamps)
valuesExpected = []float64{0, 20, 20, 20, 10, 10, 10}
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
}
func testRollupFunc(t *testing.T, funcName string, args []interface{}, meExpected *metricExpr, vExpected float64) {
@@ -143,10 +212,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 +258,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 +272,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 +333,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 +341,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 +364,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 +389,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 +409,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 +423,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 +440,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 +454,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 +468,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 +485,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 +499,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 +513,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 +527,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 +541,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 +555,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 +569,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 +583,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 +611,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 +625,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 +639,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 +653,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 +667,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 +681,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 +695,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

@@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"strconv"
"sync"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
@@ -38,11 +39,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) {
@@ -59,6 +62,20 @@ func (ts *timeseries) CopyShallow(src *timeseries) {
ts.denyReuse = true
}
func getTimeseries() *timeseries {
if v := timeseriesPool.Get(); v != nil {
return v.(*timeseries)
}
return &timeseries{}
}
func putTimeseries(ts *timeseries) {
ts.Reset()
timeseriesPool.Put(ts)
}
var timeseriesPool sync.Pool
func marshalTimeseriesFast(tss []*timeseries, maxSize int, step int64) []byte {
if len(tss) == 0 {
logger.Panicf("BUG: tss cannot be empty")

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,
@@ -123,7 +125,8 @@ func newTransformFuncOneArg(tf func(v float64) float64) transformFunc {
}
func doTransformValues(arg []*timeseries, tf func(values []float64), fe *funcExpr) ([]*timeseries, error) {
keepMetricGroup := transformFuncsKeepMetricGroup[fe.Name]
name := strings.ToLower(fe.Name)
keepMetricGroup := transformFuncsKeepMetricGroup[name]
for _, ts := range arg {
if !keepMetricGroup {
ts.MetricName.ResetMetricGroup()
@@ -294,14 +297,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 +395,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 +635,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 +808,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 +859,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 +874,7 @@ func transformLabelReplace(tfa *transformFuncArg) ([]*timeseries, error) {
mn.RemoveTag(dstLabel)
}
}
return rvs, nil
return tss, nil
}
func transformLn(v float64) float64 {

View File

@@ -28,11 +28,20 @@ var (
// Init initializes vmstorage.
func Init() {
InitWithoutMetrics()
registerStorageMetrics()
}
// InitWithoutMetrics must be called instead of Init inside tests.
//
// This allows multiple Init / Stop cycles.
func InitWithoutMetrics() {
if err := encoding.CheckPrecisionBits(uint8(*precisionBits)); err != nil {
logger.Fatalf("invalid `-precisionBits`: %s", err)
}
logger.Infof("opening storage at %q with retention period %d months", *DataPath, *retentionPeriod)
startTime := time.Now()
WG = syncwg.WaitGroup{}
strg, err := storage.OpenStorage(*DataPath, *retentionPeriod)
if err != nil {
logger.Fatalf("cannot open a storage at %s with retention period %d months: %s", *DataPath, *retentionPeriod, err)
@@ -45,10 +54,9 @@ func Init() {
partsCount := tm.SmallPartsCount + tm.BigPartsCount
blocksCount := tm.SmallBlocksCount + tm.BigBlocksCount
rowsCount := tm.SmallRowsCount + tm.BigRowsCount
logger.Infof("successfully opened storage %q in %s; partsCount: %d; blocksCount: %d; rowsCount: %d",
*DataPath, time.Since(startTime), partsCount, blocksCount, rowsCount)
registerStorageMetrics(Storage)
sizeBytes := tm.SmallSizeBytes + tm.BigSizeBytes
logger.Infof("successfully opened storage %q in %s; partsCount: %d; blocksCount: %d; rowsCount: %d; sizeBytes: %d",
*DataPath, time.Since(startTime), partsCount, blocksCount, rowsCount, sizeBytes)
}
// Storage is a storage.
@@ -96,6 +104,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)
@@ -203,7 +219,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
}
}
func registerStorageMetrics(strg *storage.Storage) {
func registerStorageMetrics() {
mCache := &storage.Metrics{}
var mCacheLock sync.Mutex
var lastUpdateTime time.Time
@@ -215,7 +231,7 @@ func registerStorageMetrics(strg *storage.Storage) {
return mCache
}
var mc storage.Metrics
strg.UpdateMetrics(&mc)
Storage.UpdateMetrics(&mc)
mCache = &mc
lastUpdateTime = time.Now()
return mCache
@@ -285,6 +301,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)
@@ -320,6 +348,16 @@ func registerStorageMetrics(strg *storage.Storage) {
return float64(idbm().BlocksCount)
})
metrics.NewGauge(`vm_data_size_bytes{type="storage/big"}`, func() float64 {
return float64(tm().BigSizeBytes)
})
metrics.NewGauge(`vm_data_size_bytes{type="storage/small"}`, func() float64 {
return float64(tm().SmallSizeBytes)
})
metrics.NewGauge(`vm_data_size_bytes{type="indexdb"}`, func() float64 {
return float64(idbm().SizeBytes)
})
metrics.NewGauge(`vm_rows{type="storage/big"}`, func() float64 {
return float64(tm().BigRowsCount)
})
@@ -342,6 +380,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,24 +398,30 @@ 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())
})
metrics.NewGauge(`vm_cache_size_bytes{type="storage/tsid"}`, func() float64 {
return float64(m().TSIDCacheBytesSize)
return float64(m().TSIDCacheSizeBytes)
})
metrics.NewGauge(`vm_cache_size_bytes{type="storage/metricIDs"}`, func() float64 {
return float64(m().MetricIDCacheBytesSize)
return float64(m().MetricIDCacheSizeBytes)
})
metrics.NewGauge(`vm_cache_size_bytes{type="storage/metricName"}`, func() float64 {
return float64(m().MetricNameCacheBytesSize)
return float64(m().MetricNameCacheSizeBytes)
})
metrics.NewGauge(`vm_cache_size_bytes{type="storage/date_metricID"}`, func() float64 {
return float64(m().DateMetricIDCacheBytesSize)
return float64(m().DateMetricIDCacheSizeBytes)
})
metrics.NewGauge(`vm_cache_size_bytes{type="indexdb/tagFilters"}`, func() float64 {
return float64(idbm().TagCacheBytesSize)
return float64(idbm().TagCacheSizeBytes)
})
metrics.NewGauge(`vm_cache_size_bytes{type="indexdb/uselessTagFilters"}`, func() float64 {
return float64(idbm().UselessTagFiltersCacheSizeBytes)
})
metrics.NewGauge(`vm_cache_requests_total{type="storage/tsid"}`, func() float64 {
@@ -404,6 +451,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 +485,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.6.2
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.6.2 h1:VMe8c8ZBPgNVZkPoT06LsoU2nb+8e7iPaOWbVRNhxjo=
github.com/VictoriaMetrics/metrics v1.6.2/go.mod h1:LU2j9qq7xqZYXz8tF3/RQnB2z2MbZms5TDiIg9/NHiQ=
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.
@@ -174,6 +185,18 @@ func MustClose(f *os.File) {
}
}
// MustFileSize returns file size for the given path.
func MustFileSize(path string) uint64 {
fi, err := os.Stat(path)
if err != nil {
logger.Panicf("FATAL: cannot stat %q: %s", path, err)
}
if fi.IsDir() {
logger.Panicf("FATAL: %q must be a file, not a directory", path)
}
return uint64(fi.Size())
}
// IsPathExist returns whether the given path exists.
func IsPathExist(path string) bool {
if _, err := os.Stat(path); err != nil {
@@ -185,18 +208,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 +311,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

@@ -2,6 +2,7 @@ package memory
import (
"io/ioutil"
"os/exec"
"strconv"
"syscall"
@@ -19,17 +20,57 @@ func sysTotalMemory() int {
// See https://stackoverflow.com/questions/42187085/check-mem-limit-within-a-docker-container .
data, err := ioutil.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes")
if err != nil {
return totalMem
// Try determining the amount of memory inside lxc container.
mem, err := readLXCMemoryLimit(totalMem)
if err != nil {
return totalMem
}
return mem
}
for len(data) > 0 && data[len(data)-1] == '\n' {
data = data[:len(data)-1]
}
mem, err := strconv.Atoi(string(data))
mem, err := readPositiveInt(data, totalMem)
if err != nil {
return totalMem
}
if mem > totalMem {
mem = totalMem
if mem != totalMem {
return mem
}
// Try reading LXC memory limit, since it looks like the cgroup limit doesn't work
mem, err = readLXCMemoryLimit(totalMem)
if err != nil {
return totalMem
}
return mem
}
func readLXCMemoryLimit(totalMem int) (int, error) {
// Read memory limit according to https://unix.stackexchange.com/questions/242718/how-to-find-out-how-much-memory-lxc-container-is-allowed-to-consume
// This should properly determine the limit inside lxc container.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/84
cmd := exec.Command("/bin/sh", "-c",
`cat /sys/fs/cgroup/memory$(cat /proc/self/cgroup | grep memory | cut -d: -f3)/memory.limit_in_bytes`)
data, err := cmd.Output()
if err != nil {
return 0, err
}
return readPositiveInt(data, totalMem)
}
func readPositiveInt(data []byte, maxN int) (int, error) {
for len(data) > 0 && data[len(data)-1] == '\n' {
data = data[:len(data)-1]
}
n, err := strconv.ParseUint(string(data), 10, 64)
if err != nil {
return 0, err
}
if int64(n) < 0 || int64(int(n)) != int64(n) {
// Int overflow.
return maxN, nil
}
ni := int(n)
if ni > maxN {
return maxN, nil
}
return ni, nil
}

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

@@ -84,13 +84,18 @@ func (ip *inmemoryPart) Init(ib *inmemoryBlock) {
// It is unsafe re-using ip while the returned part is in use.
func (ip *inmemoryPart) NewPart() *part {
ph := ip.ph
p, err := newPart(&ph, "", ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
size := ip.size()
p, err := newPart(&ph, "", size, ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
if err != nil {
logger.Panicf("BUG: cannot create a part from inmemoryPart: %s", err)
}
return p
}
func (ip *inmemoryPart) size() uint64 {
return uint64(len(ip.metaindexData.B) + len(ip.indexData.B) + len(ip.itemsData.B) + len(ip.lensData.B))
}
func getInmemoryPart() *inmemoryPart {
v := ipPool.Get()
if v == nil {

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

@@ -48,6 +48,8 @@ type part struct {
path string
size uint64
mrs []metaindexRow
indexFile fs.ReadAtCloser
@@ -71,6 +73,7 @@ func openFilePart(path string) (*part, error) {
if err != nil {
return nil, fmt.Errorf("cannot open %q: %s", metaindexPath, err)
}
metaindexSize := fs.MustFileSize(metaindexPath)
indexPath := path + "/index.bin"
indexFile, err := fs.OpenReaderAt(indexPath)
@@ -78,6 +81,7 @@ func openFilePart(path string) (*part, error) {
metaindexFile.MustClose()
return nil, fmt.Errorf("cannot open %q: %s", indexPath, err)
}
indexSize := fs.MustFileSize(indexPath)
itemsPath := path + "/items.bin"
itemsFile, err := fs.OpenReaderAt(itemsPath)
@@ -86,6 +90,7 @@ func openFilePart(path string) (*part, error) {
indexFile.MustClose()
return nil, fmt.Errorf("cannot open %q: %s", itemsPath, err)
}
itemsSize := fs.MustFileSize(itemsPath)
lensPath := path + "/lens.bin"
lensFile, err := fs.OpenReaderAt(lensPath)
@@ -95,11 +100,13 @@ func openFilePart(path string) (*part, error) {
itemsFile.MustClose()
return nil, fmt.Errorf("cannot open %q: %s", lensPath, err)
}
lensSize := fs.MustFileSize(lensPath)
return newPart(&ph, path, metaindexFile, indexFile, itemsFile, lensFile)
size := metaindexSize + indexSize + itemsSize + lensSize
return newPart(&ph, path, size, metaindexFile, indexFile, itemsFile, lensFile)
}
func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser, indexFile, itemsFile, lensFile fs.ReadAtCloser) (*part, error) {
func newPart(ph *partHeader, path string, size uint64, metaindexReader filestream.ReadCloser, indexFile, itemsFile, lensFile fs.ReadAtCloser) (*part, error) {
var errors []error
mrs, err := unmarshalMetaindexRows(nil, metaindexReader)
if err != nil {
@@ -109,6 +116,7 @@ func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser,
p := &part{
path: path,
size: size,
mrs: mrs,
indexFile: indexFile,

View File

@@ -356,16 +356,15 @@ func (ps *partSearch) readInmemoryBlock(bh *blockHeader) (*inmemoryBlock, error)
}
func binarySearchKey(items [][]byte, key []byte) int {
if len(items) > 0 {
if string(key) <= string(items[0]) {
// Fast path - the item is the first.
return 0
}
if len(items) > 1 && string(key) <= string(items[1]) {
// Fast path - the item is the second.
return 1
}
if len(items) == 0 {
return 0
}
if string(key) <= string(items[0]) {
// Fast path - the item is the first.
return 0
}
items = items[1:]
offset := uint(1)
// This has been copy-pasted from https://golang.org/src/sort/search.go
n := uint(len(items))
@@ -378,5 +377,5 @@ func binarySearchKey(items [][]byte, key []byte) int {
j = h
}
}
return int(i)
return int(i + offset)
}

View File

@@ -156,7 +156,8 @@ func newTestPart(blocksCount, maxItemsPerBlock int) (*part, []string, error) {
if itemsMerged != uint64(len(items)) {
return nil, nil, fmt.Errorf("unexpected itemsMerged; got %d; want %d", itemsMerged, len(items))
}
p, err := newPart(&ip.ph, "partName", ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
size := ip.size()
p, err := newPart(&ip.ph, "partName", size, ip.metaindexData.NewReader(), &ip.indexData, &ip.itemsData, &ip.lensData)
if err != nil {
return nil, nil, fmt.Errorf("cannot create part: %s", err)
}

View File

@@ -161,8 +161,8 @@ func OpenTable(path string) (*Table, error) {
var m TableMetrics
tb.UpdateMetrics(&m)
logger.Infof("table %q has been opened in %s; partsCount: %d; blocksCount: %d, itemsCount: %d",
path, time.Since(startTime), m.PartsCount, m.BlocksCount, m.ItemsCount)
logger.Infof("table %q has been opened in %s; partsCount: %d; blocksCount: %d, itemsCount: %d; sizeBytes: %d",
path, time.Since(startTime), m.PartsCount, m.BlocksCount, m.ItemsCount, m.SizeBytes)
return tb, nil
}
@@ -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
@@ -242,6 +242,7 @@ type TableMetrics struct {
BlocksCount uint64
ItemsCount uint64
SizeBytes uint64
DataBlocksCacheSize uint64
DataBlocksCacheRequests uint64
@@ -274,6 +275,7 @@ func (tb *Table) UpdateMetrics(m *TableMetrics) {
m.BlocksCount += p.ph.blocksCount
m.ItemsCount += p.ph.itemsCount
m.SizeBytes += p.size
m.DataBlocksCacheSize += p.ibCache.Len()
m.DataBlocksCacheRequests += p.ibCache.Requests()
@@ -727,6 +729,7 @@ func (tb *Table) mergeParts(pws []*partWrapper, stopCh <-chan struct{}, isOuterP
if err != nil {
return fmt.Errorf("cannot open merged part %q: %s", dstPartPath, err)
}
newPSize := newP.size
newPW := &partWrapper{
p: newP,
refCount: 1,
@@ -761,7 +764,7 @@ func (tb *Table) mergeParts(pws []*partWrapper, stopCh <-chan struct{}, isOuterP
d := time.Since(startTime)
if d > 10*time.Second {
logger.Infof("merged %d items in %s at %d items/sec to %q", outItemsCount, d, int(float64(outItemsCount)/d.Seconds()), dstPartPath)
logger.Infof("merged %d items in %s at %d items/sec to %q; sizeBytes: %d", outItemsCount, d, int(float64(outItemsCount)/d.Seconds()), dstPartPath, newPSize)
}
return nil
@@ -846,22 +849,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 +964,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 +1032,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 +1058,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()
@@ -115,16 +153,26 @@ func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache) (
// IndexDBMetrics contains essential metrics for indexDB.
type IndexDBMetrics struct {
TagCacheSize uint64
TagCacheBytesSize uint64
TagCacheSizeBytes uint64
TagCacheRequests uint64
TagCacheMisses uint64
UselessTagFiltersCacheSize uint64
UselessTagFiltersCacheSizeBytes 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.TagCacheSizeBytes += cs.BytesSize
m.TagCacheRequests += cs.GetBigCalls
m.TagCacheMisses += cs.Misses
cs.Reset()
db.uselessTagFiltersCache.UpdateStats(&cs)
m.UselessTagFiltersCacheSize += cs.EntriesCount
m.UselessTagFiltersCacheSizeBytes += 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,16 +1218,91 @@ 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 = append(kb.B[:0], uselessMultiTagFiltersKeyPrefix)
kb.B = encoding.MarshalUint64(kb.B, uint64(maxMetrics))
kb.B = tfs.marshal(kb.B)
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 != errTooManyMetrics {
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 = append(kb.B[:0], uselessMultiTagFiltersKeyPrefix)
kb.B = encoding.MarshalUint64(kb.B, uint64(maxMetrics))
kb.B = tfs.marshal(kb.B)
is.db.uselessTagFiltersCache.Set(kb.B, uselessTagFilterCacheValue)
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
kb := &is.kb
uselessTagFilters := 0
for i := range tfs.tfs {
tf := &tfs.tfs[i]
if tf.isNegative {
// Skip negative filters.
continue
}
metricIDs, err := is.getMetricIDsMapForTagFilter(tf, maxMetrics)
kb.B = append(kb.B[:0], uselessSingleTagFilterKeyPrefix)
kb.B = encoding.MarshalUint64(kb.B[:0], uint64(maxMetrics))
kb.B = tf.Marshal(kb.B)
if len(is.db.uselessTagFiltersCache.Get(nil, kb.B)) > 0 {
// Skip useless work below, since the tf matches at least maxMetrics metrics.
uselessTagFilters++
continue
}
metricIDs, err := is.getMetricIDsForTagFilter(tf, maxMetrics)
if err != nil {
if err == errFallbackToMetricNameMatch {
// Skip tag filters requiring to scan for too many metrics.
@@ -1149,25 +1310,37 @@ func (is *indexSearch) getTagFilterWithMinMetricIDsMap(tfs *TagFilters, maxMetri
}
return nil, nil, fmt.Errorf("cannot find MetricIDs for tagFilter %s: %s", tf, err)
}
if minTf == nil || len(metricIDs) < len(minMetricIDs) {
minMetricIDs = metricIDs
minTf = tf
maxMetrics = len(minMetricIDs)
if maxMetrics <= 1 {
// There is no need in inspecting other filters, since minTf
// already matches 0 or 1 metric.
break
}
if len(metricIDs) >= maxMetrics {
// The tf matches at least maxMetrics. Skip it
kb.B = append(kb.B[:0], uselessSingleTagFilterKeyPrefix)
kb.B = encoding.MarshalUint64(kb.B[:0], uint64(maxMetrics))
kb.B = tf.Marshal(kb.B)
is.db.uselessTagFiltersCache.Set(kb.B, uselessTagFilterCacheValue)
uselessTagFilters++
continue
}
minMetricIDs = metricIDs
minTf = tf
maxMetrics = len(minMetricIDs)
if maxMetrics <= 1 {
// There is no need in inspecting other filters, since minTf
// already matches 0 or 1 metric.
break
}
}
if minTf != nil {
return minTf, minMetricIDs, nil
}
if uselessTagFilters == len(tfs.tfs) {
// All the tag filters return at least maxMetrics entries.
return nil, nil, errTooManyMetrics
}
// 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 +1409,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 +1473,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 +1485,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 +1494,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 +1507,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,30 +1522,55 @@ func (is *indexSearch) searchMetricIDsMap(metricIDs map[uint64]struct{}, tfs *Ta
return nil
}
func (is *indexSearch) getMetricIDsMapForTagFilter(tf *tagFilter, maxMetrics int) (map[uint64]struct{}, error) {
const (
uselessSingleTagFilterKeyPrefix = 0
uselessMultiTagFiltersKeyPrefix = 1
)
var uselessTagFilterCacheValue = []byte("1")
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
}
// Slow path - scan all the rows with tf.prefix
// Slow path - scan for all the rows with the given prefix.
maxLoops := maxMetrics * maxIndexScanLoopsPerMetric
err := is.getMetricIDsForTagFilterSlow(tf, maxLoops, func(metricID uint64) bool {
metricIDs[metricID] = struct{}{}
return len(metricIDs) < maxMetrics
})
if err != nil {
return nil, err
}
return metricIDs, nil
}
func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, maxLoops int, f func(metricID uint64) bool) error {
if len(tf.orSuffixes) > 0 {
logger.Panicf("BUG: the getMetricIDsForTagFilterSlow must be called only for empty tf.orSuffixes; got %s", tf.orSuffixes)
}
// Scan all the rows with tf.prefix and call f on every tf match.
loops := 0
ts := &is.ts
kb := &is.kb
var prevMatchingK []byte
var prevMatch bool
ts.Seek(tf.prefix)
for len(metricIDs) < maxMetrics && ts.NextItem() {
for ts.NextItem() {
loops++
if loops > maxLoops {
return nil, errFallbackToMetricNameMatch
return errFallbackToMetricNameMatch
}
k := ts.Item
if !bytes.HasPrefix(k, tf.prefix) {
break
@@ -1409,28 +1579,52 @@ func (is *indexSearch) getMetricIDsMapForTagFilter(tf *tagFilter, maxMetrics int
// Get MetricID from k (the last 8 bytes).
k = k[len(tf.prefix):]
if len(k) < 8 {
return nil, fmt.Errorf("invald key suffix size; want at least %d bytes; got %d bytes", 8, len(k))
return fmt.Errorf("invald key suffix size; want at least %d bytes; got %d bytes", 8, len(k))
}
v := k[len(k)-8:]
k = k[:len(k)-8]
metricID := encoding.UnmarshalUint64(v)
if prevMatch && string(k) == string(prevMatchingK) {
// Fast path: the same tag value found.
// There is no need in checking it again with potentially
// slow tf.matchSuffix, which may call regexp.
if !f(metricID) {
break
}
continue
}
ok, err := tf.matchSuffix(k)
if err != nil {
return nil, fmt.Errorf("error when matching %s: %s", tf, err)
return fmt.Errorf("error when matching %s: %s", tf, err)
}
if !ok {
prevMatch = false
// Optimization: skip all the metricIDs for the given tag value
kb.B = append(kb.B[:0], ts.Item[:len(ts.Item)-8]...)
// The last char in kb.B must be tagSeparatorChar. Just increment it
// in order to jump to the next tag value.
if len(kb.B) == 0 || kb.B[len(kb.B)-1] != tagSeparatorChar || tagSeparatorChar >= 0xff {
return fmt.Errorf("data corruption: the last char in k=%X must be %X", kb.B, tagSeparatorChar)
}
kb.B[len(kb.B)-1]++
ts.Seek(kb.B)
continue
}
metricID := encoding.UnmarshalUint64(v)
metricIDs[metricID] = struct{}{}
prevMatch = true
prevMatchingK = append(prevMatchingK[:0], k...)
if !f(metricID) {
break
}
}
if err := ts.Error(); err != nil {
return nil, fmt.Errorf("error when searching for tag filter prefix %q: %s", tf.prefix, err)
return fmt.Errorf("error when searching for tag filter prefix %q: %s", tf.prefix, err)
}
return metricIDs, nil
return 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 +1634,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 +1644,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 +1652,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 +1686,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 {
@@ -1519,6 +1713,15 @@ func (is *indexSearch) updateMetricIDsMapForOrSuffixWithFilter(prefix []byte, me
}
metricID := encoding.UnmarshalUint64(v)
if metricID != nextMetricID {
// Skip metricIDs smaller than the found metricID, since they don't
// match anything.
if len(sortedFilter) > 0 && metricID > sortedFilter[0] {
sortedFilter = sortedFilter[1:]
n := sort.Search(len(sortedFilter), func(i int) bool {
return metricID <= sortedFilter[i]
})
sortedFilter = sortedFilter[n:]
}
continue
}
if isNegative {
@@ -1533,7 +1736,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 +1744,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 +1891,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 +1938,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 +1952,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
@@ -1676,47 +1960,19 @@ func (is *indexSearch) intersectMetricIDsMapForTagFilter(tf *tagFilter, filter m
// Slow path - scan for all the rows with the given prefix.
maxLoops := len(filter) * maxIndexScanLoopsPerMetric
loops := 0
ts := &is.ts
ts.Seek(tf.prefix)
for ts.NextItem() {
loops++
if loops > maxLoops {
return nil, errFallbackToMetricNameMatch
}
k := ts.Item
if !bytes.HasPrefix(k, tf.prefix) {
break
}
// Extract MetricID from k (the last 8 bytes).
k = k[len(tf.prefix):]
if len(k) < 8 {
return nil, fmt.Errorf("cannot extract metricID from k; want at least %d bytes; got %d bytes", 8, len(k))
}
v := k[len(k)-8:]
k = k[:len(k)-8]
ok, err := tf.matchSuffix(k)
if err != nil {
return nil, fmt.Errorf("error when matching %s: %s", tf, err)
}
if !ok {
continue
}
metricID := encoding.UnmarshalUint64(v)
err := is.getMetricIDsForTagFilterSlow(tf, maxLoops, func(metricID uint64) bool {
if tf.isNegative {
// filter must be equal to metricIDs
delete(metricIDs, metricID)
continue
return true
}
if _, ok := filter[metricID]; ok {
metricIDs[metricID] = struct{}{}
}
}
if err := ts.Error(); err != nil {
return nil, fmt.Errorf("error searching %q: %s", tf.prefix, err)
return true
})
if 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

@@ -51,7 +51,8 @@ func (mp *inmemoryPart) InitFromRows(rows []rawRow) {
// It is unsafe re-using mp while the returned part is in use.
func (mp *inmemoryPart) NewPart() (*part, error) {
ph := mp.ph
return newPart(&ph, "", mp.metaindexData.NewReader(), &mp.timestampsData, &mp.valuesData, &mp.indexData)
size := uint64(len(mp.timestampsData.B) + len(mp.valuesData.B) + len(mp.indexData.B) + len(mp.metaindexData.B))
return newPart(&ph, "", size, mp.metaindexData.NewReader(), &mp.timestampsData, &mp.valuesData, &mp.indexData)
}
func getInmemoryPart() *inmemoryPart {

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

@@ -50,6 +50,12 @@ func (tag *Tag) copyFrom(src *Tag) {
tag.Value = append(tag.Value[:0], src.Value...)
}
func marshalTagValueNoTrailingTagSeparator(dst, src []byte) []byte {
dst = marshalTagValue(dst, src)
// Remove trailing tagSeparatorChar
return dst[:len(dst)-1]
}
func marshalTagValue(dst, src []byte) []byte {
n1 := bytes.IndexByte(src, escapeChar)
n2 := bytes.IndexByte(src, tagSeparatorChar)
@@ -586,13 +592,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

@@ -36,6 +36,9 @@ type part struct {
// Empty for in-memory part.
path string
// Total size in bytes of part data.
size uint64
timestampsFile fs.ReadAtCloser
valuesFile fs.ReadAtCloser
indexFile fs.ReadAtCloser
@@ -59,6 +62,7 @@ func openFilePart(path string) (*part, error) {
if err != nil {
return nil, fmt.Errorf("cannot open timestamps file: %s", err)
}
timestampsSize := fs.MustFileSize(timestampsPath)
valuesPath := path + "/values.bin"
valuesFile, err := fs.OpenReaderAt(valuesPath)
@@ -66,6 +70,7 @@ func openFilePart(path string) (*part, error) {
timestampsFile.MustClose()
return nil, fmt.Errorf("cannot open values file: %s", err)
}
valuesSize := fs.MustFileSize(valuesPath)
indexPath := path + "/index.bin"
indexFile, err := fs.OpenReaderAt(indexPath)
@@ -74,6 +79,7 @@ func openFilePart(path string) (*part, error) {
valuesFile.MustClose()
return nil, fmt.Errorf("cannot open index file: %s", err)
}
indexSize := fs.MustFileSize(indexPath)
metaindexPath := path + "/metaindex.bin"
metaindexFile, err := filestream.Open(metaindexPath, true)
@@ -83,15 +89,17 @@ func openFilePart(path string) (*part, error) {
indexFile.MustClose()
return nil, fmt.Errorf("cannot open metaindex file: %s", err)
}
metaindexSize := fs.MustFileSize(metaindexPath)
return newPart(&ph, path, metaindexFile, timestampsFile, valuesFile, indexFile)
size := timestampsSize + valuesSize + indexSize + metaindexSize
return newPart(&ph, path, size, metaindexFile, timestampsFile, valuesFile, indexFile)
}
// newPart returns new part initialized with the given arguments.
//
// The returned part calls MustClose on all the files passed to newPart
// when calling part.MustClose.
func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser, timestampsFile, valuesFile, indexFile fs.ReadAtCloser) (*part, error) {
func newPart(ph *partHeader, path string, size uint64, metaindexReader filestream.ReadCloser, timestampsFile, valuesFile, indexFile fs.ReadAtCloser) (*part, error) {
var errors []error
metaindex, err := unmarshalMetaindexRows(nil, metaindexReader)
if err != nil {
@@ -102,6 +110,7 @@ func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser,
p := &part{
ph: *ph,
path: path,
size: size,
timestampsFile: timestampsFile,
valuesFile: valuesFile,
indexFile: indexFile,

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)
}
@@ -288,6 +282,9 @@ type partitionMetrics struct {
SmallIndexBlocksCacheRequests uint64
SmallIndexBlocksCacheMisses uint64
BigSizeBytes uint64
SmallSizeBytes uint64
BigRowsCount uint64
SmallRowsCount uint64
@@ -332,6 +329,7 @@ func (pt *partition) UpdateMetrics(m *partitionMetrics) {
m.BigIndexBlocksCacheMisses += p.ibCache.Misses()
m.BigRowsCount += p.ph.RowsCount
m.BigBlocksCount += p.ph.BlocksCount
m.BigSizeBytes += p.size
m.BigPartsRefCount += atomic.LoadUint64(&pw.refCount)
}
@@ -343,6 +341,7 @@ func (pt *partition) UpdateMetrics(m *partitionMetrics) {
m.SmallIndexBlocksCacheMisses += p.ibCache.Misses()
m.SmallRowsCount += p.ph.RowsCount
m.SmallBlocksCount += p.ph.BlocksCount
m.SmallSizeBytes += p.size
m.SmallPartsRefCount += atomic.LoadUint64(&pw.refCount)
}
@@ -599,7 +598,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
@@ -1019,12 +1018,14 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}) erro
}
var newPW *partWrapper
var newPSize uint64
if len(dstPartPath) > 0 {
// Open the merged part if it is non-empty.
newP, err := openFilePart(dstPartPath)
if err != nil {
return fmt.Errorf("cannot open merged part %q: %s", dstPartPath, err)
}
newPSize = newP.size
newPW = &partWrapper{
p: newP,
refCount: 1,
@@ -1063,7 +1064,7 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}) erro
d := time.Since(startTime)
if d > 10*time.Second {
logger.Infof("merged %d rows in %s at %d rows/sec to %q", outRowsCount, d, int(float64(outRowsCount)/d.Seconds()), dstPartPath)
logger.Infof("merged %d rows in %s at %d rows/sec to %q; sizeBytes: %d", outRowsCount, d, int(float64(outRowsCount)/d.Seconds()), dstPartPath, newPSize)
}
return nil
@@ -1223,13 +1224,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 +1339,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 +1405,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 +1433,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 +1480,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))
@@ -250,29 +272,31 @@ func (s *Storage) idb() *indexDB {
// Metrics contains essential metrics for the Storage.
type Metrics struct {
TSIDCacheSize uint64
TSIDCacheBytesSize uint64
TSIDCacheSizeBytes uint64
TSIDCacheRequests uint64
TSIDCacheMisses uint64
TSIDCacheCollisions uint64
MetricIDCacheSize uint64
MetricIDCacheBytesSize uint64
MetricIDCacheSizeBytes uint64
MetricIDCacheRequests uint64
MetricIDCacheMisses uint64
MetricIDCacheCollisions uint64
MetricNameCacheSize uint64
MetricNameCacheBytesSize uint64
MetricNameCacheSizeBytes uint64
MetricNameCacheRequests uint64
MetricNameCacheMisses uint64
MetricNameCacheCollisions uint64
DateMetricIDCacheSize uint64
DateMetricIDCacheBytesSize uint64
DateMetricIDCacheSizeBytes uint64
DateMetricIDCacheRequests uint64
DateMetricIDCacheMisses uint64
DateMetricIDCacheCollisions uint64
HourMetricIDCacheSize uint64
IndexDBMetrics IndexDBMetrics
TableMetrics TableMetrics
}
@@ -287,7 +311,7 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
var cs fastcache.Stats
s.tsidCache.UpdateStats(&cs)
m.TSIDCacheSize += cs.EntriesCount
m.TSIDCacheBytesSize += cs.BytesSize
m.TSIDCacheSizeBytes += cs.BytesSize
m.TSIDCacheRequests += cs.GetCalls
m.TSIDCacheMisses += cs.Misses
m.TSIDCacheCollisions += cs.Collisions
@@ -295,7 +319,7 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
cs.Reset()
s.metricIDCache.UpdateStats(&cs)
m.MetricIDCacheSize += cs.EntriesCount
m.MetricIDCacheBytesSize += cs.BytesSize
m.MetricIDCacheSizeBytes += cs.BytesSize
m.MetricIDCacheRequests += cs.GetCalls
m.MetricIDCacheMisses += cs.Misses
m.MetricIDCacheCollisions += cs.Collisions
@@ -303,7 +327,7 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
cs.Reset()
s.metricNameCache.UpdateStats(&cs)
m.MetricNameCacheSize += cs.EntriesCount
m.MetricNameCacheBytesSize += cs.BytesSize
m.MetricNameCacheSizeBytes += cs.BytesSize
m.MetricNameCacheRequests += cs.GetCalls
m.MetricNameCacheMisses += cs.Misses
m.MetricNameCacheCollisions += cs.Collisions
@@ -311,11 +335,19 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
cs.Reset()
s.dateMetricIDCache.UpdateStats(&cs)
m.DateMetricIDCacheSize += cs.EntriesCount
m.DateMetricIDCacheBytesSize += cs.BytesSize
m.DateMetricIDCacheSizeBytes += cs.BytesSize
m.DateMetricIDCacheRequests += cs.GetCalls
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,20 +444,91 @@ 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) mustLoadCache(info, name string, bytesSize int) *fastcache.Cache {
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; sizeBytes: %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; sizeBytes: %d", name, path, time.Since(startTime), len(hm.m), len(dst))
}
func (s *Storage) mustLoadCache(info, name string, sizeBytes int) *fastcache.Cache {
path := s.cachePath + "/" + name
logger.Infof("loading %s cache from %q...", info, path)
startTime := time.Now()
c := fastcache.LoadFromFileOrNew(path, bytesSize)
c := fastcache.LoadFromFileOrNew(path, sizeBytes)
var cs fastcache.Stats
c.UpdateStats(&cs)
logger.Infof("loaded %s cache from %q in %s; entriesCount: %d; bytesSize: %d",
logger.Infof("loaded %s cache from %q in %s; entriesCount: %d; sizeBytes: %d",
info, path, time.Since(startTime), cs.EntriesCount, cs.BytesSize)
return c
}
@@ -417,7 +544,7 @@ func (s *Storage) mustSaveCache(c *fastcache.Cache, info, name string) {
var cs fastcache.Stats
c.UpdateStats(&cs)
c.Reset()
logger.Infof("saved %s cache to %q in %s; entriesCount: %d; bytesSize: %d",
logger.Infof("saved %s cache to %q in %s; entriesCount: %d; sizeBytes: %d",
info, path, time.Since(startTime), cs.EntriesCount, cs.BytesSize)
}
@@ -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))
}
@@ -591,6 +752,7 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
}
rows = rows[:rowsLen+len(mrs)]
j := 0
minTimestamp, maxTimestamp := s.tb.getMinMaxTimestamps()
for i := range mrs {
mr := &mrs[i]
if math.IsNaN(mr.Value) {
@@ -598,6 +760,10 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
// doesn't know how to work with them.
continue
}
if mr.Timestamp < minTimestamp || mr.Timestamp > maxTimestamp {
// Skip rows with timestamps outside the retention.
continue
}
r := &rows[rowsLen+j]
j++
r.Timestamp = mr.Timestamp
@@ -656,13 +822,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 +841,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 +876,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 +935,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 +984,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) {
@@ -310,22 +310,14 @@ func (tb *table) AddRows(rows []rawRow) error {
// The slowest path - there are rows that don't fit any existing partition.
// Create new partitions for these rows.
// Do this under tb.ptwsLock.
now := timestampFromTime(time.Now())
minTimestamp := now - tb.retentionMilliseconds
maxTimestamp := now + 2*24*3600*1000 // allow max +2 days from now due to timezones shit :)
minTimestamp, maxTimestamp := tb.getMinMaxTimestamps()
tb.ptwsLock.Lock()
var errors []error
for i := range missingRows {
r := &missingRows[i]
if r.Timestamp < minTimestamp {
// Silently skip row with too small timestamp, since it should be deleted anyway.
continue
}
if r.Timestamp > maxTimestamp {
err := fmt.Errorf("cannot add row %+v with too big timestamp to table %q; the timestamp cannot be bigger than %d (+2 days from now)",
r, tb.path, maxTimestamp)
errors = append(errors, err)
if r.Timestamp < minTimestamp || r.Timestamp > maxTimestamp {
// Silently skip row outside retention, since it should be deleted anyway.
continue
}
@@ -359,6 +351,20 @@ func (tb *table) AddRows(rows []rawRow) error {
return nil
}
func (tb *table) getMinMaxTimestamps() (int64, int64) {
now := timestampFromTime(time.Now())
minTimestamp := now - tb.retentionMilliseconds
maxTimestamp := now + 2*24*3600*1000 // allow max +2 days from now due to timezones shit :)
if minTimestamp < 0 {
// Negative timestamps aren't supported by the storage.
minTimestamp = 0
}
if maxTimestamp < 0 {
maxTimestamp = (1 << 63) - 1
}
return minTimestamp, maxTimestamp
}
func (tb *table) startRetentionWatcher() {
tb.retentionWatcherWG.Add(1)
go func() {

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
@@ -161,15 +168,14 @@ func (tf *tagFilter) Init(commonPrefix, key, value []byte, isNegative, isRegexp
tf.isRegexp = false
}
}
tf.prefix = marshalTagValue(tf.prefix, prefix)
if tf.isRegexp {
// Remove the trailing tagSeparatorChar from the prefix.
tf.prefix = tf.prefix[:len(tf.prefix)-1]
}
if len(expr) == 0 {
tf.prefix = marshalTagValueNoTrailingTagSeparator(tf.prefix, prefix)
if !tf.isRegexp {
// tf contains plain value without regexp.
// Add empty orSuffix in order to trigger fast path for orSuffixes
// during the search for matching metricIDs.
tf.orSuffixes = append(tf.orSuffixes[:0], "")
return nil
}
rcv, err := getRegexpFromCache(expr)
if err != nil {
return err
@@ -180,16 +186,14 @@ func (tf *tagFilter) Init(commonPrefix, key, value []byte, isNegative, isRegexp
}
func (tf *tagFilter) matchSuffix(b []byte) (bool, error) {
if !tf.isRegexp {
return len(b) == 0, nil
}
// Remove the trailing tagSeparatorChar before applying the regexp.
// Remove the trailing tagSeparatorChar.
if len(b) == 0 || b[len(b)-1] != tagSeparatorChar {
return false, fmt.Errorf("unexpected end of b; want %d; b=%q", tagSeparatorChar, b)
}
b = b[:len(b)-1]
if !tf.isRegexp {
return len(b) == 0, nil
}
ok := tf.reSuffixMatch(b)
return ok, nil
}

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