Elasticsearch调优篇 05 - Elasticsearch 搜索层面最全优化

1、尽量少的字段

elasticsearch 的搜索引擎严重依赖于底层的 filesystem cache,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 indx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。

比如说,你的 es 节点有3台机器,每台机器 64G,总内存 64 * 3。每台机器给 es jvm heap 是32G,那么剩下来留给 filesystem cache 的就是每台机器才32g,总共集群里给 filesystem cache的就是 32 * 3 = 96gb 内存。如果此时你整个磁盘上索引数据文件,在3台机器上一共占用了1T的磁盘容量,es 数据量是 1t。filesystem cache 的内存才100g,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。
  归根结底,你要让 es 性能好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半 。比如说,你一共要在 es 中存储 1T 的数据,那么你的多台机器留给 filesystem cache 的内存加起来综合至少要到 512G,至少半数的情况下,搜索是走内存的,性能一般可以到几十毫秒。

如果最佳的情况下,我们自己的生产环境实践经验,最好是用 es 就存少量的数据,就是你要用来搜索的那些索引,内存留给 filesystem cache 的就 100G,那么你就控制在 100gb 以内,相当于你的数据几乎全部走内存来搜索,性能非常之高,一般可以在10ms以内。
  所以在 es 里就存储必须用来搜索的数据,比如说你现在有一份数据,有100个字段,其实用来搜索的只有10个字段,建议是将10个字段的数据存入 es,剩下90个字段的数据,可以放 mysql,hadoop,hbase 等都可以。 这样的话,es数据量很少,10个字段的数据,都可以放内存,就用来搜索,搜索出来一些id,通过 id 去 mysql,hbase 里面去查询明细的数据。

2、document 模型设计

document模型设计是非常重要的,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。

es能支持的操作就是那么多,不要考虑用es做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。

另外对于一些太复杂的操作,比如 join,nested,parent-child 搜索都要尽量避免,性能都很差的。

在搜索/查询的时候,要执行一些业务强相关的特别复杂的操作:

  1. 在写入数据的时候,就设计好模型,加几个字段,把处理好的数据写入加的字段里面
  2. 自己用java程序封装,es能做的,用es来做,搜索出来的数据,在java程序里面去做,比如说我们基于es用java封装一些特别复杂的操作

3、预索引数据

例如电商平台,有很多 range aggregation 操作,但有很多其范围是可以确定下来的,那么在索引的时候就增加相关的字段。

例如,要做一个按照价格区间进行分类统计的,但是这个价格区间基本上是固定的,形如 0 ~ 100,100 ~ 200, 200 ~ 500 ......,那么我们再索引的时候针对价格就可以明确确定下来该商品到底是属于哪个价格区间的,打上对应的标签,这样再聚合和查询的时候可以直接通过该字段来快速的进行定位,提升搜索性能。

4、日期查询

这里针对日期查询单独提出来,是因为日期查询写的不规范时会导致性能急剧下降。

尽量不要使用 now 这种内置函数来执行日期查询,因为默认 now 是到毫秒级的,是无法缓存结果,尽量使用一个阶段范围。

比如 now/m,就是到分钟级,那么如果一分钟内,都执行这个查询,是可以取用查询缓存的

5、禁用动态类型映射

默认的动态 string 类型映射会将 string 类型的 field 同时映射为 text 类型以及 keyword 类型,这会浪费磁盘空间,因为我们不一定两种都需要。

通常来说,id field 这种字段可能只需要 keyword 映射,而 body field 可能只需要 text field。

映射一个content,content: text,content.内置字段: keyword

可以通过手动设置 mappings 映射来避免字符串类型的field被自动映射为 text 和 keyword:

PUT index
{
 "mappings": {
   "type": {
     "dynamic_templates": [
       {
         "strings": {
           "match_mapping_type": "string",
           "mapping": {
              "type":"keyword"
           }
         }
       }
     ]
    }
  }
}

6、局部预热

如果我们重启了es,或者重建了一个新的集群,那么 filesystem cache 就会变为空壳了,就需要不断的查询才能重新让 filesystem cache 热起来。

我们可以先手动对一些数据进行查询。比如说,你本来一个查询,要用户点击以后才执行,才能从磁盘加载到 filesystem cache 里,第一次执行要 1s,以后每次就几十毫秒。

