Skip to content
On this page

记一次ES查询优化

问题引入

在电商场景下,数据量非常大,所以我们做了分库分表,这是应对海量数据的正常拆分操作。看似十分美好,完美解决了单机的性能瓶颈问题,但是同时也引入的新的问题,那就是查询数据非常不方便

比如按照用户 id 去分库分表,查询用户维度的数据是没有问题的,比如一个用户的历史订单数,这样的数据一定是在同一个数据库中。但是如果现在要求查询一个店铺下所有的订单数据呢?

一个店铺下所有的订单已经被按照用户 id 拆分到了不同的数据库中,如果此时想查询“X的店铺”下所有的用户订单,就需要把所以的分库都遍历一次,然后再聚合其结果。

毫无疑问,这样的聚合效率是极其低下的。怎么办呢?对数据做异构存储!

比如,把所有数据库的订单表数据全部同步到 ElasticSearch 中,利用 ES 适合海量数据高效全文搜索的特性,来实现查询功能。看起来又是十分美好,但是新的组件一定会带来新的问题。

慢查询问题

我们的商户需要在订单列表查询用户的订单数据,在搜索的时候可以按照用户的姓名条件去直接查询。

但是姓名重复的概率是很大的,查询的时候如果匹配到多个相同的姓名,需要按照姓名加脱敏手机号聚合的方式来展示一个汇总数据,对应的记录后面需要展示用户当前待发货的订单数。

如下:

用户订单数
X 137****00014
X 137****00023
X 137****00030

搜索到三个用户,每个用户后面对应有代发货的订单数。

很明显,这是一个聚合查询,需要按照用户来分组,统计对应状态的订单数。业务刚上线不久的时候这个查询是很快的,但是随着数据量的增长,查询已经到了1s 多,优化已经迫在眉睫。

按照这个条件查询,商铺+姓名+订单待发货状态,其实每次匹配到的数据量也不多。通常也就 100 条以内。为什么会很慢呢?

问题复现

在 ES 社区,有位用户抛出来同样的疑问。因为真实业务数据的敏感性,我们就以这个 case 为例。

他提问:当使用query查询时,耗时332ms,查询到的记录数"hits":{"total":13775},速度是很快的。

json
    "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。

json
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设计如下:

json
{
    "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"

json
"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" 的执行方式,会大大加快查询速度。

Released under the MIT License.