Elasticsearch系列---聚合查询(二)
### 概要
### 近似聚合算法
上一篇我们演练的聚合算法,在Elasticsearch分布式场景下,其实是有略微区别的,简单来说我们可以把这些聚合算法分成两类,易并行算法和不易并行算法。
#### 易并行算法
比如max,min,就是多个node或shard可以单独并行计算,并且可以随着机器数的线性增长而横向扩展,没有任何协调操作,得到的结果返回给Coordinate Node时的数据量已经非常小了,像max或min,只需返回给Coordinate Node一个Long值就行。
![易并行算法](https://imgkr.cn-bj.ufileos.com/cb324822-3011-40d3-8dcb-6ab8bd41f4e0.png)
#### 不易并行算法
没有上述的优势,每个node或shard返回的数据都特别大,节点越多,Coordinate Node处理压力越大,如经典的TOP N问题。
![不易并行算法](https://imgkr.cn-bj.ufileos.com/ceca27de-dca3-4fb7-8cb1-23df54a18a86.png)
#### 近似算法
针对易并行算法,处理节点分散,结果准确,但针对不易并行算法,ES会采取近似聚合的方式,采用cardinality或percentiles算法,这两个算法近似估计后的结果,不完全准确,误差率约为0.5%,但是速度会很快,一般会达到完全精准的算法的性能的数十倍。
##### 三角选择原则
有点类似于CAP理论,精准、实时、大数据,只能选择其中2个
1. 精准+实时: 数据量小,随便玩
2. 精准+大数据:hadoop,批处理,非实时,可以处理海量数据,保证精准,可能会跑几个小时
3. 大数据+实时:es,不精准,近似估计,可能会有百分之几的错误率
没有什么方案是完美的,完美主义在这里不好使。
### cartinality去重算法
cartinality可以对每个bucket中指定的field进行去重,取去重后的count,类似于count(distcint),例如,我们统计一下每个月新发布歌单中有多少位歌手
```java
GET /music/children/_search
{
"size" : 0,
"aggs" : {
"months" : {
"date_histogram": {
"field": "releaseDate",
"interval": "month"
},
"aggs": {
"distinct_author_cnt" : {
"cardinality" : {
"field" : "author.keyword"
}
}
}
}
}
}
```
#### cartinality优化
cartinality算法基于HyperLogLog++(HLL)算法的。HLL会先对我们的输入作哈希运算,然后根据哈希运算的结果中的bits做概率估算从而得到基数。
##### precision_threshold参数
权衡准确率与内存开销的参数,值的范围为[0,40000],超过40000的数当作40000来处理。
假设precision_threshold配置为100,如果字段值(歌手数量)在100以内,准确率基本上是,歌手数量大于100时,开始节省内存而牺牲准确度,同时也会对度量结果带入误差。
内存消耗precision_threshold * 8 byte,precision_threshold值越大,内存占用越大。
在实际应用中,100的阈值可以在值为百万的情况下仍然将误差维持5%以内。
例如:
```java
GET /music/children/_search
{
"size" : 0,
"aggs" : {
"months" : {
"date_histogram": {
"field": "releaseDate",
"interval": "month"
},
"aggs": {
"distinct_author_cnt" : {
"cardinality" : {
"field" : "author.keyword",
"precision_threshold": 100
}
}
}
}
}
}
```
##### HLL Hash加速
默认情况下,发送一个cardinality请求的时候,会动态地对所有的field value,取hash值,HLL只需要字段内容的哈希值,我们可以在索引时就预先计算好。就能在查询时跳过哈希计算然后将哈希值从fielddata直接加载出来,即查询时变索引时的优化思路。
我们创建music索引时,把author字段预先执行hash计算,
ES 6.3.1需要先安装murmur3插件:
`elasticsearch-plugin install mapper-murmur3`,
安装成功有如下日志:
```java
[root@localhost bin]# ./elasticsearch-plugin install mapper-murmur3
-> Downloading mapper-murmur3 from elastic
[=================================================]
-> Installed mapper-murmur3
```
请求示例:
```java
PUT /music/
{
"mappings": {
"children": {
"properties": {
"author": {
"type": "text",
"fields": {
"hash": {
"type": "murmur3"
}
}
}
}
}
}
}
```
使用时,我们改用author.hash,如下示例:
```java
GET /music/children/_search
{
"size" : 0,
"aggs" : {
"months" : {
"date_histogram": {
"field": "releaseDate",
"interval": "month"
},
"aggs": {
"distinct_author_cnt" : {
"cardinality" : {
"field" : "author.hash",
"precision_threshold": 100
}
}
}
}
}
}
```
这个对聚合查询可能有一点性能上的提升,但同时也在加大存储的压力,如果是针对特别大的字段,比如Content这种文本,可能有提升的价值,如果是keyword的小文本,一两个单词的那种,求hash值已经是非常快的操作,使用HLL加速的方法可能就没有多大效果。
### percentilies百分比算法
Elasticsearch提供的另一个近似算法,用来找出异常数据,我们知道平均数和中位数的统计结果,会掩盖掉很多真实情况,没有多大实际意义,比如某某地区平均薪酬是xxxx元,大家都对这种报告笑而不语。
但是正态分布的方差和标准差,可以反馈出一些数据的异常,percentilies百分比算法适用于统计这样的数据。
我们另外举一个案例,比如某某音乐网站访问时延统计数据。一般有如下几个统计点:
- tp50:50%的请求的耗时长在多长时间
- tp90:90%的请求的耗时长在多长时间
- tp99:99%的请求的耗时长在多长时间
1. 创建索引
```java
PUT /musicsite
{
"mappings": {
"_doc": {
"properties": {
"latency": {
"type": "long"
},
"province": {
"type": "keyword"
},
"timestamp": {
"type": "date"
}
}
}
}
}
```
2. 灌点测试数据
```java
POST /musicsite/_doc/_bulk
{ "index": {}}
{ "latency" : 56, "province" : "广东", "timestamp" : "2019-12-28" }
{ "index": {}}
{ "latency" : 35, "province" : "广东", "timestamp" : "2019-12-29" }
{ "index": {}}
{ "latency" : 45, "province" : "广东", "timestamp" : "2019-12-29" }
{ "index": {}}
{ "latency" : 69, "province" : "广东", "timestamp" : "2019-12-28" }
{ "index": {}}
{ "latency" : 89, "province" : "广东", "timestamp" : "2019-12-28" }
{ "index": {}}
{ "latency" : 47, "province" : "广东", "timestamp" : "2019-12-29" }
{ "index": {}}
{ "latency" : 123, "province" : "黑龙江", "timestamp" : "2019-12-28" }
{ "index": {}}
{ "latency" : 263, "province" : "黑龙江", "timestamp" : "2019-12-29" }
{ "index": {}}
{ "latency" : 142, "province" : "黑龙江", "timestamp" : "2019-12-29" }
{ "index": {}}
{ "latency" : 269, "province" : "黑龙江", "timestamp" : "2019-12-28" }
{ "index": {}}
{ "latency" : 358, "province" : "黑龙江", "timestamp" : "2019-12-28" }
{ "index": {}}
{ "latency" : 315, "province" : "黑龙江", "timestamp" : "2019-12-29" }
```
3. 执行百分比搜索
```java
GET /musicsite/_doc/_search
{
"size": 0,
"aggs": {
"latency_percentiles": {
"percentiles": {
"field": "latency",
"percents": [
50,95,99
]
}
},
"latency_avg": {
"avg": {
"field": "latency"
}
}
}
}
```
响应结果如下(有删节):
```java
{
"aggregations": {
"latency_avg": {
"value": 150.91666666666666
},
"latency_percentiles": {
"values": {
"50.0": 106,
"95.0": 353.69999999999993,
"99.0": 358
}
}
}
}
```
我们可以看到TP50、TP95、TP99的统计值,并且与平均值的对比,可以发现,平均值掩盖了很多实际的问题,如果只有均值统计,那么很多问题将难以引起警觉。
如果我们把省份条件加上,再做一次聚合统计:
```java
GET /musicsite/_doc/_search
{
"size" : 0,
"aggs" : {
"province" : {
"terms" : {
"field" : "province"
},
"aggs" : {
"load_times" : {
"percentiles" : {
"field" : "latency",
"percents" : [50, 95, 99]
}
},
"load_avg" : {
"avg" : {
"field" : "latency"
}
}
}
}
}
}
```
响应结果如下(有删节):
```java
{
"aggregations": {
"group_by_province": {
"buckets": [
{
"key": "广东",
"doc_count": 6,
"load_times": {
"values": {
"50.0": 51.5,
"95.0": 89,
"99.0": 89
}
},
"load_avg": {
"value": 56.833333333333336
}
},
{
"key": "黑龙江",
"doc_count": 6,
"load_times": {
"values": {
"50.0": 266,
"95.0": 358,
"99.0": 358
}
},
"load_avg": {
"value": 245
}
}
]
}
}
}
```
结果就很明显:黑龙江的访问时延明显比广东地区高了很多。那么基于这个数据分析,如果需要对网站进行提速,可以考虑在东北地区部署服务器或CDN。
#### percentile_ranks百分位等级
百分比算法还有一个比较重要的度量percentile_ranks,与percentiles含义是互为双向的。例如TP 50为106ms,表示50%的请求的耗时长为106ms,而用percentile_ranks的来表示的含义:耗时106ms的请求所占的比例为50%。
例如我们的音乐网站对SLA(服务等级协议)的要求:确保所有的请求,延时都必须在200ms以内。
所以我们在日常的监控中,必须了解有多少请求延时是在200ms以内的,多少请求是在800ms以内的。
```java
GET /musicsite/_doc/_search
{
"size" : 0,
"aggs" : {
"group_by_province" : {
"terms" : {
"field" : "province"
},
"aggs" : {
"load_times" : {
"percentile_ranks" : {
"field" : "latency",
"values" : [200, 800]
}
}
}
}
}
}
```
响应(有删节):
```java
{
"aggregations": {
"group_by_province": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "广东",
"doc_count": 6,
"load_times": {
"values": {
"200.0": 100,
"800.0": 100
}
}
},
{
"key": "黑龙江",
"doc_count": 6,
"load_times": {
"values": {
"200.0": 32.73809523809524,
"800.0": 100
}
}
}
]
}
}
}
```
这个结果告诉我们三点信息:
1. 所有请求均在800ms内完成
2. 广东地区的访问全部在200ms内完成,SLA达标率
3. 黑龙江地区的访问200ms内完成的占比为32.738%,SLA达标率32.738%
percentile_ranks度量提供了与percentiles 相同的信息,但它以不同方式呈现,在系统监控中,percentile_ranks比percentiles要更常用一些。
#### percentiles优化
TDigest算法特性:
1. 1%或99%的数据要50%要准确,一头一尾的数据更容易表现出问题所在,人们也更关注。
2. 数值集合较小时,结果非常准确。
3. bucket里面数据量特别大时,开始进行估算,误差与数据分布和数据量相关。
用很多节点来执行百分比的计算,近似估计,有误差,节点越多,越精准
compression参数可以控制内存与准确度之间的比值,默认值是100,值越大,内存消耗越多,结果也更。
一个node使用32字节内存,默认情况下需要消耗100 * 20 * 32 = 64KB用于TDigest计算。
这个了解一下就行。
### 小结
本篇对聚合查询的一些原理做了简单的介绍,近似算法的使用场景较多,系统数据监控是其中一个案例,可以了解一下。
专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
相关文章