乔叔教 Elastic - 28 - Elasticsearch 的优化技巧 (2/4) - Searching 搜寻效能优化

Elasticsearch 的优化技巧 系列文章索引


前言

这系列的文章主要的目的在於当我们开始使用 Elastic Stack 时,我们如何优化 Elasticsearch 的使用方式,包含 Indexing, Searching, Disk Usage, Shard Optimization 等四个主题,这篇以是 Searching 为主的介绍。

进入此章节的先备知识

  • 已经有在使用 Elasticsearch,并且了解 Elasticsearch 的基本原理与操作方式。

此章节的重点学习

  • Searching 的效能优化的各种技巧与建议。

Searching 搜寻效能优化

这篇文章主要提供 Searching 的效能优化的各种技巧与建议:

  • 与相关性计分无关的 Query,都使用 Filter 来处理
  • 确保 Filesystem 有足够的 memory cache
  • 使用更快速的储存硬体
  • Document modeled
  • 搜寻的栏位愈少愈好
  • 依照 Aggregation 的需求 Pre-index 资料
  • 尽量使用 keyword 来当作 identifiers 的型态
  • Scripts 是昂贵的,应该尽量少用
  • 使用日期时间当搜寻条件时,可以取整点,增加 Cache 利用率
  • 将 filter 条件切割来提高 Cache 利用率
  • 将不会再写入的 Index 进行 Force-merge
  • 将常会使用到 Terms Aggregations 的栏位,设定成 Eager Global Ordinals
  • 预热 filesystem cache
  • 使用 index sorting 的设定,来加速 conjunctions 的搜寻
  • 使用 preference 控制 searching request 的 routing 来增加 cache 使用率
  • Replica 数量愈多不见得对搜寻愈有帮助
  • 管理好使用 Elasticsearch 的方式,不要让使用者拥有太大的弹性
  • 使用 Profile API 来优化 Search Request
  • 在 query 或 aggregation 处理需求量较高的环境中,安排特定的 Coordinating Node

以下会分别针对这些优化项目进行说明。

与相关性计分无关的 Query,都使用 Filter 来处理

因为 Filter 的处理不需要去计算 相关性计分,所以他的处理会比较快,也因此他的结果是适合被 cache 的,Elasticsearch 也就只会 cache filter 的结果,不会 cache 其他有相关性计分的 query,所以结论就是:预设请使用 filter,只有和相关性计分有关的查询,才使用 query。

确保 Filesystem 有足够的 memory cache

和 Indexing 时的建议一样,Elasticsearch 使用时,由於使用 Lucene 进行许多 Segment files 的处理,会需要用到大量 file system 的 memory buffer,因此官方的配置建议上,会建议 JVM Heap size v.s OS filesystem 各配置 50% 的记忆体大小,因此请确保 Filesystem 拥有足够的记忆体来替较常被使用的资料进行快取。

使用更快速的储存硬体

Search 的处理有可能是 I/O bound 或是 CPU bound,如果你的 Search 是属於 I/O bound,在官方的建议配置上,会建议基本上要使用 SSD 等级的硬碟来当成 Elasticsearch 的储存硬体规格,而且使用 SSD 的配置,会让整体的 C/P 值会较高。

若是你的 Search 是属於 CPU bound,则应该将 node 配置较高等级的 CPU。

若是因资料量太大而有成本的考量,应该进一步再使用 Index Lifecycle Management 将 Indexing 完成的资料、或是较旧的资料,转移到较便宜的磁碟硬体状置上。

Document modeled

尽量将你的 Document 在 Indexing 进入 Elasticsearch 时,就规划成是 针对 Searching 优化的结构

例如:避免使用 joinnested 的资料型态配合 nested query 会让查询速度慢好几倍、parent-child 会让查询速度慢好几百倍、fuzzyregex…等查询的效能也是非常的慢,所以若是能事先去正规划、enrich raw log、透过 ngrambigramshingle …等各种 Analysis 套用在 multiple fields 中,能让 searching 阶段的处理尽量简化,并且能达到同样的效果,这样搜寻速度会有非常明显的改善。

搜寻的栏位愈少愈好

