mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-07 19:06:17 +03:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be39414f9c | ||
|
|
e74fb23189 | ||
|
|
582fdc059a | ||
|
|
1c108fc494 | ||
|
|
d6b5ed6d39 | ||
|
|
639b14e8ab | ||
|
|
483de1cc06 | ||
|
|
9e0896055d | ||
|
|
5bb61b8b38 | ||
|
|
75a58dee02 | ||
|
|
5b41122292 | ||
|
|
964c296f96 | ||
|
|
9ecb994671 | ||
|
|
9d41e0dcae | ||
|
|
09fc6e22e5 | ||
|
|
99c37c2c96 | ||
|
|
06c2c25544 | ||
|
|
ec1b185991 | ||
|
|
0967683ae9 | ||
|
|
ad8a43b4e1 | ||
|
|
7346982763 | ||
|
|
5d8d110010 | ||
|
|
0b488f1e37 | ||
|
|
b8bb74ffc6 | ||
|
|
5c9e48417a | ||
|
|
5c83f8e203 | ||
|
|
05713469c3 | ||
|
|
8822079b77 | ||
|
|
99e048c9df | ||
|
|
47e4b50112 | ||
|
|
241170dc05 | ||
|
|
1c69f4eadc | ||
|
|
8d93b15b86 | ||
|
|
fcc166622a | ||
|
|
a9f39168d2 | ||
|
|
f090b2e917 | ||
|
|
10caad4728 | ||
|
|
3b90c2a99a | ||
|
|
57ec4f5f92 | ||
|
|
01cb15b6f5 | ||
|
|
b9256511e8 | ||
|
|
3a38b23fa3 | ||
|
|
8bd6f1f6df | ||
|
|
4aaa5c2efc | ||
|
|
10f5a26bec | ||
|
|
c14fd6c43f | ||
|
|
a77e88db7d | ||
|
|
aad7236e5d | ||
|
|
5e5de6be9a | ||
|
|
90cf6f3fcb | ||
|
|
8e3d69219f | ||
|
|
b842a2eccc | ||
|
|
afcc7fb167 | ||
|
|
57a57c711a | ||
|
|
68f260d878 | ||
|
|
1eade9b358 | ||
|
|
7e8747f6ed | ||
|
|
0168a1b658 | ||
|
|
bf6cbb762c | ||
|
|
6aeac37fc5 | ||
|
|
c98725db55 | ||
|
|
d8043f7161 | ||
|
|
f586e1f83c | ||
|
|
d1132bb188 | ||
|
|
915fb6df79 | ||
|
|
89eb6d78a4 | ||
|
|
17096b5750 | ||
|
|
66efa5745f | ||
|
|
106ab78a47 | ||
|
|
8aa474d685 | ||
|
|
9e059bb330 | ||
|
|
2346335ea6 | ||
|
|
b339890dca | ||
|
|
6c4ca89d75 | ||
|
|
f0fe7b5ad6 | ||
|
|
22ed4e7fd4 | ||
|
|
162f1fb1b7 | ||
|
|
d07f616609 | ||
|
|
5bf4e5ffb5 | ||
|
|
8c3629a892 | ||
|
|
ea07cf68ba | ||
|
|
4ee41bab43 | ||
|
|
1273f31f19 | ||
|
|
0f2ecde0e6 | ||
|
|
6cd77d4847 | ||
|
|
fb14f23532 | ||
|
|
daba0cdb05 | ||
|
|
575d2f0a91 | ||
|
|
ec1b439329 | ||
|
|
6a943a6a58 | ||
|
|
998525999c | ||
|
|
ab88890523 | ||
|
|
374d681848 | ||
|
|
e75d5f47c4 | ||
|
|
fc90ebf43c | ||
|
|
62a7353479 | ||
|
|
54bd21eb4a | ||
|
|
2bd1a01d1a | ||
|
|
cd4833d3d0 | ||
|
|
101fa258e5 | ||
|
|
d031e04023 | ||
|
|
43ea4ce428 | ||
|
|
a336bb4e22 | ||
|
|
1fe6d784d8 | ||
|
|
55fe36149c | ||
|
|
9203170eb2 | ||
|
|
2db685c19c | ||
|
|
6ddfb06b52 | ||
|
|
40a6c0d672 | ||
|
|
1371024747 | ||
|
|
c27c6de297 | ||
|
|
0c629429de | ||
|
|
4dbd642c86 | ||
|
|
56c154f45b | ||
|
|
8d83dcf332 | ||
|
|
9a4b2b8315 | ||
|
|
e06866005d | ||
|
|
2c76a9c9ab | ||
|
|
b9166a60ff | ||
|
|
c7034fc51b | ||
|
|
715c423f1a | ||
|
|
ca74e29458 | ||
|
|
a41955863a | ||
|
|
2ecb117082 | ||
|
|
0c88afa386 | ||
|
|
74c0fb04f3 | ||
|
|
828078eb45 | ||
|
|
7b59466667 | ||
|
|
79ac02ba74 | ||
|
|
593bd35aaa | ||
|
|
7354f10336 | ||
|
|
e8998c69a7 | ||
|
|
55bcf60ea6 | ||
|
|
796b010139 | ||
|
|
0c8a09c8e1 | ||
|
|
c1be1e4342 | ||
|
|
0c8d463307 | ||
|
|
e0fccc6c60 | ||
|
|
1f7d9a213a | ||
|
|
7ce1f73ada | ||
|
|
e605315f01 | ||
|
|
fcef49184b | ||
|
|
844ce4731e | ||
|
|
683bf2a11f | ||
|
|
eb2283a029 | ||
|
|
e8377011ab | ||
|
|
33ea2120c3 | ||
|
|
cf63669303 | ||
|
|
feacfffe89 | ||
|
|
4bb738ddd9 | ||
|
|
90e72c2a42 | ||
|
|
ccd8b7a003 | ||
|
|
d32845781e | ||
|
|
af2ceaaa0b | ||
|
|
61926bae01 | ||
|
|
ee13256f74 | ||
|
|
3b3b2f1e6e | ||
|
|
c9cbf5351c | ||
|
|
146c6e1f72 | ||
|
|
d261fa2885 | ||
|
|
5b47c00910 | ||
|
|
9e1119dab8 |
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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).
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
4
.gitignore
vendored
@@ -9,3 +9,7 @@
|
||||
/victoria-metrics-data
|
||||
/vmstorage-data
|
||||
/vmselect-cache
|
||||
/package/temp-deb-*
|
||||
/package/temp-rpm-*
|
||||
/package/*.deb
|
||||
/package/*.rpm
|
||||
|
||||
10
.travis.yml
10
.travis.yml
@@ -13,10 +13,14 @@ before_install:
|
||||
- GO111MODULE=off go get -u github.com/kisielk/errcheck
|
||||
|
||||
script:
|
||||
- make check_all
|
||||
- make check-all
|
||||
- git diff --exit-code
|
||||
- make test_full
|
||||
- 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)
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal 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
16
CONTRIBUTING.md
Normal 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.
|
||||
16
Makefile
16
Makefile
@@ -50,19 +50,25 @@ errcheck: install-errcheck
|
||||
install-errcheck:
|
||||
which errcheck || GO111MODULE=off go get -u github.com/kisielk/errcheck
|
||||
|
||||
check_all: fmt vet lint errcheck golangci-lint
|
||||
check-all: fmt vet lint errcheck golangci-lint
|
||||
|
||||
test:
|
||||
GO111MODULE=on go test -mod=vendor ./lib/...
|
||||
GO111MODULE=on go test -mod=vendor ./app/...
|
||||
GO111MODULE=on go test -tags=integration -mod=vendor ./lib/... ./app/...
|
||||
|
||||
test_full:
|
||||
GO111MODULE=on go test -mod=vendor -coverprofile=coverage.txt -covermode=atomic ./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:
|
||||
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:
|
||||
GO111MODULE=on go get -u ./lib/...
|
||||
GO111MODULE=on go get -u ./app/...
|
||||
|
||||
330
README.md
330
README.md
@@ -1,14 +1,15 @@
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
|
||||
[](http://slack.victoriametrics.com/)
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE)
|
||||
[](https://goreportcard.com/report/github.com/VictoriaMetrics/VictoriaMetrics)
|
||||
[](https://travis-ci.org/VictoriaMetrics/VictoriaMetrics)
|
||||
[](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics)
|
||||
|
||||
<img text-align="center" alt="Victoria Metrics" src="logo.png">
|
||||
<img alt="Victoria Metrics" src="logo.png">
|
||||
|
||||
## Single-node VictoriaMetrics
|
||||
|
||||
VictoriaMetrics is fast, cost-effective and scalable time series database. It can be used as 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/victoriametrics/victoria-metrics/) and
|
||||
in [source code](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
@@ -20,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.
|
||||
@@ -50,23 +53,24 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
|
||||
|
||||
### Table of contents
|
||||
|
||||
- [Single-node VictoriaMetrics](#single-node-victoriametrics)
|
||||
- [Prominent features](#prominent-features)
|
||||
- [Operation](#operation)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [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)
|
||||
- [How to start VictoriaMetrics](#how-to-start-victoriametrics)
|
||||
- [Start with docker-compose](#start-with-docker-compose)
|
||||
- [Setting up service](#setting-up-service)
|
||||
- [Third-party contributions](#third-party-contributions)
|
||||
- [Prometheus setup](#prometheus-setup)
|
||||
- [Grafana setup](#grafana-setup)
|
||||
- [How to send data from InfluxDB-compatible agents such as Telegraf?](#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf)
|
||||
- [How to send data from Graphite-compatible agents such as StatsD?](#how-to-send-data-from-graphite-compatible-agents-such-as-statsd)
|
||||
- [How to send data from OpenTSDB-compatible agents?](#how-to-send-data-from-opentsdb-compatible-agents)
|
||||
- [How to apply new config / upgrade VictoriaMetrics?](#how-to-apply-new-config--upgrade-victoriametrics)
|
||||
- [How to work with snapshots?](#how-to-work-with-snapshots)
|
||||
- [How to delete time series?](#how-to-delete-time-series)
|
||||
- [How to export time series?](#how-to-export-time-series)
|
||||
@@ -77,10 +81,12 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
|
||||
- [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)
|
||||
@@ -91,56 +97,22 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
|
||||
- [We kindly ask:](#we-kindly-ask)
|
||||
|
||||
|
||||
### How to build from sources
|
||||
|
||||
We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) or
|
||||
[docker images](https://hub.docker.com/r/victoriametrics/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 `make victoria-metrics` from the root folder of the repository.
|
||||
It will build `victoria-metrics` binary and put it into the `bin` folder.
|
||||
|
||||
#### Production build
|
||||
|
||||
1. [Install docker](https://docs.docker.com/install/).
|
||||
2. Run `make victoria-metrics-prod` from the root folder of the repository.
|
||||
It will build `victoria-metrics-prod` binary and put it into the `bin` folder.
|
||||
|
||||
#### Building docker images
|
||||
|
||||
Run `make package-victoria-metrics`. It will build `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`.
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
### 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))
|
||||
It is recommended setting up [monitoring](#monitoring) for VictoriaMetrics.
|
||||
|
||||
|
||||
### Prometheus setup
|
||||
@@ -152,7 +124,7 @@ remote_write:
|
||||
- url: http://<victoriametrics-addr>:8428/api/v1/write
|
||||
queue_config:
|
||||
max_samples_per_send: 10000
|
||||
max_shards: 100
|
||||
max_shards: 30
|
||||
```
|
||||
|
||||
Substitute `<victoriametrics-addr>` with the hostname or IP address of VictoriaMetrics.
|
||||
@@ -162,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
|
||||
@@ -177,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
|
||||
@@ -194,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.
|
||||
@@ -208,7 +206,9 @@ Do not forget substituting `<victoriametrics-addr>` with the real address where
|
||||
|
||||
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 with `{measurement}{separator}` value. `{separator}` equals to `.` by default, but can be changed with `-influxMeasurementFieldSeparator` command-line flag.
|
||||
* 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.
|
||||
|
||||
@@ -221,17 +221,18 @@ 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
|
||||
foo_field1{tag1="value1", tag2="value2"} 12
|
||||
foo_field2{tag1="value1", tag2="value2"} 40
|
||||
```
|
||||
|
||||
Example for writing data with Influx line protocol to local VictoriaMetrics using `curl`:
|
||||
Example for writing data with [Influx line protocol](https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_tutorial/)
|
||||
to local VictoriaMetrics using `curl`:
|
||||
|
||||
```
|
||||
curl -d 'measurement,tag1=value1,tag2=value2 field1=123,field2=1.23' -X POST 'http://localhost:8428/write'
|
||||
```
|
||||
|
||||
Arbitrary number of lines delimited by '\n' may be sent in a single request.
|
||||
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:
|
||||
|
||||
```
|
||||
@@ -245,6 +246,9 @@ The `/api/v1/export` endpoint should return the following response:
|
||||
{"metric":{"__name__":"measurement.field2","tag1":"value1","tag2":"value2"},"values":[1.23],"timestamps":[1560272508147]}
|
||||
```
|
||||
|
||||
Note that Influx line protocol expects [timestamps in *nanoseconds* by default](https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_tutorial/#timestamp),
|
||||
while VictoriaMetrics stores them with *milliseconds* precision.
|
||||
|
||||
|
||||
### How to send data from Graphite-compatible agents such as [StatsD](https://github.com/etsy/statsd)?
|
||||
|
||||
@@ -265,8 +269,8 @@ Example for writing data with Graphite plaintext protocol to local VictoriaMetri
|
||||
echo "foo.bar.baz;tag1=value1;tag2=value2 123 `date +%s`" | nc -N localhost 2003
|
||||
```
|
||||
|
||||
VictoriaMetrics sets the current time if timestamp is omitted.
|
||||
Arbitrary number of lines delimited by `\n` may be sent in one go.
|
||||
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:
|
||||
|
||||
```
|
||||
@@ -280,6 +284,14 @@ The `/api/v1/export` endpoint should return the following response:
|
||||
```
|
||||
|
||||
|
||||
### 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,
|
||||
@@ -298,7 +310,7 @@ Example for writing data with OpenTSDB protocol to local VictoriaMetrics using `
|
||||
echo "put foo.bar.baz `date +%s` 123 tag1=value1 tag2=value2" | nc -N localhost 4242
|
||||
```
|
||||
|
||||
Arbitrary number of lines delimited by `\n` may be sent in one go.
|
||||
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:
|
||||
|
||||
```
|
||||
@@ -312,18 +324,79 @@ The `/api/v1/export` endpoint should return the following response:
|
||||
```
|
||||
|
||||
|
||||
### How to apply new config / upgrade VictoriaMetrics?
|
||||
### How to build from sources
|
||||
|
||||
VictoriaMetrics must be restarted in order to upgrade or apply new config:
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
#### 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 is able to create [instant snapshots](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282)
|
||||
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:
|
||||
@@ -380,31 +453,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 overridden.
|
||||
`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
|
||||
@@ -429,6 +526,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:
|
||||
@@ -446,7 +547,7 @@ There is no downsampling support at the moment, but:
|
||||
- VictoriaMetrics has good compression for on-disk data. See [this article](https://medium.com/@valyala/victoriametrics-achieving-better-compression-for-time-series-data-than-gorilla-317bc1f95932)
|
||||
for details.
|
||||
|
||||
These properties reduce the need in downsampling. We plan implementing downsampling in the future.
|
||||
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.
|
||||
|
||||
|
||||
@@ -458,17 +559,23 @@ Single-node VictoriaMetrics doesn't support multi-tenancy. Use [cluster version]
|
||||
### 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.
|
||||
@@ -483,10 +590,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.
|
||||
|
||||
|
||||
@@ -496,6 +603,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
|
||||
|
||||
@@ -506,6 +628,26 @@ 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
|
||||
|
||||
@@ -516,10 +658,18 @@ Contact us with any questions regarding VictoriaMetrics at [info@victoriametrics
|
||||
|
||||
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):
|
||||
|
||||
- Prefer simple code and architecture.
|
||||
|
||||
@@ -21,7 +21,52 @@ run-victoria-metrics:
|
||||
$(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:
|
||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOARCH=arm64 GO111MODULE=on go build -mod=vendor -ldflags "$(GO_BUILDINFO)" -o bin/victoria-metrics-arm64 ./app/victoria-metrics
|
||||
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
|
||||
|
||||
281
app/victoria-metrics/main_test.go
Normal file
281
app/victoria-metrics/main_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
8
app/victoria-metrics/testdata/graphite/basic.json
vendored
Normal file
8
app/victoria-metrics/testdata/graphite/basic.json
vendored
Normal 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]}
|
||||
]
|
||||
}
|
||||
9
app/victoria-metrics/testdata/influxdb/basic.json
vendored
Normal file
9
app/victoria-metrics/testdata/influxdb/basic.json
vendored
Normal 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]}
|
||||
]
|
||||
}
|
||||
8
app/victoria-metrics/testdata/opentsdb/basic.json
vendored
Normal file
8
app/victoria-metrics/testdata/opentsdb/basic.json
vendored
Normal 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]}
|
||||
]
|
||||
}
|
||||
@@ -24,6 +24,7 @@ func ReadLinesBlock(r io.Reader, dstBuf, tailBuf []byte) ([]byte, []byte, error)
|
||||
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.
|
||||
@@ -34,7 +35,7 @@ again:
|
||||
}
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,73 @@ func (fr *failureReader) Read(p []byte) (int, error) {
|
||||
return 0, fmt.Errorf("some error")
|
||||
}
|
||||
|
||||
func TestReadLineBlockSuccessSingleByteReader(t *testing.T) {
|
||||
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()
|
||||
|
||||
@@ -87,7 +154,7 @@ func TestReadLineBlockSuccessSingleByteReader(t *testing.T) {
|
||||
f(string(b), string(b[:maxLineSize]), "")
|
||||
}
|
||||
|
||||
func TestReadLineBlockSuccessBytesBuffer(t *testing.T) {
|
||||
func TestReadLinesBlockSuccessBytesBuffer(t *testing.T) {
|
||||
f := func(s, dstBufExpected, tailBufExpected string) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -32,6 +32,17 @@ func Init() {
|
||||
func Do(f func() error) error {
|
||||
// Limit the number of conurrent f calls in order to prevent from excess
|
||||
// memory usage and CPU trashing.
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
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{}{}:
|
||||
@@ -41,9 +52,19 @@ func Do(f func() error) error {
|
||||
return err
|
||||
case <-t.C:
|
||||
timerpool.Put(t)
|
||||
concurrencyLimitErrors.Inc()
|
||||
concurrencyLimitTimeout.Inc()
|
||||
return fmt.Errorf("the server is overloaded with %d concurrent inserts; either increase -maxConcurrentInserts or reduce the load", cap(ch))
|
||||
}
|
||||
}
|
||||
|
||||
var concurrencyLimitErrors = metrics.NewCounter(`vm_concurrency_limit_errors_total`)
|
||||
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))
|
||||
})
|
||||
)
|
||||
|
||||
@@ -114,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
|
||||
@@ -121,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:]
|
||||
|
||||
@@ -14,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.
|
||||
//
|
||||
@@ -51,6 +54,7 @@ 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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -348,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,
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
measurementFieldSeparator = flag.String("influxMeasurementFieldSeparator", ".", "Separator for `{measurement}{separator}{field_name}` metric name when inserted via Influx line protocol")
|
||||
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"}`)
|
||||
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.
|
||||
//
|
||||
@@ -84,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]
|
||||
@@ -109,8 +113,10 @@ func (ctx *pushCtx) InsertRows(db string) error {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -164,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 {
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -14,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.
|
||||
//
|
||||
@@ -51,6 +54,7 @@ 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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 := ×eries[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()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,29 +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 while until giving up. This should resolve short bursts in requests.
|
||||
t := timerpool.Get(*maxQueueDuration)
|
||||
select {
|
||||
case concurrencyCh <- struct{}{}:
|
||||
timerpool.Put(t)
|
||||
defer func() { <-concurrencyCh }()
|
||||
case <-t.C:
|
||||
timerpool.Put(t)
|
||||
httpserver.Errorf(w, "cannot handle more than %d concurrent requests", cap(concurrencyCh))
|
||||
return true
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -459,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 {
|
||||
@@ -475,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
|
||||
|
||||
@@ -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"
|
||||
@@ -65,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)
|
||||
}
|
||||
@@ -73,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
|
||||
@@ -157,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)
|
||||
}
|
||||
@@ -165,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
|
||||
@@ -230,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")
|
||||
@@ -241,6 +274,50 @@ 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.
|
||||
@@ -309,11 +386,16 @@ func SeriesHandler(w http.ResponseWriter, r *http.Request) error {
|
||||
if len(matches) == 0 {
|
||||
return fmt.Errorf("missing `match[]` arg")
|
||||
}
|
||||
start, err := getTime(r, "start", ct-defaultStep)
|
||||
end, err := getTime(r, "end", ct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
end, err := getTime(r, "end", ct)
|
||||
// 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
|
||||
}
|
||||
@@ -331,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)
|
||||
}
|
||||
@@ -339,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
|
||||
@@ -426,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)
|
||||
}
|
||||
@@ -484,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")
|
||||
@@ -502,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 {
|
||||
@@ -521,21 +603,22 @@ 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, error) {
|
||||
@@ -548,20 +631,38 @@ func getTime(r *http.Request, argKey string, defaultValue int64) (int64, error)
|
||||
// Try parsing string format
|
||||
t, err := time.Parse(time.RFC3339, argValue)
|
||||
if err != nil {
|
||||
// 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 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, minTimeMsecs, maxTimeMsecs)
|
||||
if msecs < minTimeMsecs {
|
||||
msecs = 0
|
||||
}
|
||||
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
|
||||
)
|
||||
|
||||
@@ -581,7 +682,7 @@ func getDuration(r *http.Request, argKey string, defaultValue int64) (int64, err
|
||||
}
|
||||
msecs := int64(secs * 1e3)
|
||||
if msecs <= 0 || msecs > maxDurationMsecs {
|
||||
return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, 0, maxDurationMsecs)
|
||||
return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, 0, int64(maxDurationMsecs))
|
||||
}
|
||||
return msecs, nil
|
||||
}
|
||||
|
||||
78
app/vmselect/prometheus/prometheus_test.go
Normal file
78
app/vmselect/prometheus/prometheus_test.go
Normal 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")
|
||||
}
|
||||
@@ -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]
|
||||
@@ -140,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.
|
||||
@@ -268,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]
|
||||
}
|
||||
@@ -383,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)
|
||||
}
|
||||
|
||||
450
app/vmselect/promql/aggr_incremental.go
Normal file
450
app/vmselect/promql/aggr_incremental.go
Normal 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 := ×eries{
|
||||
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)
|
||||
}
|
||||
}
|
||||
187
app/vmselect/promql/aggr_incremental_test.go
Normal file
187
app/vmselect/promql/aggr_incremental_test.go
Normal 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 := ×eries{
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -413,10 +416,25 @@ func binaryOpIfnot(left, right float64) float64 {
|
||||
}
|
||||
|
||||
func binaryOpEq(left, right float64) bool {
|
||||
// Special handling for nan == nan.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/150 .
|
||||
if math.IsNaN(left) {
|
||||
return math.IsNaN(right)
|
||||
}
|
||||
|
||||
return left == right
|
||||
}
|
||||
|
||||
func binaryOpNeq(left, right float64) bool {
|
||||
// Special handling for comparison with nan.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/150 .
|
||||
if math.IsNaN(left) {
|
||||
return !math.IsNaN(right)
|
||||
}
|
||||
if math.IsNaN(right) {
|
||||
return true
|
||||
}
|
||||
|
||||
return left != right
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ 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 minimum number of points per timeseries for enabling time rounding.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -478,9 +538,21 @@ var (
|
||||
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()
|
||||
@@ -498,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
|
||||
}
|
||||
@@ -530,36 +602,18 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me
|
||||
defer rml.Put(uint64(rollupMemorySize))
|
||||
|
||||
// Evaluate rollup
|
||||
tss := make([]*timeseries, 0, rssLen*len(rcs))
|
||||
var tssLock sync.Mutex
|
||||
err = rss.RunParallel(func(rs *netstorage.Result) {
|
||||
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
|
||||
|
||||
tssLock.Lock()
|
||||
tss = append(tss, &ts)
|
||||
tssLock.Unlock()
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
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)
|
||||
rollupResultCacheV.Put(name, ec, me, iafc, window, tss)
|
||||
|
||||
return tss, nil
|
||||
}
|
||||
@@ -576,6 +630,63 @@ func getRollupMemoryLimiter() *memoryLimiter {
|
||||
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, workerID uint) {
|
||||
preFunc(rs.Values, rs.Timestamps)
|
||||
for _, rc := range rcs {
|
||||
var ts timeseries
|
||||
doRollupForTimeseries(rc, &ts, &rs.MetricName, rs.Values, rs.Timestamps, sharedTimestamps, removeMetricGroup)
|
||||
tssLock.Lock()
|
||||
tss = append(tss, &ts)
|
||||
tssLock.Unlock()
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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] {
|
||||
@@ -585,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 {
|
||||
@@ -618,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, ""))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
|
||||
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) {
|
||||
@@ -27,14 +29,16 @@ 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: duration=%s, start=%d, end=%d, step=%d, query=%q", d, ec.Start/1000, ec.End/1000, ec.Step/1000, q)
|
||||
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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -65,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 {
|
||||
@@ -122,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)`
|
||||
@@ -2134,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({})`
|
||||
@@ -2184,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)`
|
||||
@@ -2195,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"))`
|
||||
@@ -2206,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"))`
|
||||
@@ -2230,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}
|
||||
@@ -2395,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")))`
|
||||
@@ -2494,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}
|
||||
@@ -2505,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}
|
||||
@@ -2761,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}
|
||||
@@ -2783,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}
|
||||
@@ -2794,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}
|
||||
@@ -2805,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}
|
||||
@@ -2816,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}
|
||||
@@ -2827,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}
|
||||
@@ -2838,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}
|
||||
@@ -2860,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}
|
||||
@@ -3125,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()))`
|
||||
@@ -3163,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{{
|
||||
@@ -3172,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)
|
||||
@@ -3545,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)
|
||||
}
|
||||
@@ -3688,6 +3885,13 @@ func TestExecError(t *testing.T) {
|
||||
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)`)
|
||||
|
||||
@@ -149,12 +149,6 @@ func scanString(s string) (string, error) {
|
||||
}
|
||||
|
||||
func scanPositiveNumber(s string) (string, error) {
|
||||
if strings.HasPrefix(s, "Inf") {
|
||||
return "Inf", nil
|
||||
}
|
||||
if strings.HasPrefix(s, "NaN") {
|
||||
return "NaN", nil
|
||||
}
|
||||
// Scan integer part. It may be empty if fractional part exists.
|
||||
i := 0
|
||||
for i < len(s) && isDecimalChar(s[i]) {
|
||||
@@ -333,6 +327,14 @@ func scanTagFilterOpPrefix(s string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func isInfOrNaN(s string) bool {
|
||||
if len(s) != 3 {
|
||||
return false
|
||||
}
|
||||
s = strings.ToLower(s)
|
||||
return s == "inf" || s == "nan"
|
||||
}
|
||||
|
||||
func isOffset(s string) bool {
|
||||
s = strings.ToLower(s)
|
||||
return s == "offset"
|
||||
@@ -361,7 +363,7 @@ func isPositiveNumberPrefix(s string) bool {
|
||||
|
||||
// Check for .234 numbers
|
||||
if s[0] != '.' || len(s) < 2 {
|
||||
return strings.HasPrefix(s, "Inf") || strings.HasPrefix(s, "NaN")
|
||||
return false
|
||||
}
|
||||
return isDecimalChar(s[1])
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ 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)
|
||||
)`,
|
||||
|
||||
@@ -373,7 +373,7 @@ func (p *parser) parseSingleExpr() (expr, error) {
|
||||
}
|
||||
|
||||
func (p *parser) parseSingleExprWithoutRollupSuffix() (expr, error) {
|
||||
if isPositiveNumberPrefix(p.lex.Token) {
|
||||
if isPositiveNumberPrefix(p.lex.Token) || isInfOrNaN(p.lex.Token) {
|
||||
return p.parsePositiveNumberExpr()
|
||||
}
|
||||
if isStringPrefix(p.lex.Token) {
|
||||
@@ -417,7 +417,7 @@ func (p *parser) parseSingleExprWithoutRollupSuffix() (expr, error) {
|
||||
}
|
||||
|
||||
func (p *parser) parsePositiveNumberExpr() (*numberExpr, error) {
|
||||
if !isPositiveNumberPrefix(p.lex.Token) {
|
||||
if !isPositiveNumberPrefix(p.lex.Token) && !isInfOrNaN(p.lex.Token) {
|
||||
return nil, fmt.Errorf(`positiveNumberExpr: unexpected token %q; want "number"`, p.lex.Token)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -118,6 +118,10 @@ 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"}`)
|
||||
@@ -166,14 +170,34 @@ func TestParsePromQLSuccess(t *testing.T) {
|
||||
another(`-.2`, `-0.2`)
|
||||
another(`-.2E-2`, `-0.002`)
|
||||
same(`NaN`)
|
||||
another(`nan`, `NaN`)
|
||||
another(`NAN`, `NaN`)
|
||||
another(`nAN`, `NaN`)
|
||||
another(`Inf`, `+Inf`)
|
||||
another(`INF`, `+Inf`)
|
||||
another(`inf`, `+Inf`)
|
||||
another(`+Inf`, `+Inf`)
|
||||
another(`-Inf`, `-Inf`)
|
||||
another(`-inF`, `-Inf`)
|
||||
|
||||
// binaryOpExpr
|
||||
another(`NaN + 2 *3 * Inf`, `NaN`)
|
||||
another(`Inf - Inf`, `NaN`)
|
||||
another(`Inf + Inf`, `+Inf`)
|
||||
another(`nan == nan`, `NaN`)
|
||||
another(`nan ==bool nan`, `1`)
|
||||
another(`nan !=bool nan`, `0`)
|
||||
another(`nan !=bool 2`, `1`)
|
||||
another(`2 !=bool nan`, `1`)
|
||||
another(`nan >bool nan`, `0`)
|
||||
another(`nan <bool nan`, `0`)
|
||||
another(`1 ==bool nan`, `0`)
|
||||
another(`NaN !=bool 1`, `1`)
|
||||
another(`inf >=bool 2`, `1`)
|
||||
another(`-1 >bool -inf`, `1`)
|
||||
another(`-1 <bool -inf`, `0`)
|
||||
another(`nan + 2 *3 * inf`, `NaN`)
|
||||
another(`INF - Inf`, `NaN`)
|
||||
another(`Inf + inf`, `+Inf`)
|
||||
another(`1/0`, `+Inf`)
|
||||
another(`0/0`, `NaN`)
|
||||
another(`-m`, `0 - m`)
|
||||
same(`m + ignoring () n[5m]`)
|
||||
another(`M + IGNORING () N[5m]`, `M + ignoring () N[5m]`)
|
||||
@@ -345,8 +369,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)`)
|
||||
@@ -671,7 +698,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`)
|
||||
@@ -681,8 +708,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())`)
|
||||
|
||||
@@ -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,16 +38,33 @@ 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),
|
||||
"integrate": newRollupFuncOneArg(rollupIntegrate),
|
||||
"ideriv": newRollupFuncOneArg(rollupIderiv),
|
||||
"lifetime": newRollupFuncOneArg(rollupLifetime),
|
||||
"scrape_interval": newRollupFuncOneArg(rollupScrapeInterval),
|
||||
"rollup": newRollupFuncOneArg(rollupFake),
|
||||
"rollup_rate": newRollupFuncOneArg(rollupFake), // + rollupFuncsRemoveCounterResets
|
||||
"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,
|
||||
"lifetime": true,
|
||||
"scrape_interval": true,
|
||||
}
|
||||
|
||||
var rollupFuncsRemoveCounterResets = map[string]bool{
|
||||
@@ -64,6 +82,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 +139,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 +188,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 +197,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 +235,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 +288,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 +304,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 +347,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 +393,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 +451,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 +477,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 +491,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 +507,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 +523,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 +536,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 +585,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 +611,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
|
||||
@@ -512,43 +619,61 @@ func rollupDelta(rfa *rollupFuncArg) float64 {
|
||||
if len(values) == 0 {
|
||||
return nan
|
||||
}
|
||||
if len(values) == 1 {
|
||||
// Assume that the previous non-existing value was 0.
|
||||
return values[0]
|
||||
}
|
||||
prevValue = values[0]
|
||||
values = values[1:]
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nan
|
||||
// Assume that the value didn't change on the given interval.
|
||||
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
|
||||
}
|
||||
// Assume that the value didn't change on the given interval.
|
||||
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
|
||||
// Assume that the previous non-existing value was 0.
|
||||
return lastValue
|
||||
}
|
||||
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
|
||||
prevValue := rfa.prevValue
|
||||
prevTimestamp := rfa.prevTimestamp
|
||||
if math.IsNaN(prevValue) {
|
||||
if len(values) == 0 {
|
||||
if len(values) < 2 {
|
||||
// It is impossible to calculate derivative on 0 or 1 values.
|
||||
return nan
|
||||
}
|
||||
prevValue = values[0]
|
||||
@@ -557,7 +682,8 @@ func rollupDeriv(rfa *rollupFuncArg) float64 {
|
||||
timestamps = timestamps[1:]
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nan
|
||||
// Assume that the value didn't change on the given interval.
|
||||
return 0
|
||||
}
|
||||
vEnd := values[len(values)-1]
|
||||
tEnd := timestamps[len(timestamps)-1]
|
||||
@@ -567,43 +693,85 @@ 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 len(values) < 2 {
|
||||
if len(values) == 0 || math.IsNaN(rfa.prevValue) {
|
||||
// It is impossible to calculate derivative on 0 or 1 values.
|
||||
return nan
|
||||
}
|
||||
return (values[0] - rfa.prevValue) / (float64(timestamps[0]-rfa.prevTimestamp) * 1e-3)
|
||||
}
|
||||
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) {
|
||||
// Skip data points with duplicate timestamps.
|
||||
for len(timestamps) > 0 && timestamps[len(timestamps)-1] >= tEnd {
|
||||
timestamps = timestamps[:len(timestamps)-1]
|
||||
}
|
||||
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) * 1e-3)
|
||||
}
|
||||
|
||||
func rollupLifetime(rfa *rollupFuncArg) float64 {
|
||||
// Calculate the duration between the first and the last data points.
|
||||
timestamps := rfa.timestamps
|
||||
if math.IsNaN(rfa.prevValue) {
|
||||
if len(timestamps) < 2 {
|
||||
return nan
|
||||
}
|
||||
} else {
|
||||
prevValue = values[len(values)-1]
|
||||
prevTimestamp = timestamps[len(timestamps)-1]
|
||||
return float64(timestamps[len(timestamps)-1]-timestamps[0]) * 1e-3
|
||||
}
|
||||
dv := vEnd - prevValue
|
||||
dt := tEnd - prevTimestamp
|
||||
return dv / (float64(dt) / 1000)
|
||||
if len(timestamps) == 0 {
|
||||
return nan
|
||||
}
|
||||
return float64(timestamps[len(timestamps)-1]-rfa.prevTimestamp) * 1e-3
|
||||
}
|
||||
|
||||
func rollupScrapeInterval(rfa *rollupFuncArg) float64 {
|
||||
// Calculate the average interval between data points.
|
||||
timestamps := rfa.timestamps
|
||||
if math.IsNaN(rfa.prevValue) {
|
||||
if len(timestamps) < 2 {
|
||||
return nan
|
||||
}
|
||||
return float64(timestamps[len(timestamps)-1]-timestamps[0]) * 1e-3 / float64(len(timestamps)-1)
|
||||
}
|
||||
if len(timestamps) == 0 {
|
||||
return nan
|
||||
}
|
||||
return (float64(timestamps[len(timestamps)-1]-rfa.prevTimestamp) * 1e-3) / float64(len(timestamps))
|
||||
}
|
||||
|
||||
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 +783,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 +798,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 +817,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 +826,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 +858,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 +876,7 @@ func rollupIntegrate(rfa *rollupFuncArg) float64 {
|
||||
timestamps = timestamps[1:]
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nan
|
||||
return 0
|
||||
}
|
||||
|
||||
var sum float64
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
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.
|
||||
c: workingsetcache.New(1024*1024, time.Hour), // This is a cache for testing.
|
||||
}
|
||||
var rollupResultCachePath string
|
||||
|
||||
@@ -43,15 +44,17 @@ var (
|
||||
func InitRollupResultCache(cachePath string) {
|
||||
rollupResultCachePath = cachePath
|
||||
startTime := time.Now()
|
||||
var c *fastcache.Cache
|
||||
cacheSize := getRollupResultCacheSize()
|
||||
var c *workingsetcache.Cache
|
||||
if len(rollupResultCachePath) > 0 {
|
||||
logger.Infof("loading rollupResult cache from %q...", rollupResultCachePath)
|
||||
c = fastcache.LoadFromFileOrNew(rollupResultCachePath, getRollupResultCacheSize())
|
||||
c = workingsetcache.Load(rollupResultCachePath, cacheSize, time.Hour)
|
||||
} else {
|
||||
c = fastcache.New(getRollupResultCacheSize())
|
||||
c = workingsetcache.New(cacheSize, time.Hour)
|
||||
}
|
||||
if *disableCache {
|
||||
c.Reset()
|
||||
c.Stop()
|
||||
c = nil
|
||||
}
|
||||
|
||||
stats := &fastcache.Stats{}
|
||||
@@ -71,7 +74,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)
|
||||
}
|
||||
|
||||
@@ -96,25 +99,28 @@ func InitRollupResultCache(cachePath string) {
|
||||
// StopRollupResultCache closes the rollupResult cache.
|
||||
func StopRollupResultCache() {
|
||||
if len(rollupResultCachePath) == 0 {
|
||||
rollupResultCacheV.c.Reset()
|
||||
if !*disableCache {
|
||||
rollupResultCacheV.c.Stop()
|
||||
rollupResultCacheV.c = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
gomaxprocs := runtime.GOMAXPROCS(-1)
|
||||
logger.Infof("saving rollupResult cache to %q...", rollupResultCachePath)
|
||||
startTime := time.Now()
|
||||
if err := rollupResultCacheV.c.SaveToFileConcurrent(rollupResultCachePath, gomaxprocs); err != nil {
|
||||
if err := rollupResultCacheV.c.Save(rollupResultCachePath); err != nil {
|
||||
logger.Errorf("cannot close rollupResult cache at %q: %s", rollupResultCachePath, err)
|
||||
} else {
|
||||
var fcs fastcache.Stats
|
||||
rollupResultCacheV.c.UpdateStats(&fcs)
|
||||
rollupResultCacheV.c.Reset()
|
||||
logger.Infof("saved rollupResult cache to %q in %s; entriesCount: %d, bytesSize: %d",
|
||||
rollupResultCachePath, time.Since(startTime), fcs.EntriesCount, fcs.BytesSize)
|
||||
return
|
||||
}
|
||||
var fcs fastcache.Stats
|
||||
rollupResultCacheV.c.UpdateStats(&fcs)
|
||||
rollupResultCacheV.c.Stop()
|
||||
rollupResultCacheV.c = nil
|
||||
logger.Infof("saved rollupResult cache to %q in %s; entriesCount: %d, sizeBytes: %d",
|
||||
rollupResultCachePath, time.Since(startTime), fcs.EntriesCount, fcs.BytesSize)
|
||||
}
|
||||
|
||||
type rollupResultCache struct {
|
||||
c *fastcache.Cache
|
||||
c *workingsetcache.Cache
|
||||
}
|
||||
|
||||
var rollupResultCacheResets = metrics.NewCounter(`vm_cache_resets_total{type="promql/rollupResult"}`)
|
||||
@@ -125,7 +131,7 @@ func ResetRollupResultCache() {
|
||||
rollupResultCacheV.c.Reset()
|
||||
}
|
||||
|
||||
func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExpr, window int64) (tss []*timeseries, newStart int64) {
|
||||
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
|
||||
}
|
||||
@@ -134,7 +140,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
|
||||
@@ -148,15 +154,23 @@ func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExp
|
||||
return nil, ec.Start
|
||||
}
|
||||
bb.B = key.Marshal(bb.B[:0])
|
||||
resultBuf := rrc.c.GetBig(nil, bb.B)
|
||||
if len(resultBuf) == 0 {
|
||||
compressedResultBuf := resultBufPool.Get()
|
||||
defer resultBufPool.Put(compressedResultBuf)
|
||||
compressedResultBuf.B = rrc.c.GetBig(compressedResultBuf.B[:0], bb.B)
|
||||
if len(compressedResultBuf.B) == 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
|
||||
}
|
||||
tss, err := unmarshalTimeseriesFast(resultBuf)
|
||||
// Decompress into newly allocated byte slice, since tss returned from unmarshalTimeseriesFast
|
||||
// refers to the byte slice, so it cannot be returned to the resultBufPool.
|
||||
resultBuf, err := encoding.DecompressZSTD(nil, compressedResultBuf.B)
|
||||
if err != nil {
|
||||
logger.Panicf("BUG: cannot decompress resultBuf from rollupResultCache: %s; it looks like it was improperly saved", err)
|
||||
}
|
||||
tss, err = unmarshalTimeseriesFast(resultBuf)
|
||||
if err != nil {
|
||||
logger.Panicf("BUG: cannot unmarshal timeseries from rollupResultCache: %s; it looks like it was improperly saved", err)
|
||||
}
|
||||
@@ -196,7 +210,9 @@ 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) {
|
||||
var resultBufPool bytesutil.ByteBufferPool
|
||||
|
||||
func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExpr, iafc *incrementalAggrFuncContext, window int64, tss []*timeseries) {
|
||||
if *disableCache || len(tss) == 0 || !ec.mayCache() {
|
||||
return
|
||||
}
|
||||
@@ -227,11 +243,16 @@ func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExp
|
||||
|
||||
// Store tss in the cache.
|
||||
maxMarshaledSize := getRollupResultCacheSize() / 4
|
||||
tssMarshaled := marshalTimeseriesFast(tss, maxMarshaledSize, ec.Step)
|
||||
if tssMarshaled == nil {
|
||||
resultBuf := resultBufPool.Get()
|
||||
defer resultBufPool.Put(resultBuf)
|
||||
resultBuf.B = marshalTimeseriesFast(resultBuf.B[:0], tss, maxMarshaledSize, ec.Step)
|
||||
if len(resultBuf.B) == 0 {
|
||||
tooBigRollupResults.Inc()
|
||||
return
|
||||
}
|
||||
compressedResultBuf := resultBufPool.Get()
|
||||
defer resultBufPool.Put(compressedResultBuf)
|
||||
compressedResultBuf.B = encoding.CompressZSTDLevel(compressedResultBuf.B[:0], resultBuf.B, 1)
|
||||
|
||||
bb := bbPool.Get()
|
||||
defer bbPool.Put(bb)
|
||||
@@ -240,9 +261,9 @@ func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExp
|
||||
key.prefix = rollupResultCacheKeyPrefix
|
||||
key.suffix = atomic.AddUint64(&rollupResultCacheKeySuffix, 1)
|
||||
bb.B = key.Marshal(bb.B[:0])
|
||||
rrc.c.SetBig(bb.B, tssMarshaled)
|
||||
rrc.c.SetBig(bb.B, compressedResultBuf.B)
|
||||
|
||||
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 {
|
||||
@@ -270,10 +291,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 = 6
|
||||
|
||||
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)
|
||||
|
||||
@@ -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,7 +41,7 @@ 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{
|
||||
{
|
||||
@@ -44,8 +49,29 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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{
|
||||
{
|
||||
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)
|
||||
}
|
||||
@@ -67,8 +93,8 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -86,8 +112,8 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -105,8 +131,8 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -124,8 +150,8 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -143,8 +169,8 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -166,8 +192,8 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -220,10 +246,10 @@ func TestRollupResultCache(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,79 @@ 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 !math.IsNaN(n) {
|
||||
t.Fatalf("unexpected value; got %v; want %v", n, nan)
|
||||
}
|
||||
|
||||
rfa = &rollupFuncArg{
|
||||
prevTimestamp: 90,
|
||||
prevValue: 10,
|
||||
values: []float64{15},
|
||||
timestamps: []int64{100},
|
||||
}
|
||||
n = rollupIderiv(rfa)
|
||||
if n != 500 {
|
||||
t.Fatalf("unexpected value; got %v; want %v", n, 0.5)
|
||||
}
|
||||
|
||||
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 +111,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 +132,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 +146,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 +223,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 +269,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 +283,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 +344,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 +352,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 +375,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 +400,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 +420,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 +434,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 +451,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 +465,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 +479,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 +496,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 +510,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 +524,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 +538,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 +552,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 +566,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,10 +580,66 @@ 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{123, 33, -87, 0}
|
||||
timestampsExpected := []int64{10, 50, 90, 130}
|
||||
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
t.Run("lifetime", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupLifetime,
|
||||
Start: 0,
|
||||
End: 160,
|
||||
Step: 40,
|
||||
Window: 0,
|
||||
}
|
||||
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
|
||||
values := rc.Do(nil, testValues, testTimestamps)
|
||||
valuesExpected := []float64{nan, 0.031, 0.044, 0.04, 0.01}
|
||||
timestampsExpected := []int64{0, 40, 80, 120, 160}
|
||||
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
t.Run("lifetime", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupLifetime,
|
||||
Start: 0,
|
||||
End: 160,
|
||||
Step: 40,
|
||||
Window: 200,
|
||||
}
|
||||
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
|
||||
values := rc.Do(nil, testValues, testTimestamps)
|
||||
valuesExpected := []float64{nan, 0.031, 0.075, 0.115, 0.125}
|
||||
timestampsExpected := []int64{0, 40, 80, 120, 160}
|
||||
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
t.Run("scrape_interval", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupScrapeInterval,
|
||||
Start: 0,
|
||||
End: 160,
|
||||
Step: 40,
|
||||
Window: 0,
|
||||
}
|
||||
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
|
||||
values := rc.Do(nil, testValues, testTimestamps)
|
||||
valuesExpected := []float64{nan, 0.010333333333333333, 0.011, 0.013333333333333334, 0.01}
|
||||
timestampsExpected := []int64{0, 40, 80, 120, 160}
|
||||
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
t.Run("scrape_interval", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupScrapeInterval,
|
||||
Start: 0,
|
||||
End: 160,
|
||||
Step: 40,
|
||||
Window: 80,
|
||||
}
|
||||
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
|
||||
values := rc.Do(nil, testValues, testTimestamps)
|
||||
valuesExpected := []float64{nan, 0.010333333333333333, 0.010714285714285714, 0.012, 0.0125}
|
||||
timestampsExpected := []int64{0, 40, 80, 120, 160}
|
||||
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
t.Run("changes", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupChanges,
|
||||
@@ -509,10 +650,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 +678,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 +692,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 +706,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 +720,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 +734,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 +748,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 +762,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)
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
@@ -61,7 +62,21 @@ func (ts *timeseries) CopyShallow(src *timeseries) {
|
||||
ts.denyReuse = true
|
||||
}
|
||||
|
||||
func marshalTimeseriesFast(tss []*timeseries, maxSize int, step int64) []byte {
|
||||
func getTimeseries() *timeseries {
|
||||
if v := timeseriesPool.Get(); v != nil {
|
||||
return v.(*timeseries)
|
||||
}
|
||||
return ×eries{}
|
||||
}
|
||||
|
||||
func putTimeseries(ts *timeseries) {
|
||||
ts.Reset()
|
||||
timeseriesPool.Put(ts)
|
||||
}
|
||||
|
||||
var timeseriesPool sync.Pool
|
||||
|
||||
func marshalTimeseriesFast(dst []byte, tss []*timeseries, maxSize int, step int64) []byte {
|
||||
if len(tss) == 0 {
|
||||
logger.Panicf("BUG: tss cannot be empty")
|
||||
}
|
||||
@@ -77,13 +92,13 @@ func marshalTimeseriesFast(tss []*timeseries, maxSize int, step int64) []byte {
|
||||
|
||||
if size > maxSize {
|
||||
// Do not marshal tss, since it would occupy too much space
|
||||
return nil
|
||||
return dst
|
||||
}
|
||||
|
||||
// Allocate the buffer for the marshaled tss before its' marshaling.
|
||||
// This should reduce memory fragmentation and memory usage.
|
||||
dst := make([]byte, 0, size)
|
||||
dst = marshalFastTimestamps(dst, tss[0].Timestamps)
|
||||
dst = bytesutil.Resize(dst, size)
|
||||
dst = marshalFastTimestamps(dst[:0], tss[0].Timestamps)
|
||||
for _, ts := range tss {
|
||||
dst = ts.marshalFastNoTimestamps(dst)
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestTimeseriesMarshalUnmarshalFast(t *testing.T) {
|
||||
|
||||
tssOrig = append(tssOrig, &ts)
|
||||
}
|
||||
buf := marshalTimeseriesFast(tssOrig, 1e6, 123)
|
||||
buf := marshalTimeseriesFast(nil, tssOrig, 1e6, 123)
|
||||
tssGot, err := unmarshalTimeseriesFast(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("error in unmarshalTimeseriesFast: %s", err)
|
||||
|
||||
@@ -125,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()
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -211,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
|
||||
@@ -223,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
|
||||
@@ -340,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)
|
||||
})
|
||||
@@ -388,22 +429,22 @@ func registerStorageMetrics(strg *storage.Storage) {
|
||||
})
|
||||
|
||||
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().UselessTagFiltersCacheBytesSize)
|
||||
return float64(idbm().UselessTagFiltersCacheSizeBytes)
|
||||
})
|
||||
|
||||
metrics.NewGauge(`vm_cache_requests_total{type="storage/tsid"}`, func() float64 {
|
||||
|
||||
@@ -60,12 +60,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Overview for single node VictoriaMetrics",
|
||||
"description": "Overview for single node VictoriaMetrics v1.22.2 or higher",
|
||||
"editable": true,
|
||||
"gnetId": 10229,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"iteration": 1559857416920,
|
||||
"iteration": 1563651131627,
|
||||
"links": [
|
||||
{
|
||||
"icon": "doc",
|
||||
@@ -668,7 +668,7 @@
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"description": "* `*` - unsupported query path\n* `/write` - insert into VM\n* `/metrics` - query VM system metrics\n* `/query` - query instant values\n* `/query_range` - query over a range of time\n* `/series` - match a certain label set\n* `/label/{}/values` - query a list of label values (variables mostly)",
|
||||
"description": "Shows the number of active time series with new data points inserted during the last hour. High value may result in ingestion slowdown. \n\nSee following link for details:",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
@@ -676,23 +676,27 @@
|
||||
"x": 0,
|
||||
"y": 19
|
||||
},
|
||||
"id": 35,
|
||||
"id": 51,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"sort": "current",
|
||||
"sortDesc": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null as zero",
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "troubleshooting",
|
||||
"type": "absolute",
|
||||
"url": "https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md#troubleshooting"
|
||||
}
|
||||
],
|
||||
"nullPointMode": "null",
|
||||
"options": {},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
@@ -704,10 +708,10 @@
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(vm_http_request_errors_total{job=\"$job\"}[$__interval])) by (path) > 0",
|
||||
"expr": "vm_cache_entries{job=\"$job\", type=\"storage/hour_metric_ids\"}",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{path}}",
|
||||
"legendFormat": "Active time series",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
@@ -715,10 +719,10 @@
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Requests error rate",
|
||||
"title": "Active time series",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 2,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
@@ -859,7 +863,7 @@
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"description": "",
|
||||
"description": "* `*` - unsupported query path\n* `/write` - insert into VM\n* `/metrics` - query VM system metrics\n* `/query` - query instant values\n* `/query_range` - query over a range of time\n* `/series` - match a certain label set\n* `/label/{}/values` - query a list of label values (variables mostly)",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
@@ -867,20 +871,23 @@
|
||||
"x": 0,
|
||||
"y": 27
|
||||
},
|
||||
"id": 37,
|
||||
"id": 35,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": false,
|
||||
"show": true,
|
||||
"sort": "current",
|
||||
"sortDesc": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"nullPointMode": "null as zero",
|
||||
"options": {},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
@@ -892,11 +899,10 @@
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(vm_tcplistener_conns{job=\"$job\"})",
|
||||
"expr": "sum(rate(vm_http_request_errors_total{job=\"$job\"}[$__interval])) by (path) > 0",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "connections",
|
||||
"legendFormat": "{{path}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
@@ -904,10 +910,10 @@
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "TCP connections",
|
||||
"title": "Requests error rate",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"sort": 2,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
@@ -920,7 +926,6 @@
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"decimals": null,
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
@@ -1031,13 +1036,102 @@
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"description": "",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 35
|
||||
},
|
||||
"id": 37,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": false,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"options": {},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(vm_tcplistener_conns{job=\"$job\"})",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "connections",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "TCP connections",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"decimals": null,
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 35
|
||||
"y": 43
|
||||
},
|
||||
"id": 14,
|
||||
"panels": [],
|
||||
@@ -1056,7 +1150,7 @@
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 36
|
||||
"y": 44
|
||||
},
|
||||
"id": 10,
|
||||
"legend": {
|
||||
@@ -1152,7 +1246,7 @@
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 36
|
||||
"y": 44
|
||||
},
|
||||
"id": 34,
|
||||
"legend": {
|
||||
@@ -1254,7 +1348,7 @@
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 44
|
||||
"y": 52
|
||||
},
|
||||
"id": 30,
|
||||
"legend": {
|
||||
@@ -1341,7 +1435,7 @@
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 44
|
||||
"y": 52
|
||||
},
|
||||
"id": 36,
|
||||
"legend": {
|
||||
@@ -1416,17 +1510,190 @@
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"description": "Shows amount of on-disk space occupied by data points.",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 60
|
||||
},
|
||||
"id": 53,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"options": {},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(vm_data_size_bytes{job=\"$job\", type!=\"indexdb\"})",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Size",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Disk space usage (datapoints)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "bytes",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"description": "Shows amount of on-disk space occupied by inverted index.",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 60
|
||||
},
|
||||
"id": 55,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"options": {},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "vm_data_size_bytes{job=\"$job\", type=\"indexdb\"}",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Disk space usage (index)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "bytes",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 52
|
||||
"y": 68
|
||||
},
|
||||
"id": 46,
|
||||
"panels": [],
|
||||
"title": "Go runtime",
|
||||
"title": "Resource usage",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
@@ -1441,7 +1708,7 @@
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 53
|
||||
"y": 69
|
||||
},
|
||||
"id": 44,
|
||||
"legend": {
|
||||
@@ -1538,21 +1805,20 @@
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 53
|
||||
"y": 69
|
||||
},
|
||||
"id": 42,
|
||||
"id": 57,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
@@ -1571,10 +1837,10 @@
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(go_gc_duration_seconds_sum{job=\"$job\"}[2m]))",
|
||||
"expr": "rate(process_cpu_seconds_total{job=\"$job\"}[5m])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "gc duration",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Rate 5m",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
@@ -1582,7 +1848,7 @@
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "GC duration",
|
||||
"title": "CPU",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
@@ -1598,7 +1864,7 @@
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "s",
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
@@ -1630,7 +1896,7 @@
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 61
|
||||
"y": 77
|
||||
},
|
||||
"id": 47,
|
||||
"legend": {
|
||||
@@ -1717,7 +1983,93 @@
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 61
|
||||
"y": 77
|
||||
},
|
||||
"id": 42,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": false,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"options": {},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(go_gc_duration_seconds_sum{job=\"$job\"}[2m]))",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "gc duration",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "GC duration",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "s",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 85
|
||||
},
|
||||
"id": 48,
|
||||
"legend": {
|
||||
@@ -1744,7 +2096,7 @@
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(go_threads{job=\"$job\"})",
|
||||
"expr": "sum(process_num_threads{job=\"$job\"})",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "threads",
|
||||
@@ -1878,5 +2230,5 @@
|
||||
"timezone": "",
|
||||
"title": "VictoriaMetrics",
|
||||
"uid": "wNf0q_kZk",
|
||||
"version": 1
|
||||
"version": 2
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
DOCKER_NAMESPACE := victoriametrics
|
||||
BUILDER_IMAGE := local/builder:go1.12.6
|
||||
BUILDER_IMAGE := local/builder:go1.12.9
|
||||
CERTS_IMAGE := local/certs:1.0.2
|
||||
|
||||
package-certs:
|
||||
@@ -19,8 +19,9 @@ app-via-docker: package-certs package-builder
|
||||
--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)') || (\
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
FROM golang:1.12.6
|
||||
FROM golang:1.12.9
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
8
go.mod
8
go.mod
@@ -2,15 +2,17 @@ module github.com/VictoriaMetrics/VictoriaMetrics
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/fastcache v1.5.1
|
||||
github.com/VictoriaMetrics/metrics v1.5.0
|
||||
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.6.0
|
||||
github.com/valyala/histogram v1.0.1
|
||||
github.com/valyala/quicktemplate v1.1.1
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f
|
||||
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa
|
||||
)
|
||||
|
||||
go 1.12
|
||||
|
||||
17
go.sum
17
go.sum
@@ -3,8 +3,8 @@ 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.1 h1:qHgHjyoNFV7jgucU8QZUuU4gcdhfs8QW1kw68OD2Lag=
|
||||
github.com/VictoriaMetrics/fastcache v1.5.1/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE=
|
||||
github.com/VictoriaMetrics/metrics v1.5.0 h1:WvQqPn+z9pR1U7J58CgaGiWrN8phNGSpr2xUSxJnfpE=
|
||||
github.com/VictoriaMetrics/metrics v1.5.0/go.mod h1:QZAL5yLaXvhSPeib0ahluGo9VK0HXDZHovKaKlpuWvs=
|
||||
github.com/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.6.0 h1:34qKK75C6Dx9zof2JqUiunfJQ87Up6vTHXABWDyCH+g=
|
||||
github.com/valyala/gozstd v1.6.0/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||
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-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding/zstd"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/valyala/gozstd"
|
||||
)
|
||||
|
||||
// CompressZSTDLevel appends compressed src to dst and returns
|
||||
@@ -13,7 +13,7 @@ func CompressZSTDLevel(dst, src []byte, compressLevel int) []byte {
|
||||
compressCalls.Inc()
|
||||
originalBytes.Add(len(src))
|
||||
dstLen := len(dst)
|
||||
dst = gozstd.CompressLevel(dst, src, compressLevel)
|
||||
dst = zstd.CompressLevel(dst, src, compressLevel)
|
||||
compressedBytes.Add(len(dst) - dstLen)
|
||||
return dst
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func CompressZSTDLevel(dst, src []byte, compressLevel int) []byte {
|
||||
// the appended dst.
|
||||
func DecompressZSTD(dst, src []byte) ([]byte, error) {
|
||||
decompressCalls.Inc()
|
||||
return gozstd.Decompress(dst, src)
|
||||
return zstd.Decompress(dst, src)
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
83
lib/encoding/encoding_cgo_test.go
Normal file
83
lib/encoding/encoding_cgo_test.go
Normal 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)
|
||||
}
|
||||
83
lib/encoding/encoding_pure_test.go
Normal file
83
lib/encoding/encoding_pure_test.go
Normal 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)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
19
lib/encoding/zstd/zstd_cgo.go
Normal file
19
lib/encoding/zstd/zstd_cgo.go
Normal 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)
|
||||
}
|
||||
78
lib/encoding/zstd/zstd_pure.go
Normal file
78
lib/encoding/zstd/zstd_pure.go
Normal 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
|
||||
}
|
||||
96
lib/encoding/zstd/zstd_test.go
Normal file
96
lib/encoding/zstd/zstd_test.go
Normal 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)
|
||||
}
|
||||
66
lib/fs/fs.go
66
lib/fs/fs.go
@@ -5,12 +5,15 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/filestream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ReadAtCloser is rand-access read interface.
|
||||
@@ -87,26 +90,42 @@ func MustSyncPath(path string) {
|
||||
}
|
||||
}
|
||||
|
||||
// WriteFile writes data to the given file path.
|
||||
var tmpFileNum uint64
|
||||
|
||||
// WriteFileAtomically atomically writes data to the given file path.
|
||||
//
|
||||
// WriteFile returns only after the file is fully written
|
||||
// WriteFile returns only after the file is fully written and synced
|
||||
// to the underlying storage.
|
||||
func WriteFile(path string, data []byte) error {
|
||||
func WriteFileAtomically(path string, data []byte) error {
|
||||
// Check for the existing file. It is expected that
|
||||
// the WriteFileAtomically function cannot be called concurrently
|
||||
// with the same `path`.
|
||||
if IsPathExist(path) {
|
||||
return fmt.Errorf("cannot create file %q, since it already exists", path)
|
||||
}
|
||||
f, err := filestream.Create(path, false)
|
||||
|
||||
n := atomic.AddUint64(&tmpFileNum, 1)
|
||||
tmpPath := fmt.Sprintf("%s.tmp.%d", path, n)
|
||||
f, err := filestream.Create(tmpPath, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create file %q: %s", path, err)
|
||||
return fmt.Errorf("cannot create file %q: %s", tmpPath, err)
|
||||
}
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.MustClose()
|
||||
return fmt.Errorf("cannot write %d bytes to file %q: %s", len(data), path, err)
|
||||
MustRemoveAll(tmpPath)
|
||||
return fmt.Errorf("cannot write %d bytes to file %q: %s", len(data), tmpPath, err)
|
||||
}
|
||||
|
||||
// Sync and close the file.
|
||||
f.MustClose()
|
||||
|
||||
// Atomically move the file from tmpPath to path.
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
// do not call MustRemoveAll(tmpPath) here, so the user could inspect
|
||||
// the file contents during investigating the issue.
|
||||
return fmt.Errorf("cannot move %q to %q: %s", tmpPath, path, err)
|
||||
}
|
||||
|
||||
// Sync the containing directory, so the file is guaranteed to appear in the directory.
|
||||
// See https://www.quora.com/When-should-you-fsync-the-containing-directory-in-addition-to-the-file-itself
|
||||
absPath, err := filepath.Abs(path)
|
||||
@@ -119,6 +138,15 @@ func WriteFile(path string, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTemporaryFileName returns true if fn matches temporary file name pattern
|
||||
// from WriteFileAtomically.
|
||||
func IsTemporaryFileName(fn string) bool {
|
||||
return tmpFileNameRe.MatchString(fn)
|
||||
}
|
||||
|
||||
// tmpFileNameRe is regexp for temporary file name - see WriteFileAtomically for details.
|
||||
var tmpFileNameRe = regexp.MustCompile(`\.tmp\.\d+$`)
|
||||
|
||||
// MkdirAllIfNotExist creates the given path dir if it isn't exist.
|
||||
func MkdirAllIfNotExist(path string) error {
|
||||
if IsPathExist(path) {
|
||||
@@ -185,6 +213,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 {
|
||||
@@ -346,3 +386,17 @@ func MustWriteData(w io.Writer, data []byte) {
|
||||
logger.Panicf("BUG: writer wrote %d bytes instead of %d bytes", n, len(data))
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFlockFile creates flock.lock file in the directory dir
|
||||
// and returns the handler to the file.
|
||||
func CreateFlockFile(dir string) (*os.File, error) {
|
||||
flockFile := dir + "/flock.lock"
|
||||
flockF, err := os.Create(flockFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create lock file %q: %s", flockFile, err)
|
||||
}
|
||||
if err := unix.Flock(int(flockF.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
||||
return nil, fmt.Errorf("cannot acquire lock on file %q: %s", flockFile, err)
|
||||
}
|
||||
return flockF, nil
|
||||
}
|
||||
|
||||
24
lib/fs/fs_test.go
Normal file
24
lib/fs/fs_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsTemporaryFileName(t *testing.T) {
|
||||
f := func(s string, resultExpected bool) {
|
||||
t.Helper()
|
||||
result := IsTemporaryFileName(s)
|
||||
if result != resultExpected {
|
||||
t.Fatalf("unexpected IsTemporaryFileName(%q); got %v; want %v", s, result, resultExpected)
|
||||
}
|
||||
}
|
||||
f("", false)
|
||||
f(".", false)
|
||||
f(".tmp", false)
|
||||
f("tmp.123", false)
|
||||
f(".tmp.123.xx", false)
|
||||
f(".tmp.1", true)
|
||||
f("asdf.dff.tmp.123", true)
|
||||
f("asdf.sdfds.tmp.dfd", false)
|
||||
f("dfd.sdfds.dfds.1232", false)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -164,7 +164,7 @@ func (ph *partHeader) WriteMetadata(partPath string) error {
|
||||
return fmt.Errorf("cannot marshal metadata: %s", err)
|
||||
}
|
||||
metadataPath := partPath + "/metadata.json"
|
||||
if err := fs.WriteFile(metadataPath, metadata); err != nil {
|
||||
if err := fs.WriteFileAtomically(metadataPath, metadata); err != nil {
|
||||
return fmt.Errorf("cannot create %q: %s", metadataPath, err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -134,13 +134,9 @@ func OpenTable(path string) (*Table, error) {
|
||||
}
|
||||
|
||||
// Protect from concurrent opens.
|
||||
flockFile := path + "/flock.lock"
|
||||
flockF, err := os.Create(flockFile)
|
||||
flockF, err := fs.CreateFlockFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create lock file %q: %s", flockFile, err)
|
||||
}
|
||||
if err := unix.Flock(int(flockF.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
||||
return nil, fmt.Errorf("cannot acquire lock on file %q: %s", flockFile, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open table parts.
|
||||
@@ -161,8 +157,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
|
||||
}
|
||||
@@ -242,6 +238,7 @@ type TableMetrics struct {
|
||||
|
||||
BlocksCount uint64
|
||||
ItemsCount uint64
|
||||
SizeBytes uint64
|
||||
|
||||
DataBlocksCacheSize uint64
|
||||
DataBlocksCacheRequests uint64
|
||||
@@ -274,6 +271,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()
|
||||
@@ -713,7 +711,7 @@ func (tb *Table) mergeParts(pws []*partWrapper, stopCh <-chan struct{}, isOuterP
|
||||
dstPartPath := ph.Path(tb.path, mergeIdx)
|
||||
fmt.Fprintf(&bb, "%s -> %s\n", tmpPartPath, dstPartPath)
|
||||
txnPath := fmt.Sprintf("%s/txn/%016X", tb.path, mergeIdx)
|
||||
if err := fs.WriteFile(txnPath, bb.B); err != nil {
|
||||
if err := fs.WriteFileAtomically(txnPath, bb.B); err != nil {
|
||||
return fmt.Errorf("cannot create transaction file %q: %s", txnPath, err)
|
||||
}
|
||||
|
||||
@@ -727,6 +725,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 +760,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
|
||||
@@ -991,7 +990,12 @@ func runTransactions(txnLock *sync.RWMutex, path string) error {
|
||||
})
|
||||
|
||||
for _, fi := range fis {
|
||||
txnPath := txnDir + "/" + fi.Name()
|
||||
fn := fi.Name()
|
||||
if fs.IsTemporaryFileName(fn) {
|
||||
// Skip temporary files, which could be left after unclean shutdown.
|
||||
continue
|
||||
}
|
||||
txnPath := txnDir + "/" + fn
|
||||
if err := runTransaction(txnLock, path, txnPath); err != nil {
|
||||
return fmt.Errorf("cannot run transaction from %q: %s", txnPath, err)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -205,19 +205,6 @@ func (b *Block) MarshalData(timestampsBlockOffset, valuesBlockOffset uint64) ([]
|
||||
b.bh.ValuesBlockSize = uint32(len(b.valuesData))
|
||||
b.values = b.values[:0]
|
||||
|
||||
if len(timestamps) > 1 && (b.bh.ValuesMarshalType == encoding.MarshalTypeConst || b.bh.ValuesMarshalType == encoding.MarshalTypeDeltaConst) {
|
||||
// Special case - values are constant or are changed with constant rate.
|
||||
// In this case we may 'cheat' by assuming timestamps are changed
|
||||
// at ideal constant rate. This improves timestamps' compression rate.
|
||||
minTimestamp := timestamps[0]
|
||||
maxTimestamp := timestamps[len(timestamps)-1]
|
||||
delta := (maxTimestamp - minTimestamp) / int64(len(timestamps)-1)
|
||||
ts := minTimestamp
|
||||
for i := 1; i < len(timestamps); i++ {
|
||||
ts += delta
|
||||
timestamps[i] = ts
|
||||
}
|
||||
}
|
||||
b.timestampsData, b.bh.TimestampsMarshalType, b.bh.MinTimestamp = encoding.MarshalTimestamps(b.timestampsData[:0], timestamps, b.bh.PrecisionBits)
|
||||
b.bh.TimestampsBlockOffset = timestampsBlockOffset
|
||||
b.bh.TimestampsBlockSize = uint32(len(b.timestampsData))
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/mergeset"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
xxhash "github.com/cespare/xxhash/v2"
|
||||
)
|
||||
@@ -52,17 +53,17 @@ type indexDB struct {
|
||||
extDBLock sync.Mutex
|
||||
|
||||
// Cache for fast TagFilters -> TSIDs lookup.
|
||||
tagCache *fastcache.Cache
|
||||
tagCache *workingsetcache.Cache
|
||||
|
||||
// Cache for fast MetricID -> TSID lookup.
|
||||
metricIDCache *fastcache.Cache
|
||||
metricIDCache *workingsetcache.Cache
|
||||
|
||||
// Cache for fast MetricID -> MetricName lookup.
|
||||
metricNameCache *fastcache.Cache
|
||||
metricNameCache *workingsetcache.Cache
|
||||
|
||||
// Cache holding useless TagFilters entries, which have no tag filters
|
||||
// matching low number of metrics.
|
||||
uselessTagFiltersCache *fastcache.Cache
|
||||
uselessTagFiltersCache *workingsetcache.Cache
|
||||
|
||||
indexSearchPool sync.Pool
|
||||
|
||||
@@ -101,7 +102,20 @@ type indexDB struct {
|
||||
}
|
||||
|
||||
// openIndexDB opens index db from the given path with the given caches.
|
||||
func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache, currHourMetricIDs, prevHourMetricIDs *atomic.Value) (*indexDB, error) {
|
||||
func openIndexDB(path string, metricIDCache, metricNameCache *workingsetcache.Cache, currHourMetricIDs, prevHourMetricIDs *atomic.Value) (*indexDB, error) {
|
||||
if metricIDCache == nil {
|
||||
logger.Panicf("BUG: metricIDCache must be non-nil")
|
||||
}
|
||||
if metricNameCache == nil {
|
||||
logger.Panicf("BUG: metricNameCache must be non-nil")
|
||||
}
|
||||
if currHourMetricIDs == nil {
|
||||
logger.Panicf("BUG: currHourMetricIDs must be non-nil")
|
||||
}
|
||||
if prevHourMetricIDs == nil {
|
||||
logger.Panicf("BUG: prevHourMetricIDs must be non-nil")
|
||||
}
|
||||
|
||||
tb, err := mergeset.OpenTable(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open indexDB %q: %s", path, err)
|
||||
@@ -111,18 +125,16 @@ func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache, c
|
||||
|
||||
// Do not persist tagCache in files, since it is very volatile.
|
||||
mem := memory.Allowed()
|
||||
tagCache := fastcache.New(mem / 32)
|
||||
|
||||
db := &indexDB{
|
||||
refCount: 1,
|
||||
tb: tb,
|
||||
name: name,
|
||||
|
||||
tagCache: tagCache,
|
||||
metricIDCache: metricIDCache,
|
||||
metricNameCache: metricNameCache,
|
||||
|
||||
uselessTagFiltersCache: fastcache.New(mem / 128),
|
||||
tagCache: workingsetcache.New(mem/32, time.Hour),
|
||||
metricIDCache: metricIDCache,
|
||||
metricNameCache: metricNameCache,
|
||||
uselessTagFiltersCache: workingsetcache.New(mem/128, time.Hour),
|
||||
|
||||
currHourMetricIDs: currHourMetricIDs,
|
||||
prevHourMetricIDs: prevHourMetricIDs,
|
||||
@@ -142,12 +154,12 @@ func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache, c
|
||||
// IndexDBMetrics contains essential metrics for indexDB.
|
||||
type IndexDBMetrics struct {
|
||||
TagCacheSize uint64
|
||||
TagCacheBytesSize uint64
|
||||
TagCacheSizeBytes uint64
|
||||
TagCacheRequests uint64
|
||||
TagCacheMisses uint64
|
||||
|
||||
UselessTagFiltersCacheSize uint64
|
||||
UselessTagFiltersCacheBytesSize uint64
|
||||
UselessTagFiltersCacheSizeBytes uint64
|
||||
UselessTagFiltersCacheRequests uint64
|
||||
UselessTagFiltersCacheMisses uint64
|
||||
|
||||
@@ -176,14 +188,14 @@ func (db *indexDB) UpdateMetrics(m *IndexDBMetrics) {
|
||||
cs.Reset()
|
||||
db.tagCache.UpdateStats(&cs)
|
||||
m.TagCacheSize += cs.EntriesCount
|
||||
m.TagCacheBytesSize += cs.BytesSize
|
||||
m.TagCacheSizeBytes += cs.BytesSize
|
||||
m.TagCacheRequests += cs.GetBigCalls
|
||||
m.TagCacheMisses += cs.Misses
|
||||
|
||||
cs.Reset()
|
||||
db.uselessTagFiltersCache.UpdateStats(&cs)
|
||||
m.UselessTagFiltersCacheSize += cs.EntriesCount
|
||||
m.UselessTagFiltersCacheBytesSize += cs.BytesSize
|
||||
m.UselessTagFiltersCacheSizeBytes += cs.BytesSize
|
||||
m.UselessTagFiltersCacheRequests += cs.GetBigCalls
|
||||
m.UselessTagFiltersCacheMisses += cs.Misses
|
||||
|
||||
@@ -261,6 +273,15 @@ func (db *indexDB) decRef() {
|
||||
db.tb.MustClose()
|
||||
db.SetExtDB(nil)
|
||||
|
||||
// Free space occupied by caches owned by db.
|
||||
db.tagCache.Stop()
|
||||
db.uselessTagFiltersCache.Stop()
|
||||
|
||||
db.tagCache = nil
|
||||
db.metricIDCache = nil
|
||||
db.metricNameCache = nil
|
||||
db.uselessTagFiltersCache = nil
|
||||
|
||||
if atomic.LoadUint64(&db.mustDrop) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -271,20 +292,36 @@ func (db *indexDB) decRef() {
|
||||
}
|
||||
|
||||
func (db *indexDB) getFromTagCache(key []byte) ([]TSID, bool) {
|
||||
value := db.tagCache.GetBig(nil, key)
|
||||
if len(value) == 0 {
|
||||
compressedBuf := tagBufPool.Get()
|
||||
defer tagBufPool.Put(compressedBuf)
|
||||
compressedBuf.B = db.tagCache.GetBig(compressedBuf.B[:0], key)
|
||||
if len(compressedBuf.B) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
tsids, err := unmarshalTSIDs(nil, value)
|
||||
buf := tagBufPool.Get()
|
||||
defer tagBufPool.Put(buf)
|
||||
var err error
|
||||
buf.B, err = encoding.DecompressZSTD(buf.B[:0], compressedBuf.B)
|
||||
if err != nil {
|
||||
logger.Panicf("FATAL: cannot decompress tsids from tagCache: %s", err)
|
||||
}
|
||||
tsids, err := unmarshalTSIDs(nil, buf.B)
|
||||
if err != nil {
|
||||
logger.Panicf("FATAL: cannot unmarshal tsids from tagCache: %s", err)
|
||||
}
|
||||
return tsids, true
|
||||
}
|
||||
|
||||
var tagBufPool bytesutil.ByteBufferPool
|
||||
|
||||
func (db *indexDB) putToTagCache(tsids []TSID, key []byte) {
|
||||
value := marshalTSIDs(nil, tsids)
|
||||
db.tagCache.SetBig(key, value)
|
||||
buf := tagBufPool.Get()
|
||||
buf.B = marshalTSIDs(buf.B[:0], tsids)
|
||||
compressedBuf := tagBufPool.Get()
|
||||
compressedBuf.B = encoding.CompressZSTDLevel(compressedBuf.B[:0], buf.B, 1)
|
||||
tagBufPool.Put(buf)
|
||||
db.tagCache.SetBig(key, compressedBuf.B)
|
||||
tagBufPool.Put(compressedBuf)
|
||||
}
|
||||
|
||||
func (db *indexDB) getFromMetricIDCache(dst *TSID, metricID uint64) error {
|
||||
@@ -321,8 +358,11 @@ func (db *indexDB) putMetricNameToCache(metricID uint64, metricName []byte) {
|
||||
db.metricNameCache.Set(key[:], metricName)
|
||||
}
|
||||
|
||||
func marshalTagFiltersKeyVersioned(dst []byte, tfss []*TagFilters) []byte {
|
||||
prefix := atomic.LoadUint64(&tagFiltersKeyGen)
|
||||
func marshalTagFiltersKey(dst []byte, tfss []*TagFilters, versioned bool) []byte {
|
||||
prefix := ^uint64(0)
|
||||
if versioned {
|
||||
prefix = atomic.LoadUint64(&tagFiltersKeyGen)
|
||||
}
|
||||
dst = encoding.MarshalUint64(dst, prefix)
|
||||
for _, tfs := range tfss {
|
||||
dst = append(dst, 0) // separator between tfs groups.
|
||||
@@ -920,7 +960,7 @@ func (db *indexDB) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int)
|
||||
tfKeyBuf := tagFiltersKeyBufPool.Get()
|
||||
defer tagFiltersKeyBufPool.Put(tfKeyBuf)
|
||||
|
||||
tfKeyBuf.B = marshalTagFiltersKeyVersioned(tfKeyBuf.B[:0], tfss)
|
||||
tfKeyBuf.B = marshalTagFiltersKey(tfKeyBuf.B[:0], tfss, true)
|
||||
tsids, ok := db.getFromTagCache(tfKeyBuf.B)
|
||||
if ok {
|
||||
// Fast path - tsids found in the cache.
|
||||
@@ -937,7 +977,12 @@ func (db *indexDB) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int)
|
||||
|
||||
var extTSIDs []TSID
|
||||
if db.doExtDB(func(extDB *indexDB) {
|
||||
tsids, ok := extDB.getFromTagCache(tfKeyBuf.B)
|
||||
tfKeyExtBuf := tagFiltersKeyBufPool.Get()
|
||||
defer tagFiltersKeyBufPool.Put(tfKeyExtBuf)
|
||||
|
||||
// Data in extDB cannot be changed, so use unversioned keys for tag cache.
|
||||
tfKeyExtBuf.B = marshalTagFiltersKey(tfKeyExtBuf.B[:0], tfss, false)
|
||||
tsids, ok := extDB.getFromTagCache(tfKeyExtBuf.B)
|
||||
if ok {
|
||||
extTSIDs = tsids
|
||||
return
|
||||
@@ -946,8 +991,8 @@ func (db *indexDB) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int)
|
||||
extTSIDs, err = is.searchTSIDs(tfss, tr, maxMetrics)
|
||||
extDB.putIndexSearch(is)
|
||||
|
||||
// Do not store found tsids into extDB.tagCache,
|
||||
// since they will be stored into outer cache instead.
|
||||
sort.Slice(extTSIDs, func(i, j int) bool { return extTSIDs[i].Less(&extTSIDs[j]) })
|
||||
extDB.putToTagCache(extTSIDs, tfKeyExtBuf.B)
|
||||
}) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1057,6 +1102,15 @@ func mergeTSIDs(a, b []TSID) []TSID {
|
||||
}
|
||||
|
||||
func (is *indexSearch) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int) ([]TSID, error) {
|
||||
// Verify whether `is` contains data for the given tr.
|
||||
ok, err := is.containsTimeRange(tr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in containsTimeRange(%s): %s", &tr, err)
|
||||
}
|
||||
if !ok {
|
||||
// Fast path: nothing to search.
|
||||
return nil, nil
|
||||
}
|
||||
metricIDs, err := is.searchMetricIDs(tfss, tr, maxMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1184,8 +1238,9 @@ func (is *indexSearch) updateMetricIDsByMetricNameMatch(metricIDs, srcMetricIDs
|
||||
|
||||
func (is *indexSearch) getTagFilterWithMinMetricIDsCountAdaptive(tfs *TagFilters, maxMetrics int) (*tagFilter, map[uint64]struct{}, error) {
|
||||
kb := &is.kb
|
||||
kb.B = tfs.marshal(kb.B[:0])
|
||||
kb.B = append(kb.B[:0], uselessMultiTagFiltersKeyPrefix)
|
||||
kb.B = encoding.MarshalUint64(kb.B, uint64(maxMetrics))
|
||||
kb.B = tfs.marshal(kb.B)
|
||||
if len(is.db.uselessTagFiltersCache.Get(nil, kb.B)) > 0 {
|
||||
// Skip useless work below, since the tfs doesn't contain tag filters matching less than maxMetrics metrics.
|
||||
return nil, nil, errTooManyMetrics
|
||||
@@ -1199,20 +1254,23 @@ func (is *indexSearch) getTagFilterWithMinMetricIDsCountAdaptive(tfs *TagFilters
|
||||
}
|
||||
for {
|
||||
minTf, minMetricIDs, err := is.getTagFilterWithMinMetricIDsCount(tfs, maxAllowedMetrics)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(minMetricIDs) < maxAllowedMetrics {
|
||||
// Found the tag filter with the minimum number of metrics.
|
||||
return minTf, minMetricIDs, nil
|
||||
if err != errTooManyMetrics {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(minMetricIDs) < maxAllowedMetrics {
|
||||
// Found the tag filter with the minimum number of metrics.
|
||||
return minTf, minMetricIDs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Too many metrics matched.
|
||||
if maxAllowedMetrics >= maxMetrics {
|
||||
// The tag filter with minimum matching metrics matches at least maxMetrics metrics.
|
||||
kb.B = tfs.marshal(kb.B[:0])
|
||||
kb.B = append(kb.B[:0], uselessMultiTagFiltersKeyPrefix)
|
||||
kb.B = encoding.MarshalUint64(kb.B, uint64(maxMetrics))
|
||||
is.db.uselessTagFiltersCache.Set(kb.B, []byte("1"))
|
||||
kb.B = tfs.marshal(kb.B)
|
||||
is.db.uselessTagFiltersCache.Set(kb.B, uselessTagFilterCacheValue)
|
||||
return nil, nil, errTooManyMetrics
|
||||
}
|
||||
|
||||
@@ -1226,15 +1284,50 @@ func (is *indexSearch) getTagFilterWithMinMetricIDsCountAdaptive(tfs *TagFilters
|
||||
|
||||
var errTooManyMetrics = errors.New("all the tag filters match too many metrics")
|
||||
|
||||
const maxDaysForDateMetricIDs = 40
|
||||
|
||||
func (is *indexSearch) adjustMaxMetricsAdaptive(tr TimeRange, maxMetrics int) int {
|
||||
minDate := uint64(tr.MinTimestamp) / msecPerDay
|
||||
maxDate := uint64(tr.MaxTimestamp) / msecPerDay
|
||||
if maxDate-minDate > maxDaysForDateMetricIDs {
|
||||
// Cannot reduce maxMetrics for the given time range,
|
||||
// since the it is expensive extracting metricIDs for the given tr.
|
||||
return maxMetrics
|
||||
}
|
||||
hmPrev := is.db.prevHourMetricIDs.Load().(*hourMetricIDs)
|
||||
if !hmPrev.isFull {
|
||||
return maxMetrics
|
||||
}
|
||||
hourMetrics := len(hmPrev.m)
|
||||
if hourMetrics >= 256 && maxMetrics > hourMetrics/4 {
|
||||
// It is cheaper to filter on the hour or day metrics if the minimum
|
||||
// number of matching metrics across tfs exceeds hourMetrics / 4.
|
||||
return hourMetrics / 4
|
||||
}
|
||||
return maxMetrics
|
||||
}
|
||||
|
||||
func (is *indexSearch) getTagFilterWithMinMetricIDsCount(tfs *TagFilters, maxMetrics int) (*tagFilter, map[uint64]struct{}, error) {
|
||||
var minMetricIDs map[uint64]struct{}
|
||||
var minTf *tagFilter
|
||||
kb := &is.kb
|
||||
uselessTagFilters := 0
|
||||
for i := range tfs.tfs {
|
||||
tf := &tfs.tfs[i]
|
||||
if tf.isNegative {
|
||||
// Skip negative filters.
|
||||
continue
|
||||
}
|
||||
|
||||
kb.B = append(kb.B[:0], uselessSingleTagFilterKeyPrefix)
|
||||
kb.B = encoding.MarshalUint64(kb.B[:0], uint64(maxMetrics))
|
||||
kb.B = tf.Marshal(kb.B)
|
||||
if len(is.db.uselessTagFiltersCache.Get(nil, kb.B)) > 0 {
|
||||
// Skip useless work below, since the tf matches at least maxMetrics metrics.
|
||||
uselessTagFilters++
|
||||
continue
|
||||
}
|
||||
|
||||
metricIDs, err := is.getMetricIDsForTagFilter(tf, maxMetrics)
|
||||
if err != nil {
|
||||
if err == errFallbackToMetricNameMatch {
|
||||
@@ -1243,20 +1336,32 @@ func (is *indexSearch) getTagFilterWithMinMetricIDsCount(tfs *TagFilters, maxMet
|
||||
}
|
||||
return nil, nil, fmt.Errorf("cannot find MetricIDs for tagFilter %s: %s", tf, err)
|
||||
}
|
||||
if minTf == nil || len(metricIDs) < len(minMetricIDs) {
|
||||
minMetricIDs = metricIDs
|
||||
minTf = tf
|
||||
maxMetrics = len(minMetricIDs)
|
||||
if maxMetrics <= 1 {
|
||||
// There is no need in inspecting other filters, since minTf
|
||||
// already matches 0 or 1 metric.
|
||||
break
|
||||
}
|
||||
if len(metricIDs) >= maxMetrics {
|
||||
// The tf matches at least maxMetrics. Skip it
|
||||
kb.B = append(kb.B[:0], uselessSingleTagFilterKeyPrefix)
|
||||
kb.B = encoding.MarshalUint64(kb.B[:0], uint64(maxMetrics))
|
||||
kb.B = tf.Marshal(kb.B)
|
||||
is.db.uselessTagFiltersCache.Set(kb.B, uselessTagFilterCacheValue)
|
||||
uselessTagFilters++
|
||||
continue
|
||||
}
|
||||
|
||||
minMetricIDs = metricIDs
|
||||
minTf = tf
|
||||
maxMetrics = len(minMetricIDs)
|
||||
if maxMetrics <= 1 {
|
||||
// There is no need in inspecting other filters, since minTf
|
||||
// already matches 0 or 1 metric.
|
||||
break
|
||||
}
|
||||
}
|
||||
if minTf != nil {
|
||||
return minTf, minMetricIDs, nil
|
||||
}
|
||||
if uselessTagFilters == len(tfs.tfs) {
|
||||
// All the tag filters return at least maxMetrics entries.
|
||||
return nil, nil, errTooManyMetrics
|
||||
}
|
||||
|
||||
// There is no positive filter with small number of matching metrics.
|
||||
// Create it, so it matches all the MetricIDs for tfs.commonPrefix.
|
||||
@@ -1294,7 +1399,7 @@ func matchTagFilters(mn *MetricName, tfs []*tagFilter, kb *bytesutil.ByteBuffer)
|
||||
continue
|
||||
}
|
||||
|
||||
// Found the matching tag name. Match for the value.
|
||||
// Found the matching tag name. Match the value.
|
||||
b := tag.Marshal(kb.B)
|
||||
kb.B = b[:len(kb.B)]
|
||||
ok, err := matchTagFilter(b, tf)
|
||||
@@ -1307,7 +1412,7 @@ func matchTagFilters(mn *MetricName, tfs []*tagFilter, kb *bytesutil.ByteBuffer)
|
||||
tagMatched = true
|
||||
break
|
||||
}
|
||||
if !tagMatched {
|
||||
if !tagMatched && !tf.isNegative {
|
||||
// Matching tag name wasn't found.
|
||||
return false, nil
|
||||
}
|
||||
@@ -1376,7 +1481,8 @@ func (is *indexSearch) updateMetricIDsForTagFilters(metricIDs map[uint64]struct{
|
||||
// Sort tag filters for faster ts.Seek below.
|
||||
sort.Slice(tfs.tfs, func(i, j int) bool { return bytes.Compare(tfs.tfs[i].prefix, tfs.tfs[j].prefix) < 0 })
|
||||
|
||||
minTf, minMetricIDs, err := is.getTagFilterWithMinMetricIDsCountAdaptive(tfs, maxMetrics)
|
||||
maxMetricsAdjusted := is.adjustMaxMetricsAdaptive(tr, maxMetrics)
|
||||
minTf, minMetricIDs, err := is.getTagFilterWithMinMetricIDsCountAdaptive(tfs, maxMetricsAdjusted)
|
||||
if err != nil {
|
||||
if err != errTooManyMetrics {
|
||||
return err
|
||||
@@ -1394,7 +1500,7 @@ func (is *indexSearch) updateMetricIDsForTagFilters(metricIDs map[uint64]struct{
|
||||
maxTimeRangeMetrics := 20 * maxMetrics
|
||||
metricIDsForTimeRange, err := is.getMetricIDsForTimeRange(tr, maxTimeRangeMetrics+1)
|
||||
if err == errMissingMetricIDsForDate {
|
||||
return fmt.Errorf("cannot find tag filter matching less up to %d time series; either increase -search.maxUniqueTimeseries or use more specific tag filters",
|
||||
return fmt.Errorf("cannot find tag filter matching less than %d time series; either increase -search.maxUniqueTimeseries or use more specific tag filters",
|
||||
maxMetrics)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -1443,6 +1549,13 @@ func (is *indexSearch) updateMetricIDsForTagFilters(metricIDs map[uint64]struct{
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
uselessSingleTagFilterKeyPrefix = 0
|
||||
uselessMultiTagFiltersKeyPrefix = 1
|
||||
)
|
||||
|
||||
var uselessTagFilterCacheValue = []byte("1")
|
||||
|
||||
func (is *indexSearch) getMetricIDsForTagFilter(tf *tagFilter, maxMetrics int) (map[uint64]struct{}, error) {
|
||||
if tf.isNegative {
|
||||
logger.Panicf("BUG: isNegative must be false")
|
||||
@@ -1456,17 +1569,35 @@ func (is *indexSearch) getMetricIDsForTagFilter(tf *tagFilter, maxMetrics int) (
|
||||
return metricIDs, nil
|
||||
}
|
||||
|
||||
// Slow path - scan all the rows with tf.prefix
|
||||
// Slow path - scan for all the rows with the given prefix.
|
||||
maxLoops := maxMetrics * maxIndexScanLoopsPerMetric
|
||||
err := is.getMetricIDsForTagFilterSlow(tf, maxLoops, func(metricID uint64) bool {
|
||||
metricIDs[metricID] = struct{}{}
|
||||
return len(metricIDs) < maxMetrics
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metricIDs, nil
|
||||
}
|
||||
|
||||
func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, maxLoops int, f func(metricID uint64) bool) error {
|
||||
if len(tf.orSuffixes) > 0 {
|
||||
logger.Panicf("BUG: the getMetricIDsForTagFilterSlow must be called only for empty tf.orSuffixes; got %s", tf.orSuffixes)
|
||||
}
|
||||
|
||||
// Scan all the rows with tf.prefix and call f on every tf match.
|
||||
loops := 0
|
||||
ts := &is.ts
|
||||
kb := &is.kb
|
||||
var prevMatchingK []byte
|
||||
var prevMatch bool
|
||||
ts.Seek(tf.prefix)
|
||||
for len(metricIDs) < maxMetrics && ts.NextItem() {
|
||||
for ts.NextItem() {
|
||||
loops++
|
||||
if loops > maxLoops {
|
||||
return nil, errFallbackToMetricNameMatch
|
||||
return errFallbackToMetricNameMatch
|
||||
}
|
||||
|
||||
k := ts.Item
|
||||
if !bytes.HasPrefix(k, tf.prefix) {
|
||||
break
|
||||
@@ -1475,25 +1606,49 @@ func (is *indexSearch) getMetricIDsForTagFilter(tf *tagFilter, maxMetrics int) (
|
||||
// Get MetricID from k (the last 8 bytes).
|
||||
k = k[len(tf.prefix):]
|
||||
if len(k) < 8 {
|
||||
return nil, fmt.Errorf("invald key suffix size; want at least %d bytes; got %d bytes", 8, len(k))
|
||||
return fmt.Errorf("invald key suffix size; want at least %d bytes; got %d bytes", 8, len(k))
|
||||
}
|
||||
v := k[len(k)-8:]
|
||||
k = k[:len(k)-8]
|
||||
metricID := encoding.UnmarshalUint64(v)
|
||||
|
||||
if prevMatch && string(k) == string(prevMatchingK) {
|
||||
// Fast path: the same tag value found.
|
||||
// There is no need in checking it again with potentially
|
||||
// slow tf.matchSuffix, which may call regexp.
|
||||
if !f(metricID) {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ok, err := tf.matchSuffix(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when matching %s: %s", tf, err)
|
||||
return fmt.Errorf("error when matching %s: %s", tf, err)
|
||||
}
|
||||
if !ok {
|
||||
prevMatch = false
|
||||
// Optimization: skip all the metricIDs for the given tag value
|
||||
kb.B = append(kb.B[:0], ts.Item[:len(ts.Item)-8]...)
|
||||
// The last char in kb.B must be tagSeparatorChar. Just increment it
|
||||
// in order to jump to the next tag value.
|
||||
if len(kb.B) == 0 || kb.B[len(kb.B)-1] != tagSeparatorChar || tagSeparatorChar >= 0xff {
|
||||
return fmt.Errorf("data corruption: the last char in k=%X must be %X", kb.B, tagSeparatorChar)
|
||||
}
|
||||
kb.B[len(kb.B)-1]++
|
||||
ts.Seek(kb.B)
|
||||
continue
|
||||
}
|
||||
metricID := encoding.UnmarshalUint64(v)
|
||||
metricIDs[metricID] = struct{}{}
|
||||
prevMatch = true
|
||||
prevMatchingK = append(prevMatchingK[:0], k...)
|
||||
if !f(metricID) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := ts.Error(); err != nil {
|
||||
return nil, fmt.Errorf("error when searching for tag filter prefix %q: %s", tf.prefix, err)
|
||||
return fmt.Errorf("error when searching for tag filter prefix %q: %s", tf.prefix, err)
|
||||
}
|
||||
return metricIDs, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *indexSearch) updateMetricIDsForOrSuffixesNoFilter(tf *tagFilter, maxMetrics int, metricIDs map[uint64]struct{}) error {
|
||||
@@ -1585,6 +1740,15 @@ func (is *indexSearch) updateMetricIDsForOrSuffixWithFilter(prefix []byte, metri
|
||||
}
|
||||
metricID := encoding.UnmarshalUint64(v)
|
||||
if metricID != nextMetricID {
|
||||
// Skip metricIDs smaller than the found metricID, since they don't
|
||||
// match anything.
|
||||
if len(sortedFilter) > 0 && metricID > sortedFilter[0] {
|
||||
sortedFilter = sortedFilter[1:]
|
||||
n := sort.Search(len(sortedFilter), func(i int) bool {
|
||||
return metricID <= sortedFilter[i]
|
||||
})
|
||||
sortedFilter = sortedFilter[n:]
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isNegative {
|
||||
@@ -1617,15 +1781,15 @@ func (is *indexSearch) getMetricIDsForTimeRange(tr TimeRange, maxMetrics int) (m
|
||||
|
||||
// Slow path: collect the metric ids for all the days covering the given tr.
|
||||
atomic.AddUint64(&is.db.dateMetricIDsSearchCalls, 1)
|
||||
minDate := tr.MinTimestamp / msecPerDay
|
||||
maxDate := tr.MaxTimestamp / msecPerDay
|
||||
if maxDate-minDate > 40 {
|
||||
minDate := uint64(tr.MinTimestamp) / msecPerDay
|
||||
maxDate := uint64(tr.MaxTimestamp) / msecPerDay
|
||||
if maxDate-minDate > maxDaysForDateMetricIDs {
|
||||
// Too much dates must be covered. Give up.
|
||||
return nil, errMissingMetricIDsForDate
|
||||
}
|
||||
metricIDs := make(map[uint64]struct{}, maxMetrics)
|
||||
for minDate <= maxDate {
|
||||
if err := is.getMetricIDsForDate(uint64(minDate), metricIDs, maxMetrics); err != nil {
|
||||
if err := is.getMetricIDsForDate(minDate, metricIDs, maxMetrics); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
minDate++
|
||||
@@ -1637,9 +1801,6 @@ func (is *indexSearch) getMetricIDsForTimeRange(tr TimeRange, maxMetrics int) (m
|
||||
func (is *indexSearch) getMetricIDsForRecentHours(tr TimeRange, maxMetrics int) (map[uint64]struct{}, bool) {
|
||||
minHour := uint64(tr.MinTimestamp) / msecPerHour
|
||||
maxHour := uint64(tr.MaxTimestamp) / msecPerHour
|
||||
if is.db.currHourMetricIDs == nil {
|
||||
return nil, false
|
||||
}
|
||||
hmCurr := is.db.currHourMetricIDs.Load().(*hourMetricIDs)
|
||||
if maxHour == hmCurr.hour && minHour == maxHour && hmCurr.isFull {
|
||||
// The tr fits the current hour.
|
||||
@@ -1650,9 +1811,6 @@ func (is *indexSearch) getMetricIDsForRecentHours(tr TimeRange, maxMetrics int)
|
||||
}
|
||||
return getMetricIDsCopy(hmCurr.m), true
|
||||
}
|
||||
if is.db.prevHourMetricIDs == nil {
|
||||
return nil, false
|
||||
}
|
||||
hmPrev := is.db.prevHourMetricIDs.Load().(*hourMetricIDs)
|
||||
if maxHour == hmPrev.hour && minHour == maxHour && hmPrev.isFull {
|
||||
// The tr fits the previous hour.
|
||||
@@ -1760,6 +1918,28 @@ func (is *indexSearch) getMetricIDsForDate(date uint64, metricIDs map[uint64]str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *indexSearch) containsTimeRange(tr TimeRange) (bool, error) {
|
||||
ts := &is.ts
|
||||
kb := &is.kb
|
||||
|
||||
// Verify whether the maximum date in `ts` covers tr.MinTimestamp.
|
||||
minDate := uint64(tr.MinTimestamp) / msecPerDay
|
||||
kb.B = marshalCommonPrefix(kb.B[:0], nsPrefixDateToMetricID)
|
||||
kb.B = encoding.MarshalUint64(kb.B, minDate)
|
||||
ts.Seek(kb.B)
|
||||
if !ts.NextItem() {
|
||||
if err := ts.Error(); err != nil {
|
||||
return false, fmt.Errorf("error when searching for minDate=%d, prefix %q: %s", minDate, kb.B, err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
if !bytes.HasPrefix(ts.Item, kb.B[:1]) {
|
||||
// minDate exceeds max date from ts.
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (is *indexSearch) updateMetricIDsForCommonPrefix(metricIDs map[uint64]struct{}, commonPrefix []byte, maxMetrics int) error {
|
||||
ts := &is.ts
|
||||
ts.Seek(commonPrefix)
|
||||
@@ -1807,47 +1987,19 @@ func (is *indexSearch) intersectMetricIDsWithTagFilter(tf *tagFilter, filter map
|
||||
|
||||
// Slow path - scan for all the rows with the given prefix.
|
||||
maxLoops := len(filter) * maxIndexScanLoopsPerMetric
|
||||
loops := 0
|
||||
ts := &is.ts
|
||||
ts.Seek(tf.prefix)
|
||||
for ts.NextItem() {
|
||||
loops++
|
||||
if loops > maxLoops {
|
||||
return nil, errFallbackToMetricNameMatch
|
||||
}
|
||||
|
||||
k := ts.Item
|
||||
if !bytes.HasPrefix(k, tf.prefix) {
|
||||
break
|
||||
}
|
||||
|
||||
// Extract MetricID from k (the last 8 bytes).
|
||||
k = k[len(tf.prefix):]
|
||||
if len(k) < 8 {
|
||||
return nil, fmt.Errorf("cannot extract metricID from k; want at least %d bytes; got %d bytes", 8, len(k))
|
||||
}
|
||||
v := k[len(k)-8:]
|
||||
k = k[:len(k)-8]
|
||||
|
||||
ok, err := tf.matchSuffix(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when matching %s: %s", tf, err)
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
metricID := encoding.UnmarshalUint64(v)
|
||||
err := is.getMetricIDsForTagFilterSlow(tf, maxLoops, func(metricID uint64) bool {
|
||||
if tf.isNegative {
|
||||
// filter must be equal to metricIDs
|
||||
delete(metricIDs, metricID)
|
||||
continue
|
||||
return true
|
||||
}
|
||||
if _, ok := filter[metricID]; ok {
|
||||
metricIDs[metricID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err := ts.Error(); err != nil {
|
||||
return nil, fmt.Errorf("error searching %q: %s", tf.prefix, err)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metricIDs, nil
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||
)
|
||||
|
||||
func TestMarshalUnmarshalTSIDs(t *testing.T) {
|
||||
@@ -56,12 +57,18 @@ func TestMarshalUnmarshalTSIDs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIndexDBOpenClose(t *testing.T) {
|
||||
metricIDCache := fastcache.New(1234)
|
||||
metricNameCache := fastcache.New(1234)
|
||||
defer metricIDCache.Reset()
|
||||
defer metricNameCache.Reset()
|
||||
metricIDCache := workingsetcache.New(1234, time.Hour)
|
||||
metricNameCache := workingsetcache.New(1234, time.Hour)
|
||||
defer metricIDCache.Stop()
|
||||
defer metricNameCache.Stop()
|
||||
|
||||
var hmCurr atomic.Value
|
||||
hmCurr.Store(&hourMetricIDs{})
|
||||
var hmPrev atomic.Value
|
||||
hmPrev.Store(&hourMetricIDs{})
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
db, err := openIndexDB("test-index-db", metricIDCache, metricNameCache, nil, nil)
|
||||
db, err := openIndexDB("test-index-db", metricIDCache, metricNameCache, &hmCurr, &hmPrev)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open indexDB: %s", err)
|
||||
}
|
||||
@@ -78,12 +85,18 @@ func TestIndexDB(t *testing.T) {
|
||||
const metricGroups = 10
|
||||
|
||||
t.Run("serial", func(t *testing.T) {
|
||||
metricIDCache := fastcache.New(1234)
|
||||
metricNameCache := fastcache.New(1234)
|
||||
defer metricIDCache.Reset()
|
||||
defer metricNameCache.Reset()
|
||||
metricIDCache := workingsetcache.New(1234, time.Hour)
|
||||
metricNameCache := workingsetcache.New(1234, time.Hour)
|
||||
defer metricIDCache.Stop()
|
||||
defer metricNameCache.Stop()
|
||||
|
||||
var hmCurr atomic.Value
|
||||
hmCurr.Store(&hourMetricIDs{})
|
||||
var hmPrev atomic.Value
|
||||
hmPrev.Store(&hourMetricIDs{})
|
||||
|
||||
dbName := "test-index-db-serial"
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, nil, nil)
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open indexDB: %s", err)
|
||||
}
|
||||
@@ -113,7 +126,7 @@ func TestIndexDB(t *testing.T) {
|
||||
|
||||
// Re-open the db and verify it works as expected.
|
||||
db.MustClose()
|
||||
db, err = openIndexDB(dbName, metricIDCache, metricNameCache, nil, nil)
|
||||
db, err = openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open indexDB: %s", err)
|
||||
}
|
||||
@@ -129,12 +142,18 @@ func TestIndexDB(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("concurrent", func(t *testing.T) {
|
||||
metricIDCache := fastcache.New(1234)
|
||||
metricNameCache := fastcache.New(1234)
|
||||
defer metricIDCache.Reset()
|
||||
defer metricNameCache.Reset()
|
||||
metricIDCache := workingsetcache.New(1234, time.Hour)
|
||||
metricNameCache := workingsetcache.New(1234, time.Hour)
|
||||
defer metricIDCache.Stop()
|
||||
defer metricNameCache.Stop()
|
||||
|
||||
var hmCurr atomic.Value
|
||||
hmCurr.Store(&hourMetricIDs{})
|
||||
var hmPrev atomic.Value
|
||||
hmPrev.Store(&hourMetricIDs{})
|
||||
|
||||
dbName := "test-index-db-concurrent"
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, nil, nil)
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open indexDB: %s", err)
|
||||
}
|
||||
@@ -293,6 +312,16 @@ func testIndexDBGetOrCreateTSIDByName(db *indexDB, accountsCount, projectsCount,
|
||||
}
|
||||
|
||||
func testIndexDBCheckTSIDByName(db *indexDB, mns []MetricName, tsids []TSID, isConcurrent bool) error {
|
||||
// fill Date -> MetricID cache
|
||||
date := uint64(timestampFromTime(time.Now())) / msecPerDay
|
||||
for i := range tsids {
|
||||
tsid := &tsids[i]
|
||||
if err := db.storeDateMetricID(date, tsid.MetricID); err != nil {
|
||||
return fmt.Errorf("error in storeDateMetricID(%d, %d): %s", date, tsid.MetricID, err)
|
||||
}
|
||||
}
|
||||
db.tb.DebugFlush()
|
||||
|
||||
hasValue := func(tvs []string, v []byte) bool {
|
||||
for _, tv := range tvs {
|
||||
if string(v) == tv {
|
||||
@@ -724,8 +753,8 @@ func TestMatchTagFilters(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("Shouldn't match")
|
||||
if !ok {
|
||||
t.Fatalf("Should match")
|
||||
}
|
||||
tfs.Reset()
|
||||
if err := tfs.Add([]byte("non-existing-tag"), []byte("foob.+metric"), true, true); err != nil {
|
||||
@@ -735,8 +764,19 @@ func TestMatchTagFilters(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("Shouldn't match")
|
||||
if !ok {
|
||||
t.Fatalf("Should match")
|
||||
}
|
||||
tfs.Reset()
|
||||
if err := tfs.Add([]byte("non-existing-tag"), []byte(".+"), true, true); err != nil {
|
||||
t.Fatalf("cannot add regexp, negative filter: %s", err)
|
||||
}
|
||||
ok, err = matchTagFilters(&mn, toTFPointers(tfs.tfs), &bb)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("Should match")
|
||||
}
|
||||
|
||||
// Negative match by existing tag
|
||||
@@ -830,6 +870,17 @@ func TestMatchTagFilters(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("Should match")
|
||||
}
|
||||
tfs.Reset()
|
||||
if err := tfs.Add([]byte("key 3"), []byte(""), true, false); err != nil {
|
||||
t.Fatalf("cannot add regexp, negative filter: %s", err)
|
||||
}
|
||||
ok, err = matchTagFilters(&mn, toTFPointers(tfs.tfs), &bb)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("Should match")
|
||||
}
|
||||
|
||||
// Positive match by multiple tags and MetricGroup
|
||||
tfs.Reset()
|
||||
|
||||
@@ -4,20 +4,28 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||
)
|
||||
|
||||
func BenchmarkIndexDBAddTSIDs(b *testing.B) {
|
||||
const recordsPerLoop = 1e3
|
||||
|
||||
metricIDCache := fastcache.New(1234)
|
||||
metricNameCache := fastcache.New(1234)
|
||||
defer metricIDCache.Reset()
|
||||
defer metricNameCache.Reset()
|
||||
metricIDCache := workingsetcache.New(1234, time.Hour)
|
||||
metricNameCache := workingsetcache.New(1234, time.Hour)
|
||||
defer metricIDCache.Stop()
|
||||
defer metricNameCache.Stop()
|
||||
|
||||
var hmCurr atomic.Value
|
||||
hmCurr.Store(&hourMetricIDs{})
|
||||
var hmPrev atomic.Value
|
||||
hmPrev.Store(&hourMetricIDs{})
|
||||
|
||||
const dbName = "bench-index-db-add-tsids"
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, nil, nil)
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
|
||||
if err != nil {
|
||||
b.Fatalf("cannot open indexDB: %s", err)
|
||||
}
|
||||
@@ -71,82 +79,19 @@ func benchmarkIndexDBAddTSIDs(db *indexDB, tsid *TSID, mn *MetricName, startOffs
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIndexDBSearchTSIDs(b *testing.B) {
|
||||
metricIDCache := fastcache.New(1234)
|
||||
metricNameCache := fastcache.New(1234)
|
||||
defer metricIDCache.Reset()
|
||||
defer metricNameCache.Reset()
|
||||
const dbName = "bench-index-db-search-tsids"
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, nil, nil)
|
||||
if err != nil {
|
||||
b.Fatalf("cannot open indexDB: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
db.MustClose()
|
||||
if err := os.RemoveAll(dbName); err != nil {
|
||||
b.Fatalf("cannot remove indexDB: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
const recordsCount = 1e5
|
||||
|
||||
// Fill the db with recordsCount records.
|
||||
var mn MetricName
|
||||
mn.MetricGroup = []byte("rps")
|
||||
for i := 0; i < 2; i++ {
|
||||
key := fmt.Sprintf("key_%d", i)
|
||||
value := fmt.Sprintf("value_%d", i)
|
||||
mn.AddTag(key, value)
|
||||
}
|
||||
var tsid TSID
|
||||
var metricName []byte
|
||||
is := db.getIndexSearch()
|
||||
defer db.putIndexSearch(is)
|
||||
for i := 0; i < recordsCount; i++ {
|
||||
mn.sortTags()
|
||||
metricName = mn.Marshal(metricName[:0])
|
||||
if err := is.GetOrCreateTSIDByName(&tsid, metricName); err != nil {
|
||||
b.Fatalf("cannot insert record: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(1)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
tags := []Tag{
|
||||
{[]byte("key_0"), []byte("value_0")},
|
||||
{[]byte("key_1"), []byte("value_1")},
|
||||
}
|
||||
var tfs TagFilters
|
||||
tfss := []*TagFilters{&tfs}
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
tfs.Reset()
|
||||
for j := range tags {
|
||||
if err := tfs.Add(tags[j].Key, tags[j].Value, false, false); err != nil {
|
||||
panic(fmt.Errorf("BUG: unexpected error: %s", err))
|
||||
}
|
||||
}
|
||||
tsids, err := db.searchTSIDs(tfss, TimeRange{}, 1e5)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unexpected error in search for tfs=%s: %s", &tfs, err))
|
||||
}
|
||||
if len(tsids) == 0 && i < recordsCount {
|
||||
panic(fmt.Errorf("zero tsids found for tfs=%s", &tfs))
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIndexDBGetTSIDs(b *testing.B) {
|
||||
metricIDCache := fastcache.New(1234)
|
||||
metricNameCache := fastcache.New(1234)
|
||||
defer metricIDCache.Reset()
|
||||
defer metricNameCache.Reset()
|
||||
metricIDCache := workingsetcache.New(1234, time.Hour)
|
||||
metricNameCache := workingsetcache.New(1234, time.Hour)
|
||||
defer metricIDCache.Stop()
|
||||
defer metricNameCache.Stop()
|
||||
|
||||
var hmCurr atomic.Value
|
||||
hmCurr.Store(&hourMetricIDs{})
|
||||
var hmPrev atomic.Value
|
||||
hmPrev.Store(&hourMetricIDs{})
|
||||
|
||||
const dbName = "bench-index-db-get-tsids"
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, nil, nil)
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
|
||||
if err != nil {
|
||||
b.Fatalf("cannot open indexDB: %s", err)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ func (mp *inmemoryPart) InitFromRows(rows []rawRow) {
|
||||
// It is unsafe re-using mp while the returned part is in use.
|
||||
func (mp *inmemoryPart) NewPart() (*part, error) {
|
||||
ph := mp.ph
|
||||
return newPart(&ph, "", mp.metaindexData.NewReader(), &mp.timestampsData, &mp.valuesData, &mp.indexData)
|
||||
size := uint64(len(mp.timestampsData.B) + len(mp.valuesData.B) + len(mp.indexData.B) + len(mp.metaindexData.B))
|
||||
return newPart(&ph, "", size, mp.metaindexData.NewReader(), &mp.timestampsData, &mp.valuesData, &mp.indexData)
|
||||
}
|
||||
|
||||
func getInmemoryPart() *inmemoryPart {
|
||||
|
||||
@@ -29,7 +29,7 @@ func mergeBlockStreams(ph *partHeader, bsw *blockStreamWriter, bsrs []*blockStre
|
||||
if err == errForciblyStopped {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("cannot merge %d streams: %s", len(bsrs), err)
|
||||
return fmt.Errorf("cannot merge %d streams: %s: %s", len(bsrs), bsrs, err)
|
||||
}
|
||||
|
||||
var bsmPool = &sync.Pool{
|
||||
|
||||
@@ -50,6 +50,12 @@ func (tag *Tag) copyFrom(src *Tag) {
|
||||
tag.Value = append(tag.Value[:0], src.Value...)
|
||||
}
|
||||
|
||||
func marshalTagValueNoTrailingTagSeparator(dst, src []byte) []byte {
|
||||
dst = marshalTagValue(dst, src)
|
||||
// Remove trailing tagSeparatorChar
|
||||
return dst[:len(dst)-1]
|
||||
}
|
||||
|
||||
func marshalTagValue(dst, src []byte) []byte {
|
||||
n1 := bytes.IndexByte(src, escapeChar)
|
||||
n2 := bytes.IndexByte(src, tagSeparatorChar)
|
||||
|
||||
@@ -36,6 +36,9 @@ type part struct {
|
||||
// Empty for in-memory part.
|
||||
path string
|
||||
|
||||
// Total size in bytes of part data.
|
||||
size uint64
|
||||
|
||||
timestampsFile fs.ReadAtCloser
|
||||
valuesFile fs.ReadAtCloser
|
||||
indexFile fs.ReadAtCloser
|
||||
@@ -59,6 +62,7 @@ func openFilePart(path string) (*part, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open timestamps file: %s", err)
|
||||
}
|
||||
timestampsSize := fs.MustFileSize(timestampsPath)
|
||||
|
||||
valuesPath := path + "/values.bin"
|
||||
valuesFile, err := fs.OpenReaderAt(valuesPath)
|
||||
@@ -66,6 +70,7 @@ func openFilePart(path string) (*part, error) {
|
||||
timestampsFile.MustClose()
|
||||
return nil, fmt.Errorf("cannot open values file: %s", err)
|
||||
}
|
||||
valuesSize := fs.MustFileSize(valuesPath)
|
||||
|
||||
indexPath := path + "/index.bin"
|
||||
indexFile, err := fs.OpenReaderAt(indexPath)
|
||||
@@ -74,6 +79,7 @@ func openFilePart(path string) (*part, error) {
|
||||
valuesFile.MustClose()
|
||||
return nil, fmt.Errorf("cannot open index file: %s", err)
|
||||
}
|
||||
indexSize := fs.MustFileSize(indexPath)
|
||||
|
||||
metaindexPath := path + "/metaindex.bin"
|
||||
metaindexFile, err := filestream.Open(metaindexPath, true)
|
||||
@@ -83,15 +89,17 @@ func openFilePart(path string) (*part, error) {
|
||||
indexFile.MustClose()
|
||||
return nil, fmt.Errorf("cannot open metaindex file: %s", err)
|
||||
}
|
||||
metaindexSize := fs.MustFileSize(metaindexPath)
|
||||
|
||||
return newPart(&ph, path, metaindexFile, timestampsFile, valuesFile, indexFile)
|
||||
size := timestampsSize + valuesSize + indexSize + metaindexSize
|
||||
return newPart(&ph, path, size, metaindexFile, timestampsFile, valuesFile, indexFile)
|
||||
}
|
||||
|
||||
// newPart returns new part initialized with the given arguments.
|
||||
//
|
||||
// The returned part calls MustClose on all the files passed to newPart
|
||||
// when calling part.MustClose.
|
||||
func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser, timestampsFile, valuesFile, indexFile fs.ReadAtCloser) (*part, error) {
|
||||
func newPart(ph *partHeader, path string, size uint64, metaindexReader filestream.ReadCloser, timestampsFile, valuesFile, indexFile fs.ReadAtCloser) (*part, error) {
|
||||
var errors []error
|
||||
metaindex, err := unmarshalMetaindexRows(nil, metaindexReader)
|
||||
if err != nil {
|
||||
@@ -102,6 +110,7 @@ func newPart(ph *partHeader, path string, metaindexReader filestream.ReadCloser,
|
||||
p := &part{
|
||||
ph: *ph,
|
||||
path: path,
|
||||
size: size,
|
||||
timestampsFile: timestampsFile,
|
||||
valuesFile: valuesFile,
|
||||
indexFile: indexFile,
|
||||
|
||||
@@ -28,6 +28,9 @@ type partSearch struct {
|
||||
// tr is a time range to search.
|
||||
tr TimeRange
|
||||
|
||||
// Skip populating timestampsData and valuesData in Block if fetchData=false.
|
||||
fetchData bool
|
||||
|
||||
metaindex []metaindexRow
|
||||
|
||||
ibCache *indexBlockCache
|
||||
@@ -48,6 +51,7 @@ func (ps *partSearch) reset() {
|
||||
ps.p = nil
|
||||
ps.tsids = ps.tsids[:0]
|
||||
ps.tsidIdx = 0
|
||||
ps.fetchData = true
|
||||
ps.metaindex = nil
|
||||
ps.ibCache = nil
|
||||
ps.bhs = nil
|
||||
@@ -61,7 +65,7 @@ func (ps *partSearch) reset() {
|
||||
}
|
||||
|
||||
// Init initializes the ps with the given p, tsids and tr.
|
||||
func (ps *partSearch) Init(p *part, tsids []TSID, tr TimeRange) {
|
||||
func (ps *partSearch) Init(p *part, tsids []TSID, tr TimeRange, fetchData bool) {
|
||||
ps.reset()
|
||||
ps.p = p
|
||||
|
||||
@@ -72,6 +76,7 @@ func (ps *partSearch) Init(p *part, tsids []TSID, tr TimeRange) {
|
||||
ps.tsids = append(ps.tsids[:0], tsids...)
|
||||
}
|
||||
ps.tr = tr
|
||||
ps.fetchData = fetchData
|
||||
ps.metaindex = p.metaindex
|
||||
ps.ibCache = &p.ibCache
|
||||
|
||||
@@ -281,11 +286,14 @@ func (ps *partSearch) searchBHS() bool {
|
||||
|
||||
func (ps *partSearch) readBlock(bh *blockHeader) {
|
||||
ps.Block.Reset()
|
||||
ps.Block.bh = *bh
|
||||
if !ps.fetchData {
|
||||
return
|
||||
}
|
||||
|
||||
ps.Block.timestampsData = bytesutil.Resize(ps.Block.timestampsData[:0], int(bh.TimestampsBlockSize))
|
||||
ps.p.timestampsFile.ReadAt(ps.Block.timestampsData, int64(bh.TimestampsBlockOffset))
|
||||
|
||||
ps.Block.valuesData = bytesutil.Resize(ps.Block.valuesData[:0], int(bh.ValuesBlockSize))
|
||||
ps.p.valuesFile.ReadAt(ps.Block.valuesData, int64(bh.ValuesBlockOffset))
|
||||
|
||||
ps.Block.bh = *bh
|
||||
}
|
||||
|
||||
@@ -1247,7 +1247,7 @@ func testPartSearch(t *testing.T, p *part, tsids []TSID, tr TimeRange, expectedR
|
||||
|
||||
func testPartSearchSerial(p *part, tsids []TSID, tr TimeRange, expectedRawBlocks []rawBlock) error {
|
||||
var ps partSearch
|
||||
ps.Init(p, tsids, tr)
|
||||
ps.Init(p, tsids, tr, true)
|
||||
var bs []Block
|
||||
for ps.NextBlock() {
|
||||
var b Block
|
||||
|
||||
@@ -282,6 +282,9 @@ type partitionMetrics struct {
|
||||
SmallIndexBlocksCacheRequests uint64
|
||||
SmallIndexBlocksCacheMisses uint64
|
||||
|
||||
BigSizeBytes uint64
|
||||
SmallSizeBytes uint64
|
||||
|
||||
BigRowsCount uint64
|
||||
SmallRowsCount uint64
|
||||
|
||||
@@ -326,6 +329,7 @@ func (pt *partition) UpdateMetrics(m *partitionMetrics) {
|
||||
m.BigIndexBlocksCacheMisses += p.ibCache.Misses()
|
||||
m.BigRowsCount += p.ph.RowsCount
|
||||
m.BigBlocksCount += p.ph.BlocksCount
|
||||
m.BigSizeBytes += p.size
|
||||
m.BigPartsRefCount += atomic.LoadUint64(&pw.refCount)
|
||||
}
|
||||
|
||||
@@ -337,6 +341,7 @@ func (pt *partition) UpdateMetrics(m *partitionMetrics) {
|
||||
m.SmallIndexBlocksCacheMisses += p.ibCache.Misses()
|
||||
m.SmallRowsCount += p.ph.RowsCount
|
||||
m.SmallBlocksCount += p.ph.BlocksCount
|
||||
m.SmallSizeBytes += p.size
|
||||
m.SmallPartsRefCount += atomic.LoadUint64(&pw.refCount)
|
||||
}
|
||||
|
||||
@@ -1003,7 +1008,7 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}) erro
|
||||
}
|
||||
fmt.Fprintf(&bb, "%s -> %s\n", tmpPartPath, dstPartPath)
|
||||
txnPath := fmt.Sprintf("%s/txn/%016X", ptPath, mergeIdx)
|
||||
if err := fs.WriteFile(txnPath, bb.B); err != nil {
|
||||
if err := fs.WriteFileAtomically(txnPath, bb.B); err != nil {
|
||||
return fmt.Errorf("cannot create transaction file %q: %s", txnPath, err)
|
||||
}
|
||||
|
||||
@@ -1013,12 +1018,14 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}) erro
|
||||
}
|
||||
|
||||
var newPW *partWrapper
|
||||
var newPSize uint64
|
||||
if len(dstPartPath) > 0 {
|
||||
// Open the merged part if it is non-empty.
|
||||
newP, err := openFilePart(dstPartPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open merged part %q: %s", dstPartPath, err)
|
||||
}
|
||||
newPSize = newP.size
|
||||
newPW = &partWrapper{
|
||||
p: newP,
|
||||
refCount: 1,
|
||||
@@ -1057,7 +1064,7 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}) erro
|
||||
|
||||
d := time.Since(startTime)
|
||||
if d > 10*time.Second {
|
||||
logger.Infof("merged %d rows in %s at %d rows/sec to %q", outRowsCount, d, int(float64(outRowsCount)/d.Seconds()), dstPartPath)
|
||||
logger.Infof("merged %d rows in %s at %d rows/sec to %q; sizeBytes: %d", outRowsCount, d, int(float64(outRowsCount)/d.Seconds()), dstPartPath, newPSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1360,7 +1367,12 @@ func runTransactions(txnLock *sync.RWMutex, pathPrefix1, pathPrefix2, path strin
|
||||
})
|
||||
|
||||
for _, fi := range fis {
|
||||
txnPath := txnDir + "/" + fi.Name()
|
||||
fn := fi.Name()
|
||||
if fs.IsTemporaryFileName(fn) {
|
||||
// Skip temporary files, which could be left after unclean shutdown.
|
||||
continue
|
||||
}
|
||||
txnPath := txnDir + "/" + fn
|
||||
if err := runTransaction(txnLock, pathPrefix1, pathPrefix2, txnPath); err != nil {
|
||||
return fmt.Errorf("cannot run transaction from %q: %s", txnPath, err)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (pts *partitionSearch) reset() {
|
||||
// Init initializes the search in the given partition for the given tsid and tr.
|
||||
//
|
||||
// MustClose must be called when partition search is done.
|
||||
func (pts *partitionSearch) Init(pt *partition, tsids []TSID, tr TimeRange) {
|
||||
func (pts *partitionSearch) Init(pt *partition, tsids []TSID, tr TimeRange, fetchData bool) {
|
||||
if pts.needClosing {
|
||||
logger.Panicf("BUG: missing partitionSearch.MustClose call before the next call to Init")
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (pts *partitionSearch) Init(pt *partition, tsids []TSID, tr TimeRange) {
|
||||
}
|
||||
pts.psPool = pts.psPool[:len(pts.pws)]
|
||||
for i, pw := range pts.pws {
|
||||
pts.psPool[i].Init(pw.p, tsids, tr)
|
||||
pts.psPool[i].Init(pw.p, tsids, tr, fetchData)
|
||||
}
|
||||
|
||||
// Initialize the psHeap.
|
||||
|
||||
@@ -238,7 +238,7 @@ func testPartitionSearchSerial(pt *partition, tsids []TSID, tr TimeRange, rbsExp
|
||||
|
||||
bs := []Block{}
|
||||
var pts partitionSearch
|
||||
pts.Init(pt, tsids, tr)
|
||||
pts.Init(pt, tsids, tr, true)
|
||||
for pts.NextBlock() {
|
||||
var b Block
|
||||
b.CopyFrom(pts.Block)
|
||||
@@ -263,7 +263,7 @@ func testPartitionSearchSerial(pt *partition, tsids []TSID, tr TimeRange, rbsExp
|
||||
}
|
||||
|
||||
// verify that empty tsids returns empty result
|
||||
pts.Init(pt, []TSID{}, tr)
|
||||
pts.Init(pt, []TSID{}, tr, true)
|
||||
if pts.NextBlock() {
|
||||
return fmt.Errorf("unexpected block got for an empty tsids list: %+v", pts.Block)
|
||||
}
|
||||
@@ -271,6 +271,16 @@ func testPartitionSearchSerial(pt *partition, tsids []TSID, tr TimeRange, rbsExp
|
||||
return fmt.Errorf("unexpected error on empty tsids list: %s", err)
|
||||
}
|
||||
pts.MustClose()
|
||||
|
||||
pts.Init(pt, []TSID{}, tr, false)
|
||||
if pts.NextBlock() {
|
||||
return fmt.Errorf("unexpected block got for an empty tsids list: %+v", pts.Block)
|
||||
}
|
||||
if err := pts.Error(); err != nil {
|
||||
return fmt.Errorf("unexpected error on empty tsids list: %s", err)
|
||||
}
|
||||
pts.MustClose()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ func (s *Search) reset() {
|
||||
// Init initializes s from the given storage, tfss and tr.
|
||||
//
|
||||
// MustClose must be called when the search is done.
|
||||
func (s *Search) Init(storage *Storage, tfss []*TagFilters, tr TimeRange, maxMetrics int) {
|
||||
func (s *Search) Init(storage *Storage, tfss []*TagFilters, tr TimeRange, fetchData bool, maxMetrics int) {
|
||||
if s.needClosing {
|
||||
logger.Panicf("BUG: missing MustClose call before the next call to Init")
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func (s *Search) Init(storage *Storage, tfss []*TagFilters, tr TimeRange, maxMet
|
||||
// It is ok to call Init on error from storage.searchTSIDs.
|
||||
// Init must be called before returning because it will fail
|
||||
// on Seach.MustClose otherwise.
|
||||
s.ts.Init(storage.tb, tsids, tr)
|
||||
s.ts.Init(storage.tb, tsids, tr, fetchData)
|
||||
|
||||
if err != nil {
|
||||
s.err = err
|
||||
|
||||
@@ -193,7 +193,7 @@ func testSearch(st *Storage, tr TimeRange, mrs []MetricRow, accountsCount int) e
|
||||
}
|
||||
|
||||
// Search
|
||||
s.Init(st, []*TagFilters{tfs}, tr, 1e5)
|
||||
s.Init(st, []*TagFilters{tfs}, tr, true, 1e5)
|
||||
var mbs []MetricBlock
|
||||
for s.NextMetricBlock() {
|
||||
var b Block
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const maxRetentionMonths = 12 * 100
|
||||
@@ -40,16 +40,16 @@ type Storage struct {
|
||||
tb *table
|
||||
|
||||
// tsidCache is MetricName -> TSID cache.
|
||||
tsidCache *fastcache.Cache
|
||||
tsidCache *workingsetcache.Cache
|
||||
|
||||
// metricIDCache is MetricID -> TSID cache.
|
||||
metricIDCache *fastcache.Cache
|
||||
metricIDCache *workingsetcache.Cache
|
||||
|
||||
// metricNameCache is MetricID -> MetricName cache.
|
||||
metricNameCache *fastcache.Cache
|
||||
metricNameCache *workingsetcache.Cache
|
||||
|
||||
// dateMetricIDCache is (Date, MetricID) cache.
|
||||
dateMetricIDCache *fastcache.Cache
|
||||
dateMetricIDCache *workingsetcache.Cache
|
||||
|
||||
// Fast cache for MetricID values occured during the current hour.
|
||||
currHourMetricIDs atomic.Value
|
||||
@@ -65,6 +65,13 @@ type Storage struct {
|
||||
|
||||
currHourMetricIDsUpdaterWG sync.WaitGroup
|
||||
retentionWatcherWG sync.WaitGroup
|
||||
|
||||
tooSmallTimestampRows uint64
|
||||
tooBigTimestampRows uint64
|
||||
|
||||
addRowsConcurrencyLimitReached uint64
|
||||
addRowsConcurrencyLimitTimeout uint64
|
||||
addRowsConcurrencyDroppedRows uint64
|
||||
}
|
||||
|
||||
// OpenStorage opens storage on the given path with the given number of retention months.
|
||||
@@ -96,13 +103,10 @@ func OpenStorage(path string, retentionMonths int) (*Storage, error) {
|
||||
return nil, fmt.Errorf("cannot create %q: %s", snapshotsPath, err)
|
||||
}
|
||||
|
||||
flockFile := path + "/flock.lock"
|
||||
flockF, err := os.Create(flockFile)
|
||||
// Protect from concurrent opens.
|
||||
flockF, err := fs.CreateFlockFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create lock file %q: %s", flockFile, err)
|
||||
}
|
||||
if err := unix.Flock(int(flockF.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
||||
return nil, fmt.Errorf("cannot acquire lock on file %q: %s", flockFile, err)
|
||||
return nil, err
|
||||
}
|
||||
s.flockF = flockF
|
||||
|
||||
@@ -271,26 +275,35 @@ func (s *Storage) idb() *indexDB {
|
||||
|
||||
// Metrics contains essential metrics for the Storage.
|
||||
type Metrics struct {
|
||||
TooSmallTimestampRows uint64
|
||||
TooBigTimestampRows uint64
|
||||
|
||||
AddRowsConcurrencyLimitReached uint64
|
||||
AddRowsConcurrencyLimitTimeout uint64
|
||||
AddRowsConcurrencyDroppedRows uint64
|
||||
AddRowsConcurrencyCapacity uint64
|
||||
AddRowsConcurrencyCurrent uint64
|
||||
|
||||
TSIDCacheSize uint64
|
||||
TSIDCacheBytesSize uint64
|
||||
TSIDCacheSizeBytes uint64
|
||||
TSIDCacheRequests uint64
|
||||
TSIDCacheMisses uint64
|
||||
TSIDCacheCollisions uint64
|
||||
|
||||
MetricIDCacheSize uint64
|
||||
MetricIDCacheBytesSize uint64
|
||||
MetricIDCacheSizeBytes uint64
|
||||
MetricIDCacheRequests uint64
|
||||
MetricIDCacheMisses uint64
|
||||
MetricIDCacheCollisions uint64
|
||||
|
||||
MetricNameCacheSize uint64
|
||||
MetricNameCacheBytesSize uint64
|
||||
MetricNameCacheSizeBytes uint64
|
||||
MetricNameCacheRequests uint64
|
||||
MetricNameCacheMisses uint64
|
||||
MetricNameCacheCollisions uint64
|
||||
|
||||
DateMetricIDCacheSize uint64
|
||||
DateMetricIDCacheBytesSize uint64
|
||||
DateMetricIDCacheSizeBytes uint64
|
||||
DateMetricIDCacheRequests uint64
|
||||
DateMetricIDCacheMisses uint64
|
||||
DateMetricIDCacheCollisions uint64
|
||||
@@ -308,10 +321,19 @@ func (m *Metrics) Reset() {
|
||||
|
||||
// UpdateMetrics updates m with metrics from s.
|
||||
func (s *Storage) UpdateMetrics(m *Metrics) {
|
||||
m.TooSmallTimestampRows += atomic.LoadUint64(&s.tooSmallTimestampRows)
|
||||
m.TooBigTimestampRows += atomic.LoadUint64(&s.tooBigTimestampRows)
|
||||
|
||||
m.AddRowsConcurrencyLimitReached += atomic.LoadUint64(&s.addRowsConcurrencyLimitReached)
|
||||
m.AddRowsConcurrencyLimitTimeout += atomic.LoadUint64(&s.addRowsConcurrencyLimitTimeout)
|
||||
m.AddRowsConcurrencyDroppedRows += atomic.LoadUint64(&s.addRowsConcurrencyDroppedRows)
|
||||
m.AddRowsConcurrencyCapacity = uint64(cap(addRowsConcurrencyCh))
|
||||
m.AddRowsConcurrencyCurrent = uint64(len(addRowsConcurrencyCh))
|
||||
|
||||
var cs fastcache.Stats
|
||||
s.tsidCache.UpdateStats(&cs)
|
||||
m.TSIDCacheSize += cs.EntriesCount
|
||||
m.TSIDCacheBytesSize += cs.BytesSize
|
||||
m.TSIDCacheSizeBytes += cs.BytesSize
|
||||
m.TSIDCacheRequests += cs.GetCalls
|
||||
m.TSIDCacheMisses += cs.Misses
|
||||
m.TSIDCacheCollisions += cs.Collisions
|
||||
@@ -319,7 +341,7 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
|
||||
cs.Reset()
|
||||
s.metricIDCache.UpdateStats(&cs)
|
||||
m.MetricIDCacheSize += cs.EntriesCount
|
||||
m.MetricIDCacheBytesSize += cs.BytesSize
|
||||
m.MetricIDCacheSizeBytes += cs.BytesSize
|
||||
m.MetricIDCacheRequests += cs.GetCalls
|
||||
m.MetricIDCacheMisses += cs.Misses
|
||||
m.MetricIDCacheCollisions += cs.Collisions
|
||||
@@ -327,7 +349,7 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
|
||||
cs.Reset()
|
||||
s.metricNameCache.UpdateStats(&cs)
|
||||
m.MetricNameCacheSize += cs.EntriesCount
|
||||
m.MetricNameCacheBytesSize += cs.BytesSize
|
||||
m.MetricNameCacheSizeBytes += cs.BytesSize
|
||||
m.MetricNameCacheRequests += cs.GetCalls
|
||||
m.MetricNameCacheMisses += cs.Misses
|
||||
m.MetricNameCacheCollisions += cs.Collisions
|
||||
@@ -335,7 +357,7 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
|
||||
cs.Reset()
|
||||
s.dateMetricIDCache.UpdateStats(&cs)
|
||||
m.DateMetricIDCacheSize += cs.EntriesCount
|
||||
m.DateMetricIDCacheBytesSize += cs.BytesSize
|
||||
m.DateMetricIDCacheSizeBytes += cs.BytesSize
|
||||
m.DateMetricIDCacheRequests += cs.GetCalls
|
||||
m.DateMetricIDCacheMisses += cs.Misses
|
||||
m.DateMetricIDCacheCollisions += cs.Collisions
|
||||
@@ -439,10 +461,10 @@ func (s *Storage) MustClose() {
|
||||
s.idb().MustClose()
|
||||
|
||||
// Save caches.
|
||||
s.mustSaveCache(s.tsidCache, "MetricName->TSID", "metricName_tsid")
|
||||
s.mustSaveCache(s.metricIDCache, "MetricID->TSID", "metricID_tsid")
|
||||
s.mustSaveCache(s.metricNameCache, "MetricID->MetricName", "metricID_metricName")
|
||||
s.mustSaveCache(s.dateMetricIDCache, "Date->MetricID", "date_metricID")
|
||||
s.mustSaveAndStopCache(s.tsidCache, "MetricName->TSID", "metricName_tsid")
|
||||
s.mustSaveAndStopCache(s.metricIDCache, "MetricID->TSID", "metricID_tsid")
|
||||
s.mustSaveAndStopCache(s.metricNameCache, "MetricID->MetricName", "metricID_metricName")
|
||||
s.mustSaveAndStopCache(s.dateMetricIDCache, "Date->MetricID", "date_metricID")
|
||||
|
||||
hmCurr := s.currHourMetricIDs.Load().(*hourMetricIDs)
|
||||
s.mustSaveHourMetricIDs(hmCurr, "curr_hour_metric_ids")
|
||||
@@ -492,7 +514,7 @@ func (s *Storage) mustLoadHourMetricIDs(hour uint64, name string) *hourMetricIDs
|
||||
src = src[8:]
|
||||
m[metricID] = struct{}{}
|
||||
}
|
||||
logger.Infof("loaded %s from %q in %s; entriesCount: %d; bytesSize: %d", name, path, time.Since(startTime), hmLen, srcOrigLen)
|
||||
logger.Infof("loaded %s from %q in %s; entriesCount: %d; sizeBytes: %d", name, path, time.Since(startTime), hmLen, srcOrigLen)
|
||||
return &hourMetricIDs{
|
||||
m: m,
|
||||
hour: hourLoaded,
|
||||
@@ -518,33 +540,32 @@ func (s *Storage) mustSaveHourMetricIDs(hm *hourMetricIDs, name string) {
|
||||
if err := ioutil.WriteFile(path, dst, 0644); err != nil {
|
||||
logger.Panicf("FATAL: cannot write %d bytes to %q: %s", len(dst), path, err)
|
||||
}
|
||||
logger.Infof("saved %s to %q in %s; entriesCount: %d; bytesSize: %d", name, path, time.Since(startTime), len(hm.m), len(dst))
|
||||
logger.Infof("saved %s to %q in %s; entriesCount: %d; sizeBytes: %d", name, path, time.Since(startTime), len(hm.m), len(dst))
|
||||
}
|
||||
|
||||
func (s *Storage) mustLoadCache(info, name string, bytesSize int) *fastcache.Cache {
|
||||
func (s *Storage) mustLoadCache(info, name string, sizeBytes int) *workingsetcache.Cache {
|
||||
path := s.cachePath + "/" + name
|
||||
logger.Infof("loading %s cache from %q...", info, path)
|
||||
startTime := time.Now()
|
||||
c := fastcache.LoadFromFileOrNew(path, bytesSize)
|
||||
c := workingsetcache.Load(path, sizeBytes, time.Hour)
|
||||
var cs fastcache.Stats
|
||||
c.UpdateStats(&cs)
|
||||
logger.Infof("loaded %s cache from %q in %s; entriesCount: %d; bytesSize: %d",
|
||||
logger.Infof("loaded %s cache from %q in %s; entriesCount: %d; sizeBytes: %d",
|
||||
info, path, time.Since(startTime), cs.EntriesCount, cs.BytesSize)
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *Storage) mustSaveCache(c *fastcache.Cache, info, name string) {
|
||||
gomaxprocs := runtime.GOMAXPROCS(-1)
|
||||
func (s *Storage) mustSaveAndStopCache(c *workingsetcache.Cache, info, name string) {
|
||||
path := s.cachePath + "/" + name
|
||||
logger.Infof("saving %s cache to %q...", info, path)
|
||||
startTime := time.Now()
|
||||
if err := c.SaveToFileConcurrent(path, gomaxprocs); err != nil {
|
||||
if err := c.Save(path); err != nil {
|
||||
logger.Panicf("FATAL: cannot save %s cache to %q: %s", info, path, err)
|
||||
}
|
||||
var cs fastcache.Stats
|
||||
c.UpdateStats(&cs)
|
||||
c.Reset()
|
||||
logger.Infof("saved %s cache to %q in %s; entriesCount: %d; bytesSize: %d",
|
||||
c.Stop()
|
||||
logger.Infof("saved %s cache to %q in %s; entriesCount: %d; sizeBytes: %d",
|
||||
info, path, time.Since(startTime), cs.EntriesCount, cs.BytesSize)
|
||||
}
|
||||
|
||||
@@ -713,15 +734,24 @@ func (s *Storage) AddRows(mrs []MetricRow, precisionBits uint8) error {
|
||||
// Limit the number of concurrent goroutines that may add rows to the storage.
|
||||
// This should prevent from out of memory errors and CPU trashing when too many
|
||||
// goroutines call AddRows.
|
||||
t := timerpool.Get(addRowsTimeout)
|
||||
select {
|
||||
case addRowsConcurrencyCh <- struct{}{}:
|
||||
timerpool.Put(t)
|
||||
defer func() { <-addRowsConcurrencyCh }()
|
||||
case <-t.C:
|
||||
timerpool.Put(t)
|
||||
return fmt.Errorf("Cannot add %d rows to storage in %s, since it is overloaded with %d concurrent writers. Add more CPUs or reduce load",
|
||||
len(mrs), addRowsTimeout, cap(addRowsConcurrencyCh))
|
||||
default:
|
||||
// Sleep for a while until giving up
|
||||
atomic.AddUint64(&s.addRowsConcurrencyLimitReached, 1)
|
||||
t := timerpool.Get(addRowsTimeout)
|
||||
select {
|
||||
case addRowsConcurrencyCh <- struct{}{}:
|
||||
timerpool.Put(t)
|
||||
defer func() { <-addRowsConcurrencyCh }()
|
||||
case <-t.C:
|
||||
timerpool.Put(t)
|
||||
atomic.AddUint64(&s.addRowsConcurrencyLimitTimeout, 1)
|
||||
atomic.AddUint64(&s.addRowsConcurrencyDroppedRows, uint64(len(mrs)))
|
||||
return fmt.Errorf("Cannot add %d rows to storage in %s, since it is overloaded with %d concurrent writers. Add more CPUs or reduce load",
|
||||
len(mrs), addRowsTimeout, cap(addRowsConcurrencyCh))
|
||||
}
|
||||
}
|
||||
|
||||
// Add rows to the storage.
|
||||
@@ -752,6 +782,7 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
|
||||
}
|
||||
rows = rows[:rowsLen+len(mrs)]
|
||||
j := 0
|
||||
minTimestamp, maxTimestamp := s.tb.getMinMaxTimestamps()
|
||||
for i := range mrs {
|
||||
mr := &mrs[i]
|
||||
if math.IsNaN(mr.Value) {
|
||||
@@ -759,6 +790,16 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
|
||||
// doesn't know how to work with them.
|
||||
continue
|
||||
}
|
||||
if mr.Timestamp < minTimestamp {
|
||||
// Skip rows with too small timestamps outside the retention.
|
||||
atomic.AddUint64(&s.tooSmallTimestampRows, 1)
|
||||
continue
|
||||
}
|
||||
if mr.Timestamp > maxTimestamp {
|
||||
// Skip rows with too big timestamps significantly exceeding the current time.
|
||||
atomic.AddUint64(&s.tooBigTimestampRows, 1)
|
||||
continue
|
||||
}
|
||||
r := &rows[rowsLen+j]
|
||||
j++
|
||||
r.Timestamp = mr.Timestamp
|
||||
@@ -930,7 +971,7 @@ func (s *Storage) putTSIDToCache(tsid *TSID, metricName []byte) {
|
||||
s.tsidCache.Set(metricName, buf)
|
||||
}
|
||||
|
||||
func openIndexDBTables(path string, metricIDCache, metricNameCache *fastcache.Cache, currHourMetricIDs, prevHourMetricIDs *atomic.Value) (curr, prev *indexDB, err error) {
|
||||
func openIndexDBTables(path string, metricIDCache, metricNameCache *workingsetcache.Cache, currHourMetricIDs, prevHourMetricIDs *atomic.Value) (curr, prev *indexDB, err error) {
|
||||
if err := fs.MkdirAllIfNotExist(path); err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot create directory %q: %s", path, err)
|
||||
}
|
||||
|
||||
@@ -502,12 +502,24 @@ func testStorageDeleteMetrics(s *Storage, workerNum int) error {
|
||||
MaxTimestamp: 2e10,
|
||||
}
|
||||
metricBlocksCount := func(tfs *TagFilters) int {
|
||||
// Verify the number of blocks with fetchData=true
|
||||
n := 0
|
||||
sr.Init(s, []*TagFilters{tfs}, tr, 1e5)
|
||||
sr.Init(s, []*TagFilters{tfs}, tr, true, 1e5)
|
||||
for sr.NextMetricBlock() {
|
||||
n++
|
||||
}
|
||||
sr.MustClose()
|
||||
|
||||
// Make sure the number of blocks with fetchData=false is the same.
|
||||
m := 0
|
||||
sr.Init(s, []*TagFilters{tfs}, tr, false, 1e5)
|
||||
for sr.NextMetricBlock() {
|
||||
m++
|
||||
}
|
||||
sr.MustClose()
|
||||
if n != m {
|
||||
return -1
|
||||
}
|
||||
return n
|
||||
}
|
||||
for i := 0; i < metricsCount; i++ {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// table represents a single table with time series data.
|
||||
@@ -84,13 +83,10 @@ func openTable(path string, retentionMonths int, getDeletedMetricIDs func() map[
|
||||
return nil, fmt.Errorf("cannot create directory for table %q: %s", path, err)
|
||||
}
|
||||
|
||||
flockFile := path + "/flock.lock"
|
||||
flockF, err := os.Create(flockFile)
|
||||
// Protect from concurrent opens.
|
||||
flockF, err := fs.CreateFlockFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create lock file %q: %s", flockFile, err)
|
||||
}
|
||||
if err := unix.Flock(int(flockF.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
||||
return nil, fmt.Errorf("cannot acquire lock on file %q: %s", flockFile, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create directories for small and big partitions if they don't exist yet.
|
||||
@@ -310,22 +306,14 @@ func (tb *table) AddRows(rows []rawRow) error {
|
||||
// The slowest path - there are rows that don't fit any existing partition.
|
||||
// Create new partitions for these rows.
|
||||
// Do this under tb.ptwsLock.
|
||||
now := timestampFromTime(time.Now())
|
||||
minTimestamp := now - tb.retentionMilliseconds
|
||||
maxTimestamp := now + 2*24*3600*1000 // allow max +2 days from now due to timezones shit :)
|
||||
minTimestamp, maxTimestamp := tb.getMinMaxTimestamps()
|
||||
tb.ptwsLock.Lock()
|
||||
var errors []error
|
||||
for i := range missingRows {
|
||||
r := &missingRows[i]
|
||||
|
||||
if r.Timestamp < minTimestamp {
|
||||
// Silently skip row with too small timestamp, since it should be deleted anyway.
|
||||
continue
|
||||
}
|
||||
if r.Timestamp > maxTimestamp {
|
||||
err := fmt.Errorf("cannot add row %+v with too big timestamp to table %q; the timestamp cannot be bigger than %d (+2 days from now)",
|
||||
r, tb.path, maxTimestamp)
|
||||
errors = append(errors, err)
|
||||
if r.Timestamp < minTimestamp || r.Timestamp > maxTimestamp {
|
||||
// Silently skip row outside retention, since it should be deleted anyway.
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -359,6 +347,20 @@ func (tb *table) AddRows(rows []rawRow) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tb *table) getMinMaxTimestamps() (int64, int64) {
|
||||
now := timestampFromTime(time.Now())
|
||||
minTimestamp := now - tb.retentionMilliseconds
|
||||
maxTimestamp := now + 2*24*3600*1000 // allow max +2 days from now due to timezones shit :)
|
||||
if minTimestamp < 0 {
|
||||
// Negative timestamps aren't supported by the storage.
|
||||
minTimestamp = 0
|
||||
}
|
||||
if maxTimestamp < 0 {
|
||||
maxTimestamp = (1 << 63) - 1
|
||||
}
|
||||
return minTimestamp, maxTimestamp
|
||||
}
|
||||
|
||||
func (tb *table) startRetentionWatcher() {
|
||||
tb.retentionWatcherWG.Add(1)
|
||||
go func() {
|
||||
|
||||
@@ -55,7 +55,7 @@ func (ts *tableSearch) reset() {
|
||||
// Init initializes the ts.
|
||||
//
|
||||
// MustClose must be called then the tableSearch is done.
|
||||
func (ts *tableSearch) Init(tb *table, tsids []TSID, tr TimeRange) {
|
||||
func (ts *tableSearch) Init(tb *table, tsids []TSID, tr TimeRange, fetchData bool) {
|
||||
if ts.needClosing {
|
||||
logger.Panicf("BUG: missing MustClose call before the next call to Init")
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func (ts *tableSearch) Init(tb *table, tsids []TSID, tr TimeRange) {
|
||||
}
|
||||
ts.ptsPool = ts.ptsPool[:len(ts.ptws)]
|
||||
for i, ptw := range ts.ptws {
|
||||
ts.ptsPool[i].Init(ptw.pt, tsids, tr)
|
||||
ts.ptsPool[i].Init(ptw.pt, tsids, tr, fetchData)
|
||||
}
|
||||
|
||||
// Initialize the ptsHeap.
|
||||
|
||||
@@ -251,7 +251,7 @@ func testTableSearchSerial(tb *table, tsids []TSID, tr TimeRange, rbsExpected []
|
||||
|
||||
bs := []Block{}
|
||||
var ts tableSearch
|
||||
ts.Init(tb, tsids, tr)
|
||||
ts.Init(tb, tsids, tr, true)
|
||||
for ts.NextBlock() {
|
||||
var b Block
|
||||
b.CopyFrom(ts.Block)
|
||||
@@ -276,7 +276,7 @@ func testTableSearchSerial(tb *table, tsids []TSID, tr TimeRange, rbsExpected []
|
||||
}
|
||||
|
||||
// verify that empty tsids returns empty result
|
||||
ts.Init(tb, []TSID{}, tr)
|
||||
ts.Init(tb, []TSID{}, tr, true)
|
||||
if ts.NextBlock() {
|
||||
return fmt.Errorf("unexpected block got for an empty tsids list: %+v", ts.Block)
|
||||
}
|
||||
@@ -284,5 +284,15 @@ func testTableSearchSerial(tb *table, tsids []TSID, tr TimeRange, rbsExpected []
|
||||
return fmt.Errorf("unexpected error on empty tsids list: %s", err)
|
||||
}
|
||||
ts.MustClose()
|
||||
|
||||
ts.Init(tb, []TSID{}, tr, false)
|
||||
if ts.NextBlock() {
|
||||
return fmt.Errorf("unexpected block got for an empty tsids list with fetchData=false: %+v", ts.Block)
|
||||
}
|
||||
if err := ts.Error(); err != nil {
|
||||
return fmt.Errorf("unexpected error on empty tsids list with fetchData=false: %s", err)
|
||||
}
|
||||
ts.MustClose()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ func BenchmarkTableSearch(b *testing.B) {
|
||||
b.Run(fmt.Sprintf("tsidsCount_%d", tsidsCount), func(b *testing.B) {
|
||||
for _, tsidsSearch := range []int{1, 1e1, 1e2, 1e3, 1e4} {
|
||||
b.Run(fmt.Sprintf("tsidsSearch_%d", tsidsSearch), func(b *testing.B) {
|
||||
benchmarkTableSearch(b, rowsCount, tsidsCount, tsidsSearch)
|
||||
for _, fetchData := range []bool{true, false} {
|
||||
b.Run(fmt.Sprintf("fetchData_%v", fetchData), func(b *testing.B) {
|
||||
benchmarkTableSearch(b, rowsCount, tsidsCount, tsidsSearch, fetchData)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -103,9 +107,9 @@ func createBenchTable(b *testing.B, path string, startTimestamp int64, rowsPerIn
|
||||
tb.MustClose()
|
||||
}
|
||||
|
||||
func benchmarkTableSearch(b *testing.B, rowsCount, tsidsCount, tsidsSearch int) {
|
||||
func benchmarkTableSearch(b *testing.B, rowsCount, tsidsCount, tsidsSearch int, fetchData bool) {
|
||||
startTimestamp := timestampFromTime(time.Now()) - 365*24*3600*1000
|
||||
rowsPerInsert := int(maxRawRowsPerPartition)
|
||||
rowsPerInsert := getMaxRawRowsPerPartition()
|
||||
|
||||
tb := openBenchTable(b, startTimestamp, rowsPerInsert, rowsCount, tsidsCount)
|
||||
tr := TimeRange{
|
||||
@@ -127,7 +131,7 @@ func benchmarkTableSearch(b *testing.B, rowsCount, tsidsCount, tsidsSearch int)
|
||||
for i := range tsids {
|
||||
tsids[i].MetricID = 1 + uint64(i)
|
||||
}
|
||||
ts.Init(tb, tsids, tr)
|
||||
ts.Init(tb, tsids, tr, fetchData)
|
||||
for ts.NextBlock() {
|
||||
}
|
||||
ts.MustClose()
|
||||
|
||||
@@ -168,15 +168,14 @@ func (tf *tagFilter) Init(commonPrefix, key, value []byte, isNegative, isRegexp
|
||||
tf.isRegexp = false
|
||||
}
|
||||
}
|
||||
tf.prefix = marshalTagValue(tf.prefix, prefix)
|
||||
if tf.isRegexp {
|
||||
// Remove the trailing tagSeparatorChar from the prefix.
|
||||
tf.prefix = tf.prefix[:len(tf.prefix)-1]
|
||||
}
|
||||
if len(expr) == 0 {
|
||||
tf.prefix = marshalTagValueNoTrailingTagSeparator(tf.prefix, prefix)
|
||||
if !tf.isRegexp {
|
||||
// tf contains plain value without regexp.
|
||||
// Add empty orSuffix in order to trigger fast path for orSuffixes
|
||||
// during the search for matching metricIDs.
|
||||
tf.orSuffixes = append(tf.orSuffixes[:0], "")
|
||||
return nil
|
||||
}
|
||||
|
||||
rcv, err := getRegexpFromCache(expr)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -187,16 +186,14 @@ func (tf *tagFilter) Init(commonPrefix, key, value []byte, isNegative, isRegexp
|
||||
}
|
||||
|
||||
func (tf *tagFilter) matchSuffix(b []byte) (bool, error) {
|
||||
if !tf.isRegexp {
|
||||
return len(b) == 0, nil
|
||||
}
|
||||
|
||||
// Remove the trailing tagSeparatorChar before applying the regexp.
|
||||
// Remove the trailing tagSeparatorChar.
|
||||
if len(b) == 0 || b[len(b)-1] != tagSeparatorChar {
|
||||
return false, fmt.Errorf("unexpected end of b; want %d; b=%q", tagSeparatorChar, b)
|
||||
}
|
||||
b = b[:len(b)-1]
|
||||
|
||||
if !tf.isRegexp {
|
||||
return len(b) == 0, nil
|
||||
}
|
||||
ok := tf.reSuffixMatch(b)
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
@@ -54,22 +54,23 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
key := []byte("key")
|
||||
var tf tagFilter
|
||||
|
||||
tv := func(s string) string {
|
||||
return string(marshalTagValue(nil, []byte(s)))
|
||||
tvNoTrailingTagSeparator := func(s string) string {
|
||||
return string(marshalTagValueNoTrailingTagSeparator(nil, []byte(s)))
|
||||
}
|
||||
init := func(value string, isNegative, isRegexp bool, expectedPrefix string) {
|
||||
t.Helper()
|
||||
if err := tf.Init(commonPrefix, key, []byte(value), isNegative, isRegexp); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
prefix := string(commonPrefix) + tv(string(key)) + expectedPrefix
|
||||
prefix := string(commonPrefix) + string(marshalTagValue(nil, []byte(key))) + expectedPrefix
|
||||
if prefix != string(tf.prefix) {
|
||||
t.Fatalf("unexpected tf.prefix; got %q; want %q", tf.prefix, prefix)
|
||||
}
|
||||
}
|
||||
match := func(suffix string) {
|
||||
t.Helper()
|
||||
ok, err := tf.matchSuffix([]byte(suffix))
|
||||
suffixEscaped := marshalTagValue(nil, []byte(suffix))
|
||||
ok, err := tf.matchSuffix(suffixEscaped)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
@@ -79,7 +80,8 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
}
|
||||
mismatch := func(suffix string) {
|
||||
t.Helper()
|
||||
ok, err := tf.matchSuffix([]byte(suffix))
|
||||
suffixEscaped := marshalTagValue(nil, []byte(suffix))
|
||||
ok, err := tf.matchSuffix(suffixEscaped)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
@@ -92,7 +94,7 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
value := "xx"
|
||||
isNegative := false
|
||||
isRegexp := false
|
||||
expectedPrefix := tv(value)
|
||||
expectedPrefix := tvNoTrailingTagSeparator(value)
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Plain value must match empty suffix only
|
||||
@@ -104,7 +106,7 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
value := "xx"
|
||||
isNegative := true
|
||||
isRegexp := false
|
||||
expectedPrefix := tv(value)
|
||||
expectedPrefix := tvNoTrailingTagSeparator(value)
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Negaitve plain value must match all except empty suffix
|
||||
@@ -119,7 +121,7 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
value := "http"
|
||||
isNegative := false
|
||||
isRegexp := true
|
||||
expectedPrefix := tv("http")
|
||||
expectedPrefix := tvNoTrailingTagSeparator(value)
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match only empty suffix
|
||||
@@ -132,7 +134,7 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
value := "http"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := tv("http")
|
||||
expectedPrefix := tvNoTrailingTagSeparator(value)
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match all except empty suffix
|
||||
@@ -147,164 +149,220 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
value := "http.*"
|
||||
isNegative := false
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match any suffix
|
||||
match(tv(""))
|
||||
match(tv("x"))
|
||||
match(tv("http"))
|
||||
match(tv("foobar"))
|
||||
match("")
|
||||
match("x")
|
||||
match("http")
|
||||
match("foobar")
|
||||
})
|
||||
t.Run("negative-regexp-prefix-any-suffix", func(t *testing.T) {
|
||||
value := "http.*"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Mustn't match any suffix
|
||||
mismatch(tv(""))
|
||||
mismatch(tv("x"))
|
||||
mismatch(tv("xhttp"))
|
||||
mismatch(tv("http"))
|
||||
mismatch(tv("httpsdf"))
|
||||
mismatch(tv("foobar"))
|
||||
mismatch("")
|
||||
mismatch("x")
|
||||
mismatch("xhttp")
|
||||
mismatch("http")
|
||||
mismatch("httpsdf")
|
||||
mismatch("foobar")
|
||||
})
|
||||
t.Run("regexp-prefix-contains-suffix", func(t *testing.T) {
|
||||
value := "http.*foo.*"
|
||||
isNegative := false
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match any suffix with `foo`
|
||||
mismatch(tv(""))
|
||||
mismatch(tv("x"))
|
||||
mismatch(tv("http"))
|
||||
match(tv("foo"))
|
||||
match(tv("foobar"))
|
||||
match(tv("xfoobar"))
|
||||
match(tv("xfoo"))
|
||||
mismatch("")
|
||||
mismatch("x")
|
||||
mismatch("http")
|
||||
match("foo")
|
||||
match("foobar")
|
||||
match("xfoobar")
|
||||
match("xfoo")
|
||||
})
|
||||
t.Run("negative-regexp-prefix-contains-suffix", func(t *testing.T) {
|
||||
value := "http.*foo.*"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match any suffix without `foo`
|
||||
match(tv(""))
|
||||
match(tv("x"))
|
||||
match(tv("http"))
|
||||
mismatch(tv("foo"))
|
||||
mismatch(tv("foobar"))
|
||||
mismatch(tv("xfoobar"))
|
||||
mismatch(tv("xfoo"))
|
||||
mismatch(tv("httpfoo"))
|
||||
mismatch(tv("httpfoobar"))
|
||||
mismatch(tv("httpxfoobar"))
|
||||
mismatch(tv("httpxfoo"))
|
||||
match("")
|
||||
match("x")
|
||||
match("http")
|
||||
mismatch("foo")
|
||||
mismatch("foobar")
|
||||
mismatch("xfoobar")
|
||||
mismatch("xfoo")
|
||||
mismatch("httpfoo")
|
||||
mismatch("httpfoobar")
|
||||
mismatch("httpxfoobar")
|
||||
mismatch("httpxfoo")
|
||||
})
|
||||
t.Run("negative-regexp-noprefix-contains-suffix", func(t *testing.T) {
|
||||
value := ".*foo.*"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := ""
|
||||
expectedPrefix := tvNoTrailingTagSeparator("")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match anything not matching `.*foo.*`
|
||||
match(tv(""))
|
||||
match(tv("x"))
|
||||
match(tv("http"))
|
||||
mismatch(tv("foo"))
|
||||
mismatch(tv("foobar"))
|
||||
mismatch(tv("xfoobar"))
|
||||
mismatch(tv("xfoo"))
|
||||
match("")
|
||||
match("x")
|
||||
match("http")
|
||||
mismatch("foo")
|
||||
mismatch("foobar")
|
||||
mismatch("xfoobar")
|
||||
mismatch("xfoo")
|
||||
})
|
||||
t.Run("regexp-prefix-special-suffix", func(t *testing.T) {
|
||||
value := "http.*bar"
|
||||
isNegative := false
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match suffix ending on bar
|
||||
mismatch(tv(""))
|
||||
mismatch(tv("x"))
|
||||
match(tv("bar"))
|
||||
mismatch(tv("barx"))
|
||||
match(tv("foobar"))
|
||||
mismatch(tv("foobarx"))
|
||||
mismatch("")
|
||||
mismatch("x")
|
||||
match("bar")
|
||||
mismatch("barx")
|
||||
match("foobar")
|
||||
mismatch("foobarx")
|
||||
})
|
||||
t.Run("negative-regexp-prefix-special-suffix", func(t *testing.T) {
|
||||
value := "http.*bar"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Mustn't match suffix ending on bar
|
||||
match(tv(""))
|
||||
mismatch(tv("bar"))
|
||||
mismatch(tv("xhttpbar"))
|
||||
mismatch(tv("httpbar"))
|
||||
match(tv("httpbarx"))
|
||||
mismatch(tv("httpxybar"))
|
||||
match(tv("httpxybarx"))
|
||||
mismatch(tv("ahttpxybar"))
|
||||
match("")
|
||||
mismatch("bar")
|
||||
mismatch("xhttpbar")
|
||||
mismatch("httpbar")
|
||||
match("httpbarx")
|
||||
mismatch("httpxybar")
|
||||
match("httpxybarx")
|
||||
mismatch("ahttpxybar")
|
||||
})
|
||||
t.Run("negative-regexp-noprefix-special-suffix", func(t *testing.T) {
|
||||
value := ".*bar"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := ""
|
||||
expectedPrefix := tvNoTrailingTagSeparator("")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
|
||||
// Must match all except the regexp from value
|
||||
match(tv(""))
|
||||
mismatch(tv("bar"))
|
||||
mismatch(tv("xhttpbar"))
|
||||
match(tv("barx"))
|
||||
match(tv("pbarx"))
|
||||
match("")
|
||||
mismatch("bar")
|
||||
mismatch("xhttpbar")
|
||||
match("barx")
|
||||
match("pbarx")
|
||||
})
|
||||
t.Run("regexp-or-suffixes", func(t *testing.T) {
|
||||
value := "http(foo|bar)"
|
||||
isNegative := false
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
if !reflect.DeepEqual(tf.orSuffixes, []string{"bar", "foo"}) {
|
||||
t.Fatalf("unexpected orSuffixes; got %q; want %q", tf.orSuffixes, []string{"bar", "foo"})
|
||||
}
|
||||
|
||||
// Must match foo or bar suffix
|
||||
mismatch(tv(""))
|
||||
mismatch(tv("x"))
|
||||
match(tv("bar"))
|
||||
mismatch(tv("barx"))
|
||||
match(tv("foo"))
|
||||
mismatch(tv("foobar"))
|
||||
mismatch("")
|
||||
mismatch("x")
|
||||
match("bar")
|
||||
mismatch("barx")
|
||||
match("foo")
|
||||
mismatch("foobar")
|
||||
})
|
||||
t.Run("negative-regexp-or-suffixes", func(t *testing.T) {
|
||||
value := "http(foo|bar)"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := "http"
|
||||
expectedPrefix := tvNoTrailingTagSeparator("http")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
if !reflect.DeepEqual(tf.orSuffixes, []string{"bar", "foo"}) {
|
||||
t.Fatalf("unexpected or suffixes; got %q; want %q", tf.orSuffixes, []string{"bar", "foo"})
|
||||
}
|
||||
|
||||
// Mustn't match foo or bar suffix
|
||||
match(tv(""))
|
||||
match(tv("x"))
|
||||
mismatch(tv("foo"))
|
||||
match(tv("fooa"))
|
||||
match(tv("xfooa"))
|
||||
mismatch(tv("bar"))
|
||||
match(tv("xhttpbar"))
|
||||
match("")
|
||||
match("x")
|
||||
mismatch("foo")
|
||||
match("fooa")
|
||||
match("xfooa")
|
||||
mismatch("bar")
|
||||
match("xhttpbar")
|
||||
})
|
||||
t.Run("non-empty-string-regexp-negative-match", func(t *testing.T) {
|
||||
value := ".+"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := tvNoTrailingTagSeparator("")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
if len(tf.orSuffixes) != 0 {
|
||||
t.Fatalf("unexpected non-zero number of or suffixes: %d; %q", len(tf.orSuffixes), tf.orSuffixes)
|
||||
}
|
||||
|
||||
match("")
|
||||
mismatch("x")
|
||||
mismatch("foo")
|
||||
})
|
||||
t.Run("non-empty-string-regexp-match", func(t *testing.T) {
|
||||
value := ".+"
|
||||
isNegative := false
|
||||
isRegexp := true
|
||||
expectedPrefix := tvNoTrailingTagSeparator("")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
if len(tf.orSuffixes) != 0 {
|
||||
t.Fatalf("unexpected non-zero number of or suffixes: %d; %q", len(tf.orSuffixes), tf.orSuffixes)
|
||||
}
|
||||
|
||||
mismatch("")
|
||||
match("x")
|
||||
match("foo")
|
||||
})
|
||||
t.Run("match-all-regexp-negative-match", func(t *testing.T) {
|
||||
value := ".*"
|
||||
isNegative := true
|
||||
isRegexp := true
|
||||
expectedPrefix := tvNoTrailingTagSeparator("")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
if len(tf.orSuffixes) != 0 {
|
||||
t.Fatalf("unexpected non-zero number of or suffixes: %d; %q", len(tf.orSuffixes), tf.orSuffixes)
|
||||
}
|
||||
|
||||
mismatch("")
|
||||
mismatch("x")
|
||||
mismatch("foo")
|
||||
})
|
||||
t.Run("match-all-regexp-match", func(t *testing.T) {
|
||||
value := ".*"
|
||||
isNegative := false
|
||||
isRegexp := true
|
||||
expectedPrefix := tvNoTrailingTagSeparator("")
|
||||
init(value, isNegative, isRegexp, expectedPrefix)
|
||||
if len(tf.orSuffixes) != 0 {
|
||||
t.Fatalf("unexpected non-zero number of or suffixes: %d; %q", len(tf.orSuffixes), tf.orSuffixes)
|
||||
}
|
||||
|
||||
match("")
|
||||
match("x")
|
||||
match("foo")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
255
lib/workingsetcache/cache.go
Normal file
255
lib/workingsetcache/cache.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package workingsetcache
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
)
|
||||
|
||||
// Cache is a cache for working set entries.
|
||||
//
|
||||
// The cache evicts inactive entries after the given expireDuration.
|
||||
// Recently accessed entries survive expireDuration.
|
||||
//
|
||||
// Comparing to fastcache, this cache minimizes the required RAM size
|
||||
// to values smaller than maxBytes.
|
||||
type Cache struct {
|
||||
curr atomic.Value
|
||||
prev atomic.Value
|
||||
|
||||
// skipPrev indicates whether to use only curr and skip prev.
|
||||
//
|
||||
// This flag is set if curr is filled for more than 50% space.
|
||||
// In this case using prev would result in RAM waste,
|
||||
// it is better to use only curr cache with doubled size.
|
||||
skipPrev uint64
|
||||
|
||||
// mu serializes access to curr, prev and skipPrev
|
||||
// in expirationWorker and cacheSizeWatcher.
|
||||
mu sync.Mutex
|
||||
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
|
||||
misses uint64
|
||||
}
|
||||
|
||||
// Load loads the cache from filePath and limits its size to maxBytes
|
||||
// and evicts inactive entires after expireDuration.
|
||||
//
|
||||
// Stop must be called on the returned cache when it is no longer needed.
|
||||
func Load(filePath string, maxBytes int, expireDuration time.Duration) *Cache {
|
||||
// Split maxBytes between curr and prev caches.
|
||||
maxBytes /= 2
|
||||
curr := fastcache.LoadFromFileOrNew(filePath, maxBytes)
|
||||
return newWorkingSetCache(curr, maxBytes, expireDuration)
|
||||
}
|
||||
|
||||
// New creates new cache with the given maxBytes size and the given expireDuration
|
||||
// for inactive entries.
|
||||
//
|
||||
// Stop must be called on the returned cache when it is no longer needed.
|
||||
func New(maxBytes int, expireDuration time.Duration) *Cache {
|
||||
// Split maxBytes between curr and prev caches.
|
||||
maxBytes /= 2
|
||||
curr := fastcache.New(maxBytes)
|
||||
return newWorkingSetCache(curr, maxBytes, expireDuration)
|
||||
}
|
||||
|
||||
func newWorkingSetCache(curr *fastcache.Cache, maxBytes int, expireDuration time.Duration) *Cache {
|
||||
prev := fastcache.New(1024)
|
||||
var c Cache
|
||||
c.curr.Store(curr)
|
||||
c.prev.Store(prev)
|
||||
c.stopCh = make(chan struct{})
|
||||
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
c.expirationWorker(maxBytes, expireDuration)
|
||||
}()
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
c.cacheSizeWatcher(maxBytes)
|
||||
}()
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Cache) expirationWorker(maxBytes int, expireDuration time.Duration) {
|
||||
t := time.NewTicker(expireDuration / 2)
|
||||
for {
|
||||
select {
|
||||
case <-c.stopCh:
|
||||
t.Stop()
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if atomic.LoadUint64(&c.skipPrev) != 0 {
|
||||
// Expire prev cache and create fresh curr cache.
|
||||
// Do not reuse prev cache, since it can have too big capacity.
|
||||
prev := c.prev.Load().(*fastcache.Cache)
|
||||
prev.Reset()
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
c.prev.Store(curr)
|
||||
curr = fastcache.New(maxBytes)
|
||||
c.curr.Store(curr)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) cacheSizeWatcher(maxBytes int) {
|
||||
t := time.NewTicker(time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-c.stopCh:
|
||||
t.Stop()
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
var cs fastcache.Stats
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
curr.UpdateStats(&cs)
|
||||
if cs.BytesSize < uint64(maxBytes)/2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// curr cache size exceeds 50% of its capacity. It is better
|
||||
// to double the size of curr cache and stop using prev cache,
|
||||
// since this will result in higher summary cache capacity.
|
||||
c.mu.Lock()
|
||||
curr.Reset()
|
||||
prev := c.prev.Load().(*fastcache.Cache)
|
||||
prev.Reset()
|
||||
curr = fastcache.New(maxBytes * 2)
|
||||
c.curr.Store(curr)
|
||||
atomic.StoreUint64(&c.skipPrev, 1)
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Save safes the cache to filePath.
|
||||
func (c *Cache) Save(filePath string) error {
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
concurrency := runtime.GOMAXPROCS(-1)
|
||||
return curr.SaveToFileConcurrent(filePath, concurrency)
|
||||
}
|
||||
|
||||
// Stop stops the cache.
|
||||
//
|
||||
// The cache cannot be used after the Stop call.
|
||||
func (c *Cache) Stop() {
|
||||
close(c.stopCh)
|
||||
c.wg.Wait()
|
||||
|
||||
c.Reset()
|
||||
}
|
||||
|
||||
// Reset resets the cache.
|
||||
func (c *Cache) Reset() {
|
||||
prev := c.prev.Load().(*fastcache.Cache)
|
||||
prev.Reset()
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
curr.Reset()
|
||||
|
||||
c.misses = 0
|
||||
}
|
||||
|
||||
// UpdateStats updates fcs with cache stats.
|
||||
func (c *Cache) UpdateStats(fcs *fastcache.Stats) {
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
fcsOrig := *fcs
|
||||
curr.UpdateStats(fcs)
|
||||
if atomic.LoadUint64(&c.skipPrev) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fcs.Misses = fcsOrig.Misses + atomic.LoadUint64(&c.misses)
|
||||
fcsOrig.Reset()
|
||||
prev := c.prev.Load().(*fastcache.Cache)
|
||||
prev.UpdateStats(&fcsOrig)
|
||||
fcs.EntriesCount += fcsOrig.EntriesCount
|
||||
fcs.BytesSize += fcsOrig.BytesSize
|
||||
}
|
||||
|
||||
// Get appends the found value for the given key to dst and returns the result.
|
||||
func (c *Cache) Get(dst, key []byte) []byte {
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
result := curr.Get(dst, key)
|
||||
if len(result) > len(dst) {
|
||||
// Fast path - the entry is found in the current cache.
|
||||
return result
|
||||
}
|
||||
if atomic.LoadUint64(&c.skipPrev) != 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// Search for the entry in the previous cache.
|
||||
prev := c.prev.Load().(*fastcache.Cache)
|
||||
result = prev.Get(dst, key)
|
||||
if len(result) <= len(dst) {
|
||||
// Nothing found.
|
||||
atomic.AddUint64(&c.misses, 1)
|
||||
return result
|
||||
}
|
||||
// Cache the found entry in the current cache.
|
||||
curr.Set(key, result[len(dst):])
|
||||
return result
|
||||
}
|
||||
|
||||
// Has verifies whether the cahce contains the given key.
|
||||
func (c *Cache) Has(key []byte) bool {
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
if curr.Has(key) {
|
||||
return true
|
||||
}
|
||||
if atomic.LoadUint64(&c.skipPrev) != 0 {
|
||||
return false
|
||||
}
|
||||
prev := c.prev.Load().(*fastcache.Cache)
|
||||
return prev.Has(key)
|
||||
}
|
||||
|
||||
// Set sets the given value for the given key.
|
||||
func (c *Cache) Set(key, value []byte) {
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
curr.Set(key, value)
|
||||
}
|
||||
|
||||
// GetBig appends the found value for the given key to dst and returns the result.
|
||||
func (c *Cache) GetBig(dst, key []byte) []byte {
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
result := curr.GetBig(dst, key)
|
||||
if len(result) > len(dst) {
|
||||
// Fast path - the entry is found in the current cache.
|
||||
return result
|
||||
}
|
||||
if atomic.LoadUint64(&c.skipPrev) != 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// Search for the entry in the previous cache.
|
||||
prev := c.prev.Load().(*fastcache.Cache)
|
||||
result = prev.GetBig(dst, key)
|
||||
if len(result) <= len(dst) {
|
||||
// Nothing found.
|
||||
atomic.AddUint64(&c.misses, 1)
|
||||
return result
|
||||
}
|
||||
// Cache the found entry in the current cache.
|
||||
curr.SetBig(key, result[len(dst):])
|
||||
return result
|
||||
}
|
||||
|
||||
// SetBig sets the given value for the given key.
|
||||
func (c *Cache) SetBig(key, value []byte) {
|
||||
curr := c.curr.Load().(*fastcache.Cache)
|
||||
curr.SetBig(key, value)
|
||||
}
|
||||
1
package/VAR_BUILD
Normal file
1
package/VAR_BUILD
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
1
package/VAR_VERSION
Normal file
1
package/VAR_VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.7.0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user