Filter VS Query

尽可能使用过滤器上下文(Filter)替代查询上下文(Query)

  • Query:此文档与此查询子句的匹配程度如何?
  • Filter:此文档和查询子句匹配吗?

Elasticsearch 针对 Filter 查询只需要回答「是」或者「否」,不需要像 Query 查询一样计算相关性分数,同时Filter结果可以缓存。

尽量减少不需要的字段

如果 Elasticsearch 用于业务搜索服务,一些不需要用于搜索的字段最好不存到 ES 中,这样即节省空间,同时在相同的数据量下,也能提高搜索性能。
避免使用动态值作字段,动态递增的 mapping,会导致集群崩溃;同样,也需要控制字段的数量,业务中不使用的字段,就不要索引。控制索引的字段数量、mapping 深度、索引字段的类型,对于 ES 的性能优化是重中之重。
以下是 ES 关于字段数、mapping 深度的一些默认设置:

  1. index.mapping.nested_objects.limit: 10000
  2. index.mapping.total_fields.limit: 1000
  3. index.mapping.depth.limit: 20

查询优化经验

  • query_string 或 multi_match的查询字段越多, 查询越慢。可以在mapping阶段,利用copy_to属性将多字段的值索引到一个新字段,multi_match时,用新的字段查询。
  • 日期字段的查询, 尤其是用now 的查询实际上是不存在缓存的,因此, 可以从业务的角度来考虑是否一定要用now, 毕竟利用query cache 是能够大大提高查询效率的。
  • 查询结果集的大小不能随意设置成大得离谱的值, 如query.setSize不能设置成 Integer.MAX_VALUE, 因为ES内部需要建立一个数据结构来放指定大小的结果集数据。
  • 避免层级过深的聚合查询, 层级过深的aggregation , 会导致内存、CPU消耗,建议在服务层通过程序来组装业务,也可以通过pipeline的方式来优化。
  • 复用预索引数据方式来提高AGG性能:

如通过 terms aggregations 替代 range aggregations, 如要根据年龄来分组,分组目标是: 少年(14岁以下) 青年(14-28) 中年(29-50) 老年(51以上), 可以在索引的时候设置一个age_group字段,预先将数据进行分类。从而不用按age来做range aggregations, 通过age_group字段就可以了。

GC调优

老的版本中官方文档中推荐默认设置为:Concurrent-Mark and Sweep(CMS),给的理由是当时G1 还有很多 BUG。
原因是:已知JDK 8附带的HotSpot JVM的早期版本存在一些问题,当启用G1GC收集器时,这些问题可能导致索引损坏。受影响的版本早于JDK 8u40随附的HotSpot版本。来源于官方说明
实际上如果你使用的JDK8较高版本,或者JDK9+,我推荐你使用G1 GC; 因为我们目前的项目使用的就是G1 GC,运行效果良好,对Heap大对象优化尤为明显。修改jvm.options文件,将下面几行:

  1. -XX:+UseConcMarkSweepGC
  2. -XX:CMSInitiatingOccupancyFraction=75
  3. -XX:+UseCMSInitiatingOccupancyOnly

改为

  1. -XX:+UseG1GC
  2. -XX:MaxGCPauseMillis=50

其中 -XX:MaxGCPauseMillis是控制预期的最高GC时长,默认值为200ms,如果线上业务特性对于GC停顿非常敏感,可以适当设置低一些。但是 这个值如果设置过小,可能会带来比较高的cpu消耗。
G1对于集群正常运作的情况下减轻G1停顿对服务时延的影响还是很有效的,但是如果是你描述的GC导致集群卡死,那么很有可能换G1也无法根本上解决问题。 通常都是集群的数据模型或者Query需要优化。

路由优化

不带 routing 查询

在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤:

  • 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
  • 聚合:协调节点搜集到每个分片上查询结果,再将查询的结果进行排序,之后给用户返回结果。

    带 routing 查询

    查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。
    向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。

    Cache的设置及使用

  • QueryCache: ES查询的时候,使用filter查询会使用query cache, 如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度。

