Elasticsearch调优实践

转载自:腾讯技术工程 微信公众号html

  背景node

Elasticsearch(ES)做为NOSQL+搜索引擎的有机结合体,不只有近实时的查询能力,还具备强大的聚合分析能力。所以在全文检索、日志分析、监控系统、数据分析等领域ES均有普遍应用。而完整的Elastic Stack体系(Elasticsearch、Logstash、Kibana、Beats),更是提供了数据采集、清洗、存储、可视化的整套解决方案。 linux

本文基于ES 5.6.4,从性能和稳定性两方面,从linux参数调优、ES节点配置和ES使用方式三个角度入手,介绍ES调优的基本方案。固然,ES的调优毫不能一律而论,须要根据实际业务场景作适当的取舍和调整,文中的疏漏之处也随时欢迎批评指正。算法

 

性能调优数据库

一 Linux参数调优json

 

1. 关闭交换分区,防止内存置换下降性能。 将/etc/fstab 文件中包含swap的行注释掉缓存

  1. sed -i '/swap/s/^/#/' /etc/fstab
  2. swapoff -a

 

2. 磁盘挂载选项微信

  • noatime:禁止记录访问时间戳,提升文件系统读写性能
  • data=writeback: 不记录data journal,提升文件系统写入性能
  • barrier=0:barrier保证journal先于data刷到磁盘,上面关闭了journal,这里的barrier也就不必开启了
  • nobh:关闭buffer_head,防止内核打断大块数据的IO操做
  1. mount -o noatime,data=writeback,barrier=0,nobh /dev/sda /es_data

 

3. 对于SSD磁盘,采用电梯调度算法,由于SSD提供了更智能的请求调度算法,不须要内核去作多余的调整 (仅供参考)session

  1. echo noop > /sys/block/sda/queue/scheduler

 

二 ES节点配置数据结构

 

conf/elasticsearch.yml文件:

 1. 适当增大写入buffer和bulk队列长度,提升写入性能和稳定性

 

  1. indices.memory.index_buffer_size: 15%
  2. thread_pool.bulk.queue_size: 1024

 

2. 计算disk使用量时,不考虑正在搬迁的shard

在规模比较大的集群中,能够防止新建shard时扫描全部shard的元数据,提高shard分配速度。

  1. cluster.routing.allocation.disk.include_relocations: false

 

三 ES使用方式

 

1. 控制字段的存储选项

ES底层使用Lucene存储数据,主要包括行存(StoreFiled)、列存(DocValues)和倒排索引(InvertIndex)三部分。 大多数使用场景中,没有必要同时存储这三个部分,能够经过下面的参数来作适当调整:

  • StoreFiled: 行存,其中占比最大的是source字段,它控制doc原始数据的存储。在写入数据时,ES把doc原始数据的整个json结构体当作一个string,存储为source字段。查询时,能够经过source字段拿到当初写入时的整个json结构体。 因此,若是没有取出整个原始json结构体的需求,能够经过下面的命令,在mapping中关闭source字段或者只在source中存储部分字段,数据查询时仍可经过ES的docvaluefields获取全部字段的值。

注意:关闭source后, update, updatebyquery, reindex等接口将没法正常使用,因此有update等需求的index不能关闭source。

  1. # 关闭 _source
  2. PUT my_index
  3. {
  4.  "mappings": {
  5.    "my_type": {
  6.      "_source": {
  7.        "enabled": false
  8.      }
  9.    }
  10.  }
  11. }
  12. # _source只存储部分字段,经过includes指定要存储的字段或者经过excludes滤除不须要的字段
  13. PUT my_index
  14. {
  15.  "mappings": {
  16.    "_doc": {
  17.      "_source": {
  18.        "includes": [
  19.          "*.count",
  20.          "meta.*"
  21.        ],
  22.        "excludes": [
  23.          "meta.description",
  24.          "meta.other.*"
  25.        ]
  26.      }
  27.    }
  28.  }
  29. }
  • docvalues:控制列存。

ES主要使用列存来支持sorting, aggregations和scripts功能,对于没有上述需求的字段,能够经过下面的命令关闭docvalues,下降存储成本。

  1. PUT my_index
  2. {
  3.  "mappings": {
  4.    "my_type": {
  5.      "properties": {
  6.        "session_id": {
  7.          "type": "keyword",
  8.          "doc_values": false
  9.        }
  10.      }
  11.    }
  12.  }
  13. }
  • index:控制倒排索引。

