检索锦囊 1:尽可能的使用缓存
对于精准匹配的查询,不关注评分结果,只关注数据是否满足检索需求。
可以考虑用 filter “包裹一层”,如处理时间范围检索,Elasticsearch 能缓存部分结果。但,要说明的是更换时间窗口,换不同时间段检索,原有缓存不起作用。
关于 filter 的缓存效果,官方文档如是说:
“Frequently used filters will be cached automatically by Elasticsearch, to speed up performance.”
即:“频繁的使用过滤器会有自动缓存的“效果”,以提高性能。”
举例如下,“/m”的本质使检索不是具体到某秒的精确值,而是扩展到分钟。
代码语言:javascript复制GET kibana_sample_data_flights/_search
{
"profile": true,
"query": {
"constant_score": {
"filter": {
"range": {
"timestamp": {
"gte": "now-1h/m",
"lte": "now/m"
}
}
}
}
}
}
加“profile;true”后,看到的检索结果如下。
官方文档解释的不够准确,这里结合 profile:true的结果,解释一下。
- 起始时间:now-1h/m 为当前时间戳减去1分钟所在分钟的 00 秒的时刻;
- 结束时间:now为当前时间戳所在分钟的 59秒的时刻。
更为确切的说,时间跨度为 2 分钟了。
https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-search-speed.html#_search_rounded_dates
检索锦囊 2:文件系统缓存预热
如果 Elasticsearch 节点重启,文件系统缓存通常是空的。
操作系统需要一些时间才能将索引的热数据加载到内存中,以便快速检索。
我们可以通过如下的设置来告知操作系统哪些文件应立即加载到内存中。
代码语言:javascript复制PUT /my-index-000001
{
"settings": {
"index.store.preload": ["nvd", "dvd"]
}
}
支持的文件扩展名及释义如下所示。
扩展名 | 英文释义 | 详细释义 |
---|---|---|
.nvd | Norms data | 查询时使用的各种归一化因子数据 |
.dvd | doc values Per-Document Values | 用于聚合、排序的正排索引文件 |
.tim | terms dictionaries | 单词词典 |
.doc | Frequencies | 倒排列表,包含:文档列表及词频 |
.dim | points | Point values |
注意:index.store.preload
设置为“*”通常没有意义,因为所有文件加载到内存中通常没有用。
而更好的选择可能是将其设置为 ["nvd", "dvd", "tim", "doc", "dim"],也就是包含 Norms data、docvalues、单词字典、倒排索引列表等,这些是搜索和聚合中最重要的部分。
https://lucene.apache.org/core/8_0_0/core/org/apache/lucene/codecs/lucene80/package-summary.html
https://www.shenyanchao.cn/blog/2018/12/04/lucene-index-files/
检索锦囊 3:使用预过滤分片执行检索
大背景是:对于时序数据,可以使用 ilm 索引生命周期管理,ilm 索引生命周期管理的前置条件是冷热集群架构。也就是:我们有 rollover 滚动索引机制,可以设置索引在热节点、温节点、冷节点的生存时长。
进一步说,索引不是普通的索引,索引有了时间戳的后缀。这样的好处是:当我们需要检索数据的时候,是可以通过别名等方式物理缩小索引范围区间的。
举个例子:如下图所示,weibo_2527 实际指的是上面的“20190225,20190226,20190227”三个索引,如果只检索这三天的数据,相比于全量数据,weibo_2527别名意味着极大的降低了检索数据样本空间。
但,索引层面还足够大,看上面截图我们知道,每个索引下面又有 N 多分片。能否继续优化,下沉到分片层面进行快速锁定分片执行高效检索呢?
这就用到了 7.4 版本才有的新特性:prefilter shard。使用 prefilter shard,Elasticsearch 能够根据我们的请求确定需要查询的分片。
预处理分片的本质如张超老师所讲:“对于 Date 类型的 Range 查询,在对分片执行搜索之前,先检查一下分片是否包括被查询的数据范围,如果查询的范围与分片持有的数据没有交集,就跳过该分片。”本质一句话:有助于避免查询到达不必要的分片。
默认情况下,此预过滤分片阶段在以下情况下执行:
- 条件一:该请求针对超过 128 个分片。
- 条件二:请求针对一个或多个只读索引。
- 条件三:基于创建过索引的字段进行排序。
这点,参考张超老师验证且给出的结论——“pre-filter 最主要的作用不是降低查询延迟,而是 pre-filter 阶段可以不占用 search theadpool(检索线程池),相比于不加这个参数,会减少了检索线程池的占用情况。“
具体使用方式如下所示。
相当于在原来检索的基础上加了:pre_filter_shard_size
参数。
POST kibana_sample_data_flights_20220727/_search?pre_filter_shard_size=1000
{
"query": {
"range": {
"timestamp": {
"gte": "2022-07-01",
"lte":"2022-07-31"
}
}
}
}
https://easyice.cn/archives/350
检索锦囊 4:合并只读分片
我们发现,除了借助 rollover (ilm 索引生命周期管理)将冷数据索引标记为只读之外,我们还可以强制合并(force merge)一个或多个索引的分片。与磁盘碎片整理类似,此操作在不涉及缓存时可极大地提高了查询性能。经过只读分片的合并,最大响应时间由 30 秒降到了2 秒。
代码语言:javascript复制POST /.ds-my-data-stream-2099.03.07-000001/_forcemerge?max_num_segments=1&pretty
https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html#forcemerge-api-time-based-index-ex
检索锦囊 5:新建索引时配置排序方式
在 Elasticsearch 中创建新索引时,可以配置指定每个 Shard 中的 Segments 的排序方式。
默认情况下,Lucene 不应用任何排序。index.sort.*
设置定义了应该使用哪些字段来对每个段内的文档进行排序。
举例:如下索引的定义中(篇幅原因,省略了 Mapping),指定了段内基于 timestamp 字段进降序排序。
代码语言:javascript复制PUT kibana_sample_data_flights_20220727
{
"settings": {
"index": {
"sort.field": "timestamp",
"sort.order": "desc",
"number_of_shards":10,
"number_of_replicas":0
}
},
"mappings": {
"properties": {
....
}
}
}
}
同时,有些业务场景,用户不真正关心跟踪命中的总数,并且只希望查询的Top N 个结果。这时候可以基于“提前终止查询”来快速获取检索结果。那么如何做到提前终止查询呢?
我们都知道:Elasticsearch 默认会在 query 阶段查询每个文档,基于给定条件排序后,然后在 fetch 阶段取满足排序条件的结果数据并返回给客户端。
这就意味着分段数越多,排序自然也会越慢,查询的时间越久。
提前终止查询的前置条件是:写入的时候,已经基于字段排序了。假设我们最终期望返回 Top 10 数据,每个分段内各自取 Top 10 然后再整体排序得到 Top 10,不就可以了吗?
打个不恰当的类比,世界杯需要决出前 10 名,那么:亚洲取前10,欧洲取前10,非洲取前10,美洲取前10,整体排序不就是世界足球 Top10了吗?
原理明白了,问题就转化为:如何提前终止呢?
实际上并没有特殊参数控制,因为我们前置设置了:"sort.field": "timestamp", Elasticsearch 会根据 size 大小每个分段取 Top 10 数据后自动终止。
`"track_total_hits": false`的目的是不显示文档总数,这也能降低检索时间,提高检索效率。
执行操作如下所示。
代码语言:javascript复制POST _reindex
{
"source": {
"index": "kibana_sample_data_flights"
},
"dest": {
"index": "kibana_sample_data_flights_20220727"
}
}
POST kibana_sample_data_flights_20220727/_search
{
"size": 10,
"sort": [
{
"timestamp": "desc"
}
],
"track_total_hits": false
}
https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-index-sorting.html#early-terminate
6、小结
- 在查询中使用近似日期(now/m)而非精准日期格式,以利用节点查询缓存。
- 时序数据场景,可以将冷数据显示设置为只读来强制执行预过滤分片机制。
- 必要时对索引执行强制合并(force merge),确保“零碎”的分片合并为一个大分段,以提高检索效率。
- 关闭超过一年 的索引(具体结合业务需求,如果还在用就不能关),以减少打开的分片数量,避免将资源浪费到无用的数据上。
- 借助索引生命周期管理 ILM 管理时序数据,实现索引数据的 rollover(滚动),设置只读、强制合并及索引关闭任务,而不是手动执行这种操作。
在“每月存储大约 新增 500 万 数据,每天后端接收 2万次查询请求”的实战业务场景下,如上的优化效果卓著。看对比效果图:
检索对比 | 平均响应时间(秒) | 最长响应时间(秒) | 最短响应时间(秒) |
---|---|---|---|
优化前 | 4.619 | 29.863 | 0.365 |
优化后 | 0.059 | 1.806 | 0.010 |
如上文章翻译自:https://medium.com/teads-engineering/practical-elasticsearch-performance-tuning-on-aws-8c08066e598c
我做了细节展开解读。
你的业务开发或运维中如何做的检索优化呢?欢迎留言讨论交流。