indices.queries.cache.size: 10%(默认),可设置成百分比,也可设置成具体值,如256mb。
当然也可以禁用查询缓存(默认是开启), 通过index.queries.cache.enabled:false设置。

  • FieldDataCache: 在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情形下很有必要,可通过indices.fielddata.cache.size:30% 或具体值10GB来设置。但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的。
  • ShardRequestCache: 查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node), 由协调节点将结果整合。 如果有需求,可以设置开启; 通过设置index.requests.cache.enable: true来开启。 不过,shard request cache只缓存hits.total, aggregations, suggestions类型的数据,并不会缓存hits的内容。也可以通过设置indices.requests.cache.size: 1%(默认)来控制缓存空间大小。

写调优

副本数置0

如果是集群首次灌入数据,可以将副本数设置为0,写入完毕再调整回去,这样副本分片只需要拷 贝,节省了索引过程。

  1. PUT /my_temp_index/_settings
  2. {
  3. "number_of_replicas": 0
  4. }
  5. # 顺便先关闭refresh, 暂时搜不到
  6. PUT /my_logs/_settings
  7. { "refresh_interval":-1 }
  8. PUT /my_logs/_settings
  9. { "refresh_interval": "1s" }

自动生成doc ID

通过Elasticsearch写入流程可以看出,如果写入doc时如果外部指定了id,则Elasticsearch会先尝试 读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作,通过自动生成doc ID可 以避免这个环节。

合理设置mappings

  • 将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。

尤其是binary(二进制)类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有什么意义。

  • 减少字段内容长度,如果原始数据的大段内容无须全部建立索引,则可以尽量减少不必要的内容。
  • 使用不同的分析器(analyzer),不同的分析器在索引过程中运算复杂度也有较大的差异

    调整_source字段

    source 字段用于存储 doc 原始数据,对于部分不需要存储的字段,可以通过 includes excludes过滤,或者将source禁用, 一般用于索引和数据分离,这样可以降低 I/O 的压力,不过实际场景中大多不 会禁用_source。

    对analyzed的字段禁用norms

    Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用:

    1. "title": {
    2. "type": "string",
    3. "norms": {
    4. "enabled": false
    5. }

    调整索引的刷新间隔

    该参数缺省是1s,强制ES每秒创建一个新segment,从而保证新写入的数据近实时的可见、可被搜索 到。比如该参数被调整为30s,降低了刷新的次数,把刷新操作消耗的系统资源释放出来给index操作使用。

    1. PUT /my_index/_settings {
    2. "index" : {
    3. "refresh_interval": "30s"
    4. }
    5. }

    这种方案以牺牲可见性的方式,提高了index操作的性能。 :::info 加载大量数据的时候可以将 refresh 次数临时关闭,即 index.refresh_interval 设置为-1,数据导入成功后再打开到正常模式,比如30s。
    在加载大量数据时候可以暂时不用 refresh 和 repliccas,index.refresh_interval 设置为-1,index.number_of_replicas 设置为0 :::

    增加index buffer size

    索引缓冲的设置可以控制多少内存分配给索引进程。这是一个全局配置,会应用于一个节点上所有不同的分片上
    增大 index buffer size,参数为 indices. memory index_buffer_size(静态参数,需要设定在 elasticsearch ym中),默认为10%

    1. indices.memory.index_buffer_size: 10%
    2. indices.memory.min_index_buffer_size: 48mb

    indices.memory.index_buffer_size 接受一个百分比或者一个表示字节大小的值。默认是10%,意味着分配给节点的总内存的10%用来做索引缓冲的大小。这个数值被分到不同的分片(shards)上。如果设置的是百分比,还可以设置 min_index_buffer_size (默认 48mb)和 max_index_buffer_size(默认没有上限)。

    translog写入优化

    目标是降低 translog写磁盘的频率,从而提高写效率,但会降低容灾能力

  • index.translog.durability 设置为 async异步

  • index.translog. sync_interval 设置需要的大小,比如120s,那么 translog会改为每120s写一次磁盘
  • index.translog.flush_threshold_size 默认为512mb,即 translog超过该大小时会触发一次fush,那么调大该大小可以避免fush的发生

    elasticsearch.yml

    ```scala

    已经索引好的文档会先存放在内存缓存中,等待被写到到段(segment)中。

    缓存满的时候会触发段刷盘(吃i/o和cpu的操作)。默认最小缓存大小为48m,不太够,最大为堆内存的10%。

    对于大量写入的场景也显得有点小

    indices.memory.index_buffer_size: 20% indices.memory.min_index_buffer_size: 96mb

设置index、merge、bulk、search的线程数和队列数

Search pool

thread_pool.search.size: 5 thread_pool.search.queue_size: 100

这个参数慎用!强制修改cpu核数,以突破写线程数限制

processors: 16

Bulk pool

thread_pool.bulk.size: 16

thread_pool.bulk.queue_size: 300

Index pool

thread_pool.index.size: 16

thread_pool.index.queue_size: 300

设置filedata cache大小

filedata cache的使用场景是一些聚合操作(包括排序),构建filedata cache是个相对昂贵的操作。所以尽量能让他保留在内存中

indices.fielddata.cache.size: 40%

设置节点之间的故障检测配置

discovery.zen.fd.ping_timeout: 120s discovery.zen.fd.ping_retries: 6 discovery.zen.fd.ping_interval: 30s

  1. 索引优化配置:
  2. ```scala
  3. PUT /_template/elk
  4. {
  5. "order": 6,
  6. "template": "logstash-*", #这里配置模板匹配的Index名称
  7. "settings": {
  8. "number_of_replicas" : 0, #副本数为0,需要查询性能高可以设置为1
  9. "number_of_shards" : 6, #分片数为6, 副本为1时可以设置成5
  10. "refresh_interval": "30s",
  11. "index.translog.durability": "async",
  12. "index.translog.sync_interval": "30s"
  13. }
  14. }

设置段合并的线程数量

curl -XPUT 'your-es-host:9200/nginx_log-2018-03-20/_settings' -d '{ 
   "index.merge.scheduler.max_thread_count" : 1
}'

