題記
隨著 Elastic 的上市,ELK Stack 不僅在 BAT 的大公司得到長足的發展,而且在各個中小公司都得到非常廣泛的應用,甚至連“婚慶網站”都開始使用 Elasticsearch 了。隨之而來的是 Elasticsearch 相關部署、框架、性能優化的文章早已鋪天蓋地。
初學者甚至會進入幻覺——“一鍵部署、導入數據、檢索&聚合、動態擴展, So Easy,媽媽再也不用擔心我的 Elastic 學習”!
但,實際上呢?僅就 Elasticsearch 索引設計,請回答如下幾個問題:
- 每天幾百 GB 增量實時數據的TB級甚至PB級別的大索引如何設計?
- 分片數和副本數大小如何設計,才能提升 ES 集群的性能?
- ES 的 Mapping 該如何設計,才能保證檢索的高效?
- 檢索類型 term/match/matchphrase/querystring /match_phrase _prefix /fuzzy 那麼多,設計階段如何選型呢?
- 分詞該如何設計,才能滿足複雜業務場景需求?
- 傳統數據庫中的多表關聯在 ES 中如何設計?......
這麼看來,沒有那麼 Easy,坑還是得一步步的踩出來的。
正如攜程架構師 WOOD 大叔所說“做搜索容易,做好搜索相當難!”,
VIVO 搜索引擎架構師所說“ 熟練使用 ES 離做好搜索還差很遠!”。
本文主結合作者近千萬級開發實戰經驗,和大家一起深入探討一下Elasticsearch 索引設計......
索引設計的重要性
在美團寫給工程師的十條精進原則中強調了“設計優先”。無數事實證明,忽略了前期設計,往往會帶來很大的延期風險。並且未經評估的不當的設計會帶來巨大的維護成本,後期不得不騰出時間,專門進行優化和重構。
而 Elasticsearch 日漸成為大家非結構數據庫的首選方案,項目前期良好的設計和評審是必須的,能給整個項目帶來收益。
索引層面的設計在 Elasticsearch 相關產品、項目的設計階段的作用舉重若輕。
- 好的索引設計在整個集群規劃中佔據舉足輕重的作用,索引的設計直接影響集群設計的好壞和複雜度。
- 好的索引設計應該是充分結合業務場景的時間維度和空間維度,結合業務場景充分考量增、刪、改、查等全維度設計的。
- 好的索引設計是完全基於“設計先行,編碼在後”的原則,前期會花很長時間,為的是後期工作更加順暢,避免不必要的返工。
1、PB 級別的大索引如何設計?
單純的普通數據索引,如果不考慮增量數據,基本上普通索引就能夠滿足性能要求。
我們通常的操作就是:
- 步驟 1:創建索引;
- 步驟 2:導入或者寫入數據;
- 步驟 3:提供查詢請求訪問或者查詢服務。
1.1 大索引的缺陷
如果每天億萬+的實時增量數據呢,基於以下幾點原因,單個索引是無法滿足要求的。在 360 技術訪談中也提到了大索引的設計的困惑。
1.1.1 存儲大小限制維度
單個分片(Shard)實際是 Lucene 的索引,單分片能存儲的最大文檔數是:2,147,483,519 (= Integer.MAX_VALUE - 128)。如下命令能查看全部索引的分隔分片的文檔大小:
<code>GET
_cat/shardsapp_index
2
p
STARTED
9443
2.
8mb
127.0
.0
.1
Hk9wFwUapp_index
2
r
UNASSIGNED
app_index
3
p
STARTED
9462
2.
7mb
127.0
.0
.1
Hk9wFwUapp_index
3
r
UNASSIGNED
app_index
4
p
STARTED
9520
3.
5mb
127.0
.0
.1
Hk9wFwUapp_index
4
r
UNASSIGNED
app_index
1
p
STARTED
9453
2.
4mb
127.0
.0
.1
Hk9wFwUapp_index
1
r
UNASSIGNED
app_index
0
p
STARTED
9365
2.
3mb
127.0
.0
.1
Hk9wFwUapp_index
0
r
UNASSIGNED
/<code>
1.1.2 性能維度
當然一個索引很大的話,數據寫入和查詢性能都會變差。
而高效檢索體現在:基於日期的檢索可以直接檢索對應日期的索引,無形中縮減了很大的數據規模。
比如檢索:“2019-02-01”號的數據,之前的檢索會是在一個月甚至更大體量的索引中進行。
現在直接檢索"index_2019-02-01"的索引,效率提升好幾倍。
1.1.3 風險維度
一旦一個大索引出現故障,相關的數據都會受到影響。而分成滾動索引的話,相當於做了物理隔離。
1.2 PB 級索引設計實現
綜上,結合實踐經驗,大索引設計建議:使用模板+Rollover+Curator動態創建索引。動態索引使用效果如下:
<code>index_2019-01
-01
-000001i
ndex_2019-01
-02
-000002i
ndex_2019-01
-03
-000003i
ndex_2019-01
-04
-000004i
ndex_2019-01
-05
-000005
/<code>
1.2.1 使用模板統一配置索引
目的:統一管理索引,相關索引字段完全一致。
1.2.2 使用 Rollver 增量管理索引
目的:按照日期、文檔數、文檔存儲大小三個維度進行更新索引。使用舉例:
<code>POST
/logs_write/_rollover {"conditions"
: {"max_age"
:"7d"
,"max_docs"
: 1000,"max_size"
:"5gb"
}}/<code>
1.2.3 索引增量更新原理
一圖勝千言。
索引更新的時機是:當原始索引滿足設置條件的三個中的一個的時候,就會更新為新的索引。為保證業務的全索引檢索,一般採用別名機制。
在索引模板設計階段,模板定義一個全局別名:用途是全局檢索,如圖所示的別名:indexall。每次更新到新的索引後,新索引指向一個用於實時新數據寫入的別名,如圖所示的別名:indexlatest。同時將舊索引的別名 index_latest 移除。
別名刪除和新增操作舉例:
<code>POST /_aliases{"actions"
: [ {"remove"
: {"index"
:"index_2019-01-01-000001"
,"alias"
:"index_latest"
} }, {"add"
: {"index"
:"index_2019-01-02-000002"
,"alias"
:"index_latest"
} } ]}/<code>
經過如上步驟,即可完成索引的更新操作。
1.2.4 使用 curator 高效清理歷史數據
目的:按照日期定期刪除、歸檔歷史數據。
一個大索引的數據刪除方式只能使用 delete_by_query,由於 ES 中使用更新版本機制。刪除索引後,由於沒有物理刪除,磁盤存儲信息會不減反增。有同學就反饋 500GB+ 的索引 delete_by_query 導致負載增高的情況。
而按照日期劃分索引後,不需要的歷史數據可以做如下的處理。
- 刪除——對應 delete 索引操作。
- 壓縮——對應 shrink 操作。
- 段合併——對應 force_merge 操作。
而這一切,可以藉助:curator 工具通過簡單的配置文件結合定義任務 crontab 一鍵實現。
注意:7.X高版本藉助iLM實現更為簡單。
舉例,一鍵刪除 30 天前的歷史數據:
<code> [root@localhost .curator]/<code>
2、分片數和副本數如何設計?
2.1 分片/副本認知
- 1、分片:分片本身都是一個功能齊全且獨立的“索引”,可以託管在集群中的任何節點上。
數據切分分片的主要目的:
(1)水平分割/縮放內容量 。
(2)跨分片(可能在多個節點上)分佈和並行化操作,提高性能/吞吐量。
注意:分片一旦創建,不可以修改大小。
- 2、副本:它在分片/節點出現故障時提供高可用性。
副本的好處:因為可以在所有副本上並行執行搜索——因此擴展了搜索量/吞吐量。
注意:副本分片與主分片存儲在集群中不同的節點。副本的大小可以通過:number_of_replicas動態修改。
2.2 分片和副本實戰中設計
最常見問題答疑
2.2.1 問題 1:索引設置多少分片?
Shard 大小官方推薦值為 20-40GB, 具體原理呢?Elasticsearch 員工 Medcl 曾經討論如下:
Lucene 底層沒有這個大小的限制,20-40GB 的這個區間範圍本身就比較大,經驗值有時候就是拍腦袋,不一定都好使。
Elasticsearch 對數據的隔離和遷移是以分片為單位進行的,分片太大,會加大遷移成本。
一個分片就是一個 Lucene 的庫,一個 Lucene 目錄裡面包含很多 Segment,每個 Segment 有文檔數的上限,Segment 內部的文檔 ID 目前使用的是 Java 的整型,也就是 2 的 31 次方,所以能夠表示的總的文檔數為Integer.MAXVALUE - 128 = 2^31 - 128 = 2147483647 - 1 = 2,147,483,519,也就是21.4億條。
同樣,如果你不 forcemerge 成一個 Segment,單個 shard 的文檔數能超過這個數。
單個 Lucene 越大,索引會越大,查詢的操作成本自然要越高,IO 壓力越大,自然會影響查詢體驗。
具體一個分片多少數據合適,還是需要結合實際的業務數據和實際的查詢來進行測試以進行評估。
綜合實戰+網上各種經驗分享,梳理如下:
- 第一步:預估一下數據量的規模。一共要存儲多久的數據,每天新增多少數據?兩者的乘積就是總數據量。
- 第二步:預估分多少個索引存儲。索引的劃分可以根據業務需要。
- 第三步:考慮和衡量可擴展性,預估需要搭建幾臺機器的集群。存儲主要看磁盤空間,假設每臺機器2TB,可用:2TB0.85(磁盤實際利用率)0.85(ES 警戒水位線)。
- 第四步:單分片的大小建議最大設置為 30GB。此處如果是增量索引,可以結合大索引的設計部分的實現一起規劃。
前三步能得出一個索引的大小。分片數考慮維度:
- 1)分片數 = 索引大小/分片大小經驗值 30GB 。
- 2)分片數建議和節點數一致。設計的時候1)、2)兩者權衡考慮+rollover 動態更新索引結合。
每個 shard 大小是按照經驗值 30G 到 50G,因為在這個範圍內查詢和寫入性能較好。
經驗值的探推薦閱讀:
Elasticsearch究竟要設置多少分片數?
探究 | Elasticsearch集群規模和容量規劃的底層邏輯
2.2.2 問題 2:索引設置多少副本?
結合集群的規模,對於集群數據節點 >=2 的場景:建議副本至少設置為 1。
之前有同學出現過:副本設置為 0,長久以後會出現——數據寫入向指定機器傾斜的情況。
注意:
單節點的機器設置了副本也不會生效的。副本數的設計結合數據的安全需要。對於數據安全性要求非常高的業務場景,建議做好:增強備份(結合 ES 官方備份方案)。
3、Mapping 如何設計?
3.1 Mapping 認知
Mapping 是定義文檔及其包含的字段的存儲和索引方式的過程。例如,使用映射來定義:
- 應將哪些字符串字段定義為全文檢索字段;
- 哪些字段包含數字,日期或地理位置;
- 定義日期值的格式(時間戳還是日期類型等);
- 用於控制動態添加字段的映射的自定義規則。
3.2 設計 Mapping 的注意事項
ES 支持增加字段 //新增字段
<code>PUT
new_index {"mappings"
: {"_doc"
: {"properties"
: {"status_code"
: {"type"
:"keyword"
} } } } }/<code>
- ES 不支持直接刪除字段
- ES 不支持直接修改字段
- ES 不支持直接修改字段類型 如果非要做靈活設計,ES 有其他方案可以替換,藉助reindex。但是數據量大會有性能問題,建議設計階段綜合權衡考慮。
3.3 Mapping 字段的設置流程
索引分為靜態 Mapping(自定義字段)+動態 Mapping(ES 自動根據導入數據適配)。
實戰業務場景建議:選用靜態 Mapping,根據業務類型自己定義字段類型。
好處:
- 可控;
- 節省存儲空間(默認 string 是 text+keyword,實際業務不一定需要)。
設置字段的時候,務必過一下如下圖示的流程。根據實際業務需要,主要關注點:
- 數據類型選型;
- 是否需要檢索;
- 是否需要排序+聚合分析;
- 是否需要另行存儲。
核心參數的含義,梳理如下:
3.4 Mapping 建議結合模板定義
索引 Templates——索引模板允許您定義在創建新索引時自動應用的模板。模板包括settings和Mappings以及控制是否應將模板應用於新索引。
注意:模板僅在索引創建時應用。更改模板不會對現有索引產生影響。
第1部分也有說明,針對大索引,使用模板是必須的。核心需要設置的setting(僅列舉了實戰中最常用、可以動態修改的)如下:
- index.numberofreplicas 每個主分片具有的副本數。默認為 1(7.X 版本,低於 7.X 為 5)。
- index.maxresultwindow 深度分頁 rom + size 的最大值—— 默認為 10000。
- index.refresh_interval 默認 1s:代表最快 1s 搜索可見;
寫入時候建議設置為 -1,提高寫入性能;
實戰業務如果對實時性要求不高,建議設置為 30s 或者更高。
3.5 包含 Mapping 的 template 設計萬能模板
以下模板已經在 7.2 驗證 ok,可以直接拷貝修改後實戰項目中使用。
<code>PUT _template/test_template{"index_patterns"
: ["test_index_*"
,"test_*"
],"settings"
: {"number_of_shards"
: 1,"number_of_replicas"
: 1,"max_result_window"
: 100000,"refresh_interval"
:"30s"
},"mappings"
: {"properties"
: {"id"
: {"type"
:"long"
},"title"
: {"type"
:"keyword"
},"content"
: {"analyzer"
:"ik_max_word"
,"type"
:"text"
,"fields"
: {"keyword"
: {"ignore_above"
: 256,"type"
:"keyword"
} } },"available"
: {"type"
:"boolean"
},"review"
: {"type"
:"nested"
,"properties"
: {"nickname"
: {"type"
:"text"
},"text"
: {"type"
:"text"
},"stars"
: {"type"
:"integer"
} } },"publish_time"
: {"type"
:"date"
,"format"
:"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},"expected_attendees"
: {"type"
:"integer_range"
},"ip_addr"
: {"type"
:"ip"
},"suggest"
: {"type"
:"completion"
} } }}/<code>
4、分詞的選型
主要以 ik 來說明,最新版本的ik支持兩種類型。ik_maxword 細粒度匹配,適用切分非常細的場景。ik_smart 粗粒度匹配,適用切分粗的場景。
4.1 坑 1:分詞選型
實際業務中:建議適用ik_max_word分詞 + match_phrase短語檢索。
原因:ik_smart有覆蓋不全的情況,數據量大了以後,即便 reindex 能滿足要求,但面對極大的索引的情況,reindex 的耗時我們承擔不起。建議ik_max_word一步到位。
4.2 坑 2:ik 要裝集群的所有機器嗎?
建議:安裝在集群的所有節點上。
4.3 坑 3:ik 匹配不到怎麼辦?
- 方案1:擴充 ik 開源自帶的詞庫+動態更新詞庫;原生的詞庫分詞數量級很小,基礎詞庫儘量更大更全,網上搜索一下“搜狗詞庫“。
動態更新詞庫:可以結合 mysql+ik 自帶的更新詞庫的方式動態更新詞庫。
更新詞庫僅對新創建的索引生效,部分老數據索引建議使用 reindex 升級處理。
- 方案2:採用字詞混合索引的方式,避免“明明存在,但是檢索不到的”場景。探究 | 明明存在,怎麼搜索不出來呢?
5、檢索類型如何選型呢?
前提:5.X 版本之後,string 類型不再存在,取代的是text和keyword類型。
- text 類型作用:分詞,將大段的文字根據分詞器切分成獨立的詞或者詞組,以便全文檢索。
適用於:email 內容、某產品的描述等需要分詞全文檢索的字段;
不適用:排序或聚合(Significant Terms 聚合例外)
- keyword 類型:無需分詞、整段完整精確匹配。
適用於:email 地址、住址、狀態碼、分類 tags。
以一個實戰例子說明:
<code>PUT
zz_test {"mappings"
: {"doc"
: {"properties"
: {"title"
: {"type"
:"text"
,"analyzer"
:"ik_max_word"
,"fields"
: {"keyword"
: {"type"
:"keyword"
,"ignore_above"
: 256 } } } } } } }GET zz_test/_mappingPUT zz_test/doc/1{"title"
:"錘子加溼器官方致歉,難產後臨時推遲一個月發貨遭diss耍流氓"
}POST zz_test/_analyze{"text"
:"錘子加溼器官方致歉,難產後臨時推遲一個月發貨遭diss耍流氓"
,"analyzer"
:"ik_max_word"
}/<code>
ik_max_word的分詞結果如下:
錘子、錘、子、加溼器、溼、器官、官方、方、致歉、致、歉、難產、產後、後、臨時、臨、時、推遲、遲、一個、 一個、 一、個月、 個、 月、 發貨、發、貨、遭、diss、耍流氓、耍、流氓、氓。
5.1 term 精確匹配
- 核心功能:不受到分詞器的影響,屬於完整的精確匹配。
- 應用場景:精確、精準匹配。
- 適用類型:keyword。
- 舉例:term 最適合匹配的類型是 keyword,如下所示的精確完整匹配:
<code>POST
zz_test/_search {"query"
: {"term"
: {"title.keyword"
:"錘子加溼器官方致歉,難產後臨時推遲一個月發貨遭diss耍流氓"
} } }/<code>
- 注意:如下是匹配不到結果的。
<code>POST
zz_test/_search{"query"
: {"term"
: {"title"
:"錘子加溼器"
} }}/<code>
- 原因:對於 title 中的錘子加溼器,term 不會做分詞拆分匹配的。且 ik_max_word 分詞也是沒有“錘子加溼器”這組關鍵詞的。
5.2 prefix 前綴匹配
- 核心功能:前綴匹配。
- 應用場景:前綴自動補全的業務場景。
- 適用類型:keyword。
如下能匹配到文檔 id 為 1 的文章。
<code>POST
zz_test/_search{"query"
: {"prefix"
: {"title.keyword"
:"錘子加溼器"
} }}/<code>
5.3 wildcard 模糊匹配
- 核心功能:匹配具有匹配通配符表達式 keyword 類型的文檔。支持的通配符:*,它匹配任何字符序列(包括空字符序列);?,它匹配任何單個字符。
- 應用場景:請注意,選型務必要慎重!此查詢可能很慢多組關鍵次的情況下可能會導致宕機,因為它需要遍歷多個術語。為了防止非常慢的通配符查詢,通配符不能以任何一個通配符*或?開頭。
- 適用類型:keyword。
如下匹配,類似 MySQL 中的通配符匹配,能匹配所有包含加溼器的文章。
<code>POST
zz_test/_search{"query"
: {"wildcard"
: {"title.keyword"
:"*加溼器*"
} }}/<code>
5.4 match 分詞匹配
- 核心功能:全文檢索,分詞詞項匹配。
- 應用場景:實際業務中較少使用,原因:匹配範圍太寬泛,不夠準確。
- 適用類型:text。
- 如下示例,title 包含"錘子"和“加溼器”的都會被檢索到。
<code>POST zz_test/_search{"profile"
:true
,"query"
: {"match"
: {"title"
:"錘子加溼器"
} }}/<code>
5.5 match_phrase 短語匹配
- 核心功能:match_phrase 查詢首先將查詢字符串解析成一個詞項列表,然後對這些詞項進行搜索; 只保留那些包含 全部 搜索詞項,且 位置"position" 與搜索詞項相同的文檔。
- 應用場景:業務開發中 90%+ 的全文檢索都會使用 match_phrase 或者 query_string 類型,而不是 match。
- 適用類型:text。
- 注意:
<code>POST
zz_test/_analyze{"text"
:"錘子加溼器"
,"analyzer"
:"ik_max_word"
}/<code>
- 分詞結果:
錘子, 錘,子, 加溼器, 溼,器。而:id為1的文檔的分詞結果:錘子, 錘, 子, 加溼器, 溼, 器官。所以,如下的檢索是匹配不到結果的。
<code>POST
zz_test/_search{"query"
: {"match_phrase"
: {"title"
:"錘子加溼器"
}}}/<code>
如果想匹配到,怎麼辦呢?這裡可以字詞組合索引的形式。
來自:大數據技術架構