mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-07 19:06:17 +03:00
Compare commits
93 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 |
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.
|
||||
14
Makefile
14
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:
|
||||
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/...
|
||||
|
||||
237
README.md
237
README.md
@@ -1,4 +1,5 @@
|
||||
[](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)
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
## 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).
|
||||
@@ -26,8 +27,8 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
|
||||
[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)
|
||||
and [comparing Thanos to VictoriaMetrics cluster](https://medium.com/@valyala/comparing-thanos-to-victoriametrics-cluster-b193bea1683).
|
||||
@@ -52,20 +53,24 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
|
||||
|
||||
### Table of contents
|
||||
|
||||
- [How to build from sources](#how-to-build-from-sources)
|
||||
- [Development build](#development-build)
|
||||
- [Production build](#production-build)
|
||||
- [Building docker images](#building-docker-images)
|
||||
- [How to start VictoriaMetrics](#how-to-start-victoriametrics)
|
||||
- [Setting up service](#setting-up-service)
|
||||
- [Third-party contributions](#third-party-contributions)
|
||||
- [Prometheus setup](#prometheus-setup)
|
||||
- [Grafana setup](#grafana-setup)
|
||||
- [How to upgrade VictoriaMetrics?](#how-to-upgrade-victoriametrics)
|
||||
- [How to apply new config to VictoriaMetrics?](#how-to-apply-new-config-to-victoriametrics)
|
||||
- [How to send data from InfluxDB-compatible agents such as Telegraf?](#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf)
|
||||
- [How to send data from Graphite-compatible agents such as StatsD?](#how-to-send-data-from-graphite-compatible-agents-such-as-statsd)
|
||||
- [Querying Graphite data](#querying-graphite-data)
|
||||
- [How to send data from OpenTSDB-compatible agents?](#how-to-send-data-from-opentsdb-compatible-agents)
|
||||
- [How to build from sources](#how-to-build-from-sources)
|
||||
- [Development build](#development-build)
|
||||
- [Production build](#production-build)
|
||||
- [ARM build](#arm-build)
|
||||
- [Pure Go build (CGO_ENABLED=0)](#pure-go-build-cgo_enabled0)
|
||||
- [Building docker images](#building-docker-images)
|
||||
- [Start with docker-compose](#start-with-docker-compose)
|
||||
- [Setting up service](#setting-up-service)
|
||||
- [Third-party contributions](#third-party-contributions)
|
||||
- [How to work with snapshots?](#how-to-work-with-snapshots)
|
||||
- [How to delete time series?](#how-to-delete-time-series)
|
||||
- [How to export time series?](#how-to-export-time-series)
|
||||
@@ -81,6 +86,7 @@ Cluster version is available [here](https://github.com/VictoriaMetrics/VictoriaM
|
||||
- [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,7 @@ 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,
|
||||
@@ -217,7 +189,7 @@ VictoriaMetrics must be restarted for applying new config:
|
||||
|
||||
1) Send `SIGINT` signal to VictoriaMetrics process in order to gracefully stop it.
|
||||
2) Wait until the process stops. This can take a few seconds.
|
||||
3) Start VictoriaMetrics with new config.
|
||||
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/)?
|
||||
@@ -249,8 +221,8 @@ 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](https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_tutorial/)
|
||||
@@ -260,7 +232,7 @@ 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:
|
||||
|
||||
```
|
||||
@@ -274,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)?
|
||||
|
||||
@@ -294,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:
|
||||
|
||||
```
|
||||
@@ -309,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,
|
||||
@@ -327,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:
|
||||
|
||||
```
|
||||
@@ -341,9 +324,79 @@ The `/api/v1/export` endpoint should return the following response:
|
||||
```
|
||||
|
||||
|
||||
### How to build from sources
|
||||
|
||||
We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) or
|
||||
[docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/) instead of building VictoriaMetrics
|
||||
from sources. Building from sources is reasonable when developing additional features specific
|
||||
to your needs.
|
||||
|
||||
|
||||
#### Development build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.12.
|
||||
2. Run `make victoria-metrics` from the root folder of the repository.
|
||||
It builds `victoria-metrics` binary and puts it into the `bin` folder.
|
||||
|
||||
#### Production build
|
||||
|
||||
1. [Install docker](https://docs.docker.com/install/).
|
||||
2. Run `make victoria-metrics-prod` from the root folder of the repository.
|
||||
It builds `victoria-metrics-prod` binary and puts it into the `bin` folder.
|
||||
|
||||
#### ARM build
|
||||
|
||||
ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://blog.cloudflare.com/arm-takes-wing/).
|
||||
|
||||
#### Development ARM build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.12.
|
||||
2. Run `make victoria-metrics-arm` or `make victoria-metrics-arm64` from the root folder of the repository.
|
||||
It builds `victoria-metrics-arm` or `victoria-metrics-arm64` binary respectively and puts it into the `bin` folder.
|
||||
|
||||
#### Production ARM build
|
||||
|
||||
1. [Install docker](https://docs.docker.com/install/).
|
||||
2. Run `make victoria-metrics-arm-prod` or `make victoria-metrics-arm64-prod` from the root folder of the repository.
|
||||
It builds `victoria-metrics-arm-prod` or `victoria-metrics-arm64-prod` binary respectively and puts it into the `bin` folder.
|
||||
|
||||
#### Pure Go build (CGO_ENABLED=0)
|
||||
|
||||
`Pure Go` mode builds only Go code without [cgo](https://golang.org/cmd/cgo/) dependencies.
|
||||
This is an experimental mode, which may result in a lower compression ratio and slower decompression performance.
|
||||
Use it with caution!
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.12.
|
||||
2. Run `make victoria-metrics-pure` from the root folder of the repository.
|
||||
It builds `victoria-metrics-pure` binary and puts it into the `bin` folder.
|
||||
|
||||
#### Building docker images
|
||||
|
||||
Run `make package-victoria-metrics`. It builds `victoriametrics/victoria-metrics:<PKG_TAG>` docker image locally.
|
||||
`<PKG_TAG>` is auto-generated image tag, which depends on source code in the repository.
|
||||
The `<PKG_TAG>` may be manually set via `PKG_TAG=foobar make package-victoria-metrics`.
|
||||
|
||||
|
||||
### Start with docker-compose
|
||||
|
||||
[Docker-compose](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/docker-compose.yml)
|
||||
helps to spin up VictoriaMetrics, Prometheus and Grafana with one command.
|
||||
More details may be found [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#folder-contains-basic-images-and-tools-for-building-and-running-victoria-metrics-in-docker).
|
||||
|
||||
|
||||
### Setting up service
|
||||
|
||||
Read [these instructions](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/43) on how to set up VictoriaMetrics as a service in your OS.
|
||||
|
||||
|
||||
### Third-party contributions
|
||||
|
||||
* [Unofficial yum repository](https://copr.fedorainfracloud.org/coprs/antonpatsev/VictoriaMetrics/) ([source code](https://github.com/patsevanton/victoriametrics-rpm))
|
||||
|
||||
|
||||
### How to work with snapshots?
|
||||
|
||||
VictoriaMetrics 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:
|
||||
@@ -400,15 +453,15 @@ 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.
|
||||
@@ -422,12 +475,29 @@ Rough estimation of the required resources:
|
||||
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
|
||||
|
||||
@@ -457,7 +527,7 @@ kill -HUP `pidof prometheus`
|
||||
|
||||
|
||||
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`.
|
||||
to write data to `victoriametrics-addr-1`, while each `r2` should write data to `victoriametrics-addr-2`.
|
||||
|
||||
|
||||
### Multiple retentions
|
||||
@@ -477,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.
|
||||
|
||||
|
||||
@@ -489,7 +559,7 @@ 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
|
||||
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
|
||||
@@ -505,7 +575,7 @@ on [Prometheus side](https://prometheus.io/docs/alerting/overview/) or on [Grafa
|
||||
|
||||
### 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.
|
||||
@@ -520,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.
|
||||
|
||||
|
||||
@@ -558,11 +628,26 @@ The most interesting metrics are:
|
||||
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
|
||||
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
|
||||
|
||||
@@ -579,7 +664,7 @@ Feel free asking any questions regarding VictoriaMetrics:
|
||||
- [google groups](https://groups.google.com/forum/#!forum/victorametrics-users)
|
||||
|
||||
|
||||
If you like VictoriaMetrics and want contributing, then we need the following:
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -22,7 +22,10 @@ var (
|
||||
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,13 +386,16 @@ func SeriesHandler(w http.ResponseWriter, r *http.Request) error {
|
||||
if len(matches) == 0 {
|
||||
return fmt.Errorf("missing `match[]` arg")
|
||||
}
|
||||
// Set start to minTimeMsecs by default as Prometheus does.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/91
|
||||
start, err := getTime(r, "start", minTimeMsecs)
|
||||
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
|
||||
}
|
||||
@@ -333,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)
|
||||
}
|
||||
@@ -341,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
|
||||
|
||||
@@ -312,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]
|
||||
}
|
||||
|
||||
@@ -13,30 +13,37 @@ import (
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -44,8 +51,8 @@ var incrementalAggrFuncCallbacksMap = map[string]*incrementalAggrFuncCallbacks{
|
||||
type incrementalAggrFuncContext struct {
|
||||
ae *aggrFuncExpr
|
||||
|
||||
mu sync.Mutex
|
||||
m map[string]*incrementalAggrContext
|
||||
mLock sync.Mutex
|
||||
m map[uint]map[string]*incrementalAggrContext
|
||||
|
||||
callbacks *incrementalAggrFuncCallbacks
|
||||
}
|
||||
@@ -53,17 +60,24 @@ type incrementalAggrFuncContext struct {
|
||||
func newIncrementalAggrFuncContext(ae *aggrFuncExpr, callbacks *incrementalAggrFuncCallbacks) *incrementalAggrFuncContext {
|
||||
return &incrementalAggrFuncContext{
|
||||
ae: ae,
|
||||
m: make(map[string]*incrementalAggrContext, 1),
|
||||
m: make(map[uint]map[string]*incrementalAggrContext),
|
||||
callbacks: callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
func (iafc *incrementalAggrFuncContext) updateTimeseries(ts *timeseries) {
|
||||
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)
|
||||
iafc.mu.Lock()
|
||||
iac := iafc.m[string(bb.B)]
|
||||
iac := m[string(bb.B)]
|
||||
if iac == nil {
|
||||
tsAggr := ×eries{
|
||||
Values: make([]float64, len(ts.Values)),
|
||||
@@ -75,19 +89,30 @@ func (iafc *incrementalAggrFuncContext) updateTimeseries(ts *timeseries) {
|
||||
ts: tsAggr,
|
||||
values: make([]float64, len(ts.Values)),
|
||||
}
|
||||
iafc.m[string(bb.B)] = iac
|
||||
m[string(bb.B)] = iac
|
||||
}
|
||||
iafc.callbacks.updateAggrFunc(iac, ts.Values)
|
||||
iafc.mu.Unlock()
|
||||
bbPool.Put(bb)
|
||||
iafc.callbacks.updateAggrFunc(iac, ts.Values)
|
||||
}
|
||||
|
||||
func (iafc *incrementalAggrFuncContext) finalizeTimeseries() []*timeseries {
|
||||
// There is no need in iafc.mu.Lock here, since getTimeseries must be called
|
||||
// There is no need in iafc.mLock.Lock here, since finalizeTimeseries must be called
|
||||
// without concurrent goroutines touching iafc.
|
||||
tss := make([]*timeseries, 0, len(iafc.m))
|
||||
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 iafc.m {
|
||||
for _, iac := range mGlobal {
|
||||
finalizeAggrFunc(iac)
|
||||
tss = append(tss, iac.ts)
|
||||
}
|
||||
@@ -96,6 +121,7 @@ func (iafc *incrementalAggrFuncContext) finalizeTimeseries() []*timeseries {
|
||||
|
||||
type incrementalAggrFuncCallbacks struct {
|
||||
updateAggrFunc func(iac *incrementalAggrContext, values []float64)
|
||||
mergeAggrFunc func(dst, src *incrementalAggrContext)
|
||||
finalizeAggrFunc func(iac *incrementalAggrContext)
|
||||
}
|
||||
|
||||
@@ -129,8 +155,33 @@ func updateAggrSum(iac *incrementalAggrContext, values []float64) {
|
||||
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
|
||||
dstCounts[i] = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +205,29 @@ func updateAggrMin(iac *incrementalAggrContext, values []float64) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -174,6 +248,29 @@ func updateAggrMax(iac *incrementalAggrContext, values []float64) {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -195,6 +292,28 @@ func updateAggrAvg(iac *incrementalAggrContext, values []float64) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -219,8 +338,22 @@ func updateAggrCount(iac *incrementalAggrContext, values []float64) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Nothing to do
|
||||
dstValues := iac.ts.Values
|
||||
for i, v := range dstValues {
|
||||
if v == 0 {
|
||||
dstValues[i] = nan
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateAggrSum2(iac *incrementalAggrContext, values []float64) {
|
||||
@@ -232,8 +365,33 @@ func updateAggrSum2(iac *incrementalAggrContext, values []float64) {
|
||||
if math.IsNaN(v) {
|
||||
continue
|
||||
}
|
||||
if dstCounts[i] == 0 {
|
||||
dstValues[i] = v * v
|
||||
dstCounts[i] = 1
|
||||
continue
|
||||
}
|
||||
dstValues[i] += v * v
|
||||
dstCounts[i] = 1
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +414,28 @@ func updateAggrGeomean(iac *incrementalAggrContext, values []float64) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -466,31 +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
|
||||
}
|
||||
|
||||
@@ -582,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
|
||||
}
|
||||
@@ -614,22 +602,16 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me
|
||||
defer rml.Put(uint64(rollupMemorySize))
|
||||
|
||||
// Evaluate rollup
|
||||
removeMetricGroup := !rollupFuncsKeepMetricGroup[name]
|
||||
var tss []*timeseries
|
||||
if iafc != nil {
|
||||
tss, err = evalRollupWithIncrementalAggregate(iafc, rss, rcs, preFunc, sharedTimestamps)
|
||||
tss, err = evalRollupWithIncrementalAggregate(iafc, rss, rcs, preFunc, sharedTimestamps, removeMetricGroup)
|
||||
} else {
|
||||
tss, err = evalRollupNoIncrementalAggregate(rss, rcs, preFunc, sharedTimestamps)
|
||||
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, iafc, window, tss)
|
||||
|
||||
@@ -649,21 +631,19 @@ func getRollupMemoryLimiter() *memoryLimiter {
|
||||
}
|
||||
|
||||
func evalRollupWithIncrementalAggregate(iafc *incrementalAggrFuncContext, rss *netstorage.Results, rcs []*rollupConfig,
|
||||
preFunc func(values []float64, timestamps []int64), sharedTimestamps []int64) ([]*timeseries, error) {
|
||||
err := rss.RunParallel(func(rs *netstorage.Result) {
|
||||
preFunc 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()
|
||||
ts.MetricName.CopyFrom(&rs.MetricName)
|
||||
if len(rc.TagValue) > 0 {
|
||||
ts.MetricName.AddTag("rollup", rc.TagValue)
|
||||
}
|
||||
ts.Values = rc.Do(ts.Values[:0], rs.Values, rs.Timestamps)
|
||||
ts.Timestamps = sharedTimestamps
|
||||
iafc.updateTimeseries(ts)
|
||||
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 {
|
||||
@@ -674,21 +654,14 @@ func evalRollupWithIncrementalAggregate(iafc *incrementalAggrFuncContext, rss *n
|
||||
}
|
||||
|
||||
func evalRollupNoIncrementalAggregate(rss *netstorage.Results, rcs []*rollupConfig,
|
||||
preFunc func(values []float64, timestamps []int64), sharedTimestamps []int64) ([]*timeseries, error) {
|
||||
preFunc func(values []float64, timestamps []int64), sharedTimestamps []int64, removeMetricGroup bool) ([]*timeseries, error) {
|
||||
tss := make([]*timeseries, 0, rss.Len()*len(rcs))
|
||||
var tssLock sync.Mutex
|
||||
err := rss.RunParallel(func(rs *netstorage.Result) {
|
||||
err := rss.RunParallel(func(rs *netstorage.Result, workerID uint) {
|
||||
preFunc(rs.Values, rs.Timestamps)
|
||||
for _, rc := range rcs {
|
||||
var ts timeseries
|
||||
ts.MetricName.CopyFrom(&rs.MetricName)
|
||||
if len(rc.TagValue) > 0 {
|
||||
ts.MetricName.AddTag("rollup", rc.TagValue)
|
||||
}
|
||||
ts.Values = rc.Do(ts.Values[:0], rs.Values, rs.Timestamps)
|
||||
ts.Timestamps = sharedTimestamps
|
||||
ts.denyReuse = true
|
||||
|
||||
doRollupForTimeseries(rc, &ts, &rs.MetricName, rs.Values, rs.Timestamps, sharedTimestamps, removeMetricGroup)
|
||||
tssLock.Lock()
|
||||
tss = append(tss, &ts)
|
||||
tssLock.Unlock()
|
||||
@@ -700,6 +673,20 @@ func evalRollupNoIncrementalAggregate(rss *netstorage.Results, rcs []*rollupConf
|
||||
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] {
|
||||
|
||||
@@ -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) {
|
||||
@@ -36,6 +38,7 @@ func Exec(ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result,
|
||||
if d >= *logSlowQueryDuration {
|
||||
logger.Infof("slow query according to -search.logSlowQueryDuration=%s: duration=%s, start=%d, end=%d, step=%d, query=%q",
|
||||
*logSlowQueryDuration, d, ec.Start/1000, ec.End/1000, ec.Step/1000, q)
|
||||
slowQueries.Inc()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -131,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
|
||||
}
|
||||
|
||||
|
||||
@@ -2158,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({})`
|
||||
@@ -2343,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}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -170,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]`)
|
||||
|
||||
@@ -45,6 +45,8 @@ var rollupFuncs = map[string]newRollupFunc{
|
||||
"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),
|
||||
@@ -54,10 +56,15 @@ var rollupFuncs = map[string]newRollupFunc{
|
||||
}
|
||||
|
||||
var rollupFuncsMayAdjustWindow = map[string]bool{
|
||||
"deriv": true,
|
||||
"deriv_fast": true,
|
||||
"irate": true,
|
||||
"rate": true,
|
||||
"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{
|
||||
@@ -228,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) {
|
||||
@@ -595,10 +619,15 @@ 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 {
|
||||
// Assume that the value didn't change on the given interval.
|
||||
return 0
|
||||
}
|
||||
return values[len(values)-1] - prevValue
|
||||
@@ -612,6 +641,7 @@ func rollupIdelta(rfa *rollupFuncArg) float64 {
|
||||
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]
|
||||
@@ -619,7 +649,8 @@ func rollupIdelta(rfa *rollupFuncArg) float64 {
|
||||
if len(values) == 0 {
|
||||
prevValue := rfa.prevValue
|
||||
if math.IsNaN(prevValue) {
|
||||
return 0
|
||||
// Assume that the previous non-existing value was 0.
|
||||
return lastValue
|
||||
}
|
||||
return lastValue - prevValue
|
||||
}
|
||||
@@ -641,7 +672,8 @@ func rollupDerivFast(rfa *rollupFuncArg) float64 {
|
||||
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]
|
||||
@@ -650,6 +682,7 @@ func rollupDerivFast(rfa *rollupFuncArg) float64 {
|
||||
timestamps = timestamps[1:]
|
||||
}
|
||||
if len(values) == 0 {
|
||||
// Assume that the value didn't change on the given interval.
|
||||
return 0
|
||||
}
|
||||
vEnd := values[len(values)-1]
|
||||
@@ -664,11 +697,12 @@ func rollupIderiv(rfa *rollupFuncArg) float64 {
|
||||
// before calling rollup funcs.
|
||||
values := rfa.values
|
||||
timestamps := rfa.timestamps
|
||||
if len(values) == 0 {
|
||||
if math.IsNaN(rfa.prevValue) {
|
||||
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 0
|
||||
return (values[0] - rfa.prevValue) / (float64(timestamps[0]-rfa.prevTimestamp) * 1e-3)
|
||||
}
|
||||
vEnd := values[len(values)-1]
|
||||
tEnd := timestamps[len(timestamps)-1]
|
||||
@@ -692,7 +726,37 @@ func rollupIderiv(rfa *rollupFuncArg) float64 {
|
||||
}
|
||||
dv := vEnd - vStart
|
||||
dt := tEnd - tStart
|
||||
return dv / (float64(dt) / 1000)
|
||||
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
|
||||
}
|
||||
return float64(timestamps[len(timestamps)-1]-timestamps[0]) * 1e-3
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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{}
|
||||
@@ -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, sizeBytes: %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"}`)
|
||||
@@ -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, 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,6 +210,8 @@ func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExp
|
||||
return tss, newStart
|
||||
}
|
||||
|
||||
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,7 +261,7 @@ 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, iafc, window, ec.Step)
|
||||
metainfoBuf := rrc.c.Get(nil, bb.B)
|
||||
@@ -270,7 +291,7 @@ var (
|
||||
var tooBigRollupResults = metrics.NewCounter("vm_too_big_rollup_results_total")
|
||||
|
||||
// Increment this value every time the format of the cache changes.
|
||||
const rollupResultCacheVersion = 5
|
||||
const rollupResultCacheVersion = 6
|
||||
|
||||
func marshalRollupResultCacheKey(dst []byte, funcName string, me *metricExpr, iafc *incrementalAggrFuncContext, window, step int64) []byte {
|
||||
dst = append(dst, rollupResultCacheVersion)
|
||||
|
||||
@@ -45,8 +45,19 @@ func TestRollupIderivDuplicateTimestamps(t *testing.T) {
|
||||
timestamps: []int64{100},
|
||||
}
|
||||
n = rollupIderiv(rfa)
|
||||
if n != 0 {
|
||||
t.Fatalf("unexpected value; got %v; want %v", n, 0)
|
||||
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{
|
||||
@@ -347,7 +358,7 @@ func TestRollupNoWindowNoPoints(t *testing.T) {
|
||||
}
|
||||
rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
|
||||
values := rc.Do(nil, testValues, testTimestamps)
|
||||
valuesExpected := []float64{2, 0, 0, 0, 0, 0, 0, nan}
|
||||
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)
|
||||
})
|
||||
@@ -371,15 +382,15 @@ func TestRollupWindowNoPoints(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -454,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{44, 34, 34, nan}
|
||||
valuesExpected := []float64{44, 34, 34, 34}
|
||||
timestampsExpected := []int64{100, 120, 140, 160}
|
||||
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
@@ -569,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{0, 33, -87, 0}
|
||||
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,
|
||||
|
||||
@@ -76,7 +76,7 @@ func putTimeseries(ts *timeseries) {
|
||||
|
||||
var timeseriesPool sync.Pool
|
||||
|
||||
func marshalTimeseriesFast(tss []*timeseries, maxSize int, step int64) []byte {
|
||||
func marshalTimeseriesFast(dst []byte, tss []*timeseries, maxSize int, step int64) []byte {
|
||||
if len(tss) == 0 {
|
||||
logger.Panicf("BUG: tss cannot be empty")
|
||||
}
|
||||
@@ -92,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)
|
||||
|
||||
@@ -324,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
|
||||
}
|
||||
@@ -334,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
|
||||
|
||||
@@ -358,6 +358,29 @@ func registerStorageMetrics() {
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -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": 1562261153865,
|
||||
"iteration": 1563651131627,
|
||||
"links": [
|
||||
{
|
||||
"icon": "doc",
|
||||
@@ -1693,7 +1693,7 @@
|
||||
},
|
||||
"id": 46,
|
||||
"panels": [],
|
||||
"title": "Go runtime",
|
||||
"title": "Resource usage",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
@@ -1805,7 +1805,6 @@
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
@@ -1813,13 +1812,13 @@
|
||||
"x": 12,
|
||||
"y": 69
|
||||
},
|
||||
"id": 42,
|
||||
"id": 57,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
@@ -1838,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"
|
||||
}
|
||||
],
|
||||
@@ -1849,7 +1848,7 @@
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "GC duration",
|
||||
"title": "CPU",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
@@ -1865,7 +1864,7 @@
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "s",
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
@@ -1986,6 +1985,92 @@
|
||||
"x": 12,
|
||||
"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": {
|
||||
"avg": false,
|
||||
@@ -2011,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",
|
||||
@@ -2145,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.6.2
|
||||
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.1
|
||||
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.6.2 h1:VMe8c8ZBPgNVZkPoT06LsoU2nb+8e7iPaOWbVRNhxjo=
|
||||
github.com/VictoriaMetrics/metrics v1.6.2/go.mod h1:LU2j9qq7xqZYXz8tF3/RQnB2z2MbZms5TDiIg9/NHiQ=
|
||||
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.1 h1:ZLepItgu2g+B2CfVQy6KCV/as8lnJ7ef1KU6DPxQSS0=
|
||||
github.com/valyala/gozstd v1.5.1/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)
|
||||
}
|
||||
54
lib/fs/fs.go
54
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) {
|
||||
@@ -358,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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -715,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)
|
||||
}
|
||||
|
||||
@@ -994,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,7 @@ 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")
|
||||
}
|
||||
@@ -130,10 +131,10 @@ func openIndexDB(path string, metricIDCache, metricNameCache *fastcache.Cache, c
|
||||
tb: tb,
|
||||
name: name,
|
||||
|
||||
tagCache: fastcache.New(mem / 32),
|
||||
tagCache: workingsetcache.New(mem/32, time.Hour),
|
||||
metricIDCache: metricIDCache,
|
||||
metricNameCache: metricNameCache,
|
||||
uselessTagFiltersCache: fastcache.New(mem / 128),
|
||||
uselessTagFiltersCache: workingsetcache.New(mem/128, time.Hour),
|
||||
|
||||
currHourMetricIDs: currHourMetricIDs,
|
||||
prevHourMetricIDs: prevHourMetricIDs,
|
||||
@@ -273,8 +274,8 @@ func (db *indexDB) decRef() {
|
||||
db.SetExtDB(nil)
|
||||
|
||||
// Free space occupied by caches owned by db.
|
||||
db.tagCache.Reset()
|
||||
db.uselessTagFiltersCache.Reset()
|
||||
db.tagCache.Stop()
|
||||
db.uselessTagFiltersCache.Stop()
|
||||
|
||||
db.tagCache = nil
|
||||
db.metricIDCache = nil
|
||||
@@ -291,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 {
|
||||
@@ -974,7 +991,8 @@ func (db *indexDB) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int)
|
||||
extTSIDs, err = is.searchTSIDs(tfss, tr, maxMetrics)
|
||||
extDB.putIndexSearch(is)
|
||||
|
||||
db.putToTagCache(tsids, tfKeyExtBuf.B)
|
||||
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
|
||||
@@ -1381,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)
|
||||
@@ -1394,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
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||
)
|
||||
|
||||
func TestMarshalUnmarshalTSIDs(t *testing.T) {
|
||||
@@ -57,10 +57,10 @@ 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{})
|
||||
@@ -85,10 +85,10 @@ 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{})
|
||||
@@ -142,10 +142,10 @@ 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{})
|
||||
@@ -753,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 {
|
||||
@@ -764,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
|
||||
@@ -859,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()
|
||||
|
||||
@@ -6,17 +6,18 @@ import (
|
||||
"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{})
|
||||
@@ -78,86 +79,11 @@ 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()
|
||||
|
||||
var hmCurr atomic.Value
|
||||
hmCurr.Store(&hourMetricIDs{})
|
||||
var hmPrev atomic.Value
|
||||
hmPrev.Store(&hourMetricIDs{})
|
||||
|
||||
const dbName = "bench-index-db-search-tsids"
|
||||
db, err := openIndexDB(dbName, metricIDCache, metricNameCache, &hmCurr, &hmPrev)
|
||||
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{})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1008,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)
|
||||
}
|
||||
|
||||
@@ -1367,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,6 +275,15 @@ 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
|
||||
TSIDCacheSizeBytes uint64
|
||||
TSIDCacheRequests uint64
|
||||
@@ -308,6 +321,15 @@ 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
|
||||
@@ -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")
|
||||
@@ -521,11 +543,11 @@ func (s *Storage) mustSaveHourMetricIDs(hm *hourMetricIDs, name string) {
|
||||
logger.Infof("saved %s to %q in %s; entriesCount: %d; sizeBytes: %d", name, path, time.Since(startTime), len(hm.m), len(dst))
|
||||
}
|
||||
|
||||
func (s *Storage) mustLoadCache(info, name string, sizeBytes int) *fastcache.Cache {
|
||||
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, sizeBytes)
|
||||
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; sizeBytes: %d",
|
||||
@@ -533,17 +555,16 @@ func (s *Storage) mustLoadCache(info, name string, sizeBytes int) *fastcache.Cac
|
||||
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()
|
||||
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.
|
||||
@@ -760,8 +790,14 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
|
||||
// doesn't know how to work with them.
|
||||
continue
|
||||
}
|
||||
if mr.Timestamp < minTimestamp || mr.Timestamp > maxTimestamp {
|
||||
// Skip rows with timestamps outside the retention.
|
||||
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]
|
||||
@@ -935,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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -308,6 +308,62 @@ func TestTagFilterMatchSuffix(t *testing.T) {
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOrValues(t *testing.T) {
|
||||
|
||||
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
|
||||
0
package/deb/conffile
Normal file
0
package/deb/conffile
Normal file
7
package/deb/control
Normal file
7
package/deb/control
Normal file
@@ -0,0 +1,7 @@
|
||||
Package: victoria-metrics
|
||||
Maintainer: Aliaksandr Valialkin
|
||||
Depends: libc6 (>= 2.7-1)
|
||||
Section: net
|
||||
Priority: optional
|
||||
Homepage: https://github.com/VictoriaMetrics/VictoriaMetrics
|
||||
Description: VictoriaMetrics is fast, cost-effective and scalable time series database. It can be used as a long-term remote storage for Prometheus.
|
||||
5
package/deb/postinst
Normal file
5
package/deb/postinst
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Reload systemd unit
|
||||
systemctl daemon-reload
|
||||
5
package/deb/postrm
Normal file
5
package/deb/postrm
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Reload systemd unit
|
||||
systemctl daemon-reload
|
||||
5
package/deb/prerm
Normal file
5
package/deb/prerm
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Reload systemd unit
|
||||
systemctl stop victoria-metrics
|
||||
103
package/package_deb.sh
Executable file
103
package/package_deb.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
|
||||
ARCH="amd64"
|
||||
if [[ $# -ge 1 ]]
|
||||
then
|
||||
ARCH="$1"
|
||||
fi
|
||||
|
||||
# Map to Debian architecture
|
||||
if [[ "$ARCH" == "amd64" ]]; then
|
||||
DEB_ARCH=amd64
|
||||
EXENAME_SRC="victoria-metrics-prod"
|
||||
elif [[ "$ARCH" == "arm64" ]]; then
|
||||
DEB_ARCH=arm64
|
||||
EXENAME_SRC="victoria-metrics-arm64-prod"
|
||||
else
|
||||
echo "*** Unknown arch $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKDIR="./package"
|
||||
TEMPDIR="${PACKDIR}/temp-deb-${DEB_ARCH}"
|
||||
EXENAME_DST="victoria-metrics-prod"
|
||||
|
||||
# Pull in version info
|
||||
|
||||
VERSION=`cat ${PACKDIR}/VAR_VERSION | perl -ne 'chomp and print'`
|
||||
BUILD=`cat ${PACKDIR}/VAR_BUILD | perl -ne 'chomp and print'`
|
||||
|
||||
# Create directories
|
||||
|
||||
[[ -d "${TEMPDIR}" ]] && rm -rf "${TEMPDIR}"
|
||||
|
||||
mkdir -p "${TEMPDIR}" && echo "*** Created : ${TEMPDIR}"
|
||||
|
||||
mkdir -p "${TEMPDIR}/usr/bin/"
|
||||
mkdir -p "${TEMPDIR}/lib/systemd/system/"
|
||||
|
||||
echo "*** Version : ${VERSION}-${BUILD}"
|
||||
echo "*** Arch : ${DEB_ARCH}"
|
||||
|
||||
OUT_DEB="victoria-metrics_${VERSION}-${BUILD}_$DEB_ARCH.deb"
|
||||
|
||||
echo "*** Out .deb : ${OUT_DEB}"
|
||||
|
||||
# Copy the binary
|
||||
|
||||
cp "./bin/${EXENAME_SRC}" "${TEMPDIR}/usr/bin/${EXENAME_DST}"
|
||||
|
||||
# Copy supporting files
|
||||
|
||||
cp "${PACKDIR}/victoria-metrics.service" "${TEMPDIR}/lib/systemd/system/"
|
||||
|
||||
# Generate debian-binary
|
||||
|
||||
echo "2.0" > "${TEMPDIR}/debian-binary"
|
||||
|
||||
# Generate control
|
||||
|
||||
echo "Version: $VERSION-$BUILD" > "${TEMPDIR}/control"
|
||||
echo "Installed-Size:" `du -sb "${TEMPDIR}" | awk '{print int($1/1024)}'` >> "${TEMPDIR}/control"
|
||||
echo "Architecture: $DEB_ARCH" >> "${TEMPDIR}/control"
|
||||
cat "${PACKDIR}/deb/control" >> "${TEMPDIR}/control"
|
||||
|
||||
# Copy conffile
|
||||
|
||||
cp "${PACKDIR}/deb/conffile" "${TEMPDIR}/conffile"
|
||||
|
||||
# Copy postinst and postrm
|
||||
|
||||
cp "${PACKDIR}/deb/postinst" "${TEMPDIR}/postinst"
|
||||
cp "${PACKDIR}/deb/prerm" "${TEMPDIR}/prerm"
|
||||
cp "${PACKDIR}/deb/postrm" "${TEMPDIR}/postrm"
|
||||
|
||||
(
|
||||
# Generate md5 sums
|
||||
|
||||
cd "${TEMPDIR}"
|
||||
|
||||
find ./usr ./lib -type f | while read i ; do
|
||||
md5sum "$i" | sed 's/\.\///g' >> md5sums
|
||||
done
|
||||
|
||||
# Archive control
|
||||
|
||||
chmod 644 control md5sums
|
||||
chmod 755 postinst postrm prerm
|
||||
fakeroot -- tar -c --xz -f ./control.tar.xz ./control ./md5sums ./postinst ./prerm ./postrm
|
||||
|
||||
# Archive data
|
||||
|
||||
fakeroot -- tar -c --xz -f ./data.tar.xz ./usr ./lib
|
||||
|
||||
# Make final archive
|
||||
|
||||
fakeroot -- ar -cr "${OUT_DEB}" debian-binary control.tar.xz data.tar.xz
|
||||
)
|
||||
|
||||
ls -lh "${TEMPDIR}/${OUT_DEB}"
|
||||
|
||||
cp "${TEMPDIR}/${OUT_DEB}" "${PACKDIR}"
|
||||
|
||||
echo "*** Created : ${PACKDIR}/${OUT_DEB}"
|
||||
90
package/package_rpm.sh
Executable file
90
package/package_rpm.sh
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
|
||||
if ! which rpmbuild 2> /dev/null
|
||||
then
|
||||
echo "*** Fatal: please install rpmbuild"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH="amd64"
|
||||
if [[ $# -ge 1 ]]
|
||||
then
|
||||
ARCH="$1"
|
||||
fi
|
||||
|
||||
# Map to Debian architecture
|
||||
if [[ "$ARCH" == "amd64" ]]; then
|
||||
RPM_ARCH=x86_64
|
||||
EXENAME_SRC="victoria-metrics-prod"
|
||||
elif [[ "$ARCH" == "arm64" ]]; then
|
||||
RPM_ARCH=aarch64
|
||||
EXENAME_SRC="victoria-metrics-arm64-prod"
|
||||
else
|
||||
echo "*** Unknown arch $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKDIR="./package"
|
||||
TEMPDIR="${PACKDIR}/temp-rpm-${RPM_ARCH}"
|
||||
EXENAME_DST="victoria-metrics-prod"
|
||||
|
||||
# Pull in version info
|
||||
|
||||
VERSION=`cat ${PACKDIR}/VAR_VERSION | perl -ne 'chomp and print'`
|
||||
BUILD=`cat ${PACKDIR}/VAR_BUILD | perl -ne 'chomp and print'`
|
||||
|
||||
# Create directories
|
||||
|
||||
[[ -d "${TEMPDIR}" ]] && rm -rf "${TEMPDIR}"
|
||||
|
||||
mkdir -p "${TEMPDIR}" && echo "*** Created : ${TEMPDIR}"
|
||||
|
||||
echo "*** Version : ${VERSION}-${BUILD}"
|
||||
echo "*** Arch : ${RPM_ARCH}"
|
||||
|
||||
OUT_RPM="victoria-metrics-${VERSION}-${BUILD}.${RPM_ARCH}.rpm"
|
||||
|
||||
echo "*** Out .rpm : ${OUT_RPM}"
|
||||
|
||||
cat > "${TEMPDIR}/victoria-metrics.spec" <<EOF
|
||||
Summary: The best long-term remote storage for Prometheus
|
||||
Name: victoria-metrics
|
||||
Version: ${VERSION}
|
||||
Release: ${BUILD}
|
||||
License: Apache License 2.0
|
||||
URL: https://victoriametrics.com/
|
||||
Group: System
|
||||
Packager: Aliaksandr Valialkin
|
||||
Requires: libpthread
|
||||
Requires: libc
|
||||
|
||||
%description
|
||||
VictoriaMetrics is fast, cost-effective and scalable time series database. It can be used as a long-term remote storage for Prometheus.
|
||||
|
||||
%files
|
||||
%attr(0744, root, root) /usr/bin/*
|
||||
%attr(0644, root, root) /lib/systemd/system/*
|
||||
|
||||
%prep
|
||||
mkdir -p \$RPM_BUILD_ROOT/usr/bin/
|
||||
mkdir -p \$RPM_BUILD_ROOT/lib/systemd/system/
|
||||
|
||||
cp ${PWD}/bin/${EXENAME_SRC} \$RPM_BUILD_ROOT/usr/bin/${EXENAME_DST}
|
||||
cp ${PWD}/package/victoria-metrics.service \$RPM_BUILD_ROOT/lib/systemd/system/
|
||||
|
||||
%post
|
||||
/usr/bin/systemctl daemon-reload
|
||||
|
||||
%preun
|
||||
/usr/bin/systemctl stop victoria-metrics
|
||||
|
||||
%postun
|
||||
/usr/bin/systemctl daemon-reload
|
||||
EOF
|
||||
|
||||
rpmbuild -bb --target "${RPM_ARCH}" \
|
||||
"${TEMPDIR}/victoria-metrics.spec"
|
||||
|
||||
cp "${HOME}/rpmbuild/RPMS/${RPM_ARCH}/${OUT_RPM}" "${PACKDIR}"
|
||||
|
||||
echo "*** Created : ${PACKDIR}/${OUT_RPM}"
|
||||
16
package/rpm/README.md
Normal file
16
package/rpm/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# victoriametrics-rpm
|
||||
RPM for VictoriaMetrics - the best long-term remote storage for Prometheus
|
||||
|
||||
*Get and started*
|
||||
|
||||
```
|
||||
yum -y install yum-plugin-copr
|
||||
|
||||
yum copr enable antonpatsev/VictoriaMetrics
|
||||
|
||||
yum makecache
|
||||
|
||||
yum -y install victoriametrics
|
||||
|
||||
systemctl start victoriametrics
|
||||
```
|
||||
53
package/rpm/victoriametrics-rpm.spec
Normal file
53
package/rpm/victoriametrics-rpm.spec
Normal file
@@ -0,0 +1,53 @@
|
||||
Name: victoriametrics
|
||||
Version: 1.23.0
|
||||
Release: 3
|
||||
Summary: The best long-term remote storage for Prometheus
|
||||
|
||||
Group: Development Tools
|
||||
License: ASL 2.0
|
||||
URL: https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v%{version}/victoria-metrics-v%{version}.tar.gz
|
||||
Source0: %{name}.service
|
||||
|
||||
# Use systemd for fedora >= 18, rhel >=7, SUSE >= 12 SP1 and openSUSE >= 42.1
|
||||
%define use_systemd (0%{?fedora} && 0%{?fedora} >= 18) || (0%{?rhel} && 0%{?rhel} >= 7) || (!0%{?is_opensuse} && 0%{?suse_version} >=1210) || (0%{?is_opensuse} && 0%{?sle_version} >= 120100)
|
||||
|
||||
%description
|
||||
VictoriaMetrics - the best long-term remote storage for Prometheus
|
||||
|
||||
%prep
|
||||
curl -L %{url} > victoria-metrics.tar.gz
|
||||
tar -zxf victoria-metrics.tar.gz
|
||||
|
||||
%install
|
||||
%{__install} -m 0755 -d %{buildroot}%{_bindir}
|
||||
cp victoria-metrics-prod %{buildroot}%{_bindir}/victoria-metrics-prod
|
||||
%{__install} -m 0755 -d %{buildroot}/var/lib/victoria-metrics-data
|
||||
|
||||
%if %{use_systemd}
|
||||
%{__mkdir} -p %{buildroot}%{_unitdir}
|
||||
%{__install} -m644 %{SOURCE0} \
|
||||
%{buildroot}%{_unitdir}/%{name}.service
|
||||
%endif
|
||||
|
||||
%post
|
||||
%if %use_systemd
|
||||
/usr/bin/systemctl daemon-reload
|
||||
%endif
|
||||
|
||||
%preun
|
||||
%if %use_systemd
|
||||
/usr/bin/systemctl stop %{name}
|
||||
%endif
|
||||
|
||||
%postun
|
||||
%if %use_systemd
|
||||
/usr/bin/systemctl daemon-reload
|
||||
%endif
|
||||
|
||||
%files
|
||||
%{_bindir}/victoria-metrics-prod
|
||||
/var/lib/victoria-metrics-data
|
||||
%if %{use_systemd}
|
||||
%{_unitdir}/%{name}.service
|
||||
%endif
|
||||
|
||||
17
package/rpm/victoriametrics.service
Normal file
17
package/rpm/victoriametrics.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=High-performance, cost-effective and scalable time series database, long-term remote storage for Prometheus
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
StartLimitBurst=5
|
||||
StartLimitInterval=0
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
ExecStart=/usr/bin/victoria-metrics-prod -storageDataPath=/var/lib/victoria-metrics-data
|
||||
ExecStop=/bin/kill -s SIGTERM $MAINPID
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=32000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
17
package/victoria-metrics.service
Normal file
17
package/victoria-metrics.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=High-performance, cost-effective and scalable time series database, long-term remote storage for Prometheus
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
StartLimitBurst=5
|
||||
StartLimitInterval=0
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
ExecStart=/usr/bin/victoria-metrics-prod -storageDataPath=/var/lib/victoria-metrics-data
|
||||
ExecStop=/bin/kill -s SIGTERM $MAINPID
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=32000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
64
vendor/github.com/VictoriaMetrics/metrics/go_metrics.go
generated
vendored
Normal file
64
vendor/github.com/VictoriaMetrics/metrics/go_metrics.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
|
||||
"github.com/valyala/histogram"
|
||||
)
|
||||
|
||||
func writeGoMetrics(w io.Writer) {
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
fmt.Fprintf(w, "go_memstats_alloc_bytes %d\n", ms.Alloc)
|
||||
fmt.Fprintf(w, "go_memstats_alloc_bytes_total %d\n", ms.TotalAlloc)
|
||||
fmt.Fprintf(w, "go_memstats_buck_hash_sys_bytes %d\n", ms.BuckHashSys)
|
||||
fmt.Fprintf(w, "go_memstats_frees_total %d\n", ms.Frees)
|
||||
fmt.Fprintf(w, "go_memstats_gc_cpu_fraction %g\n", ms.GCCPUFraction)
|
||||
fmt.Fprintf(w, "go_memstats_gc_sys_bytes %d\n", ms.GCSys)
|
||||
fmt.Fprintf(w, "go_memstats_heap_alloc_bytes %d\n", ms.HeapAlloc)
|
||||
fmt.Fprintf(w, "go_memstats_heap_idle_bytes %d\n", ms.HeapIdle)
|
||||
fmt.Fprintf(w, "go_memstats_heap_inuse_bytes %d\n", ms.HeapInuse)
|
||||
fmt.Fprintf(w, "go_memstats_heap_objects %d\n", ms.HeapObjects)
|
||||
fmt.Fprintf(w, "go_memstats_heap_released_bytes %d\n", ms.HeapReleased)
|
||||
fmt.Fprintf(w, "go_memstats_heap_sys_bytes %d\n", ms.HeapSys)
|
||||
fmt.Fprintf(w, "go_memstats_last_gc_time_seconds %g\n", float64(ms.LastGC)/1e9)
|
||||
fmt.Fprintf(w, "go_memstats_lookups_total %d\n", ms.Lookups)
|
||||
fmt.Fprintf(w, "go_memstats_mallocs_total %d\n", ms.Mallocs)
|
||||
fmt.Fprintf(w, "go_memstats_mcache_inuse_bytes %d\n", ms.MCacheInuse)
|
||||
fmt.Fprintf(w, "go_memstats_mcache_sys_bytes %d\n", ms.MCacheSys)
|
||||
fmt.Fprintf(w, "go_memstats_mspan_inuse_bytes %d\n", ms.MSpanInuse)
|
||||
fmt.Fprintf(w, "go_memstats_mspan_sys_bytes %d\n", ms.MSpanSys)
|
||||
fmt.Fprintf(w, "go_memstats_next_gc_bytes %d\n", ms.NextGC)
|
||||
fmt.Fprintf(w, "go_memstats_other_sys_bytes %d\n", ms.OtherSys)
|
||||
fmt.Fprintf(w, "go_memstats_stack_inuse_bytes %d\n", ms.StackInuse)
|
||||
fmt.Fprintf(w, "go_memstats_stack_sys_bytes %d\n", ms.StackSys)
|
||||
fmt.Fprintf(w, "go_memstats_sys_bytes %d\n", ms.Sys)
|
||||
|
||||
fmt.Fprintf(w, "go_cgo_calls_count %d\n", runtime.NumCgoCall())
|
||||
fmt.Fprintf(w, "go_cpu_count %d\n", runtime.NumCPU())
|
||||
|
||||
gcPauses := histogram.NewFast()
|
||||
for _, pauseNs := range ms.PauseNs[:] {
|
||||
gcPauses.Update(float64(pauseNs) / 1e9)
|
||||
}
|
||||
phis := []float64{0, 0.25, 0.5, 0.75, 1}
|
||||
quantiles := make([]float64, 0, len(phis))
|
||||
for i, q := range gcPauses.Quantiles(quantiles[:0], phis) {
|
||||
fmt.Fprintf(w, `go_gc_duration_seconds{quantile="%g"} %g`+"\n", phis[i], q)
|
||||
}
|
||||
fmt.Fprintf(w, `go_gc_duration_seconds_sum %g`+"\n", float64(ms.PauseTotalNs)/1e9)
|
||||
fmt.Fprintf(w, `go_gc_duration_seconds_count %d`+"\n", ms.NumGC)
|
||||
fmt.Fprintf(w, `go_gc_forced_count %d`+"\n", ms.NumForcedGC)
|
||||
|
||||
fmt.Fprintf(w, `go_gomaxprocs %d`+"\n", runtime.GOMAXPROCS(0))
|
||||
fmt.Fprintf(w, `go_goroutines %d`+"\n", runtime.NumGoroutine())
|
||||
numThread, _ := runtime.ThreadCreateProfile(nil)
|
||||
fmt.Fprintf(w, `go_threads %d`+"\n", numThread)
|
||||
|
||||
// Export build details.
|
||||
fmt.Fprintf(w, "go_info{version=%q} 1\n", runtime.Version())
|
||||
fmt.Fprintf(w, "go_info_ext{compiler=%q, GOARCH=%q, GOOS=%q, GOROOT=%q} 1\n",
|
||||
runtime.Compiler, runtime.GOARCH, runtime.GOOS, runtime.GOROOT())
|
||||
}
|
||||
64
vendor/github.com/VictoriaMetrics/metrics/metrics.go
generated
vendored
64
vendor/github.com/VictoriaMetrics/metrics/metrics.go
generated
vendored
@@ -13,11 +13,7 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
|
||||
"github.com/valyala/histogram"
|
||||
)
|
||||
|
||||
type namedMetric struct {
|
||||
@@ -33,8 +29,8 @@ var defaultSet = NewSet()
|
||||
|
||||
// WritePrometheus writes all the registered metrics in Prometheus format to w.
|
||||
//
|
||||
// If exposeProcessMetrics is true, then various `go_*` metrics are exposed
|
||||
// for the current process.
|
||||
// If exposeProcessMetrics is true, then various `go_*` and `process_*` metrics
|
||||
// are exposed for the current process.
|
||||
//
|
||||
// The WritePrometheus func is usually called inside "/metrics" handler:
|
||||
//
|
||||
@@ -45,61 +41,7 @@ var defaultSet = NewSet()
|
||||
func WritePrometheus(w io.Writer, exposeProcessMetrics bool) {
|
||||
defaultSet.WritePrometheus(w)
|
||||
if exposeProcessMetrics {
|
||||
writeGoMetrics(w)
|
||||
writeProcessMetrics(w)
|
||||
}
|
||||
}
|
||||
|
||||
func writeProcessMetrics(w io.Writer) {
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
fmt.Fprintf(w, `go_memstats_alloc_bytes %d`+"\n", ms.Alloc)
|
||||
fmt.Fprintf(w, `go_memstats_alloc_bytes_total %d`+"\n", ms.TotalAlloc)
|
||||
fmt.Fprintf(w, `go_memstats_buck_hash_sys_bytes %d`+"\n", ms.BuckHashSys)
|
||||
fmt.Fprintf(w, `go_memstats_frees_total %d`+"\n", ms.Frees)
|
||||
fmt.Fprintf(w, `go_memstats_gc_cpu_fraction %f`+"\n", ms.GCCPUFraction)
|
||||
fmt.Fprintf(w, `go_memstats_gc_sys_bytes %d`+"\n", ms.GCSys)
|
||||
fmt.Fprintf(w, `go_memstats_heap_alloc_bytes %d`+"\n", ms.HeapAlloc)
|
||||
fmt.Fprintf(w, `go_memstats_heap_idle_bytes %d`+"\n", ms.HeapIdle)
|
||||
fmt.Fprintf(w, `go_memstats_heap_inuse_bytes %d`+"\n", ms.HeapInuse)
|
||||
fmt.Fprintf(w, `go_memstats_heap_objects %d`+"\n", ms.HeapObjects)
|
||||
fmt.Fprintf(w, `go_memstats_heap_released_bytes %d`+"\n", ms.HeapReleased)
|
||||
fmt.Fprintf(w, `go_memstats_heap_sys_bytes %d`+"\n", ms.HeapSys)
|
||||
fmt.Fprintf(w, `go_memstats_last_gc_time_seconds %f`+"\n", float64(ms.LastGC)/1e9)
|
||||
fmt.Fprintf(w, `go_memstats_lookups_total %d`+"\n", ms.Lookups)
|
||||
fmt.Fprintf(w, `go_memstats_mallocs_total %d`+"\n", ms.Mallocs)
|
||||
fmt.Fprintf(w, `go_memstats_mcache_inuse_bytes %d`+"\n", ms.MCacheInuse)
|
||||
fmt.Fprintf(w, `go_memstats_mcache_sys_bytes %d`+"\n", ms.MCacheSys)
|
||||
fmt.Fprintf(w, `go_memstats_mspan_inuse_bytes %d`+"\n", ms.MSpanInuse)
|
||||
fmt.Fprintf(w, `go_memstats_mspan_sys_bytes %d`+"\n", ms.MSpanSys)
|
||||
fmt.Fprintf(w, `go_memstats_next_gc_bytes %d`+"\n", ms.NextGC)
|
||||
fmt.Fprintf(w, `go_memstats_other_sys_bytes %d`+"\n", ms.OtherSys)
|
||||
fmt.Fprintf(w, `go_memstats_stack_inuse_bytes %d`+"\n", ms.StackInuse)
|
||||
fmt.Fprintf(w, `go_memstats_stack_sys_bytes %d`+"\n", ms.StackSys)
|
||||
fmt.Fprintf(w, `go_memstats_sys_bytes %d`+"\n", ms.Sys)
|
||||
|
||||
fmt.Fprintf(w, `go_cgo_calls_count %d`+"\n", runtime.NumCgoCall())
|
||||
fmt.Fprintf(w, `go_cpu_count %d`+"\n", runtime.NumCPU())
|
||||
|
||||
gcPauses := histogram.NewFast()
|
||||
for _, pauseNs := range ms.PauseNs[:] {
|
||||
gcPauses.Update(float64(pauseNs) / 1e9)
|
||||
}
|
||||
phis := []float64{0, 0.25, 0.5, 0.75, 1}
|
||||
quantiles := make([]float64, 0, len(phis))
|
||||
for i, q := range gcPauses.Quantiles(quantiles[:0], phis) {
|
||||
fmt.Fprintf(w, `go_gc_duration_seconds{quantile="%g"} %f`+"\n", phis[i], q)
|
||||
}
|
||||
fmt.Fprintf(w, `go_gc_duration_seconds_sum %f`+"\n", float64(ms.PauseTotalNs)/1e9)
|
||||
fmt.Fprintf(w, `go_gc_duration_seconds_count %d`+"\n", ms.NumGC)
|
||||
fmt.Fprintf(w, `go_gc_forced_count %d`+"\n", ms.NumForcedGC)
|
||||
|
||||
fmt.Fprintf(w, `go_gomaxprocs %d`+"\n", runtime.GOMAXPROCS(0))
|
||||
fmt.Fprintf(w, `go_goroutines %d`+"\n", runtime.NumGoroutine())
|
||||
numThread, _ := runtime.ThreadCreateProfile(nil)
|
||||
fmt.Fprintf(w, `go_threads %d`+"\n", numThread)
|
||||
|
||||
// Export build details.
|
||||
fmt.Fprintf(w, "go_info{version=%q} 1\n", runtime.Version())
|
||||
fmt.Fprintf(w, "go_info_ext{compiler=%q, GOARCH=%q, GOOS=%q, GOROOT=%q} 1\n",
|
||||
runtime.Compiler, runtime.GOARCH, runtime.GOOS, runtime.GOROOT())
|
||||
}
|
||||
|
||||
83
vendor/github.com/VictoriaMetrics/metrics/process_metrics_linux.go
generated
vendored
Normal file
83
vendor/github.com/VictoriaMetrics/metrics/process_metrics_linux.go
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
const statFilepath = "/proc/self/stat"
|
||||
|
||||
// See https://github.com/prometheus/procfs/blob/a4ac0826abceb44c40fc71daed2b301db498b93e/proc_stat.go#L40 .
|
||||
const userHZ = 100
|
||||
|
||||
// See http://man7.org/linux/man-pages/man5/proc.5.html
|
||||
type procStat struct {
|
||||
State byte
|
||||
Ppid int
|
||||
Pgrp int
|
||||
Session int
|
||||
TtyNr int
|
||||
Tpgid int
|
||||
Flags uint
|
||||
Minflt uint
|
||||
Cminflt uint
|
||||
Majflt uint
|
||||
Cmajflt uint
|
||||
Utime uint
|
||||
Stime uint
|
||||
Cutime int
|
||||
Cstime int
|
||||
Priority int
|
||||
Nice int
|
||||
NumThreads int
|
||||
ItrealValue int
|
||||
Starttime uint64
|
||||
Vsize uint
|
||||
Rss int
|
||||
}
|
||||
|
||||
func writeProcessMetrics(w io.Writer) {
|
||||
data, err := ioutil.ReadFile(statFilepath)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: cannot open %s: %s", statFilepath, err)
|
||||
return
|
||||
}
|
||||
// Search for the end of command.
|
||||
n := bytes.LastIndex(data, []byte(") "))
|
||||
if n < 0 {
|
||||
log.Printf("ERROR: cannot find command in parentheses in %q read from %s", data, statFilepath)
|
||||
return
|
||||
}
|
||||
data = data[n+2:]
|
||||
|
||||
var p procStat
|
||||
bb := bytes.NewBuffer(data)
|
||||
_, err = fmt.Fscanf(bb, "%c %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d",
|
||||
&p.State, &p.Ppid, &p.Pgrp, &p.Session, &p.TtyNr, &p.Tpgid, &p.Flags, &p.Minflt, &p.Cminflt, &p.Majflt, &p.Cmajflt,
|
||||
&p.Utime, &p.Stime, &p.Cutime, &p.Cstime, &p.Priority, &p.Nice, &p.NumThreads, &p.ItrealValue, &p.Starttime, &p.Vsize, &p.Rss)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: cannot parse %q read from %s: %s", data, statFilepath, err)
|
||||
return
|
||||
}
|
||||
|
||||
// It is expensive obtaining `process_open_fds` when big number of file descriptors is opened,
|
||||
// don't do it here.
|
||||
|
||||
utime := float64(p.Utime) / userHZ
|
||||
stime := float64(p.Stime) / userHZ
|
||||
fmt.Fprintf(w, "process_cpu_seconds_system_total %g\n", stime)
|
||||
fmt.Fprintf(w, "process_cpu_seconds_total %g\n", utime+stime)
|
||||
fmt.Fprintf(w, "process_cpu_seconds_user_total %g\n", utime)
|
||||
fmt.Fprintf(w, "process_major_pagefaults_total %d\n", p.Majflt)
|
||||
fmt.Fprintf(w, "process_minor_pagefaults_total %d\n", p.Minflt)
|
||||
fmt.Fprintf(w, "process_num_threads %d\n", p.NumThreads)
|
||||
fmt.Fprintf(w, "process_resident_memory_bytes %d\n", p.Rss*4096)
|
||||
fmt.Fprintf(w, "process_start_time_seconds %d\n", startTimeSeconds)
|
||||
fmt.Fprintf(w, "process_virtual_memory_bytes %d\n", p.Vsize)
|
||||
}
|
||||
|
||||
var startTimeSeconds = time.Now().Unix()
|
||||
11
vendor/github.com/VictoriaMetrics/metrics/process_metrics_other.go
generated
vendored
Normal file
11
vendor/github.com/VictoriaMetrics/metrics/process_metrics_other.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build !linux
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func writeProcessMetrics(w io.Writer) {
|
||||
// TODO: implement it
|
||||
}
|
||||
28
vendor/github.com/klauspost/compress/LICENSE
generated
vendored
Normal file
28
vendor/github.com/klauspost/compress/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
Copyright (c) 2019 Klaus Post. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
79
vendor/github.com/klauspost/compress/fse/README.md
generated
vendored
Normal file
79
vendor/github.com/klauspost/compress/fse/README.md
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
# Finite State Entropy
|
||||
|
||||
This package provides Finite State Entropy encoding and decoding.
|
||||
|
||||
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
|
||||
encoding provides a fast near-optimal symbol encoding/decoding
|
||||
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
|
||||
|
||||
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
|
||||
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
|
||||
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
|
||||
|
||||
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
|
||||
|
||||
## News
|
||||
|
||||
* Feb 2018: First implementation released. Consider this beta software for now.
|
||||
|
||||
# Usage
|
||||
|
||||
This package provides a low level interface that allows to compress single independent blocks.
|
||||
|
||||
Each block is separate, and there is no built in integrity checks.
|
||||
This means that the caller should keep track of block sizes and also do checksums if needed.
|
||||
|
||||
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
|
||||
You must provide input and will receive the output and maybe an error.
|
||||
|
||||
These error values can be returned:
|
||||
|
||||
| Error | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------|
|
||||
| `<nil>` | Everything ok, output is returned |
|
||||
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
|
||||
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
|
||||
| `(error)` | An internal error occurred. |
|
||||
|
||||
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
|
||||
|
||||
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
|
||||
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
|
||||
object can be used for both.
|
||||
|
||||
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
|
||||
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
|
||||
|
||||
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
|
||||
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
|
||||
your input was likely corrupted.
|
||||
|
||||
It is important to note that a successful decoding does *not* mean your output matches your original input.
|
||||
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
|
||||
|
||||
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
|
||||
|
||||
# Performance
|
||||
|
||||
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
|
||||
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
|
||||
|
||||
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
|
||||
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
|
||||
beneficial to transpose all your input values down by 64.
|
||||
|
||||
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
|
||||
around 300MB/s decompression speed.
|
||||
|
||||
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
|
||||
|
||||
# Plans
|
||||
|
||||
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
|
||||
|
||||
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
|
||||
changes will likely not be accepted. If in doubt open an issue before writing the PR.
|
||||
107
vendor/github.com/klauspost/compress/fse/bitreader.go
generated
vendored
Normal file
107
vendor/github.com/klauspost/compress/fse/bitreader.go
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright 2018 Klaus Post. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Based on work Copyright (c) 2013, Yann Collet, released under BSD License.
|
||||
|
||||
package fse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// bitReader reads a bitstream in reverse.
|
||||
// The last set bit indicates the start of the stream and is used
|
||||
// for aligning the input.
|
||||
type bitReader struct {
|
||||
in []byte
|
||||
off uint // next byte to read is at in[off - 1]
|
||||
value uint64
|
||||
bitsRead uint8
|
||||
}
|
||||
|
||||
// init initializes and resets the bit reader.
|
||||
func (b *bitReader) init(in []byte) error {
|
||||
if len(in) < 1 {
|
||||
return errors.New("corrupt stream: too short")
|
||||
}
|
||||
b.in = in
|
||||
b.off = uint(len(in))
|
||||
// The highest bit of the last byte indicates where to start
|
||||
v := in[len(in)-1]
|
||||
if v == 0 {
|
||||
return errors.New("corrupt stream, did not find end of stream")
|
||||
}
|
||||
b.bitsRead = 64
|
||||
b.value = 0
|
||||
b.fill()
|
||||
b.fill()
|
||||
b.bitsRead += 8 - uint8(highBits(uint32(v)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBits will return n bits. n can be 0.
|
||||
func (b *bitReader) getBits(n uint8) uint16 {
|
||||
if n == 0 || b.bitsRead >= 64 {
|
||||
return 0
|
||||
}
|
||||
return b.getBitsFast(n)
|
||||
}
|
||||
|
||||
// getBitsFast requires that at least one bit is requested every time.
|
||||
// There are no checks if the buffer is filled.
|
||||
func (b *bitReader) getBitsFast(n uint8) uint16 {
|
||||
const regMask = 64 - 1
|
||||
v := uint16((b.value << (b.bitsRead & regMask)) >> ((regMask + 1 - n) & regMask))
|
||||
b.bitsRead += n
|
||||
return v
|
||||
}
|
||||
|
||||
// fillFast() will make sure at least 32 bits are available.
|
||||
// There must be at least 4 bytes available.
|
||||
func (b *bitReader) fillFast() {
|
||||
if b.bitsRead < 32 {
|
||||
return
|
||||
}
|
||||
// Do single re-slice to avoid bounds checks.
|
||||
v := b.in[b.off-4 : b.off]
|
||||
low := (uint32(v[0])) | (uint32(v[1]) << 8) | (uint32(v[2]) << 16) | (uint32(v[3]) << 24)
|
||||
b.value = (b.value << 32) | uint64(low)
|
||||
b.bitsRead -= 32
|
||||
b.off -= 4
|
||||
}
|
||||
|
||||
// fill() will make sure at least 32 bits are available.
|
||||
func (b *bitReader) fill() {
|
||||
if b.bitsRead < 32 {
|
||||
return
|
||||
}
|
||||
if b.off > 4 {
|
||||
v := b.in[b.off-4 : b.off]
|
||||
low := (uint32(v[0])) | (uint32(v[1]) << 8) | (uint32(v[2]) << 16) | (uint32(v[3]) << 24)
|
||||
b.value = (b.value << 32) | uint64(low)
|
||||
b.bitsRead -= 32
|
||||
b.off -= 4
|
||||
return
|
||||
}
|
||||
for b.off > 0 {
|
||||
b.value = (b.value << 8) | uint64(b.in[b.off-1])
|
||||
b.bitsRead -= 8
|
||||
b.off--
|
||||
}
|
||||
}
|
||||
|
||||
// finished returns true if all bits have been read from the bit stream.
|
||||
func (b *bitReader) finished() bool {
|
||||
return b.off == 0 && b.bitsRead >= 64
|
||||
}
|
||||
|
||||
// close the bitstream and returns an error if out-of-buffer reads occurred.
|
||||
func (b *bitReader) close() error {
|
||||
// Release reference.
|
||||
b.in = nil
|
||||
if b.bitsRead > 64 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
168
vendor/github.com/klauspost/compress/fse/bitwriter.go
generated
vendored
Normal file
168
vendor/github.com/klauspost/compress/fse/bitwriter.go
generated
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright 2018 Klaus Post. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Based on work Copyright (c) 2013, Yann Collet, released under BSD License.
|
||||
|
||||
package fse
|
||||
|
||||
import "fmt"
|
||||
|
||||
// bitWriter will write bits.
|
||||
// First bit will be LSB of the first byte of output.
|
||||
type bitWriter struct {
|
||||
bitContainer uint64
|
||||
nBits uint8
|
||||
out []byte
|
||||
}
|
||||
|
||||
// bitMask16 is bitmasks. Has extra to avoid bounds check.
|
||||
var bitMask16 = [32]uint16{
|
||||
0, 1, 3, 7, 0xF, 0x1F,
|
||||
0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF,
|
||||
0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0xFFFF} /* up to 16 bits */
|
||||
|
||||
// addBits16NC will add up to 16 bits.
|
||||
// It will not check if there is space for them,
|
||||
// so the caller must ensure that it has flushed recently.
|
||||
func (b *bitWriter) addBits16NC(value uint16, bits uint8) {
|
||||
b.bitContainer |= uint64(value&bitMask16[bits&31]) << (b.nBits & 63)
|
||||
b.nBits += bits
|
||||
}
|
||||
|
||||
// addBits16Clean will add up to 16 bits. value may not contain more set bits than indicated.
|
||||
// It will not check if there is space for them, so the caller must ensure that it has flushed recently.
|
||||
func (b *bitWriter) addBits16Clean(value uint16, bits uint8) {
|
||||
b.bitContainer |= uint64(value) << (b.nBits & 63)
|
||||
b.nBits += bits
|
||||
}
|
||||
|
||||
// addBits16ZeroNC will add up to 16 bits.
|
||||
// It will not check if there is space for them,
|
||||
// so the caller must ensure that it has flushed recently.
|
||||
// This is fastest if bits can be zero.
|
||||
func (b *bitWriter) addBits16ZeroNC(value uint16, bits uint8) {
|
||||
if bits == 0 {
|
||||
return
|
||||
}
|
||||
value <<= (16 - bits) & 15
|
||||
value >>= (16 - bits) & 15
|
||||
b.bitContainer |= uint64(value) << (b.nBits & 63)
|
||||
b.nBits += bits
|
||||
}
|
||||
|
||||
// flush will flush all pending full bytes.
|
||||
// There will be at least 56 bits available for writing when this has been called.
|
||||
// Using flush32 is faster, but leaves less space for writing.
|
||||
func (b *bitWriter) flush() {
|
||||
v := b.nBits >> 3
|
||||
switch v {
|
||||
case 0:
|
||||
case 1:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
)
|
||||
case 2:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
)
|
||||
case 3:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
byte(b.bitContainer>>16),
|
||||
)
|
||||
case 4:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
byte(b.bitContainer>>16),
|
||||
byte(b.bitContainer>>24),
|
||||
)
|
||||
case 5:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
byte(b.bitContainer>>16),
|
||||
byte(b.bitContainer>>24),
|
||||
byte(b.bitContainer>>32),
|
||||
)
|
||||
case 6:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
byte(b.bitContainer>>16),
|
||||
byte(b.bitContainer>>24),
|
||||
byte(b.bitContainer>>32),
|
||||
byte(b.bitContainer>>40),
|
||||
)
|
||||
case 7:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
byte(b.bitContainer>>16),
|
||||
byte(b.bitContainer>>24),
|
||||
byte(b.bitContainer>>32),
|
||||
byte(b.bitContainer>>40),
|
||||
byte(b.bitContainer>>48),
|
||||
)
|
||||
case 8:
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
byte(b.bitContainer>>16),
|
||||
byte(b.bitContainer>>24),
|
||||
byte(b.bitContainer>>32),
|
||||
byte(b.bitContainer>>40),
|
||||
byte(b.bitContainer>>48),
|
||||
byte(b.bitContainer>>56),
|
||||
)
|
||||
default:
|
||||
panic(fmt.Errorf("bits (%d) > 64", b.nBits))
|
||||
}
|
||||
b.bitContainer >>= v << 3
|
||||
b.nBits &= 7
|
||||
}
|
||||
|
||||
// flush32 will flush out, so there are at least 32 bits available for writing.
|
||||
func (b *bitWriter) flush32() {
|
||||
if b.nBits < 32 {
|
||||
return
|
||||
}
|
||||
b.out = append(b.out,
|
||||
byte(b.bitContainer),
|
||||
byte(b.bitContainer>>8),
|
||||
byte(b.bitContainer>>16),
|
||||
byte(b.bitContainer>>24))
|
||||
b.nBits -= 32
|
||||
b.bitContainer >>= 32
|
||||
}
|
||||
|
||||
// flushAlign will flush remaining full bytes and align to next byte boundary.
|
||||
func (b *bitWriter) flushAlign() {
|
||||
nbBytes := (b.nBits + 7) >> 3
|
||||
for i := uint8(0); i < nbBytes; i++ {
|
||||
b.out = append(b.out, byte(b.bitContainer>>(i*8)))
|
||||
}
|
||||
b.nBits = 0
|
||||
b.bitContainer = 0
|
||||
}
|
||||
|
||||
// close will write the alignment bit and write the final byte(s)
|
||||
// to the output.
|
||||
func (b *bitWriter) close() error {
|
||||
// End mark
|
||||
b.addBits16Clean(1, 1)
|
||||
// flush until next byte.
|
||||
b.flushAlign()
|
||||
return nil
|
||||
}
|
||||
|
||||
// reset and continue writing by appending to out.
|
||||
func (b *bitWriter) reset(out []byte) {
|
||||
b.bitContainer = 0
|
||||
b.nBits = 0
|
||||
b.out = out
|
||||
}
|
||||
56
vendor/github.com/klauspost/compress/fse/bytereader.go
generated
vendored
Normal file
56
vendor/github.com/klauspost/compress/fse/bytereader.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2018 Klaus Post. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Based on work Copyright (c) 2013, Yann Collet, released under BSD License.
|
||||
|
||||
package fse
|
||||
|
||||
// byteReader provides a byte reader that reads
|
||||
// little endian values from a byte stream.
|
||||
// The input stream is manually advanced.
|
||||
// The reader performs no bounds checks.
|
||||
type byteReader struct {
|
||||
b []byte
|
||||
off int
|
||||
}
|
||||
|
||||
// init will initialize the reader and set the input.
|
||||
func (b *byteReader) init(in []byte) {
|
||||
b.b = in
|
||||
b.off = 0
|
||||
}
|
||||
|
||||
// advance the stream b n bytes.
|
||||
func (b *byteReader) advance(n uint) {
|
||||
b.off += int(n)
|
||||
}
|
||||
|
||||
// Int32 returns a little endian int32 starting at current offset.
|
||||
func (b byteReader) Int32() int32 {
|
||||
b2 := b.b[b.off : b.off+4 : b.off+4]
|
||||
v3 := int32(b2[3])
|
||||
v2 := int32(b2[2])
|
||||
v1 := int32(b2[1])
|
||||
v0 := int32(b2[0])
|
||||
return v0 | (v1 << 8) | (v2 << 16) | (v3 << 24)
|
||||
}
|
||||
|
||||
// Uint32 returns a little endian uint32 starting at current offset.
|
||||
func (b byteReader) Uint32() uint32 {
|
||||
b2 := b.b[b.off : b.off+4 : b.off+4]
|
||||
v3 := uint32(b2[3])
|
||||
v2 := uint32(b2[2])
|
||||
v1 := uint32(b2[1])
|
||||
v0 := uint32(b2[0])
|
||||
return v0 | (v1 << 8) | (v2 << 16) | (v3 << 24)
|
||||
}
|
||||
|
||||
// unread returns the unread portion of the input.
|
||||
func (b byteReader) unread() []byte {
|
||||
return b.b[b.off:]
|
||||
}
|
||||
|
||||
// remain will return the number of bytes remaining.
|
||||
func (b byteReader) remain() int {
|
||||
return len(b.b) - b.off
|
||||
}
|
||||
684
vendor/github.com/klauspost/compress/fse/compress.go
generated
vendored
Normal file
684
vendor/github.com/klauspost/compress/fse/compress.go
generated
vendored
Normal file
@@ -0,0 +1,684 @@
|
||||
// Copyright 2018 Klaus Post. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Based on work Copyright (c) 2013, Yann Collet, released under BSD License.
|
||||
|
||||
package fse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Compress the input bytes. Input must be < 2GB.
|
||||
// Provide a Scratch buffer to avoid memory allocations.
|
||||
// Note that the output is also kept in the scratch buffer.
|
||||
// If input is too hard to compress, ErrIncompressible is returned.
|
||||
// If input is a single byte value repeated ErrUseRLE is returned.
|
||||
func Compress(in []byte, s *Scratch) ([]byte, error) {
|
||||
if len(in) <= 1 {
|
||||
return nil, ErrIncompressible
|
||||
}
|
||||
if len(in) > (2<<30)-1 {
|
||||
return nil, errors.New("input too big, must be < 2GB")
|
||||
}
|
||||
s, err := s.prepare(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create histogram, if none was provided.
|
||||
maxCount := s.maxCount
|
||||
if maxCount == 0 {
|
||||
maxCount = s.countSimple(in)
|
||||
}
|
||||
// Reset for next run.
|
||||
s.clearCount = true
|
||||
s.maxCount = 0
|
||||
if maxCount == len(in) {
|
||||
// One symbol, use RLE
|
||||
return nil, ErrUseRLE
|
||||
}
|
||||
if maxCount == 1 || maxCount < (len(in)>>7) {
|
||||
// Each symbol present maximum once or too well distributed.
|
||||
return nil, ErrIncompressible
|
||||
}
|
||||
s.optimalTableLog()
|
||||
err = s.normalizeCount()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.writeCount()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if false {
|
||||
err = s.validateNorm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.buildCTable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.compress(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Out = s.bw.out
|
||||
// Check if we compressed.
|
||||
if len(s.Out) >= len(in) {
|
||||
return nil, ErrIncompressible
|
||||
}
|
||||
return s.Out, nil
|
||||
}
|
||||
|
||||
// cState contains the compression state of a stream.
|
||||
type cState struct {
|
||||
bw *bitWriter
|
||||
stateTable []uint16
|
||||
state uint16
|
||||
}
|
||||
|
||||
// init will initialize the compression state to the first symbol of the stream.
|
||||
func (c *cState) init(bw *bitWriter, ct *cTable, tableLog uint8, first symbolTransform) {
|
||||
c.bw = bw
|
||||
c.stateTable = ct.stateTable
|
||||
|
||||
nbBitsOut := (first.deltaNbBits + (1 << 15)) >> 16
|
||||
im := int32((nbBitsOut << 16) - first.deltaNbBits)
|
||||
lu := (im >> nbBitsOut) + first.deltaFindState
|
||||
c.state = c.stateTable[lu]
|
||||
return
|
||||
}
|
||||
|
||||
// encode the output symbol provided and write it to the bitstream.
|
||||
func (c *cState) encode(symbolTT symbolTransform) {
|
||||
nbBitsOut := (uint32(c.state) + symbolTT.deltaNbBits) >> 16
|
||||
dstState := int32(c.state>>(nbBitsOut&15)) + symbolTT.deltaFindState
|
||||
c.bw.addBits16NC(c.state, uint8(nbBitsOut))
|
||||
c.state = c.stateTable[dstState]
|
||||
}
|
||||
|
||||
// encode the output symbol provided and write it to the bitstream.
|
||||
func (c *cState) encodeZero(symbolTT symbolTransform) {
|
||||
nbBitsOut := (uint32(c.state) + symbolTT.deltaNbBits) >> 16
|
||||
dstState := int32(c.state>>(nbBitsOut&15)) + symbolTT.deltaFindState
|
||||
c.bw.addBits16ZeroNC(c.state, uint8(nbBitsOut))
|
||||
c.state = c.stateTable[dstState]
|
||||
}
|
||||
|
||||
// flush will write the tablelog to the output and flush the remaining full bytes.
|
||||
func (c *cState) flush(tableLog uint8) {
|
||||
c.bw.flush32()
|
||||
c.bw.addBits16NC(c.state, tableLog)
|
||||
c.bw.flush()
|
||||
}
|
||||
|
||||
// compress is the main compression loop that will encode the input from the last byte to the first.
|
||||
func (s *Scratch) compress(src []byte) error {
|
||||
if len(src) <= 2 {
|
||||
return errors.New("compress: src too small")
|
||||
}
|
||||
tt := s.ct.symbolTT[:256]
|
||||
s.bw.reset(s.Out)
|
||||
|
||||
// Our two states each encodes every second byte.
|
||||
// Last byte encoded (first byte decoded) will always be encoded by c1.
|
||||
var c1, c2 cState
|
||||
|
||||
// Encode so remaining size is divisible by 4.
|
||||
ip := len(src)
|
||||
if ip&1 == 1 {
|
||||
c1.init(&s.bw, &s.ct, s.actualTableLog, tt[src[ip-1]])
|
||||
c2.init(&s.bw, &s.ct, s.actualTableLog, tt[src[ip-2]])
|
||||
c1.encodeZero(tt[src[ip-3]])
|
||||
ip -= 3
|
||||
} else {
|
||||
c2.init(&s.bw, &s.ct, s.actualTableLog, tt[src[ip-1]])
|
||||
c1.init(&s.bw, &s.ct, s.actualTableLog, tt[src[ip-2]])
|
||||
ip -= 2
|
||||
}
|
||||
if ip&2 != 0 {
|
||||
c2.encodeZero(tt[src[ip-1]])
|
||||
c1.encodeZero(tt[src[ip-2]])
|
||||
ip -= 2
|
||||
}
|
||||
|
||||
// Main compression loop.
|
||||
switch {
|
||||
case !s.zeroBits && s.actualTableLog <= 8:
|
||||
// We can encode 4 symbols without requiring a flush.
|
||||
// We do not need to check if any output is 0 bits.
|
||||
for ip >= 4 {
|
||||
s.bw.flush32()
|
||||
v3, v2, v1, v0 := src[ip-4], src[ip-3], src[ip-2], src[ip-1]
|
||||
c2.encode(tt[v0])
|
||||
c1.encode(tt[v1])
|
||||
c2.encode(tt[v2])
|
||||
c1.encode(tt[v3])
|
||||
ip -= 4
|
||||
}
|
||||
case !s.zeroBits:
|
||||
// We do not need to check if any output is 0 bits.
|
||||
for ip >= 4 {
|
||||
s.bw.flush32()
|
||||
v3, v2, v1, v0 := src[ip-4], src[ip-3], src[ip-2], src[ip-1]
|
||||
c2.encode(tt[v0])
|
||||
c1.encode(tt[v1])
|
||||
s.bw.flush32()
|
||||
c2.encode(tt[v2])
|
||||
c1.encode(tt[v3])
|
||||
ip -= 4
|
||||
}
|
||||
case s.actualTableLog <= 8:
|
||||
// We can encode 4 symbols without requiring a flush
|
||||
for ip >= 4 {
|
||||
s.bw.flush32()
|
||||
v3, v2, v1, v0 := src[ip-4], src[ip-3], src[ip-2], src[ip-1]
|
||||
c2.encodeZero(tt[v0])
|
||||
c1.encodeZero(tt[v1])
|
||||
c2.encodeZero(tt[v2])
|
||||
c1.encodeZero(tt[v3])
|
||||
ip -= 4
|
||||
}
|
||||
default:
|
||||
for ip >= 4 {
|
||||
s.bw.flush32()
|
||||
v3, v2, v1, v0 := src[ip-4], src[ip-3], src[ip-2], src[ip-1]
|
||||
c2.encodeZero(tt[v0])
|
||||
c1.encodeZero(tt[v1])
|
||||
s.bw.flush32()
|
||||
c2.encodeZero(tt[v2])
|
||||
c1.encodeZero(tt[v3])
|
||||
ip -= 4
|
||||
}
|
||||
}
|
||||
|
||||
// Flush final state.
|
||||
// Used to initialize state when decoding.
|
||||
c2.flush(s.actualTableLog)
|
||||
c1.flush(s.actualTableLog)
|
||||
|
||||
return s.bw.close()
|
||||
}
|
||||
|
||||
// writeCount will write the normalized histogram count to header.
|
||||
// This is read back by readNCount.
|
||||
func (s *Scratch) writeCount() error {
|
||||
var (
|
||||
tableLog = s.actualTableLog
|
||||
tableSize = 1 << tableLog
|
||||
previous0 bool
|
||||
charnum uint16
|
||||
|
||||
maxHeaderSize = ((int(s.symbolLen) * int(tableLog)) >> 3) + 3
|
||||
|
||||
// Write Table Size
|
||||
bitStream = uint32(tableLog - minTablelog)
|
||||
bitCount = uint(4)
|
||||
remaining = int16(tableSize + 1) /* +1 for extra accuracy */
|
||||
threshold = int16(tableSize)
|
||||
nbBits = uint(tableLog + 1)
|
||||
)
|
||||
if cap(s.Out) < maxHeaderSize {
|
||||
s.Out = make([]byte, 0, s.br.remain()+maxHeaderSize)
|
||||
}
|
||||
outP := uint(0)
|
||||
out := s.Out[:maxHeaderSize]
|
||||
|
||||
// stops at 1
|
||||
for remaining > 1 {
|
||||
if previous0 {
|
||||
start := charnum
|
||||
for s.norm[charnum] == 0 {
|
||||
charnum++
|
||||
}
|
||||
for charnum >= start+24 {
|
||||
start += 24
|
||||
bitStream += uint32(0xFFFF) << bitCount
|
||||
out[outP] = byte(bitStream)
|
||||
out[outP+1] = byte(bitStream >> 8)
|
||||
outP += 2
|
||||
bitStream >>= 16
|
||||
}
|
||||
for charnum >= start+3 {
|
||||
start += 3
|
||||
bitStream += 3 << bitCount
|
||||
bitCount += 2
|
||||
}
|
||||
bitStream += uint32(charnum-start) << bitCount
|
||||
bitCount += 2
|
||||
if bitCount > 16 {
|
||||
out[outP] = byte(bitStream)
|
||||
out[outP+1] = byte(bitStream >> 8)
|
||||
outP += 2
|
||||
bitStream >>= 16
|
||||
bitCount -= 16
|
||||
}
|
||||
}
|
||||
|
||||
count := s.norm[charnum]
|
||||
charnum++
|
||||
max := (2*threshold - 1) - remaining
|
||||
if count < 0 {
|
||||
remaining += count
|
||||
} else {
|
||||
remaining -= count
|
||||
}
|
||||
count++ // +1 for extra accuracy
|
||||
if count >= threshold {
|
||||
count += max // [0..max[ [max..threshold[ (...) [threshold+max 2*threshold[
|
||||
}
|
||||
bitStream += uint32(count) << bitCount
|
||||
bitCount += nbBits
|
||||
if count < max {
|
||||
bitCount--
|
||||
}
|
||||
|
||||
previous0 = count == 1
|
||||
if remaining < 1 {
|
||||
return errors.New("internal error: remaining<1")
|
||||
}
|
||||
for remaining < threshold {
|
||||
nbBits--
|
||||
threshold >>= 1
|
||||
}
|
||||
|
||||
if bitCount > 16 {
|
||||
out[outP] = byte(bitStream)
|
||||
out[outP+1] = byte(bitStream >> 8)
|
||||
outP += 2
|
||||
bitStream >>= 16
|
||||
bitCount -= 16
|
||||
}
|
||||
}
|
||||
|
||||
out[outP] = byte(bitStream)
|
||||
out[outP+1] = byte(bitStream >> 8)
|
||||
outP += (bitCount + 7) / 8
|
||||
|
||||
if uint16(charnum) > s.symbolLen {
|
||||
return errors.New("internal error: charnum > s.symbolLen")
|
||||
}
|
||||
s.Out = out[:outP]
|
||||
return nil
|
||||
}
|
||||
|
||||
// symbolTransform contains the state transform for a symbol.
|
||||
type symbolTransform struct {
|
||||
deltaFindState int32
|
||||
deltaNbBits uint32
|
||||
}
|
||||
|
||||
// String prints values as a human readable string.
|
||||
func (s symbolTransform) String() string {
|
||||
return fmt.Sprintf("dnbits: %08x, fs:%d", s.deltaNbBits, s.deltaFindState)
|
||||
}
|
||||
|
||||
// cTable contains tables used for compression.
|
||||
type cTable struct {
|
||||
tableSymbol []byte
|
||||
stateTable []uint16
|
||||
symbolTT []symbolTransform
|
||||
}
|
||||
|
||||
// allocCtable will allocate tables needed for compression.
|
||||
// If existing tables a re big enough, they are simply re-used.
|
||||
func (s *Scratch) allocCtable() {
|
||||
tableSize := 1 << s.actualTableLog
|
||||
// get tableSymbol that is big enough.
|
||||
if cap(s.ct.tableSymbol) < int(tableSize) {
|
||||
s.ct.tableSymbol = make([]byte, tableSize)
|
||||
}
|
||||
s.ct.tableSymbol = s.ct.tableSymbol[:tableSize]
|
||||
|
||||
ctSize := tableSize
|
||||
if cap(s.ct.stateTable) < ctSize {
|
||||
s.ct.stateTable = make([]uint16, ctSize)
|
||||
}
|
||||
s.ct.stateTable = s.ct.stateTable[:ctSize]
|
||||
|
||||
if cap(s.ct.symbolTT) < 256 {
|
||||
s.ct.symbolTT = make([]symbolTransform, 256)
|
||||
}
|
||||
s.ct.symbolTT = s.ct.symbolTT[:256]
|
||||
}
|
||||
|
||||
// buildCTable will populate the compression table so it is ready to be used.
|
||||
func (s *Scratch) buildCTable() error {
|
||||
tableSize := uint32(1 << s.actualTableLog)
|
||||
highThreshold := tableSize - 1
|
||||
var cumul [maxSymbolValue + 2]int16
|
||||
|
||||
s.allocCtable()
|
||||
tableSymbol := s.ct.tableSymbol[:tableSize]
|
||||
// symbol start positions
|
||||
{
|
||||
cumul[0] = 0
|
||||
for ui, v := range s.norm[:s.symbolLen-1] {
|
||||
u := byte(ui) // one less than reference
|
||||
if v == -1 {
|
||||
// Low proba symbol
|
||||
cumul[u+1] = cumul[u] + 1
|
||||
tableSymbol[highThreshold] = u
|
||||
highThreshold--
|
||||
} else {
|
||||
cumul[u+1] = cumul[u] + v
|
||||
}
|
||||
}
|
||||
// Encode last symbol separately to avoid overflowing u
|
||||
u := int(s.symbolLen - 1)
|
||||
v := s.norm[s.symbolLen-1]
|
||||
if v == -1 {
|
||||
// Low proba symbol
|
||||
cumul[u+1] = cumul[u] + 1
|
||||
tableSymbol[highThreshold] = byte(u)
|
||||
highThreshold--
|
||||
} else {
|
||||
cumul[u+1] = cumul[u] + v
|
||||
}
|
||||
if uint32(cumul[s.symbolLen]) != tableSize {
|
||||
return fmt.Errorf("internal error: expected cumul[s.symbolLen] (%d) == tableSize (%d)", cumul[s.symbolLen], tableSize)
|
||||
}
|
||||
cumul[s.symbolLen] = int16(tableSize) + 1
|
||||
}
|
||||
// Spread symbols
|
||||
s.zeroBits = false
|
||||
{
|
||||
step := tableStep(tableSize)
|
||||
tableMask := tableSize - 1
|
||||
var position uint32
|
||||
// if any symbol > largeLimit, we may have 0 bits output.
|
||||
largeLimit := int16(1 << (s.actualTableLog - 1))
|
||||
for ui, v := range s.norm[:s.symbolLen] {
|
||||
symbol := byte(ui)
|
||||
if v > largeLimit {
|
||||
s.zeroBits = true
|
||||
}
|
||||
for nbOccurrences := int16(0); nbOccurrences < v; nbOccurrences++ {
|
||||
tableSymbol[position] = symbol
|
||||
position = (position + step) & tableMask
|
||||
for position > highThreshold {
|
||||
position = (position + step) & tableMask
|
||||
} /* Low proba area */
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have gone through all positions
|
||||
if position != 0 {
|
||||
return errors.New("position!=0")
|
||||
}
|
||||
}
|
||||
|
||||
// Build table
|
||||
table := s.ct.stateTable
|
||||
{
|
||||
tsi := int(tableSize)
|
||||
for u, v := range tableSymbol {
|
||||
// TableU16 : sorted by symbol order; gives next state value
|
||||
table[cumul[v]] = uint16(tsi + u)
|
||||
cumul[v]++
|
||||
}
|
||||
}
|
||||
|
||||
// Build Symbol Transformation Table
|
||||
{
|
||||
total := int16(0)
|
||||
symbolTT := s.ct.symbolTT[:s.symbolLen]
|
||||
tableLog := s.actualTableLog
|
||||
tl := (uint32(tableLog) << 16) - (1 << tableLog)
|
||||
for i, v := range s.norm[:s.symbolLen] {
|
||||
switch v {
|
||||
case 0:
|
||||
case -1, 1:
|
||||
symbolTT[i].deltaNbBits = tl
|
||||
symbolTT[i].deltaFindState = int32(total - 1)
|
||||
total++
|
||||
default:
|
||||
maxBitsOut := uint32(tableLog) - highBits(uint32(v-1))
|
||||
minStatePlus := uint32(v) << maxBitsOut
|
||||
symbolTT[i].deltaNbBits = (maxBitsOut << 16) - minStatePlus
|
||||
symbolTT[i].deltaFindState = int32(total - v)
|
||||
total += v
|
||||
}
|
||||
}
|
||||
if total != int16(tableSize) {
|
||||
return fmt.Errorf("total mismatch %d (got) != %d (want)", total, tableSize)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// countSimple will create a simple histogram in s.count.
|
||||
// Returns the biggest count.
|
||||
// Does not update s.clearCount.
|
||||
func (s *Scratch) countSimple(in []byte) (max int) {
|
||||
for _, v := range in {
|
||||
s.count[v]++
|
||||
}
|
||||
m := uint32(0)
|
||||
for i, v := range s.count[:] {
|
||||
if v > m {
|
||||
m = v
|
||||
}
|
||||
if v > 0 {
|
||||
s.symbolLen = uint16(i) + 1
|
||||
}
|
||||
}
|
||||
return int(m)
|
||||
}
|
||||
|
||||
// minTableLog provides the minimum logSize to safely represent a distribution.
|
||||
func (s *Scratch) minTableLog() uint8 {
|
||||
minBitsSrc := highBits(uint32(s.br.remain()-1)) + 1
|
||||
minBitsSymbols := highBits(uint32(s.symbolLen-1)) + 2
|
||||
if minBitsSrc < minBitsSymbols {
|
||||
return uint8(minBitsSrc)
|
||||
}
|
||||
return uint8(minBitsSymbols)
|
||||
}
|
||||
|
||||
// optimalTableLog calculates and sets the optimal tableLog in s.actualTableLog
|
||||
func (s *Scratch) optimalTableLog() {
|
||||
tableLog := s.TableLog
|
||||
minBits := s.minTableLog()
|
||||
maxBitsSrc := uint8(highBits(uint32(s.br.remain()-1))) - 2
|
||||
if maxBitsSrc < tableLog {
|
||||
// Accuracy can be reduced
|
||||
tableLog = maxBitsSrc
|
||||
}
|
||||
if minBits > tableLog {
|
||||
tableLog = minBits
|
||||
}
|
||||
// Need a minimum to safely represent all symbol values
|
||||
if tableLog < minTablelog {
|
||||
tableLog = minTablelog
|
||||
}
|
||||
if tableLog > maxTableLog {
|
||||
tableLog = maxTableLog
|
||||
}
|
||||
s.actualTableLog = tableLog
|
||||
}
|
||||
|
||||
var rtbTable = [...]uint32{0, 473195, 504333, 520860, 550000, 700000, 750000, 830000}
|
||||
|
||||
// normalizeCount will normalize the count of the symbols so
|
||||
// the total is equal to the table size.
|
||||
func (s *Scratch) normalizeCount() error {
|
||||
var (
|
||||
tableLog = s.actualTableLog
|
||||
scale = 62 - uint64(tableLog)
|
||||
step = (1 << 62) / uint64(s.br.remain())
|
||||
vStep = uint64(1) << (scale - 20)
|
||||
stillToDistribute = int16(1 << tableLog)
|
||||
largest int
|
||||
largestP int16
|
||||
lowThreshold = (uint32)(s.br.remain() >> tableLog)
|
||||
)
|
||||
|
||||
for i, cnt := range s.count[:s.symbolLen] {
|
||||
// already handled
|
||||
// if (count[s] == s.length) return 0; /* rle special case */
|
||||
|
||||
if cnt == 0 {
|
||||
s.norm[i] = 0
|
||||
continue
|
||||
}
|
||||
if cnt <= lowThreshold {
|
||||
s.norm[i] = -1
|
||||
stillToDistribute--
|
||||
} else {
|
||||
proba := (int16)((uint64(cnt) * step) >> scale)
|
||||
if proba < 8 {
|
||||
restToBeat := vStep * uint64(rtbTable[proba])
|
||||
v := uint64(cnt)*step - (uint64(proba) << scale)
|
||||
if v > restToBeat {
|
||||
proba++
|
||||
}
|
||||
}
|
||||
if proba > largestP {
|
||||
largestP = proba
|
||||
largest = i
|
||||
}
|
||||
s.norm[i] = proba
|
||||
stillToDistribute -= proba
|
||||
}
|
||||
}
|
||||
|
||||
if -stillToDistribute >= (s.norm[largest] >> 1) {
|
||||
// corner case, need another normalization method
|
||||
return s.normalizeCount2()
|
||||
}
|
||||
s.norm[largest] += stillToDistribute
|
||||
return nil
|
||||
}
|
||||
|
||||
// Secondary normalization method.
|
||||
// To be used when primary method fails.
|
||||
func (s *Scratch) normalizeCount2() error {
|
||||
const notYetAssigned = -2
|
||||
var (
|
||||
distributed uint32
|
||||
total = uint32(s.br.remain())
|
||||
tableLog = s.actualTableLog
|
||||
lowThreshold = uint32(total >> tableLog)
|
||||
lowOne = uint32((total * 3) >> (tableLog + 1))
|
||||
)
|
||||
for i, cnt := range s.count[:s.symbolLen] {
|
||||
if cnt == 0 {
|
||||
s.norm[i] = 0
|
||||
continue
|
||||
}
|
||||
if cnt <= lowThreshold {
|
||||
s.norm[i] = -1
|
||||
distributed++
|
||||
total -= cnt
|
||||
continue
|
||||
}
|
||||
if cnt <= lowOne {
|
||||
s.norm[i] = 1
|
||||
distributed++
|
||||
total -= cnt
|
||||
continue
|
||||
}
|
||||
s.norm[i] = notYetAssigned
|
||||
}
|
||||
toDistribute := (1 << tableLog) - distributed
|
||||
|
||||
if (total / toDistribute) > lowOne {
|
||||
// risk of rounding to zero
|
||||
lowOne = uint32((total * 3) / (toDistribute * 2))
|
||||
for i, cnt := range s.count[:s.symbolLen] {
|
||||
if (s.norm[i] == notYetAssigned) && (cnt <= lowOne) {
|
||||
s.norm[i] = 1
|
||||
distributed++
|
||||
total -= cnt
|
||||
continue
|
||||
}
|
||||
}
|
||||
toDistribute = (1 << tableLog) - distributed
|
||||
}
|
||||
if distributed == uint32(s.symbolLen)+1 {
|
||||
// all values are pretty poor;
|
||||
// probably incompressible data (should have already been detected);
|
||||
// find max, then give all remaining points to max
|
||||
var maxV int
|
||||
var maxC uint32
|
||||
for i, cnt := range s.count[:s.symbolLen] {
|
||||
if cnt > maxC {
|
||||
maxV = i
|
||||
maxC = cnt
|
||||
}
|
||||
}
|
||||
s.norm[maxV] += int16(toDistribute)
|
||||
return nil
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
// all of the symbols were low enough for the lowOne or lowThreshold
|
||||
for i := uint32(0); toDistribute > 0; i = (i + 1) % (uint32(s.symbolLen)) {
|
||||
if s.norm[i] > 0 {
|
||||
toDistribute--
|
||||
s.norm[i]++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
vStepLog = 62 - uint64(tableLog)
|
||||
mid = uint64((1 << (vStepLog - 1)) - 1)
|
||||
rStep = (((1 << vStepLog) * uint64(toDistribute)) + mid) / uint64(total) // scale on remaining
|
||||
tmpTotal = mid
|
||||
)
|
||||
for i, cnt := range s.count[:s.symbolLen] {
|
||||
if s.norm[i] == notYetAssigned {
|
||||
var (
|
||||
end = tmpTotal + uint64(cnt)*rStep
|
||||
sStart = uint32(tmpTotal >> vStepLog)
|
||||
sEnd = uint32(end >> vStepLog)
|
||||
weight = sEnd - sStart
|
||||
)
|
||||
if weight < 1 {
|
||||
return errors.New("weight < 1")
|
||||
}
|
||||
s.norm[i] = int16(weight)
|
||||
tmpTotal = end
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateNorm validates the normalized histogram table.
|
||||
func (s *Scratch) validateNorm() (err error) {
|
||||
var total int
|
||||
for _, v := range s.norm[:s.symbolLen] {
|
||||
if v >= 0 {
|
||||
total += int(v)
|
||||
} else {
|
||||
total -= int(v)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
fmt.Printf("selected TableLog: %d, Symbol length: %d\n", s.actualTableLog, s.symbolLen)
|
||||
for i, v := range s.norm[:s.symbolLen] {
|
||||
fmt.Printf("%3d: %5d -> %4d \n", i, s.count[i], v)
|
||||
}
|
||||
}()
|
||||
if total != (1 << s.actualTableLog) {
|
||||
return fmt.Errorf("warning: Total == %d != %d", total, 1<<s.actualTableLog)
|
||||
}
|
||||
for i, v := range s.count[s.symbolLen:] {
|
||||
if v != 0 {
|
||||
return fmt.Errorf("warning: Found symbol out of range, %d after cut", i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user