ES默认对于全部字段都开启了倒排索引,用于查询。对于没有查询需求的字段,能够经过下面的命令关闭倒排索引。

  1. PUT my_index
  2. {
  3.  "mappings": {
  4.    "my_type": {
  5.      "properties": {
  6.        "session_id": {
  7.          "type": "keyword",
  8.          "index": false
  9.        }
  10.      }
  11.    }
  12.  }
  13. }
  • all:ES的一个特殊的字段,ES把用户写入json的全部字段值拼接成一个字符串后,作分词,而后保存倒排索引,用于支持整个json的全文检索。

这种需求适用的场景较少,能够经过下面的命令将all字段关闭,节约存储成本和cpu开销。(ES 6.0+以上的版本再也不支持_all字段,不须要设置)

  1. PUT /my_index
  2. {
  3.  "mapping": {
  4.    "my_type": {
  5.      "_all": {
  6.        "enabled": false  
  7.      }
  8.    }
  9.  }
  10. }
  • fieldnames:该字段用于exists查询,来确认某个doc里面有无一个字段存在。若没有这种需求,能够将其关闭。
  1. PUT /my_index
  2. {
  3.  "mapping": {
  4.    "my_type": {
  5.      "_field_names": {
  6.        "enabled": false  
  7.      }
  8.    }
  9.  }
  10. }

 

2. 开启最佳压缩

对于打开了上述_source字段的index,能够经过下面的命令来把lucene适用的压缩算法替换成 DEFLATE,提升数据压缩率。

  1. PUT /my_index/_settings
  2. {
  3.    "index.codec": "best_compression"
  4. }

 

3. bulk批量写入

写入数据时尽可能使用下面的bulk接口批量写入,提升写入效率。每一个bulk请求的doc数量设定区间推荐为1k~1w,具体可根据业务场景选取一个适当的数量。

  1. POST _bulk
  2. { "index" : { "_index" : "test", "_type" : "type1" } }
  3. { "field1" : "value1" }
  4. { "index" : { "_index" : "test", "_type" : "type1" } }
  5. { "field1" : "value2" }

 

4. 调整translog同步策略

默认状况下,translog的持久化策略是,对于每一个写入请求都作一次flush,刷新translog数据到磁盘上。这种频繁的磁盘IO操做是严重影响写入性能的,若是能够接受必定几率的数据丢失(这种硬件故障的几率很小),能够经过下面的命令调整 translog 持久化策略为异步周期性执行,并适当调整translog的刷盘周期。

  1. PUT my_index
  2. {
  3.  "settings": {
  4.    "index": {
  5.      "translog": {
  6.        "sync_interval": "5s",
  7.        "durability": "async"
  8.      }
  9.    }
  10.  }
  11. }

 

5. 调整refresh_interval

写入Lucene的数据,并非实时可搜索的,ES必须经过refresh的过程把内存中的数据转换成Lucene的完整segment后,才能够被搜索。默认状况下,ES每一秒会refresh一次,产生一个新的segment,这样会致使产生的segment较多,从而segment merge较为频繁,系统开销较大。若是对数据的实时可见性要求较低,能够经过下面的命令提升refresh的时间间隔,下降系统开销。

  1. PUT my_index
  2. {
  3.  "settings": {
  4.    "index": {
  5.        "refresh_interval" : "30s"
  6.    }
  7.  }
  8. }

 

6. merge并发控制

ES的一个index由多个shard组成,而一个shard其实就是一个Lucene的index,它又由多个segment组成,且Lucene会不断地把一些小的segment合并成一个大的segment,这个过程被称为merge。默认值是Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2)),当节点配置的cpu核数较高时,merge占用的资源可能会偏高,影响集群的性能,能够经过下面的命令调整某个index的merge过程的并发度:

  1. PUT /my_index/_settings
  2. {
  3.    "index.merge.scheduler.max_thread_count": 2
  4. }

 

7. 写入数据不指定_id,让ES自动产生