如果有使用 query_stringmulti_match 这类查询来同时查询多个栏位时,优化的方式是使用 copy_toindexing 时期就将这些会同时查询的栏位合并到一个栏位中,并且 searching 时直接针对这个栏位进行搜寻,减少搜寻时的栏位数量,也会优化查询的效率。

依照 Aggregation 的需求 Pre-index 资料

如果你的搜寻应用上常会针对一个栏位进行 range aggregation,而且都是一些固定的区间,例如:

PUT index/_doc/1
{
  "designation": "spoon",
  "price": 13
}

GET index/_search
{
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 10 },
          { "from": 10, "to": 100 },
          { "from": 100 }
        ]
      }
    }
  }
}

针对这种例子, pre-index 指的就可以将资料在 indexing 时,多增加一个栏位并使用 keyword 来存放这个分类的结果,如下:

PUT index
{
  "mappings": {
    "properties": {
      "price_range": {
        "type": "keyword"
      }
    }
  }
}

PUT index/_doc/1
{
  "designation": "spoon",
  "price": 13,
  "price_range": "10-100"
}

之後在使用时,就可以直接用这个栏位来进行 aggregation。

GET index/_search
{
  "aggs": {
    "price_ranges": {
      "terms": {
        "field": "price_range"
      }
    }
  }
}

这样也能有效的提升 aggregation 的效率。

尽量使用 keyword 来当作 identifiers 的型态

不是所有的数值型态的资料都应该使用 numeric 的 data type。

Elasticsearch 针对 numeric 型态的栏位特别着重优化 range query 或 aggregation,而针对 keyword 栏位,会特别优化 term 或其他 term-level 相关的查询。

因此如果你的 identifier 这类的资料是数值的型态,而且不需要进行 range query,那你应该考虑把他定义成 keyword 的型态。

如果你不确定会如何使用的话,就用 multi-fieldkeywordnumeric 都定义起来,也就是用空间换时间的方式,至少在 searching 阶段能使用最合适的方式来进行最有效率的搜寻。

Scripts 是昂贵的,应该尽量少用

不论是 scripts query 或是 scripted fields ,因为使用到 script 时,就没办法使用 Elasticsearch 的 index structure 或是相关的优化机制,所以如果 scripts 使用到的这些规则,若是能在 indexing 时期就先把资料预先算好并准备好,这样也能有效的增加搜寻的效率。

使用日期时间当搜寻条件时,可以取整点,增加 Cache 利用率

这边的原理,是因为 filter 的 cache 机制会依照 filter 的查询条件来当成 cache key,一但 filter 的条件改变,这个 cache 自然就不会被 hit,以下面为例:

PUT index/_doc/1
{
  "my_date": "2016-05-11T16:30:55.328Z"
}

GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "my_date": {
            "gte": "now-1h",
            "lte": "now"
          }
        }
      }
    }
  }
}

如果我们现在的时间是 16:31:29 ,我们进行了一次 search,过了一秒之後, 16:31:29 这时再进行一次 search,如果第一次有产生 cache 的话,其实第二次的 filter 是无法利用到第一次的 cache 的,因为时间已经不同了,反之若是使用 rounded date,也就是直接 /m 取到分钟为颗粒度的整数。

GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "my_date": {
            "gte": "now-1h/m",
            "lte": "now/m"
          }
        }
      }
    }
  }
}

以同样上述的时间,产生出来的结果就会都是 16:31:00 ,这样就能提上 cache hit rate,而这个颗粒度也就取决於应用端可以接受的情境。

将 filter 条件切割来提高 Cache 利用率

如果我们的使用情境上有许多颗粒度很细、也就是 Cache hit rate 很低的查询,例如我们总是要查询 最近一小时的资料 ,而且又想要愈即时、也就是颗粒度要很细的 filter,这个一般的查询方式可能如下:

GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "my_date": {
            "gte": "now-1h",
            "lte": "now/"
          }
        }
      }
    }
  }
}

可以想像这个 cache hit rate 应该会极低,所以我们可以把这段时间切成三块,让其中一大块的 cache rate 提高,并让没办法 cache 的部份切成小块而舍弃他的 cache 使用率:

GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "bool": {
          "should": [
            {
              "range": {
                "my_date": {
                  "gte": "now-1h",
                  "lte": "now-1h/m"
                }
              }
            },
            {
              "range": {
                "my_date": {
                  "gt": "now-1h/m",
                  "lt": "now/m"
                }
              }
            },
            {
              "range": {
                "my_date": {
                  "gte": "now/m",
                  "lte": "now"
                }
              }
            }
          ]
        }
      }
    }
  }
}

这样三块分别是:

  • 一个很精确的开始时间 ~ 开始时间之後的分钟整点时间: now-1h ~ now-1h/m
  • 开始时间之後的分钟整点时间 ~ 最接近目前时间的分钟整点时间:now-1h/m ~ now/m
  • 最接近目前时间的分钟整点时间 ~ 目前的时间:now/m ~ now

这种方式有利有弊,好处是让中间那段有用分钟整点来切齐的 cache hit rate 提高,但缺点是将 filter 切成三份,还是会增加一些 overhead,这部份就要依实际的使用情况来评估与调整了。

将不会再写入的 Index 进行 Force-merge

如果是不会再写入资料的 Index,例如 time-based indices 如果是已经 rotated,那麽不会再被写入的 Index 应该要进行 segment files 的 force-merge,并且强制 merge 成只剩下一个 segment file,这样也会提升搜寻的效率。

一般情况请不要针对一个还会持续写入的 index 进行 force-merge ,这样有可能会让 performance 更差。

将常会使用到 Terms Aggregations 的栏位,设定成 Eager Global Ordinals

Global ordinals 是执行 terms aggregation 时会使用到的资料结果,预设是 lazy loading,因为 Elasticsearch 不知道你会针对哪些 keyword 栏位执行 terms aggregation。因此如果你有某个栏位明确的会频繁执行 terms aggregation,可以进行以下的设定,将 eager_global_oridnals 设成 true

PUT index
{
  "mappings": {
    "properties": {
      "foo": {
        "type": "keyword",
        "eager_global_ordinals": true
      }
    }
  }
}

预热 filesystem cache

可以使用 index.store.preload 来告诉 OS 在 shard 起动时,要先将哪些 index 预先载入进 memory cache 中,另外载入的设定有以下几种:

  • nvd :norms
  • dvd :doc values
  • tim :terms dictionaries
  • doc :postings lists
  • dim :points

例如:

PUT /my-index-000001
{
  "settings": {
    "index.store.preload": ["nvd", "dvd"]
  }
}

注意: 若是预先载入太多的 indices 而导致 filesystem cache 不够大来处理这些 indices 的话,searching 的效率是会下降的。

使用 index sorting 的设定,来加速 conjunctions 的搜寻

conjunctions 搜寻指的像是: a AND b AND c ... 这样的搜寻,由於 conjunctions query 时的处理方式,是会将这些查询条件,一个个去比对哪些文件"不符合条件",若是一遇到不符合,就会跳过这个条件的文件,进行下一个条件的找寻。

宣告 Index sorting 的目的,主要是可以让 符合 与 不符合 的文件先排在一起,不论是 ascdesc 都没关系,只要让他们先排在一起,一但使用 conjunctions 搜寻时,有某一个文件的条件不符合时,就能以较快的速度直接跳过这些不符合的文件,进入下一个条件的比较。

这边要注意,这种小技巧只有对於资料内容差异较小的会较有效,也就是重覆的资料愈多愈有效。

使用 preference 控制 searching request 的 routing 来增加 cache 使用率

我们有 filesystem cache, request cache, query cache 等这些 cache 能优化 searching 的效能,不过这些都是 Node level 的 cache,如果我们有多份的 replica,同样的 search request 若是导到不台,自然就没办法利用到另一台的 cache。

所以这边的优化方式,是依照使用的情境,例如同一个使用者搜寻资料时的查询条件应该会比较接近、或是相同地区的查询条件会比较接近…等,我们就可以使用 preference 设定为 user id, session id, 甚至是 region id,来让同样的使用者或地区,能导到相同的 node,以增加 cache hit rate。

Replica 数量愈多不见得对搜寻愈有帮助

