Агрегация перцентилей
Агрегация метрик, которая вычисляет один или несколько перцентилей по числовым значениям, извлечённым из агрегированных документов. Эти значения могут быть извлечены из определённых числовых или гистограммных полей в документах.
Перцентили показывают точку, в которой находится определённый процент наблюдаемых значений. Например, 95-й перцентиль — это значение, которое больше 95% наблюдаемых значений.
Перцентили часто используются для поиска выбросов. В нормальных распределениях 0,13-й и 99,87-й перцентили представляют собой три стандартных отклонения от среднего значения. Любые данные, которые выходят за пределы трёх стандартных отклонений, часто считаются аномалиями.
Когда извлекается диапазон перцентилей, их можно использовать для оценки распределения данных и определения, является ли оно смещённым, бимодальным и т. д.
Предположим, ваши данные представляют собой время загрузки веб-сайта. Среднее и медианное время загрузки не очень полезны администратору. Максимальное значение может быть интересным, но его легко исказить одним медленным ответом.
Давайте рассмотрим диапазон перцентилей, представляющих время загрузки:
resp = client.search(
index="latency",
size=0,
aggs={
"load_time_outlier": {
"percentiles": {
"field": "load_time"
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
aggregations: {
load_time_outlier: {
percentiles: {
field: 'load_time'
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
aggs: {
load_time_outlier: {
percentiles: {
field: "load_time",
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time"
}
}
}
} | Поле |
По умолчанию метрика percentile сгенерирует диапазон перцентилей: [ 1, 5, 25, 50, 75, 95, 99 ]. Ответ будет выглядеть так:
{
...
"aggregations": {
"load_time_outlier": {
"values": {
"1.0": 10.0,
"5.0": 30.0,
"25.0": 170.0,
"50.0": 445.0,
"75.0": 720.0,
"95.0": 940.0,
"99.0": 980.0
}
}
}
} Как вы можете видеть, агрегация вернёт вычисленное значение для каждого перцентиля в стандартном диапазоне. Если мы предположим, что время отклика измеряется в миллисекундах, то сразу становится очевидно, что веб-страница обычно загружается за 10–720 мс, но иногда время загрузки возрастает до 940–980 мс.
Часто администраторов интересуют только выбросы — крайние перцентили. Мы можем указать только те проценты, которые нас интересуют (запрашиваемые перцентили должны быть значениями от 0 до 100 включительно):
resp = client.search(
index="latency",
size=0,
aggs={
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"percents": [
95,
99,
99.9
]
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
aggregations: {
load_time_outlier: {
percentiles: {
field: 'load_time',
percents: [
95,
99,
99.9
]
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
aggs: {
load_time_outlier: {
percentiles: {
field: "load_time",
percents: [95, 99, 99.9],
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"percents": [ 95, 99, 99.9 ]
}
}
}
} | Используйте параметр |
Ключевой ответ
По умолчанию флаг keyed установлен в значение true, что связывает уникальный строковый ключ с каждым бакетом и возвращает диапазоны в виде словаря, а не массива. Установка флага keyed в значение false отключит это поведение:
resp = client.search(
index="latency",
size=0,
aggs={
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"keyed": False
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
aggregations: {
load_time_outlier: {
percentiles: {
field: 'load_time',
keyed: false
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
aggs: {
load_time_outlier: {
percentiles: {
field: "load_time",
keyed: false,
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"keyed": false
}
}
}
} Ответ:
{
...
"aggregations": {
"load_time_outlier": {
"values": [
{
"key": 1.0,
"value": 10.0
},
{
"key": 5.0,
"value": 30.0
},
{
"key": 25.0,
"value": 170.0
},
{
"key": 50.0,
"value": 445.0
},
{
"key": 75.0,
"value": 720.0
},
{
"key": 95.0,
"value": 940.0
},
{
"key": 99.0,
"value": 980.0
}
]
}
}
} Скрипт
Если вам нужно выполнить агрегацию по значениям, которые не индексируются, используйте динамическое поле. Например, если наше время загрузки измеряется в миллисекундах, но вы хотите вычислить перцентили в секундах:
resp = client.search(
index="latency",
size=0,
runtime_mappings={
"load_time.seconds": {
"type": "long",
"script": {
"source": "emit(doc['load_time'].value / params.timeUnit)",
"params": {
"timeUnit": 1000
}
}
}
},
aggs={
"load_time_outlier": {
"percentiles": {
"field": "load_time.seconds"
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
runtime_mappings: {
'load_time.seconds' => {
type: 'long',
script: {
source: "emit(doc['load_time'].value / params.timeUnit)",
params: {
"timeUnit": 1000
}
}
}
},
aggregations: {
load_time_outlier: {
percentiles: {
field: 'load_time.seconds'
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
runtime_mappings: {
"load_time.seconds": {
type: "long",
script: {
source: "emit(doc['load_time'].value / params.timeUnit)",
params: {
timeUnit: 1000,
},
},
},
},
aggs: {
load_time_outlier: {
percentiles: {
field: "load_time.seconds",
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"runtime_mappings": {
"load_time.seconds": {
"type": "long",
"script": {
"source": "emit(doc['load_time'].value / params.timeUnit)",
"params": {
"timeUnit": 1000
}
}
}
},
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time.seconds"
}
}
}
} Перцентили (обычно) приблизительные
Существует много различных алгоритмов для вычисления перцентилей. Примитивное реализация просто сохраняет все значения в отсортированном массиве. Чтобы найти 50-й перцентиль, вы просто находите значение, которое находится в my_array[count(my_array) * 0.5].
Очевидно, что примитивная реализация не масштабируется — отсортированный массив растёт линейно с количеством значений в наборе данных. Для вычисления перцентилей по потенциально миллиардам значений в кластере Elasticsearch вычисляются *приблизительные* перцентили.
Алгоритм, используемый метрикой percentile, называется TDigest (представлен Тедом Даннингом в Computing Accurate Quantiles using T-Digests).
При использовании этой метрики следует учитывать несколько рекомендаций:
- Точность пропорциональна
q(1-q). Это означает, что крайние перцентили (например, 99%) более точные, чем менее крайние перцентили, такие как медиана - Для небольших наборов значений перцентили очень точные (и потенциально 100% точные, если данные достаточно малы).
- По мере увеличения количества значений в бакете алгоритм начинает приближать перцентили. Он фактически жертвует точностью для экономии памяти. Точный уровень неточности сложно обобщить, так как он зависит от распределения ваших данных и объёма агрегируемых данных
На следующем графике показана относительная ошибка на равномерном распределении в зависимости от количества собранных значений и запрашиваемого перцентиля:
Он показывает, как точность лучше для крайних перцентилей. Причина, по которой ошибка уменьшается при большом количестве значений, заключается в том, что закон больших чисел делает распределение значений всё более равномерным, и дерево t-digest может лучше его обобщить. Это не будет происходить при более смещённых распределениях.
Агрегации перцентилей также являются недетерминированными. Это означает, что вы можете получить немного разные результаты при использовании одних и тех же данных.
Сжатие
Приблизительные алгоритмы должны балансировать использование памяти и точность оценки. Этот баланс можно контролировать с помощью параметра compression:
resp = client.search(
index="latency",
size=0,
aggs={
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"tdigest": {
"compression": 200
}
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
aggregations: {
load_time_outlier: {
percentiles: {
field: 'load_time',
tdigest: {
compression: 200
}
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
aggs: {
load_time_outlier: {
percentiles: {
field: "load_time",
tdigest: {
compression: 200,
},
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"tdigest": {
"compression": 200
}
}
}
}
} | Сжатие управляет использованием памяти и ошибкой приближения |
Алгоритм TDigest использует ряд «узлов» для приближения перцентилей — чем больше узлов доступно, тем выше точность (и больший объём памяти), пропорционально объёму данных. Параметр compression ограничивает максимальное количество узлов до 20 * compression.
Следовательно, увеличивая значение сжатия, вы можете повысить точность перцентилей за счёт большей потребности в памяти. Более высокие значения сжатия также замедляют алгоритм, так как размер базовой структуры данных дерева увеличивается, что приводит к более дорогостоящим операциям. Значение сжатия по умолчанию равно 100.
«Узел» использует примерно 32 байта памяти, поэтому в худшем случае (большое количество данных, которые поступают в отсортированном и упорядоченном виде) значения по умолчанию создадут TDigest размером примерно 64 КБ. На практике данные имеют тенденцию к большей случайности, и TDigest будет использовать меньше памяти.
Подсказка по выполнению
По умолчанию реализация TDigest оптимизирована для производительности, масштабируется до миллионов или даже миллиардов значений выборки, сохраняя приемлемые уровни точности (в некоторых случаях близкие к 1% относительной ошибки для миллионов выборок). Существует возможность использовать реализацию, оптимизированную для точности, установив параметр execution_hint в значение high_accuracy:
resp = client.search(
index="latency",
size=0,
aggs={
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"tdigest": {
"execution_hint": "high_accuracy"
}
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
aggregations: {
load_time_outlier: {
percentiles: {
field: 'load_time',
tdigest: {
execution_hint: 'high_accuracy'
}
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
aggs: {
load_time_outlier: {
percentiles: {
field: "load_time",
tdigest: {
execution_hint: "high_accuracy",
},
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"tdigest": {
"execution_hint": "high_accuracy"
}
}
}
}
} | Оптимизировать TDigest для точности, за счёт производительности |
Этот вариант может привести к улучшению точности (относительная ошибка близка к 0,01% для миллионов выборок в некоторых случаях), но тогда запросы перцентилей будут выполняться в 2–10 раз дольше.
HDR гистограмма
HDR гистограмма (High Dynamic Range Histogram) — это альтернативная реализация, которая может быть полезна при расчёте перцентилей для измерений задержки, так как она может быть быстрее, чем реализация t-digest, но при этом требует больше памяти. Эта реализация поддерживает фиксированную процентную ошибку в худшем случае (указана как количество значащих цифр). Это означает, что если данные записываются со значениями от 1 микросекунды до 1 часа (3 600 000 000 микросекунд) в гистограмме с 3 значащими цифрами, она будет сохранять разрешение в 1 микросекунду для значений до 1 миллисекунды и 3,6 секунды (или лучше) для максимального отслеживаемого значения (1 час).
HDR гистограмму можно использовать, указав параметр hdr в запросе:
resp = client.search(
index="latency",
size=0,
aggs={
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"percents": [
95,
99,
99.9
],
"hdr": {
"number_of_significant_value_digits": 3
}
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
aggregations: {
load_time_outlier: {
percentiles: {
field: 'load_time',
percents: [
95,
99,
99.9
],
hdr: {
number_of_significant_value_digits: 3
}
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
aggs: {
load_time_outlier: {
percentiles: {
field: "load_time",
percents: [95, 99, 99.9],
hdr: {
number_of_significant_value_digits: 3,
},
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time",
"percents": [ 95, 99, 99.9 ],
"hdr": {
"number_of_significant_value_digits": 3
}
}
}
}
} | Объект | |
|
|
HDR гистограмма поддерживает только положительные значения и выдаст ошибку, если ей передано отрицательное значение. Также не рекомендуется использовать HDR гистограмму, если диапазон значений неизвестен, так как это может привести к высокому потреблению памяти.
Пропущенное значение
Параметр missing определяет, как должны обрабатываться документы, у которых отсутствует значение. По умолчанию они будут игнорироваться, но также можно обработать их так, как будто у них есть значение.
resp = client.search(
index="latency",
size=0,
aggs={
"grade_percentiles": {
"percentiles": {
"field": "grade",
"missing": 10
}
}
},
)
print(resp) response = client.search(
index: 'latency',
body: {
size: 0,
aggregations: {
grade_percentiles: {
percentiles: {
field: 'grade',
missing: 10
}
}
}
}
)
puts response const response = await client.search({
index: "latency",
size: 0,
aggs: {
grade_percentiles: {
percentiles: {
field: "grade",
missing: 10,
},
},
},
});
console.log(response); GET latency/_search
{
"size": 0,
"aggs": {
"grade_percentiles": {
"percentiles": {
"field": "grade",
"missing": 10
}
}
}
} | Документы без значения в поле |
© 2023-2025 Elasticsearch
As of September 2024, Elasticsearch is available under a choice of three licenses: the Server Side Public License (SSPL), the Elastic License, or the AGPLv3 (OSI approved).
Elasticsearch and the Elasticsearch logo are trademarks of Elasticsearch B.V., registered in the U.S. and in other countries.
https://www.elastic.co/guide/en/elasticsearch/reference/8.17/search-aggregations-metrics-percentile-aggregation.html