百分位数(percentiles)聚合

一个 multi-value(多值) 度量聚合,计算从聚合文档中提取的数值的一个或多个百分位数。 这些值可以由给定的脚本生成,也可以从文档中的特定数值或 histogram 字段 中提取。

百分位数表示观察值出现一定百分比的点。 例如,第 95 百分位是大于观察值的 95% 的值。

百分位数通常用于发现异常值。 在正态分布中,第 0.13 和 99.87 百分位代表平均值的 3 个标准差。 任何超出 3 个标准偏差的数据通常被认为是异常的。

当检索到一个百分比范围时,可以使用它们来估计数据分布,并确定数据是否倾斜、双峰等。

假设数据由网站加载时间组成。 加载时间的平均值和中值对管理员来说不是很有用。 最大值可能很有趣,但它很容易被一个缓慢的响应所扭曲。

让我们看看代表加载时间(load_time)的百分比范围:

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time" 
            }
        }
    }
}

字段 load_time 必须是一个 numeric 类型的字段。

默认情况下,percentile 度量将生成一个百分位数范围:[ 1, 5, 25, 50, 75, 95, 99 ]。 响应将如下所示:

{
    ...

   "aggregations": {
      "load_time_outlier": {
         "values" : {
            "1.0": 5.0,
            "5.0": 25.0,
            "25.0": 165.0,
            "50.0": 445.0,
            "75.0": 725.0,
            "95.0": 945.0,
            "99.0": 985.0
         }
      }
   }
}

如你所见,该聚合返回默认范围内每个百分位的计算值。 如果假设响应时间以毫秒为单位,很明显网页通常在 10-725 毫秒内加载,但偶尔会达到 945-985毫秒。

通常,管理员只对异常值-极端的百分位数-感兴趣。 我们可以只指定我们感兴趣的百分比(请求的百分位数必须是0-100之间的值,包括0和100):

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time",
                "percents" : [95, 99, 99.9] 
            }
        }
    }
}

使用 percents 参数指定要计算的特定百分位数

keyed 响应

默认情况下,keyed 标志设置为 true,它将唯一的字符串键与每个桶相关联,并将范围作为哈希而不是数组返回。 将 keyed 标志设置为 false 将禁用此行为:

GET latency/_search
{
    "size": 0,
    "aggs": {
        "load_time_outlier": {
            "percentiles": {
                "field": "load_time",
                "keyed": false
            }
        }
    }
}

响应:

{
    ...

    "aggregations": {
        "load_time_outlier": {
            "values": [
                {
                    "key": 1.0,
                    "value": 5.0
                },
                {
                    "key": 5.0,
                    "value": 25.0
                },
                {
                    "key": 25.0,
                    "value": 165.0
                },
                {
                    "key": 50.0,
                    "value": 445.0
                },
                {
                    "key": 75.0,
                    "value": 725.0
                },
                {
                    "key": 95.0,
                    "value": 945.0
                },
                {
                    "key": 99.0,
                    "value": 985.0
                }
            ]
        }
    }
}

脚本

百分位数度量支持脚本。 例如,如果加载时间是以毫秒为单位的,但我们希望以秒为单位计算百分位数,我们可以使用一个脚本来进行动态转换:

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "script" : {
                    "lang": "painless",
                    "source": "doc['load_time'].value / params.timeUnit", 
                    "params" : {
                        "timeUnit" : 1000   
                    }
                }
            }
        }
    }
}

参数 field 被替换为 script,它使用脚本来生成计算百分位的值

像任何其他脚本一样,这里的脚本支持参数化输入

这将使用 painless(无痛) 脚本语言,没有脚本参数。 要使用存储的脚本,请使用以下语法:

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "script" : {
                    "id": "my_script",
                    "params": {
                        "field": "load_time"
                    }
                }
            }
        }
    }
}

百分位数(通常)是近似值

有许多不同的算法来计算百分位数。 简单的实现只是将所有的值存储在一个有序的数组中。 要找到第50个百分位数,只需找到位于 my_array[count(my_array) * 0.5] 的值。

显然,简单的实现是不可伸缩的——排序后的数组随着数据集中值的数量线性增长。 要计算 Elasticsearch 集群中潜在的数十亿个值的百分位数,需要计算近似(approximate)百分位数。

percentile 度量使用的算法称为 TDigest (由 Ted Dunning 在使用T-Digests计算精确分位数中引入)。

使用这一度量时,需要记住一些准则:

  • 精度与 q(1-q) 成正比。 这意味着极端百分位数(例如99%)比不太极端的百分位数(例如中值)更准确
  • 对于小的数值集合,百分位数是高度准确的(如果数据足够小,可能是100%准确)。
  • 随着桶中值的数量增加,算法开始近似百分位数。 这实际上是在用准确性来减少内存的使用量。 不准确的确切程度很难概括,因为它取决于数据分布和要聚合的数据量

下图显示了均匀分布的相对误差,它取决于采集值的数量和要求的百分位数:

percentiles error

它显示了极端百分位数的精度更好。 对于大量的值,误差减小的原因是大数定律使值的分布越来越均匀,t-digest 树可以更好地进行汇总。 在偏态分布上就不是这样了。

百分位数聚合也是不确定的(non-deterministic)。 这意味着使用相同的数据可以得到稍微不同的结果。

压缩(compression)

近似算法必须平衡内存利用率和估计精度。 这种平衡可以使用参数 compression 来控制:

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time",
                "tdigest": {
                  "compression" : 200 
                }
            }
        }
    }
}

compression 控制内存使用和近似误差

TDigest算法使用多个“节点”来近似百分位数,可用的节点越多,与数据量成比例的精度(和大内存占用)就越高。 参数 compression 将最大节点数限制为 20 * compression

因此,通过增加 compression 的值,可以以更多内存为代价来提高百分位数的准确性。 较大的 compression 值也会使算法变慢,因为底层树数据结构的大小会增加,从而导致更昂贵的操作。 compression 的默认值为 100

一个“节点”使用大约 32 字节的内存,因此在最坏的情况下(大量数据被排序并按顺序到达),默认设置将产生大约 64KB 大小的 TDigest。 实际上,数据往往更加随机,TDigest 将使用更少的内存。

HDR 直方图(Histogram)

此设置公开了 HDR 直方图的内部实现,语法将来可能会改变。

HDR直方图 (High Dynamic Range Histogram, 高动态范围直方图)是一种替代实现,在计算延迟测量的百分位数时非常有用,因为它比 t-digest 实现更快,但需要更大的内存。 这种实现保持固定的最坏情况百分比误差(指定为有效数字的数量)。 这意味着,如果在设置为 3 个有效数字的直方图中记录的数据值从 1 微秒到 1 小时(3,600,000,000微秒),则对于 1 毫秒和 3.6 秒(或更好)的最大跟踪值(1小时),它将保持 1 微秒的值分辨率。

通过在请求中指定参数 method,可以使用 HDR 直方图:

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直方图来计算百分位数,并且可以在对象内部指定该算法的特定设置

number_of_significant_value_digits 指定直方图数值的分辨率,以有效位数表示

HDR 直方图仅支持正值,如果向其传递负值,将会出错。 如果值的范围未知,使用HDR 直方图也不是一个好主意,因为这可能会导致很高的内存使用率。

缺失的值

参数 missing 定义应该如何处理有缺失值的文档。 默认情况下,它们会被忽略,但也可以将它们视为有一个(默认)值。

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "grade_percentiles" : {
            "percentiles" : {
                "field" : "grade",
                "missing": 10 
            }
        }
    }
}

grade 字段中没有值的文档将与值为 10 的文档落入同一个桶。