当用户显示指定id写入数据时,ES会先发起查询来肯定index中是否已经有相同id的doc存在,如有则先删除原有doc再写入新doc。这样每次写入时,ES都会耗费必定的资源作查询。若是用户写入数据时不指定doc,ES则经过内部算法产生一个随机的id,而且保证id的惟一性,这样就能够跳过前面查询id的步骤,提升写入效率。 因此,在不须要经过id字段去重、update的使用场景中,写入不指定id能够提高写入速率。基础架构部数据库团队的测试结果显示,无id的数据写入性能可能比有_id的高出近一倍,实际损耗和具体测试场景相关。

  1. # 写入时指定_id
  2. POST _bulk
  3. { "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
  4. { "field1" : "value1" }
  5. # 写入时不指定_id
  6. POST _bulk
  7. { "index" : { "_index" : "test", "_type" : "type1" } }
  8. { "field1" : "value1" }

 

8. 使用routing

对于数据量较大的index,通常会配置多个shard来分摊压力。这种场景下,一个查询会同时搜索全部的shard,而后再将各个shard的结果合并后,返回给用户。对于高并发的小查询场景,每一个分片一般仅抓取极少许数据,此时查询过程当中的调度开销远大于实际读取数据的开销,且查询速度取决于最慢的一个分片。开启routing功能后,ES会将routing相同的数据写入到同一个分片中(也能够是多个,由index.routingpartitionsize参数控制)。若是查询时指定routing,那么ES只会查询routing指向的那个分片,可显著下降调度开销,提高查询效率。 routing的使用方式以下:

  1. # 写入
  2. PUT my_index/my_type/1?routing=user1
  3. {
  4.  "title": "This is a document"
  5. }
  6. # 查询
  7. GET my_index/_search?routing=user1,user2
  8. {
  9.  "query": {
  10.    "match": {
  11.      "title": "document"
  12.    }
  13.  }
  14. }

 

9. 为string类型的字段选取合适的存储方式

  • 存为text类型的字段(string字段默认类型为text): 作分词后存储倒排索引,支持全文检索,能够经过下面几个参数优化其存储方式:
    • norms:用于在搜索时计算该doc的_score(表明这条数据与搜索条件的相关度),若是不须要评分,能够将其关闭。
    • indexoptions:控制倒排索引中包括哪些信息(docs、freqs、positions、offsets)。对于不太注重score/highlighting的使用场景,能够设为 docs来下降内存/磁盘资源消耗。
    • fields: 用于添加子字段。对于有sort和聚合查询需求的场景,能够添加一个keyword子字段以支持这两种功能。
  1. PUT my_index
  2. {
  3.  "mappings": {
  4.    "my_type": {
  5.      "properties": {
  6.        "title": {
  7.          "type": "text",
  8.          "norms": false,
  9.          "index_options": "docs",
  10.          "fields": {
  11.            "raw": {
  12.              "type":  "keyword"
  13.            }
  14.          }
  15.        }
  16.      }
  17.    }
  18.  }
  19. }
  • 存为keyword类型的字段: 不作分词,不支持全文检索。text分词消耗CPU资源,冗余存储keyword子字段占用存储空间。若是没有全文索引需求,只是要经过整个字段作搜索,能够设置该字段的类型为keyword,提高写入速率,下降存储成本。 设置字段类型的方法有两种:一是建立一个具体的index时,指定字段的类型;二是经过建立template,控制某一类index的字段类型。
  1. # 1. 经过mapping指定 tags 字段为keyword类型
  2. PUT my_index
  3. {
  4.  "mappings": {
  5.    "my_type": {
  6.      "properties": {
  7.        "tags": {
  8.          "type":  "keyword"
  9.        }
  10.      }
  11.    }
  12.  }
  13. }
  14. # 2. 经过template,指定my_index*类的index,其全部string字段默认为keyword类型
  15. PUT _template/my_template
  16. {
  17.    "order": 0,
  18.    "template": "my_index*",
  19.    "mappings": {
  20.      "_default_": {
  21.        "dynamic_templates": [
  22.          {
  23.            "strings": {
  24.              "match_mapping_type": "string",
  25.              "mapping": {
  26.                "type": "keyword",
  27.                "ignore_above": 256
  28.              }
  29.            }
  30.          }
  31.        ]
  32.      }
  33.    },
  34.    "aliases": {}
  35.  }

 

10. 查询时,使用query-bool-filter组合取代普通query

默认状况下,ES经过必定的算法计算返回的每条数据与查询语句的相关度,并经过score字段来表征。但对于非全文索引的使用场景,用户并不care查询结果与查询条件的相关度,只是想精确的查找目标数据。此时,能够经过query-bool-filter组合来让ES不计算score,而且尽量的缓存filter的结果集,供后续包含相同filter的查询使用,提升查询效率。

  1. # 普通查询
  2. POST my_index/_search
  3. {
  4.  "query": {
  5.    "term" : { "user" : "Kimchy" }
  6.  }
  7. }
  8. # query-bool-filter 加速查询
  9. POST my_index/_search
  10. {
  11.  "query": {
  12.    "bool": {
  13.      "filter": {
  14.        "term": { "user": "Kimchy" }
  15.      }
  16.    }
  17.  }
  18. }

 

11. index按日期滚动,便于管理

写入ES的数据最好经过某种方式作分割,存入不一样的index。常见的作法是将数据按模块/功能分类,写入不一样的index,而后按照时间去滚动生成index。这样作的好处是各类数据分开管理不会混淆,也易于提升查询效率。同时index按时间滚动,数据过时时删除整个index,要比一条条删除数据或deletebyquery效率高不少,由于删除整个index是直接删除底层文件,而deletebyquery是查询-标记-删除。

举例说明,假若有[modulea,moduleb]两个模块产生的数据,那么index规划能够是这样的:一类index名称是modulea + {日期},另外一类index名称是module_b+ {日期}。对于名字中的日期,能够在写入数据时本身指定精确的日期,也能够经过ES的ingest pipeline中的index-name-processor实现(会有写入性能损耗)。

  1. # module_a 类index
  2. - 建立index:
  3. PUT module_a@2018_01_01
  4. {
  5.    "settings" : {
  6.        "index" : {
  7.            "number_of_shards" : 3,
  8.            "number_of_replicas" : 2
  9.        }
  10.    }
  11. }
  12. PUT module_a@2018_01_02
  13. {
  14.    "settings" : {
  15.        "index" : {
  16.            "number_of_shards" : 3,
  17.            "number_of_replicas" : 2
  18.        }
  19.    }
  20. }
  21. ...
  22. - 查询数据:
  23. GET module_a@*/_search
  24. #  module_b 类index
  25. - 建立index:
  26. PUT module_b@2018_01_01
  27. {
  28.    "settings" : {
  29.        "index" : {
  30.            "number_of_shards" : 3,
  31.            "number_of_replicas" : 2
  32.        }
  33.    }
  34. }
  35. PUT module_b@2018_01_02
  36. {
  37.    "settings" : {
  38.        "index" : {
  39.            "number_of_shards" : 3,
  40.            "number_of_replicas" : 2
  41.        }
  42.    }
  43. }
  44. ...
  45. - 查询数据:
  46. GET module_b@*/_search

 

12. 按需控制index的分片数和副本数

分片(shard):一个ES的index由多个shard组成,每一个shard承载index的一部分数据。

副本(replica):index也能够设定副本数(numberofreplicas),也就是同一个shard有多少个备份。对于查询压力较大的index,能够考虑提升副本数(numberofreplicas),经过多个副本均摊查询压力。

shard数量(numberofshards)设置过多或太低都会引起一些问题:shard数量过多,则批量写入/查询请求被分割为过多的子写入/查询,致使该index的写入、查询拒绝率上升;对于数据量较大的inex,当其shard数量太小时,没法充分利用节点资源,形成机器资源利用率不高 或 不均衡,影响写入/查询的效率。

对于每一个index的shard数量,能够根据数据总量、写入压力、节点数量等综合考量后设定,而后根据数据增加状态按期检测下shard数量是否合理。基础架构部数据库团队的推荐方案是:

  • 对于数据量较小(100GB如下)的index,每每写入压力查询压力相对较低,通常设置3~5个shard,numberofreplicas设置为1便可(也就是一主一从,共两副本) 。
  • 对于数据量较大(100GB以上)的index:
    • 通常把单个shard的数据量控制在(20GB~50GB)
    • 让index压力分摊至多个节点:可经过index.routing.allocation.totalshardsper_node参数,强制限定一个节点上该index的shard数量,让shard尽可能分配到不一样节点上
    • 综合考虑整个index的shard数量,若是shard数量(不包括副本)超过50个,就极可能引起拒绝率上升的问题,此时可考虑把该index拆分为多个独立的index,分摊数据量,同时配合routing使用,下降每一个查询须要访问的shard数量。

 

稳定性调优

一 Linux参数调优

 

  1. # 修改系统资源限制
  2. # 单用户能够打开的最大文件数量,能够设置为官方推荐的65536或更大些
  3. echo "* - nofile 655360" >>/etc/security/limits.conf
  4. # 单用户内存地址空间
  5. echo "* - as unlimited" >>/etc/security/limits.conf
  6. # 单用户线程数
  7. echo "* - nproc 2056474" >>/etc/security/limits.conf
  8. # 单用户文件大小
  9. echo "* - fsize unlimited" >>/etc/security/limits.conf
  10. # 单用户锁定内存
  11. echo "* - memlock unlimited" >>/etc/security/limits.conf
  12. # 单进程可使用的最大map内存区域数量
  13. echo "vm.max_map_count = 655300" >>/etc/sysctl.conf
  14. # TCP全链接队列参数设置, 这样设置的目的是防止节点数较多(好比超过100)的ES集群中,节点异常重启时全链接队列在启动瞬间打满,形成节点hang住,整个集群响应迟滞的状况
  15. echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf
  16. echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf
  17. # 下降tcp alive time,防止无效连接占用连接数
  18. echo 300 >/proc/sys/net/ipv4/tcp_keepalive_time

 

二 ES节点配置

 

1. jvm.options

-Xms和-Xmx设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给系统cache使用。

  • jvm内存建议不要低于2G,不然有可能由于内存不足致使ES没法正常启动或OOM
  • jvm建议不要超过32G,不然jvm会禁用内存对象指针压缩技术,形成内存浪费

 

2. elasticsearch.yml

  • 设置内存熔断参数,防止写入或查询压力太高致使OOM,具体数值可根据使用场景调整。 indices.breaker.total.limit: 30% indices.breaker.request.limit: 6% indices.breaker.fielddata.limit: 3%

 

  • 调小查询使用的cache,避免cache占用过多的jvm内存,具体数值可根据使用场景调整。 indices.queries.cache.count: 500 indices.queries.cache.size: 5%

 

  • 单机多节点时,主从shard分配以ip为依据,分配到不一样的机器上,避免单机挂掉致使数据丢失。 cluster.routing.allocation.awareness.attributes: ip node.attr.ip: 1.1.1.1

 

三 ES使用方式

 

1. 节点数较多的集群,增长专有master,提高集群稳定性

ES集群的元信息管理、index的增删操做、节点的加入剔除等集群管理的任务都是由master节点来负责的,master节点按期将最新的集群状态广播至各个节点。因此,master的稳定性对于集群总体的稳定性是相当重要的。当集群的节点数量较大时(好比超过30个节点),集群的管理工做会变得复杂不少。此时应该建立专有master节点,这些节点只负责集群管理,不存储数据,不承担数据读写压力;其余节点则仅负责数据读写,不负责集群管理的工做。

这样把集群管理和数据的写入/查询分离,互不影响,防止因读写压力过大形成集群总体不稳定。 将专有master节点和数据节点的分离,须要修改ES的配置文件,而后滚动重启各个节点。

  1. # 专有master节点的配置文件(conf/elasticsearch.yml)增长以下属性:
  2. node.master: true
  3. node.data: false
  4. node.ingest: false
  5. # 数据节点的配置文件增长以下属性(与上面的属性相反):
  6. node.master: false
  7. node.data: true
  8. node.ingest: true

 

2. 控制index、shard总数量

上面提到,ES的元信息由master节点管理,按期同步给各个节点,也就是每一个节点都会存储一份。这个元信息主要存储在clusterstate中,如全部node元信息(indices、节点各类统计参数)、全部index/shard的元信息(mapping, location, size)、元数据ingest等。

ES在建立新分片时,要根据现有的分片分布状况指定分片分配策略,从而使各个节点上的分片数基本一致,此过程当中就须要深刻遍历clusterstate。当集群中的index/shard过多时,clusterstate结构会变得过于复杂,致使遍历clusterstate效率低下,集群响应迟滞。基础架构部数据库团队曾经在一个20个节点的集群里,建立了4w+个shard,致使新建一个index须要60s+才能完成。 当index/shard数量过多时,能够考虑从如下几方面改进:

  • 下降数据量较小的index的shard数量
  • 把一些有关联的index合并成一个index
  • 数据按某个维度作拆分,写入多个集群

 

3. Segment Memory优化

前面提到,ES底层采用Lucene作存储,而Lucene的一个index又由若干segment组成,每一个segment都会创建本身的倒排索引用于数据查询。Lucene为了加速查询,为每一个segment的倒排作了一层前缀索引,这个索引在Lucene4.0之后采用的数据结构是FST (Finite State Transducer)。Lucene加载segment的时候将其全量装载到内存中,加快查询速度。这部份内存被称为SegmentMemory, 常驻内存,占用heap,没法被GC。

前面提到,为利用JVM的对象指针压缩技术来节约内存,一般建议JVM内存分配不要超过32G。当集群的数据量过大时,SegmentMemory会吃掉大量的堆内存,而JVM内存空间又有限,此时就须要想办法下降SegmentMemory的使用量了,经常使用方法有下面几个:

  • 按期删除不使用的index
  • 对于不常访问的index,能够经过close接口将其关闭,用到时再打开
  • 经过force_merge接口强制合并segment,下降segment数量

基础架构部数据库团队在此基础上,对FST部分进行了优化,释放高达40%的Segment Memory内存空间。

相关文章
相关标签/搜索