隨着 Elastic 的上市,ELK Stack 不只在 BAT 的大公司獲得長足的發展,並且在各個中小公司都獲得很是普遍的應用,甚至連「婚慶網站」都開始使用 Elasticsearch 了。隨之而來的是 Elasticsearch 相關部署、框架、性能優化的文章早已鋪天蓋地。mysql
初學者甚至會進入幻覺——「一鍵部署、導入數據、檢索&聚合、動態擴展, So Easy,媽媽不再用擔憂個人 Elastic 學習」!sql
但,實際上呢?僅就 Elasticsearch 索引設計,請回答以下幾個問題:數據庫
這麼看來,沒有那麼 Easy,坑仍是得一步步的踩出來的。數組
正如攜程架構師 WOOD 大叔所說「作搜索容易,作好搜索至關難!」,安全
VIVO 搜索引擎架構師所說「 熟練使用 ES 離作好搜索還差很遠!」。性能優化
本文主結合做者近千萬級開發實戰經驗,和你們一塊兒深刻探討一下Elasticsearch 索引設計......bash
在美團寫給工程師的十條精進原則中強調了「設計優先」。無數事實證實,忽略了前期設計,每每會帶來很大的延期風險。而且未經評估的不當的設計會帶來巨大的維護成本,後期不得不騰出時間,專門進行優化和重構。markdown
而 Elasticsearch 日漸成爲你們非結構數據庫的首選方案,項目前期良好的設計和評審是必須的,能給整個項目帶來收益。架構
索引層面的設計在 Elasticsearch 相關產品、項目的設計階段的做用舉重若輕。app
單純的普通數據索引,若是不考慮增量數據,基本上普通索引就可以知足性能要求。
咱們一般的操做就是:
若是天天億萬+的實時增量數據呢,基於如下幾點緣由,單個索引是沒法知足要求的。在 360 技術訪談中也提到了大索引的設計的困惑。
單個分片(Shard)實際是 Lucene 的索引,單分片能存儲的最大文檔數是:2,147,483,519 (= Integer.MAX_VALUE - 128)。以下命令能查看所有索引的分隔分片的文檔大小:
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複製代碼
固然一個索引很大的話,數據寫入和查詢性能都會變差。
而高效檢索體如今:基於日期的檢索能夠直接檢索對應日期的索引,無形中縮減了很大的數據規模。
好比檢索:「2019-02-01」號的數據,以前的檢索會是在一個月甚至更大致量的索引中進行。
如今直接檢索"index_2019-02-01"的索引,效率提高好幾倍。
一旦一個大索引出現故障,相關的數據都會受到影響。而分紅滾動索引的話,至關於作了物理隔離。
綜上,結合實踐經驗,大索引設計建議:使用模板+Rollover+Curator動態建立索引。動態索引使用效果以下:
index_2019-01-01-000001index_2019-01-02-000002index_2019-01-03-000003index_2019-01-04-000004index_2019-01-05-000005複製代碼
目的:統一管理索引,相關索引字段徹底一致。
目的:按照日期、文檔數、文檔存儲大小三個維度進行更新索引。使用舉例:
POST /logs_write/_rollover { "conditions": { "max_age": "7d", "max_docs": 1000, "max_size": "5gb" }}複製代碼
一圖勝千言。
索引更新的時機是:當原始索引知足設置條件的三個中的一個的時候,就會更新爲新的索引。爲保證業務的全索引檢索,通常採用別名機制。
在索引模板設計階段,模板定義一個全局別名:用途是全局檢索,如圖所示的別名:indexall。每次更新到新的索引後,新索引指向一個用於實時新數據寫入的別名,如圖所示的別名:indexlatest。同時將舊索引的別名 index_latest 移除。
別名刪除和新增操做舉例:
POST /_aliases{ "actions" : [ { "remove" : { "index" : "index_2019-01-01-000001", "alias" : "index_latest" } }, { "add" : { "index" : "index_2019-01-02-000002", "alias" : "index_latest" } } ]}複製代碼
通過如上步驟,便可完成索引的更新操做。
目的:按照日期按期刪除、歸檔歷史數據。
一個大索引的數據刪除方式只能使用 delete_by_query,因爲 ES 中使用更新版本機制。刪除索引後,因爲沒有物理刪除,磁盤存儲信息會不減反增。有同窗就反饋 500GB+ 的索引 delete_by_query 致使負載增高的狀況。
而按照日期劃分索引後,不須要的歷史數據能夠作以下的處理。
刪除——對應 delete 索引操做。
壓縮——對應 shrink 操做。
段合併——對應 force_merge 操做。
而這一切,能夠藉助:curator 工具經過簡單的配置文件結合定義任務 crontab 一鍵實現。
注意:7.X高版本藉助iLM實現更爲簡單。
舉例,一鍵刪除 30 天前的歷史數據:
[root@localhost .curator]# cat action.yml actions: 1: action: delete_indices description: >- Delete indices older than 30 days (based on index name), for logstash- prefixed indices. Ignore the error if the filter does not result in an actionable list of indices (ignore_empty_list) and exit cleanly. options: ignore_empty_list: True disable_action: False filters: - filtertype: pattern kind: prefix value: logs_ - filtertype: age source: name direction: older timestring: '%Y.%m.%d' unit: days unit_count: 30複製代碼
數據切分分片的主要目的:
(1)水平分割/縮放內容量 。
(2)跨分片(可能在多個節點上)分佈和並行化操做,提升性能/吞吐量。
注意:分片一旦建立,不能夠修改大小。
副本的好處:由於能夠在全部副本上並行執行搜索——所以擴展了搜索量/吞吐量。
注意:副本分片與主分片存儲在集羣中不一樣的節點。副本的大小能夠經過:number_of_replicas動態修改。
最多見問題答疑
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。此處若是是增量索引,能夠結合大索引的設計部分的實現一塊兒規劃。
前三步能得出一個索引的大小。分片數考慮維度:
每一個 shard 大小是按照經驗值 30G 到 50G,由於在這個範圍內查詢和寫入性能較好。
經驗值的探推薦閱讀:
探究 | Elasticsearch集羣規模和容量規劃的底層邏輯
結合集羣的規模,對於集羣數據節點 >=2 的場景:建議副本至少設置爲 1。
以前有同窗出現過:副本設置爲 0,長久之後會出現——數據寫入向指定機器傾斜的狀況。
注意:
單節點的機器設置了副本也不會生效的。副本數的設計結合數據的安全須要。對於數據安全性要求很是高的業務場景,建議作好:加強備份(結合 ES 官方備份方案)。
Mapping 是定義文檔及其包含的字段的存儲和索引方式的過程。例如,使用映射來定義:
ES 支持增長字段 //新增字段
PUT new_index { "mappings": { "_doc": { "properties": { "status_code": { "type": "keyword" } } } } }複製代碼
索引分爲靜態 Mapping(自定義字段)+動態 Mapping(ES 自動根據導入數據適配)。
實戰業務場景建議:選用靜態 Mapping,根據業務類型本身定義字段類型。
好處:
設置字段的時候,務必過一下以下圖示的流程。根據實際業務須要,主要關注點:
核心參數的含義,梳理以下:
索引 Templates——索引模板容許您定義在建立新索引時自動應用的模板。模板包括settings和Mappings以及控制是否應將模板應用於新索引。
注意:模板僅在索引建立時應用。更改模板不會對現有索引產生影響。
第1部分也有說明,針對大索引,使用模板是必須的。核心須要設置的setting(僅列舉了實戰中最經常使用、能夠動態修改的)以下:
寫入時候建議設置爲 -1,提升寫入性能;
實戰業務若是對實時性要求不高,建議設置爲 30s 或者更高。
如下模板已經在 7.2 驗證 ok,能夠直接拷貝修改後實戰項目中使用。
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" } } }}複製代碼
主要以 ik 來講明,最新版本的ik支持兩種類型。ik_maxword 細粒度匹配,適用切分很是細的場景。ik_smart 粗粒度匹配,適用切分粗的場景。
實際業務中:建議適用ik_max_word分詞 + match_phrase短語檢索。
緣由:ik_smart有覆蓋不全的狀況,數據量大了之後,即使 reindex 能知足要求,但面對極大的索引的狀況,reindex 的耗時咱們承擔不起。建議ik_max_word一步到位。
建議:安裝在集羣的全部節點上。
動態更新詞庫:能夠結合 mysql+ik 自帶的更新詞庫的方式動態更新詞庫。
更新詞庫僅對新建立的索引生效,部分老數據索引建議使用 reindex 升級處理。
前提:5.X 版本以後,string 類型再也不存在,取代的是text和keyword類型。
text 類型做用:分詞,將大段的文字根據分詞器切分紅獨立的詞或者詞組,以便全文檢索。
適用於:email 內容、某產品的描述等須要分詞全文檢索的字段;
不適用:排序或聚合(Significant Terms 聚合例外)
keyword 類型:無需分詞、整段完整精確匹配。
適用於:email 地址、住址、狀態碼、分類 tags。
以一個實戰例子說明:
PUT zz_test { "mappings": { "doc": { "properties": { "title": { "type": "text", "analyzer":"ik_max_word", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } }GET zz_test/_mapping PUT zz_test/doc/1{ "title":"錘子加溼器官方致歉,難產後臨時推遲一個月發貨遭diss耍流氓"} POST zz_test/_analyze{ "text": "錘子加溼器官方致歉,難產後臨時推遲一個月發貨遭diss耍流氓", "analyzer": "ik_max_word"}複製代碼
ik_max_word的分詞結果以下:
錘子、錘、子、加溼器、溼、器官、官方、方、致歉、致、歉、難產、產後、後、臨時、臨、時、推遲、遲、一個、 一個、 1、個月、 個、 月、 發貨、發、貨、遭、diss、耍流氓、耍、流氓、氓。
POST zz_test/_search { "query": { "term": { "title.keyword": "錘子加溼器官方致歉,難產後臨時推遲一個月發貨遭diss耍流氓" } } }複製代碼
POST zz_test/_search{ "query": { "term": { "title": "錘子加溼器" } }}複製代碼
以下能匹配到文檔 id 爲 1 的文章。
POST zz_test/_search{ "query": { "prefix": { "title.keyword": "錘子加溼器" } }}複製代碼
5.3 wildcard 模糊匹配
應用場景:請注意,選型務必要慎重!此查詢可能很慢多組關鍵次的狀況下可能會致使宕機,由於它須要遍歷多個術語。爲了防止很是慢的通配符查詢,通配符不能以任何一個通配符*或?開頭。
適用類型:keyword。
以下匹配,相似 MySQL 中的通配符匹配,能匹配全部包含加溼器的文章。
POST zz_test/_search{ "query": { "wildcard": { "title.keyword": "*加溼器*" } }}複製代碼
POST zz_test/_search{ "profile": true, "query": { "match": { "title": "錘子加溼器" } }}複製代碼
5.5 match_phrase 短語匹配
核心功能:match_phrase 查詢首先將查詢字符串解析成一個詞項列表,而後對這些詞項進行搜索; 只保留那些包含 所有 搜索詞項,且 位置"position" 與搜索詞項相同的文檔。
應用場景:業務開發中 90%+ 的全文檢索都會使用 match_phrase 或者 query_string 類型,而不是 match。
適用類型:text。
注意:
POST zz_test/_analyze{ "text": "錘子加溼器", "analyzer": "ik_max_word"}複製代碼
錘子, 錘,子, 加溼器, 溼,器。而:id爲1的文檔的分詞結果:錘子, 錘, 子, 加溼器, 溼, 器官。因此,以下的檢索是匹配不到結果的。
POST zz_test/_search{"query": { "match_phrase": { "title": "錘子加溼器" }}}複製代碼
若是想匹配到,怎麼辦呢?這裏能夠字詞組合索引的形式。
推薦閱讀:
POST zz_test/_search{ "query": { "multi_match": { "query": "加溼器", "fields": [ "title", "content" ] } }}複製代碼
POST zz_test/_search{ "query": { "query_string": { "default_field": "title", "query": "(錘子 AND 加溼器) OR (官方 AND 道歉)" } }}複製代碼
{ "bool" : { "must" : [], "should" : [], "must_not" : [], "filter": [] }}複製代碼
多表關聯是被問的最多的問題之一。幾乎每週都會被問到。
主要緣由:常規基於關係型數據庫開發,多多少少都會遇到關聯查詢。而關係型數據庫設計的思惟很容易帶到 ES 的設計中。
MySQL 寬表導入 ES,使用 ES 查詢+檢索。適用場景:基礎業務都在 MySQL,存在幾十張甚至幾百張表,準備同步到 ES,使用 ES 作全文檢索。
將數據整合成一個寬表後寫到 ES,寬表的實現能夠藉助關係型數據庫的視圖實現。
寬表處理在處理一對多、多對多關係時,會有字段冗餘問題,若是藉助:logstash_input_jdbc,關係型數據庫如 MySQL 中的每個字段都會自動幫你轉成 ES 中對應索引下的對應 document 下的某個相同字段下的數據。
步驟 1:提早關聯好數據,將關聯的表創建好視圖,一個索引對應你的一個視圖,並確認視圖中數據的正確性。
步驟 2:ES 中針對每一個視圖定義好索引名稱及 Mapping。
步驟 3:以視圖爲單位經過 logstash_input_jdbc 同步到 ES 中。
MySQL+ES 結合,各取所長。適用場景:關係型數據庫全量同步到 ES 存儲,沒有作冗餘視圖關聯。
ES 擅長的是檢索,而 MySQL 才擅長關係管理。
因此能夠考慮兩者結合,使用 ES 多索引創建相同的別名,針對別名檢索到對應 ID 後再回 MySQL 經過關聯 ID join 出須要的數據。
適用場景:1 對少許的場景。
舉例:有一個文檔描述了一個帖子和一個包含帖子上全部評論的內部對象評論。能夠藉助 Nested 實現。
Nested 類型選型——若是須要索引對象數組並保持數組中每一個對象的獨立性,則應使用嵌套 Nested 數據類型而不是對象 Oject 數據類型。
當使用嵌套文檔時,使用通用的查詢方式是沒法訪問到的,必須使用合適的查詢方式(nested query、nested filter、nested facet等),不少場景下,使用嵌套文檔的複雜度在於索引階段對關聯關係的組織拼裝。
方案四:使用 ES6.X+ 父子關係 Join 作關聯
適用場景:1 對多量的場景。
舉例:1 個產品和供應商之間是1對N的關聯關係。
Join 類型:join 數據類型是一個特殊字段,用於在同一索引的文檔中建立父/子關係。關係部分定義文檔中的一組可能關係,每一個關係是父名稱和子名稱。
當使用父子文檔時,使用has_child 或者has_parent作父子關聯查詢。
注意:方案三&方案四選型必須考慮性能問題。文檔應該儘可能經過合理的建模來提高檢索效率。
Join 類型應該儘可能避免使用。nested 類型檢索使得檢索效率慢幾倍,父子Join 類型檢索會使得檢索效率慢幾百倍。
儘可能將業務轉化爲沒有關聯關係的文檔形式,在文檔建模處多下功夫,以提高檢索效率。
若是能重來,我會如何設計 Elasticsearch 系統?
來自累計近千萬實戰項目設計的思考。
坑1: 數據清洗必定發生在寫入 es 以前!而不是請求數據後處理,拿勢必會下降請求速度和效率。
坑2:高亮不要重複造輪子,用原生就能夠。
坑3:讓 es 作他擅長的事,檢索+不復雜的聚合,不然數據量+複雜的業務邏輯大會有性能問題。
坑4:設計的工做必須不要省!快了就是慢了,不然無休止的因設計缺陷引起的 bug 會增長團隊的戳敗感!
坑5:在給定時間的前提下,永遠不會有完美的設計,必須相對合理的設計+重構結合,纔會有相對靠譜的系統。
坑6:SSD 能提高性能,但若是系統業務邏輯很是負責,換了 SSD 未必達到預期。
坑7:因爲 Elasticsearch 不支持事務 ACID 特性,數據庫做爲實時數據補充,對於實時數據要求嚴格的場景,必須同時採起雙寫或者同步的方式。這樣,一旦實時數據出現不一致,能夠經過數據庫進行同步遞增更新。
原文連接:銘毅天下