记一次ES查询优化
问题引入
在电商场景下,数据量非常大,所以我们做了分库分表,这是应对海量数据的正常拆分操作。看似十分美好,完美解决了单机的性能瓶颈问题,但是同时也引入的新的问题,那就是查询数据非常不方便
。
比如按照用户 id 去分库分表,查询用户维度的数据是没有问题的,比如一个用户的历史订单数,这样的数据一定是在同一个数据库中。但是如果现在要求查询一个店铺下所有的订单数据呢?
一个店铺下所有的订单已经被按照用户 id 拆分到了不同的数据库中,如果此时想查询“X的店铺”下所有的用户订单,就需要把所以的分库都遍历一次,然后再聚合其结果。
毫无疑问,这样的聚合效率是极其低下的。怎么办呢?对数据做异构存储!
比如,把所有数据库的订单表数据全部同步到 ElasticSearch 中,利用 ES 适合海量数据高效全文搜索的特性,来实现查询功能。看起来又是十分美好,但是新的组件一定会带来新的问题。
慢查询问题
我们的商户需要在订单列表查询用户的订单数据,在搜索的时候可以按照用户的姓名条件去直接查询。
但是姓名重复的概率是很大的,查询的时候如果匹配到多个相同的姓名,需要按照姓名加脱敏手机号聚合的方式来展示一个汇总数据,对应的记录后面需要展示用户当前待发货的订单数。
如下:
用户 | 订单数 |
---|---|
X 137****0001 | 4 |
X 137****0002 | 3 |
X 137****0003 | 0 |
搜索到三个用户,每个用户后面对应有代发货的订单数。
很明显,这是一个聚合查询,需要按照用户来分组,统计对应状态的订单数。业务刚上线不久的时候这个查询是很快的,但是随着数据量的增长,查询已经到了1s 多,优化已经迫在眉睫。
按照这个条件查询,商铺+姓名+订单待发货状态,其实每次匹配到的数据量也不多。通常也就 100 条以内。为什么会很慢呢?
问题复现
在 ES 社区,有位用户抛出来同样的疑问。因为真实业务数据的敏感性,我们就以这个 case 为例。
他提问:当使用query查询时,耗时332ms,查询到的记录数"hits":{"total":13775},速度是很快的。
"query": {
"bool": {
"must": [
{
"range": {
"date": {
"gte": "2016-08-01 00:00:00",
"lte": "2016-08-30 23:59:59",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
},
{
"term":{
"sc": "0"
}
},
{
"terms": {
"channel": [
".4399sj.com"//频道列表
]
}
}
]
}
}
但是当加上聚合分组时,速度就会变得很慢!查询耗时:37217ms即37s,查询结果记录数:hits为13755。
curl -XPOST 'http://localhost:9200/index1/type1/_search' -d '{
"query": {
"bool": {
"must": [
{
"range": {
"date": {
"gte": "2016-08-01 00:00:00",
"lte": "2016-08-30 23:59:59",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
},
{
"term":{
"sc": "0"
}
},
{
"terms": {
"channel": [
".4399sj.com" // 频道列表
]
}
}
]
}
},
"aggs": {
"new_to_url": {
"terms": {
"field": "to_url"
},
"aggs": {
"new_from_url": {
"terms": {
"field": "from_url"
},
"aggs": {
"sum_m__visit": {
"sum": {
"field": "m_visit"
}
}
}
}
}
}
}
}'
其中mappings设计如下:
{
"index1": {
"mappings": {
"type1": {
"_ttl": {
"enabled": true,
"default": 63072000000
},
"properties": {
"channel": {
"type": "string",
"index": "not_analyzed"
},
"date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"from_url": {
"type": "string",
"index": "not_analyzed"
},
"m_visit": {
"type": "long",
"index": "no",
"doc_values": true
},
"sc": {
"type": "string",
"index": "not_analyzed"
},
"to_url": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
}
提问:为何query查询数据量少,做二维分组聚合时,耗时要如此之久?elasticsearch版本是2.3,16个shard,8台机器,1个副本。是查询语句写错了,还是?有何优化可以使聚合性能提升。
解决思路
在每一层terms aggregation内部加一个 "execution_hint": "map"
"aggs": {
"new_to_url": {
"terms": {
"field": "to_url",
"execution_hint": "map"
},
"aggs": {
"new_from_url": {
"terms": {
"field": "from_url",
"execution_hint": "map"
},
"aggs": {
"sum_m__visit": {
"sum": {
"field": "m_visit"
}
}
}
}
}
}
}
具体回答内容
Terms aggregation默认的计算方式并非直观感觉上的先查询,然后在查询结果上直接做聚合。
ES假定用户需要聚合的数据集是海量的,如果将查询结果全部读取回来放到内存里计算,内存消耗会非常大。因此ES利用了一种叫做global ordinals的数据结构来对聚合的字段来做bucket分配,这个ordinals用有序的数值来代表字段里唯一的一个字符串,因此为每个ordinals值分配一个bucket就等同于为每个唯一的term分配了bucket。之后遍历查询结果的时候,可以将结果映射到各个bucket里,就可以很快的统计出每个bucket里的文档数了。
这种计算方式主要开销在构建global ordinals和分配bucket上,如果索引包含的原始文档非常多,查询结果包含的文档也很多,那么默认的这种计算方式是内存消耗最小,速度最快的。
如果指定execution_hint:map则会更改聚合执行的方式,这种方式不需要构造global ordinals,而是直接将查询结果拿回来在内存里构造一个map来计算,因此在查询结果集很小的情况下会显著的比global ordinals快。
要注意的是这中间有一个平衡点,当结果集大到一定程度的时候,map的内存开销带来的代价可能就抵消了构造global ordinals的开销,从而比global ordinals更慢,所以需要根据实际情况测试对比一下才能找好平衡点。
总结
在ES 聚合查询匹配到数据量不多的时候,可以采用 "execution_hint": "map" 的执行方式,会大大加快查询速度。