一、前言
本文分享了在工作中關(guān)于 ElasticSearch 的一些使用建議。和其他更偏向手冊化更注重結(jié)論的文章不同,本文將一定程度上闡述部分建議背后的原理及使用姿勢參考,避免流于表面,只知其然而不知其所以然。如有不當(dāng)?shù)牡胤剑瑲g迎指正!
二、查詢相關(guān)
充分利用緩存
- 分片查詢緩存(Shard Request Cache)
ES 層面的緩存實(shí)現(xiàn),封裝在 IndicesRequestCache 類中。緩存的 Key 是整個客戶端請求,緩存內(nèi)容為單個分片的查詢結(jié)果。主要作用是對聚合的緩存,查詢結(jié)果中被緩存的內(nèi)容主要包括:Aggregations(聚合結(jié)果)、Hits.total、以及 Suggestions等。
并非所有的分片級查詢都會被緩存。只有客戶端查詢請求中 size=0 的情況下才會被緩存。其他不被緩存的條件還包括 Scroll、設(shè)置了 Profile 屬性,查詢類型不是 QUERY_THEN_FETCH,以及設(shè)置了 requestCache=false 等。另外一些存在不確定性的查詢例如:范圍查詢帶有 Now,由于它是毫秒級別的,緩存下來沒有意義,類似的還有在腳本查詢中使用了 Math.random() 等函數(shù)的查詢也不會進(jìn)行緩存。
當(dāng)有新的 Segment 寫入到分片后,緩存會失效,因?yàn)橹暗木彺娼Y(jié)果已經(jīng)無法代表整個分片的查詢結(jié)果。所以分片每次 Refresh 之后,緩存會被清除。
- 節(jié)點(diǎn)查詢緩存/過濾器緩存(Node Query Cache /Filter Cache)
Lucene 層面的緩存實(shí)現(xiàn),封裝在 LRUQueryCache 類中,默認(rèn)開啟。緩存的是某個 Filter 子查詢語句在一個 Segment 上的查詢結(jié)果。
并非所有的 Filter 查詢都會被緩存。對于體積較小的 Segment 不會建立 Query Cache,因?yàn)樗麄兒芸鞎缓喜ⅰegment 的 Doc 數(shù)量需要大于 10000,并且占整個分片的 3% 以上才會走 Cache 策略(參考:緩存)。
當(dāng) Segment 合并的時候,被刪除的 Segment 其關(guān)聯(lián) Cache 會失效。
01. 使用過濾器上下文(Filter)替代查詢上下文(Query)。
- Filter不會進(jìn)行打分操作,而 Must 會。
- Filter 查詢可以被緩存,從而提高查詢性能。
正例:
// 創(chuàng)建BoolQueryBuilder
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 構(gòu)建過濾器上下文
boolQuery.filter(QueryBuilders.termQuery("field", "value"));
反例:
// 創(chuàng)建BoolQueryBuilder
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 構(gòu)建查詢上下文
boolQuery.must(QueryBuilders.termQuery("field1", "value1"));
02. 只關(guān)注聚合結(jié)果而不關(guān)注文檔細(xì)節(jié)時,Size 設(shè)置為 0 利用分片查詢緩存。
參考示例:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加聚合查詢
sourceBuilder.aggregation(
AggregationBuilders.terms("term_agg").field("field")
.subAggregation(AggregationBuilders.sum("sum_agg").field("field"))
);
// 設(shè)置size為0,只返回聚合結(jié)果而不返回文檔
sourceBuilder.size(0);
03. 日期范圍查詢使用絕對時間值。
日期字段上使用 Now,一般來說不會被緩存,因?yàn)槠ヅ涞降臅r間一直在變化。因此, 可以從業(yè)務(wù)的角度來考慮是否一定要用 Now,盡量使用絕對時間值,不需要解析相對時間表達(dá)式且利用 Query Cache 能夠提高查詢效率。例如時間范圍查詢中使用 Now/h,使用小時級別的單位,可以讓緩存在 1 小時內(nèi)都可能被訪問到。
正例:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 獲取當(dāng)前日期并格式化為絕對時間值
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
String currentDate = now.format(formatter);
// 創(chuàng)建日期范圍查詢
sourceBuilder.query(QueryBuilders.rangeQuery("date_field")
.gte("2022-01-01")
.lte(currentDate));
反例:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 創(chuàng)建日期范圍查詢,使用相對時間值
sourceBuilder.query(QueryBuilders.rangeQuery("date_field")
.gte("now-7d")
.lte("now"));
聚合查詢
04.避免多層聚合嵌套查詢。
聚合查詢的中間結(jié)果和最終結(jié)果都會在內(nèi)存中進(jìn)行,嵌套過多,會導(dǎo)致內(nèi)存耗盡。
如:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 創(chuàng)建主要查詢
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 創(chuàng)建第一層聚合
TermsAggregationBuilder termAggBuilder1 = AggregationBuilders.terms("term_agg1").field("field_name1");
// 創(chuàng)建第二層聚合
TermsAggregationBuilder termAggBuilder2 = AggregationBuilders.terms("term_agg2").field("field_name2");
termAggBuilder1.subAggregation(termAggBuilder2);
// 創(chuàng)建第三層聚合
TermsAggregationBuilder termAggBuilder3 = AggregationBuilders.terms("term_agg3").field("field_name3");
termAggBuilder2.subAggregation(termAggBuilder3);
sourceBuilder.aggregation(termAggBuilder1);
05. 嵌套查詢建議使用 Composite 聚合查詢方式。
對于常見的 Group by A,B,C 這種多維度 Groupby 查詢,嵌套聚合的性能很差,嵌套聚合被設(shè)計(jì)為在每個桶內(nèi)進(jìn)行指標(biāo)計(jì)算,對于平鋪的 Group by 來說有存在很多冗余計(jì)算,另外在 Meta 字段上的序列化反序列化代價也非常大,這類 Group by 替換為 Composite 可以將查詢速度提升 2 倍左右。
正例:
// 創(chuàng)建Composite Aggregation構(gòu)建器
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")
);
// 創(chuàng)建查詢條件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
.query(QueryBuilders.matchAllQuery())
.aggregation(compositeAggregationBuilder)
.size(0);
反例:
// 創(chuàng)建Terms Aggregation構(gòu)建器,按照字段A分組
TermsAggregationBuilder termsAggregationA = AggregationBuilders.terms("group_by_A").field("fieldA.keyword");
// 在字段A的基礎(chǔ)上創(chuàng)建Terms Aggregation構(gòu)建器,按照字段B分組
TermsAggregationBuilder termsAggregationB = AggregationBuilders.terms("group_by_B").field("fieldB.keyword");
// 在字段B的基礎(chǔ)上創(chuàng)建Terms Aggregation構(gòu)建器,按照字段C分組
TermsAggregationBuilder termsAggregationC = AggregationBuilders.terms("group_by_C").field("fieldC.keyword");
// 將字段C的聚合添加到字段B的聚合中
termsAggregationB.subAggregation(termsAggregationC);
// 將字段B的聚合添加到字段A的聚合中
termsAggregationA.subAggregation(termsAggregationB);
// 創(chuàng)建查詢條件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
.query(QueryBuilders.matchAllQuery())
.aggregation(termsAggregationA)
.size(0);
06. 避免大聚合查詢。
聚合查詢的中間結(jié)果和最終結(jié)果都會在內(nèi)存中進(jìn)行,數(shù)據(jù)量太大會導(dǎo)致內(nèi)存耗盡。
07. 高基數(shù)場景嵌套聚合查詢建議使用 BFS 搜索。
聚合是在 ES 內(nèi)存完成的。當(dāng)一個聚合操作包含了嵌套的聚合操作時,每個嵌套的聚合操作都會使用上一級聚合操作中構(gòu)建出的桶作為輸入,然后根據(jù)自己的聚合條件再進(jìn)行桶的進(jìn)一步分組。這樣對于每一層嵌套,都會再次動態(tài)構(gòu)建一組新的聚合桶。在高基數(shù)場景,嵌套聚合操作會導(dǎo)致聚合桶數(shù)量隨著嵌套層數(shù)的增加指數(shù)級增長,最終結(jié)果就是占用 ES 大量內(nèi)存,從而導(dǎo)致 OOM 的情況發(fā)生。
默認(rèn)情況下,ES 使用 DFS(深度優(yōu)先)搜索。深度優(yōu)先先構(gòu)建完整的樹,然后修剪無用節(jié)點(diǎn)。BFS(廣度優(yōu)先)先執(zhí)行第一層聚合,再繼續(xù)下一層聚合之前會先做修剪。
在聚合查詢中,使用廣度優(yōu)先算法需要在每個桶級別上緩存文檔數(shù)據(jù),然后在剪枝階段后向子聚合重放這些文檔。因此,廣度優(yōu)先算法的內(nèi)存消耗取決于每個桶中的文檔數(shù)量。對于許多聚合查詢,每個桶中的文檔數(shù)量都非常大,聚合可能會有數(shù)千或數(shù)十萬個文檔。
但是,有大量桶但每個桶中文檔數(shù)量相對較少的情況下,使用廣度優(yōu)先算法能更加高效地利用內(nèi)存資源,而且可以讓我們構(gòu)建更加復(fù)雜的聚合查詢。雖然可能會產(chǎn)生大量的桶,但每個桶中只有相對較少的文檔,因此使用廣度優(yōu)先搜索算法可以更加節(jié)約內(nèi)存。
參考示例:
searchSourceBuilder.aggregation(
AggregationBuilders.terms("brandIds")
.collectMode(Aggregator.SubAggCollectionMode.BREADTH_FIRST)
.field("brandId")
.size(2000)
.order(BucketOrder.key(true))
);
08.避免對 text 字段類型使用聚合查詢。
- text 的 Fielddata 會加大對內(nèi)存的占用,如有需求使用,建議使用 Keyword。
09. 不建議使用 bucket_sort 進(jìn)行聚合深分頁查詢。
ES 的高 Cardinality 聚合查詢非常消耗內(nèi)存,超過百萬基數(shù)的聚合很容易導(dǎo)致節(jié)點(diǎn)內(nèi)存不夠用以至 OOM。
bucket_sort 使用桶排序算法,性能問題主要是由于它需要在內(nèi)存中緩存所有的文檔和聚合桶,然后才能進(jìn)行排序和分頁,隨著文檔數(shù)量增多和分頁深度增加,性能會逐漸變差,有深分頁問題。因?yàn)橥芭判蛐枰獙λ形臋n進(jìn)行整體排序,所以它的時間復(fù)雜度是 O(NlogN),其中 N 是文檔總數(shù)。
目前Elasticsearch支持聚合分頁(滾動聚合)的目前只有復(fù)合聚合(Composite Aggregation)一種。滾動的方式類似于SearchAfter。聚合時指定一個復(fù)合鍵,然后每個分片都按照這個復(fù)合鍵進(jìn)行排序和聚合,不需要在內(nèi)存中緩存所有文檔和桶,而是可以每次返回一頁的數(shù)據(jù)。
反例:使用 bucket_sort 深分頁 RT 達(dá)到 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 優(yōu)化后深分頁查詢: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 中深度翻頁排序的花費(fèi)會隨著分頁的深度而成倍增長,分頁搜索不會單獨(dú)“Cache”。每次分頁的請求都是一次重新搜索的過程,而不是從第一次搜索的結(jié)果中獲取。如果數(shù)據(jù)特別大對 CPU 和內(nèi)存的消耗會非常巨大甚至?xí)?dǎo)致 OOM。
11. 避免高實(shí)時性&大結(jié)果集場景使用 Scroll 方式。
基于快照的上下文。實(shí)時性高的業(yè)務(wù)場景不建議使用。大結(jié)果集場景將生成大量Scroll 上下文,可能導(dǎo)致內(nèi)存消耗過大,建議使用 SearcheAfter 方式。
思考:對于 Scroll 和 SearchAfter 的選用怎么看?兩者分別適用于哪種場景?SearchAfter 可以完全替代 Scroll 嗎?
Scroll 維護(hù)一份當(dāng)前索引段的快照,適用于非實(shí)時滾動遍歷全量數(shù)據(jù)查詢,但大量Contexts 占用堆內(nèi)存的代價較高;7.10 引入的新特性 Search After + PIT,查詢本質(zhì)是利用前向頁面的一組排序之檢索匹配下一頁,從而保證數(shù)據(jù)一致性;8.10 官方文檔明確指出不再建議使用 Scroll API 進(jìn)行深分頁。如果分頁檢索超過 Top10000+ 推薦使用 PIT + Search After。
12. SearchAfter 分頁/Scroll ID/ 遍歷索引中的數(shù)據(jù)指定 Sort 字段要保證唯一性,否則會造成分頁/遍歷數(shù)據(jù)不完整或重復(fù)。
13. 建議指定業(yè)務(wù)字段排序,不要采用默認(rèn)打分排序。
ES 默認(rèn)使用“_score”字段按評分排序。如在使用 Scroll API 獲取數(shù)據(jù)時,如果沒有特殊的排序需求,推薦使用"sort":"_doc"讓 ES 按索引順序返回命中文檔,可以節(jié)省排序開銷。原因如下:
- 使用非文檔 ID 排序,會導(dǎo)致每次查詢 ES 需要在每個分片記住上次返回的最后一個文檔,然后下次查詢中會對之前已經(jīng)返回的文檔進(jìn)行忽略過濾,同時在協(xié)調(diào)節(jié)點(diǎn)進(jìn)行排序操作。文檔 ID 排序則不需要上述操作。
- 對于文檔 ID 排序,ES 內(nèi)部進(jìn)行了特殊優(yōu)化,性能表現(xiàn)更優(yōu)。
14. Scroll 查詢確保顯式調(diào)用 clearScroll() 方法清除 Scroll ID。
否則會導(dǎo)致 ES 在過期時間前無法釋放 Scroll 結(jié)果集占用的內(nèi)存資源,同時也會占用默認(rèn) 3000 個 Scroll 查詢的容量,導(dǎo)致 too many scroll ID 的查詢拒絕報(bào)錯,影響業(yè)務(wù)。
其他
15. 注意 Must 和 Should 同時出現(xiàn)在語句里的時候,Should 會失效;注意 Must 和 Should 同時出現(xiàn)在同一層級的 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-*。
因?yàn)?Elasticsearch 中的索引名稱是全局可見的,可以通過查詢所有索引的方式來枚舉某個集群中的所有索引名稱。可以通過在 Elasticsearch 配置文件中設(shè)置 action.destructive_requires_name 參數(shù)來禁止查詢 indexName-*。
17. 腳本使用 Stored 方式,避免使用 Inline 方式。
對于固定結(jié)構(gòu)的 Script,使用 Stored 方式,把腳本通過 Kibana 存入 ES 集群,降低重復(fù)編譯腳本帶來的性能損耗。
正例:
第1步:通過stored方式,建script模版:
POST _script/activity_discount_price
{
"script":{
"lang":"pAInless",
"source":"doc.xxx.value * params.discount"
}
}
第2步:調(diào)用script腳本模版:cal_activity_discount
GET 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 字段包含了所有的索引字段,如果沒有獲取原始文檔數(shù)據(jù)的需求,可通過設(shè)置Includes、Excludes 屬性來定義放入 _source 的字段。_all 默認(rèn)將寫入的字段拼接成一個大的字符串,并對該字段進(jìn)行分詞,用于支持整個 Doc 的全文檢索,“_all”字段在查詢時占用更多的 CPU,同時占用更多的磁盤存儲空間,默認(rèn)為“false”,不建議開啟該字段和使用。
19. 建議用 Get 查詢替換 Search 查詢。
GET/MGET 直接根據(jù)文檔 ID 從正排索引中獲取內(nèi)容。Search 不指定_id,根據(jù)關(guān)鍵詞從倒排索引中獲取內(nèi)容。
20. 避免進(jìn)行多索引查詢。
反例:
GET /index1,index2,index3/_search
{
"query": {
"match_all": {}
}
}
21. 避免單次召回大量數(shù)據(jù),建議使用 _source_includes 和 _source_excludes 參數(shù)來包含或排除字段。
大型文檔尤其有用,部分字段檢索可以節(jié)省網(wǎng)絡(luò)開銷。
參考示例:
// 創(chuàng)建SearchSourceBuilder,并設(shè)置查詢條件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 設(shè)置要包含的字段
String[] includes = {"field1", "field2"};
sourceBuilder.fetchSource(includes, Strings.EMPTY_ARRAY);
// 設(shè)置要排除的字段
String[] excludes = {"field3"};
sourceBuilder.fetchSource(Strings.EMPTY_ARRAY, excludes);
22. 避免使用 Wildcard 進(jìn)行中綴模糊查詢。
ES 官方文檔并不推薦使用 Wildcard 來進(jìn)行中綴模糊的查詢,原因在于 ES 內(nèi)部為了加速這種帶有通配符查詢,會將輸入的字符串 Pattern 構(gòu)建成一個 DFA (Deterministic Finite Automaton),而帶有通配符的 Pattern 構(gòu)造出來的 DFA 可能會很復(fù)雜,開銷很大。
建議使用 ES 官方在 7.9 推出的一種專門用來解決模糊查詢慢的 Wildcard 字段類型。與 Text 字段相比,它不會將文本看作是標(biāo)點(diǎn)符號分割的單詞集合;與 Keyword 字段比,它在中綴搜索場景下具有無與倫比的查詢速度,且對輸入沒有大小限制,這是 Keyword 類型無法相比的。
23. 避免使用 Scripting。
Painless 腳本語言語法相對簡單,靈活度高,安全性高,性能高(相對于其他腳本,但是其性能比 DSL 要低)。不適用于非復(fù)雜業(yè)務(wù),一般 DSL 能解決大部分的問題,解決不了的用類似 Painless 等腳本語言。主要性能影響如下:單次查詢或更新耗時增加,腳本的執(zhí)行時間相比于其他查詢和更新操作可能會更長,因?yàn)樵趫?zhí)行腳本之前需要對其進(jìn)行詞法分析、語法分析和代碼編譯等預(yù)處理工作。
24. 避免使用腳本查詢(Script Query)計(jì)算動態(tài)字段,建議在索引時計(jì)算并在文檔中添加該字段。
例如,我們有一個包含大量用戶信息的索引,我們需要查詢以"1234"開頭的所有用戶。運(yùn)行一個腳本查詢?nèi)?quot;source":“doc[‘num’].value.startsWith(‘1234’)”。這個查詢非常耗費(fèi)資源,索引時考慮添加“num_prefix”的keyword字段,然后查詢"name_prefix":“1234”。
三、寫入相關(guān)
25. 避免代碼中或手工直接 Refresh 操作。
合理設(shè)置索引 Settings/Refresh_Interval 時間,通過系統(tǒng)完成 Refresh 動作。
26. 避免單個文檔過大。
鑒于默認(rèn) http.max_content_length 設(shè)置為 100MB,Elasticsearch 將拒絕索引任何大于該值的文檔。
27. 寫入數(shù)據(jù)不指定 Doc_ID,讓 ES 自動生成。
索引具有顯式 ID 的文檔時 ES 在寫入過程中會多一步判斷的過程,即檢查具有相同ID 的文檔是否已經(jīng)存在于相同的分片中,隨著索引增長而變得更加昂貴。
28. 合理使用 Bulk API 批量寫。
大數(shù)據(jù)量寫入時可以使用 Bulk,但是請求響應(yīng)的耗時會增加,即使連接斷開,ES 集群內(nèi)部也仍然在執(zhí)行。高速大批量數(shù)據(jù)寫入時,可能造成集群短時間內(nèi)響應(yīng)緩慢甚至假死的的情況。
- 可以通過性能測試確定最佳數(shù)量,官方建議大約 5-15mb。
- 超時時間需要足夠長,建議 60s 以上。
- 寫入端盡量將數(shù)據(jù)輪詢打到不同節(jié)點(diǎn)上。
29. 腳本刷大量數(shù)據(jù),寫入前調(diào)大 Refresh Interval,不建議將副本分片為 0,待寫入完成后再調(diào)回來。
副本分片重新加入節(jié)點(diǎn)會觸發(fā)副分片恢復(fù) Recovery 流程,如果是大分片會影響集群性能。
四、索引創(chuàng)建
分片
30. 副本分片數(shù)大于等于 1。
高可用性保證。增加副本數(shù)可以一定程度上提高搜索性能;但會降低寫入性能,建議每個主分片對應(yīng) 1-2 個副本分片即可。
31. 官方建議單分片限制最大數(shù)據(jù)條數(shù)不超過 2^32 - 1。
32. 索引主分片數(shù)量不要設(shè)置過大。
ES 創(chuàng)建好索引后,一般情況下不再動態(tài)調(diào)整主分片數(shù)量。
每個分片本質(zhì)上就是一個 Lucene 索引,因此會消耗相應(yīng)的文件句柄、內(nèi)存和 CPU 資源。
ES 使用詞頻統(tǒng)計(jì)來計(jì)算相關(guān)性,當(dāng)然這些統(tǒng)計(jì)也會分配到各個分片上,如果在大量分片上只維護(hù)了很少的數(shù)據(jù),則將導(dǎo)致最終的文檔相關(guān)性較差。
一般來說,我們遵循一些原則:
- 讀場景較多則可以設(shè)置少一點(diǎn),寫場景則可以設(shè)置多一些。
- 控制每個分片占用的硬盤容量不超過ES的最大 JVM 的堆空間設(shè)置(32G),因此,如果索引的總?cè)萘吭?200G 左右,那分片大小在 7-8 個左右即可。
- 考慮一下 Node 數(shù)量,一般一個節(jié)點(diǎn)對應(yīng)一臺物理機(jī),如果分片數(shù)遠(yuǎn)大于節(jié)點(diǎn)數(shù),則一個節(jié)點(diǎn)上存在多個分片,一旦該節(jié)點(diǎn)故障,即使保持了1個以上的副本,同樣有可能會導(dǎo)致數(shù)據(jù)丟失,集群無法恢復(fù)。所以, 一般都設(shè)置分片數(shù)不超過節(jié)點(diǎn)數(shù)的 3 倍。
33. 單個分片數(shù)據(jù)量不要超過 50GB。
單個索引的規(guī)模控制在 1TB 以內(nèi),單個分片大小控制在 30 ~ 50GB ,Docs 數(shù)控制在 10 億內(nèi),如果超過建議滾動。
Mapping設(shè)計(jì)
34. 避免使用字段動態(tài)映射功能,指定具體字段類型,子類型(若需要),分詞器(特別有場景需要)。
35. 對于不需要分詞的字符串字段,使用 Keyword 類型而不是 Text 類型。
36. ES 默認(rèn)字段個數(shù)最大 1000,建議不要超過 100。
單個 Doc 在建立索引時的運(yùn)算復(fù)雜度,最大的因素不在于 Doc 的字節(jié)數(shù)或者說某個字段 Value 的長度,而是字段的數(shù)量。例如在滿負(fù)載的寫入壓力測試中,Mapping 相同的情況下,一個有 10 個字段,200 字節(jié)的 Doc, 通過增加某些字段 Value 的長度到 500 字節(jié),寫入 ES 時速度下降很少,而如果字段數(shù)增加到 20,即使整個 Doc 字節(jié)數(shù)沒增加多少,寫入速度也會降低一倍。
37. 對于不索引字段,Index 屬性設(shè)置為 False。
在下面的例子中,Title 字段的 Index 屬性被設(shè)置為 False,表示該字段不會被包含在索引中。而 Content 字段的 Index 屬性默認(rèn)為 True,表示該字段會被包含在索引中。需要注意的是,即使 Index 屬性被設(shè)置為 False,該字段仍然會被保存在文檔中,可以被查詢和聚合。
參考示例:
{
"mappings": {
"properties": {
"title": {
"type": "text",
"index": false
},
"content": {
"type": "text"
}
}
}
}
38. 避免使用 Nested 或 Parent/Child。
Nested Query慢,Parent/Child Query 更慢,針對 1 個 Document,每一個 Nested Field 都會生成一個獨(dú)立的 Document,這將使 Doc 數(shù)量劇增,影響查詢效率尤其是 JOIN 的效率。因此能在 Mapping 設(shè)計(jì)階段搞定的(大寬表設(shè)計(jì)或采用比較 Smart 的數(shù)據(jù)結(jié)構(gòu)),就不要用父子關(guān)系的 Mapping。如果一定要使用 Nested Fields,保證 Nested Fields字段不能過多,目前ES默認(rèn)限制是Index.mapping.nested_fields.limit=50。不建議使用 Nested,那有什么方式來解決 ES 無法 JOIN 的問題?主要有幾種實(shí)現(xiàn)方式:
- 在文檔建模上盡可能在設(shè)計(jì)時將業(yè)務(wù)轉(zhuǎn)化有關(guān)聯(lián)關(guān)系的文檔形式,使用扁平的文檔模型。
- 獨(dú)立索引存儲,實(shí)際業(yè)務(wù)層分多次請求實(shí)現(xiàn)。
- 通過寬表冗余存儲避免關(guān)聯(lián)。
- 否則 Nested 和 Parent/Child 存儲對性能均有一定影響,由于 Nested 更新子文檔時需要 Reindex 整個文檔,所以對寫入性能影響較大,適用于 1 對 n(n 較小)場景;Parent/Child 存儲在相同 Type中,寫入相比 Nested性能高,用于 1 對 n(n 較大)場景,但比 Nested 查詢更慢,官網(wǎng)說是 5-10 倍左右。
39. 避免使用 Norms。
Norm 是索引評分因子,如果不用按評分對文檔進(jìn)行排序,設(shè)置為“False”。
參考示例:
"title": {"type": "string","norms": {"enabled": false}}
對于 Text 類型的字段而言,默認(rèn)開啟了 Norms,而 Keyword 類型的字段則默認(rèn)關(guān)閉了 Norms。
開啟 Norms 之后,每篇文檔的每個字段需要一個字節(jié)存儲 Norms。對于 Text 類型的字段而言是默認(rèn)開啟 Norms 的,因此對于不需要評分的 Text 類型的字段,可以禁用 Norms。
40. 對不需要進(jìn)行聚合/排序的字段禁用列存 Doc_Values。
面向列的方式存儲,主要用戶排序、聚合和訪問腳本中字段值等數(shù)據(jù)訪問場景。幾乎所有字段類型都支持 Doc_Values,值得注意的是,需要分析的字符串字段除外。默認(rèn)情況下,所有支持 Doc_Values 的字段都啟用了這個功能。如果確定不需要對字段進(jìn)行排序或聚合,或從腳本訪問字段值,則可以禁用此功能以減少冗余存儲成本。
Keyword和Numeric的選擇
Keyword 類型的主要缺點(diǎn)是在聚合的時候需要構(gòu)建全局序數(shù),而數(shù)值類型則不用。但低基數(shù)字段通常會命中大量結(jié)果集,例如性別,使用 Numeric 則會在構(gòu)建 Bitset 上產(chǎn)生很高的代價。
綜上所述,在類型選擇上可以參考下面的原則:
- 在僅查詢的情況下,如果有 Range 查詢需求,使用 Numeric,否則使用 KeyWord。
- 在僅聚合的情況下,如果明確字段是低基數(shù)的,使用 Keyword 配合 Execution_hint:map,其他情況使用 Numeric。
- 剩下 Term 查詢+聚合的場景,需要綜合考慮 Numeric 類型 Term 查詢構(gòu)建 BitSet 和 Keyword 類型構(gòu)建全局序數(shù)哪個代價更大,需要看實(shí)際場景,但是目前所知的最壞情況下,構(gòu)建 Bitset 會導(dǎo)致 CPU 跑滿,構(gòu)建全局序數(shù)的主要問題是帶來的查詢延遲,也會給 JVM 帶來一些壓力。
41. 對于極少使用 Range 查詢的數(shù)字值,使用 Keyword 類型。
并非所有數(shù)值數(shù)據(jù)都應(yīng)映射為數(shù)值字段數(shù)據(jù)類型。Elasticsearch 為查詢優(yōu)化數(shù)字字段,例如 Integer or long。如果不需要范圍查找,對于 Term 查詢而言,Keyword 比 Integer 性能更好。
42. 對于有頻繁且較為固定的 Range 查詢字段,增加 Keyword 類型 Pre-Indexing字段。
如果對字段的大多數(shù)查詢在一個固定的范圍上運(yùn)行 Range 聚合,那么可以增加一個 Keyword 類型的字段,通過將范圍“Pre-Indexing”到索引中并使用 Terms 聚合來加快聚合速度。
43. 對需要聚合查詢的高基數(shù) Keyword 字段啟用 Eager_Global_Ordinals。
參考:eager_global_ordinals
序號(Ordinals)用于在 Keyword 字段上運(yùn)行 Terms 聚合。序號用一個自增數(shù)值表示,ES 維護(hù)這個自增數(shù)字與實(shí)際值的映射關(guān)系,并為每一數(shù)值分配一個 Bucket,映射關(guān)系是 Segment 級別的。
但是做聚合操作時往往需要結(jié)合多個 Segment 的結(jié)果,而每個 Segment 的 Ordinals 映射關(guān)系是不一致的,所以 ES 會在每個分片上創(chuàng)建全局序號(Global Ordinals)結(jié)構(gòu) ,一個全局統(tǒng)一的映射,維護(hù)全局的 Ordinal 與每個 Segment 的 Ordinal 的映射關(guān)系。
默認(rèn)情況下,Global Ordinals 默認(rèn)是延時構(gòu)建,在第一次查詢?nèi)?Term Aggregation 使用到時才會構(gòu)建。因?yàn)?ES 不知道哪些字段將用于 Terms 聚合,哪些字段不會。對于基數(shù)大的字段,構(gòu)建成本較大。
啟用 eager_global_ordinals 后,Elasticsearch 會在分片構(gòu)建時預(yù)先計(jì)算出全局詞項(xiàng)表,以便在查詢時能夠更快地加載和使用。但啟用 eager_global_ordinals 后,每次執(zhí)行 Refresh 操作都會構(gòu)建 Global Ordinals,相當(dāng)于把搜索時候花費(fèi)的構(gòu)建成本轉(zhuǎn)移到寫入時,所以會對寫入效率有一定的影響,可以配合增大索引的 Refresh Interval 來使用。
參考示例:
PUT index
{
"mappings": {
"type": {
"properties": {
"foo": {
"type": "keyword",
"eager_global_ordinals" : true
}
}
}
}
}
五、總結(jié)
最近十年,Elasticsearch 已經(jīng)成為了最受歡迎的開源檢索引擎,并沉淀了大量的實(shí)踐案例及優(yōu)化總結(jié)。在本文中,我們盡可能全面地總結(jié)了 Elasticsearch 日常開發(fā)中的一些重要實(shí)踐&避坑指南,希望能為大家提供 Elasticsearch 使用上的一些借鑒點(diǎn),歡迎討論!
參考文章:
1.《Elasticsearch 源碼解析與優(yōu)化實(shí)戰(zhàn)》
2.《Elasticsearch權(quán)威指南》
3.https://www.easyice.cn/archives/367
4.https://www.elastic.co/guide/en/elasticsearch/guide/current/filter-caching.html#_independent_query_caching
5.https://www.elastic.co/guide/cn/elasticsearch/guide/current/_preventing_combinatorial_explosions.html
6.https://www.elastic.co/guide/en/elasticsearch/reference/current/eager-global-ordinals.html