一、前言
本文分享了在上班中关于 ElasticSearch 的一些经常使用倡导。和其余更倾向手册化更器重论断的文章不同,本文将必定水平上论述局部倡导面前的原理及经常使用姿态参考,防止流于外表,只知其但是不知其所以然。如有不当的中央,欢迎斧正!
二、查问相关
充沛应用缓存
ES 层面的缓存成功,封装在 IndicesRequestCache 类中。缓存的 Key 是整个客户端恳求,缓存内容为单个分片的查问结果。关键作用是对聚合的缓存,查问结果中被缓存的内容关键包括:Aggregations(聚合结果)、Hits.total、以及 Suggestions等。
并非一切的分片级查问都会被缓存。只要客户端查问恳求中size=0的状况下才会被缓存。其余不被缓存的条件还包括 Scroll、设置了 Profile 属性,查问类型不是 QUERY_THEN_FETCH,以及设置了 requestCache=false 等。另外一些存在不确定性的查问例如:范围查问带有 Now,由于它是毫秒级别的,缓存上去没无心义,相似的还有在脚本查问中经常使用了 Math.random() 等函数的查问也不会启动缓存。
当有新的 Segment 写入到分片后,缓存会失效,由于之前的缓存结果曾经不可代表整个分片的查问结果。所以分片每次Refresh之后,缓存会被肃清。
Lucene 层面的缓存成功,封装在 LRUQueryCache 类中,自动开启。缓存的是某个 Filter 子查问语句在一个 Segment 上的查问结果。
并非一切的 Filter 查问都会被缓存。关于体积较小的 Segment 不会建设 Query Cache,由于他们很快会被兼并。Segment 的 Doc 数量须要大于 10000,并且占整个分片的 3% 以上才会走 Cache 战略(参考:缓存)。
当 Segment 兼并的时刻,被删除的 Segment 其关联 Cache 会失效。
01.经常使用过滤器高低文(Filter)代替查问高低文(Query)。
正例:
// 创立BoolQueryBuilderBoolQueryBuilder boolQuery = QueryBuilders.boolQuery();// 构建过滤器高低文boolQuery.filter(QueryBuilders.termQuery("field", "value"));
反例:
// 创立BoolQueryBuilderBoolQueryBuilder boolQuery = QueryBuilders.boolQuery();// 构建查问高低文boolQuery.must(QueryBuilders.termQuery("field1", "value1"));
02. 只关注聚合结果而不关注文档细节时,Size设置为0应用分片查问缓存。
参考示例:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();// 参与聚合查问sourceBuilder.aggregation(AggregationBuilders.terms("term_agg").field("field").subAggregation(AggregationBuilders.sum("sum_agg").field("field")));// 设置size为0,只前往聚合结果而不前往文档sourceBuilder.size(0);
日期字段上经常使用 Now,普通来说不会被缓存,由于婚配到的时期不时在变动。因此, 可以从业务的角度来思索能否必定要用 Now,尽量经常使用相对时期值,不须要解析相对时期表白式且应用 Query Cache 能够提高查问效率。例如时期范围查问中经常使用 Now/h,经常使用小时级别的单位,可以让缓存在 1 小时内都或许被访问到。
正例:
聚合查问
04.防止多层聚合嵌套查问。
聚合查问的两边结果和最终结果都会在内存中启动,嵌套过多,会造成内存耗尽。
如:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();// 创立关键查问sourceBuilder.query(QueryBuilders.matchAllQuery());// 创立第一层聚合TermsAggregationBuilder termAggBuilder1 = AggregationBuilders.terms("term_agg1").field("field_name1");// 创立第二层聚合TermsAggregationBuilder termAggBuilder2 = AggregationBuilders.terms("term_agg2").field("field_name2");termAggBuilder1.subAggregation(termAggBuilder2);// 创立第三层聚合TermsAggregationBuilder termAggBuilder3 = AggregationBuilders.terms("term_agg3").field("field_name3");termAggBuilder2.subAggregation(termAggBuilder3);sourceBuilder.aggregation(termAggBuilder1);
05. 嵌套查问倡导经常使用 Composite 聚合查问方式。
关于经常出现的 Group by A,B,C 这种多维度 Groupby 查问,嵌套聚合的性能很差,嵌套聚合被设计为在每个桶内启动目的计算,关于平铺的 Group by 来说有存在很多冗余计算,另内在 Meta 字段上的序列化反序列化代价也十分大,这类 Group by 交流为 Composite 可以将查问速度优化 2 倍左右。
正例:
// 创立Composite Aggregation构建器CompositeAggregationBuilder compositeAggregationBuilder = AggregationBuilders.composite("group_by_A_B_C").sources(AggregationBuilders.terms("group_by_A").field("fieldA.keyword"),AggregationBuilders.terms("group_by_B").field("fieldB.keyword"),AggregationBuilders.terms("group_by_C").field("fieldC.keyword"));// 创立查问条件SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).aggregation(compositeAggregationBuilder).size(0);
反例:
// 创立Terms Aggregation构建器,依照字段A分组TermsAggregationBuilder termsAggregationA = AggregationBuilders.terms("group_by_A").field("fieldA.keyword");// 在字段A的基础上创立Terms Aggregation构建器,依照字段B分组TermsAggregationBuilder termsAggregationB = AggregationBuilders.terms("group_by_B").field("fieldB.keyword");// 在字段B的基础上创立Terms Aggregation构建器,依照字段C分组TermsAggregationBuilder termsAggregationC = AggregationBuilders.terms("group_by_C").field("fieldC.keyword");// 将字段C的聚合参与到字段B的聚合中termsAggregationB.subAggregation(termsAggregationC);// 将字段B的聚合参与到字段A的聚合中termsAggregationA.subAggregation(termsAggregationB);// 创立查问条件SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).aggregation(termsAggregationA).size(0);
06. 防止大聚合查问。
聚合查问的两边结果和最终结果都会在内存中启动,数据量太大会造成内存耗尽。
07. 高基数场景嵌套聚合查问倡导经常使用 BFS 搜查。
聚合是在 ES 内存成功的。当一个聚合操作蕴含了嵌套的聚合操作时,每个嵌套的聚合操作都会经常使用上一级聚合操作中构建出的桶作为输入,而后依据自己的聚合条件再启动桶的进一步分组。这样关于每一层嵌套,都会再次灵活构建一组新的聚合桶。在高基数场景,嵌套聚合操作会造成聚合桶数量随着嵌套层数的参与指数级增长,最终结果就是占用 ES 少量内存,从而造成 OOM 的状况出现。
自动状况下,ES 经常使用 DFS(深度优先)搜查。深度优先先构建完整的树,而后修剪无用节点。BFS(广度优先)先口头第一层聚合,再继续下一层聚合之前会先做修剪。
在聚合查问中,经常使用广度优先算法须要在每个桶级别上缓存文档数据,而后在剪枝阶段后向子聚合重放这些文档。因此,广度优先算法的内存消耗取决于每个桶中的文档数量。关于许多聚合查问,每个桶中的文档数量都十分大,聚合或许会有数千或数十万个文档。
但是,有少量桶但每个桶中文档数量相对较少的状况下,经常使用广度优先算法能愈加高效地应用内存资源,而且可以让咱们构建愈加复杂的聚合查问。只管或许会发生少量的桶,但每个桶中只要相对较少的文档,因此经常使用广度优先搜查算法可以愈加浪费内存。
参考示例:
searchSourceBuilder.aggregation(AggregationBuilders.terms("brandIds").collectMode(Aggregator.SubAggCollectionMode.BREADTH_FIRST).field("brandId").size(2000).order(BucketOrder.key(true)));
08.防止对 text 字段类型经常使用聚合查问。
09. 不倡导经常使用bucket_sort启动聚合深分页查问。
ES 的高 Cardinality 聚合查问十分消耗内存,超越百万基数的聚合很容易造成节点内存不够用以致 OOM。
bucket_sort经常使用桶排序算法,性能疑问关键是由于它须要在内存中缓存一切的文档和聚合桶,而后能力启动排序和分页,随着文档数量增多和分页深度参与,性能会逐突变差,有深分页疑问。由于桶排序须要对一切文档启动全体排序,所以它的时期复杂度是 O(NlogN),其中 N 是文档总数。
目前Elasticsearch允许聚合分页(滚动聚合)的目前只要复合聚合(Composite Aggregation)一种。滚动的方式相似于SearchAfter。聚合时指定一个复合键,而后每个分片都依照这个复合键启动排序和聚合,不须要在内存中缓存一切文档和桶,而是可以每次前往一页的数据。
反例:经常使用 bucket_sort 深分页 RT 到达 5000ms+
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();boolQuery.filter(QueryBuilders.termQuery(EsNewApplyDocumentFields.IS_DEL, 0));TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("spuIdAgg").field("spuId").order(BucketOrder.key(false)).size(pageNum*pageSize);termsAggregationBuilder.subAggregation(new BucketSortPipelineAggregationBuilder("spuBucket",null).from((pageNum-1)*pageSize).size(pageSize));searchSourceBuilder.query(boolQuery).aggregation(termsAggregationBuilder).size(0);
正例:经常使用 Composite Aggregation 优化后深分页查问:423ms
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();boolQuery.filter(QueryBuilders.termQuery(EsNewApplyDocumentFields.IS_DEL, 0));CompositeAggregationBuilder compositeBuilder = new CompositeAggregationBuilder("spuIdAgg",Collections.singletonList(new TermsValuesSourceBuilder("spuId").field("spuId").order("desc"))).aggregateAfter(ImmutableMap.of("spuId", "603030")).size(20);searchSourceBuilder.query(boolQuery).aggregation(compositeBuilder).aggregation(totalAgg).size(0);
分页
10. 防止经常使用 from+size 方式。
ES 中深度翻页排序的破费会随着分页的深度而成倍增长,分页搜查不会独自“Cache”。每次分页的恳求都是一次性从新搜查的环节,而不是从第一次性搜查的结果中失掉。假设数据特意大对 CPU 和内存的消耗会十分渺小甚至会造成 OOM。
11. 防止高实时性&大结果集场景经常使用 Scroll 方式。
基于快照的高低文。实时性高的业务场景不倡导经常使用。大结果集场景将生成少量Scroll 高低文,或许造成内存消耗过大,倡导经常使用 SearcheAfter 方式。
思索:关于 Scroll 和 SearchAfter 的选择怎样看?两者区分实用于哪种场景?SearchAfter 可以齐全代替 Scroll 吗?
Scroll 保养一份索引段的快照,实用于非实时滚动遍历全量数据查问,但少量Contexts 占用堆内存的代价较高;7.10 引入的新特性 Search After + PIT,查问实质是应用前向页面的一组排序之检索婚配下一页,从而保障数据分歧性;8.10 官方文档明白指出不再倡导经常使用 Scroll API 启动深分页。假设分页检索超越 Top10000+ 介绍经常使用 PIT + Search After。
12. SearchAfter 分页/Scroll ID/ 遍历索引中的数据指定 Sort 字段要保障惟一性,否则会形成分页/遍历数据不完整或重复。
13. 倡导指定业务字段排序,不要驳回自动打分排序。
ES 自动经常使用“_score”字段按评分排序。如在经常使用Scroll API失掉数据时,假设没有不凡的排序需求,介绍经常使用"sort":"_doc"让 ES 按索引顺序前往命中文档,可以节俭排序开支。要素如下:
14. Scroll 查问确保显式调用 clearScroll() 方法肃清 Scroll ID。
否则会造成 ES 在过时时期前不可监禁 Scroll 结果集占用的内存资源,同时也会占用自动 3000 个 Scroll 查问的容量,造成 too many scroll ID 的查问拒绝报错,影响业务。
其余
15. 留意 Must 和 Should 同时出如今语句里的时刻,Should 会失效;留意 Must 和 Should 同时出如今同一层级的 bool 查问时,Should 查问会失效。
正例:
{"query":{ "bool":{"must":[{"bool":{"must":[{"term":{"status.keyword":"1"} }]}},{"bool":{"should":[{"term":{"tag.keyword":"1"} } ] }}]}}}
反例:
{"query":{"bool":{"must":[{"term":{"status.keyword":"1"}}],"should":[{"term":{"tag.keyword":"1"}}]}}}
16. 防止查问 indexName-*。
由于 Elasticsearch 中的索引称号是全局可见的,可以经过查问一切索引的方式来枚举某个集群中的一切索引称号。可以经过在 Elasticsearch 性能文件中设置action.destructive_requires_name参数来制止查问indexName-*。
17. 脚本经常使用 Stored 方式,防止经常使用 Inline 方式。
关于固定结构的 Script,经常使用 Stored 方式,把脚本经过 Kibana 存入 ES 集群,降低重复编译脚本带来的性能损耗。
正例:
第1步:经过stored方式,建script模版:POST _script/activity_discount_price{"script":{"lang":"painless","source":"doc.xxx.value * params.discount"}}第2步:调用script脚本模版:cal_activity_discountGET index/_search{"script_fields": {"discount_price": {"script": {"id": "activity_discount_price","params":{"discount": 0.8}}}}}
反例:
//间接inline方式,恳求中传入脚本:GET index/_search{"script_fields": {"activity_discount_price": {"script": {"source":"doc.xxx.value * 0.8"}}}}
18. 防止经常使用 _all 字段。
_all 字段蕴含了一切的索引字段,假设没有失掉原始文档数据的需求,可经过设置Includes、Excludes 属性来定义放入 _source 的字段。_all 自动将写入的字段拼接成一个大的字符串,并对该字段启动分词,用于允许整个 Doc 的全文检索,“_all”字段在查问时占用更多的 CPU,同时占用更多的磁盘存储空间,默以为“false”,不倡导开启该字段和经常使用。
19. 倡导用 Get 查问交流 Search 查问。
GET/MGET 间接依据文档 ID 从正排索引中失掉内容。Search 不指定_id,依据关键词从倒排索引中失掉内容。
20. 防止启动多索引查问。
反例:
GET /index1,index2,index3/_search{"query": {"match_all": {}}}
21. 防止单次召回少量数据,倡导经常使用 _source_includes 和 _source_excludes 参数来蕴含或扫除字段。
大型文档尤其有用,局部字段检索可以节俭网络开支。
参考示例:
// 创立SearchSourceBuilder,并设置查问条件SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();sourceBuilder.query(QueryBuilders.matchAllQuery());// 设置要蕴含的字段String[] includes = {"field1", "field2"};sourceBuilder.fetchSource(includes, Strings.EMPTY_ARRAY);// 设置要扫除的字段String[] excludes = {"field3"};sourceBuilder.fetchSource(Strings.EMPTY_ARRAY, excludes);
22. 防止经常使用 Wildcard 启动中缀含糊查问。
ES 官方文档并不介绍经常使用 Wildcard 来启动中缀含糊的查问,要素在于 ES 外部为了减速这种带有通配符查问,会将输入的字符串 Pattern 构建成一个 DFA (Deterministic Finite Automaton),而带有通配符的 Pattern 结构进去的 DFA 或许会很复杂,开支很大。
倡导经常使用 ES 官方在 7.9 推出的一种专门用来处置含糊查问慢的 Wildcard 字段类型。与 Text 字段相比,它不会将文本看作是标点符号宰割的单词汇合;与 Keyword 字段比,它在中缀搜查场景下具备无可比拟的查问速度,且对输入没有大小限度,这是 Keyword 类型不可相比的。
23. 防止经常使用 Scripting。
Painless 脚本言语语法相对便捷,灵敏度高,安保性高,性能高(相关于其余脚本,但是其性能比 DSL 要低)。不实用于非复杂业务,普通 DSL 能处置大局部的疑问,处置不了的用相似 Painless 等脚本言语。关键性能影响如下:单次查问或更新耗时参与,脚本的口头时期相比于其余查问和更新操作或许会更长,由于在口头脚本之前须要对其启动词法剖析、语法剖析和代码编译等预处置上班。
24. 防止经常使用脚本查问(Script Query)计算灵活字段,倡导在索引时计算并在文档中参与该字段。
例如,咱们有一个蕴含少量用户信息的索引,咱们须要查问以"1234"扫尾的一切用户。运转一个脚本查问如"source":“doc[‘num’].value.startsWith(‘1234’)”。这个查问十分消耗资源,索引时思索参与“num_prefix”的keyword字段,而后查问"name_prefix":“1234”。
三、写入相关
25. 防止代码中或手工间接 Refresh 操作。
正当设置索引 Settings/Refresh_Interval 时期,经过系统成功Refresh举措。
26. 防止单个文档过大。
鉴于自动 http.max_content_length 设置为 100MB,Elasticsearch 将拒绝索引任何大于该值的文档。
27. 写入数据不指定 Doc_ID,让 ES 智能生成。
索引具备显式 ID 的文档时 ES 在写入环节中会多一步判别的环节,即审核具备相反ID 的文档能否曾经存在于相反的分片中,随着索引增长而变得愈加低廉。
28. 正当经常使用 Bulk API 批量写。
大数据量写入时可以经常使用 Bulk,但是恳求照应的耗时会参与,即使衔接断开,ES 集群外部也依然在口头。高速少量量数据写入时,或许形成集群短时期内照应缓慢甚至假死的的状况。
29. 脚本刷少量数据,写入前调大 Refresh Interval,不倡导将正本分片为 0,待写入成功后再调回来。
正本分片从新参与节点会触发副分片复原 Recovery 流程,假设是大分片会影响集群性能。
四、索引创立
分片
30. 正本分片数大于等于 1。
高可用性保障。参与正本数可以必定水平上提高搜查性能;但会降低写入性能,倡导每个主分片对应 1-2 个正本分片即可。
31. 官方倡导单分片限度最大数据条数不超越 2^32 - 1。
32. 索引主分片数量不要设置过大。
ES 创立好索引后,普通状况下不再灵活调整主分片数量。
每个分片实质上就是一个 Lucene 索引,因此会消耗相应的文件句柄、内存和 CPU 资源。
ES 经常使用词频统计来计算相关性,当然这些统计也会调配到各个分片上,假设在少量分片上只保养了很少的数据,则将造成最终的文档相关性较差。
普通来说,咱们遵照一些准则:
33. 单个分片数据量不要超越 50GB。
单个索引的规模控制在 1TB 以内,单个分片大小控制在 30 ~ 50GB ,Docs 数控制在 10 亿内,假设超越倡导滚动。
Mapping设计
34. 防止经常使用字段灵活映射性能,指定详细字段类型,子类型(若须要),分词器(特意有场景须要)。
35. 关于不须要分词的字符串字段,经常使用 Keyword 类型而不是 Text 类型。
36. ES 自动字段个数最大 1000,倡导不要超越 100。
单个 Doc 在建设索引时的运算复杂度,最大的要素不在于 Doc 的字节数或许说某个字段 Value 的长度,而是字段的数量。例如在满负载的写入压力测试中,Mapping 相反的状况下,一个有 10 个字段,200 字节的 Doc, 经过参与某些字段 Value 的长度到 500 字节,写入 ES 时速度降低很少,而假设字段数参与到 20,即使整个 Doc 字节数没参与多少,写入速度也会降低一倍。
37. 关于不索引字段,Index 属性设置为 False。
在上方的例子中,Title 字段的 Index 属性被设置为 False,示意该字段不会被蕴含在索引中。而 Content 字段的 Index 属性默以为 True,示意该字段会被蕴含在索引中。须要留意的是,即使 Index 属性被设置为 False,该字段依然会被保留在文档中,可以被查问和聚合。
参考示例:
{"mappings": {"properties": {"title": {"type": "text","index": false},"content": {"type": "text"}}}}
38. 防止经常使用 Nested 或 Parent/Child。
Nested Query慢,Parent/Child Query 更慢,针对 1 个 Document,每一个 Nested Field 都会生成一个独立的 Document,这将使 Doc 数量剧增,影响查问效率尤其是 JOIN 的效率。因此能在 Mapping 设计阶段搞定的(大宽表设计或驳回比拟 Smart 的数据结构),就不要用父子相关的 Mapping。假设必定要经常使用 Nested Fields,保障 Nested Fields字段不能过多,目前ES自动限度是Index.mapping.nested_fields.limit=50。不倡导经常使用 Nested,那有什么方式来处置 ES 不可 JOIN 的疑问?关键有几种成功方式:
39. 防止经常使用 Norms。
Norm 是索引评分因子,假设不用按评分对文档启动排序,设置为“False”。
参考示例:
"title": {"type": "string","norms": {"enabled": false}}
关于Text类型的字段而言,自动开启了Norms,而Keyword类型的字段则自动封锁了Norms。
开启 Norms 之后,每篇文档的每个字段须要一个字节存储 Norms。关于 Text 类型的字段而言是自动开启 Norms 的,因此关于不须要评分的 Text 类型的字段,可以禁用 Norms。
40. 对不须要启动聚合/排序的字段禁用列存 Doc_Values。
面向列的方式存储,关键用户排序、聚合和访问脚本中字段值等数据访问场景。简直一切字段类型都允许 Doc_Values,值得留意的是,须要剖析的字符串字段除外。自动状况下,一切允许 Doc_Values 的字段都启用了这特性能。假设确定不须要对字段启动排序或聚合,或从脚本访问字段值,则可以禁用此性能以缩小冗余存储老本。
Keyword和Numeric的选择
Keyword 类型的关键缺陷是在聚合的时刻须要构建全局序数,而数值类型则不用。但低基数字段通常会命中少量结果集,例如性别,经常使用 Numeric 则会在构建 Bitset 上发生很高的代价。
综上所述,在类型选择上可以参考上方的准则:
41. 关于极少经常使用 Range 查问的数字值,经常使用 Keyword 类型。
并非一切数值数据都应映射为数值字段数据类型。Elasticsearch 为查问优化数字字段,例如 Integer or long。假设不须要范围查找,关于 Term 查问而言,Keyword 比 Integer 性能更好。
42. 关于有频繁且较为固定的 Range 查问字段,参与 Keyword 类型 Pre-Indexing字段。
假设对字段的大少数查问在一个固定的范围上运转 Range 聚合,那么可以参与一个 Keyword 类型的字段,经过将范围“Pre-Indexing”到索引中并经常使用 Terms 聚合来放慢聚合速度。
43. 对须要聚合查问的高基数 Keyword 字段启用 Eager_Global_Ordinals。
参考:eager_global_ordinals
序号(Ordinals)用于在 Keyword 字段上运转 Terms 聚合。序号用一个自增数值示意,ES 保养这个自增数字与实践值的映射相关,并为每一数值调配一个 Bucket,映射相关是 Segment 级别的。
但是做聚合操作时往往须要联合多个 Segment 的结果,而每个 Segment 的 Ordinals 映射相关是不分歧的,所以 ES 会在每个分片上创立全局序号(Global Ordinals)结构 ,一个全局一致的映射,保养全局的 Ordinal 与每个 Segment 的 Ordinal 的映射相关。
自动状况下,Global Ordinals 自动是延时构建,在第一次性查问如 Term Aggregation 经常使用到时才会构建。由于 ES 不知道哪些字段将用于 Terms 聚合,哪些字段不会。关于基数大的字段,构建老本较大。
启用eager_global_ordinals后,Elasticsearch 会在分片构建时预先计算出全局词项表,以便在查问时能够更快地加载和经常使用。但启用eager_global_ordinals后,每次口头 Refresh 操作都会构建 Global Ordinals,相当于把搜查时刻破费的构建老本转移到写入时,所以会对写入效率有必定的影响,可以配合增大索引的 Refresh Interval 来经常使用。
参考示例:
PUT index{"mappings": {"type": {"properties": {"foo": {"type": "keyword","eager_global_ordinals" : true}}}}}
五、总结
最近十年,Elasticsearch 曾经成为了最受欢迎的开源检索引擎,并积淀了少量的通常案例及优化总结。在本文中,咱们尽或许片面地总结了 Elasticsearch 日常开发中的一些关键通常&避坑指南,宿愿能为大家提供 Elasticsearch 经常使用上的一些自创点,欢迎探讨!
参考文章:
1.《Elasticsearch 源码解析与优化实战》
2.《Elasticsearch威望指南》
3.
4.
5.
6.