你完全可以起一个程序执行那个查询,预热,数据就加载到 filesystem cahce,程序执行的时候是 1s,以后用户真的来看的时候就才几十毫秒。

7、适当增大副本

一般副本量大会增加搜索的吞吐量,但是也会降低索引性能,所以在实际场景中会根据不同的业务场景来设置合理的副本数量,一般我们设置副本数量常为  2。

如果更新不是很频繁而查询量比较大的时候,可以适当增大副本数量,例如增大的 3、4、5 等。

如果更新比较频繁,而查询的 qps 不高的话,可以将副本数量设置为 1。

8、用性能更好的硬件设备

可以使用性能更好的 SSD 磁盘来替代 机械磁盘,尤其是这种随机读取的性能按照厂商给到的数据会比机械磁盘快 100 倍。

9、避免稀疏数据

lucene的内核结构,跟稠密的数据配合起来,性能会更好。

举个例子,比如有100个document,每个document都有20个field,20个field都有值,这就是稠密的数据。

但是如果100个document,每个document的field都不一样,有的document有2个field,有的 document 有 50个field,这就是稀疏的数据。

原因就是,lucene在内部会通过 doc id 来唯一标识一个document,这个 doc id 是integer类型,范围在 0到索引中含有的document数量之间。

这些 doc id 是用来在 lucene 内部的 api 之间进行通信的,比如说,对一个 term 用一个 match query 来进行搜索,就会产生一个 doc ids 集合,然后这些 doc ids 会用来获取对应的norm值,以用来计算每个doc的相关度分数。

而根据 doc id 查找 norm 的过程,是通过每个document的每个field保留一个字节来进行的一个算法,这个过程叫做norm查找,norm就是每个document的每个field保留的一个字节。

对于每个doc id对应的那个norm值,可以通过读取es一个内置索引,叫做doc_id的索引中的一个字节来获取。这个过程是性能很高的,而且可以帮助lucene快速的定位到每个document的norm值,但是同时这样的话document本身就不需要存储这一个字节的norm值了。

在实际运行过程中,这就意味着,如果一个索引有100个document,对于每个field,就需要100个字节来存储norm值,即使100个document中只有10个document含有某个field,但是对那个field来说,还是要100个字节来存储norm值。这就会对存储产生更大的开销,存储空间被浪费的一个问题,而且也会影响读写性能。

下面有一些避免稀疏数据的办法:

(1)避免将没有任何关联性的数据写入同一个索引

我们必须避免将结构完全不一样的数据写入同一个索引中,因为结构完全不一样的数据,field是完全不一样的,会导致index数据非常稀疏。

最好将这种数据写入不同的索引中,如果这种索引数据量比较少,那么可以考虑给其很少的primary shard,比如1个,避免资源浪费。

(2)对document的结构进行规范化/标准化

即使我们真的要将不同类型的document写入相同的索引中,还是有办法可以避免稀疏性,那就是对不同类型的document进行标准化。

比如说,如果所有的document都有一个时间戳field,不过有的叫做timestamp,有的叫做creation_date,那么可以将不同document的这个field重命名为相同的字段,尽量让documment的结构相同。

另外一个,就是比如有的document有一个字段,叫做goods_type,但是有的document没有这个字段,此时可以对没有这个字段的document,补充一个goods_type给一个默认值,比如default。

(3)避免使用多个types存储不一样结构的document

很多人会很喜欢在一个index中放很多个types来存储不同类型的数据。

但是其实不是这样的,最好不要这么干,如果你在一个index中有多个type,但是这些type的数据结构不太一样,那么这些type实际上底层都是写到这个索引中的,还是会导致稀疏性。

如果多个type的结构不太一样,最好放入不同的索引中,不要写入一个索引中。

(4)对稀疏的field禁用norms和doc_values

如果上面的步骤都没法做,那么只能对那种稀疏的field,禁止 norms 和 doc_values 字段,因为这两个字段的存储机制类似,都是每个field有一个全量的存储,对存储浪费很大。

如果一个field不需要考虑其相关度分数,那么可以禁用norms,如果不需要对一个field进行排序或者聚合,那么可以禁用doc_values字段。 .