段合并的计算量庞大,而且还要吃掉大量磁盘I/O。合并在后台定期操作,因为他们可能要很长时间才能完成,尤其是比较大的段
机械磁盘在并发I/O支持方面比较差,所以我们需要降低每个索引并发访问磁盘的线程数。这个设置允许max_thread_count + 2个线程同时进行磁盘操作,也就是设置为1允许三个线程

响应数据压缩功能

ES使得修改HTTP压缩非常容易,仅仅在 elasticsearch.yml文件中提供下面属性即可:

http.compression: true
http.compression_level: 1

TCP压缩使用下面属性:

transport.compress: true

锁定elasticsearch内存

在Linux/Unix系统上使用mlockall,尝试将进程地址空间锁定到RAM中,以防止任何Elasticsearch内存被换出。可以在config/elasticsearch.yml文件中增加此配置:

bootstrap.memory_lock: true

在启动项目后,通过此API验证是否配置成功

GET _nodes?filter_path=**.mlockall

如果是false,这意味着mlockall请求失败了。

内存配置

如果有一种资源是最先被耗尽的,它可能是内存。排序和聚合都很耗内存,所以有足够的堆空间来应付它们是很重要的。即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存。因为 Lucene 使用的许多数据结构是基于磁盘的格式,Elasticsearch 利用操作系统缓存能产生很大效果。
64 GB 内存的机器是非常理想的,但是 32 GB 和 16 GB 机器也是很常见的。少于8 GB 会适得其反(你最终需要很多很多的小机器),大于 64 GB 的机器也会有问题。
由于 ES 构建基于 lucene,而 lucene 设计强大之处在于 lucene 能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能。lucene 的索引文件 segements 是存储在单文件中的,并且不可变,对于 OS 来说,能够很友好地将索引文件保持在 cache 中,以便快速访问;因此,我们很有必要将一半的物理内存留给 lucene;另一半的物理内存留给 ES(JVM heap)。

内存分配

当机器内存小于 64G 时,遵循通用的原则,50% 给 ES,50% 留给 lucene。
当机器内存大于 64G 时,遵循以下原则:

  • 如果主要的使用场景是全文检索,那么建议给 ES Heap 分配 4~32G 的内存即可;其它内存留给操作系统,供 lucene 使用(segments cache),以提供更快的查询性能。
  • 如果主要的使用场景是聚合或排序,并且大多数是 numerics,dates,geo_points 以及 not_analyzed 的字符类型,建议分配给 ES Heap 分配 4~32G 的内存即可,其它内存留给操作系统,供 lucene 使用,提供快速的基于文档的聚类、排序性能。
  • 如果使用场景是聚合或排序,并且都是基于 analyzed 字符数据,这时需要更多的 heap size,建议机器上运行多 ES 实例,每个实例保持不超过 50% 的 ES heap 设置(但不超过 32 G,堆内存设置 32 G 以下时,JVM 使用对象指标压缩技巧节省空间),50% 以上留给 lucene