replica 的数量还是要参考 primary shard 与 node 的数量来一并考量,如果 node 数量 4 个,primary shard 数量也是 4 个,并且 replica 是 0,这时 1 个 node 放 1 个 shard 的资料,这时 filesystem cache 的机制是最好的,如果 replica 设成 1,每一份 shard 都会有一份额外的 replica ,但这时 replica shard 也会占用到 filesystem cache。

不过 replica 的数量另外一个最重要的目的是 availability,所以这会需要一并考虑,官方有个简单的公式可做参考:

max(max_failures, ceil(num_nodes / num_primaries) - 1)
  • max_failures: 代表 availability,也就是允许一口气有几个 node 坏掉时资料还是需要保留完整性。
  • num_nodes: cluster node 数量。
  • num_primaries: cluster 中,所有 primary shard 的数量。

管理好使用 Elasticsearch 的方式,不要让使用者拥有太大的弹性

先前上课时最喜欢举一个例子,如果大家使用过 Kibana ,可能会有过类似的经验,在看 dashboard 时,调整时间时一不小心时间拉太长,拉到近1年,整个查询就要等很久很久,最惨的是有可能把 cluster 的资源耗尽,又或着是我们提供给使用者的是 search box 让使用者自己输入 query_string 的字串,结果使用者输了个超级复杂的查询条件…

以上的例子都是我们开放让使用者产生一些我们无法事先管理的 searching request,而这些 requests 是非常耗资源的,而甚至会影响到其他正常使用的状况,这部份就会是应该要有良好的控管,确认我们适合提供的查询方式,例如:

  • 使用 alias + filter,限制只能查询最近一段时间的资料,这可搭配 RBAC 来绑定在使用者的帐号上。
  • 使用较有局限的 UI 设计,让使用者能产生的 searching request 都是在我们的掌握之中。

使用 Profile API 来优化 Search Request

可以使用 Profile API 或是 Kibana Dev tools 的 Search Profiler,针对 search 底下运作的方式进行分析,也就可以针对某一个执行时间较长的查询进行剖析或是调整。

Query Profiler Visualization

在 query 或 aggregation 处理需求量较高的环境中,安排特定的 Coordinating Node

Corrdinator 在处理 aggregation 或是包含较复杂 sorting 处理的 query 时,会需要使用到较大量的 memory,因此将 Node 的身份进行有效的管理与分工,让处理大量搜寻请求的任务,由专门的 Coordinating Node,有独立的 memory 与系统资源,让 Data node 在执行 searching 时,减少系统资源被其他处理占用的情况。

也可以考虑将 Ingest Node 等专门的任务也都与 data node 中独立出来,以确保处理 search request 的 node 的系统资源不会被其他处理占用。

参考资料


查看最新 Elasticsearch 或是 Elastic Stack 教育训练资讯: https://training.onedoggo.com
欢迎追踪我的 FB 粉丝页: 乔叔 - Elastic Stack 技术交流
不论是技术分享的文章、公开线上分享、或是实体课程资讯,都会在粉丝页通知大家哦!

此系列文章已整理成书
乔叔带你上手 Elastic Stack:Elasticsearch 的最佳实践与最佳化技巧
书中包含许多的修正、补充,也依照 Elastic 新版本的异动做出不少修改。
有兴趣的读者欢迎支持! 天珑书局连结
乔叔带你上手 Elastic Stack:Elasticsearch 的最佳实践与最佳化技巧


<<:  认识 React Hooks 之二

>>:  [day-28] U-net Conclusion

DAY27-EXCEL统计分析:相关分析实例

让我们用上一个范例来做相关分析的练习 有一位学生想知道每天读书时间与考试成绩之间的线性相关程度,所以...

微聊 Promise 试做一回

昨天说到 Promise 的好处多多,今天来说一下 Promise 是该怎麽用呢?其实使用 Prom...

sed - 5 Replace command

前篇回顾 sed - 简介 读取编辑文字档的好工具 sed - 2 Pattern sed - 3 ...

[Day 28] Gitea - 如何自签凭证与Nginx注意

Gitea Docker版本 绑定自签凭证by Nginx 当你的Gitea需要绑定SSL时有个快速...