Compare commits

...

261 Commits

Author SHA1 Message Date
Aliaksandr Valialkin
0b488f1e37 lib/storage: do not change timestamps to constant rate if values are constant or have constant delta
This breaks the original timestamps, which results in issues like
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/120 and
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/141 .
2019-08-06 15:40:07 +03:00
Aliaksandr Valialkin
b8bb74ffc6 app/vmstorage: add vm_concurrent_addrows_* metrics for tracking concurrency for Storage.AddRows calls
Track also the number of dropped rows due to the exceeded timeout
on concurrency limit for Storage.AddRows. This number is tracked in `vm_concurrent_addrows_dropped_rows_total`
2019-08-06 15:08:33 +03:00
Aliaksandr Valialkin
5c9e48417a vendor: update github.com/VictoriaMetrics/metrics to v1.7.1 2019-08-05 19:21:36 +03:00
Aliaksandr Valialkin
5c83f8e203 app: add vm_concurrent_ metrics for visibility in concurrency limiters for vminsert and vmselect 2019-08-05 18:30:57 +03:00
Aliaksandr Valialkin
05713469c3 vendor: make vendor-update 2019-08-05 10:33:21 +03:00
Aliaksandr Valialkin
8822079b77 lib/storage: properly reset partSearch.fetchData in partSearch.reset 2019-08-05 09:56:06 +03:00
Aliaksandr Valialkin
99e048c9df app/vmselect: allow passing match[], start and time to /api/v1/label/<label_name>/values
`/api/v1/label/<label_name>/values?match[]=q` emulates emulates `label_values(q, <label_name>)`
call in Grafana templating.
2019-08-04 23:09:21 +03:00
Aliaksandr Valialkin
47e4b50112 app/vmselect: optimize /api/v1/series by skipping storage data
Fetch and process only time series metainfo.
2019-08-04 23:01:28 +03:00
Aliaksandr Valialkin
241170dc05 app/vmselect/prometheus: prevent from fetching and scanning all the data on /api/v1/searies call by default 2019-08-04 19:42:36 +03:00
Aliaksandr Valialkin
1c69f4eadc app/vmselect/promql: tune automatic window adjustement
Increase the windows adjustement for small scrape intervals,
since they usually have higher jitter.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/139
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/134
2019-08-04 19:34:05 +03:00
Aliaksandr Valialkin
8d93b15b86 app/vmselect/promql: further increase the allowed jitter for scrape interval
Real-world production data shows higher jitter than 1/8 of scrape interval.
This may results in gaps on the graph. So increase the allowed jitter to 1/4
of scrape interval in order to reduce the probability of gaps on the graphs
over time series with high jitter for scrape_interval.
2019-08-02 20:10:23 +03:00
Aliaksandr Valialkin
fcc166622a README.md: mention that monitoring is recommended for VictoriaMetrics 2019-08-02 15:27:10 +03:00
Aliaksandr Valialkin
a9f39168d2 app/vminsert/influx: round automatically generated timestamp according to the given precision arg 2019-08-02 00:24:06 +03:00
Aliaksandr Valialkin
f090b2e917 app/vmselect/promql: tolerate higher jitter in scrape interval
Allow jitter for up to 1/8 instead of 1/16 for the scrape interval.
This should imrpove graphs when `step` is smaller than the `scrape_interval`.
2019-08-01 23:26:00 +03:00
Aliaksandr Valialkin
10caad4728 lib/decimal: modernize tests a bit 2019-07-31 21:10:03 +03:00
Aliaksandr Valialkin
3b90c2a99a Add CODE_OF_CONDUCT.md 2019-07-31 15:44:26 +03:00
Aliaksandr Valialkin
57ec4f5f92 Update issue templates
Add a template for feature request
2019-07-31 15:41:57 +03:00
Aliaksandr Valialkin
01cb15b6f5 Update issue templates
Add a template for bug report.
2019-07-31 15:39:41 +03:00
Aliaksandr Valialkin
b9256511e8 README.md: add join slack badge 2019-07-31 15:27:11 +03:00
Aliaksandr Valialkin
3a38b23fa3 app/vmselect/promql: add vm_slow_queries_total metric for counting slow queries
The query is slow if its execution time exceeds `-search.logSlowQueryDuration`
2019-07-31 03:36:37 +03:00
Aliaksandr Valialkin
8bd6f1f6df app/vmselect/promql: return NaN from histogram_quantile if at least a single bucket is broken 2019-07-31 01:18:07 +03:00
Aliaksandr Valialkin
4aaa5c2efc app/vmselect/promql: allow adjusting window for default rollup function
Default rollup function is `last_over_time`. It must support adjusting
the provided window in order to prevent from gaps on the graph
for window values smaller than scrape interval.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/134
2019-07-31 00:45:54 +03:00
Aliaksandr Valialkin
10f5a26bec app/vmselect/promql: return NaN values if invalid bucket counts are passed to histogram_quantile
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/136
2019-07-30 22:05:10 +03:00
Aliaksandr Valialkin
c14fd6c43f lib/storage: typo fixes after a77e88db7d 2019-07-30 15:38:52 +03:00
Aliaksandr Valialkin
a77e88db7d lib/storage: fix matching against tag filter with empty name
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/137
2019-07-30 15:15:09 +03:00
Aliaksandr Valialkin
aad7236e5d README.md: formatting fixes 2019-07-28 22:02:42 +03:00
Artem Navoiev
5e5de6be9a Create CONTRIBUTING.md 2019-07-28 20:42:32 +03:00
Anton Patsev
90cf6f3fcb change /usr/bin/victoriametrics to /usr/bin/victoria-metrics-prod (#132) 2019-07-28 20:40:46 +03:00
Artem Navoiev
8e3d69219f Add roadmap (#130)
* Add roadmap
* Fix typos
2019-07-28 18:39:39 +01:00
Aliaksandr Valialkin
b842a2eccc README.md: mention that VictoriaMetrics needs free disk space for background merges 2019-07-28 12:26:16 +03:00
Aliaksandr Valialkin
afcc7fb167 app/vmselect/netstorage: improve error message when reading data blocks from storage
Mention the block number in the error. This should simplify troubleshooting in this code.
2019-07-28 12:12:35 +03:00
Aliaksandr Valialkin
57a57c711a package: changed the remaining /usr/local/bin to /usr/bin
This is a follo-up after 68f260d878
2019-07-28 11:08:07 +03:00
Anton Patsev
68f260d878 change /usr/local/bin to /usr/bin (#131) 2019-07-28 11:06:24 +03:00
Aliaksandr Valialkin
1eade9b358 app/vminsert: add vm_rows_per_insert summary metric
This metric should help tuning batch sizes on clients writing data to VictoriaMetrics
2019-07-27 13:21:46 +03:00
Aliaksandr Valialkin
7e8747f6ed README.md: add a section for production ARM build 2019-07-26 22:34:31 +03:00
Aliaksandr Valialkin
0168a1b658 package: various fixes
- Use `-prod` binaries instead of development binaries for both deb and rpm packages.
- Fix binary directory from /usr/sbin to /usr/local/bin as outlined in package/victoria-metrics.service
- Fix binary name from `victoriametrics` to `victoria-metrics-prod` in package/victoria-metrics.service
2019-07-26 22:31:04 +03:00
Aliaksandr Valialkin
bf6cbb762c app/vminsert: improve error messages for Influx, OpenTSDB and Graphite parsing
Include in the error message the line which failed to parse.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/127
2019-07-26 22:08:52 +03:00
Kostya Vasilyev
6aeac37fc5 pick up .service file from ./rpm (#126)
* pick up .service file from ./rpm

* feedback from @patsevanton

* remove 'start' from ExecStart command
2019-07-26 21:56:30 +03:00
Aliaksandr Valialkin
c98725db55 app/vmstorage: consistency renaming for ignored rows metrics
vm_too_big_timestamp_rows_total -> vm_rows_ignored_total{reason="big_timestamp"}
  vm_too_small_timestamp_rows_total -> vm_rows_ignored_total{reason="small_timestamp"}
2019-07-26 20:02:06 +03:00
Anton Patsev
d8043f7161 Change default value storageDataPath (#125)
Fixes #124 .
2019-07-26 14:13:55 +03:00
Aliaksandr Valialkin
f586e1f83c lib/storage: add metrics for calculating skipped rows outside the retention
The metrics are:

    - vm_too_big_timestamp_rows_total
    - vm_too_small_timestamp_rows_total
2019-07-26 14:11:01 +03:00
Kostya Vasilyev
d1132bb188 deb packaging fixes: 1) stop the service in prerm 2) reload services in postrm (#123) 2019-07-26 12:38:59 +03:00
Aliaksandr Valialkin
915fb6df79 README.md: mention that arm builds can run on Raspberry Pi 2019-07-26 12:28:40 +03:00
Kostya Vasilyev
89eb6d78a4 RPM packaging (#122) 2019-07-25 23:47:41 +03:00
Aliaksandr Valialkin
17096b5750 app/vmselect/promql: return NaN from count() over zero time series
This aligns `count` behavior with Prometheus.
2019-07-25 22:02:30 +03:00
Aliaksandr Valialkin
66efa5745f app/vmselect/promql: properly calculate incremental aggregations grouped by __name__
Previously the following query may fail on multiple distinct metric names match:

    sum(count_over_time{__name__!=''}) by (__name__)
2019-07-25 21:53:20 +03:00
Anton Patsev
106ab78a47 Add package/rpm/ (#121) 2019-07-25 11:21:55 +03:00
Aliaksandr Valialkin
8aa474d685 README.md: move how to build VictoriaMetrics section to the bottom
This streamlines `getting started` experience
2019-07-25 11:17:30 +03:00
Aliaksandr Valialkin
9e059bb330 README.md: add links to ARM build and Pure Go build in TOC 2019-07-25 11:05:35 +03:00
Aliaksandr Valialkin
2346335ea6 README.md: moved advanced topics to the bottom, so they don't clutter getting started workflow 2019-07-25 11:00:41 +03:00
Aliaksandr Valialkin
b339890dca lib/encoding/zstd: go fmt 2019-07-25 01:37:16 +03:00
Aliaksandr Valialkin
6c4ca89d75 lib/encoding/zstd: disable CRC checks in pure Go build
This should give slightly better compression and decompressions performance.
Additionally this shaves off 4 bytes per each compressed block.
2019-07-24 19:17:16 +03:00
Roman Khavronenko
f0fe7b5ad6 fix typo (#117) 2019-07-24 07:48:28 +01:00
Aliaksandr Valialkin
22ed4e7fd4 vendor: make vendor-update 2019-07-23 20:00:19 +03:00
Aliaksandr Valialkin
162f1fb1b7 all: small updates after PR #114 2019-07-23 19:54:50 +03:00
Aliaksandr Valialkin
d07f616609 lib/encoding: small fixes in tests after the PR #114 2019-07-23 19:37:51 +03:00
Roman Khavronenko
5bf4e5ffb5 all: add Pure Go build (pull request #114)
Updates #94
2019-07-23 19:26:39 +03:00
Kostya Vasilyev
8c3629a892 Debian packaging (#116)
* initial commit of deb packaging

* Incorporated feedback from @valyala:
- Put data directory under /var/lib
- More beef in systemd file
- Packaging for arm64
- Package all target which builds and packages both amd64 and arm64

* Remove PIDFile from systemd unit, useless

* per PR feedback, move debian specific files into deb subdirectory

Updates #107 .
2019-07-22 17:12:48 +03:00
Aliaksandr Valialkin
ea07cf68ba README.md: add querying Graphite data section
Mention that Graphite data may be read either via Prometheus querying API
or via go-graphite/carbonapi. See https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml
2019-07-21 16:10:19 +03:00
Roman Khavronenko
4ee41bab43 add versioning to dashboard description (#113) 2019-07-21 14:34:50 +03:00
Roman Khavronenko
1273f31f19 Add CPU usage panel; rename Go runtime to Resource usage (#112)
* add CPU usage panel; rename `Go runtime` to `Resource usage`

* rm irate from CPU usage panel

Updates #92 .
2019-07-20 17:24:24 +03:00
Aliaksandr Valialkin
0f2ecde0e6 lib/encoding: improve gauge series detection
- Series with negative values are always gauges
- Counters may only have increasing values with possible counter resets

This should improve compression ratio for gauge series which
were previously mistakenly detected as counters.
2019-07-20 14:05:09 +03:00
Aliaksandr Valialkin
6cd77d4847 deployment: switch builder from go1.12.6 to go1.12.7 2019-07-20 12:15:05 +03:00
Roman Khavronenko
fb14f23532 mention docker-compose as option to spin up VM (#97) 2019-07-16 00:45:21 +03:00
Aliaksandr Valialkin
daba0cdb05 lib/netutil: do not count timeouts as network errors 2019-07-15 23:05:35 +03:00
Aliaksandr Valialkin
575d2f0a91 app/vminsert: use netutil.TCPListener for collecting network-related metrics for Graphite and OpenTSDB TCP traffic 2019-07-15 22:58:00 +03:00
Aliaksandr Valialkin
ec1b439329 README.md: expand capacity planning section a bit 2019-07-12 21:19:27 +03:00
Aliaksandr Valialkin
6a943a6a58 app/vmselect/promql: remove empty time series after applying filters like q > 0
This should reduce CPU and RAM usage for queries over high number of time series.
2019-07-12 19:59:27 +03:00
Aliaksandr Valialkin
998525999c vendor: update github.com/VictoriaMetrics/metrics to v1.7.0
This version adds support for `process_*` metrics similar
to metrics exposed by https://github.com/prometheus/client_golang .

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/92
2019-07-12 17:22:53 +03:00
Aliaksandr Valialkin
ab88890523 app/vmselect/promql: parallelize incremental aggregation to multiple CPU cores
This may reduce response times for aggregation over big number of time series
with small step between output data points.
2019-07-12 15:52:22 +03:00
Aliaksandr Valialkin
374d681848 README.md: clarify that Prometheus replicates data to remote storage 2019-07-12 02:51:04 +03:00
Aliaksandr Valialkin
e75d5f47c4 lib/storage: remove unused function isTooBigTimeRangeForDateMetricIDs 2019-07-12 02:28:23 +03:00
Aliaksandr Valialkin
fc90ebf43c lib/storage: do not reduce maxMetrics on time ranges exceeding maxDaysForDateMetricIDs
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/95
2019-07-12 02:20:34 +03:00
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
333 changed files with 32718 additions and 4024 deletions

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version**
The line returned when passing `--version` command line flag to binary. For example:
```
$ ./victoria-metrics-prod --version
victoria-metrics-20190730-121249-heads-single-node-0-g671d9e55
```
**Additional context**
Add any other context about the problem here such as error logs, `/metrics` output, screenshots from [the official Grafana dashboard for VictoriaMetrics](https://grafana.com/dashboards/10229).

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

4
.gitignore vendored
View File

@@ -9,3 +9,7 @@
/victoria-metrics-data
/vmstorage-data
/vmselect-cache
/package/temp-deb-*
/package/temp-rpm-*
/package/*.deb
/package/*.rpm

26
.travis.yml Normal file
View File

@@ -0,0 +1,26 @@
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 test-pure
- make victoria-metrics
- make victoria-metrics-pure
- make victoria-metrics-arm
- make victoria-metrics-arm64
after_success:
- bash <(curl -s https://codecov.io/bash)

76
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at info@victoriametrics.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

16
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,16 @@
If you like VictoriaMetrics and want to contribute, 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):
- Prefer simple code and architecture.
- Avoid complex abstractions.
- Avoid magic code and fancy algorithms.
- Avoid [big external dependencies](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d).
- Minimize the number of moving parts in the distributed system.
- Avoid automated decisions, which may hurt cluster availability, consistency or performance.
Adhering `KISS` principle simplifies the resulting code and architecture, so it can be reviewed, understood and verified by many people.

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,40 @@ 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 -tags=integration -mod=vendor ./lib/... ./app/...
test-pure:
GO111MODULE=on CGO_ENABLED=0 go test -tags=integration -mod=vendor ./lib/... ./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/...
benchmark-pure:
GO111MODULE=on CGO_ENABLED=0 go test -mod=vendor -bench=. ./lib/...
GO111MODULE=on CGO_ENABLED=0 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

474
README.md
View File

@@ -1,12 +1,17 @@
<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)
[![Slack](https://img.shields.io/badge/join%20slack-%23victoriametrics-brightgreen.svg)](http://slack.victoriametrics.com/)
[![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 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,15 +21,17 @@ 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).
* [Uses 10x less RAM than InfluxDB](https://medium.com/@valyala/insert-benchmarks-with-inch-influxdb-vs-victoriametrics-e31a41ae2893) when working with millions of unique time series (aka high cardinality).
* High data compression, so [up to 70x more data points](https://medium.com/@valyala/when-size-matters-benchmarking-victoriametrics-vs-timescale-and-influxdb-6035811952d4)
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).
may be crammed into 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 +44,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,72 +53,67 @@ 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
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
from sources. Building from sources is reasonable when developing an additional features specific
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.
#### Production build
1. [Install docker](https://docs.docker.com/install/).
2. Run `make victoria-metrics-prod` from the root folder of the respository.
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.
`<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`.
- [How to start VictoriaMetrics](#how-to-start-victoriametrics)
- [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)
- [Querying Graphite data](#querying-graphite-data)
- [How to send data from OpenTSDB-compatible agents?](#how-to-send-data-from-opentsdb-compatible-agents)
- [How to build from sources](#how-to-build-from-sources)
- [Development build](#development-build)
- [Production build](#production-build)
- [ARM build](#arm-build)
- [Pure Go build (CGO_ENABLED=0)](#pure-go-build-cgo_enabled0)
- [Building docker images](#building-docker-images)
- [Start with docker-compose](#start-with-docker-compose)
- [Setting up service](#setting-up-service)
- [Third-party contributions](#third-party-contributions)
- [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)
- [Roadmap](#roadmap)
- [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 start VictoriaMetrics
Just start VictoriaMetrics executable or docker image with the desired command-line flags.
Just start VictoriaMetrics [executable](https://github.com/VictoriaMetrics/VictoriaMetrics/releases)
or [docker image](https://hub.docker.com/r/victoriametrics/victoria-metrics/) with the desired command-line flags.
The following command line flags are used the most:
The following command-line flags are used the most:
* `-storageDataPath` - path to data directory. VictoriaMetrics stores all the data in this directory.
* `-retentionPeriod` - retention period in months for the data. Older data is automatically deleted.
* `-httpListenAddr` - TCP address to listen to for http requests. By default it listens port `8428` on all the network interfaces.
* `-graphiteListenAddr` - TCP and UDP address to listen to for Graphite data. By default it is disabled.
* `-opentsdbListenAddr` - TCP and UDP address to listen to for OpenTSDB data. By default it is disabled.
* `-httpListenAddr` - TCP address to listen to for http requests. By default, it listens port `8428` on all the network interfaces.
* `-graphiteListenAddr` - TCP and UDP address to listen to for Graphite data. By default, it is disabled.
* `-opentsdbListenAddr` - TCP and UDP address to listen to for OpenTSDB data. By default, it is disabled.
Pass `-help` to see all the available flags with description and default values.
It is recommended setting up [monitoring](#monitoring) for VictoriaMetrics.
### Prometheus setup
@@ -122,6 +124,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.
@@ -131,11 +134,11 @@ Then apply the new config via the following command:
kill -HUP `pidof prometheus`
```
Prometheus writes incoming data to local storage and to remote storage in parallel.
Prometheus writes incoming data to local storage and replicates it to remote storage in parallel.
This means the data remains available in local storage for `--storage.tsdb.retention.time` duration
if remote storage stops working.
even if remote storage is unavailable.
If you plan sending data to VictoriaMetrics from multiple Prometheus instances, then add the following lines into `global` section
If you plan to send data to VictoriaMetrics from multiple Prometheus instances, then add the following lines into `global` section
of [Prometheus config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file):
```yml
@@ -146,7 +149,11 @@ global:
This instructs Prometheus to add `datacenter=dc-123` label to each time series sent to remote storage.
The label name may be arbitrary - `datacenter` is just an example. The label value must be unique
across Prometheus instances, so time series may be filtered and grouped by this label.
across Prometheus instances, so those 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
@@ -163,6 +170,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 the 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 +205,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'
```
An 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 +253,148 @@ 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 the timestamp is omitted.
An 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]}
```
### Querying Graphite data
Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read either via
[Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/)
or via [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml).
### 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.
An 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 build from sources
We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) or
[docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/) instead of building VictoriaMetrics
from sources. Building from sources is reasonable when developing additional features specific
to your needs.
#### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.12.
2. Run `make victoria-metrics` from the root folder of the repository.
It builds `victoria-metrics` binary and puts 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 repository.
It builds `victoria-metrics-prod` binary and puts it into the `bin` folder.
#### ARM build
ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://blog.cloudflare.com/arm-takes-wing/).
#### Development ARM build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.12.
2. Run `make victoria-metrics-arm` or `make victoria-metrics-arm64` from the root folder of the repository.
It builds `victoria-metrics-arm` or `victoria-metrics-arm64` binary respectively and puts it into the `bin` folder.
#### Production ARM build
1. [Install docker](https://docs.docker.com/install/).
2. Run `make victoria-metrics-arm-prod` or `make victoria-metrics-arm64-prod` from the root folder of the repository.
It builds `victoria-metrics-arm-prod` or `victoria-metrics-arm64-prod` binary respectively and puts it into the `bin` folder.
#### Pure Go build (CGO_ENABLED=0)
`Pure Go` mode builds only Go code without [cgo](https://golang.org/cmd/cgo/) dependencies.
This is an experimental mode, which may result in a lower compression ratio and slower decompression performance.
Use it with caution!
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.12.
2. Run `make victoria-metrics-pure` from the root folder of the repository.
It builds `victoria-metrics-pure` binary and puts it into the `bin` folder.
#### Building docker images
Run `make package-victoria-metrics`. It builds `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-victoria-metrics`.
### Start with docker-compose
[Docker-compose](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/docker-compose.yml)
helps to spin up VictoriaMetrics, Prometheus and Grafana with one command.
More details may be found [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#folder-contains-basic-images-and-tools-for-building-and-running-victoria-metrics-in-docker).
### 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))
### How to work with snapshots?
VictoriaMetrics can 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 +403,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 +413,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?
@@ -267,31 +450,55 @@ VictoriaMetrics exports [Prometheus-compatible federation data](https://promethe
at `http://<victoriametrics-addr>:8428/federate?match[]=<timeseries_selector_for_federation>`.
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.
`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. The 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`.
### Capacity planning
Rough estimation of the required resources:
A rough estimation of the required resources for ingestion path:
* 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
* Storage space: 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.
* Network usage: outbound traffic is negligible. Ingress traffic is ~100 bytes per ingested data point via
[Prometheus remote_write API](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write).
The actual ingress bandwidth usage depends on the average number of labels per ingested metric and the average size
of label values. The higher number of per-metric labels and longer label values mean the higher ingress bandwidth.
The required resources for query path:
* RAM size: depends on the number of time series to scan in each query and the `step`
argument passed to [/api/v1/query_range](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries).
The higher number of scanned time series and lower `step` argument results in the higher RAM usage.
* CPU cores: a CPU core per 30 millions of scanned data points per second.
* Network usage: depends on the frequency and the type of incoming requests. Typical Grafana dashboards usually
require negligible network bandwidth.
### 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 +523,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,20 +536,43 @@ 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 to implement 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.
This means that a single-node VictoriaMetrics may scale vertically and substitute a moderately sized cluster built with competing solutions
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.
Do not forget protecting sensitive endpoints in VictoriaMetrics when exposing it to untrusted networks such as the internet.
Consider setting the following command-line flags:
* `-tls`, `-tlsCertFile` and `-tlsKeyFile` for switching from HTTP to HTTPS.
@@ -353,10 +587,10 @@ For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<i
### Tuning
* There is no need in VictoriaMetrics tuning, since it uses reasonable defaults for command-line flags,
* There is no need in VictoriaMetrics tuning since it uses reasonable defaults for command-line flags,
which are automatically adjusted for the available CPU and RAM resources.
* There is no need in Operating System tuning, since VictoriaMetrics is optimized for default OS settings.
The only option is increasing the limit on [the number open files in the OS](https://medium.com/@muhammadtriwibowo/set-permanently-ulimit-n-open-files-in-ubuntu-4d61064429a),
* There is no need in Operating System tuning since VictoriaMetrics is optimized for default OS settings.
The only option is increasing the limit on [the number of open files in the OS](https://medium.com/@muhammadtriwibowo/set-permanently-ulimit-n-open-files-in-ubuntu-4d61064429a),
so Prometheus instances could establish more connections to VictoriaMetrics.
@@ -366,6 +600,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 +625,47 @@ 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.
* VictoriaMetrics requires free disk space for [merging data files to bigger ones](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282).
It may slow down when there is no enough free space left. So make sure `-storageDataPath` directory
has at least 20% of free space comparing to disk size.
* 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.
## Roadmap
- [ ] Replication [#118](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/118)
- [ ] Support of Object Storages (GCS, S3, Azure Storage) [#38](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/38)
- [ ] Data downsampling [#36](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/36)
- [ ] Alert Manager Integration [#119](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/119)
- [ ] CLI tool for data migration, re-balancing and adding/removing nodes [#103](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/103)
The discussion happens [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/129). Feel free to comment any item or add own one.
## 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 to contribute, 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,58 @@ 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
CGO_ENABLED=0 GOOS=linux GOARCH=arm GO111MODULE=on go build -mod=vendor -ldflags "$(GO_BUILDINFO)" -o bin/victoria-metrics-arm ./app/victoria-metrics
victoria-metrics-arm-prod:
APP_NAME=victoria-metrics APP_SUFFIX='-arm' DOCKER_OPTS='--env CGO_ENABLED=0 --env GOARCH=arm' $(MAKE) app-via-docker
victoria-metrics-arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go build -mod=vendor -ldflags "$(GO_BUILDINFO)" -o bin/victoria-metrics-arm64 ./app/victoria-metrics
victoria-metrics-arm64-prod:
APP_NAME=victoria-metrics APP_SUFFIX='-arm64' DOCKER_OPTS='--env CGO_ENABLED=0 --env GOARCH=arm64' $(MAKE) app-via-docker
victoria-metrics-pure:
GO111MODULE=on CGO_ENABLED=0 go build -mod=vendor -ldflags "$(GO_BUILDINFO)" -o bin/victoria-metrics-pure ./app/victoria-metrics
victoria-metrics-pure-prod:
APP_NAME=victoria-metrics APP_SUFFIX='-pure' DOCKER_OPTS='--env CGO_ENABLED=0' $(MAKE) app-via-docker
### Packaging as DEB - amd64
victoria-metrics-package-deb: victoria-metrics-prod
./package/package_deb.sh amd64
### Packaging as DEB - arm64
victoria-metrics-package-deb-arm64: victoria-metrics-arm64-prod
./package/package_deb.sh arm64
### Packaging as DEB - all
victoria-metrics-package-deb-all: \
victoria-metrics-package-deb \
victoria-metrics-package-deb-arm64
### Packaging as RPM - amd64
victoria-metrics-package-rpm: victoria-metrics-prod
./package/package_rpm.sh amd64
### Packaging as RPM - arm64
victoria-metrics-package-rpm-arm64: victoria-metrics-arm64-prod
./package/package_rpm.sh arm64
### Packaging as RPM - all
victoria-metrics-package-rpm-all: \
victoria-metrics-package-rpm \
victoria-metrics-package-rpm-arm64
### Packaging as both DEB and RPM - all
victoria-metrics-package-deb-rpm-all: \
victoria-metrics-package-deb \
victoria-metrics-package-deb-arm64 \
victoria-metrics-package-rpm \
victoria-metrics-package-rpm-arm64

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,70 @@
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)
select {
case ch <- struct{}{}:
t.Stop()
err := f()
<-ch
return err
default:
}
// All the workers are busy.
// Sleep for up to waitDuration.
concurrencyLimitReached.Inc()
t := timerpool.Get(waitDuration)
select {
case ch <- struct{}{}:
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)
concurrencyLimitTimeout.Inc()
return fmt.Errorf("the server is overloaded with %d concurrent inserts; either increase -maxConcurrentInserts or reduce the load", cap(ch))
}
}
var (
concurrencyLimitReached = metrics.NewCounter(`vm_concurrent_insert_limit_reached_total`)
concurrencyLimitTimeout = metrics.NewCounter(`vm_concurrent_insert_limit_timeout_total`)
_ = metrics.NewGauge(`vm_concurrent_insert_capacity`, func() float64 {
return float64(cap(ch))
})
_ = metrics.NewGauge(`vm_concurrent_insert_current`, func() float64 {
return float64(len(ch))
})
)

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:])
@@ -112,6 +114,7 @@ func unmarshalRows(dst []Row, s string, tagsPool []Tag) ([]Row, []Tag, error) {
var err error
tagsPool, err = r.unmarshal(s, tagsPool)
if err != nil {
err = fmt.Errorf("cannot unmarshal Graphite line %q: %s", s, err)
return dst, tagsPool, err
}
return dst, tagsPool, nil
@@ -119,6 +122,7 @@ func unmarshalRows(dst []Row, s string, tagsPool []Tag) ([]Row, []Tag, error) {
var err error
tagsPool, err = r.unmarshal(s[:n], tagsPool)
if err != nil {
err = fmt.Errorf("cannot unmarshal Graphite line %q: %s", s[:n], err)
return dst, tagsPool, err
}
s = s[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"
@@ -15,7 +14,10 @@ import (
"github.com/VictoriaMetrics/metrics"
)
var rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="graphite"}`)
var (
rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="graphite"}`)
rowsPerInsert = metrics.NewSummary(`vm_rows_per_insert{type="graphite"}`)
)
// insertHandler processes remote write for graphite plaintext protocol.
//
@@ -52,11 +54,10 @@ func (ctx *pushCtx) InsertRows() error {
ic.WriteDataPoint(nil, ic.Labels, r.Timestamp, r.Value)
}
rowsInserted.Add(len(rows))
rowsPerInsert.Update(float64(len(rows)))
return ic.FlushBufs()
}
const maxReadPacketSize = 4 * 1024 * 1024
const flushTimeout = 3 * time.Second
func (ctx *pushCtx) Read(r io.Reader) bool {
@@ -71,40 +72,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 +113,8 @@ type pushCtx struct {
Rows Rows
Common common.InsertCtx
reqBuf bytesutil.ByteBuffer
reqBuf []byte
tailBuf []byte
copyBuf [16 * 1024]byte
err error
}
@@ -129,7 +129,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

@@ -9,6 +9,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
"github.com/VictoriaMetrics/metrics"
)
@@ -23,7 +24,7 @@ var (
// Serve starts graphite server on the given addr.
func Serve(addr string) {
logger.Infof("starting TCP Graphite server at %q", addr)
lnTCP, err := net.Listen("tcp4", addr)
lnTCP, err := netutil.NewTCPListener("graphite", addr)
if err != nil {
logger.Fatalf("cannot start TCP Graphite server at %q: %s", addr, err)
}

View File

@@ -196,6 +196,7 @@ func unmarshalRows(dst []Row, s string, tagsPool []Tag, fieldsPool []Field) ([]R
var err error
tagsPool, fieldsPool, err = r.unmarshal(s, tagsPool, fieldsPool)
if err != nil {
err = fmt.Errorf("cannot unmarshal Influx line %q: %s", s, err)
return dst, tagsPool, fieldsPool, err
}
return dst, tagsPool, fieldsPool, nil
@@ -203,6 +204,7 @@ func unmarshalRows(dst []Row, s string, tagsPool []Tag, fieldsPool []Field) ([]R
var err error
tagsPool, fieldsPool, err = r.unmarshal(s[:n], tagsPool, fieldsPool)
if err != nil {
err = fmt.Errorf("cannot unmarshal Influx line %q: %s", s[:n], err)
return dst, tagsPool, fieldsPool, err
}
s = s[n+1:]
@@ -293,8 +295,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,7 +17,15 @@ import (
"github.com/VictoriaMetrics/metrics"
)
var rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="influx"}`)
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"}`)
rowsPerInsert = metrics.NewSummary(`vm_rows_per_insert{type="influx"}`)
)
// InsertHandler processes remote write for influx line protocol.
//
@@ -79,6 +87,7 @@ func (ctx *pushCtx) InsertRows(db string) error {
}
ic := &ctx.Common
ic.Reset(rowsLen)
rowsTotal := 0
for i := range rows {
r := &rows[i]
ic.Labels = ic.Labels[:0]
@@ -89,18 +98,25 @@ 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)
ic.WriteDataPoint(ctx.metricNameBuf, ic.Labels[:1], r.Timestamp, f.Value)
}
rowsInserted.Add(len(r.Fields))
rowsTotal += len(r.Fields)
}
rowsInserted.Add(rowsTotal)
rowsPerInsert.Update(float64(rowsTotal))
return ic.FlushBufs()
}
@@ -123,36 +139,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
}
@@ -169,6 +170,7 @@ func (ctx *pushCtx) Read(r io.Reader, tsMultiplier int64) bool {
}
} else if tsMultiplier < 0 {
tsMultiplier = -tsMultiplier
currentTs -= currentTs % tsMultiplier
for i := range ctx.Rows.Rows {
row := &ctx.Rows.Rows[i]
if row.Timestamp == 0 {
@@ -191,9 +193,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 +212,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

@@ -111,6 +111,7 @@ func unmarshalRows(dst []Row, s string, tagsPool []Tag) ([]Row, []Tag, error) {
var err error
tagsPool, err = r.unmarshal(s, tagsPool)
if err != nil {
err = fmt.Errorf("cannot unmarshal OpenTSDB line %q: %s", s, err)
return dst, tagsPool, err
}
return dst, tagsPool, nil
@@ -118,6 +119,7 @@ func unmarshalRows(dst []Row, s string, tagsPool []Tag) ([]Row, []Tag, error) {
var err error
tagsPool, err = r.unmarshal(s[:n], tagsPool)
if err != nil {
err = fmt.Errorf("cannot unmarshal OpenTSDB line %q: %s", s[:n], err)
return dst, tagsPool, err
}
s = s[n+1:]

View File

@@ -1,7 +1,6 @@
package opentsdb
import (
"bytes"
"fmt"
"io"
"net"
@@ -15,7 +14,10 @@ import (
"github.com/VictoriaMetrics/metrics"
)
var rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="opentsdb"}`)
var (
rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="opentsdb"}`)
rowsPerInsert = metrics.NewSummary(`vm_rows_per_insert{type="opentsdb"}`)
)
// insertHandler processes remote write for OpenTSDB put protocol.
//
@@ -52,11 +54,10 @@ func (ctx *pushCtx) InsertRows() error {
ic.WriteDataPoint(nil, ic.Labels, r.Timestamp, r.Value)
}
rowsInserted.Add(len(rows))
rowsPerInsert.Update(float64(len(rows)))
return ic.FlushBufs()
}
const maxReadPacketSize = 4 * 1024 * 1024
const flushTimeout = 3 * time.Second
func (ctx *pushCtx) Read(r io.Reader) bool {
@@ -71,33 +72,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 +102,8 @@ type pushCtx struct {
Rows Rows
Common common.InsertCtx
reqBuf bytesutil.ByteBuffer
reqBuf []byte
tailBuf []byte
copyBuf [16 * 1024]byte
err error
}
@@ -129,7 +118,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

@@ -9,6 +9,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
"github.com/VictoriaMetrics/metrics"
)
@@ -23,7 +24,7 @@ var (
// Serve starts OpenTSDB collector on the given addr.
func Serve(addr string) {
logger.Infof("starting TCP OpenTSDB collector at %q", addr)
lnTCP, err := net.Listen("tcp4", addr)
lnTCP, err := netutil.NewTCPListener("opentsdb", addr)
if err != nil {
logger.Fatalf("cannot start TCP OpenTSDB collector at %q: %s", addr, err)
}

View File

@@ -12,7 +12,10 @@ import (
"github.com/VictoriaMetrics/metrics"
)
var rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="prometheus"}`)
var (
rowsInserted = metrics.NewCounter(`vm_rows_inserted_total{type="prometheus"}`)
rowsPerInsert = metrics.NewSummary(`vm_rows_per_insert{type="prometheus"}`)
)
// InsertHandler processes remote write for prometheus.
func InsertHandler(r *http.Request, maxSize int64) error {
@@ -34,6 +37,7 @@ func insertHandlerInternal(r *http.Request, maxSize int64) error {
}
ic := &ctx.Common
ic.Reset(rowsLen)
rowsTotal := 0
for i := range timeseries {
ts := &timeseries[i]
var metricNameRaw []byte
@@ -41,8 +45,10 @@ func insertHandlerInternal(r *http.Request, maxSize int64) error {
r := &ts.Samples[i]
metricNameRaw = ic.WriteDataPointExt(metricNameRaw, ts.Labels, r.Timestamp, r.Value)
}
rowsInserted.Add(len(ts.Samples))
rowsTotal += len(ts.Samples)
}
rowsInserted.Add(rowsTotal)
rowsPerInsert.Update(float64(rowsTotal))
return ic.FlushBufs()
}

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"
)
@@ -29,28 +30,49 @@ func Init() {
fs.RemoveDirContents(tmpDirPath)
netstorage.InitTmpBlocksDir(tmpDirPath)
promql.InitRollupResultCache(*vmstorage.DataPath + "/cache/rollupResult")
concurrencyCh = make(chan struct{}, *maxConcurrentRequests)
}
var concurrencyCh chan struct{}
// Stop stops vmselect
func Stop() {
promql.StopRollupResultCache()
}
var concurrencyCh chan struct{}
var (
concurrencyLimitReached = metrics.NewCounter(`vm_concurrent_select_limit_reached_total`)
concurrencyLimitTimeout = metrics.NewCounter(`vm_concurrent_select_limit_timeout_total`)
_ = metrics.NewGauge(`vm_concurrent_select_capacity`, func() float64 {
return float64(cap(concurrencyCh))
})
_ = metrics.NewGauge(`vm_concurrent_select_current`, func() float64 {
return float64(len(concurrencyCh))
})
)
// 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)
select {
case concurrencyCh <- struct{}{}:
t.Stop()
defer func() { <-concurrencyCh }()
case <-t.C:
httpserver.Errorf(w, "cannot handle more than %d concurrent requests", cap(concurrencyCh))
return true
default:
// Sleep for a while until giving up. This should resolve short bursts in requests.
concurrencyLimitReached.Inc()
t := timerpool.Get(*maxQueueDuration)
select {
case concurrencyCh <- struct{}{}:
timerpool.Put(t)
defer func() { <-concurrencyCh }()
case <-t.C:
timerpool.Put(t)
concurrencyLimitTimeout.Inc()
httpserver.Errorf(w, "cannot handle more than %d concurrent requests", cap(concurrencyCh))
return true
}
}
path := strings.Replace(r.URL.Path, "//", "/", -1)
@@ -115,6 +137,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 +209,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

@@ -49,8 +49,9 @@ func (r *Result) reset() {
// Results holds results returned from ProcessSearchQuery.
type Results struct {
tr storage.TimeRange
deadline Deadline
tr storage.TimeRange
fetchData bool
deadline Deadline
tbf *tmpBlocksFile
@@ -71,9 +72,10 @@ func (rss *Results) Cancel() {
// RunParallel runs in parallel f for all the results from rss.
//
// f shouldn't hold references to rs after returning.
// workerID is the id of the worker goroutine that calls f.
//
// rss becomes unusable after the call to RunParallel.
func (rss *Results) RunParallel(f func(rs *Result)) error {
func (rss *Results) RunParallel(f func(rs *Result, workerID uint)) error {
defer func() {
putTmpBlocksFile(rss.tbf)
rss.tbf = nil
@@ -91,7 +93,7 @@ func (rss *Results) RunParallel(f func(rs *Result)) error {
// Start workers.
for i := 0; i < workersCount; i++ {
go func() {
go func(workerID uint) {
rs := getResult()
defer putResult(rs)
maxWorkersCount := gomaxprocs / workersCount
@@ -102,20 +104,20 @@ func (rss *Results) RunParallel(f func(rs *Result)) error {
err = fmt.Errorf("timeout exceeded during query execution: %s", rss.deadline.Timeout)
break
}
if err = pts.Unpack(rss.tbf, rs, rss.tr, maxWorkersCount); err != nil {
if err = pts.Unpack(rss.tbf, rs, rss.tr, rss.fetchData, maxWorkersCount); err != nil {
break
}
if len(rs.Timestamps) == 0 {
if len(rs.Timestamps) == 0 && rss.fetchData {
// Skip empty blocks.
continue
}
f(rs)
f(rs, workerID)
}
// Drain the remaining work
for range workCh {
}
doneCh <- err
}()
}(uint(i))
}
// Feed workers with work.
@@ -148,7 +150,7 @@ type packedTimeseries struct {
}
// Unpack unpacks pts to dst.
func (pts *packedTimeseries) Unpack(tbf *tmpBlocksFile, dst *Result, tr storage.TimeRange, maxWorkersCount int) error {
func (pts *packedTimeseries) Unpack(tbf *tmpBlocksFile, dst *Result, tr storage.TimeRange, fetchData bool, maxWorkersCount int) error {
dst.reset()
if err := dst.MetricName.Unmarshal(bytesutil.ToUnsafeBytes(pts.metricName)); err != nil {
@@ -175,7 +177,7 @@ func (pts *packedTimeseries) Unpack(tbf *tmpBlocksFile, dst *Result, tr storage.
var err error
for addr := range workCh {
sb := getSortBlock()
if err = sb.unpackFrom(tbf, addr, tr); err != nil {
if err = sb.unpackFrom(tbf, addr, tr, fetchData); err != nil {
break
}
@@ -294,10 +296,12 @@ func (sb *sortBlock) reset() {
sb.NextIdx = 0
}
func (sb *sortBlock) unpackFrom(tbf *tmpBlocksFile, addr tmpBlockAddr, tr storage.TimeRange) error {
func (sb *sortBlock) unpackFrom(tbf *tmpBlocksFile, addr tmpBlockAddr, tr storage.TimeRange, fetchData bool) error {
tbf.MustReadBlockAt(&sb.b, addr)
if err := sb.b.UnmarshalData(); err != nil {
return fmt.Errorf("cannot unmarshal block: %s", err)
if fetchData {
if err := sb.b.UnmarshalData(); err != nil {
return fmt.Errorf("cannot unmarshal block: %s", err)
}
}
timestamps := sb.b.Timestamps()
@@ -400,6 +404,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()
@@ -429,7 +463,7 @@ var ssPool sync.Pool
var missingMetricNamesForMetricID = metrics.NewCounter(`vm_missing_metric_names_for_metric_id_total`)
// ProcessSearchQuery performs sq on storage nodes until the given deadline.
func ProcessSearchQuery(sq *storage.SearchQuery, deadline Deadline) (*Results, error) {
func ProcessSearchQuery(sq *storage.SearchQuery, fetchData bool, deadline Deadline) (*Results, error) {
// Setup search.
tfss, err := setupTfss(sq.TagFilterss)
if err != nil {
@@ -445,35 +479,38 @@ func ProcessSearchQuery(sq *storage.SearchQuery, deadline Deadline) (*Results, e
sr := getStorageSearch()
defer putStorageSearch(sr)
sr.Init(vmstorage.Storage, tfss, tr, *maxMetricsPerSearch)
sr.Init(vmstorage.Storage, tfss, tr, fetchData, *maxMetricsPerSearch)
tbf := getTmpBlocksFile()
m := make(map[string][]tmpBlockAddr)
blocksRead := 0
for sr.NextMetricBlock() {
blocksRead++
addr, err := tbf.WriteBlock(sr.MetricBlock.Block)
if err != nil {
putTmpBlocksFile(tbf)
return nil, fmt.Errorf("cannot write data to temporary blocks file: %s", err)
return nil, fmt.Errorf("cannot write data block #%d to temporary blocks file: %s", blocksRead, err)
}
if time.Until(deadline.Deadline) < 0 {
putTmpBlocksFile(tbf)
return nil, fmt.Errorf("timeout exceeded while fetching data from storage: %s", deadline.Timeout)
return nil, fmt.Errorf("timeout exceeded while fetching data block #%d from storage: %s", blocksRead, deadline.Timeout)
}
metricName := sr.MetricBlock.MetricName
m[string(metricName)] = append(m[string(metricName)], addr)
}
if err := sr.Error(); err != nil {
putTmpBlocksFile(tbf)
return nil, fmt.Errorf("search error: %s", err)
return nil, fmt.Errorf("search error after reading %d data blocks: %s", blocksRead, err)
}
if err := tbf.Finalize(); err != nil {
putTmpBlocksFile(tbf)
return nil, fmt.Errorf("cannot finalize temporary blocks file: %s", err)
return nil, fmt.Errorf("cannot finalize temporary blocks file with %d blocks: %s", blocksRead, err)
}
var rss Results
rss.packedTimeseries = make([]packedTimeseries, len(m))
rss.tr = tr
rss.fetchData = fetchData
rss.deadline = deadline
rss.tbf = tbf
i := 0

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

@@ -6,12 +6,15 @@ import (
"math"
"net/http"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/metrics"
"github.com/valyala/quicktemplate"
@@ -37,9 +40,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
@@ -53,7 +68,7 @@ func FederateHandler(w http.ResponseWriter, r *http.Request) error {
MaxTimestamp: end,
TagFilterss: tagFilterss,
}
rss, err := netstorage.ProcessSearchQuery(sq, deadline)
rss, err := netstorage.ProcessSearchQuery(sq, true, deadline)
if err != nil {
return fmt.Errorf("cannot fetch data for %q: %s", sq, err)
}
@@ -61,7 +76,7 @@ func FederateHandler(w http.ResponseWriter, r *http.Request) error {
resultsCh := make(chan *quicktemplate.ByteBuffer)
doneCh := make(chan error)
go func() {
err := rss.RunParallel(func(rs *netstorage.Result) {
err := rss.RunParallel(func(rs *netstorage.Result, workerID uint) {
bb := quicktemplate.AcquireByteBuffer()
WriteFederate(bb, rs)
resultsCh <- bb
@@ -97,10 +112,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 {
@@ -136,7 +160,7 @@ func exportHandler(w http.ResponseWriter, matches []string, start, end int64, fo
MaxTimestamp: end,
TagFilterss: tagFilterss,
}
rss, err := netstorage.ProcessSearchQuery(sq, deadline)
rss, err := netstorage.ProcessSearchQuery(sq, true, deadline)
if err != nil {
return fmt.Errorf("cannot fetch data for %q: %s", sq, err)
}
@@ -144,7 +168,7 @@ func exportHandler(w http.ResponseWriter, matches []string, start, end int64, fo
resultsCh := make(chan *quicktemplate.ByteBuffer, runtime.GOMAXPROCS(-1))
doneCh := make(chan error)
go func() {
err := rss.RunParallel(func(rs *netstorage.Result) {
err := rss.RunParallel(func(rs *netstorage.Result, workerID uint) {
bb := quicktemplate.AcquireByteBuffer()
writeLineFunc(bb, rs)
resultsCh <- bb
@@ -156,6 +180,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 +204,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
@@ -201,9 +233,39 @@ var deleteDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/
func LabelValuesHandler(labelName string, w http.ResponseWriter, r *http.Request) error {
startTime := time.Now()
deadline := getDeadline(r)
labelValues, err := netstorage.GetLabelValues(labelName, deadline)
if err != nil {
return fmt.Errorf(`cannot obtain label values for %q: %s`, labelName, err)
if err := r.ParseForm(); err != nil {
return fmt.Errorf("cannot parse form values: %s", err)
}
var labelValues []string
if len(r.Form["match[]"]) == 0 && len(r.Form["start"]) == 0 && len(r.Form["end"]) == 0 {
var err error
labelValues, err = netstorage.GetLabelValues(labelName, deadline)
if err != nil {
return fmt.Errorf(`cannot obtain label values for %q: %s`, labelName, err)
}
} else {
// Extended functionality that allows filtering by label filters and time range
// i.e. /api/v1/label/foo/values?match[]=foobar{baz="abc"}&start=...&end=...
// is equivalent to `label_values(foobar{baz="abc"}, foo)` call on the selected
// time range in Grafana templating.
matches := r.Form["match[]"]
if len(matches) == 0 {
matches = []string{fmt.Sprintf("{%s!=''}", labelName)}
}
ct := currentTime()
end, err := getTime(r, "end", ct)
if err != nil {
return err
}
start, err := getTime(r, "start", end-defaultStep)
if err != nil {
return err
}
labelValues, err = labelValuesWithMatches(labelName, matches, start, end, deadline)
if err != nil {
return fmt.Errorf("cannot obtain label values for %q, match[]=%q, start=%d, end=%d: %s", labelName, matches, start, end, err)
}
}
w.Header().Set("Content-Type", "application/json")
@@ -212,8 +274,69 @@ func LabelValuesHandler(labelName string, w http.ResponseWriter, r *http.Request
return nil
}
func labelValuesWithMatches(labelName string, matches []string, start, end int64, deadline netstorage.Deadline) ([]string, error) {
if len(matches) == 0 {
logger.Panicf("BUG: matches must be non-empty")
}
tagFilterss, err := getTagFilterssFromMatches(matches)
if err != nil {
return nil, err
}
if start >= end {
start = end - defaultStep
}
sq := &storage.SearchQuery{
MinTimestamp: start,
MaxTimestamp: end,
TagFilterss: tagFilterss,
}
rss, err := netstorage.ProcessSearchQuery(sq, false, deadline)
if err != nil {
return nil, fmt.Errorf("cannot fetch data for %q: %s", sq, err)
}
m := make(map[string]struct{})
var mLock sync.Mutex
err = rss.RunParallel(func(rs *netstorage.Result, workerID uint) {
labelValue := rs.MetricName.GetTagValue(labelName)
if len(labelValue) == 0 {
return
}
mLock.Lock()
m[string(labelValue)] = struct{}{}
mLock.Unlock()
})
if err != nil {
return nil, fmt.Errorf("error when data fetching: %s", err)
}
labelValues := make([]string, 0, len(m))
for labelValue := range m {
labelValues = append(labelValues, labelValue)
}
sort.Strings(labelValues)
return labelValues, nil
}
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 +383,22 @@ 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")
}
end, err := getTime(r, "end", ct)
if err != nil {
return err
}
// Do not set start to minTimeMsecs by default as Prometheus does,
// since this leads to fetching and scanning all the data from the storage,
// which can take a lot of time for big storages.
// It is better setting start as end-defaultStep by default.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/91
start, err := getTime(r, "start", end-defaultStep)
if err != nil {
return err
}
deadline := getDeadline(r)
tagFilterss, err := getTagFilterssFromMatches(matches)
@@ -276,7 +413,7 @@ func SeriesHandler(w http.ResponseWriter, r *http.Request) error {
MaxTimestamp: end,
TagFilterss: tagFilterss,
}
rss, err := netstorage.ProcessSearchQuery(sq, deadline)
rss, err := netstorage.ProcessSearchQuery(sq, false, deadline)
if err != nil {
return fmt.Errorf("cannot fetch data for %q: %s", sq, err)
}
@@ -284,7 +421,7 @@ func SeriesHandler(w http.ResponseWriter, r *http.Request) error {
resultsCh := make(chan *quicktemplate.ByteBuffer)
doneCh := make(chan error)
go func() {
err := rss.RunParallel(func(rs *netstorage.Result) {
err := rss.RunParallel(func(rs *netstorage.Result, workerID uint) {
bb := quicktemplate.AcquireByteBuffer()
writemetricNameObject(bb, &rs.MetricName)
resultsCh <- bb
@@ -297,11 +434,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 +456,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 +508,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 +529,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 +566,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 +584,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 +603,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.
@@ -260,7 +312,11 @@ func aggrFuncCount(tss []*timeseries) []*timeseries {
}
count++
}
dst.Values[i] = float64(count)
v := float64(count)
if count == 0 {
v = nan
}
dst.Values[i] = v
}
return tss[:1]
}
@@ -301,6 +357,9 @@ func aggrFuncCountValues(afa *aggrFuncArg) ([]*timeseries, error) {
m := make(map[float64]bool)
for _, ts := range tss {
for _, v := range ts.Values {
if math.IsNaN(v) {
continue
}
m[v] = true
}
}
@@ -313,7 +372,7 @@ func aggrFuncCountValues(afa *aggrFuncArg) ([]*timeseries, error) {
var rvs []*timeseries
for _, v := range values {
var dst timeseries
dst.CopyFrom(tss[0])
dst.CopyFromShallowTimestamps(tss[0])
dst.MetricName.RemoveTag(dstLabel)
dst.MetricName.AddTag(dstLabel, strconv.FormatFloat(v, 'g', -1, 64))
for i := range dst.Values {
@@ -372,7 +431,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 +516,7 @@ func newAggrQuantileFunc(phis []float64) func(tss []*timeseries) []*timeseries {
idx := int(math.Round(float64(len(tss)-1) * phi))
dst.Values[n] = tss[idx].Values[n]
}
tss[0] = dst
return tss[:1]
}
}

View File

@@ -0,0 +1,450 @@
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,
mergeAggrFunc: mergeAggrSum,
finalizeAggrFunc: finalizeAggrCommon,
},
"min": {
updateAggrFunc: updateAggrMin,
mergeAggrFunc: mergeAggrMin,
finalizeAggrFunc: finalizeAggrCommon,
},
"max": {
updateAggrFunc: updateAggrMax,
mergeAggrFunc: mergeAggrMax,
finalizeAggrFunc: finalizeAggrCommon,
},
"avg": {
updateAggrFunc: updateAggrAvg,
mergeAggrFunc: mergeAggrAvg,
finalizeAggrFunc: finalizeAggrAvg,
},
"count": {
updateAggrFunc: updateAggrCount,
mergeAggrFunc: mergeAggrCount,
finalizeAggrFunc: finalizeAggrCount,
},
"sum2": {
updateAggrFunc: updateAggrSum2,
mergeAggrFunc: mergeAggrSum2,
finalizeAggrFunc: finalizeAggrCommon,
},
"geomean": {
updateAggrFunc: updateAggrGeomean,
mergeAggrFunc: mergeAggrGeomean,
finalizeAggrFunc: finalizeAggrGeomean,
},
}
type incrementalAggrFuncContext struct {
ae *aggrFuncExpr
mLock sync.Mutex
m map[uint]map[string]*incrementalAggrContext
callbacks *incrementalAggrFuncCallbacks
}
func newIncrementalAggrFuncContext(ae *aggrFuncExpr, callbacks *incrementalAggrFuncCallbacks) *incrementalAggrFuncContext {
return &incrementalAggrFuncContext{
ae: ae,
m: make(map[uint]map[string]*incrementalAggrContext),
callbacks: callbacks,
}
}
func (iafc *incrementalAggrFuncContext) updateTimeseries(ts *timeseries, workerID uint) {
iafc.mLock.Lock()
m := iafc.m[workerID]
if m == nil {
m = make(map[string]*incrementalAggrContext, 1)
iafc.m[workerID] = m
}
iafc.mLock.Unlock()
removeGroupTags(&ts.MetricName, &iafc.ae.Modifier)
bb := bbPool.Get()
bb.B = marshalMetricNameSorted(bb.B[:0], &ts.MetricName)
iac := 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)),
}
m[string(bb.B)] = iac
}
bbPool.Put(bb)
iafc.callbacks.updateAggrFunc(iac, ts.Values)
}
func (iafc *incrementalAggrFuncContext) finalizeTimeseries() []*timeseries {
// There is no need in iafc.mLock.Lock here, since finalizeTimeseries must be called
// without concurrent goroutines touching iafc.
mGlobal := make(map[string]*incrementalAggrContext)
mergeAggrFunc := iafc.callbacks.mergeAggrFunc
for _, m := range iafc.m {
for k, iac := range m {
iacGlobal := mGlobal[k]
if iacGlobal == nil {
mGlobal[k] = iac
continue
}
mergeAggrFunc(iacGlobal, iac)
}
}
tss := make([]*timeseries, 0, len(mGlobal))
finalizeAggrFunc := iafc.callbacks.finalizeAggrFunc
for _, iac := range mGlobal {
finalizeAggrFunc(iac)
tss = append(tss, iac.ts)
}
return tss
}
type incrementalAggrFuncCallbacks struct {
updateAggrFunc func(iac *incrementalAggrContext, values []float64)
mergeAggrFunc func(dst, src *incrementalAggrContext)
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
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = 1
continue
}
dstValues[i] += v
}
}
func mergeAggrSum(dst, src *incrementalAggrContext) {
srcValues := src.ts.Values
dstValues := dst.ts.Values
srcCounts := src.values
dstCounts := dst.values
_ = srcCounts[len(srcValues)-1]
_ = dstCounts[len(srcValues)-1]
_ = dstValues[len(srcValues)-1]
for i, v := range srcValues {
if srcCounts[i] == 0 {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = 1
continue
}
dstValues[i] += v
}
}
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 mergeAggrMin(dst, src *incrementalAggrContext) {
srcValues := src.ts.Values
dstValues := dst.ts.Values
srcCounts := src.values
dstCounts := dst.values
_ = srcCounts[len(srcValues)-1]
_ = dstCounts[len(srcValues)-1]
_ = dstValues[len(srcValues)-1]
for i, v := range srcValues {
if srcCounts[i] == 0 {
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 mergeAggrMax(dst, src *incrementalAggrContext) {
srcValues := src.ts.Values
dstValues := dst.ts.Values
srcCounts := src.values
dstCounts := dst.values
_ = srcCounts[len(srcValues)-1]
_ = dstCounts[len(srcValues)-1]
_ = dstValues[len(srcValues)-1]
for i, v := range srcValues {
if srcCounts[i] == 0 {
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 mergeAggrAvg(dst, src *incrementalAggrContext) {
srcValues := src.ts.Values
dstValues := dst.ts.Values
srcCounts := src.values
dstCounts := dst.values
_ = srcCounts[len(srcValues)-1]
_ = dstCounts[len(srcValues)-1]
_ = dstValues[len(srcValues)-1]
for i, v := range srcValues {
if srcCounts[i] == 0 {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = srcCounts[i]
continue
}
dstValues[i] += v
dstCounts[i] += srcCounts[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 mergeAggrCount(dst, src *incrementalAggrContext) {
srcValues := src.ts.Values
dstValues := dst.ts.Values
_ = dstValues[len(srcValues)-1]
for i, v := range srcValues {
dstValues[i] += v
}
}
func finalizeAggrCount(iac *incrementalAggrContext) {
dstValues := iac.ts.Values
for i, v := range dstValues {
if v == 0 {
dstValues[i] = nan
}
}
}
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
}
if dstCounts[i] == 0 {
dstValues[i] = v * v
dstCounts[i] = 1
continue
}
dstValues[i] += v * v
}
}
func mergeAggrSum2(dst, src *incrementalAggrContext) {
srcValues := src.ts.Values
dstValues := dst.ts.Values
srcCounts := src.values
dstCounts := dst.values
_ = srcCounts[len(srcValues)-1]
_ = dstCounts[len(srcValues)-1]
_ = dstValues[len(srcValues)-1]
for i, v := range srcValues {
if srcCounts[i] == 0 {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = 1
continue
}
dstValues[i] += v
}
}
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 mergeAggrGeomean(dst, src *incrementalAggrContext) {
srcValues := src.ts.Values
dstValues := dst.ts.Values
srcCounts := src.values
dstCounts := dst.values
_ = srcCounts[len(srcValues)-1]
_ = dstCounts[len(srcValues)-1]
_ = dstValues[len(srcValues)-1]
for i, v := range srcValues {
if srcCounts[i] == 0 {
continue
}
if dstCounts[i] == 0 {
dstValues[i] = v
dstCounts[i] = srcCounts[i]
continue
}
dstValues[i] *= v
dstCounts[i] += srcCounts[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,187 @@
package promql
import (
"fmt"
"math"
"reflect"
"runtime"
"sync"
"testing"
)
func TestIncrementalAggr(t *testing.T) {
defaultTimestamps := []int64{100e3, 200e3, 300e3, 400e3}
values := [][]float64{
{1, nan, 2, nan},
{3, nan, nan, 4},
{nan, nan, 5, 6},
{7, nan, 8, 9},
{4, nan, nan, nan},
{2, nan, 3, 2},
{0, nan, 1, 1},
}
tssSrc := make([]*timeseries, len(values))
for i, vs := range values {
ts := &timeseries{
Timestamps: defaultTimestamps,
Values: vs,
}
tssSrc[i] = ts
}
copyTimeseries := func(tssSrc []*timeseries) []*timeseries {
tssDst := make([]*timeseries, len(tssSrc))
for i, tsSrc := range tssSrc {
var tsDst timeseries
tsDst.CopyFromShallowTimestamps(tsSrc)
tssDst[i] = &tsDst
}
return tssDst
}
f := func(name string, valuesExpected []float64) {
t.Helper()
callbacks := getIncrementalAggrFuncCallbacks(name)
ae := &aggrFuncExpr{
Name: name,
}
tssExpected := []*timeseries{{
Timestamps: defaultTimestamps,
Values: valuesExpected,
}}
// run the test multiple times to make sure there are no side effects on concurrency
for i := 0; i < 10; i++ {
iafc := newIncrementalAggrFuncContext(ae, callbacks)
tssSrcCopy := copyTimeseries(tssSrc)
if err := testIncrementalParallelAggr(iafc, tssSrcCopy, tssExpected); err != nil {
t.Fatalf("unexpected error on iteration %d: %s", i, err)
}
}
}
t.Run("sum", func(t *testing.T) {
t.Parallel()
valuesExpected := []float64{17, nan, 19, 22}
f("sum", valuesExpected)
})
t.Run("min", func(t *testing.T) {
t.Parallel()
valuesExpected := []float64{0, nan, 1, 1}
f("min", valuesExpected)
})
t.Run("max", func(t *testing.T) {
t.Parallel()
valuesExpected := []float64{7, nan, 8, 9}
f("max", valuesExpected)
})
t.Run("avg", func(t *testing.T) {
t.Parallel()
valuesExpected := []float64{2.8333333333333335, nan, 3.8, 4.4}
f("avg", valuesExpected)
})
t.Run("count", func(t *testing.T) {
t.Parallel()
valuesExpected := []float64{6, nan, 5, 5}
f("count", valuesExpected)
})
t.Run("sum2", func(t *testing.T) {
t.Parallel()
valuesExpected := []float64{79, nan, 103, 138}
f("sum2", valuesExpected)
})
t.Run("geomean", func(t *testing.T) {
t.Parallel()
valuesExpected := []float64{0, nan, 2.9925557394776896, 3.365865436338599}
f("geomean", valuesExpected)
})
}
func testIncrementalParallelAggr(iafc *incrementalAggrFuncContext, tssSrc, tssExpected []*timeseries) error {
const workersCount = 3
tsCh := make(chan *timeseries)
var wg sync.WaitGroup
wg.Add(workersCount)
for i := 0; i < workersCount; i++ {
go func(workerID uint) {
defer wg.Done()
for ts := range tsCh {
runtime.Gosched() // allow other goroutines performing the work
iafc.updateTimeseries(ts, workerID)
}
}(uint(i))
}
for _, ts := range tssSrc {
tsCh <- ts
}
close(tsCh)
wg.Wait()
tssActual := iafc.finalizeTimeseries()
if err := expectTimeseriesEqual(tssActual, tssExpected); err != nil {
return fmt.Errorf("%s; tssActual=%v, tssExpected=%v", err, tssActual, tssExpected)
}
return nil
}
func expectTimeseriesEqual(actual, expected []*timeseries) error {
if len(actual) != len(expected) {
return fmt.Errorf("unexpected number of time series; got %d; want %d", len(actual), len(expected))
}
mActual := timeseriesToMap(actual)
mExpected := timeseriesToMap(expected)
if len(mActual) != len(mExpected) {
return fmt.Errorf("unexpected number of time series after converting to map; got %d; want %d", len(mActual), len(mExpected))
}
for k, tsExpected := range mExpected {
tsActual := mActual[k]
if tsActual == nil {
return fmt.Errorf("missing time series for key=%q", k)
}
if err := expectTsEqual(tsActual, tsExpected); err != nil {
return err
}
}
return nil
}
func timeseriesToMap(tss []*timeseries) map[string]*timeseries {
m := make(map[string]*timeseries, len(tss))
for _, ts := range tss {
k := ts.MetricName.Marshal(nil)
m[string(k)] = ts
}
return m
}
func expectTsEqual(actual, expected *timeseries) error {
mnActual := actual.MetricName.Marshal(nil)
mnExpected := expected.MetricName.Marshal(nil)
if string(mnActual) != string(mnExpected) {
return fmt.Errorf("unexpected metric name; got %q; want %q", mnActual, mnExpected)
}
if !reflect.DeepEqual(actual.Timestamps, expected.Timestamps) {
return fmt.Errorf("unexpected timestamps; got %v; want %v", actual.Timestamps, expected.Timestamps)
}
if err := compareValues(actual.Values, expected.Values); err != nil {
return fmt.Errorf("%s; actual %v; expected %v", err, actual.Values, expected.Values)
}
return nil
}
func compareValues(vs1, vs2 []float64) error {
if len(vs1) != len(vs2) {
return fmt.Errorf("unexpected number of values; got %d; want %d", len(vs1), len(vs2))
}
for i, v1 := range vs1 {
v2 := vs2[i]
if math.IsNaN(v1) {
if !math.IsNaN(v2) {
return fmt.Errorf("unexpected value; got %v; want %v", v1, v2)
}
continue
}
if v1 != v2 {
return fmt.Errorf("unexpected value; got %v; want %v", v1, v2)
}
}
return nil
}

View File

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

View File

@@ -260,6 +260,9 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
dstValues[j] = bf(a, b, isBool)
}
}
// Optimization: remove time series containing only NaNs.
// This is quite common after applying filters like `q > 0`.
dst = removeNaNs(dst)
return dst, nil
}
}
@@ -271,7 +274,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 +284,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 +299,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 +343,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 +357,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 +518,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
@@ -395,30 +466,19 @@ func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, re *
preFunc, rcs := getRollupConfigs(name, rf, ec.Start, ec.End, ec.Step, window, sharedTimestamps)
tss := make([]*timeseries, 0, len(tssSQ)*len(rcs))
var tssLock sync.Mutex
removeMetricGroup := !rollupFuncsKeepMetricGroup[name]
doParallel(tssSQ, func(tsSQ *timeseries, values []float64, timestamps []int64) ([]float64, []int64) {
values, timestamps = removeNanValues(values[:0], timestamps[:0], tsSQ.Values, tsSQ.Timestamps)
preFunc(values, timestamps)
for _, rc := range rcs {
var ts timeseries
ts.MetricName.CopyFrom(&tsSQ.MetricName)
if len(rc.TagValue) > 0 {
ts.MetricName.AddTag("rollup", rc.TagValue)
}
ts.Values = rc.Do(ts.Values[:0], values, timestamps)
ts.Timestamps = sharedTimestamps
ts.denyReuse = true
doRollupForTimeseries(rc, &ts, &tsSQ.MetricName, values, timestamps, sharedTimestamps, removeMetricGroup)
tssLock.Lock()
tss = append(tss, &ts)
tssLock.Unlock()
}
return values, timestamps
})
if !rollupFuncsKeepMetricGroup[name] {
tss = copyTimeseriesMetricNames(tss)
for _, ts := range tss {
ts.MetricName.ResetMetricGroup()
}
}
return tss, nil
}
@@ -472,31 +532,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()
@@ -514,7 +570,7 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me
MaxTimestamp: ec.End + ec.Step,
TagFilterss: [][]storage.TagFilter{me.TagFilters},
}
rss, err := netstorage.ProcessSearchQuery(sq, ec.Deadline)
rss, err := netstorage.ProcessSearchQuery(sq, true, ec.Deadline)
if err != nil {
return nil, err
}
@@ -533,28 +589,79 @@ 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))
removeMetricGroup := !rollupFuncsKeepMetricGroup[name]
var tss []*timeseries
if iafc != nil {
tss, err = evalRollupWithIncrementalAggregate(iafc, rss, rcs, preFunc, sharedTimestamps, removeMetricGroup)
} else {
tss, err = evalRollupNoIncrementalAggregate(rss, rcs, preFunc, sharedTimestamps, removeMetricGroup)
}
if err != nil {
return nil, err
}
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, removeMetricGroup bool) ([]*timeseries, error) {
err := rss.RunParallel(func(rs *netstorage.Result, workerID uint) {
preFunc(rs.Values, rs.Timestamps)
ts := getTimeseries()
defer putTimeseries(ts)
for _, rc := range rcs {
ts.Reset()
doRollupForTimeseries(rc, ts, &rs.MetricName, rs.Values, rs.Timestamps, sharedTimestamps, removeMetricGroup)
iafc.updateTimeseries(ts, workerID)
// ts.Timestamps points to sharedTimestamps. Zero it, so it can be re-used.
ts.Timestamps = nil
ts.denyReuse = false
}
})
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, removeMetricGroup bool) ([]*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, workerID uint) {
preFunc(rs.Values, rs.Timestamps)
for _, rc := range rcs {
var ts timeseries
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
ts.denyReuse = true
doRollupForTimeseries(rc, &ts, &rs.MetricName, rs.Values, rs.Timestamps, sharedTimestamps, removeMetricGroup)
tssLock.Lock()
tss = append(tss, &ts)
tssLock.Unlock()
@@ -563,18 +670,23 @@ 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
}
func doRollupForTimeseries(rc *rollupConfig, tsDst *timeseries, mnSrc *storage.MetricName, valuesSrc []float64, timestampsSrc []int64,
sharedTimestamps []int64, removeMetricGroup bool) {
tsDst.MetricName.CopyFrom(mnSrc)
if len(rc.TagValue) > 0 {
tsDst.MetricName.AddTag("rollup", rc.TagValue)
}
if removeMetricGroup {
tsDst.MetricName.ResetMetricGroup()
}
tsDst.Values = rc.Do(tsDst.Values[:0], valuesSrc, timestampsSrc)
tsDst.Timestamps = sharedTimestamps
tsDst.denyReuse = true
}
func getRollupConfigs(name string, rf rollupFunc, start, end, step, window int64, sharedTimestamps []int64) (func(values []float64, timestamps []int64), []*rollupConfig) {
preFunc := func(values []float64, timestamps []int64) {}
if rollupFuncsRemoveCounterResets[name] {
@@ -584,13 +696,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 +730,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 +771,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,23 @@
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")
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
// ExpandWithExprs expands WITH expressions inside q and returns the resulting
// PromQL without WITH expressions.
func ExpandWithExprs(q string) (string, error) {
@@ -22,8 +29,20 @@ 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)
slowQueries.Inc()
}
}()
}
ec.validate()
e, err := parsePromQLWithCache(q)
@@ -50,6 +69,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 {
@@ -107,18 +134,23 @@ func timeseriesToResult(tss []*timeseries, maySort bool) ([]netstorage.Result, e
func removeNaNs(tss []*timeseries) []*timeseries {
rvs := tss[:0]
for _, ts := range tss {
nans := 0
allNans := true
for _, v := range ts.Values {
if math.IsNaN(v) {
nans++
if !math.IsNaN(v) {
allNans = false
break
}
}
if nans == len(ts.Values) {
if allNans {
// Skip timeseries with all NaNs.
continue
}
rvs = append(rvs, ts)
}
for i := len(rvs); i < len(tss); i++ {
// Zero unused time series, so GC could reclaim them.
tss[i] = nil
}
return rvs
}

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)`
@@ -2018,6 +2158,26 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected)
})
t.Run(`histogram_quantile(negative-bucket-count)`, func(t *testing.T) {
t.Parallel()
q := `sort(histogram_quantile(0.6,
label_set(90, "foo", "bar", "le", "10")
or label_set(-100, "foo", "bar", "le", "30")
or label_set(300, "foo", "bar", "le", "+Inf")
))`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`histogram_quantile(nan-bucket-count)`, func(t *testing.T) {
t.Parallel()
q := `sort(histogram_quantile(0.6,
label_set(90, "foo", "bar", "le", "10")
or label_set(NaN, "foo", "bar", "le", "30")
or label_set(300, "foo", "bar", "le", "+Inf")
))`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`median_over_time()`, func(t *testing.T) {
t.Parallel()
q := `median_over_time({})`
@@ -2068,6 +2228,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 +2250,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 +2306,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"))`
@@ -2114,10 +2363,10 @@ func TestExecSuccess(t *testing.T) {
})
t.Run(`count(multi-vector)`, func(t *testing.T) {
t.Parallel()
q := `count(label_set(10, "foo", "bar") or label_set((15-time()/100)^0.5, "baz", "sss"))`
q := `count(label_set(time()<1500, "foo", "bar") or label_set(time()<1800, "baz", "sss"))`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{2, 2, 2, 1, 1, 1},
Values: []float64{2, 2, 2, 1, nan, nan},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
@@ -2279,6 +2528,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 +2642,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 +2653,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 +2909,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 +2931,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 +2942,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 +2953,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 +2964,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 +2975,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 +2986,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 +3008,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 +3273,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 +3353,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 +3362,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 +3735,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 +3774,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 +3820,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 +3852,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 +3867,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,17 @@ 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{
"default_rollup": true,
"first_over_time": true,
"last_over_time": true,
"deriv": true,
"deriv_fast": true,
"irate": true,
"rate": true,
}
var rollupFuncsRemoveCounterResets = map[string]bool{
@@ -64,6 +78,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 +135,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 +184,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 +193,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
@@ -210,10 +231,27 @@ func getMaxPrevInterval(timestamps []int64) int64 {
}
d := (timestamps[len(timestamps)-1] - timestamps[0]) / int64(len(timestamps)-1)
if d <= 0 {
return 1
return int64(maxSilenceInterval)
}
// Slightly increase d in order to handle possible jitter in scrape interval.
return d + (d / 16)
// Increase d more for smaller scrape intervals in order to hide possible gaps
// when high jitter is present.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/139 .
if d <= 2*1000 {
return d + 4*d
}
if d <= 4*1000 {
return d + 2*d
}
if d <= 8*1000 {
return d + d
}
if d <= 16*1000 {
return d + d/2
}
if d <= 32*1000 {
return d + d/4
}
return d + d/8
}
func removeCounterResets(values []float64) {
@@ -246,12 +284,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 +300,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 +343,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 +389,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 +447,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 +473,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 +487,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 +503,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 +519,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 +532,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 +581,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 +607,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 +619,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 +670,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 +680,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 +739,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 +754,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 +773,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 +782,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 +814,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 +832,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, 0}
timestampsExpected := []int64{120, 124, 128, 132, 136, 140, 144, 148}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
}
@@ -290,22 +364,22 @@ 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)
})
t.Run("afterEnd", func(t *testing.T) {
rc := rollupConfig{
Func: rollupFirst,
Start: 141,
End: 171,
Start: 161,
End: 191,
Step: 10,
Window: 3,
}
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
values := rc.Do(nil, testValues, testTimestamps)
valuesExpected := []float64{34, nan, nan, nan}
timestampsExpected := []int64{141, 151, 161, 171}
valuesExpected := []float64{34, 34, 34, nan}
timestampsExpected := []int64{161, 171, 181, 191}
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, 34}
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)
@@ -323,6 +324,14 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
if math.IsNaN(phi) {
return nan
}
// Verify for broken buckets with NaN or negative values.
for _, xs := range xss {
v := xs.ts.Values[i]
if math.IsNaN(v) || v < 0 {
// Broken bucket.
return nan
}
}
if phi < 0 {
return -inf
}
@@ -333,10 +342,6 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
for _, xs := range xss {
v := xs.ts.Values[i]
le := xs.le
if v <= vPrev {
v = vPrev
le = lePrev
}
if v < vReq {
vPrev = v
lePrev = le
@@ -394,13 +399,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 +639,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 +812,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 +863,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 +878,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,39 @@ 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_ignored_total{reason="big_timestamp"}`, func() float64 {
return float64(m().TooBigTimestampRows)
})
metrics.NewGauge(`vm_rows_ignored_total{reason="small_timestamp"}`, func() float64 {
return float64(m().TooSmallTimestampRows)
})
metrics.NewGauge(`vm_concurrent_addrows_limit_reached_total`, func() float64 {
return float64(m().AddRowsConcurrencyLimitReached)
})
metrics.NewGauge(`vm_concurrent_addrows_limit_timeout_total`, func() float64 {
return float64(m().AddRowsConcurrencyLimitTimeout)
})
metrics.NewGauge(`vm_concurrent_addrows_dropped_rows_total`, func() float64 {
return float64(m().AddRowsConcurrencyDroppedRows)
})
metrics.NewGauge(`vm_concurrent_addrows_capacity`, func() float64 {
return float64(m().AddRowsConcurrencyCapacity)
})
metrics.NewGauge(`vm_concurrent_addrows_current`, func() float64 {
return float64(m().AddRowsConcurrencyCurrent)
})
metrics.NewGauge(`vm_rows{type="storage/big"}`, func() float64 {
return float64(tm().BigRowsCount)
})
@@ -342,6 +403,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 +421,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 +474,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 +508,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.7
CERTS_IMAGE := local/certs:1.0.2
package-certs:
@@ -18,8 +18,10 @@ 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 \
$(DOCKER_OPTS) \
$(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)
go build $(RACE) -mod=vendor -ldflags "-s -w -extldflags '-static' $(GO_BUILDINFO)" -tags 'netgo osusergo' -o bin/$(APP_NAME)$(APP_SUFFIX)-prod $(PKG_PREFIX)/app/$(APP_NAME)
package-via-docker:
(docker image ls --format '{{.Repository}}:{{.Tag}}' | grep -q '$(DOCKER_NAMESPACE)/$(APP_NAME):$(PKG_TAG)$(RACE)') || (\

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.7
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

10
go.mod
View File

@@ -1,16 +1,18 @@
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.7.1
github.com/cespare/xxhash/v2 v2.0.1-0.20190104013014-3767db7a7e18
github.com/golang/snappy v0.0.1
github.com/google/go-cmp v0.3.0 // indirect
github.com/klauspost/compress v1.7.5
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-20190804053845-51ab0e2deafa
)
go 1.12

21
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.7.1 h1:g2qrY6Upn8rvlvR40cGHFY0crwi4hpqF0n9vJMNsCSg=
github.com/VictoriaMetrics/metrics v1.7.1/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=
@@ -16,9 +16,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.7.5 h1:NMapGoDIKPKpk2hpcgAU6XHfsREHG2p8PIg7C3f/jpI=
github.com/klauspost/compress v1.7.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -36,13 +41,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-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/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

@@ -108,60 +108,60 @@ func testCalibrateScale(t *testing.T, a, b []int64, ae, be int16, aExpected, bEx
}
func TestMaxUpExponent(t *testing.T) {
testMaxUpExponent(t, 0, 1024)
testMaxUpExponent(t, -1<<63, 0)
testMaxUpExponent(t, (-1<<63)+1, 0)
testMaxUpExponent(t, (1<<63)-1, 0)
testMaxUpExponent(t, 1, 18)
testMaxUpExponent(t, 12, 17)
testMaxUpExponent(t, 123, 16)
testMaxUpExponent(t, 1234, 15)
testMaxUpExponent(t, 12345, 14)
testMaxUpExponent(t, 123456, 13)
testMaxUpExponent(t, 1234567, 12)
testMaxUpExponent(t, 12345678, 11)
testMaxUpExponent(t, 123456789, 10)
testMaxUpExponent(t, 1234567890, 9)
testMaxUpExponent(t, 12345678901, 8)
testMaxUpExponent(t, 123456789012, 7)
testMaxUpExponent(t, 1234567890123, 6)
testMaxUpExponent(t, 12345678901234, 5)
testMaxUpExponent(t, 123456789012345, 4)
testMaxUpExponent(t, 1234567890123456, 3)
testMaxUpExponent(t, 12345678901234567, 2)
testMaxUpExponent(t, 123456789012345678, 1)
testMaxUpExponent(t, 1234567890123456789, 0)
testMaxUpExponent(t, 923456789012345678, 0)
testMaxUpExponent(t, 92345678901234567, 1)
testMaxUpExponent(t, 9234567890123456, 2)
testMaxUpExponent(t, 923456789012345, 3)
testMaxUpExponent(t, 92345678901234, 4)
testMaxUpExponent(t, 9234567890123, 5)
testMaxUpExponent(t, 923456789012, 6)
testMaxUpExponent(t, 92345678901, 7)
testMaxUpExponent(t, 9234567890, 8)
testMaxUpExponent(t, 923456789, 9)
testMaxUpExponent(t, 92345678, 10)
testMaxUpExponent(t, 9234567, 11)
testMaxUpExponent(t, 923456, 12)
testMaxUpExponent(t, 92345, 13)
testMaxUpExponent(t, 9234, 14)
testMaxUpExponent(t, 923, 15)
testMaxUpExponent(t, 92, 17)
testMaxUpExponent(t, 9, 18)
}
f := func(v int64, eExpected int16) {
t.Helper()
func testMaxUpExponent(t *testing.T, v int64, eExpected int16) {
t.Helper()
e := maxUpExponent(v)
if e != eExpected {
t.Fatalf("unexpected e for v=%d; got %d; epxecting %d", v, e, eExpected)
}
e = maxUpExponent(-v)
if e != eExpected {
t.Fatalf("unexpected e for v=%d; got %d; expecting %d", -v, e, eExpected)
}
}
e := maxUpExponent(v)
if e != eExpected {
t.Fatalf("unexpected e for v=%d; got %d; epxecting %d", v, e, eExpected)
}
e = maxUpExponent(-v)
if e != eExpected {
t.Fatalf("unexpected e for v=%d; got %d; expecting %d", -v, e, eExpected)
}
f(0, 1024)
f(-1<<63, 0)
f((-1<<63)+1, 0)
f((1<<63)-1, 0)
f(1, 18)
f(12, 17)
f(123, 16)
f(1234, 15)
f(12345, 14)
f(123456, 13)
f(1234567, 12)
f(12345678, 11)
f(123456789, 10)
f(1234567890, 9)
f(12345678901, 8)
f(123456789012, 7)
f(1234567890123, 6)
f(12345678901234, 5)
f(123456789012345, 4)
f(1234567890123456, 3)
f(12345678901234567, 2)
f(123456789012345678, 1)
f(1234567890123456789, 0)
f(923456789012345678, 0)
f(92345678901234567, 1)
f(9234567890123456, 2)
f(923456789012345, 3)
f(92345678901234, 4)
f(9234567890123, 5)
f(923456789012, 6)
f(92345678901, 7)
f(9234567890, 8)
f(923456789, 9)
f(92345678, 10)
f(9234567, 11)
f(923456, 12)
f(92345, 13)
f(9234, 14)
f(923, 15)
f(92, 17)
f(9, 18)
}
func TestAppendFloatToDecimal(t *testing.T) {
@@ -207,83 +207,103 @@ func testAppendFloatToDecimal(t *testing.T, fa []float64, daExpected []int64, eE
}
func TestFloatToDecimal(t *testing.T) {
testFloatToDecimal(t, 0, 0, 0)
testFloatToDecimal(t, 1, 1, 0)
testFloatToDecimal(t, -1, -1, 0)
testFloatToDecimal(t, 0.9, 9, -1)
testFloatToDecimal(t, 0.99, 99, -2)
testFloatToDecimal(t, 9, 9, 0)
testFloatToDecimal(t, 99, 99, 0)
testFloatToDecimal(t, 20, 2, 1)
testFloatToDecimal(t, 100, 1, 2)
testFloatToDecimal(t, 3000, 3, 3)
testFloatToDecimal(t, 0.123, 123, -3)
testFloatToDecimal(t, -0.123, -123, -3)
testFloatToDecimal(t, 1.2345, 12345, -4)
testFloatToDecimal(t, -1.2345, -12345, -4)
testFloatToDecimal(t, 12000, 12, 3)
testFloatToDecimal(t, -12000, -12, 3)
testFloatToDecimal(t, 1e-30, 1, -30)
testFloatToDecimal(t, -1e-30, -1, -30)
testFloatToDecimal(t, 1e-260, 1, -260)
testFloatToDecimal(t, -1e-260, -1, -260)
testFloatToDecimal(t, 321e260, 321, 260)
testFloatToDecimal(t, -321e260, -321, 260)
testFloatToDecimal(t, 1234567890123, 1234567890123, 0)
testFloatToDecimal(t, -1234567890123, -1234567890123, 0)
testFloatToDecimal(t, 123e5, 123, 5)
testFloatToDecimal(t, 15e18, 15, 18)
testFloatToDecimal(t, math.Inf(1), vInfPos, 0)
testFloatToDecimal(t, math.Inf(-1), vInfNeg, 0)
testFloatToDecimal(t, 1<<63-1, 922337203685, 7)
testFloatToDecimal(t, -1<<63, -922337203685, 7)
}
func testFloatToDecimal(t *testing.T, f float64, vExpected int64, eExpected int16) {
t.Helper()
v, e := FromFloat(f)
if v != vExpected {
t.Fatalf("unexpected v for f=%e; got %d; expecting %d", f, v, vExpected)
}
if e != eExpected {
t.Fatalf("unexpected e for f=%e; got %d; expecting %d", f, e, eExpected)
f := func(f float64, vExpected int64, eExpected int16) {
t.Helper()
v, e := FromFloat(f)
if v != vExpected {
t.Fatalf("unexpected v for f=%e; got %d; expecting %d", f, v, vExpected)
}
if e != eExpected {
t.Fatalf("unexpected e for f=%e; got %d; expecting %d", f, e, eExpected)
}
}
f(0, 0, 0)
f(1, 1, 0)
f(-1, -1, 0)
f(0.9, 9, -1)
f(0.99, 99, -2)
f(9, 9, 0)
f(99, 99, 0)
f(20, 2, 1)
f(100, 1, 2)
f(3000, 3, 3)
f(0.123, 123, -3)
f(-0.123, -123, -3)
f(1.2345, 12345, -4)
f(-1.2345, -12345, -4)
f(12000, 12, 3)
f(-12000, -12, 3)
f(1e-30, 1, -30)
f(-1e-30, -1, -30)
f(1e-260, 1, -260)
f(-1e-260, -1, -260)
f(321e260, 321, 260)
f(-321e260, -321, 260)
f(1234567890123, 1234567890123, 0)
f(-1234567890123, -1234567890123, 0)
f(123e5, 123, 5)
f(15e18, 15, 18)
f(math.Inf(1), vInfPos, 0)
f(math.Inf(-1), vInfNeg, 0)
f(1<<63-1, 922337203685, 7)
f(-1<<63, -922337203685, 7)
// Test precision loss due to conversionPrecision.
f(0.1234567890123456, 12345678901234, -14)
f(-123456.7890123456, -12345678901234, -8)
}
func TestFloatToDecimalRoundtrip(t *testing.T) {
testFloatToDecimalRoundtrip(t, 0)
testFloatToDecimalRoundtrip(t, 1)
testFloatToDecimalRoundtrip(t, 0.123)
testFloatToDecimalRoundtrip(t, 1.2345)
testFloatToDecimalRoundtrip(t, 12000)
testFloatToDecimalRoundtrip(t, 1e-30)
testFloatToDecimalRoundtrip(t, 1e-260)
testFloatToDecimalRoundtrip(t, 321e260)
testFloatToDecimalRoundtrip(t, 1234567890123)
testFloatToDecimalRoundtrip(t, 12.34567890125)
testFloatToDecimalRoundtrip(t, 15e18)
f := func(f float64) {
t.Helper()
testFloatToDecimalRoundtrip(t, math.Inf(1))
testFloatToDecimalRoundtrip(t, math.Inf(-1))
testFloatToDecimalRoundtrip(t, 1<<63-1)
testFloatToDecimalRoundtrip(t, -1<<63)
v, e := FromFloat(f)
fNew := ToFloat(v, e)
if !equalFloat(fNew, f) {
t.Fatalf("unexpected fNew for v=%d, e=%d; got %g; expecting %g", v, e, fNew, f)
}
v, e = FromFloat(-f)
fNew = ToFloat(v, e)
if !equalFloat(fNew, -f) {
t.Fatalf("unexepcted fNew for v=%d, e=%d; got %g; expecting %g", v, e, fNew, -f)
}
}
f(0)
f(1)
f(0.123)
f(1.2345)
f(12000)
f(1e-30)
f(1e-260)
f(321e260)
f(1234567890123)
f(12.34567890125)
f(-1234567.8901256789)
f(15e18)
f(math.Inf(1))
f(math.Inf(-1))
f(1<<63 - 1)
f(-1 << 63)
for i := 0; i < 1e4; i++ {
f := rand.NormFloat64()
testFloatToDecimalRoundtrip(t, f)
testFloatToDecimalRoundtrip(t, f*1e-6)
testFloatToDecimalRoundtrip(t, f*1e6)
v := rand.NormFloat64()
f(v)
f(v * 1e-6)
f(v * 1e6)
testFloatToDecimalRoundtrip(t, roundFloat(f, 20))
testFloatToDecimalRoundtrip(t, roundFloat(f, 10))
testFloatToDecimalRoundtrip(t, roundFloat(f, 5))
testFloatToDecimalRoundtrip(t, roundFloat(f, 0))
testFloatToDecimalRoundtrip(t, roundFloat(f, -5))
testFloatToDecimalRoundtrip(t, roundFloat(f, -10))
testFloatToDecimalRoundtrip(t, roundFloat(f, -20))
f(roundFloat(v, 20))
f(roundFloat(v, 10))
f(roundFloat(v, 5))
f(roundFloat(v, 0))
f(roundFloat(v, -5))
f(roundFloat(v, -10))
f(roundFloat(v, -20))
}
}
@@ -292,22 +312,6 @@ func roundFloat(f float64, exp int) float64 {
return math.Trunc(f) * math.Pow10(exp)
}
func testFloatToDecimalRoundtrip(t *testing.T, f float64) {
t.Helper()
v, e := FromFloat(f)
fNew := ToFloat(v, e)
if !equalFloat(fNew, f) {
t.Fatalf("unexpected fNew for v=%d, e=%d; got %g; expecting %g", v, e, fNew, f)
}
v, e = FromFloat(-f)
fNew = ToFloat(v, e)
if !equalFloat(fNew, -f) {
t.Fatalf("unexepcted fNew for v=%d, e=%d; got %g; expecting %g", v, e, fNew, -f)
}
}
func equalFloat(f1, f2 float64) bool {
if math.IsInf(f1, 0) {
return math.IsInf(f1, 1) == math.IsInf(f2, 1) || math.IsInf(f1, -1) == math.IsInf(f2, -1)

View File

@@ -1,27 +1,34 @@
package encoding
import (
"github.com/valyala/gozstd"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding/zstd"
"github.com/VictoriaMetrics/metrics"
)
// 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 = zstd.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) {
return gozstd.Decompress(dst, src)
decompressCalls.Inc()
return zstd.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 {
@@ -296,7 +296,7 @@ func isDeltaConst(a []int64) bool {
// i.e. arbitrary changing values.
//
// It is OK if a few gauges aren't detected (i.e. detected as counters),
// since misdetected counters as gauges are much worse condition.
// since misdetected counters as gauges leads to worser compression ratio.
func isGauge(a []int64) bool {
// Check all the items in a, since a part of items may lead
// to incorrect gauge detection.
@@ -305,32 +305,36 @@ func isGauge(a []int64) bool {
return false
}
extremes := 0
plus := a[0] <= a[1]
v1 := a[1]
for _, v2 := range a[2:] {
if plus {
if v2 < v1 {
extremes++
plus = false
}
} else {
if v2 > v1 {
extremes++
plus = true
}
}
v1 = v2
resets := 0
vPrev := a[0]
if vPrev < 0 {
// Counter values cannot be negative.
return true
}
if extremes <= 2 {
// Probably counter reset.
for _, v := range a[1:] {
if v < vPrev {
if v < 0 {
// Counter values cannot be negative.
return true
}
if v > (vPrev >> 3) {
// Decreasing sequence detected.
// This is a gauge.
return true
}
// Possible counter reset.
resets++
}
vPrev = v
}
if resets <= 2 {
// Counter with a few resets.
return false
}
// A few extremes may indicate counter resets.
// Let it be a gauge if extremes exceed len(a)/32,
// otherwise assume counter reset.
return extremes > (len(a) >> 5)
// Let it be a gauge if resets exceeds len(a)/8,
// otherwise assume counter.
return resets > (len(a) >> 3)
}
func getCompressLevel(itemsCount int) int {

View File

@@ -0,0 +1,83 @@
// +build cgo
package encoding
import (
"math/rand"
"testing"
)
func TestMarshalUnmarshalInt64Array(t *testing.T) {
var va []int64
var v int64
// Verify nearest delta encoding.
va = va[:0]
v = 0
for i := 0; i < 8*1024; i++ {
v += int64(rand.NormFloat64() * 1e6)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 23; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeZSTDNearestDelta)
}
for precisionBits := uint8(23); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta)
}
// Verify nearest delta2 encoding.
va = va[:0]
v = 0
for i := 0; i < 8*1024; i++ {
v += 30e6 + int64(rand.NormFloat64()*1e6)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 24; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeZSTDNearestDelta2)
}
for precisionBits := uint8(24); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta2)
}
// Verify nearest delta encoding.
va = va[:0]
v = 1000
for i := 0; i < 6; i++ {
v += int64(rand.NormFloat64() * 100)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta)
}
// Verify nearest delta2 encoding.
va = va[:0]
v = 0
for i := 0; i < 6; i++ {
v += 3000 + int64(rand.NormFloat64()*100)
va = append(va, v)
}
for precisionBits := uint8(5); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta2)
}
}
func TestMarshalInt64ArraySize(t *testing.T) {
var va []int64
v := int64(rand.Float64() * 1e9)
for i := 0; i < 8*1024; i++ {
va = append(va, v)
v += 30e3 + int64(rand.NormFloat64()*1e3)
}
testMarshalInt64ArraySize(t, va, 1, 500, 1300)
testMarshalInt64ArraySize(t, va, 2, 500, 1400)
testMarshalInt64ArraySize(t, va, 3, 800, 1800)
testMarshalInt64ArraySize(t, va, 4, 1300, 2100)
testMarshalInt64ArraySize(t, va, 5, 2000, 3200)
testMarshalInt64ArraySize(t, va, 6, 3000, 4800)
testMarshalInt64ArraySize(t, va, 7, 4000, 6400)
testMarshalInt64ArraySize(t, va, 8, 6000, 8000)
testMarshalInt64ArraySize(t, va, 9, 7000, 8800)
testMarshalInt64ArraySize(t, va, 10, 8000, 10000)
}

View File

@@ -0,0 +1,83 @@
// +build !cgo
package encoding
import (
"math/rand"
"testing"
)
func TestMarshalUnmarshalInt64Array(t *testing.T) {
var va []int64
var v int64
// Verify nearest delta encoding.
va = va[:0]
v = 0
for i := 0; i < 8*1024; i++ {
v += int64(rand.NormFloat64() * 1e6)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 17; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeZSTDNearestDelta)
}
for precisionBits := uint8(23); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta)
}
// Verify nearest delta2 encoding.
va = va[:0]
v = 0
for i := 0; i < 8*1024; i++ {
v += 30e6 + int64(rand.NormFloat64()*1e6)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 15; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeZSTDNearestDelta2)
}
for precisionBits := uint8(24); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta2)
}
// Verify nearest delta encoding.
va = va[:0]
v = 1000
for i := 0; i < 6; i++ {
v += int64(rand.NormFloat64() * 100)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta)
}
// Verify nearest delta2 encoding.
va = va[:0]
v = 0
for i := 0; i < 6; i++ {
v += 3000 + int64(rand.NormFloat64()*100)
va = append(va, v)
}
for precisionBits := uint8(5); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta2)
}
}
func TestMarshalInt64ArraySize(t *testing.T) {
var va []int64
v := int64(rand.Float64() * 1e9)
for i := 0; i < 8*1024; i++ {
va = append(va, v)
v += 30e3 + int64(rand.NormFloat64()*1e3)
}
testMarshalInt64ArraySize(t, va, 1, 500, 1700)
testMarshalInt64ArraySize(t, va, 2, 600, 1800)
testMarshalInt64ArraySize(t, va, 3, 900, 2100)
testMarshalInt64ArraySize(t, va, 4, 1300, 2200)
testMarshalInt64ArraySize(t, va, 5, 2000, 3300)
testMarshalInt64ArraySize(t, va, 6, 3000, 5000)
testMarshalInt64ArraySize(t, va, 7, 4000, 6500)
testMarshalInt64ArraySize(t, va, 8, 6000, 8000)
testMarshalInt64ArraySize(t, va, 9, 7000, 8800)
testMarshalInt64ArraySize(t, va, 10, 8000, 17000)
}

View File

@@ -43,27 +43,29 @@ func TestIsDeltaConst(t *testing.T) {
}
func TestIsGauge(t *testing.T) {
testIsGauge(t, []int64{}, false)
testIsGauge(t, []int64{0}, false)
testIsGauge(t, []int64{1, 2}, false)
testIsGauge(t, []int64{0, 1, 2, 3, 4, 5}, false)
testIsGauge(t, []int64{0, -1, -2, -3, -4}, false)
testIsGauge(t, []int64{0, 0, 0, 0, 0, 0, 0}, false)
testIsGauge(t, []int64{1, 1, 1, 1, 1}, false)
testIsGauge(t, []int64{1, 1, 2, 2, 2, 2}, false)
testIsGauge(t, []int64{1, 5, 2, 3}, false) // a single counter reset
testIsGauge(t, []int64{1, 5, 2, 3, 2}, true)
testIsGauge(t, []int64{-1, -5, -2, -3}, false) // a single counter reset
testIsGauge(t, []int64{-1, -5, -2, -3, -2}, true)
}
func testIsGauge(t *testing.T, a []int64, okExpected bool) {
t.Helper()
ok := isGauge(a)
if ok != okExpected {
t.Fatalf("unexpected result for isGauge(%d); got %v; expecting %v", a, ok, okExpected)
f := func(a []int64, okExpected bool) {
t.Helper()
ok := isGauge(a)
if ok != okExpected {
t.Fatalf("unexpected result for isGauge(%d); got %v; expecting %v", a, ok, okExpected)
}
}
f([]int64{}, false)
f([]int64{0}, false)
f([]int64{1, 2}, false)
f([]int64{0, 1, 2, 3, 4, 5}, false)
f([]int64{0, -1, -2, -3, -4}, true)
f([]int64{0, 0, 0, 0, 0, 0, 0}, false)
f([]int64{1, 1, 1, 1, 1}, false)
f([]int64{1, 1, 2, 2, 2, 2}, false)
f([]int64{1, 17, 2, 3}, false) // a single counter reset
f([]int64{1, 5, 2, 3}, true)
f([]int64{1, 5, 2, 3, 2}, true)
f([]int64{-1, -5, -2, -3}, true)
f([]int64{-1, -5, -2, -3, -2}, true)
f([]int64{5, 6, 4, 3, 2}, true)
f([]int64{4, 5, 6, 5, 4, 3, 2}, true)
f([]int64{1064, 1132, 1083, 1062, 856, 747}, true)
}
func TestEnsureNonDecreasingSequence(t *testing.T) {
@@ -87,73 +89,6 @@ func testEnsureNonDecreasingSequence(t *testing.T, a []int64, vMin, vMax int64,
}
}
func TestMarshalUnmarshalInt64Array(t *testing.T) {
testMarshalUnmarshalInt64Array(t, []int64{1, 20, 234}, 4, MarshalTypeNearestDelta2)
testMarshalUnmarshalInt64Array(t, []int64{1, 20, -2345, 678934, 342}, 4, MarshalTypeNearestDelta)
testMarshalUnmarshalInt64Array(t, []int64{1, 20, 2345, 6789, 12342}, 4, MarshalTypeNearestDelta2)
// Constant encoding
testMarshalUnmarshalInt64Array(t, []int64{1}, 4, MarshalTypeConst)
testMarshalUnmarshalInt64Array(t, []int64{1, 2}, 4, MarshalTypeDeltaConst)
testMarshalUnmarshalInt64Array(t, []int64{-1, 0, 1, 2, 3, 4, 5}, 4, MarshalTypeDeltaConst)
testMarshalUnmarshalInt64Array(t, []int64{-10, -1, 8, 17, 26}, 4, MarshalTypeDeltaConst)
testMarshalUnmarshalInt64Array(t, []int64{0, 0, 0, 0, 0, 0}, 4, MarshalTypeConst)
testMarshalUnmarshalInt64Array(t, []int64{100, 100, 100, 100}, 4, MarshalTypeConst)
var va []int64
var v int64
// Verify nearest delta encoding.
va = va[:0]
v = 0
for i := 0; i < 8*1024; i++ {
v += int64(rand.NormFloat64() * 1e6)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 23; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeZSTDNearestDelta)
}
for precisionBits := uint8(23); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta)
}
// Verify nearest delta2 encoding.
va = va[:0]
v = 0
for i := 0; i < 8*1024; i++ {
v += 30e6 + int64(rand.NormFloat64()*1e6)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 24; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeZSTDNearestDelta2)
}
for precisionBits := uint8(24); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta2)
}
// Verify nearest delta encoding.
va = va[:0]
v = 1000
for i := 0; i < 6; i++ {
v += int64(rand.NormFloat64() * 100)
va = append(va, v)
}
for precisionBits := uint8(1); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta)
}
// Verify nearest delta2 encoding.
va = va[:0]
v = 0
for i := 0; i < 6; i++ {
v += 3000 + int64(rand.NormFloat64()*100)
va = append(va, v)
}
for precisionBits := uint8(5); precisionBits < 65; precisionBits++ {
testMarshalUnmarshalInt64Array(t, va, precisionBits, MarshalTypeNearestDelta2)
}
}
func testMarshalUnmarshalInt64Array(t *testing.T, va []int64, precisionBits uint8, mtExpected MarshalType) {
t.Helper()
@@ -257,24 +192,18 @@ func TestMarshalUnmarshalValues(t *testing.T) {
}
}
func TestMarshalInt64ArraySize(t *testing.T) {
var va []int64
v := int64(rand.Float64() * 1e9)
for i := 0; i < 8*1024; i++ {
va = append(va, v)
v += 30e3 + int64(rand.NormFloat64()*1e3)
}
func TestMarshalUnmarshalInt64ArrayGeneric(t *testing.T) {
testMarshalUnmarshalInt64Array(t, []int64{1, 20, 234}, 4, MarshalTypeNearestDelta2)
testMarshalUnmarshalInt64Array(t, []int64{1, 20, -2345, 678934, 342}, 4, MarshalTypeNearestDelta)
testMarshalUnmarshalInt64Array(t, []int64{1, 20, 2345, 6789, 12342}, 4, MarshalTypeNearestDelta2)
testMarshalInt64ArraySize(t, va, 1, 500, 1300)
testMarshalInt64ArraySize(t, va, 2, 600, 1400)
testMarshalInt64ArraySize(t, va, 3, 900, 1800)
testMarshalInt64ArraySize(t, va, 4, 1300, 2100)
testMarshalInt64ArraySize(t, va, 5, 2000, 3200)
testMarshalInt64ArraySize(t, va, 6, 3000, 4800)
testMarshalInt64ArraySize(t, va, 7, 4000, 6400)
testMarshalInt64ArraySize(t, va, 8, 6000, 8000)
testMarshalInt64ArraySize(t, va, 9, 7000, 8800)
testMarshalInt64ArraySize(t, va, 10, 8000, 10000)
// Constant encoding
testMarshalUnmarshalInt64Array(t, []int64{1}, 4, MarshalTypeConst)
testMarshalUnmarshalInt64Array(t, []int64{1, 2}, 4, MarshalTypeDeltaConst)
testMarshalUnmarshalInt64Array(t, []int64{-1, 0, 1, 2, 3, 4, 5}, 4, MarshalTypeDeltaConst)
testMarshalUnmarshalInt64Array(t, []int64{-10, -1, 8, 17, 26}, 4, MarshalTypeDeltaConst)
testMarshalUnmarshalInt64Array(t, []int64{0, 0, 0, 0, 0, 0}, 4, MarshalTypeConst)
testMarshalUnmarshalInt64Array(t, []int64{100, 100, 100, 100}, 4, MarshalTypeConst)
}
func testMarshalInt64ArraySize(t *testing.T, va []int64, precisionBits uint8, minSizeExpected, maxSizeExpected int) {

View File

@@ -0,0 +1,19 @@
// +build cgo
package zstd
import (
"github.com/valyala/gozstd"
)
// Decompress appends decompressed src to dst and returns the result.
func Decompress(dst, src []byte) ([]byte, error) {
return gozstd.Decompress(dst, src)
}
// CompressLevel appends compressed src to dst and returns the result.
//
// The given compressionLevel is used for the compression.
func CompressLevel(dst, src []byte, compressionLevel int) []byte {
return gozstd.CompressLevel(dst, src, compressionLevel)
}

View File

@@ -0,0 +1,78 @@
// +build !cgo
package zstd
import (
"sync"
"sync/atomic"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/klauspost/compress/zstd"
)
var (
decoder *zstd.Decoder
mu sync.Mutex
av atomic.Value
)
type registry map[int]*zstd.Encoder
func init() {
r := make(registry)
av.Store(r)
var err error
decoder, err = zstd.NewReader(nil)
if err != nil {
logger.Panicf("BUG: failed to create ZSTD reader: %s", err)
}
}
// Decompress appends decompressed src to dst and returns the result.
func Decompress(dst, src []byte) ([]byte, error) {
return decoder.DecodeAll(src, dst)
}
// CompressLevel appends compressed src to dst and returns the result.
//
// The given compressionLevel is used for the compression.
func CompressLevel(dst, src []byte, compressionLevel int) []byte {
e := getEncoder(compressionLevel)
return e.EncodeAll(src, dst)
}
func getEncoder(compressionLevel int) *zstd.Encoder {
r := av.Load().(registry)
e := r[compressionLevel]
if e != nil {
return e
}
mu.Lock()
// Create the encoder under lock in order to prevent from wasted work
// when concurrent goroutines create encoder for the same compressionLevel.
e = newEncoder(compressionLevel)
r1 := av.Load().(registry)
r2 := make(registry)
for k, v := range r1 {
r2[k] = v
}
r2[compressionLevel] = e
av.Store(r2)
mu.Unlock()
return e
}
func newEncoder(compressionLevel int) *zstd.Encoder {
level := zstd.EncoderLevelFromZstd(compressionLevel)
e, err := zstd.NewWriter(nil,
zstd.WithEncoderCRC(false), // Disable CRC for performance reasons.
zstd.WithEncoderLevel(level))
if err != nil {
logger.Panicf("BUG: failed to create ZSTD writer: %s", err)
}
return e
}

View File

@@ -0,0 +1,96 @@
// +build cgo
package zstd
import (
"math/rand"
"testing"
pure "github.com/klauspost/compress/zstd"
cgo "github.com/valyala/gozstd"
)
func TestCompressDecompress(t *testing.T) {
testCrossCompressDecompress(t, []byte("a"))
testCrossCompressDecompress(t, []byte("foobarbaz"))
var b []byte
for i := 0; i < 64*1024; i++ {
b = append(b, byte(rand.Int31n(256)))
}
testCrossCompressDecompress(t, b)
}
func testCrossCompressDecompress(t *testing.T, b []byte) {
testCompressDecompress(t, pureCompress, pureDecompress, b)
testCompressDecompress(t, cgoCompress, cgoDecompress, b)
testCompressDecompress(t, pureCompress, cgoDecompress, b)
testCompressDecompress(t, cgoCompress, pureDecompress, b)
}
func testCompressDecompress(t *testing.T, compress compressFn, decompress decompressFn, b []byte) {
bc, err := compress(nil, b, 5)
if err != nil {
t.Fatalf("unexpected error when compressing b=%x: %s", b, err)
}
bNew, err := decompress(nil, bc)
if err != nil {
t.Fatalf("unexpected error when decompressing b=%x from bc=%x: %s", b, bc, err)
}
if string(bNew) != string(b) {
t.Fatalf("invalid bNew; got\n%x; expecting\n%x", bNew, b)
}
prefix := []byte{1, 2, 33}
bcNew, err := compress(prefix, b, 5)
if err != nil {
t.Fatalf("unexpected error when compressing b=%x: %s", bcNew, err)
}
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)
}
if string(bcNew[len(prefix):]) != string(bc) {
t.Fatalf("invalid prefixed bcNew for b=%x; got\n%x; expecting\n%x", b, bcNew[len(prefix):], bc)
}
bNew, err = decompress(prefix, bc)
if err != nil {
t.Fatalf("unexpected error when decompressing b=%x from bc=%x with prefix: %s", b, bc, err)
}
if string(bNew[:len(prefix)]) != string(prefix) {
t.Fatalf("invalid bNew prefix when decompressing bc=%x; got\n%x; expecting\n%x", bc, bNew[:len(prefix)], prefix)
}
if string(bNew[len(prefix):]) != string(b) {
t.Fatalf("invalid prefixed bNew; got\n%x; expecting\n%x", bNew[len(prefix):], b)
}
}
type compressFn func(dst, src []byte, compressionLevel int) ([]byte, error)
func pureCompress(dst, src []byte, _ int) ([]byte, error) {
w, err := pure.NewWriter(nil,
pure.WithEncoderCRC(false), // Disable CRC for performance reasons.
pure.WithEncoderLevel(pure.SpeedBestCompression))
if err != nil {
return nil, err
}
return w.EncodeAll(src, dst), nil
}
func cgoCompress(dst, src []byte, compressionLevel int) ([]byte, error) {
return cgo.CompressLevel(dst, src, compressionLevel), nil
}
type decompressFn func(dst, src []byte) ([]byte, error)
func pureDecompress(dst, src []byte) ([]byte, error) {
decoder, err := pure.NewReader(nil)
if err != nil {
return nil, err
}
return decoder.DecodeAll(src, dst)
}
func cgoDecompress(dst, src []byte) ([]byte, error) {
return cgo.Decompress(dst, src)
}

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

@@ -72,9 +72,10 @@ func (sc *statConn) Read(p []byte) (int, error) {
sc.cm.readCalls.Inc()
sc.cm.readBytes.Add(n)
if err != nil && err != io.EOF {
sc.cm.readErrors.Inc()
if ne, ok := err.(net.Error); ok && ne.Timeout() {
sc.cm.readTimeouts.Inc()
} else {
sc.cm.readErrors.Inc()
}
}
return n, err
@@ -96,9 +97,10 @@ func (sc *statConn) Write(p []byte) (int, error) {
sc.cm.writeCalls.Inc()
sc.cm.writtenBytes.Add(n)
if err != nil {
sc.cm.writeErrors.Inc()
if ne, ok := err.(net.Error); ok && ne.Timeout() {
sc.cm.writeTimeouts.Inc()
} else {
sc.cm.writeErrors.Inc()
}
}
return n, err

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 {

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